Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
329ccdf
feat: :sparkles: add support for EIP-7623
manuelmauro Aug 29, 2025
5684cbd
feat: :sparkles: add post_execution method to gasometer
manuelmauro Aug 29, 2025
01340c7
fix: :bug: check for invalid gas limit
manuelmauro Aug 29, 2025
df052f9
feat: :sparkles: add gas tracker
manuelmauro Sep 1, 2025
bdb8019
style: :art: fmt
manuelmauro Sep 1, 2025
e129161
fix: :bug: add check for recorded transaction
manuelmauro Sep 1, 2025
78a2d7c
Revert "fix: :bug: add check for recorded transaction"
manuelmauro Sep 1, 2025
4b649e1
fix: :bug: use saturanting arithmetic in execution gas calculation
manuelmauro Sep 1, 2025
b423907
refactor: :fire: remove apply_eip_7623_adjustment function
manuelmauro Sep 1, 2025
29a1a20
fix: :bug: fix computation of EIP-7623 cost
manuelmauro Sep 2, 2025
66c8018
fix: :bug: fix execution cost computation
manuelmauro Sep 2, 2025
058ae7b
refactor: :recycle: move cost computation inside GasTracker
manuelmauro Sep 2, 2025
92ce5e7
perf: :zap: add caching to GasTracker
manuelmauro Sep 2, 2025
f62bb08
test: :white_check_mark: add proper testing
manuelmauro Sep 2, 2025
021f491
refactor: :rotating_light: clippy
manuelmauro Sep 3, 2025
76b381b
refactor: :recycle: rename GasTracker to GasMetrics and move it to a …
manuelmauro Sep 3, 2025
f7d545a
refactor: :recycle: rename execution_cost to non_intrinsic_cost
manuelmauro Sep 3, 2025
27f7f3a
refactor: :recycle: rename tracker to metrics
manuelmauro Sep 3, 2025
8b3aa09
docs: :memo: improve docs
manuelmauro Sep 3, 2025
fa7707c
fix: :bug: add instrinsic gas cost metric
manuelmauro Sep 3, 2025
78abe6d
docs: :memo: document intrinsic_cost_metrics method
manuelmauro Sep 3, 2025
c425c74
refactor: :recycle: add init method to GasMetrics
manuelmauro Sep 3, 2025
2a0af91
refactor: :recycle: reduce code duplication
manuelmauro Sep 3, 2025
f279eff
refactor: :recycle: improve if condition
manuelmauro Sep 3, 2025
70ae6e1
fix: :bug: add contract creation component to intrinsic gas calculation
manuelmauro Sep 3, 2025
d4a11c6
fix: :bug: remove duplicated init_code_cost component
manuelmauro Sep 3, 2025
416841c
refactor: :fire: remove intrinsic cost metrics
manuelmauro Sep 3, 2025
3847259
test: :white_check_mark: test invalid transaction with gas lower than…
manuelmauro Sep 3, 2025
dc1f984
refactor: :recycle: wildly simplify the solution
manuelmauro Sep 5, 2025
9212a7a
revert: :rewind: remove refactoring
manuelmauro Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions gasometer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl<'config> Gasometer<'config> {
inner: Ok(Inner {
memory_gas: 0,
used_gas: 0,
floor_gas: 0,
refunded_gas: 0,
config,
}),
Expand Down Expand Up @@ -241,7 +242,7 @@ impl<'config> Gasometer<'config> {

/// Record transaction cost.
pub fn record_transaction(&mut self, cost: TransactionCost) -> Result<(), ExitError> {
let gas_cost = match cost {
let (gas_cost, floor_gas) = match cost {
TransactionCost::Call {
zero_data_len,
non_zero_data_len,
Expand All @@ -257,6 +258,8 @@ impl<'config> Gasometer<'config> {
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key
+ authorization_list_len as u64 * self.config.gas_per_empty_account_cost;

let floor = floor_gas(zero_data_len, non_zero_data_len, self.config);

log_gas!(
self,
"Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, authorization_list_len: {}]",
Expand All @@ -269,7 +272,7 @@ impl<'config> Gasometer<'config> {
authorization_list_len
);

cost
(cost, floor)
}
TransactionCost::Create {
zero_data_len,
Expand All @@ -289,6 +292,8 @@ impl<'config> Gasometer<'config> {
cost += initcode_cost;
}

let floor = floor_gas(zero_data_len, non_zero_data_len, self.config);

log_gas!(
self,
"Record Create {} [gas_transaction_create: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, initcode_cost: {}, authorization_list_len: {}]",
Expand All @@ -301,7 +306,8 @@ impl<'config> Gasometer<'config> {
initcode_cost,
authorization_list_len
);
cost

(cost, floor)
}
};

Expand All @@ -310,12 +316,25 @@ impl<'config> Gasometer<'config> {
snapshot: self.snapshot(),
});

// EIP-7623 validation: Check if gas limit meets floor requirement
if self.config.has_eip_7623 {
// Any transaction with a gas limit below:
// 21000 + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
if self.gas() < floor_gas {
self.inner = Err(ExitError::OutOfGas);
return Err(ExitError::OutOfGas);
}
}

// Any transaction with a gas limit below its intrinsic gas cost
// is considered invalid.
if self.gas() < gas_cost {
self.inner = Err(ExitError::OutOfGas);
return Err(ExitError::OutOfGas);
}

self.inner_mut()?.used_gas += gas_cost;
self.inner_mut()?.floor_gas += floor_gas;
Ok(())
}

Expand All @@ -328,6 +347,19 @@ impl<'config> Gasometer<'config> {
refunded_gas: inner.refunded_gas,
})
}

/// Apply post-execution adjustments for various EIPs.
/// This method handles gas adjustments that need to be calculated after transaction execution.
pub fn post_execution(&mut self) -> Result<(), ExitError> {
// Apply EIP-7623 adjustments
if self.config.has_eip_7623 {
let inner = self.inner_mut()?;

inner.used_gas = max(inner.used_gas, inner.floor_gas);
}

Ok(())
}
}

/// Calculate the call transaction cost.
Expand Down Expand Up @@ -385,6 +417,25 @@ pub fn init_code_cost(data: &[u8]) -> u64 {
2 * ((data.len() as u64 + 31) / 32)
}

pub fn floor_gas(
zero_bytes_in_calldata: usize,
non_zero_bytes_in_calldata: usize,
config: &Config,
) -> u64 {
config
.gas_transaction_call
.saturating_add(
config
.gas_calldata_zero_floor
.saturating_mul(zero_bytes_in_calldata as u64),
)
.saturating_add(
config
.gas_calldata_non_zero_floor
.saturating_mul(non_zero_bytes_in_calldata as u64),
)
}

/// Counts the number of addresses and storage keys in the access list
fn count_access_list(access_list: &[(H160, Vec<H256>)]) -> (usize, usize) {
let access_list_address_len = access_list.len();
Expand Down Expand Up @@ -799,6 +850,7 @@ pub fn dynamic_opcode_cost<H: Handler>(
struct Inner<'config> {
memory_gas: u64,
used_gas: u64,
floor_gas: u64,
refunded_gas: i64,
config: &'config Config,
}
Expand Down
39 changes: 39 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ pub struct Config {
pub gas_auth_base_cost: u64,
/// EIP-7702: Gas cost per empty account in authorization list
pub gas_per_empty_account_cost: u64,
/// EIP-7623: Gas cost floor per zero byte
pub gas_calldata_zero_floor: u64,
/// EIP-7623: Gas cost floor per non-zero byte
pub gas_calldata_non_zero_floor: u64,
/// Whether to throw out of gas error when
/// CALL/CALLCODE/DELEGATECALL requires more than maximum amount
/// of gas.
Expand Down Expand Up @@ -300,6 +304,8 @@ pub struct Config {
pub has_eip_6780: bool,
/// Has EIP-7702. See [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md)
pub has_eip_7702: bool,
/// Has EIP-7623. See [EIP-7623](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7623.md)
pub has_eip_7623: bool,
}

impl Config {
Expand Down Expand Up @@ -360,6 +366,9 @@ impl Config {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand Down Expand Up @@ -420,6 +429,9 @@ impl Config {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand Down Expand Up @@ -470,6 +482,9 @@ impl Config {
has_eip_7702,
gas_auth_base_cost,
gas_per_empty_account_cost,
has_eip_7623,
gas_calldata_zero_floor,
gas_calldata_non_zero_floor,
} = inputs;

// See https://eips.ethereum.org/EIPS/eip-2929
Expand Down Expand Up @@ -539,6 +554,9 @@ impl Config {
has_eip_7702,
gas_auth_base_cost,
gas_per_empty_account_cost,
has_eip_7623,
gas_calldata_zero_floor,
gas_calldata_non_zero_floor,
}
}
}
Expand All @@ -561,6 +579,9 @@ struct DerivedConfigInputs {
has_eip_7702: bool,
gas_auth_base_cost: u64,
gas_per_empty_account_cost: u64,
has_eip_7623: bool,
gas_calldata_zero_floor: u64,
gas_calldata_non_zero_floor: u64,
Copy link
Contributor

@RomarQ RomarQ Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having many fields in DerivedConfigInputs, we could have a single field, which is specify to eip-7623

pub struct EIP7623Config {
 enabled: bool,
 standard_token_cost: u64,
 total_cost_floor_per_token: u64
}
Suggested change
gas_calldata_non_zero_floor: u64,
eip_7623: EIP7623Config

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we already 2 constants that are used for this.

gas_transaction_zero_data: 4,
gas_transaction_non_zero_data: 16,

Copy link
Contributor

@RomarQ RomarQ Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eip-7623 changes gas_transaction_non_zero_data from 16 to 40 and gas_transaction_zero_data from 4 to 10.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to keep both, because the 10/40 is applied only in the case the 4/16 plus execution costs do not reach the floor.

tx.gasUsed = (
    21000
    +
    max(
        STANDARD_TOKEN_COST * tokens_in_calldata
        + execution_gas_used
        + isContractCreation * (32000 + INITCODE_WORD_COST * words(calldata)),
        TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
    )
)

The STANDARD_TOKEN_COST stays 4, while we add TOTAL_COST_FLOOR_PER_TOKEN which is 10.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having many fields in DerivedConfigInputs, we could have a single field, which is specify to eip-7623

I like this, but maybe better to evaluate it as a refactoring in another PR?

}

impl DerivedConfigInputs {
Expand All @@ -581,6 +602,9 @@ impl DerivedConfigInputs {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand All @@ -601,6 +625,9 @@ impl DerivedConfigInputs {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand All @@ -621,6 +648,9 @@ impl DerivedConfigInputs {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand All @@ -642,6 +672,9 @@ impl DerivedConfigInputs {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand All @@ -663,6 +696,9 @@ impl DerivedConfigInputs {
has_eip_7702: false,
gas_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_eip_7623: false,
gas_calldata_zero_floor: 0,
gas_calldata_non_zero_floor: 0,
}
}

Expand All @@ -687,6 +723,9 @@ impl DerivedConfigInputs {
gas_auth_base_cost: 12500,
// PER_EMPTY_ACCOUNT_COST from EIP-7702
gas_per_empty_account_cost: 25000,
has_eip_7623: true,
gas_calldata_zero_floor: 10,
gas_calldata_non_zero_floor: 40,
}
}
}
39 changes: 30 additions & 9 deletions src/executor/stack/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,22 +496,29 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
}
}

match self.create_inner(
let result = match self.create_inner(
caller,
CreateScheme::Legacy { caller },
value,
init_code,
Some(gas_limit),
false,
) {
Capture::Exit((s, _, v)) => emit_exit!(s, v),
Capture::Exit((s, _, v)) => (s, v),
Capture::Trap(rt) => {
let mut cs = Vec::with_capacity(DEFAULT_CALL_STACK_CAPACITY);
cs.push(rt.0);
let (s, _, v) = self.execute_with_call_stack(&mut cs, None);
emit_exit!(s, v)
(s, v)
}
};

// Apply post-execution adjustments
if let Err(e) = self.state.metadata_mut().gasometer.post_execution() {
return emit_exit!(e.into(), Vec::new());
}

emit_exit!(result.0, result.1)
}

/// Execute a `CREATE2` transaction.
Expand Down Expand Up @@ -560,7 +567,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
}
}

match self.create_inner(
let result = match self.create_inner(
caller,
CreateScheme::Create2 {
caller,
Expand All @@ -572,14 +579,21 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
Some(gas_limit),
false,
) {
Capture::Exit((s, _, v)) => emit_exit!(s, v),
Capture::Exit((s, _, v)) => (s, v),
Capture::Trap(rt) => {
let mut cs = Vec::with_capacity(DEFAULT_CALL_STACK_CAPACITY);
cs.push(rt.0);
let (s, _, v) = self.execute_with_call_stack(&mut cs, None);
emit_exit!(s, v)
(s, v)
}
};

// Apply post-execution adjustments
if let Err(e) = self.state.metadata_mut().gasometer.post_execution() {
return emit_exit!(e.into(), Vec::new());
}

emit_exit!(result.0, result.1)
}

/// Execute a `CREATE` transaction that force the contract address
Expand Down Expand Up @@ -714,7 +728,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
apparent_value: value,
};

match self.call_inner(
let result = match self.call_inner(
address,
Some(Transfer {
source: caller,
Expand All @@ -728,14 +742,21 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
false,
context,
) {
Capture::Exit((s, v)) => emit_exit!(s, v),
Capture::Exit((s, v)) => (s, v),
Capture::Trap(rt) => {
let mut cs = Vec::with_capacity(DEFAULT_CALL_STACK_CAPACITY);
cs.push(rt.0);
let (s, _, v) = self.execute_with_call_stack(&mut cs, Some(caller));
emit_exit!(s, v)
(s, v)
}
};

// Apply post-execution adjustments
if let Err(e) = self.state.metadata_mut().gasometer.post_execution() {
return emit_exit!(e.into(), Vec::new());
}

emit_exit!(result.0, result.1)
}

/// Get used gas for the current executor, given the price.
Expand Down
Loading