From 329ccdfaff9ee5147b3f6bff0bd5de377d588cbe Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 29 Aug 2025 16:52:43 +0300 Subject: [PATCH 01/30] feat: :sparkles: add support for EIP-7623 --- gasometer/src/lib.rs | 33 +++++++-- runtime/src/lib.rs | 26 +++++++ tests/eip7623.rs | 159 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 tests/eip7623.rs diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 0c2ceb78..85318979 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -249,11 +249,21 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { + let mut calldata_cost = zero_data_len as u64 + * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; + + // EIP-7623: Apply floor cost for calldata if enabled + if self.config.has_eip_7623 { + let tokens_in_calldata = (zero_data_len + non_zero_data_len) as u64; + let floor_cost = tokens_in_calldata * self.config.gas_calldata_floor_per_token; + calldata_cost = calldata_cost.max(floor_cost); + } + #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call - + zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data - + access_list_address_len as u64 * self.config.gas_access_list_address + + calldata_cost + access_list_address_len as u64 + * self.config.gas_access_list_address + 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; @@ -279,12 +289,23 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { + let mut calldata_cost = zero_data_len as u64 + * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; + + // EIP-7623: Apply floor cost for calldata if enabled + if self.config.has_eip_7623 { + let tokens_in_calldata = (zero_data_len + non_zero_data_len) as u64; + let floor_cost = tokens_in_calldata * self.config.gas_calldata_floor_per_token; + calldata_cost = calldata_cost.max(floor_cost); + } + let mut cost = self.config.gas_transaction_create - + zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data - + access_list_address_len as u64 * self.config.gas_access_list_address + + calldata_cost + access_list_address_len as u64 + * self.config.gas_access_list_address + 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; + if self.config.max_initcode_size.is_some() { cost += initcode_cost; } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 0d5e3cab..5f5f5650 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -248,6 +248,8 @@ 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 calldata token (zero or non-zero byte) + pub gas_calldata_floor_per_token: u64, /// Whether to throw out of gas error when /// CALL/CALLCODE/DELEGATECALL requires more than maximum amount /// of gas. @@ -300,6 +302,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 { @@ -360,6 +364,8 @@ impl Config { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -420,6 +426,8 @@ impl Config { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -470,6 +478,8 @@ impl Config { has_eip_7702, gas_auth_base_cost, gas_per_empty_account_cost, + has_eip_7623, + gas_calldata_floor_per_token, } = inputs; // See https://eips.ethereum.org/EIPS/eip-2929 @@ -539,6 +549,8 @@ impl Config { has_eip_7702, gas_auth_base_cost, gas_per_empty_account_cost, + has_eip_7623, + gas_calldata_floor_per_token, } } } @@ -561,6 +573,8 @@ struct DerivedConfigInputs { has_eip_7702: bool, gas_auth_base_cost: u64, gas_per_empty_account_cost: u64, + has_eip_7623: bool, + gas_calldata_floor_per_token: u64, } impl DerivedConfigInputs { @@ -581,6 +595,8 @@ impl DerivedConfigInputs { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -601,6 +617,8 @@ impl DerivedConfigInputs { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -621,6 +639,8 @@ impl DerivedConfigInputs { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -642,6 +662,8 @@ impl DerivedConfigInputs { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -663,6 +685,8 @@ impl DerivedConfigInputs { has_eip_7702: false, gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, + has_eip_7623: false, + gas_calldata_floor_per_token: 0, } } @@ -687,6 +711,8 @@ 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_floor_per_token: 10, } } } diff --git a/tests/eip7623.rs b/tests/eip7623.rs new file mode 100644 index 00000000..90b94be2 --- /dev/null +++ b/tests/eip7623.rs @@ -0,0 +1,159 @@ +#[cfg(test)] +mod eip7623_tests { + use evm::Config; + use evm_gasometer::{call_transaction_cost, create_transaction_cost, Gasometer}; + use primitive_types::{H160, H256}; + + #[test] + fn test_eip7623_call_transaction_standard_wins() { + // Test case where standard cost is higher than floor cost + let config = Config::pectra(); + + // Small calldata: 10 bytes (5 zero, 5 non-zero) + let data = vec![0, 0, 0, 0, 0, 1, 2, 3, 4, 5]; + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(1_000_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard cost: 21000 + 5*4 + 5*16 = 21000 + 20 + 80 = 21100 + // Floor cost: 10 * 10 = 100 + // Max(21100, 100) = 21100 + assert_eq!(gasometer.total_used_gas(), 21100); + } + + #[test] + fn test_eip7623_call_transaction_floor_wins() { + // Test case where floor cost is higher than standard cost + let config = Config::pectra(); + + // Large calldata: 3000 bytes of zeros + let data = vec![0; 3000]; + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard calldata cost: 3000*4 = 12000 + // Floor calldata cost: 3000 * 10 = 30000 + // Calldata cost: Max(12000, 30000) = 30000 + // Total: 21000 + 30000 = 51000 + assert_eq!(gasometer.total_used_gas(), 51000); + } + + #[test] + fn test_eip7623_call_transaction_floor_wins_large() { + // Test case where floor cost definitely wins + let config = Config::pectra(); + + // Very large calldata: 10000 bytes of zeros + let data = vec![0; 10000]; + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(200_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard calldata cost: 10000*4 = 40000 + // Floor calldata cost: 10000 * 10 = 100000 + // Calldata cost: Max(40000, 100000) = 100000 + // Total: 21000 + 100000 = 121000 + assert_eq!(gasometer.total_used_gas(), 121000); + } + + #[test] + fn test_eip7623_create_transaction() { + // Test create transaction with EIP-7623 + let config = Config::pectra(); + + // Initcode with mixed data + let data = vec![0x60, 0x80, 0x60, 0x40, 0x52, 0, 0, 0, 0, 0]; // 5 non-zero, 5 zero + let cost = create_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard cost: 53000 + 5*4 + 5*16 = 53000 + 20 + 80 = 53100 + // Floor cost: 10 * 10 = 100 + // Max(53100, 100) = 53100 + assert_eq!(gasometer.total_used_gas(), 53102); + } + + #[test] + fn test_eip7623_disabled() { + // Test that when EIP-7623 is disabled, only standard cost applies + let config = Config::london(); + + // Large calldata that would trigger floor cost if enabled + let data = vec![0; 10000]; + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard cost only: 21000 + 10000*4 = 61000 + assert_eq!(gasometer.total_used_gas(), 61000); + } + + #[test] + fn test_eip7623_mixed_calldata() { + // Test with mixed zero and non-zero bytes + let config = Config::pectra(); + + // 1000 zeros and 1000 non-zeros + let mut data = vec![0; 1000]; + data.extend(vec![0xFF; 1000]); + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard cost: 21000 + 1000*4 + 1000*16 = 21000 + 4000 + 16000 = 41000 + // Floor cost: 2000 * 10 = 20000 + // Max(41000, 20000) = 41000 + assert_eq!(gasometer.total_used_gas(), 41000); + } + + #[test] + fn test_eip7623_with_access_list() { + // Test transaction with access list + let config = Config::pectra(); + + // Small calldata with access list + let data = vec![1, 2, 3, 4, 5]; + let access_list = vec![ + (H160::zero(), vec![H256::zero(), H256::from_low_u64_be(1)]), + (H160::from_low_u64_be(1), vec![H256::zero()]), + ]; + let cost = call_transaction_cost(&data, &access_list, &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard cost: 21000 + 0*4 + 5*16 + 2*2400 + 3*1900 = 21000 + 80 + 4800 + 5700 = 31580 + // Floor cost: 5 * 10 = 50 + // Max(31580, 50) = 31580 + assert_eq!(gasometer.total_used_gas(), 31580); + } + + #[test] + fn test_eip7623_exact_boundary() { + // Test the exact boundary where floor cost equals standard cost + let config = Config::pectra(); + + // For zero bytes: standard = 4, floor = 10 + // At equilibrium for calldata: n*4 = n*10 + // This never happens since 10 > 4 + // So let's test a case where they're close + let data = vec![0; 3500]; + let cost = call_transaction_cost(&data, &[], &[]); + + let mut gasometer = Gasometer::new(100_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Standard calldata cost: 3500*4 = 14000 + // Floor calldata cost: 3500 * 10 = 35000 + // Calldata cost: Max(14000, 35000) = 35000 + // Total: 21000 + 35000 = 56000 + assert_eq!(gasometer.total_used_gas(), 56000); + } +} From 5684cbdc5e12a4bce7901574b630b81ee661c029 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 29 Aug 2025 17:49:46 +0300 Subject: [PATCH 02/30] feat: :sparkles: add post_execution method to gasometer --- gasometer/src/lib.rs | 87 +++++++++++++++++++++++++++++----- src/executor/stack/executor.rs | 39 +++++++++++---- tests/eip7623.rs | 67 +++++++++++++++++++------- 3 files changed, 154 insertions(+), 39 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 85318979..1e537870 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -83,6 +83,8 @@ impl<'config> Gasometer<'config> { used_gas: 0, refunded_gas: 0, config, + zero_data_len: 0, + non_zero_data_len: 0, }), } } @@ -249,15 +251,13 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { - let mut calldata_cost = zero_data_len as u64 - * self.config.gas_transaction_zero_data + let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - // EIP-7623: Apply floor cost for calldata if enabled + // Store calldata info for EIP-7623 adjustment after execution if self.config.has_eip_7623 { - let tokens_in_calldata = (zero_data_len + non_zero_data_len) as u64; - let floor_cost = tokens_in_calldata * self.config.gas_calldata_floor_per_token; - calldata_cost = calldata_cost.max(floor_cost); + self.inner_mut()?.zero_data_len = zero_data_len; + self.inner_mut()?.non_zero_data_len = non_zero_data_len; } #[deny(clippy::let_and_return)] @@ -289,15 +289,13 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { - let mut calldata_cost = zero_data_len as u64 - * self.config.gas_transaction_zero_data + let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - // EIP-7623: Apply floor cost for calldata if enabled + // Store calldata info for EIP-7623 adjustment after execution if self.config.has_eip_7623 { - let tokens_in_calldata = (zero_data_len + non_zero_data_len) as u64; - let floor_cost = tokens_in_calldata * self.config.gas_calldata_floor_per_token; - calldata_cost = calldata_cost.max(floor_cost); + self.inner_mut()?.zero_data_len = zero_data_len; + self.inner_mut()?.non_zero_data_len = non_zero_data_len; } let mut cost = self.config.gas_transaction_create @@ -349,6 +347,67 @@ 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 { + self.apply_eip_7623_adjustment()?; + } + + Ok(()) + } + + /// Apply EIP-7623 adjustment after execution. + fn apply_eip_7623_adjustment(&mut self) -> Result<(), ExitError> { + // Get values from config before borrowing inner + let gas_transaction_call = self.config.gas_transaction_call; + let gas_transaction_create = self.config.gas_transaction_create; + let gas_transaction_zero_data = self.config.gas_transaction_zero_data; + let gas_transaction_non_zero_data = self.config.gas_transaction_non_zero_data; + let gas_calldata_floor_per_token = self.config.gas_calldata_floor_per_token; + + let inner = self.inner_mut()?; + + // Skip if no calldata was recorded + if inner.zero_data_len == 0 && inner.non_zero_data_len == 0 { + return Ok(()); + } + + // Calculate standard calldata cost + let standard_calldata_cost = inner.zero_data_len as u64 * gas_transaction_zero_data + + inner.non_zero_data_len as u64 * gas_transaction_non_zero_data; + + // Calculate execution gas (excluding the initial 21000 and calldata costs) + let base_cost = if gas_transaction_call == 21000 { + 21000 + } else { + gas_transaction_create // For create transactions + }; + + // execution_gas = total_used_gas - base_cost - standard_calldata_cost + let execution_gas = inner + .used_gas + .saturating_sub(base_cost) + .saturating_sub(standard_calldata_cost); + + // Calculate floor cost + let tokens_in_calldata = (inner.zero_data_len + inner.non_zero_data_len) as u64; + let floor_cost = tokens_in_calldata * gas_calldata_floor_per_token; + + // Apply EIP-7623 formula + let standard_total = standard_calldata_cost + execution_gas; + if floor_cost > standard_total { + // Add the difference to used_gas + // Note: We don't check gas_limit here since we're post-execution + // and the transaction has already completed successfully + let adjustment = floor_cost - standard_total; + inner.used_gas += adjustment; + } + + Ok(()) + } } /// Calculate the call transaction cost. @@ -822,6 +881,10 @@ struct Inner<'config> { used_gas: u64, refunded_gas: i64, config: &'config Config, + /// EIP-7623: Track zero data length for floor cost calculation + zero_data_len: usize, + /// EIP-7623: Track non-zero data length for floor cost calculation + non_zero_data_len: usize, } impl Inner<'_> { diff --git a/src/executor/stack/executor.rs b/src/executor/stack/executor.rs index f0d74e91..366dc9aa 100644 --- a/src/executor/stack/executor.rs +++ b/src/executor/stack/executor.rs @@ -496,7 +496,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> } } - match self.create_inner( + let result = match self.create_inner( caller, CreateScheme::Legacy { caller }, value, @@ -504,14 +504,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 `CREATE2` transaction. @@ -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, @@ -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 @@ -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, @@ -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. diff --git a/tests/eip7623.rs b/tests/eip7623.rs index 90b94be2..3618593e 100644 --- a/tests/eip7623.rs +++ b/tests/eip7623.rs @@ -33,11 +33,21 @@ mod eip7623_tests { let mut gasometer = Gasometer::new(100_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - - // Standard calldata cost: 3000*4 = 12000 - // Floor calldata cost: 3000 * 10 = 30000 - // Calldata cost: Max(12000, 30000) = 30000 - // Total: 21000 + 30000 = 51000 + + // Initially only standard cost is applied: 21000 + 3000*4 = 33000 + assert_eq!(gasometer.total_used_gas(), 33000); + + // Simulate some execution gas (e.g., 5000) + assert!(gasometer.record_cost(5000).is_ok()); + + // Apply post-execution adjustments + assert!(gasometer.post_execution().is_ok()); + + // After adjustment: + // Standard: 3000*4 + 5000 = 17000 + // Floor: 3000 * 10 = 30000 + // Adjustment: 30000 - 17000 = 13000 + // Total: 33000 + 5000 + 13000 = 51000 assert_eq!(gasometer.total_used_gas(), 51000); } @@ -52,11 +62,21 @@ mod eip7623_tests { let mut gasometer = Gasometer::new(200_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - - // Standard calldata cost: 10000*4 = 40000 - // Floor calldata cost: 10000 * 10 = 100000 - // Calldata cost: Max(40000, 100000) = 100000 - // Total: 21000 + 100000 = 121000 + + // Initially only standard cost: 21000 + 10000*4 = 61000 + assert_eq!(gasometer.total_used_gas(), 61000); + + // Simulate some execution gas + assert!(gasometer.record_cost(5000).is_ok()); + + // Apply EIP-7623 adjustment + assert!(gasometer.post_execution().is_ok()); + + // After adjustment: + // Standard: 10000*4 + 5000 = 45000 + // Floor: 10000 * 10 = 100000 + // Adjustment: 100000 - 45000 = 55000 + // Total: 61000 + 5000 + 55000 = 121000 assert_eq!(gasometer.total_used_gas(), 121000); } @@ -141,19 +161,30 @@ mod eip7623_tests { let config = Config::pectra(); // For zero bytes: standard = 4, floor = 10 - // At equilibrium for calldata: n*4 = n*10 - // This never happens since 10 > 4 - // So let's test a case where they're close + // With execution gas, we can reach equilibrium let data = vec![0; 3500]; let cost = call_transaction_cost(&data, &[], &[]); let mut gasometer = Gasometer::new(100_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - - // Standard calldata cost: 3500*4 = 14000 - // Floor calldata cost: 3500 * 10 = 35000 - // Calldata cost: Max(14000, 35000) = 35000 - // Total: 21000 + 35000 = 56000 + + // Initially: 21000 + 3500*4 = 35000 + assert_eq!(gasometer.total_used_gas(), 35000); + + // Add execution gas to reach equilibrium + // Floor: 3500 * 10 = 35000 + // Standard calldata: 3500 * 4 = 14000 + // Need execution gas: 35000 - 14000 = 21000 + assert!(gasometer.record_cost(21000).is_ok()); + + // Apply EIP-7623 adjustment + assert!(gasometer.post_execution().is_ok()); + + // After adjustment: + // Standard: 14000 + 21000 = 35000 + // Floor: 35000 + // No adjustment needed (they're equal) + // Total: 35000 + 21000 = 56000 assert_eq!(gasometer.total_used_gas(), 56000); } } From 01340c7ec09e5bc86a1dab92287cea363c2682a7 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 29 Aug 2025 17:50:20 +0300 Subject: [PATCH 03/30] fix: :bug: check for invalid gas limit --- gasometer/src/lib.rs | 37 +++++++++++++++----- tests/eip7623.rs | 80 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 1e537870..7b81da94 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -329,6 +329,28 @@ impl<'config> Gasometer<'config> { snapshot: self.snapshot(), }); + // EIP-7623 validation: Check if gas limit meets floor requirement + if self.config.has_eip_7623 { + let tokens_in_calldata = match cost { + TransactionCost::Call { zero_data_len, non_zero_data_len, .. } => { + zero_data_len as u64 + non_zero_data_len as u64 * 4 + } + TransactionCost::Create { zero_data_len, non_zero_data_len, .. } => { + zero_data_len as u64 + non_zero_data_len as u64 * 4 + } + }; + + if tokens_in_calldata > 0 { + let min_floor_cost = 21000 + tokens_in_calldata * self.config.gas_calldata_floor_per_token; + let required_gas_limit = gas_cost.max(min_floor_cost); + + if self.gas_limit < required_gas_limit { + self.inner = Err(ExitError::OutOfGas); + return Err(ExitError::OutOfGas); + } + } + } + if self.gas() < gas_cost { self.inner = Err(ExitError::OutOfGas); return Err(ExitError::OutOfGas); @@ -392,18 +414,17 @@ impl<'config> Gasometer<'config> { .saturating_sub(base_cost) .saturating_sub(standard_calldata_cost); - // Calculate floor cost - let tokens_in_calldata = (inner.zero_data_len + inner.non_zero_data_len) as u64; + // Calculate floor cost using EIP-7623 token formula + let tokens_in_calldata = inner.zero_data_len as u64 + inner.non_zero_data_len as u64 * 4; let floor_cost = tokens_in_calldata * gas_calldata_floor_per_token; - // Apply EIP-7623 formula + // Apply EIP-7623 formula: 21000 + max(standard_calldata + execution, floor_calldata) let standard_total = standard_calldata_cost + execution_gas; if floor_cost > standard_total { - // Add the difference to used_gas - // Note: We don't check gas_limit here since we're post-execution - // and the transaction has already completed successfully - let adjustment = floor_cost - standard_total; - inner.used_gas += adjustment; + // Calculate what the final cost should be + let target_total = base_cost + floor_cost; + // Adjust used_gas to match the target + inner.used_gas = target_total; } Ok(()) diff --git a/tests/eip7623.rs b/tests/eip7623.rs index 3618593e..5cf1cb8d 100644 --- a/tests/eip7623.rs +++ b/tests/eip7623.rs @@ -17,8 +17,9 @@ mod eip7623_tests { assert!(gasometer.record_transaction(cost).is_ok()); // Standard cost: 21000 + 5*4 + 5*16 = 21000 + 20 + 80 = 21100 - // Floor cost: 10 * 10 = 100 - // Max(21100, 100) = 21100 + // Tokens: 5 + 5*4 = 25 + // Floor cost: 25 * 10 = 250 + // Max(21100, 250) = 21100 assert_eq!(gasometer.total_used_gas(), 21100); } @@ -93,8 +94,9 @@ mod eip7623_tests { assert!(gasometer.record_transaction(cost).is_ok()); // Standard cost: 53000 + 5*4 + 5*16 = 53000 + 20 + 80 = 53100 - // Floor cost: 10 * 10 = 100 - // Max(53100, 100) = 53100 + // Tokens: 5 + 5*4 = 25 + // Floor cost: 25 * 10 = 250 + // Max(53100, 250) = 53100 assert_eq!(gasometer.total_used_gas(), 53102); } @@ -124,13 +126,21 @@ mod eip7623_tests { data.extend(vec![0xFF; 1000]); let cost = call_transaction_cost(&data, &[], &[]); - let mut gasometer = Gasometer::new(100_000, &config); + let mut gasometer = Gasometer::new(80_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); // Standard cost: 21000 + 1000*4 + 1000*16 = 21000 + 4000 + 16000 = 41000 - // Floor cost: 2000 * 10 = 20000 - // Max(41000, 20000) = 41000 + // Tokens: 1000 + 1000*4 = 5000 + // Floor cost: 5000 * 10 = 50000 + // Since floor cost (50000) > standard cost (41000), floor wins + // But this happens in post_execution, so initial cost is still 41000 assert_eq!(gasometer.total_used_gas(), 41000); + + // Apply post-execution adjustment + assert!(gasometer.post_execution().is_ok()); + + // After adjustment: 21000 + floor cost of 50000 = 71000 + assert_eq!(gasometer.total_used_gas(), 71000); } #[test] @@ -150,8 +160,9 @@ mod eip7623_tests { assert!(gasometer.record_transaction(cost).is_ok()); // Standard cost: 21000 + 0*4 + 5*16 + 2*2400 + 3*1900 = 21000 + 80 + 4800 + 5700 = 31580 - // Floor cost: 5 * 10 = 50 - // Max(31580, 50) = 31580 + // Tokens: 0 + 5*4 = 20 + // Floor cost: 20 * 10 = 200 + // Max(31580, 200) = 31580 assert_eq!(gasometer.total_used_gas(), 31580); } @@ -187,4 +198,55 @@ mod eip7623_tests { // Total: 35000 + 21000 = 56000 assert_eq!(gasometer.total_used_gas(), 56000); } + + #[test] + fn test_eip7623_insufficient_gas_limit() { + // Test that transactions with insufficient gas limit are rejected + let config = Config::pectra(); + + // Large calldata that requires high floor cost + let data = vec![0; 5000]; // 5000 tokens + let cost = call_transaction_cost(&data, &[], &[]); + + // Tokens: 5000 + 0*4 = 5000 + // Floor cost: 21000 + 5000 * 10 = 71000 + // Set gas limit below floor requirement + let mut gasometer = Gasometer::new(70_000, &config); + + // Should fail with OutOfGas due to insufficient gas limit + assert!(gasometer.record_transaction(cost).is_err()); + } + + #[test] + fn test_eip7623_sufficient_gas_limit_with_floor() { + // Test that transactions with sufficient gas limit but requiring floor adjustment work + let config = Config::pectra(); + + // Large calldata requiring floor cost + let data = vec![0; 5000]; // 5000 tokens + let cost = call_transaction_cost(&data, &[], &[]); + + // Tokens: 5000 + 0*4 = 5000 + // Floor cost: 21000 + 5000 * 10 = 71000 + // Set gas limit above floor requirement + let mut gasometer = Gasometer::new(80_000, &config); + assert!(gasometer.record_transaction(cost).is_ok()); + + // Initially: 21000 + 5000*4 = 41000 + assert_eq!(gasometer.total_used_gas(), 41000); + + // Add some execution gas + assert!(gasometer.record_cost(5000).is_ok()); + + // Apply EIP-7623 adjustment + assert!(gasometer.post_execution().is_ok()); + + // After adjustment: + // Standard: 5000*4 + 5000 = 25000 + // Tokens: 5000 + 0*4 = 5000 + // Floor: 5000 * 10 = 50000 + // Adjustment: 50000 - 25000 = 25000 + // Total: 41000 + 5000 + 25000 = 71000 + assert_eq!(gasometer.total_used_gas(), 71000); + } } From df052f9c16fe498f1d5586105c6d633f7b1411f8 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 17:40:26 +0300 Subject: [PATCH 04/30] feat: :sparkles: add gas tracker --- gasometer/src/lib.rs | 148 ++++++++++++++++++++++--------------------- runtime/src/lib.rs | 39 ++++++++---- 2 files changed, 103 insertions(+), 84 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 7b81da94..71835705 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -83,8 +83,7 @@ impl<'config> Gasometer<'config> { used_gas: 0, refunded_gas: 0, config, - zero_data_len: 0, - non_zero_data_len: 0, + tracker: GasTracker::new(), }), } } @@ -254,11 +253,8 @@ impl<'config> Gasometer<'config> { let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - // Store calldata info for EIP-7623 adjustment after execution - if self.config.has_eip_7623 { - self.inner_mut()?.zero_data_len = zero_data_len; - self.inner_mut()?.non_zero_data_len = non_zero_data_len; - } + self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; + self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -292,11 +288,9 @@ impl<'config> Gasometer<'config> { let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - // Store calldata info for EIP-7623 adjustment after execution - if self.config.has_eip_7623 { - self.inner_mut()?.zero_data_len = zero_data_len; - self.inner_mut()?.non_zero_data_len = non_zero_data_len; - } + self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; + self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; + self.inner_mut()?.tracker.is_contract_creation = true; let mut cost = self.config.gas_transaction_create + calldata_cost + access_list_address_len as u64 @@ -331,23 +325,16 @@ impl<'config> Gasometer<'config> { // EIP-7623 validation: Check if gas limit meets floor requirement if self.config.has_eip_7623 { - let tokens_in_calldata = match cost { - TransactionCost::Call { zero_data_len, non_zero_data_len, .. } => { - zero_data_len as u64 + non_zero_data_len as u64 * 4 - } - TransactionCost::Create { zero_data_len, non_zero_data_len, .. } => { - zero_data_len as u64 + non_zero_data_len as u64 * 4 - } - }; - - if tokens_in_calldata > 0 { - let min_floor_cost = 21000 + tokens_in_calldata * self.config.gas_calldata_floor_per_token; - let required_gas_limit = gas_cost.max(min_floor_cost); - - if self.gas_limit < required_gas_limit { - self.inner = Err(ExitError::OutOfGas); - return Err(ExitError::OutOfGas); - } + // Any transaction with a gas limit below: + // 21000 + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + // or below its intrinsic gas cost (take the maximum of these two calculations) + // is considered invalid. + // TODO check intrinsic gas cost + if self.gas_limit + < self.config().gas_transaction_call + self.inner_mut()?.floor_calldata_cost() + { + self.inner = Err(ExitError::OutOfGas); + return Err(ExitError::OutOfGas); } } @@ -383,48 +370,18 @@ impl<'config> Gasometer<'config> { /// Apply EIP-7623 adjustment after execution. fn apply_eip_7623_adjustment(&mut self) -> Result<(), ExitError> { - // Get values from config before borrowing inner - let gas_transaction_call = self.config.gas_transaction_call; - let gas_transaction_create = self.config.gas_transaction_create; - let gas_transaction_zero_data = self.config.gas_transaction_zero_data; - let gas_transaction_non_zero_data = self.config.gas_transaction_non_zero_data; - let gas_calldata_floor_per_token = self.config.gas_calldata_floor_per_token; - let inner = self.inner_mut()?; - // Skip if no calldata was recorded - if inner.zero_data_len == 0 && inner.non_zero_data_len == 0 { - return Ok(()); - } - - // Calculate standard calldata cost - let standard_calldata_cost = inner.zero_data_len as u64 * gas_transaction_zero_data - + inner.non_zero_data_len as u64 * gas_transaction_non_zero_data; + // Apply EIP-7623 + let standard_cost = inner.standard_calldata_cost(); + let floor_cost = inner.floor_calldata_cost(); + let execution_cost = inner.execution_cost(); + let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); - // Calculate execution gas (excluding the initial 21000 and calldata costs) - let base_cost = if gas_transaction_call == 21000 { - 21000 - } else { - gas_transaction_create // For create transactions - }; - - // execution_gas = total_used_gas - base_cost - standard_calldata_cost - let execution_gas = inner - .used_gas - .saturating_sub(base_cost) - .saturating_sub(standard_calldata_cost); - - // Calculate floor cost using EIP-7623 token formula - let tokens_in_calldata = inner.zero_data_len as u64 + inner.non_zero_data_len as u64 * 4; - let floor_cost = tokens_in_calldata * gas_calldata_floor_per_token; - - // Apply EIP-7623 formula: 21000 + max(standard_calldata + execution, floor_calldata) - let standard_total = standard_calldata_cost + execution_gas; - if floor_cost > standard_total { - // Calculate what the final cost should be - let target_total = base_cost + floor_cost; + if floor_cost > standard_cost { // Adjust used_gas to match the target - inner.used_gas = target_total; + inner.used_gas -= standard_cost + execution_cost; + inner.used_gas += eip_7623_cost; } Ok(()) @@ -895,6 +852,24 @@ pub fn dynamic_opcode_cost( Ok((gas_cost, storage_target, memory_cost)) } +/// Tracks gas parameters for a Gasometer instance. +#[derive(Clone, Debug)] +struct GasTracker { + zero_bytes_in_calldata: usize, + non_zero_bytes_in_calldata: usize, + is_contract_creation: bool, +} + +impl GasTracker { + fn new() -> Self { + Self { + zero_bytes_in_calldata: 0, + non_zero_bytes_in_calldata: 0, + is_contract_creation: false, + } + } +} + /// Holds the gas consumption for a Gasometer instance. #[derive(Clone, Debug)] struct Inner<'config> { @@ -902,10 +877,7 @@ struct Inner<'config> { used_gas: u64, refunded_gas: i64, config: &'config Config, - /// EIP-7623: Track zero data length for floor cost calculation - zero_data_len: usize, - /// EIP-7623: Track non-zero data length for floor cost calculation - non_zero_data_len: usize, + tracker: GasTracker, } impl Inner<'_> { @@ -1061,6 +1033,40 @@ impl Inner<'_> { _ => 0, } } + + fn standard_calldata_cost(&self) -> u64 { + (self.config.gas_transaction_zero_data * (self.tracker.zero_bytes_in_calldata as u64)) + + (self.config.gas_transaction_non_zero_data + * (self.tracker.non_zero_bytes_in_calldata as u64)) + } + + fn floor_calldata_cost(&self) -> u64 { + (self.config.gas_calldata_zero_floor * (self.tracker.zero_bytes_in_calldata as u64)) + + (self.config.gas_calldata_non_zero_floor + * (self.tracker.non_zero_bytes_in_calldata as u64)) + } + + fn base_cost(&self) -> u64 { + if self.tracker.is_contract_creation { + self.config.gas_transaction_call as u64 + } else { + self.config.gas_transaction_create as u64 + } + } + + fn init_code_cost(&self) -> u64 { + 2 * (((self.tracker.zero_bytes_in_calldata as u64 + + self.tracker.non_zero_bytes_in_calldata as u64) + + 31) / 32) + } + + fn execution_cost(&self) -> u64 { + if self.tracker.is_contract_creation { + self.used_gas - self.base_cost() - self.standard_calldata_cost() + } else { + self.used_gas - self.base_cost() - self.init_code_cost() - self.standard_calldata_cost() + } + } } /// Gas cost. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5f5f5650..4ed0a124 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -248,8 +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 calldata token (zero or non-zero byte) - pub gas_calldata_floor_per_token: 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. @@ -365,7 +367,8 @@ impl Config { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -427,7 +430,8 @@ impl Config { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -479,7 +483,8 @@ impl Config { gas_auth_base_cost, gas_per_empty_account_cost, has_eip_7623, - gas_calldata_floor_per_token, + gas_calldata_zero_floor, + gas_calldata_non_zero_floor, } = inputs; // See https://eips.ethereum.org/EIPS/eip-2929 @@ -550,7 +555,8 @@ impl Config { gas_auth_base_cost, gas_per_empty_account_cost, has_eip_7623, - gas_calldata_floor_per_token, + gas_calldata_zero_floor, + gas_calldata_non_zero_floor, } } } @@ -574,7 +580,8 @@ struct DerivedConfigInputs { gas_auth_base_cost: u64, gas_per_empty_account_cost: u64, has_eip_7623: bool, - gas_calldata_floor_per_token: u64, + gas_calldata_zero_floor: u64, + gas_calldata_non_zero_floor: u64, } impl DerivedConfigInputs { @@ -596,7 +603,8 @@ impl DerivedConfigInputs { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -618,7 +626,8 @@ impl DerivedConfigInputs { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -640,7 +649,8 @@ impl DerivedConfigInputs { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -663,7 +673,8 @@ impl DerivedConfigInputs { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -686,7 +697,8 @@ impl DerivedConfigInputs { gas_auth_base_cost: 0, gas_per_empty_account_cost: 0, has_eip_7623: false, - gas_calldata_floor_per_token: 0, + gas_calldata_zero_floor: 0, + gas_calldata_non_zero_floor: 0, } } @@ -712,7 +724,8 @@ impl DerivedConfigInputs { // PER_EMPTY_ACCOUNT_COST from EIP-7702 gas_per_empty_account_cost: 25000, has_eip_7623: true, - gas_calldata_floor_per_token: 10, + gas_calldata_zero_floor: 10, + gas_calldata_non_zero_floor: 40, } } } From bdb80198237909f0fbcf25b512b85623e73b9f4f Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 17:40:43 +0300 Subject: [PATCH 05/30] style: :art: fmt --- tests/eip7623.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/eip7623.rs b/tests/eip7623.rs index 5cf1cb8d..ef2e39ae 100644 --- a/tests/eip7623.rs +++ b/tests/eip7623.rs @@ -34,13 +34,13 @@ mod eip7623_tests { let mut gasometer = Gasometer::new(100_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - + // Initially only standard cost is applied: 21000 + 3000*4 = 33000 assert_eq!(gasometer.total_used_gas(), 33000); - + // Simulate some execution gas (e.g., 5000) assert!(gasometer.record_cost(5000).is_ok()); - + // Apply post-execution adjustments assert!(gasometer.post_execution().is_ok()); @@ -63,13 +63,13 @@ mod eip7623_tests { let mut gasometer = Gasometer::new(200_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - + // Initially only standard cost: 21000 + 10000*4 = 61000 assert_eq!(gasometer.total_used_gas(), 61000); - + // Simulate some execution gas assert!(gasometer.record_cost(5000).is_ok()); - + // Apply EIP-7623 adjustment assert!(gasometer.post_execution().is_ok()); @@ -135,10 +135,10 @@ mod eip7623_tests { // Since floor cost (50000) > standard cost (41000), floor wins // But this happens in post_execution, so initial cost is still 41000 assert_eq!(gasometer.total_used_gas(), 41000); - + // Apply post-execution adjustment assert!(gasometer.post_execution().is_ok()); - + // After adjustment: 21000 + floor cost of 50000 = 71000 assert_eq!(gasometer.total_used_gas(), 71000); } @@ -178,16 +178,16 @@ mod eip7623_tests { let mut gasometer = Gasometer::new(100_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - + // Initially: 21000 + 3500*4 = 35000 assert_eq!(gasometer.total_used_gas(), 35000); - + // Add execution gas to reach equilibrium // Floor: 3500 * 10 = 35000 // Standard calldata: 3500 * 4 = 14000 // Need execution gas: 35000 - 14000 = 21000 assert!(gasometer.record_cost(21000).is_ok()); - + // Apply EIP-7623 adjustment assert!(gasometer.post_execution().is_ok()); @@ -212,7 +212,7 @@ mod eip7623_tests { // Floor cost: 21000 + 5000 * 10 = 71000 // Set gas limit below floor requirement let mut gasometer = Gasometer::new(70_000, &config); - + // Should fail with OutOfGas due to insufficient gas limit assert!(gasometer.record_transaction(cost).is_err()); } @@ -231,13 +231,13 @@ mod eip7623_tests { // Set gas limit above floor requirement let mut gasometer = Gasometer::new(80_000, &config); assert!(gasometer.record_transaction(cost).is_ok()); - + // Initially: 21000 + 5000*4 = 41000 assert_eq!(gasometer.total_used_gas(), 41000); - + // Add some execution gas assert!(gasometer.record_cost(5000).is_ok()); - + // Apply EIP-7623 adjustment assert!(gasometer.post_execution().is_ok()); From e129161c802c28615a38f45497486ac4fe377ac2 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 18:06:49 +0300 Subject: [PATCH 06/30] fix: :bug: add check for recorded transaction --- gasometer/src/lib.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 71835705..ad39378a 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -255,6 +255,7 @@ impl<'config> Gasometer<'config> { self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; + self.inner_mut()?.tracker.has_recorded_transaction = true; #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -290,6 +291,7 @@ impl<'config> Gasometer<'config> { self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; + self.inner_mut()?.tracker.has_recorded_transaction = true; self.inner_mut()?.tracker.is_contract_creation = true; let mut cost = self.config.gas_transaction_create @@ -375,7 +377,7 @@ impl<'config> Gasometer<'config> { // Apply EIP-7623 let standard_cost = inner.standard_calldata_cost(); let floor_cost = inner.floor_calldata_cost(); - let execution_cost = inner.execution_cost(); + let execution_cost = inner.execution_cost()?; let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); if floor_cost > standard_cost { @@ -857,6 +859,7 @@ pub fn dynamic_opcode_cost( struct GasTracker { zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, + has_recorded_transaction: bool, is_contract_creation: bool, } @@ -865,6 +868,7 @@ impl GasTracker { Self { zero_bytes_in_calldata: 0, non_zero_bytes_in_calldata: 0, + has_recorded_transaction: false, is_contract_creation: false, } } @@ -1060,11 +1064,19 @@ impl Inner<'_> { + 31) / 32) } - fn execution_cost(&self) -> u64 { + fn execution_cost(&self) -> Result { + if !self.tracker.has_recorded_transaction { + // TODO: Implement proper error handling + return Err(ExitError::OutOfGas); + } + if self.tracker.is_contract_creation { - self.used_gas - self.base_cost() - self.standard_calldata_cost() + Ok(self.used_gas - self.base_cost() - self.standard_calldata_cost()) } else { - self.used_gas - self.base_cost() - self.init_code_cost() - self.standard_calldata_cost() + Ok(self.used_gas + - self.base_cost() + - self.init_code_cost() + - self.standard_calldata_cost()) } } } From 78a2d7c571c1d1aaa328c12918acee3514c3a293 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 18:09:11 +0300 Subject: [PATCH 07/30] Revert "fix: :bug: add check for recorded transaction" This reverts commit e129161c802c28615a38f45497486ac4fe377ac2. --- gasometer/src/lib.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index ad39378a..71835705 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -255,7 +255,6 @@ impl<'config> Gasometer<'config> { self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; - self.inner_mut()?.tracker.has_recorded_transaction = true; #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -291,7 +290,6 @@ impl<'config> Gasometer<'config> { self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; - self.inner_mut()?.tracker.has_recorded_transaction = true; self.inner_mut()?.tracker.is_contract_creation = true; let mut cost = self.config.gas_transaction_create @@ -377,7 +375,7 @@ impl<'config> Gasometer<'config> { // Apply EIP-7623 let standard_cost = inner.standard_calldata_cost(); let floor_cost = inner.floor_calldata_cost(); - let execution_cost = inner.execution_cost()?; + let execution_cost = inner.execution_cost(); let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); if floor_cost > standard_cost { @@ -859,7 +857,6 @@ pub fn dynamic_opcode_cost( struct GasTracker { zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, - has_recorded_transaction: bool, is_contract_creation: bool, } @@ -868,7 +865,6 @@ impl GasTracker { Self { zero_bytes_in_calldata: 0, non_zero_bytes_in_calldata: 0, - has_recorded_transaction: false, is_contract_creation: false, } } @@ -1064,19 +1060,11 @@ impl Inner<'_> { + 31) / 32) } - fn execution_cost(&self) -> Result { - if !self.tracker.has_recorded_transaction { - // TODO: Implement proper error handling - return Err(ExitError::OutOfGas); - } - + fn execution_cost(&self) -> u64 { if self.tracker.is_contract_creation { - Ok(self.used_gas - self.base_cost() - self.standard_calldata_cost()) + self.used_gas - self.base_cost() - self.standard_calldata_cost() } else { - Ok(self.used_gas - - self.base_cost() - - self.init_code_cost() - - self.standard_calldata_cost()) + self.used_gas - self.base_cost() - self.init_code_cost() - self.standard_calldata_cost() } } } From 4b649e12deb2f0b56e48511cb285a256baa5984c Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 18:10:41 +0300 Subject: [PATCH 08/30] fix: :bug: use saturanting arithmetic in execution gas calculation --- gasometer/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 71835705..5b87dccf 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -1062,9 +1062,14 @@ impl Inner<'_> { fn execution_cost(&self) -> u64 { if self.tracker.is_contract_creation { - self.used_gas - self.base_cost() - self.standard_calldata_cost() + self.used_gas + .saturating_sub(self.base_cost()) + .saturating_sub(self.standard_calldata_cost()) } else { - self.used_gas - self.base_cost() - self.init_code_cost() - self.standard_calldata_cost() + self.used_gas + .saturating_sub(self.base_cost()) + .saturating_sub(self.init_code_cost()) + .saturating_sub(self.standard_calldata_cost()) } } } From b423907a9f38bac995ab3a71d62d1acbf3e22b0a Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 1 Sep 2025 18:15:15 +0300 Subject: [PATCH 09/30] refactor: :fire: remove apply_eip_7623_adjustment function --- gasometer/src/lib.rs | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 5b87dccf..d5055c2c 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -362,26 +362,17 @@ impl<'config> Gasometer<'config> { pub fn post_execution(&mut self) -> Result<(), ExitError> { // Apply EIP-7623 adjustments if self.config.has_eip_7623 { - self.apply_eip_7623_adjustment()?; - } - - Ok(()) - } - - /// Apply EIP-7623 adjustment after execution. - fn apply_eip_7623_adjustment(&mut self) -> Result<(), ExitError> { - let inner = self.inner_mut()?; - - // Apply EIP-7623 - let standard_cost = inner.standard_calldata_cost(); - let floor_cost = inner.floor_calldata_cost(); - let execution_cost = inner.execution_cost(); - let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); - - if floor_cost > standard_cost { - // Adjust used_gas to match the target - inner.used_gas -= standard_cost + execution_cost; - inner.used_gas += eip_7623_cost; + let inner = self.inner_mut()?; + let standard_cost = inner.standard_calldata_cost(); + let floor_cost = inner.floor_calldata_cost(); + let execution_cost = inner.execution_cost(); + let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); + + if floor_cost > standard_cost { + // Adjust used_gas to match the target + inner.used_gas -= standard_cost + execution_cost; + inner.used_gas += eip_7623_cost; + } } Ok(()) From 29a1a20e9103c416aa1f642b86a7bf4001de1adb Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Tue, 2 Sep 2025 10:49:23 +0300 Subject: [PATCH 10/30] fix: :bug: fix computation of EIP-7623 cost --- gasometer/src/lib.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index d5055c2c..1f89b727 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -363,16 +363,16 @@ impl<'config> Gasometer<'config> { // Apply EIP-7623 adjustments if self.config.has_eip_7623 { let inner = self.inner_mut()?; - let standard_cost = inner.standard_calldata_cost(); - let floor_cost = inner.floor_calldata_cost(); + let standard_calldata_cost = inner.standard_calldata_cost(); + let floor_calldata_cost = inner.floor_calldata_cost(); + let contract_creation_cost = inner.contract_creation_cost(); let execution_cost = inner.execution_cost(); - let eip_7623_cost = max(standard_cost + execution_cost, floor_cost); + let cost = standard_calldata_cost + execution_cost + contract_creation_cost; + let eip_7623_cost = max(cost, floor_calldata_cost); - if floor_cost > standard_cost { - // Adjust used_gas to match the target - inner.used_gas -= standard_cost + execution_cost; - inner.used_gas += eip_7623_cost; - } + // Adjust used_gas to match the target + inner.used_gas -= cost; + inner.used_gas += eip_7623_cost; } Ok(()) @@ -1051,6 +1051,15 @@ impl Inner<'_> { + 31) / 32) } + fn contract_creation_cost(&self) -> u64 { + if self.tracker.is_contract_creation { + return (self.config.gas_transaction_create - self.config.gas_transaction_call) + + self.init_code_cost(); + } + + 0 + } + fn execution_cost(&self) -> u64 { if self.tracker.is_contract_creation { self.used_gas From 66c8018a2e0f8cd83a9778c7bd8e4a5add1559f7 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Tue, 2 Sep 2025 10:57:05 +0300 Subject: [PATCH 11/30] fix: :bug: fix execution cost computation --- gasometer/src/lib.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 1f89b727..424a81d5 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -1046,9 +1046,14 @@ impl Inner<'_> { } fn init_code_cost(&self) -> u64 { - 2 * (((self.tracker.zero_bytes_in_calldata as u64 - + self.tracker.non_zero_bytes_in_calldata as u64) - + 31) / 32) + if self.tracker.is_contract_creation { + return 2 + * (((self.tracker.zero_bytes_in_calldata as u64 + + self.tracker.non_zero_bytes_in_calldata as u64) + + 31) / 32); + } + + 0 } fn contract_creation_cost(&self) -> u64 { @@ -1061,16 +1066,10 @@ impl Inner<'_> { } fn execution_cost(&self) -> u64 { - if self.tracker.is_contract_creation { - self.used_gas - .saturating_sub(self.base_cost()) - .saturating_sub(self.standard_calldata_cost()) - } else { - self.used_gas - .saturating_sub(self.base_cost()) - .saturating_sub(self.init_code_cost()) - .saturating_sub(self.standard_calldata_cost()) - } + self.used_gas + .saturating_sub(self.base_cost()) + .saturating_sub(self.init_code_cost()) + .saturating_sub(self.standard_calldata_cost()) } } From 058ae7b5b073a34e830c42c3355b60f42290b307 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Tue, 2 Sep 2025 11:15:01 +0300 Subject: [PATCH 12/30] refactor: :recycle: move cost computation inside GasTracker --- gasometer/src/lib.rs | 89 +++++++++++++++++++--------------- src/executor/stack/executor.rs | 2 +- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 424a81d5..3735312c 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -415,7 +415,7 @@ pub fn create_transaction_cost( // Per EIP-7702: Initially charge PER_EMPTY_ACCOUNT_COST for all authorizations // Non-empty accounts will be refunded later when we have access to account state let authorization_list_len = authorization_list.len(); - let initcode_cost = init_code_cost(data); + let initcode_cost = init_code_cost(data.len() as u64); TransactionCost::Create { zero_data_len, @@ -427,11 +427,11 @@ pub fn create_transaction_cost( } } -pub fn init_code_cost(data: &[u8]) -> u64 { +pub fn init_code_cost(code_length: u64) -> u64 { // As per EIP-3860: // > We define initcode_cost(initcode) to equal INITCODE_WORD_COST * ceil(len(initcode) / 32). // where INITCODE_WORD_COST is 2. - 2 * ((data.len() as u64 + 31) / 32) + 2 * ((code_length + 31) / 32) } /// Counts the number of addresses and storage keys in the access list @@ -859,6 +859,50 @@ impl GasTracker { is_contract_creation: false, } } + + fn standard_calldata_cost(&self, config: &Config) -> u64 { + (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) + + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)) + } + + fn floor_calldata_cost(&self, config: &Config) -> u64 { + (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) + + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)) + } + + fn base_cost(&self, config: &Config) -> u64 { + if self.is_contract_creation { + config.gas_transaction_call as u64 + } else { + config.gas_transaction_create as u64 + } + } + + fn init_code_cost(&self) -> u64 { + if self.is_contract_creation { + return init_code_cost( + self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, + ); + } + + 0 + } + + fn contract_creation_cost(&self, config: &Config) -> u64 { + if self.is_contract_creation { + return (config.gas_transaction_create - config.gas_transaction_call) + + self.init_code_cost(); + } + + 0 + } + + fn execution_cost(&self, used_gas: u64, config: &Config) -> u64 { + used_gas + .saturating_sub(self.base_cost(config)) + .saturating_sub(self.init_code_cost()) + .saturating_sub(self.standard_calldata_cost(config)) + } } /// Holds the gas consumption for a Gasometer instance. @@ -1026,50 +1070,19 @@ impl Inner<'_> { } fn standard_calldata_cost(&self) -> u64 { - (self.config.gas_transaction_zero_data * (self.tracker.zero_bytes_in_calldata as u64)) - + (self.config.gas_transaction_non_zero_data - * (self.tracker.non_zero_bytes_in_calldata as u64)) + self.tracker.standard_calldata_cost(self.config) } fn floor_calldata_cost(&self) -> u64 { - (self.config.gas_calldata_zero_floor * (self.tracker.zero_bytes_in_calldata as u64)) - + (self.config.gas_calldata_non_zero_floor - * (self.tracker.non_zero_bytes_in_calldata as u64)) - } - - fn base_cost(&self) -> u64 { - if self.tracker.is_contract_creation { - self.config.gas_transaction_call as u64 - } else { - self.config.gas_transaction_create as u64 - } - } - - fn init_code_cost(&self) -> u64 { - if self.tracker.is_contract_creation { - return 2 - * (((self.tracker.zero_bytes_in_calldata as u64 - + self.tracker.non_zero_bytes_in_calldata as u64) - + 31) / 32); - } - - 0 + self.tracker.floor_calldata_cost(self.config) } fn contract_creation_cost(&self) -> u64 { - if self.tracker.is_contract_creation { - return (self.config.gas_transaction_create - self.config.gas_transaction_call) - + self.init_code_cost(); - } - - 0 + self.tracker.contract_creation_cost(self.config) } fn execution_cost(&self) -> u64 { - self.used_gas - .saturating_sub(self.base_cost()) - .saturating_sub(self.init_code_cost()) - .saturating_sub(self.standard_calldata_cost()) + self.tracker.execution_cost(self.used_gas, self.config) } } diff --git a/src/executor/stack/executor.rs b/src/executor/stack/executor.rs index 366dc9aa..a541644d 100644 --- a/src/executor/stack/executor.rs +++ b/src/executor/stack/executor.rs @@ -453,7 +453,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> .state .metadata_mut() .gasometer - .record_cost(gasometer::init_code_cost(init_code)); + .record_cost(gasometer::init_code_cost(init_code.len() as u64)); } Ok(()) } From 92ce5e77d78d80995b55b0c9c47a9a3ce5c643f6 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Tue, 2 Sep 2025 11:25:32 +0300 Subject: [PATCH 13/30] perf: :zap: add caching to GasTracker --- gasometer/src/lib.rs | 125 ++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 32 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 3735312c..33dd8960 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -253,8 +253,9 @@ impl<'config> Gasometer<'config> { let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; - self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; + self.inner_mut()? + .tracker + .set_calldata_params(zero_data_len, non_zero_data_len); #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -288,9 +289,10 @@ impl<'config> Gasometer<'config> { let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - self.inner_mut()?.tracker.zero_bytes_in_calldata = zero_data_len; - self.inner_mut()?.tracker.non_zero_bytes_in_calldata = non_zero_data_len; - self.inner_mut()?.tracker.is_contract_creation = true; + self.inner_mut()? + .tracker + .set_calldata_params(zero_data_len, non_zero_data_len); + self.inner_mut()?.tracker.set_contract_creation(true); let mut cost = self.config.gas_transaction_create + calldata_cost + access_list_address_len as u64 @@ -849,6 +851,12 @@ struct GasTracker { zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, is_contract_creation: bool, + // Cached values + cached_standard_calldata_cost: Option, + cached_floor_calldata_cost: Option, + cached_base_cost: Option, + cached_init_code_cost: Option, + cached_contract_creation_cost: Option, } impl GasTracker { @@ -857,47 +865,100 @@ impl GasTracker { zero_bytes_in_calldata: 0, non_zero_bytes_in_calldata: 0, is_contract_creation: false, + cached_standard_calldata_cost: None, + cached_floor_calldata_cost: None, + cached_base_cost: None, + cached_init_code_cost: None, + cached_contract_creation_cost: None, } } - fn standard_calldata_cost(&self, config: &Config) -> u64 { - (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) - + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)) + fn invalidate_cache(&mut self) { + self.cached_standard_calldata_cost = None; + self.cached_floor_calldata_cost = None; + self.cached_base_cost = None; + self.cached_init_code_cost = None; + self.cached_contract_creation_cost = None; } - fn floor_calldata_cost(&self, config: &Config) -> u64 { - (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) - + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)) + fn set_calldata_params(&mut self, zero_bytes: usize, non_zero_bytes: usize) { + self.zero_bytes_in_calldata = zero_bytes; + self.non_zero_bytes_in_calldata = non_zero_bytes; + self.invalidate_cache(); } - fn base_cost(&self, config: &Config) -> u64 { - if self.is_contract_creation { - config.gas_transaction_call as u64 - } else { - config.gas_transaction_create as u64 + fn set_contract_creation(&mut self, is_creation: bool) { + self.is_contract_creation = is_creation; + self.invalidate_cache(); + } + + fn standard_calldata_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_standard_calldata_cost { + return cached; + } + + let cost = (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) + + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)); + self.cached_standard_calldata_cost = Some(cost); + cost + } + + fn floor_calldata_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_floor_calldata_cost { + return cached; } + + let cost = (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) + + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)); + self.cached_floor_calldata_cost = Some(cost); + cost } - fn init_code_cost(&self) -> u64 { - if self.is_contract_creation { - return init_code_cost( - self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, - ); + fn base_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_base_cost { + return cached; + } + + let cost = if self.is_contract_creation { + config.gas_transaction_create as u64 + } else { + config.gas_transaction_call as u64 + }; + self.cached_base_cost = Some(cost); + cost + } + + fn init_code_cost(&mut self) -> u64 { + if let Some(cached) = self.cached_init_code_cost { + return cached; } - 0 + let cost = if self.is_contract_creation { + init_code_cost( + self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, + ) + } else { + 0 + }; + self.cached_init_code_cost = Some(cost); + cost } - fn contract_creation_cost(&self, config: &Config) -> u64 { - if self.is_contract_creation { - return (config.gas_transaction_create - config.gas_transaction_call) - + self.init_code_cost(); + fn contract_creation_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_contract_creation_cost { + return cached; } - 0 + let cost = if self.is_contract_creation { + (config.gas_transaction_create - config.gas_transaction_call) + self.init_code_cost() + } else { + 0 + }; + self.cached_contract_creation_cost = Some(cost); + cost } - fn execution_cost(&self, used_gas: u64, config: &Config) -> u64 { + fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { used_gas .saturating_sub(self.base_cost(config)) .saturating_sub(self.init_code_cost()) @@ -1069,19 +1130,19 @@ impl Inner<'_> { } } - fn standard_calldata_cost(&self) -> u64 { + fn standard_calldata_cost(&mut self) -> u64 { self.tracker.standard_calldata_cost(self.config) } - fn floor_calldata_cost(&self) -> u64 { + fn floor_calldata_cost(&mut self) -> u64 { self.tracker.floor_calldata_cost(self.config) } - fn contract_creation_cost(&self) -> u64 { + fn contract_creation_cost(&mut self) -> u64 { self.tracker.contract_creation_cost(self.config) } - fn execution_cost(&self) -> u64 { + fn execution_cost(&mut self) -> u64 { self.tracker.execution_cost(self.used_gas, self.config) } } From f62bb0815b7b7e757ed4b41cf7ee0d609a910ee5 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Tue, 2 Sep 2025 18:59:36 +0300 Subject: [PATCH 14/30] test: :white_check_mark: add proper testing --- tests/eip7623.rs | 1280 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 1105 insertions(+), 175 deletions(-) diff --git a/tests/eip7623.rs b/tests/eip7623.rs index ef2e39ae..3d66402b 100644 --- a/tests/eip7623.rs +++ b/tests/eip7623.rs @@ -1,252 +1,1182 @@ +use evm::{ + backend::MemoryBackend, + executor::stack::{MemoryStackState, StackExecutor, StackSubstateMetadata}, + gasometer::{call_transaction_cost, create_transaction_cost, Gasometer, TransactionCost}, + Config, ExitError, ExitReason, +}; +use primitive_types::{H160, U256}; +use std::collections::BTreeMap; + +// ============================================================================ +// Constants from EIP-7623 +// ============================================================================ + +const TOTAL_COST_FLOOR_PER_TOKEN: u64 = 10; +const INITCODE_WORD_COST: u64 = 2; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Calculate tokens in calldata as per EIP-7623 specification +fn calculate_tokens_in_calldata(zero_bytes: usize, non_zero_bytes: usize) -> u64 { + zero_bytes as u64 + (non_zero_bytes as u64 * 4) +} + +/// Create test configuration with EIP-7623 enabled +fn create_eip7623_config() -> Config { + let config = Config::pectra(); + assert!( + config.has_eip_7623, + "EIP-7623 must be enabled in Pectra config" + ); + assert_eq!( + config.gas_calldata_zero_floor, 10, + "Zero byte floor cost should be 10" + ); + assert_eq!( + config.gas_calldata_non_zero_floor, 40, + "Non-zero byte floor cost should be 40" + ); + config +} + +/// Create test configuration without EIP-7623 +fn create_pre_eip7623_config() -> Config { + let config = Config::cancun(); + assert!( + !config.has_eip_7623, + "EIP-7623 should not be enabled in Cancun" + ); + config +} + +/// Create a test vicinity +fn create_test_vicinity() -> evm::backend::MemoryVicinity { + evm::backend::MemoryVicinity { + gas_price: U256::from(1), + origin: H160::default(), + block_hashes: Vec::new(), + block_number: U256::zero(), + block_coinbase: H160::default(), + block_timestamp: U256::zero(), + block_difficulty: U256::zero(), + block_randomness: None, + block_gas_limit: U256::from(30_000_000), + block_base_fee_per_gas: U256::from(7), + chain_id: U256::from(1), + } +} + +/// Create test calldata with specified zero and non-zero byte counts +fn create_test_calldata(zero_bytes: usize, non_zero_bytes: usize) -> Vec { + let mut data = Vec::new(); + data.extend(vec![0u8; zero_bytes]); + data.extend(vec![0xffu8; non_zero_bytes]); + data +} + +// ============================================================================ +// Section 1: Basic Gas Cost Calculation Tests +// ============================================================================ + +#[cfg(test)] +mod basic_gas_cost_tests { + use super::*; + + #[test] + fn test_tokens_calculation() { + // Test the tokens_in_calldata calculation + assert_eq!(calculate_tokens_in_calldata(0, 0), 0); + assert_eq!(calculate_tokens_in_calldata(10, 0), 10); + assert_eq!(calculate_tokens_in_calldata(0, 10), 40); + assert_eq!(calculate_tokens_in_calldata(10, 10), 50); + assert_eq!(calculate_tokens_in_calldata(100, 100), 500); + } + + #[test] + fn test_floor_cost_calculation() { + let _config = create_eip7623_config(); + + // Test floor cost calculations + // Floor cost = TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + + // Empty calldata: 0 tokens * 10 = 0 + let tokens = calculate_tokens_in_calldata(0, 0); + assert_eq!(tokens * TOTAL_COST_FLOOR_PER_TOKEN, 0); + + // 10 zero bytes: 10 tokens * 10 = 100 + let tokens = calculate_tokens_in_calldata(10, 0); + assert_eq!(tokens * TOTAL_COST_FLOOR_PER_TOKEN, 100); + + // 10 non-zero bytes: 40 tokens * 10 = 400 + let tokens = calculate_tokens_in_calldata(0, 10); + assert_eq!(tokens * TOTAL_COST_FLOOR_PER_TOKEN, 400); + + // Mixed: 10 zero + 10 non-zero = 50 tokens * 10 = 500 + let tokens = calculate_tokens_in_calldata(10, 10); + assert_eq!(tokens * TOTAL_COST_FLOOR_PER_TOKEN, 500); + } + + #[test] + fn test_standard_cost_calculation() { + let config = create_eip7623_config(); + + // Standard cost = gas_transaction_zero_data * zero_bytes + gas_transaction_non_zero_data * non_zero_bytes + // For EIP-7623 config: zero = 4, non_zero = 16 + + // Empty calldata: 0 + assert_eq!( + 0 * config.gas_transaction_zero_data + 0 * config.gas_transaction_non_zero_data, + 0 + ); + + // 10 zero bytes: 10 * 4 = 40 + assert_eq!( + 10 * config.gas_transaction_zero_data + 0 * config.gas_transaction_non_zero_data, + 40 + ); + + // 10 non-zero bytes: 10 * 16 = 160 + assert_eq!( + 0 * config.gas_transaction_zero_data + 10 * config.gas_transaction_non_zero_data, + 160 + ); + + // Mixed: 10 * 4 + 10 * 16 = 200 + assert_eq!( + 10 * config.gas_transaction_zero_data + 10 * config.gas_transaction_non_zero_data, + 200 + ); + } + + #[test] + fn test_max_formula() { + // Test the max() formula from EIP-7623 + let _config = create_eip7623_config(); + + // Case 1: Standard cost is higher + let standard_cost = 1000u64; + let floor_cost = 500u64; + assert_eq!(std::cmp::max(standard_cost, floor_cost), 1000); + + // Case 2: Floor cost is higher + let standard_cost = 500u64; + let floor_cost = 1000u64; + assert_eq!(std::cmp::max(standard_cost, floor_cost), 1000); + + // Case 3: Equal costs + let standard_cost = 1000u64; + let floor_cost = 1000u64; + assert_eq!(std::cmp::max(standard_cost, floor_cost), 1000); + } +} + +// ============================================================================ +// Section 2: Transaction Cost Tests +// ============================================================================ + +#[cfg(test)] +mod transaction_cost_tests { + use super::*; + + #[test] + fn test_call_transaction_with_empty_calldata() { + let _config = create_eip7623_config(); + let data = vec![]; + let access_list = vec![]; + let authorization_list = vec![]; + + let cost = call_transaction_cost(&data, &access_list, &authorization_list); + + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + assert_eq!(zero_data_len, 0); + assert_eq!(non_zero_data_len, 0); + } else { + panic!("Expected Call transaction cost"); + } + } + + #[test] + fn test_call_transaction_with_zero_bytes() { + let _config = create_eip7623_config(); + let data = vec![0u8; 100]; + let access_list = vec![]; + let authorization_list = vec![]; + + let cost = call_transaction_cost(&data, &access_list, &authorization_list); + + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + assert_eq!(zero_data_len, 100); + assert_eq!(non_zero_data_len, 0); + } else { + panic!("Expected Call transaction cost"); + } + } + + #[test] + fn test_call_transaction_with_non_zero_bytes() { + let _config = create_eip7623_config(); + let data = vec![0xffu8; 100]; + let access_list = vec![]; + let authorization_list = vec![]; + + let cost = call_transaction_cost(&data, &access_list, &authorization_list); + + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + assert_eq!(zero_data_len, 0); + assert_eq!(non_zero_data_len, 100); + } else { + panic!("Expected Call transaction cost"); + } + } + + #[test] + fn test_call_transaction_with_mixed_bytes() { + let _config = create_eip7623_config(); + let mut data = vec![0u8; 50]; + data.extend(vec![0xffu8; 50]); + let access_list = vec![]; + let authorization_list = vec![]; + + let cost = call_transaction_cost(&data, &access_list, &authorization_list); + + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + assert_eq!(zero_data_len, 50); + assert_eq!(non_zero_data_len, 50); + } else { + panic!("Expected Call transaction cost"); + } + } + + #[test] + fn test_create_transaction_cost() { + let _config = create_eip7623_config(); + let data = create_test_calldata(10, 10); + let access_list = vec![]; + let authorization_list = vec![]; + + let cost = create_transaction_cost(&data, &access_list, &authorization_list); + + if let TransactionCost::Create { + zero_data_len, + non_zero_data_len, + initcode_cost, + .. + } = cost + { + assert_eq!(zero_data_len, 10); + assert_eq!(non_zero_data_len, 10); + // Initcode cost = INITCODE_WORD_COST * words(initcode) + // words = (len + 31) / 32 + let words = (data.len() + 31) / 32; + assert_eq!(initcode_cost, INITCODE_WORD_COST * words as u64); + } else { + panic!("Expected Create transaction cost"); + } + } +} + +// ============================================================================ +// Section 3: Gasometer Integration Tests +// ============================================================================ + +#[cfg(test)] +mod gasometer_tests { + use super::*; + + #[test] + fn test_gasometer_with_eip7623_enabled() { + let config = create_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // Record a simple call transaction + let data = create_test_calldata(10, 10); + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + let result = gasometer.record_transaction(cost); + assert!(result.is_ok(), "Should successfully record transaction"); + + // Verify gas consumption follows EIP-7623 rules + let used_gas = gasometer.total_used_gas(); + assert!(used_gas > 0, "Should consume gas"); + } + + #[test] + fn test_gasometer_with_insufficient_gas_limit() { + let config = create_eip7623_config(); + // Set gas limit below the floor requirement + let gas_limit = 21_000; // Just base cost, no room for calldata + let mut gasometer = Gasometer::new(gas_limit, &config); + + // Create calldata that requires floor cost + let data = create_test_calldata(0, 100); // 400 tokens * 10 = 4000 floor cost + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + let result = gasometer.record_transaction(cost); + assert!( + matches!(result, Err(ExitError::OutOfGas)), + "Should fail with OutOfGas" + ); + } + + #[test] + fn test_gasometer_comparison_with_and_without_eip7623() { + // Test with EIP-7623 disabled + let config_without = create_pre_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer_without = Gasometer::new(gas_limit, &config_without); + + let data = create_test_calldata(0, 1000); // Large calldata + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + gasometer_without.record_transaction(cost.clone()).unwrap(); + let used_without = gasometer_without.total_used_gas(); + + // Test with EIP-7623 enabled + let config_with = create_eip7623_config(); + let mut gasometer_with = Gasometer::new(gas_limit, &config_with); + + gasometer_with.record_transaction(cost).unwrap(); + gasometer_with.post_execution().unwrap(); + let used_with = gasometer_with.total_used_gas(); + + // With large calldata, EIP-7623 should charge more due to floor cost + assert!( + used_with > used_without, + "EIP-7623 should not reduce gas cost" + ); + } +} + +// ============================================================================ +// Section 4: Contract Creation Tests +// ============================================================================ + +#[cfg(test)] +mod contract_creation_tests { + use super::*; + + #[test] + fn test_contract_creation_with_initcode() { + let config = create_eip7623_config(); + let gas_limit = 500_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // Create initcode (contract bytecode) + let initcode = vec![0x60, 0x80, 0x60, 0x40, 0x52]; // Simple initcode + let cost = create_transaction_cost(&initcode, &vec![], &vec![]); + + let result = gasometer.record_transaction(cost); + assert!( + result.is_ok(), + "Should successfully record contract creation" + ); + + // Verify initcode cost is included + let used_gas = gasometer.total_used_gas(); + assert!( + used_gas >= config.gas_transaction_create, + "Should include base creation cost" + ); + } + + #[test] + fn test_contract_creation_floor_cost() { + let config = create_eip7623_config(); + let gas_limit = 1_000_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // Create large initcode that triggers floor cost + let initcode = vec![0xffu8; 10000]; // Large non-zero initcode + let cost = create_transaction_cost(&initcode, &vec![], &vec![]); + + if let TransactionCost::Create { + zero_data_len, + non_zero_data_len, + initcode_cost: _, + .. + } = cost + { + let tokens = calculate_tokens_in_calldata(zero_data_len, non_zero_data_len); + let floor_cost = tokens * TOTAL_COST_FLOOR_PER_TOKEN; + + // Record transaction and apply post-execution adjustments + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + + let used_gas = gasometer.total_used_gas(); + + // Gas should be at least the floor cost + assert!( + used_gas >= floor_cost + config.gas_transaction_call, + "Should apply floor cost for large initcode" + ); + } + } +} + +// ============================================================================ +// Section 5: Edge Cases and Boundary Tests +// ============================================================================ + #[cfg(test)] -mod eip7623_tests { - use evm::Config; - use evm_gasometer::{call_transaction_cost, create_transaction_cost, Gasometer}; - use primitive_types::{H160, H256}; +mod edge_case_tests { + use super::*; #[test] - fn test_eip7623_call_transaction_standard_wins() { - // Test case where standard cost is higher than floor cost - let config = Config::pectra(); + fn test_maximum_calldata_size() { + let _config = create_eip7623_config(); + // Maximum theoretical calldata that could fit in a block pre-EIP-7623 + // was about 1.79 MB, post-EIP-7623 it's reduced to ~0.72 MB - // Small calldata: 10 bytes (5 zero, 5 non-zero) - let data = vec![0, 0, 0, 0, 0, 1, 2, 3, 4, 5]; - let cost = call_transaction_cost(&data, &[], &[]); + // Test with 1 MB of calldata + let data = vec![0xffu8; 1_000_000]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); - let mut gasometer = Gasometer::new(1_000_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + let tokens = calculate_tokens_in_calldata(zero_data_len, non_zero_data_len); + let floor_cost = tokens * TOTAL_COST_FLOOR_PER_TOKEN; - // Standard cost: 21000 + 5*4 + 5*16 = 21000 + 20 + 80 = 21100 - // Tokens: 5 + 5*4 = 25 - // Floor cost: 25 * 10 = 250 - // Max(21100, 250) = 21100 - assert_eq!(gasometer.total_used_gas(), 21100); + // With 1MB of non-zero bytes: 1,000,000 * 4 = 4,000,000 tokens + // Floor cost: 4,000,000 * 10 = 40,000,000 gas + assert_eq!( + floor_cost, 40_000_000, + "Floor cost for 1MB should be 40M gas" + ); + } } #[test] - fn test_eip7623_call_transaction_floor_wins() { - // Test case where floor cost is higher than standard cost - let config = Config::pectra(); + fn test_zero_length_calldata() { + let config = create_eip7623_config(); + let gas_limit = 50_000; + let mut gasometer = Gasometer::new(gas_limit, &config); - // Large calldata: 3000 bytes of zeros - let data = vec![0; 3000]; - let cost = call_transaction_cost(&data, &[], &[]); + let data = vec![]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); - let mut gasometer = Gasometer::new(100_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + let result = gasometer.record_transaction(cost); + assert!(result.is_ok(), "Empty calldata should be valid"); - // Initially only standard cost is applied: 21000 + 3000*4 = 33000 - assert_eq!(gasometer.total_used_gas(), 33000); + let used_gas = gasometer.total_used_gas(); + assert_eq!( + used_gas, config.gas_transaction_call, + "Should only charge base cost for empty calldata" + ); + } + + #[test] + fn test_single_byte_calldata() { + let config = create_eip7623_config(); + let gas_limit = 50_000; - // Simulate some execution gas (e.g., 5000) - assert!(gasometer.record_cost(5000).is_ok()); + // Test with single zero byte + let mut gasometer = Gasometer::new(gas_limit, &config); + let data = vec![0x00]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + let zero_byte_gas = gasometer.total_used_gas(); - // Apply post-execution adjustments - assert!(gasometer.post_execution().is_ok()); + // Test with single non-zero byte + let mut gasometer = Gasometer::new(gas_limit, &config); + let data = vec![0xff]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + let non_zero_byte_gas = gasometer.total_used_gas(); - // After adjustment: - // Standard: 3000*4 + 5000 = 17000 - // Floor: 3000 * 10 = 30000 - // Adjustment: 30000 - 17000 = 13000 - // Total: 33000 + 5000 + 13000 = 51000 - assert_eq!(gasometer.total_used_gas(), 51000); + // Non-zero byte should cost more + assert!( + non_zero_byte_gas > zero_byte_gas, + "Non-zero byte should cost more than zero byte" + ); } #[test] - fn test_eip7623_call_transaction_floor_wins_large() { - // Test case where floor cost definitely wins - let config = Config::pectra(); + fn test_alternating_byte_pattern() { + let config = create_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // Create alternating pattern: 0x00, 0xff, 0x00, 0xff, ... + let mut data = Vec::new(); + for _ in 0..100 { + data.push(0x00); + data.push(0xff); + } + + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + if let TransactionCost::Call { + zero_data_len, + non_zero_data_len, + .. + } = cost + { + assert_eq!(zero_data_len, 100, "Should have 100 zero bytes"); + assert_eq!(non_zero_data_len, 100, "Should have 100 non-zero bytes"); + } + + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + + let used_gas = gasometer.total_used_gas(); + let tokens = calculate_tokens_in_calldata(100, 100); + let floor_cost = tokens * TOTAL_COST_FLOOR_PER_TOKEN; + + // Should use floor cost if it's higher than standard cost + assert!( + used_gas >= config.gas_transaction_call + floor_cost, + "Should apply floor cost for mixed byte pattern" + ); + } +} - // Very large calldata: 10000 bytes of zeros - let data = vec![0; 10000]; - let cost = call_transaction_cost(&data, &[], &[]); +// ============================================================================ +// Section 6: Snapshot Tests for Gas Calculations +// ============================================================================ - let mut gasometer = Gasometer::new(200_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); +#[cfg(test)] +mod snapshot_tests { + use super::*; - // Initially only standard cost: 21000 + 10000*4 = 61000 - assert_eq!(gasometer.total_used_gas(), 61000); + #[test] + fn test_snapshot_empty_calldata() { + let config = create_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer = Gasometer::new(gas_limit, &config); - // Simulate some execution gas - assert!(gasometer.record_cost(5000).is_ok()); + let data = vec![]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); - // Apply EIP-7623 adjustment - assert!(gasometer.post_execution().is_ok()); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); - // After adjustment: - // Standard: 10000*4 + 5000 = 45000 - // Floor: 10000 * 10 = 100000 - // Adjustment: 100000 - 45000 = 55000 - // Total: 61000 + 5000 + 55000 = 121000 - assert_eq!(gasometer.total_used_gas(), 121000); + let used_gas = gasometer.total_used_gas(); + + // Snapshot: Empty calldata should use exactly base cost (21000) + assert_eq!(used_gas, 21_000, "Empty calldata gas mismatch"); } #[test] - fn test_eip7623_create_transaction() { - // Test create transaction with EIP-7623 - let config = Config::pectra(); + fn test_snapshot_small_calldata() { + let config = create_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // 10 zero bytes, 10 non-zero bytes + let data = create_test_calldata(10, 10); + let cost = call_transaction_cost(&data, &vec![], &vec![]); - // Initcode with mixed data - let data = vec![0x60, 0x80, 0x60, 0x40, 0x52, 0, 0, 0, 0, 0]; // 5 non-zero, 5 zero - let cost = create_transaction_cost(&data, &[], &[]); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); - let mut gasometer = Gasometer::new(100_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + let used_gas = gasometer.total_used_gas(); - // Standard cost: 53000 + 5*4 + 5*16 = 53000 + 20 + 80 = 53100 - // Tokens: 5 + 5*4 = 25 - // Floor cost: 25 * 10 = 250 - // Max(53100, 250) = 53100 - assert_eq!(gasometer.total_used_gas(), 53102); + // Calculation: + // Base cost: 21000 + // Standard calldata cost: 10*4 + 10*16 = 200 + // Total standard: 21200 + // Floor cost: (10 + 10*4) * 10 = 500 + // Total floor: 21000 + 500 = 21500 + // Should use max(21200, 21500) = 21500 + assert_eq!(used_gas, 21_500, "Small calldata gas mismatch"); } #[test] - fn test_eip7623_disabled() { - // Test that when EIP-7623 is disabled, only standard cost applies - let config = Config::london(); + fn test_snapshot_medium_calldata() { + let config = create_eip7623_config(); + let gas_limit = 200_000; + let mut gasometer = Gasometer::new(gas_limit, &config); + + // 100 non-zero bytes + let data = vec![0xffu8; 100]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); - // Large calldata that would trigger floor cost if enabled - let data = vec![0; 10000]; - let cost = call_transaction_cost(&data, &[], &[]); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); - let mut gasometer = Gasometer::new(100_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + let used_gas = gasometer.total_used_gas(); - // Standard cost only: 21000 + 10000*4 = 61000 - assert_eq!(gasometer.total_used_gas(), 61000); + // Calculation: + // Base cost: 21000 + // Standard calldata cost: 100*16 = 1600 + // Total standard: 22600 + // Floor cost: 100*4*10 = 4000 + // Total floor: 21000 + 4000 = 25000 + // Should use max(22600, 25000) = 25000 + assert_eq!(used_gas, 25_000, "Medium calldata gas mismatch"); } #[test] - fn test_eip7623_mixed_calldata() { - // Test with mixed zero and non-zero bytes - let config = Config::pectra(); + fn test_snapshot_large_calldata() { + let config = create_eip7623_config(); + let gas_limit = 500_000; + let mut gasometer = Gasometer::new(gas_limit, &config); - // 1000 zeros and 1000 non-zeros - let mut data = vec![0; 1000]; - data.extend(vec![0xFF; 1000]); - let cost = call_transaction_cost(&data, &[], &[]); + // 1000 non-zero bytes + let data = vec![0xffu8; 1000]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + + let used_gas = gasometer.total_used_gas(); + + // Calculation: + // Base cost: 21000 + // Standard calldata cost: 1000*16 = 16000 + // Total standard: 37000 + // Floor cost: 1000*4*10 = 40000 + // Total floor: 21000 + 40000 = 61000 + // Should use max(37000, 61000) = 61000 + assert_eq!(used_gas, 61_000, "Large calldata gas mismatch"); + } + + #[test] + fn test_snapshot_mixed_calldata() { + let config = create_eip7623_config(); + let gas_limit = 100_000; + let mut gasometer = Gasometer::new(gas_limit, &config); - let mut gasometer = Gasometer::new(80_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + // 50 zero bytes, 50 non-zero bytes + let mut data = vec![0u8; 50]; + data.extend(vec![0xffu8; 50]); + let cost = call_transaction_cost(&data, &vec![], &vec![]); - // Standard cost: 21000 + 1000*4 + 1000*16 = 21000 + 4000 + 16000 = 41000 - // Tokens: 1000 + 1000*4 = 5000 - // Floor cost: 5000 * 10 = 50000 - // Since floor cost (50000) > standard cost (41000), floor wins - // But this happens in post_execution, so initial cost is still 41000 - assert_eq!(gasometer.total_used_gas(), 41000); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); - // Apply post-execution adjustment - assert!(gasometer.post_execution().is_ok()); + let used_gas = gasometer.total_used_gas(); - // After adjustment: 21000 + floor cost of 50000 = 71000 - assert_eq!(gasometer.total_used_gas(), 71000); + // Calculation: + // Base cost: 21000 + // Standard calldata cost: 50*4 + 50*16 = 1000 + // Total standard: 22000 + // Tokens: 50 + 50*4 = 250 + // Floor cost: 250*10 = 2500 + // Total floor: 21000 + 2500 = 23500 + // Should use max(22000, 23500) = 23500 + assert_eq!(used_gas, 23_500, "Mixed calldata gas mismatch"); } #[test] - fn test_eip7623_with_access_list() { - // Test transaction with access list - let config = Config::pectra(); + fn test_snapshot_contract_creation() { + let config = create_eip7623_config(); + let gas_limit = 200_000; + let mut gasometer = Gasometer::new(gas_limit, &config); - // Small calldata with access list - let data = vec![1, 2, 3, 4, 5]; - let access_list = vec![ - (H160::zero(), vec![H256::zero(), H256::from_low_u64_be(1)]), - (H160::from_low_u64_be(1), vec![H256::zero()]), + // Simple initcode: 20 bytes (mix of zero and non-zero) + let initcode = vec![ + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0xf3, 0x00, // RETURN + padding ]; - let cost = call_transaction_cost(&data, &access_list, &[]); - let mut gasometer = Gasometer::new(100_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + let cost = create_transaction_cost(&initcode, &vec![], &vec![]); - // Standard cost: 21000 + 0*4 + 5*16 + 2*2400 + 3*1900 = 21000 + 80 + 4800 + 5700 = 31580 - // Tokens: 0 + 5*4 = 20 - // Floor cost: 20 * 10 = 200 - // Max(31580, 200) = 31580 - assert_eq!(gasometer.total_used_gas(), 31580); + gasometer.record_transaction(cost).unwrap(); + gasometer.post_execution().unwrap(); + + let used_gas = gasometer.total_used_gas(); + + // Precise calculation based on the actual initcode content + let zero_count = initcode.iter().filter(|&&b| b == 0).count(); + let non_zero_count = initcode.len() - zero_count; + + // Verify our test data: should be 10 zero bytes (0x00) and 10 non-zero bytes (0x60, 0xf3) + assert_eq!(zero_count, 10, "Expected 10 zero bytes in test initcode"); + assert_eq!( + non_zero_count, 10, + "Expected 10 non-zero bytes in test initcode" + ); + + // Standard costs: + // Base: 53000 (gas_transaction_create) + // Standard calldata: 10*4 + 10*16 = 200 + // Initcode words: ((20 + 31) / 32) * 2 = 1 * 2 = 2 + // Total standard: 53000 + 200 + 2 = 53202 + + // EIP-7623 floor comparison: + // Standard calldata + execution + contract_creation = 200 + 0 + 2 = 202 + // Floor calldata only = 50 * 10 = 500 + // Since floor (500) > standard (202), should add difference + // Final cost = 53000 (base) + 500 (floor calldata) + 2 (initcode) = 53502 + + // Actually, let's verify what we observe vs what we expect + let tokens = zero_count as u64 + (non_zero_count as u64 * 4); // 10 + 10*4 = 50 + let standard_calldata = (zero_count as u64 * 4) + (non_zero_count as u64 * 16); // 200 + let floor_calldata = tokens * 10; // 500 + let initcode_cost = 2; // (20+31)/32 * 2 = 2 + let base_cost = config.gas_transaction_create; // 53000 + + // Standard path: 200 (calldata) + 2 (execution) + 32002 (contract creation) = 32204 + // Floor path: 500 (floor calldata only) + // max(32204, 500) = 32204, so no adjustment + // Total: 53000 (base) + 200 (calldata) + 2 (initcode) = 53202 + let expected_gas = 53_202; + assert_eq!( + used_gas, + expected_gas, + "Contract creation gas mismatch: expected {}, got {} \ + (zero_count: {}, non_zero_count: {}, standard_calldata: {}, floor_calldata: {}, \ + base_cost: {}, initcode_cost: {})", + expected_gas, + used_gas, + zero_count, + non_zero_count, + standard_calldata, + floor_calldata, + base_cost, + initcode_cost + ); + + // Verify components are included + assert!( + used_gas >= config.gas_transaction_create, + "Should include base creation cost" + ); } #[test] - fn test_eip7623_exact_boundary() { - // Test the exact boundary where floor cost equals standard cost - let config = Config::pectra(); + fn test_contract_creation_post_execution_investigation() { + // This test investigates why contract creation doesn't seem to apply floor cost + let config = create_eip7623_config(); + let gas_limit = 200_000; + + // Use the same initcode as the snapshot test + let initcode = vec![ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, + 0x60, 0x00, 0x60, 0x00, 0xf3, 0x00, + ]; + let cost = create_transaction_cost(&initcode, &vec![], &vec![]); + + // Test without post_execution + let mut gasometer_before = Gasometer::new(gas_limit, &config); + gasometer_before.record_transaction(cost.clone()).unwrap(); + let gas_before_post = gasometer_before.total_used_gas(); + + // Test with post_execution + let mut gasometer_after = Gasometer::new(gas_limit, &config); + gasometer_after.record_transaction(cost).unwrap(); + gasometer_after.post_execution().unwrap(); + let gas_after_post = gasometer_after.total_used_gas(); - // For zero bytes: standard = 4, floor = 10 - // With execution gas, we can reach equilibrium - let data = vec![0; 3500]; - let cost = call_transaction_cost(&data, &[], &[]); + println!( + "Contract creation gas before post_execution: {}", + gas_before_post + ); + println!( + "Contract creation gas after post_execution: {}", + gas_after_post + ); + println!( + "Difference: {}", + gas_after_post as i64 - gas_before_post as i64 + ); - let mut gasometer = Gasometer::new(100_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + // For comparison, test a regular call with similar calldata + let call_data = initcode.clone(); // Same bytes as initcode + let call_cost = call_transaction_cost(&call_data, &vec![], &vec![]); - // Initially: 21000 + 3500*4 = 35000 - assert_eq!(gasometer.total_used_gas(), 35000); + let mut call_gasometer = Gasometer::new(gas_limit, &config); + call_gasometer + .record_transaction(call_cost.clone()) + .unwrap(); + let call_gas_before = call_gasometer.total_used_gas(); - // Add execution gas to reach equilibrium - // Floor: 3500 * 10 = 35000 - // Standard calldata: 3500 * 4 = 14000 - // Need execution gas: 35000 - 14000 = 21000 - assert!(gasometer.record_cost(21000).is_ok()); + call_gasometer.post_execution().unwrap(); + let call_gas_after = call_gasometer.total_used_gas(); - // Apply EIP-7623 adjustment - assert!(gasometer.post_execution().is_ok()); + println!("Call gas before post_execution: {}", call_gas_before); + println!("Call gas after post_execution: {}", call_gas_after); + println!( + "Call difference: {}", + call_gas_after as i64 - call_gas_before as i64 + ); - // After adjustment: - // Standard: 14000 + 21000 = 35000 - // Floor: 35000 - // No adjustment needed (they're equal) - // Total: 35000 + 21000 = 56000 - assert_eq!(gasometer.total_used_gas(), 56000); + // CORRECT BEHAVIOR: + // 1. Contract creation shows NO difference before/after post_execution + // because the contract creation cost (32002) makes the standard path higher than floor + // 2. Regular calls show positive difference when floor cost > standard cost + // 3. This is the correct EIP-7623 behavior as specified + + // Verify correct behavior with assertions + assert_eq!( + gas_after_post, gas_before_post, + "Contract creation should NOT change with post_execution (contract creation cost dominates)" + ); + assert!( + call_gas_after > call_gas_before, + "Regular calls should increase with post_execution due to floor cost" + ); + + // Calculate expected floor increase for call + let zero_count = call_data.iter().filter(|&&b| b == 0).count(); + let non_zero_count = call_data.len() - zero_count; + let tokens = zero_count as u64 + (non_zero_count as u64 * 4); + let standard_calldata = (zero_count as u64 * 4) + (non_zero_count as u64 * 16); + let floor_calldata = tokens * 10; + let expected_increase = floor_calldata.saturating_sub(standard_calldata); + + assert_eq!( + call_gas_after - call_gas_before, + expected_increase, + "Call gas increase should match floor - standard difference" + ); } #[test] - fn test_eip7623_insufficient_gas_limit() { - // Test that transactions with insufficient gas limit are rejected - let config = Config::pectra(); + fn test_snapshot_comparison_with_without_eip7623() { + // Test the difference in gas consumption with and without EIP-7623 + let data = vec![0xffu8; 500]; // 500 non-zero bytes + + // Without EIP-7623 + let config_without = create_pre_eip7623_config(); + let mut gasometer_without = Gasometer::new(100_000, &config_without); + let cost = call_transaction_cost(&data, &vec![], &vec![]); + gasometer_without.record_transaction(cost.clone()).unwrap(); + let gas_without = gasometer_without.total_used_gas(); - // Large calldata that requires high floor cost - let data = vec![0; 5000]; // 5000 tokens - let cost = call_transaction_cost(&data, &[], &[]); + // With EIP-7623 + let config_with = create_eip7623_config(); + let mut gasometer_with = Gasometer::new(100_000, &config_with); + gasometer_with.record_transaction(cost).unwrap(); + gasometer_with.post_execution().unwrap(); + let gas_with = gasometer_with.total_used_gas(); - // Tokens: 5000 + 0*4 = 5000 - // Floor cost: 21000 + 5000 * 10 = 71000 - // Set gas limit below floor requirement - let mut gasometer = Gasometer::new(70_000, &config); + // Without EIP-7623: + // Base: 21000 + 500*16 = 29000 + assert_eq!(gas_without, 29_000, "Gas without EIP-7623 mismatch"); - // Should fail with OutOfGas due to insufficient gas limit - assert!(gasometer.record_transaction(cost).is_err()); + // With EIP-7623: + // Standard: 21000 + 500*16 = 29000 + // Floor: 21000 + 500*4*10 = 41000 + // Should use max(29000, 41000) = 41000 + assert_eq!(gas_with, 41_000, "Gas with EIP-7623 mismatch"); + + // EIP-7623 should increase cost for large calldata + assert!( + gas_with > gas_without, + "EIP-7623 should increase gas for large calldata" + ); + + // The increase should be exactly the difference between floor and standard + assert_eq!( + gas_with - gas_without, + 12_000, + "Gas increase should match floor - standard difference" + ); } +} + +// ============================================================================ +// Section 7: Integration Tests with Full Transaction Execution +// ============================================================================ + +#[cfg(test)] +mod integration_tests { + use super::*; #[test] - fn test_eip7623_sufficient_gas_limit_with_floor() { - // Test that transactions with sufficient gas limit but requiring floor adjustment work - let config = Config::pectra(); + fn test_full_transaction_execution_with_eip7623() { + let caller = H160::from_slice(&[1u8; 20]); + let target = H160::from_slice(&[2u8; 20]); + + let config = create_eip7623_config(); + + // Create initial state + let mut state = BTreeMap::new(); + state.insert( + caller, + evm::backend::MemoryAccount { + nonce: U256::zero(), + balance: U256::from(10_000_000), + storage: BTreeMap::new(), + code: Vec::new(), + }, + ); + + state.insert( + target, + evm::backend::MemoryAccount { + nonce: U256::zero(), + balance: U256::zero(), + storage: BTreeMap::new(), + // Simple contract that stores a value + code: vec![ + 0x60, 0x01, // PUSH1 0x01 + 0x60, 0x00, // PUSH1 0x00 + 0x55, // SSTORE + ], + }, + ); + + let vicinity = create_test_vicinity(); + let mut backend = MemoryBackend::new(&vicinity, state); + + // Create large calldata to trigger floor cost + let calldata = vec![0xffu8; 1000]; + let gas_limit = 500_000; + + let metadata = StackSubstateMetadata::new(gas_limit, &config); + let state = MemoryStackState::new(metadata, &mut backend); + let mut precompiles = (); + let mut executor = StackExecutor::new_with_precompiles(state, &config, &mut precompiles); + + let authorization_list = vec![]; + let (exit_reason, _result) = executor.transact_call( + caller, + target, + U256::zero(), + calldata.clone(), + gas_limit, + vec![], + authorization_list, + ); + + match exit_reason { + ExitReason::Succeed(_) => { + let gas_used = executor.used_gas(); + + // Calculate expected minimum gas with floor cost + let tokens = calculate_tokens_in_calldata(0, calldata.len()); + let floor_cost = tokens * TOTAL_COST_FLOOR_PER_TOKEN; + + assert!( + gas_used >= floor_cost, + "Gas used ({}) should be at least floor cost ({})", + gas_used, + floor_cost + ); + } + _ => panic!("Transaction should succeed, got {:?}", exit_reason), + } + } + + #[test] + fn test_contract_deployment_with_eip7623() { + let deployer = H160::from_slice(&[1u8; 20]); + + let config = create_eip7623_config(); + + // Create initial state + let mut state = BTreeMap::new(); + state.insert( + deployer, + evm::backend::MemoryAccount { + nonce: U256::zero(), + balance: U256::from(10_000_000), + storage: BTreeMap::new(), + code: Vec::new(), + }, + ); + + let vicinity = create_test_vicinity(); + let mut backend = MemoryBackend::new(&vicinity, state); + + // Contract initcode that deploys a simple storage contract + let initcode = vec![ + // Constructor code + 0x60, 0x0a, // PUSH1 0x0a (size of runtime code) + 0x60, 0x0c, // PUSH1 0x0c (offset of runtime code) + 0x60, 0x00, // PUSH1 0x00 (destination in memory) + 0x39, // CODECOPY + 0x60, 0x0a, // PUSH1 0x0a (size to return) + 0x60, 0x00, // PUSH1 0x00 (offset to return) + 0xf3, // RETURN + // Runtime code (10 bytes) + 0x60, 0x42, // PUSH1 0x42 + 0x60, 0x00, // PUSH1 0x00 + 0x55, // SSTORE + 0x60, 0x01, // PUSH1 0x01 + 0x60, 0x00, // PUSH1 0x00 + 0xf3, // RETURN + ]; + + let gas_limit = 500_000; + + let metadata = StackSubstateMetadata::new(gas_limit, &config); + let state = MemoryStackState::new(metadata, &mut backend); + let mut precompiles = (); + let mut executor = StackExecutor::new_with_precompiles(state, &config, &mut precompiles); + + let authorization_list = vec![]; + let (exit_reason, _result) = executor.transact_create( + deployer, + U256::zero(), + initcode.clone(), + gas_limit, + vec![], + authorization_list, + ); + + match exit_reason { + ExitReason::Succeed(_) => { + let gas_used = executor.used_gas(); + + // Calculate expected costs + let tokens = calculate_tokens_in_calldata( + initcode.iter().filter(|&&b| b == 0).count(), + initcode.iter().filter(|&&b| b != 0).count(), + ); + let floor_cost = tokens * TOTAL_COST_FLOOR_PER_TOKEN; + + // Contract creation should include initcode cost + let words = (initcode.len() + 31) / 32; + let initcode_word_cost = INITCODE_WORD_COST * words as u64; + + println!( + "Gas used: {}, Floor cost: {}, Initcode cost: {}", + gas_used, floor_cost, initcode_word_cost + ); + + // Verify gas consumption includes necessary costs + assert!( + gas_used >= config.gas_transaction_create, + "Should include base creation cost" + ); + } + _ => panic!("Contract deployment should succeed, got {:?}", exit_reason), + } + } + + #[test] + fn test_comparison_regular_vs_large_calldata_transaction() { + let caller = H160::from_slice(&[1u8; 20]); + let target = H160::from_slice(&[2u8; 20]); + + let config = create_eip7623_config(); + + // Test with small calldata (regular transaction) + let small_calldata = vec![0x01, 0x02, 0x03, 0x04]; // 4 bytes + + // Test with large calldata (should trigger floor cost) + let large_calldata = vec![0xffu8; 10000]; // 10KB + + // Create initial state + let mut state = BTreeMap::new(); + state.insert( + caller, + evm::backend::MemoryAccount { + nonce: U256::zero(), + balance: U256::from(100_000_000), + storage: BTreeMap::new(), + code: Vec::new(), + }, + ); + + state.insert( + target, + evm::backend::MemoryAccount { + nonce: U256::zero(), + balance: U256::zero(), + storage: BTreeMap::new(), + code: vec![0x00], // STOP + }, + ); + + let vicinity = create_test_vicinity(); + + // Execute small calldata transaction + let mut backend = MemoryBackend::new(&vicinity, state.clone()); + let metadata = StackSubstateMetadata::new(1_000_000, &config); + let state_small = MemoryStackState::new(metadata, &mut backend); + let mut precompiles_small = (); + let mut executor_small = + StackExecutor::new_with_precompiles(state_small, &config, &mut precompiles_small); + + let (exit_small, _) = executor_small.transact_call( + caller, + target, + U256::zero(), + small_calldata.clone(), + 1_000_000, + vec![], + vec![], + ); + + assert!(matches!(exit_small, ExitReason::Succeed(_))); + let gas_used_small = executor_small.used_gas(); + + // Execute large calldata transaction + let mut backend = MemoryBackend::new(&vicinity, state); + let metadata = StackSubstateMetadata::new(1_000_000, &config); + let state_large = MemoryStackState::new(metadata, &mut backend); + let mut precompiles_large = (); + let mut executor_large = + StackExecutor::new_with_precompiles(state_large, &config, &mut precompiles_large); - // Large calldata requiring floor cost - let data = vec![0; 5000]; // 5000 tokens - let cost = call_transaction_cost(&data, &[], &[]); + let (exit_large, _) = executor_large.transact_call( + caller, + target, + U256::zero(), + large_calldata.clone(), + 1_000_000, + vec![], + vec![], + ); - // Tokens: 5000 + 0*4 = 5000 - // Floor cost: 21000 + 5000 * 10 = 71000 - // Set gas limit above floor requirement - let mut gasometer = Gasometer::new(80_000, &config); - assert!(gasometer.record_transaction(cost).is_ok()); + assert!(matches!(exit_large, ExitReason::Succeed(_))); + let gas_used_large = executor_large.used_gas(); - // Initially: 21000 + 5000*4 = 41000 - assert_eq!(gasometer.total_used_gas(), 41000); + // Calculate expected floor costs + let tokens_large = calculate_tokens_in_calldata(0, large_calldata.len()); + let floor_cost_large = tokens_large * TOTAL_COST_FLOOR_PER_TOKEN; - // Add some execution gas - assert!(gasometer.record_cost(5000).is_ok()); + println!("Small transaction gas: {}", gas_used_small); + println!("Large transaction gas: {}", gas_used_large); + println!("Expected floor cost for large: {}", floor_cost_large); - // Apply EIP-7623 adjustment - assert!(gasometer.post_execution().is_ok()); + // Large calldata should use significantly more gas due to floor cost + // The ratio should be significant but not necessarily 100x + // With 4 bytes vs 10,000 bytes, we expect at least 10x more gas + assert!( + gas_used_large > gas_used_small * 10, + "Large calldata should use much more gas than small calldata: {} vs {}", + gas_used_large, + gas_used_small + ); - // After adjustment: - // Standard: 5000*4 + 5000 = 25000 - // Tokens: 5000 + 0*4 = 5000 - // Floor: 5000 * 10 = 50000 - // Adjustment: 50000 - 25000 = 25000 - // Total: 41000 + 5000 + 25000 = 71000 - assert_eq!(gasometer.total_used_gas(), 71000); + // Verify floor cost is being applied + assert!( + gas_used_large >= floor_cost_large, + "Large transaction should meet floor cost requirement" + ); } } From 021f4912a3ab607121475365564ab8fb92f97088 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 11:09:10 +0300 Subject: [PATCH 15/30] refactor: :rotating_light: clippy --- gasometer/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 33dd8960..ce1ab0d4 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -920,9 +920,9 @@ impl GasTracker { } let cost = if self.is_contract_creation { - config.gas_transaction_create as u64 + config.gas_transaction_create } else { - config.gas_transaction_call as u64 + config.gas_transaction_call }; self.cached_base_cost = Some(cost); cost From 76b381b077ccf22a9a4904f6028c327d32a73df1 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 11:22:10 +0300 Subject: [PATCH 16/30] refactor: :recycle: rename GasTracker to GasMetrics and move it to a new module --- gasometer/src/lib.rs | 128 ++------------------------------------- gasometer/src/metrics.rs | 122 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 123 deletions(-) create mode 100644 gasometer/src/metrics.rs diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index ce1ab0d4..1b0a3b26 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -35,6 +35,7 @@ macro_rules! event { mod consts; mod costs; mod memory; +mod metrics; mod utils; use alloc::vec::Vec; @@ -43,6 +44,8 @@ use evm_core::{ExitError, Opcode, Stack}; use evm_runtime::{Config, Handler}; use primitive_types::{H160, H256, U256}; +use crate::metrics::GasMetrics; + macro_rules! try_or_fail { ( $inner:expr, $e:expr ) => { match $e { @@ -83,7 +86,7 @@ impl<'config> Gasometer<'config> { used_gas: 0, refunded_gas: 0, config, - tracker: GasTracker::new(), + tracker: GasMetrics::new(), }), } } @@ -845,127 +848,6 @@ pub fn dynamic_opcode_cost( Ok((gas_cost, storage_target, memory_cost)) } -/// Tracks gas parameters for a Gasometer instance. -#[derive(Clone, Debug)] -struct GasTracker { - zero_bytes_in_calldata: usize, - non_zero_bytes_in_calldata: usize, - is_contract_creation: bool, - // Cached values - cached_standard_calldata_cost: Option, - cached_floor_calldata_cost: Option, - cached_base_cost: Option, - cached_init_code_cost: Option, - cached_contract_creation_cost: Option, -} - -impl GasTracker { - fn new() -> Self { - Self { - zero_bytes_in_calldata: 0, - non_zero_bytes_in_calldata: 0, - is_contract_creation: false, - cached_standard_calldata_cost: None, - cached_floor_calldata_cost: None, - cached_base_cost: None, - cached_init_code_cost: None, - cached_contract_creation_cost: None, - } - } - - fn invalidate_cache(&mut self) { - self.cached_standard_calldata_cost = None; - self.cached_floor_calldata_cost = None; - self.cached_base_cost = None; - self.cached_init_code_cost = None; - self.cached_contract_creation_cost = None; - } - - fn set_calldata_params(&mut self, zero_bytes: usize, non_zero_bytes: usize) { - self.zero_bytes_in_calldata = zero_bytes; - self.non_zero_bytes_in_calldata = non_zero_bytes; - self.invalidate_cache(); - } - - fn set_contract_creation(&mut self, is_creation: bool) { - self.is_contract_creation = is_creation; - self.invalidate_cache(); - } - - fn standard_calldata_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_standard_calldata_cost { - return cached; - } - - let cost = (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) - + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)); - self.cached_standard_calldata_cost = Some(cost); - cost - } - - fn floor_calldata_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_floor_calldata_cost { - return cached; - } - - let cost = (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) - + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)); - self.cached_floor_calldata_cost = Some(cost); - cost - } - - fn base_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_base_cost { - return cached; - } - - let cost = if self.is_contract_creation { - config.gas_transaction_create - } else { - config.gas_transaction_call - }; - self.cached_base_cost = Some(cost); - cost - } - - fn init_code_cost(&mut self) -> u64 { - if let Some(cached) = self.cached_init_code_cost { - return cached; - } - - let cost = if self.is_contract_creation { - init_code_cost( - self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, - ) - } else { - 0 - }; - self.cached_init_code_cost = Some(cost); - cost - } - - fn contract_creation_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_contract_creation_cost { - return cached; - } - - let cost = if self.is_contract_creation { - (config.gas_transaction_create - config.gas_transaction_call) + self.init_code_cost() - } else { - 0 - }; - self.cached_contract_creation_cost = Some(cost); - cost - } - - fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { - used_gas - .saturating_sub(self.base_cost(config)) - .saturating_sub(self.init_code_cost()) - .saturating_sub(self.standard_calldata_cost(config)) - } -} - /// Holds the gas consumption for a Gasometer instance. #[derive(Clone, Debug)] struct Inner<'config> { @@ -973,7 +855,7 @@ struct Inner<'config> { used_gas: u64, refunded_gas: i64, config: &'config Config, - tracker: GasTracker, + tracker: GasMetrics, } impl Inner<'_> { diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs new file mode 100644 index 00000000..aad2baee --- /dev/null +++ b/gasometer/src/metrics.rs @@ -0,0 +1,122 @@ +use evm_runtime::Config; + +/// Tracks gas parameters for a Gasometer instance. +#[derive(Clone, Debug)] +pub struct GasMetrics { + zero_bytes_in_calldata: usize, + non_zero_bytes_in_calldata: usize, + is_contract_creation: bool, + // Cached values + cached_standard_calldata_cost: Option, + cached_floor_calldata_cost: Option, + cached_base_cost: Option, + cached_init_code_cost: Option, + cached_contract_creation_cost: Option, +} + +impl GasMetrics { + pub fn new() -> Self { + Self { + zero_bytes_in_calldata: 0, + non_zero_bytes_in_calldata: 0, + is_contract_creation: false, + cached_standard_calldata_cost: None, + cached_floor_calldata_cost: None, + cached_base_cost: None, + cached_init_code_cost: None, + cached_contract_creation_cost: None, + } + } + + fn invalidate_cache(&mut self) { + self.cached_standard_calldata_cost = None; + self.cached_floor_calldata_cost = None; + self.cached_base_cost = None; + self.cached_init_code_cost = None; + self.cached_contract_creation_cost = None; + } + + pub fn set_calldata_params(&mut self, zero_bytes: usize, non_zero_bytes: usize) { + self.zero_bytes_in_calldata = zero_bytes; + self.non_zero_bytes_in_calldata = non_zero_bytes; + self.invalidate_cache(); + } + + pub fn set_contract_creation(&mut self, is_creation: bool) { + self.is_contract_creation = is_creation; + self.invalidate_cache(); + } + + pub fn standard_calldata_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_standard_calldata_cost { + return cached; + } + + let cost = (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) + + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)); + self.cached_standard_calldata_cost = Some(cost); + cost + } + + pub fn floor_calldata_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_floor_calldata_cost { + return cached; + } + + let cost = (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) + + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)); + self.cached_floor_calldata_cost = Some(cost); + cost + } + + fn base_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_base_cost { + return cached; + } + + let cost = if self.is_contract_creation { + config.gas_transaction_create + } else { + config.gas_transaction_call + }; + self.cached_base_cost = Some(cost); + cost + } + + pub fn init_code_cost(&mut self) -> u64 { + if let Some(cached) = self.cached_init_code_cost { + return cached; + } + + let cost = if self.is_contract_creation { + super::init_code_cost( + self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, + ) + } else { + 0 + }; + self.cached_init_code_cost = Some(cost); + cost + } + + pub fn contract_creation_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_contract_creation_cost { + return cached; + } + + let cost = if self.is_contract_creation { + (config.gas_transaction_create - config.gas_transaction_call) + self.init_code_cost() + } else { + 0 + }; + self.cached_contract_creation_cost = Some(cost); + cost + } + + pub fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { + used_gas + .saturating_sub(self.base_cost(config)) + .saturating_sub(self.init_code_cost()) + .saturating_sub(self.standard_calldata_cost(config)) + } +} From f7d545acff1a16963189628516578413484273f3 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 11:26:57 +0300 Subject: [PATCH 17/30] refactor: :recycle: rename execution_cost to non_intrinsic_cost --- gasometer/src/lib.rs | 9 ++++++--- gasometer/src/metrics.rs | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 1b0a3b26..8761b9e3 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -371,7 +371,7 @@ impl<'config> Gasometer<'config> { let standard_calldata_cost = inner.standard_calldata_cost(); let floor_calldata_cost = inner.floor_calldata_cost(); let contract_creation_cost = inner.contract_creation_cost(); - let execution_cost = inner.execution_cost(); + let execution_cost = inner.non_intrinsic_cost(); let cost = standard_calldata_cost + execution_cost + contract_creation_cost; let eip_7623_cost = max(cost, floor_calldata_cost); @@ -1024,8 +1024,11 @@ impl Inner<'_> { self.tracker.contract_creation_cost(self.config) } - fn execution_cost(&mut self) -> u64 { - self.tracker.execution_cost(self.used_gas, self.config) + /// Gas consumed during transaction execution, excluding base transaction costs, + /// calldata costs, and contract creation costs. This value only represents + /// the actual execution cost within post_execution() invocation + fn non_intrinsic_cost(&mut self) -> u64 { + self.tracker.non_intrinsic_cost(self.used_gas, self.config) } } diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index aad2baee..214f4b82 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -113,7 +113,10 @@ impl GasMetrics { cost } - pub fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { + /// Gas consumed during transaction execution, excluding base transaction costs, + /// calldata costs, and contract creation costs. This value only represents + /// the actual execution cost within post_execution() invocation. + pub fn non_intrinsic_cost(&mut self, used_gas: u64, config: &Config) -> u64 { used_gas .saturating_sub(self.base_cost(config)) .saturating_sub(self.init_code_cost()) From 27f7f3a4030ea583ae3f70c0f790b8db986f1881 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 11:40:17 +0300 Subject: [PATCH 18/30] refactor: :recycle: rename tracker to metrics --- gasometer/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 8761b9e3..f0c9365c 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -86,7 +86,7 @@ impl<'config> Gasometer<'config> { used_gas: 0, refunded_gas: 0, config, - tracker: GasMetrics::new(), + metrics: GasMetrics::new(), }), } } @@ -257,7 +257,7 @@ impl<'config> Gasometer<'config> { + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; self.inner_mut()? - .tracker + .metrics .set_calldata_params(zero_data_len, non_zero_data_len); #[deny(clippy::let_and_return)] @@ -293,9 +293,9 @@ impl<'config> Gasometer<'config> { + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; self.inner_mut()? - .tracker + .metrics .set_calldata_params(zero_data_len, non_zero_data_len); - self.inner_mut()?.tracker.set_contract_creation(true); + self.inner_mut()?.metrics.set_contract_creation(true); let mut cost = self.config.gas_transaction_create + calldata_cost + access_list_address_len as u64 @@ -855,7 +855,7 @@ struct Inner<'config> { used_gas: u64, refunded_gas: i64, config: &'config Config, - tracker: GasMetrics, + metrics: GasMetrics, } impl Inner<'_> { @@ -1013,22 +1013,22 @@ impl Inner<'_> { } fn standard_calldata_cost(&mut self) -> u64 { - self.tracker.standard_calldata_cost(self.config) + self.metrics.standard_calldata_cost(self.config) } fn floor_calldata_cost(&mut self) -> u64 { - self.tracker.floor_calldata_cost(self.config) + self.metrics.floor_calldata_cost(self.config) } fn contract_creation_cost(&mut self) -> u64 { - self.tracker.contract_creation_cost(self.config) + self.metrics.contract_creation_cost(self.config) } /// Gas consumed during transaction execution, excluding base transaction costs, /// calldata costs, and contract creation costs. This value only represents /// the actual execution cost within post_execution() invocation fn non_intrinsic_cost(&mut self) -> u64 { - self.tracker.non_intrinsic_cost(self.used_gas, self.config) + self.metrics.non_intrinsic_cost(self.used_gas, self.config) } } From 8b3aa099aad163b03b4c8ce248057e2e223c2d61 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 12:10:35 +0300 Subject: [PATCH 19/30] docs: :memo: improve docs --- gasometer/src/lib.rs | 46 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index f0c9365c..315b0498 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -253,17 +253,15 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { - let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - self.inner_mut()? .metrics .set_calldata_params(zero_data_len, non_zero_data_len); #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call - + calldata_cost + access_list_address_len as u64 - * self.config.gas_access_list_address + + zero_data_len as u64 * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + 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; @@ -289,17 +287,15 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { - let calldata_cost = zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data; - self.inner_mut()? .metrics .set_calldata_params(zero_data_len, non_zero_data_len); self.inner_mut()?.metrics.set_contract_creation(true); let mut cost = self.config.gas_transaction_create - + calldata_cost + access_list_address_len as u64 - * self.config.gas_access_list_address + + zero_data_len as u64 * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + 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; @@ -336,7 +332,8 @@ impl<'config> Gasometer<'config> { // is considered invalid. // TODO check intrinsic gas cost if self.gas_limit - < self.config().gas_transaction_call + self.inner_mut()?.floor_calldata_cost() + < self.config().gas_transaction_call + + self.inner_mut()?.floor_calldata_cost_metrics() { self.inner = Err(ExitError::OutOfGas); return Err(ExitError::OutOfGas); @@ -368,10 +365,10 @@ impl<'config> Gasometer<'config> { // Apply EIP-7623 adjustments if self.config.has_eip_7623 { let inner = self.inner_mut()?; - let standard_calldata_cost = inner.standard_calldata_cost(); - let floor_calldata_cost = inner.floor_calldata_cost(); - let contract_creation_cost = inner.contract_creation_cost(); - let execution_cost = inner.non_intrinsic_cost(); + let standard_calldata_cost = inner.standard_calldata_cost_metrics(); + let floor_calldata_cost = inner.floor_calldata_cost_metrics(); + let contract_creation_cost = inner.contract_creation_cost_metrics(); + let execution_cost = inner.non_intrinsic_cost_metrics(); let cost = standard_calldata_cost + execution_cost + contract_creation_cost; let eip_7623_cost = max(cost, floor_calldata_cost); @@ -1012,22 +1009,29 @@ impl Inner<'_> { } } - fn standard_calldata_cost(&mut self) -> u64 { + /// Standard gas cost for transaction calldata. + /// Valid only after [`GasMetrics::set_calldata_params`] has been called. + fn standard_calldata_cost_metrics(&mut self) -> u64 { self.metrics.standard_calldata_cost(self.config) } - fn floor_calldata_cost(&mut self) -> u64 { + /// Floor gas cost for transaction calldata as defined by EIP-7623. + /// Valid only after [`GasMetrics::set_calldata_params`] has been called. + fn floor_calldata_cost_metrics(&mut self) -> u64 { self.metrics.floor_calldata_cost(self.config) } - fn contract_creation_cost(&mut self) -> u64 { + /// Gas cost for contract creation. + /// Valid only after both [`GasMetrics::set_calldata_params`] and + /// [`GasMetrics::set_contract_creation`] have been called. + fn contract_creation_cost_metrics(&mut self) -> u64 { self.metrics.contract_creation_cost(self.config) } /// Gas consumed during transaction execution, excluding base transaction costs, - /// calldata costs, and contract creation costs. This value only represents - /// the actual execution cost within post_execution() invocation - fn non_intrinsic_cost(&mut self) -> u64 { + /// calldata costs, and contract creation costs. This value represents + /// the actual execution cost only when called from within [`Gasometer::post_execution`]. + fn non_intrinsic_cost_metrics(&mut self) -> u64 { self.metrics.non_intrinsic_cost(self.used_gas, self.config) } } From fa7707c4ee3bf96ff7420433497ab400fd1b589f Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 13:54:44 +0300 Subject: [PATCH 20/30] fix: :bug: add instrinsic gas cost metric --- gasometer/src/lib.rs | 24 +++++++++++++--- gasometer/src/metrics.rs | 59 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 315b0498..6ec2bbab 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -256,6 +256,12 @@ impl<'config> Gasometer<'config> { self.inner_mut()? .metrics .set_calldata_params(zero_data_len, non_zero_data_len); + self.inner_mut()? + .metrics + .set_access_list_len(access_list_address_len, access_list_storage_len); + self.inner_mut()? + .metrics + .set_authorization_list_len(authorization_list_len); #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -290,6 +296,12 @@ impl<'config> Gasometer<'config> { self.inner_mut()? .metrics .set_calldata_params(zero_data_len, non_zero_data_len); + self.inner_mut()? + .metrics + .set_access_list_len(access_list_address_len, access_list_storage_len); + self.inner_mut()? + .metrics + .set_authorization_list_len(authorization_list_len); self.inner_mut()?.metrics.set_contract_creation(true); let mut cost = self.config.gas_transaction_create @@ -330,10 +342,10 @@ impl<'config> Gasometer<'config> { // 21000 + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata // or below its intrinsic gas cost (take the maximum of these two calculations) // is considered invalid. - // TODO check intrinsic gas cost if self.gas_limit < self.config().gas_transaction_call + self.inner_mut()?.floor_calldata_cost_metrics() + || self.gas_limit < self.inner_mut()?.intrinsic_cost_metrics() { self.inner = Err(ExitError::OutOfGas); return Err(ExitError::OutOfGas); @@ -368,7 +380,7 @@ impl<'config> Gasometer<'config> { let standard_calldata_cost = inner.standard_calldata_cost_metrics(); let floor_calldata_cost = inner.floor_calldata_cost_metrics(); let contract_creation_cost = inner.contract_creation_cost_metrics(); - let execution_cost = inner.non_intrinsic_cost_metrics(); + let execution_cost = inner.execution_cost_metrics(); let cost = standard_calldata_cost + execution_cost + contract_creation_cost; let eip_7623_cost = max(cost, floor_calldata_cost); @@ -1028,11 +1040,15 @@ impl Inner<'_> { self.metrics.contract_creation_cost(self.config) } + fn intrinsic_cost_metrics(&mut self) -> u64 { + self.metrics.intrinsic_cost(self.config) + } + /// Gas consumed during transaction execution, excluding base transaction costs, /// calldata costs, and contract creation costs. This value represents /// the actual execution cost only when called from within [`Gasometer::post_execution`]. - fn non_intrinsic_cost_metrics(&mut self) -> u64 { - self.metrics.non_intrinsic_cost(self.used_gas, self.config) + fn execution_cost_metrics(&mut self) -> u64 { + self.metrics.execution_cost(self.used_gas, self.config) } } diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index 214f4b82..00345e43 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -5,6 +5,9 @@ use evm_runtime::Config; pub struct GasMetrics { zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, + access_list_address_len: usize, + access_list_storage_len: usize, + authorization_list_len: usize, is_contract_creation: bool, // Cached values cached_standard_calldata_cost: Option, @@ -12,6 +15,8 @@ pub struct GasMetrics { cached_base_cost: Option, cached_init_code_cost: Option, cached_contract_creation_cost: Option, + cached_access_list_cost: Option, + cached_authorization_list_cost: Option, } impl GasMetrics { @@ -19,12 +24,17 @@ impl GasMetrics { Self { zero_bytes_in_calldata: 0, non_zero_bytes_in_calldata: 0, + access_list_address_len: 0, + access_list_storage_len: 0, + authorization_list_len: 0, is_contract_creation: false, cached_standard_calldata_cost: None, cached_floor_calldata_cost: None, cached_base_cost: None, cached_init_code_cost: None, cached_contract_creation_cost: None, + cached_access_list_cost: None, + cached_authorization_list_cost: None, } } @@ -34,6 +44,8 @@ impl GasMetrics { self.cached_base_cost = None; self.cached_init_code_cost = None; self.cached_contract_creation_cost = None; + self.cached_access_list_cost = None; + self.cached_authorization_list_cost = None; } pub fn set_calldata_params(&mut self, zero_bytes: usize, non_zero_bytes: usize) { @@ -42,6 +54,21 @@ impl GasMetrics { self.invalidate_cache(); } + pub fn set_access_list_len( + &mut self, + access_list_address_len: usize, + access_list_storage_len: usize, + ) { + self.access_list_address_len = access_list_address_len; + self.access_list_storage_len = access_list_storage_len; + self.invalidate_cache(); + } + + pub fn set_authorization_list_len(&mut self, authorization_list_len: usize) { + self.authorization_list_len = authorization_list_len; + self.invalidate_cache(); + } + pub fn set_contract_creation(&mut self, is_creation: bool) { self.is_contract_creation = is_creation; self.invalidate_cache(); @@ -83,6 +110,27 @@ impl GasMetrics { cost } + fn access_list_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_access_list_cost { + return cached; + } + + let cost = self.access_list_address_len as u64 * config.gas_access_list_address + + self.access_list_storage_len as u64 * config.gas_access_list_storage_key; + self.cached_access_list_cost = Some(cost); + cost + } + + fn authorization_list_cost(&mut self, config: &Config) -> u64 { + if let Some(cached) = self.cached_authorization_list_cost { + return cached; + } + + let cost = self.authorization_list_len as u64 * config.gas_per_empty_account_cost; + self.cached_authorization_list_cost = Some(cost); + cost + } + pub fn init_code_cost(&mut self) -> u64 { if let Some(cached) = self.cached_init_code_cost { return cached; @@ -113,10 +161,19 @@ impl GasMetrics { cost } + /// Intrinsic gas costs of a transaction. + pub fn intrinsic_cost(&mut self, config: &Config) -> u64 { + self.base_cost(config) + .saturating_add(self.init_code_cost()) + .saturating_add(self.standard_calldata_cost(config)) + .saturating_add(self.access_list_cost(config)) + .saturating_add(self.authorization_list_cost(config)) + } + /// Gas consumed during transaction execution, excluding base transaction costs, /// calldata costs, and contract creation costs. This value only represents /// the actual execution cost within post_execution() invocation. - pub fn non_intrinsic_cost(&mut self, used_gas: u64, config: &Config) -> u64 { + pub fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { used_gas .saturating_sub(self.base_cost(config)) .saturating_sub(self.init_code_cost()) From 78abe6daee19c5cb8bf264ba7a44a5986b909c70 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 13:56:26 +0300 Subject: [PATCH 21/30] docs: :memo: document intrinsic_cost_metrics method --- gasometer/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 6ec2bbab..585c7ec8 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -1040,6 +1040,16 @@ impl Inner<'_> { self.metrics.contract_creation_cost(self.config) } + /// Calculates the intrinsic gas cost for the transaction. + /// + /// The intrinsic cost includes: + /// - Base transaction cost + /// - Calldata costs + /// - Access list costs (per EIP-2930) + /// - Authorization list costs (per EIP-7702) + /// - Contract creation costs if applicable (per EIP-3860) + /// + /// This represents the minimum gas required before any code execution begins. fn intrinsic_cost_metrics(&mut self) -> u64 { self.metrics.intrinsic_cost(self.config) } From c425c74e53e079e23870b68311af5ee49f000856 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 14:00:44 +0300 Subject: [PATCH 22/30] refactor: :recycle: add init method to GasMetrics --- gasometer/src/lib.rs | 46 ++++++++++++++++++---------------------- gasometer/src/metrics.rs | 24 +++++++-------------- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 585c7ec8..ab4ead54 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -253,15 +253,14 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { - self.inner_mut()? - .metrics - .set_calldata_params(zero_data_len, non_zero_data_len); - self.inner_mut()? - .metrics - .set_access_list_len(access_list_address_len, access_list_storage_len); - self.inner_mut()? - .metrics - .set_authorization_list_len(authorization_list_len); + self.inner_mut()?.metrics.init( + zero_data_len, + non_zero_data_len, + access_list_address_len, + access_list_storage_len, + authorization_list_len, + false, + ); #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -293,16 +292,14 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { - self.inner_mut()? - .metrics - .set_calldata_params(zero_data_len, non_zero_data_len); - self.inner_mut()? - .metrics - .set_access_list_len(access_list_address_len, access_list_storage_len); - self.inner_mut()? - .metrics - .set_authorization_list_len(authorization_list_len); - self.inner_mut()?.metrics.set_contract_creation(true); + self.inner_mut()?.metrics.init( + zero_data_len, + non_zero_data_len, + access_list_address_len, + access_list_storage_len, + authorization_list_len, + true, + ); let mut cost = self.config.gas_transaction_create + zero_data_len as u64 * self.config.gas_transaction_zero_data @@ -1022,33 +1019,32 @@ impl Inner<'_> { } /// Standard gas cost for transaction calldata. - /// Valid only after [`GasMetrics::set_calldata_params`] has been called. + /// Valid only after [`GasMetrics::init`] has been called. fn standard_calldata_cost_metrics(&mut self) -> u64 { self.metrics.standard_calldata_cost(self.config) } /// Floor gas cost for transaction calldata as defined by EIP-7623. - /// Valid only after [`GasMetrics::set_calldata_params`] has been called. + /// Valid only after [`GasMetrics::init`] has been called. fn floor_calldata_cost_metrics(&mut self) -> u64 { self.metrics.floor_calldata_cost(self.config) } /// Gas cost for contract creation. - /// Valid only after both [`GasMetrics::set_calldata_params`] and - /// [`GasMetrics::set_contract_creation`] have been called. + /// Valid only after [`GasMetrics::init`] has been called with `is_contract_creation` set to true. fn contract_creation_cost_metrics(&mut self) -> u64 { self.metrics.contract_creation_cost(self.config) } /// Calculates the intrinsic gas cost for the transaction. - /// + /// /// The intrinsic cost includes: /// - Base transaction cost /// - Calldata costs /// - Access list costs (per EIP-2930) /// - Authorization list costs (per EIP-7702) /// - Contract creation costs if applicable (per EIP-3860) - /// + /// /// This represents the minimum gas required before any code execution begins. fn intrinsic_cost_metrics(&mut self) -> u64 { self.metrics.intrinsic_cost(self.config) diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index 00345e43..fa304b12 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -48,29 +48,21 @@ impl GasMetrics { self.cached_authorization_list_cost = None; } - pub fn set_calldata_params(&mut self, zero_bytes: usize, non_zero_bytes: usize) { - self.zero_bytes_in_calldata = zero_bytes; - self.non_zero_bytes_in_calldata = non_zero_bytes; - self.invalidate_cache(); - } - - pub fn set_access_list_len( + pub fn init( &mut self, + zero_bytes_in_calldata: usize, + non_zero_bytes_in_calldata: usize, access_list_address_len: usize, access_list_storage_len: usize, + authorization_list_len: usize, + is_contract_creation: bool, ) { + self.zero_bytes_in_calldata = zero_bytes_in_calldata; + self.non_zero_bytes_in_calldata = non_zero_bytes_in_calldata; self.access_list_address_len = access_list_address_len; self.access_list_storage_len = access_list_storage_len; - self.invalidate_cache(); - } - - pub fn set_authorization_list_len(&mut self, authorization_list_len: usize) { self.authorization_list_len = authorization_list_len; - self.invalidate_cache(); - } - - pub fn set_contract_creation(&mut self, is_creation: bool) { - self.is_contract_creation = is_creation; + self.is_contract_creation = is_contract_creation; self.invalidate_cache(); } From 2a0af911dbd00095a1cd3e72708c9f04f3d4a7d0 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 14:21:09 +0300 Subject: [PATCH 23/30] refactor: :recycle: reduce code duplication --- gasometer/src/lib.rs | 47 +++++++++++++++++++++++++++++++--------- gasometer/src/metrics.rs | 19 +++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index ab4ead54..4e71e447 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -264,11 +264,12 @@ impl<'config> Gasometer<'config> { #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call - + zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data - + access_list_address_len as u64 * self.config.gas_access_list_address - + 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; + + calldata_cost(zero_data_len, non_zero_data_len, self.config) + + access_list_cost( + access_list_address_len, + access_list_storage_len, + self.config, + ) + authorization_list_cost(authorization_list_len, self.config); log_gas!( self, @@ -302,11 +303,12 @@ impl<'config> Gasometer<'config> { ); let mut cost = self.config.gas_transaction_create - + zero_data_len as u64 * self.config.gas_transaction_zero_data - + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data - + access_list_address_len as u64 * self.config.gas_access_list_address - + 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; + + calldata_cost(zero_data_len, non_zero_data_len, self.config) + + access_list_cost( + access_list_address_len, + access_list_storage_len, + self.config, + ) + authorization_list_cost(authorization_list_len, self.config); if self.config.max_initcode_size.is_some() { cost += initcode_cost; @@ -438,6 +440,31 @@ pub fn create_transaction_cost( } } +/// Calculate total calldata cost (zero + non-zero bytes) +pub fn calldata_cost( + zero_bytes_in_calldata: usize, + non_zero_bytes_in_calldata: usize, + config: &Config, +) -> u64 { + zero_bytes_in_calldata as u64 * config.gas_transaction_zero_data + + non_zero_bytes_in_calldata as u64 * config.gas_transaction_non_zero_data +} + +/// Calculate total access list cost (addresses + storage keys) +pub fn access_list_cost( + access_list_address_len: usize, + access_list_storage_len: usize, + config: &Config, +) -> u64 { + access_list_address_len as u64 * config.gas_access_list_address + + access_list_storage_len as u64 * config.gas_access_list_storage_key +} + +/// Calculate authorization list cost +pub fn authorization_list_cost(authorization_list_len: usize, config: &Config) -> u64 { + authorization_list_len as u64 * config.gas_per_empty_account_cost +} + pub fn init_code_cost(code_length: u64) -> u64 { // As per EIP-3860: // > We define initcode_cost(initcode) to equal INITCODE_WORD_COST * ceil(len(initcode) / 32). diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index fa304b12..0bda7c75 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -71,8 +71,12 @@ impl GasMetrics { return cached; } - let cost = (config.gas_transaction_zero_data * (self.zero_bytes_in_calldata as u64)) - + (config.gas_transaction_non_zero_data * (self.non_zero_bytes_in_calldata as u64)); + let cost = super::calldata_cost( + self.zero_bytes_in_calldata, + self.non_zero_bytes_in_calldata, + config, + ); + self.cached_standard_calldata_cost = Some(cost); cost } @@ -107,8 +111,12 @@ impl GasMetrics { return cached; } - let cost = self.access_list_address_len as u64 * config.gas_access_list_address - + self.access_list_storage_len as u64 * config.gas_access_list_storage_key; + let cost = super::access_list_cost( + self.access_list_address_len, + self.access_list_storage_len, + config, + ); + self.cached_access_list_cost = Some(cost); cost } @@ -118,7 +126,8 @@ impl GasMetrics { return cached; } - let cost = self.authorization_list_len as u64 * config.gas_per_empty_account_cost; + let cost = super::authorization_list_cost(self.authorization_list_len, config); + self.cached_authorization_list_cost = Some(cost); cost } From f279eff39ddd66e9b4cfee4ac9c1fe967032099b Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 14:24:40 +0300 Subject: [PATCH 24/30] refactor: :recycle: improve if condition --- gasometer/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 4e71e447..d72d39c4 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -342,10 +342,11 @@ impl<'config> Gasometer<'config> { // or below its intrinsic gas cost (take the maximum of these two calculations) // is considered invalid. if self.gas_limit - < self.config().gas_transaction_call - + self.inner_mut()?.floor_calldata_cost_metrics() - || self.gas_limit < self.inner_mut()?.intrinsic_cost_metrics() - { + < max( + self.config().gas_transaction_call + + self.inner_mut()?.floor_calldata_cost_metrics(), + self.inner_mut()?.intrinsic_cost_metrics(), + ) { self.inner = Err(ExitError::OutOfGas); return Err(ExitError::OutOfGas); } From 70ae6e1be7f24c7215d61e82f937baf2d501262d Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 14:27:08 +0300 Subject: [PATCH 25/30] fix: :bug: add contract creation component to intrinsic gas calculation --- gasometer/src/metrics.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index 0bda7c75..5914faf7 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -169,6 +169,7 @@ impl GasMetrics { .saturating_add(self.standard_calldata_cost(config)) .saturating_add(self.access_list_cost(config)) .saturating_add(self.authorization_list_cost(config)) + .saturating_add(self.init_code_cost()) } /// Gas consumed during transaction execution, excluding base transaction costs, From d4a11c64e1eccf535d0e36b1aab4665bfc657ea1 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 17:26:26 +0300 Subject: [PATCH 26/30] fix: :bug: remove duplicated init_code_cost component --- gasometer/src/metrics.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index 5914faf7..73b09d39 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -165,7 +165,6 @@ impl GasMetrics { /// Intrinsic gas costs of a transaction. pub fn intrinsic_cost(&mut self, config: &Config) -> u64 { self.base_cost(config) - .saturating_add(self.init_code_cost()) .saturating_add(self.standard_calldata_cost(config)) .saturating_add(self.access_list_cost(config)) .saturating_add(self.authorization_list_cost(config)) From 416841cbf4b6e2f3a3c2bfd9566497eb61b880c1 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 17:32:34 +0300 Subject: [PATCH 27/30] refactor: :fire: remove intrinsic cost metrics --- gasometer/src/lib.rs | 50 +++++++++---------------------------- gasometer/src/metrics.rs | 53 ---------------------------------------- 2 files changed, 12 insertions(+), 91 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index d72d39c4..a176cbd0 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -253,14 +253,9 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { - self.inner_mut()?.metrics.init( - zero_data_len, - non_zero_data_len, - access_list_address_len, - access_list_storage_len, - authorization_list_len, - false, - ); + self.inner_mut()? + .metrics + .init(zero_data_len, non_zero_data_len, false); #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call @@ -293,14 +288,9 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { - self.inner_mut()?.metrics.init( - zero_data_len, - non_zero_data_len, - access_list_address_len, - access_list_storage_len, - authorization_list_len, - true, - ); + self.inner_mut()? + .metrics + .init(zero_data_len, non_zero_data_len, true); let mut cost = self.config.gas_transaction_create + calldata_cost(zero_data_len, non_zero_data_len, self.config) @@ -339,19 +329,17 @@ impl<'config> Gasometer<'config> { if self.config.has_eip_7623 { // Any transaction with a gas limit below: // 21000 + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata - // or below its intrinsic gas cost (take the maximum of these two calculations) - // is considered invalid. - if self.gas_limit - < max( - self.config().gas_transaction_call - + self.inner_mut()?.floor_calldata_cost_metrics(), - self.inner_mut()?.intrinsic_cost_metrics(), - ) { + if self.gas() + < self.config().gas_transaction_call + + self.inner_mut()?.floor_calldata_cost_metrics() + { 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); @@ -1064,20 +1052,6 @@ impl Inner<'_> { self.metrics.contract_creation_cost(self.config) } - /// Calculates the intrinsic gas cost for the transaction. - /// - /// The intrinsic cost includes: - /// - Base transaction cost - /// - Calldata costs - /// - Access list costs (per EIP-2930) - /// - Authorization list costs (per EIP-7702) - /// - Contract creation costs if applicable (per EIP-3860) - /// - /// This represents the minimum gas required before any code execution begins. - fn intrinsic_cost_metrics(&mut self) -> u64 { - self.metrics.intrinsic_cost(self.config) - } - /// Gas consumed during transaction execution, excluding base transaction costs, /// calldata costs, and contract creation costs. This value represents /// the actual execution cost only when called from within [`Gasometer::post_execution`]. diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs index 73b09d39..f2f9e0fa 100644 --- a/gasometer/src/metrics.rs +++ b/gasometer/src/metrics.rs @@ -5,9 +5,6 @@ use evm_runtime::Config; pub struct GasMetrics { zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, - access_list_address_len: usize, - access_list_storage_len: usize, - authorization_list_len: usize, is_contract_creation: bool, // Cached values cached_standard_calldata_cost: Option, @@ -15,8 +12,6 @@ pub struct GasMetrics { cached_base_cost: Option, cached_init_code_cost: Option, cached_contract_creation_cost: Option, - cached_access_list_cost: Option, - cached_authorization_list_cost: Option, } impl GasMetrics { @@ -24,17 +19,12 @@ impl GasMetrics { Self { zero_bytes_in_calldata: 0, non_zero_bytes_in_calldata: 0, - access_list_address_len: 0, - access_list_storage_len: 0, - authorization_list_len: 0, is_contract_creation: false, cached_standard_calldata_cost: None, cached_floor_calldata_cost: None, cached_base_cost: None, cached_init_code_cost: None, cached_contract_creation_cost: None, - cached_access_list_cost: None, - cached_authorization_list_cost: None, } } @@ -44,24 +34,16 @@ impl GasMetrics { self.cached_base_cost = None; self.cached_init_code_cost = None; self.cached_contract_creation_cost = None; - self.cached_access_list_cost = None; - self.cached_authorization_list_cost = None; } pub fn init( &mut self, zero_bytes_in_calldata: usize, non_zero_bytes_in_calldata: usize, - access_list_address_len: usize, - access_list_storage_len: usize, - authorization_list_len: usize, is_contract_creation: bool, ) { self.zero_bytes_in_calldata = zero_bytes_in_calldata; self.non_zero_bytes_in_calldata = non_zero_bytes_in_calldata; - self.access_list_address_len = access_list_address_len; - self.access_list_storage_len = access_list_storage_len; - self.authorization_list_len = authorization_list_len; self.is_contract_creation = is_contract_creation; self.invalidate_cache(); } @@ -106,32 +88,6 @@ impl GasMetrics { cost } - fn access_list_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_access_list_cost { - return cached; - } - - let cost = super::access_list_cost( - self.access_list_address_len, - self.access_list_storage_len, - config, - ); - - self.cached_access_list_cost = Some(cost); - cost - } - - fn authorization_list_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_authorization_list_cost { - return cached; - } - - let cost = super::authorization_list_cost(self.authorization_list_len, config); - - self.cached_authorization_list_cost = Some(cost); - cost - } - pub fn init_code_cost(&mut self) -> u64 { if let Some(cached) = self.cached_init_code_cost { return cached; @@ -162,15 +118,6 @@ impl GasMetrics { cost } - /// Intrinsic gas costs of a transaction. - pub fn intrinsic_cost(&mut self, config: &Config) -> u64 { - self.base_cost(config) - .saturating_add(self.standard_calldata_cost(config)) - .saturating_add(self.access_list_cost(config)) - .saturating_add(self.authorization_list_cost(config)) - .saturating_add(self.init_code_cost()) - } - /// Gas consumed during transaction execution, excluding base transaction costs, /// calldata costs, and contract creation costs. This value only represents /// the actual execution cost within post_execution() invocation. From 384725911032616f8886165850eb4dccae193d64 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 3 Sep 2025 17:40:14 +0300 Subject: [PATCH 28/30] test: :white_check_mark: test invalid transaction with gas lower than floor/intrinsic cost --- tests/eip7623.rs | 334 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 332 insertions(+), 2 deletions(-) diff --git a/tests/eip7623.rs b/tests/eip7623.rs index 3d66402b..cdddd38c 100644 --- a/tests/eip7623.rs +++ b/tests/eip7623.rs @@ -4,7 +4,7 @@ use evm::{ gasometer::{call_transaction_cost, create_transaction_cost, Gasometer, TransactionCost}, Config, ExitError, ExitReason, }; -use primitive_types::{H160, U256}; +use primitive_types::{H160, H256, U256}; use std::collections::BTreeMap; // ============================================================================ @@ -906,7 +906,337 @@ mod snapshot_tests { } // ============================================================================ -// Section 7: Integration Tests with Full Transaction Execution +// Section 7: EIP-7623 Gas Limit Validation Tests +// ============================================================================ + +#[cfg(test)] +mod gas_limit_validation_tests { + use super::*; + + #[test] + fn test_gas_limit_validation_passes_with_sufficient_gas() { + let config = create_eip7623_config(); + + // Create calldata that requires floor cost + let data = vec![0xffu8; 100]; // 100 non-zero bytes = 400 tokens * 10 = 4000 floor cost + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Calculate required minimum gas + // Base cost: 21000 + Floor cost: 4000 = 25000 + let required_gas = 25_000; + let gas_limit = required_gas + 1000; // Add some buffer + + let mut gasometer = Gasometer::new(gas_limit, &config); + let result = gasometer.record_transaction(cost); + + assert!( + result.is_ok(), + "Should accept transaction with sufficient gas limit" + ); + } + + #[test] + fn test_gas_limit_validation_fails_below_floor_requirement() { + let config = create_eip7623_config(); + + // Create calldata that requires significant floor cost + let data = vec![0xffu8; 100]; // 100 non-zero bytes = 400 tokens * 10 = 4000 floor cost + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Set gas limit below floor requirement + // Floor requirement: 21000 (base) + 4000 (floor) = 25000 + let insufficient_gas = 24_999; + + let mut gasometer = Gasometer::new(insufficient_gas, &config); + let result = gasometer.record_transaction(cost); + + assert!( + matches!(result, Err(ExitError::OutOfGas)), + "Should reject transaction when gas limit is below floor requirement" + ); + } + + #[test] + fn test_gas_limit_validation_exact_floor_requirement() { + let config = create_eip7623_config(); + + // Create calldata with known floor cost + let data = vec![0xffu8; 50]; // 50 non-zero bytes = 200 tokens * 10 = 2000 floor cost + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Set gas limit to exactly the floor requirement + // Floor requirement: 21000 (base) + 2000 (floor) = 23000 + let exact_gas = 23_000; + + let mut gasometer = Gasometer::new(exact_gas, &config); + let result = gasometer.record_transaction(cost); + + assert!( + result.is_ok(), + "Should accept transaction at exact floor requirement" + ); + } + + #[test] + fn test_gas_limit_validation_with_mixed_calldata() { + let config = create_eip7623_config(); + + // Mixed calldata: 20 zero bytes + 30 non-zero bytes + // Tokens: 20 + (30 * 4) = 140 + // Floor cost: 140 * 10 = 1400 + let mut data = vec![0u8; 20]; + data.extend(vec![0xffu8; 30]); + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Test with insufficient gas (below floor) + let insufficient_gas = 22_000; // 21000 + 999 < 21000 + 1400 + let mut gasometer_fail = Gasometer::new(insufficient_gas, &config); + let result_fail = gasometer_fail.record_transaction(cost.clone()); + + assert!( + matches!(result_fail, Err(ExitError::OutOfGas)), + "Should fail with insufficient gas for mixed calldata" + ); + + // Test with sufficient gas (above floor) + let sufficient_gas = 23_000; // 21000 + 2000 > 21000 + 1400 + let mut gasometer_pass = Gasometer::new(sufficient_gas, &config); + let result_pass = gasometer_pass.record_transaction(cost); + + assert!( + result_pass.is_ok(), + "Should pass with sufficient gas for mixed calldata" + ); + } + + #[test] + fn test_gas_limit_validation_compares_floor_vs_intrinsic() { + let config = create_eip7623_config(); + + // Create calldata where intrinsic cost might be higher than floor cost + // Small calldata with access list to increase intrinsic cost + let data = vec![0xffu8; 10]; // 10 non-zero bytes = 40 tokens * 10 = 400 floor cost + let access_list = vec![( + H160::from_slice(&[1u8; 20]), + vec![H256::from_slice(&[1u8; 32]), H256::from_slice(&[2u8; 32])], + )]; // 1 address + 2 storage keys + + let cost = call_transaction_cost(&data, &access_list, &vec![]); + + // Calculate intrinsic cost: + // Base: 21000 + // Calldata: 10 * 16 = 160 + // Access list: 1 * 2400 + 2 * 1900 = 6200 + // Total intrinsic: 21000 + 160 + 6200 = 27360 + + // Floor cost: 21000 + 400 = 21400 + // max(27360, 21400) = 27360 (intrinsic wins) + + // Test with gas just below intrinsic cost + let insufficient_gas = 27_000; + let mut gasometer_fail = Gasometer::new(insufficient_gas, &config); + let result_fail = gasometer_fail.record_transaction(cost.clone()); + + assert!( + matches!(result_fail, Err(ExitError::OutOfGas)), + "Should fail when below intrinsic cost (even if above floor cost)" + ); + + // Test with gas above intrinsic cost + let sufficient_gas = 28_000; + let mut gasometer_pass = Gasometer::new(sufficient_gas, &config); + let result_pass = gasometer_pass.record_transaction(cost); + + assert!(result_pass.is_ok(), "Should pass when above intrinsic cost"); + } + + #[test] + fn test_gas_limit_validation_disabled_without_eip7623() { + let config = create_pre_eip7623_config(); + + // Create calldata that would fail under EIP-7623 + let data = vec![0xffu8; 1000]; // Large calldata + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Calculate actual intrinsic cost without EIP-7623: + // Base: 21000 + Calldata: 1000 * 16 = 37000 total + let required_gas = 37_000; // Account for actual intrinsic cost + + let mut gasometer = Gasometer::new(required_gas, &config); + let result = gasometer.record_transaction(cost); + + // Should pass because EIP-7623 validation is disabled (no floor cost applied) + assert!( + result.is_ok(), + "Should pass when EIP-7623 is disabled, result: {:?}", + result + ); + } + + #[test] + fn test_gas_limit_validation_empty_calldata() { + let config = create_eip7623_config(); + + // Empty calldata - no floor cost beyond base + let data = vec![]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Just base gas should be sufficient + let minimal_gas = 21_000; + + let mut gasometer = Gasometer::new(minimal_gas, &config); + let result = gasometer.record_transaction(cost); + + assert!(result.is_ok(), "Empty calldata should pass with base gas"); + } + + #[test] + fn test_gas_limit_validation_contract_creation() { + let config = create_eip7623_config(); + + // For contract creation, the validation compares: + // max(21000 + floor_calldata_cost, intrinsic_cost) + // where intrinsic_cost includes the full 53000 creation cost + // + // So for contracts, intrinsic cost will almost always win unless + // we have massive calldata. Let's test both scenarios: + + // Scenario 1: Small calldata where intrinsic cost wins + let small_initcode = vec![0u8; 10]; // 10 zero bytes + let cost_small = create_transaction_cost(&small_initcode, &vec![], &vec![]); + + // Intrinsic: 53000 + 40 + 2 = 53042 (base + calldata + initcode) + // Floor: 21000 + 100 = 21100 (21000 + 10*10) + // Required: max(21100, 53042) = 53042 + + let mut gasometer_small = Gasometer::new(53_000, &config); // Below intrinsic + let result_small = gasometer_small.record_transaction(cost_small); + assert!( + matches!(result_small, Err(ExitError::OutOfGas)), + "Should fail when below intrinsic cost" + ); + + // Scenario 2: Demonstrate floor cost comparison with actual values + // Let's create a scenario where the floor calculation matters + let large_initcode = vec![0u8; 5000]; // 5000 zero bytes = 5000*10 = 50000 floor + let cost_large = create_transaction_cost(&large_initcode, &vec![], &vec![]); + + // Floor comparison: 21000 + 50000 = 71000 + // Intrinsic: 53000 + 20000 + 314 = 73314 (base + calldata + initcode) + // Required: max(71000, 73314) = 73314 + + let mut gasometer_large_fail = Gasometer::new(73_000, &config); // Below requirement + let result_large_fail = gasometer_large_fail.record_transaction(cost_large.clone()); + assert!( + matches!(result_large_fail, Err(ExitError::OutOfGas)), + "Should fail when below required gas for large initcode" + ); + + let mut gasometer_large_pass = Gasometer::new(74_000, &config); // Above requirement + let result_large_pass = gasometer_large_pass.record_transaction(cost_large); + assert!( + result_large_pass.is_ok(), + "Should pass with sufficient gas for large initcode" + ); + } + + #[test] + fn test_gas_limit_validation_edge_case_boundary() { + let config = create_eip7623_config(); + + // Create calldata that results in exact boundary conditions + // Use 21 non-zero bytes: 21 * 4 = 84 tokens * 10 = 840 floor cost + let data = vec![0xffu8; 21]; + let cost = call_transaction_cost(&data, &vec![], &vec![]); + + // Calculate exact requirements + // Base: 21000 + // Standard calldata: 21 * 16 = 336 + // Floor: 84 * 10 = 840 + // Required: max(21000 + 336, 21000 + 840) = 21840 + + let boundary_tests = vec![ + (21_839, false, "one below boundary"), + (21_840, true, "exact boundary"), + (21_841, true, "one above boundary"), + ]; + + for (gas_limit, should_pass, description) in boundary_tests { + let mut gasometer = Gasometer::new(gas_limit, &config); + let result = gasometer.record_transaction(cost.clone()); + + if should_pass { + assert!( + result.is_ok(), + "Should pass {}: gas_limit={}", + description, + gas_limit + ); + } else { + assert!( + matches!(result, Err(ExitError::OutOfGas)), + "Should fail {}: gas_limit={}", + description, + gas_limit + ); + } + } + } + + #[test] + fn test_gas_limit_validation_with_authorization_list() { + let config = create_eip7623_config(); + + // Create transaction with authorization list (EIP-7702) + let data = vec![0xffu8; 50]; // 50 non-zero bytes = 200 tokens * 10 = 2000 floor + let authorization_list = vec![ + ( + U256::from(1), + H160::from_slice(&[1u8; 20]), + U256::from(1), + None, + ), + ( + U256::from(2), + H160::from_slice(&[2u8; 20]), + U256::from(2), + None, + ), + ]; // 2 authorizations * gas_per_empty_account_cost + + let cost = call_transaction_cost(&data, &vec![], &authorization_list); + + // Calculate intrinsic cost including authorization list + // Base: 21000 + // Calldata: 50 * 16 = 800 + // Authorizations: 2 * gas_per_empty_account_cost (25000 each) = 50000 + // Total intrinsic: 21000 + 800 + 50000 = 71800 + + // Floor cost: 21000 + 2000 = 23000 + // max(71800, 23000) = 71800 (intrinsic wins due to authorizations) + + let insufficient_gas = 71_000; + let mut gasometer_fail = Gasometer::new(insufficient_gas, &config); + let result_fail = gasometer_fail.record_transaction(cost.clone()); + + assert!( + matches!(result_fail, Err(ExitError::OutOfGas)), + "Should fail when below intrinsic cost with authorization list" + ); + + let sufficient_gas = 72_000; + let mut gasometer_pass = Gasometer::new(sufficient_gas, &config); + let result_pass = gasometer_pass.record_transaction(cost); + + assert!( + result_pass.is_ok(), + "Should pass when above intrinsic cost with authorization list" + ); + } +} + +// ============================================================================ +// Section 8: Integration Tests with Full Transaction Execution // ============================================================================ #[cfg(test)] From dc1f98408fdaf9eea780a6f3df2b4bd5970558a0 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 5 Sep 2025 15:45:14 +0300 Subject: [PATCH 29/30] refactor: :recycle: wildly simplify the solution --- gasometer/src/lib.rs | 88 ++++++++++---------------- gasometer/src/metrics.rs | 130 --------------------------------------- 2 files changed, 33 insertions(+), 185 deletions(-) delete mode 100644 gasometer/src/metrics.rs diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index a176cbd0..02537b64 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -35,7 +35,6 @@ macro_rules! event { mod consts; mod costs; mod memory; -mod metrics; mod utils; use alloc::vec::Vec; @@ -44,8 +43,6 @@ use evm_core::{ExitError, Opcode, Stack}; use evm_runtime::{Config, Handler}; use primitive_types::{H160, H256, U256}; -use crate::metrics::GasMetrics; - macro_rules! try_or_fail { ( $inner:expr, $e:expr ) => { match $e { @@ -84,9 +81,9 @@ impl<'config> Gasometer<'config> { inner: Ok(Inner { memory_gas: 0, used_gas: 0, + floor_gas: 0, refunded_gas: 0, config, - metrics: GasMetrics::new(), }), } } @@ -245,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, @@ -253,10 +250,6 @@ impl<'config> Gasometer<'config> { access_list_storage_len, authorization_list_len, } => { - self.inner_mut()? - .metrics - .init(zero_data_len, non_zero_data_len, false); - #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call + calldata_cost(zero_data_len, non_zero_data_len, self.config) @@ -266,6 +259,8 @@ impl<'config> Gasometer<'config> { self.config, ) + authorization_list_cost(authorization_list_len, self.config); + 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: {}]", @@ -278,7 +273,7 @@ impl<'config> Gasometer<'config> { authorization_list_len ); - cost + (cost, floor) } TransactionCost::Create { zero_data_len, @@ -288,10 +283,6 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len, } => { - self.inner_mut()? - .metrics - .init(zero_data_len, non_zero_data_len, true); - let mut cost = self.config.gas_transaction_create + calldata_cost(zero_data_len, non_zero_data_len, self.config) + access_list_cost( @@ -304,6 +295,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: {}]", @@ -316,7 +309,8 @@ impl<'config> Gasometer<'config> { initcode_cost, authorization_list_len ); - cost + + (cost, floor) } }; @@ -329,10 +323,7 @@ impl<'config> Gasometer<'config> { 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() - < self.config().gas_transaction_call - + self.inner_mut()?.floor_calldata_cost_metrics() - { + if self.gas() < floor_gas { self.inner = Err(ExitError::OutOfGas); return Err(ExitError::OutOfGas); } @@ -346,6 +337,7 @@ impl<'config> Gasometer<'config> { } self.inner_mut()?.used_gas += gas_cost; + self.inner_mut()?.floor_gas += floor_gas; Ok(()) } @@ -365,16 +357,8 @@ impl<'config> Gasometer<'config> { // Apply EIP-7623 adjustments if self.config.has_eip_7623 { let inner = self.inner_mut()?; - let standard_calldata_cost = inner.standard_calldata_cost_metrics(); - let floor_calldata_cost = inner.floor_calldata_cost_metrics(); - let contract_creation_cost = inner.contract_creation_cost_metrics(); - let execution_cost = inner.execution_cost_metrics(); - let cost = standard_calldata_cost + execution_cost + contract_creation_cost; - let eip_7623_cost = max(cost, floor_calldata_cost); - - // Adjust used_gas to match the target - inner.used_gas -= cost; - inner.used_gas += eip_7623_cost; + + inner.used_gas = max(inner.used_gas, inner.floor_gas); } Ok(()) @@ -461,6 +445,25 @@ pub fn init_code_cost(code_length: u64) -> u64 { 2 * ((code_length + 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)]) -> (usize, usize) { let access_list_address_len = access_list.len(); @@ -875,9 +878,9 @@ pub fn dynamic_opcode_cost( struct Inner<'config> { memory_gas: u64, used_gas: u64, + floor_gas: u64, refunded_gas: i64, config: &'config Config, - metrics: GasMetrics, } impl Inner<'_> { @@ -1033,31 +1036,6 @@ impl Inner<'_> { _ => 0, } } - - /// Standard gas cost for transaction calldata. - /// Valid only after [`GasMetrics::init`] has been called. - fn standard_calldata_cost_metrics(&mut self) -> u64 { - self.metrics.standard_calldata_cost(self.config) - } - - /// Floor gas cost for transaction calldata as defined by EIP-7623. - /// Valid only after [`GasMetrics::init`] has been called. - fn floor_calldata_cost_metrics(&mut self) -> u64 { - self.metrics.floor_calldata_cost(self.config) - } - - /// Gas cost for contract creation. - /// Valid only after [`GasMetrics::init`] has been called with `is_contract_creation` set to true. - fn contract_creation_cost_metrics(&mut self) -> u64 { - self.metrics.contract_creation_cost(self.config) - } - - /// Gas consumed during transaction execution, excluding base transaction costs, - /// calldata costs, and contract creation costs. This value represents - /// the actual execution cost only when called from within [`Gasometer::post_execution`]. - fn execution_cost_metrics(&mut self) -> u64 { - self.metrics.execution_cost(self.used_gas, self.config) - } } /// Gas cost. diff --git a/gasometer/src/metrics.rs b/gasometer/src/metrics.rs deleted file mode 100644 index f2f9e0fa..00000000 --- a/gasometer/src/metrics.rs +++ /dev/null @@ -1,130 +0,0 @@ -use evm_runtime::Config; - -/// Tracks gas parameters for a Gasometer instance. -#[derive(Clone, Debug)] -pub struct GasMetrics { - zero_bytes_in_calldata: usize, - non_zero_bytes_in_calldata: usize, - is_contract_creation: bool, - // Cached values - cached_standard_calldata_cost: Option, - cached_floor_calldata_cost: Option, - cached_base_cost: Option, - cached_init_code_cost: Option, - cached_contract_creation_cost: Option, -} - -impl GasMetrics { - pub fn new() -> Self { - Self { - zero_bytes_in_calldata: 0, - non_zero_bytes_in_calldata: 0, - is_contract_creation: false, - cached_standard_calldata_cost: None, - cached_floor_calldata_cost: None, - cached_base_cost: None, - cached_init_code_cost: None, - cached_contract_creation_cost: None, - } - } - - fn invalidate_cache(&mut self) { - self.cached_standard_calldata_cost = None; - self.cached_floor_calldata_cost = None; - self.cached_base_cost = None; - self.cached_init_code_cost = None; - self.cached_contract_creation_cost = None; - } - - pub fn init( - &mut self, - zero_bytes_in_calldata: usize, - non_zero_bytes_in_calldata: usize, - is_contract_creation: bool, - ) { - self.zero_bytes_in_calldata = zero_bytes_in_calldata; - self.non_zero_bytes_in_calldata = non_zero_bytes_in_calldata; - self.is_contract_creation = is_contract_creation; - self.invalidate_cache(); - } - - pub fn standard_calldata_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_standard_calldata_cost { - return cached; - } - - let cost = super::calldata_cost( - self.zero_bytes_in_calldata, - self.non_zero_bytes_in_calldata, - config, - ); - - self.cached_standard_calldata_cost = Some(cost); - cost - } - - pub fn floor_calldata_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_floor_calldata_cost { - return cached; - } - - let cost = (config.gas_calldata_zero_floor * (self.zero_bytes_in_calldata as u64)) - + (config.gas_calldata_non_zero_floor * (self.non_zero_bytes_in_calldata as u64)); - self.cached_floor_calldata_cost = Some(cost); - cost - } - - fn base_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_base_cost { - return cached; - } - - let cost = if self.is_contract_creation { - config.gas_transaction_create - } else { - config.gas_transaction_call - }; - self.cached_base_cost = Some(cost); - cost - } - - pub fn init_code_cost(&mut self) -> u64 { - if let Some(cached) = self.cached_init_code_cost { - return cached; - } - - let cost = if self.is_contract_creation { - super::init_code_cost( - self.zero_bytes_in_calldata as u64 + self.non_zero_bytes_in_calldata as u64, - ) - } else { - 0 - }; - self.cached_init_code_cost = Some(cost); - cost - } - - pub fn contract_creation_cost(&mut self, config: &Config) -> u64 { - if let Some(cached) = self.cached_contract_creation_cost { - return cached; - } - - let cost = if self.is_contract_creation { - (config.gas_transaction_create - config.gas_transaction_call) + self.init_code_cost() - } else { - 0 - }; - self.cached_contract_creation_cost = Some(cost); - cost - } - - /// Gas consumed during transaction execution, excluding base transaction costs, - /// calldata costs, and contract creation costs. This value only represents - /// the actual execution cost within post_execution() invocation. - pub fn execution_cost(&mut self, used_gas: u64, config: &Config) -> u64 { - used_gas - .saturating_sub(self.base_cost(config)) - .saturating_sub(self.init_code_cost()) - .saturating_sub(self.standard_calldata_cost(config)) - } -} From 9212a7aad2da27ddbd65a6a48d9002dddccc9f98 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 5 Sep 2025 16:01:35 +0300 Subject: [PATCH 30/30] revert: :rewind: remove refactoring --- gasometer/src/lib.rs | 54 ++++++++-------------------------- src/executor/stack/executor.rs | 2 +- 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 02537b64..679163ee 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -252,12 +252,11 @@ impl<'config> Gasometer<'config> { } => { #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call - + calldata_cost(zero_data_len, non_zero_data_len, self.config) - + access_list_cost( - access_list_address_len, - access_list_storage_len, - self.config, - ) + authorization_list_cost(authorization_list_len, self.config); + + zero_data_len as u64 * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + + 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); @@ -284,13 +283,11 @@ impl<'config> Gasometer<'config> { authorization_list_len, } => { let mut cost = self.config.gas_transaction_create - + calldata_cost(zero_data_len, non_zero_data_len, self.config) - + access_list_cost( - access_list_address_len, - access_list_storage_len, - self.config, - ) + authorization_list_cost(authorization_list_len, self.config); - + + zero_data_len as u64 * self.config.gas_transaction_zero_data + + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + + 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; if self.config.max_initcode_size.is_some() { cost += initcode_cost; } @@ -401,7 +398,7 @@ pub fn create_transaction_cost( // Per EIP-7702: Initially charge PER_EMPTY_ACCOUNT_COST for all authorizations // Non-empty accounts will be refunded later when we have access to account state let authorization_list_len = authorization_list.len(); - let initcode_cost = init_code_cost(data.len() as u64); + let initcode_cost = init_code_cost(data); TransactionCost::Create { zero_data_len, @@ -413,36 +410,11 @@ pub fn create_transaction_cost( } } -/// Calculate total calldata cost (zero + non-zero bytes) -pub fn calldata_cost( - zero_bytes_in_calldata: usize, - non_zero_bytes_in_calldata: usize, - config: &Config, -) -> u64 { - zero_bytes_in_calldata as u64 * config.gas_transaction_zero_data - + non_zero_bytes_in_calldata as u64 * config.gas_transaction_non_zero_data -} - -/// Calculate total access list cost (addresses + storage keys) -pub fn access_list_cost( - access_list_address_len: usize, - access_list_storage_len: usize, - config: &Config, -) -> u64 { - access_list_address_len as u64 * config.gas_access_list_address - + access_list_storage_len as u64 * config.gas_access_list_storage_key -} - -/// Calculate authorization list cost -pub fn authorization_list_cost(authorization_list_len: usize, config: &Config) -> u64 { - authorization_list_len as u64 * config.gas_per_empty_account_cost -} - -pub fn init_code_cost(code_length: u64) -> u64 { +pub fn init_code_cost(data: &[u8]) -> u64 { // As per EIP-3860: // > We define initcode_cost(initcode) to equal INITCODE_WORD_COST * ceil(len(initcode) / 32). // where INITCODE_WORD_COST is 2. - 2 * ((code_length + 31) / 32) + 2 * ((data.len() as u64 + 31) / 32) } pub fn floor_gas( diff --git a/src/executor/stack/executor.rs b/src/executor/stack/executor.rs index a541644d..366dc9aa 100644 --- a/src/executor/stack/executor.rs +++ b/src/executor/stack/executor.rs @@ -453,7 +453,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> .state .metadata_mut() .gasometer - .record_cost(gasometer::init_code_cost(init_code.len() as u64)); + .record_cost(gasometer::init_code_cost(init_code)); } Ok(()) }