From de544b14037cfbbc3b5aaa9531ed2cde22f64647 Mon Sep 17 00:00:00 2001 From: Edgar Luque Date: Tue, 28 Apr 2026 17:56:02 +0200 Subject: [PATCH 1/2] chore(l1): sync EIP-8037 implementation with PR 11573 - pin CPSB at 1174 (drop dead dynamic-formula consts and update doc) - rename STATE_BYTES_PER_AUTH_ONLY -> STATE_BYTES_PER_AUTH_BASE; drop STATE_BYTES_PER_AUTH_TOTAL (compute as NEW_ACCOUNT + AUTH_BASE inline) - add SYSTEM_MAX_SSTORES_PER_CALL=16, SYS_CALL_STATE_GAS_RESERVOIR=601_088, SYS_CALL_GAS_LIMIT_AMSTERDAM=30_601_088; place the extra in state_gas_reservoir for system calls on Amsterdam+ and bypass TX_MAX_GAS_LIMIT for them - replace dynamic-formula CPSB unit tests with a fixed-1174 assertion --- crates/vm/backends/levm/mod.rs | 21 +++++++-- crates/vm/levm/src/constants.rs | 12 +++++ crates/vm/levm/src/gas_cost.rs | 24 +++++----- crates/vm/levm/src/state_diff.rs | 21 +++++---- crates/vm/levm/src/utils.rs | 56 +++++++++++++++-------- test/tests/levm/eip8037_tests.rs | 78 +++++++++++--------------------- 6 files changed, 116 insertions(+), 96 deletions(-) diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index 9ed80d15e0..2fd0962867 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -31,8 +31,8 @@ use ethrex_levm::EVMConfig; use ethrex_levm::account::{AccountStatus, LevmAccount}; use ethrex_levm::call_frame::Stack; use ethrex_levm::constants::{ - POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, TX_BASE_COST, - TX_MAX_GAS_LIMIT_AMSTERDAM, + POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, SYS_CALL_GAS_LIMIT_AMSTERDAM, + TX_BASE_COST, TX_MAX_GAS_LIMIT_AMSTERDAM, }; use ethrex_levm::db::Database; use ethrex_levm::db::gen_db::{CacheDB, GeneralizedDatabase}; @@ -2526,11 +2526,22 @@ pub fn generic_system_contract_levm( .current_accounts_state .get(&block_header.coinbase) .cloned(); + // EIPs 2935, 4788, 7002 and 7251 give system calls a fixed 30M execution budget + // and they do not pay intrinsic gas. From Amsterdam, EIP-8037 (ethereum/EIPs#11573) + // additionally reserves `STATE_BYTES_PER_STORAGE_SET × CPSB × SYSTEM_MAX_SSTORES_PER_CALL` + // for state-creation in `state_gas_reservoir`, lifting the total budget to + // `SYS_CALL_GAS_LIMIT_AMSTERDAM`. The reservoir split is performed in + // `prepare_execution` keyed on `is_system_call`. + let sys_call_gas_limit = if config.fork >= Fork::Amsterdam { + SYS_CALL_GAS_LIMIT_AMSTERDAM + } else { + SYS_CALL_GAS_LIMIT + }; let env = Environment { origin: system_address, - // EIPs 2935, 4788, 7002 and 7251 dictate that the system calls have a gas limit of 30 million and they do not use intrinsic gas. - // So we add the base cost that will be taken in the execution. - gas_limit: SYS_CALL_GAS_LIMIT + TX_BASE_COST, + // We add TX_BASE_COST because intrinsic gas (which includes it) is consumed + // during execution. + gas_limit: sys_call_gas_limit + TX_BASE_COST, block_number: block_header.number, coinbase: block_header.coinbase, timestamp: block_header.timestamp, diff --git a/crates/vm/levm/src/constants.rs b/crates/vm/levm/src/constants.rs index a533863eeb..c9b334cdbc 100644 --- a/crates/vm/levm/src/constants.rs +++ b/crates/vm/levm/src/constants.rs @@ -20,6 +20,18 @@ pub const MEMORY_EXPANSION_QUOTIENT: u64 = 512; // Dedicated gas limit for system calls according to EIPs 2935, 4788, 7002 and 7251 pub const SYS_CALL_GAS_LIMIT: u64 = 30000000; +// EIP-8037 (ethereum/EIPs#11573): per-fork system-call gas limit. From Amsterdam, system +// calls receive an extra `STATE_BYTES_PER_STORAGE_SET * CPSB * SYSTEM_MAX_SSTORES_PER_CALL` +// budget on top of the legacy 30M, placed in `state_gas_reservoir`. This preserves the +// existing 30M execution margin under the higher state-creation costs. +// +// 30_000_000 + 32 * 1174 * 16 = 30_601_088 +pub const SYS_CALL_GAS_LIMIT_AMSTERDAM: u64 = SYS_CALL_GAS_LIMIT + SYS_CALL_STATE_GAS_RESERVOIR; +// Extra state-gas budget reserved for system contracts that perform up to +// SYSTEM_MAX_SSTORES_PER_CALL (=16) new SSTOREs per invocation (matches +// MAX_WITHDRAWAL_REQUESTS_PER_BLOCK from EIP-7002). +pub const SYS_CALL_STATE_GAS_RESERVOIR: u64 = 601_088; // 32 * 1174 * 16 + // Transaction costs in gas pub const TX_BASE_COST: u64 = 21000; diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index 4303916fe6..6f00368cbe 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -164,22 +164,20 @@ pub const CREATE_BASE_COST: u64 = 32000; // EIP-8037: Multidimensional gas for state creation (Amsterdam only) pub const STATE_BYTES_PER_NEW_ACCOUNT: u64 = 112; pub const STATE_BYTES_PER_STORAGE_SET: u64 = 32; -pub const STATE_BYTES_PER_AUTH_TOTAL: u64 = 135; // 112 account + 23 auth-specific -pub const STATE_BYTES_PER_AUTH_ONLY: u64 = 23; // auth-specific delta when authority pre-existed (downgrade) +pub const STATE_BYTES_PER_AUTH_BASE: u64 = 23; -// EIP-8037: Dynamic cost_per_state_byte formula constants (execution-specs#2687) -pub const BLOCKS_PER_YEAR: u64 = 2_628_000; -pub const TARGET_STATE_GROWTH_PER_YEAR: u64 = 100 * (1u64 << 30); // 100 GiB -pub const CPSB_SIGNIFICANT_BITS: u32 = 5; -pub const CPSB_OFFSET: u64 = 9578; +// EIP-8037: Per-system-call upper bound on new storage slots written. Matches +// MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the largest per-block bound across +// the existing system contracts. +pub const SYSTEM_MAX_SSTORES_PER_CALL: u64 = 16; -/// Compute cost_per_state_byte from the block gas limit (EIP-8037, execution-specs#2687). +/// Cost per state byte for EIP-8037 (ethereum/EIPs#11573). /// -/// TEMPORARY for bal-devnet-4: returns the fixed value 1174 used by bal-devnet-3 -/// regardless of `block_gas_limit`. The dynamic formula (BLOCKS_PER_YEAR / -/// TARGET_STATE_GROWTH_PER_YEAR / CPSB_SIGNIFICANT_BITS / CPSB_OFFSET) is preserved -/// in the consts above so this commit can be reverted with a single `git revert` to -/// restore the formula body. See execution-specs#2687. +/// Pinned at 1174, the value derived from a 100 GiB/year state-growth target at a +/// 96M-gas reference block limit. The original draft computed this dynamically from +/// the block gas limit; PR 11573 collapses that to a single fixed constant, so +/// `block_gas_limit` is no longer an input. The argument is retained to keep call +/// sites stable; if a future EIP re-derives CPSB, change the body. pub fn cost_per_state_byte(_block_gas_limit: u64) -> u64 { 1174 } diff --git a/crates/vm/levm/src/state_diff.rs b/crates/vm/levm/src/state_diff.rs index d4ad4ee961..8e2bfb881f 100644 --- a/crates/vm/levm/src/state_diff.rs +++ b/crates/vm/levm/src/state_diff.rs @@ -16,9 +16,12 @@ pub struct StateDiff { pub new_storage_slots: FxHashSet<(Address, H256)>, /// Per-address deployed-code byte counts (charged at code-deposit step in CREATE). pub code_deposits: FxHashMap, - /// EIP-7702 auth-total entries: authority address → 135 bytes (full new-account+auth charge). + /// EIP-7702 auth-total entries: authority address → + /// `STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE` bytes + /// (full new-account + auth-base charge, authority did not pre-exist). pub auth_total: FxHashSet
, - /// EIP-7702 auth-only entries: authority address → 23 bytes (downgraded — authority pre-existed). + /// EIP-7702 auth-only entries: authority address → `STATE_BYTES_PER_AUTH_BASE` bytes + /// (downgraded — authority pre-existed). pub auth_only: FxHashSet
, /// Cross-frame cancellations: storage slots cleared (N→0) but created in an ancestor. @@ -37,9 +40,13 @@ impl StateDiff { )] pub fn bytes(&self) -> u64 { use crate::gas_cost::{ - STATE_BYTES_PER_AUTH_ONLY, STATE_BYTES_PER_AUTH_TOTAL, STATE_BYTES_PER_NEW_ACCOUNT, - STATE_BYTES_PER_STORAGE_SET, + STATE_BYTES_PER_AUTH_BASE, STATE_BYTES_PER_NEW_ACCOUNT, STATE_BYTES_PER_STORAGE_SET, }; + // EIP-8037 ethereum/EIPs#11573: a fresh 7702 authorization charges + // STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE; an authority that + // pre-existed downgrades to STATE_BYTES_PER_AUTH_BASE alone. + let auth_total_bytes = + STATE_BYTES_PER_NEW_ACCOUNT.saturating_add(STATE_BYTES_PER_AUTH_BASE); (self.new_accounts.len() as u64) .saturating_mul(STATE_BYTES_PER_NEW_ACCOUNT) @@ -52,10 +59,8 @@ impl StateDiff { .copied() .fold(0u64, u64::saturating_add), ) - .saturating_add( - (self.auth_total.len() as u64).saturating_mul(STATE_BYTES_PER_AUTH_TOTAL), - ) - .saturating_add((self.auth_only.len() as u64).saturating_mul(STATE_BYTES_PER_AUTH_ONLY)) + .saturating_add((self.auth_total.len() as u64).saturating_mul(auth_total_bytes)) + .saturating_add((self.auth_only.len() as u64).saturating_mul(STATE_BYTES_PER_AUTH_BASE)) } // ------------------------------------------------------------------------- diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 7b89c7fd88..34056a6e3a 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -8,7 +8,7 @@ use crate::{ gas_cost::{ self, ACCESS_LIST_ADDRESS_COST, ACCESS_LIST_STORAGE_KEY_COST, BLOB_GAS_PER_BLOB, COLD_ADDRESS_ACCESS_COST, CREATE_BASE_COST, REGULAR_GAS_CREATE, STANDARD_TOKEN_COST, - STATE_BYTES_PER_AUTH_TOTAL, STATE_BYTES_PER_NEW_ACCOUNT, WARM_ADDRESS_ACCESS_COST, + STATE_BYTES_PER_AUTH_BASE, STATE_BYTES_PER_NEW_ACCOUNT, WARM_ADDRESS_ACCESS_COST, cost_per_state_byte, floor_tokens_in_access_list, total_cost_floor_per_token, }, vm::{Substate, VM}, @@ -284,8 +284,9 @@ impl<'a> VM<'a> { // or any other validation in this function never reach the // `record_auth_downgrade_to_only` call below. They remain in `auth_total` in // state_diff_intrinsic_seed (seeded by add_intrinsic_gas), matching legacy behavior - // where invalid tuples still cost the full STATE_BYTES_PER_AUTH_TOTAL (135 bytes) - // intrinsic state gas. + // where invalid tuples still cost the full new-account + auth-base charge + // (STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE = 135 bytes) of intrinsic + // state gas. let mut refunded_gas: u64 = 0; // IMPORTANT: // If any of the below steps fail, immediately stop processing that tuple and continue to the next tuple in the list. It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence. @@ -451,14 +452,25 @@ impl<'a> VM<'a> { // execution_gas = what remains after all intrinsic gas; regular_gas_budget = how much // regular execution gas is allowed (capped at TX_MAX_GAS_LIMIT_AMSTERDAM); the difference becomes // the reservoir for drawing state gas without consuming regular gas_remaining. + // + // System calls (EIPs 2935, 4788, 7002, 7251) bypass EIP-7825's TX_MAX_GAS_LIMIT cap and + // instead receive a fixed split per ethereum/EIPs#11573: + // gas_left = SYS_CALL_GAS_LIMIT (the legacy 30M execution budget) + // state_gas_reservoir = STATE_BYTES_PER_STORAGE_SET * CPSB * SYSTEM_MAX_SSTORES_PER_CALL + // The caller (`generic_system_contract_levm`) sets `env.gas_limit` to the sum of both + // plus TX_BASE_COST, so we just split `execution_gas` here. if self.env.config.fork >= Fork::Amsterdam { let gas_limit = self.tx.gas_limit(); let execution_gas = gas_limit.saturating_sub(total_gas); - let regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM.saturating_sub(regular_gas); - let gas_left = regular_gas_budget.min(execution_gas); - let reservoir = execution_gas.saturating_sub(gas_left); + let reservoir = if self.env.is_system_call { + execution_gas.saturating_sub(SYS_CALL_GAS_LIMIT) + } else { + let regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM.saturating_sub(regular_gas); + let gas_left = regular_gas_budget.min(execution_gas); + execution_gas.saturating_sub(gas_left) + }; if reservoir > 0 { - // Pre-consume reservoir from gas_remaining so GAS opcode returns <= TX_MAX_GAS_LIMIT_AMSTERDAM + // Pre-consume reservoir from gas_remaining so GAS opcode returns gas_left only. let reservoir_i64 = i64::try_from(reservoir).map_err(|_| InternalError::Overflow)?; self.current_call_frame.gas_remaining = self @@ -568,14 +580,17 @@ impl<'a> VM<'a> { }; if fork >= Fork::Amsterdam { - // EIP-8037: per-auth regular cost is PER_AUTH_BASE_COST, state is STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte + // EIP-8037 (ethereum/EIPs#11573): per-auth regular cost is PER_AUTH_BASE_COST; + // state cost assumes account creation, i.e. + // (STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) * cost_per_state_byte. let regular_auth_cost = PER_AUTH_BASE_COST .checked_mul(amount_of_auth_tuples) .ok_or(InternalError::Overflow)?; regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?; #[expect(clippy::arithmetic_side_effects, reason = "bounded constants")] - let auth_total_state_gas = - gas_cost::STATE_BYTES_PER_AUTH_TOTAL * self.cost_per_state_byte; + let auth_total_state_gas = (gas_cost::STATE_BYTES_PER_NEW_ACCOUNT + + gas_cost::STATE_BYTES_PER_AUTH_BASE) + * self.cost_per_state_byte; let state_auth_cost = auth_total_state_gas .checked_mul(amount_of_auth_tuples) .ok_or(InternalError::Overflow)?; @@ -689,14 +704,19 @@ pub fn intrinsic_gas_dimensions( let (state_gas_new_account, state_gas_auth_total) = if fork >= Fork::Amsterdam { let cpsb = cost_per_state_byte(block_gas_limit); - ( - STATE_BYTES_PER_NEW_ACCOUNT - .checked_mul(cpsb) - .ok_or(InternalError::Overflow)?, - STATE_BYTES_PER_AUTH_TOTAL - .checked_mul(cpsb) - .ok_or(InternalError::Overflow)?, - ) + let state_gas_new_account = STATE_BYTES_PER_NEW_ACCOUNT + .checked_mul(cpsb) + .ok_or(InternalError::Overflow)?; + // EIP-8037 (ethereum/EIPs#11573): a fresh authorization charges + // STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE bytes; the + // per-byte cost is the same constant CPSB. + let auth_total_bytes = STATE_BYTES_PER_NEW_ACCOUNT + .checked_add(STATE_BYTES_PER_AUTH_BASE) + .ok_or(InternalError::Overflow)?; + let state_gas_auth_total = auth_total_bytes + .checked_mul(cpsb) + .ok_or(InternalError::Overflow)?; + (state_gas_new_account, state_gas_auth_total) } else { (0, 0) }; diff --git a/test/tests/levm/eip8037_tests.rs b/test/tests/levm/eip8037_tests.rs index 4607bc6a4d..627c963dfa 100644 --- a/test/tests/levm/eip8037_tests.rs +++ b/test/tests/levm/eip8037_tests.rs @@ -1,4 +1,9 @@ -//! EIP-8037: Dynamic cost_per_state_byte Tests +//! EIP-8037: cost_per_state_byte (CPSB) tests. +//! +//! Per ethereum/EIPs#11573, CPSB is a fixed constant (1174). The original draft +//! defined a dynamic, block-gas-limit-dependent formula with quantization; that +//! mechanism has been removed from the spec, so we only assert the fixed value +//! here. //! //! Also covers parity between the standalone `intrinsic_gas_dimensions` //! helper (used by mempool / payload builder) and `VM::get_intrinsic_gas` @@ -26,58 +31,27 @@ use ethrex_levm::{ use rustc_hash::FxHashMap; use std::sync::Arc; -/// Sanity check: cost_per_state_byte(120_000_000) == 1174 -/// (matches the legacy hardcoded COST_PER_STATE_BYTE constant) -#[test] -fn test_cpsb_120m() { - assert_eq!(cost_per_state_byte(120_000_000), 1174); -} - -/// gas_limit = 30_000_000 -/// num = 30_000_000 * 2_628_000 = 78_840_000_000_000 -/// denom = 2 * 100 * 2^30 = 214_748_364_800 -/// raw = ceil(78_840_000_000_000 / 214_748_364_800) = 368 -/// shifted = 368 + 9578 = 9946 -/// bit_length = 14, shift = 9 -/// quantized = (9946 >> 9) << 9 = 19 * 512 = 9728 -/// result = 9728 - 9578 = 150 -#[test] -#[ignore = "bal-devnet-4: cost_per_state_byte temporarily fixed to 1174; re-enable when dynamic formula is restored"] -fn test_cpsb_30m() { - assert_eq!(cost_per_state_byte(30_000_000), 150); -} - -/// gas_limit = 500_000_000 -/// raw = ceil(500_000_000 * 2_628_000 / 214_748_364_800) = 6119 -/// shifted = 6119 + 9578 = 15697 -/// bit_length = 14, shift = 9 -/// quantized = (15697 >> 9) << 9 = 30 * 512 = 15360 -/// result = 15360 - 9578 = 5782 +/// CPSB is a fixed constant of 1174 (ethereum/EIPs#11573). The block-gas-limit +/// argument is retained in the function signature for forward-compatibility but +/// must not influence the result. #[test] -#[ignore = "bal-devnet-4: cost_per_state_byte temporarily fixed to 1174; re-enable when dynamic formula is restored"] -fn test_cpsb_500m() { - assert_eq!(cost_per_state_byte(500_000_000), 5782); -} - -/// Low-end clamp: formula produces `quantized <= CPSB_OFFSET`, so the function -/// returns 1 (the minimum viable cost). Guard against an off-by-one in the -/// `if quantized > CPSB_OFFSET` branch. -#[test] -#[ignore = "bal-devnet-4: cost_per_state_byte temporarily fixed to 1174; re-enable when dynamic formula is restored"] -fn test_cpsb_clamp_to_one_for_tiny_gas_limit() { - assert_eq!(cost_per_state_byte(1), 1); - assert_eq!(cost_per_state_byte(5_000_000), 1); -} - -/// Upper boundary of the 30M quantization bin — `cpsb(14_999_999)` must not -/// jump across the next bin's value just because `raw` changes by 1. All -/// gas_limits in the 5M–30M range quantize to 150. -#[test] -#[ignore = "bal-devnet-4: cost_per_state_byte temporarily fixed to 1174; re-enable when dynamic formula is restored"] -fn test_cpsb_30m_bin_boundary() { - assert_eq!(cost_per_state_byte(14_999_999), 150); - assert_eq!(cost_per_state_byte(15_000_000), 150); - assert_eq!(cost_per_state_byte(29_999_999), 150); +fn test_cpsb_is_fixed_at_1174() { + for gas_limit in [ + 1u64, + 5_000_000, + 29_999_999, + 30_000_000, + 96_000_000, + 120_000_000, + 500_000_000, + u64::MAX, + ] { + assert_eq!( + cost_per_state_byte(gas_limit), + 1174, + "CPSB must be fixed at 1174 regardless of block gas limit (input: {gas_limit})", + ); + } } // ==================== intrinsic_gas_dimensions parity ==================== From 167a0a7b264ef33037a905edc15323ba5e910068 Mon Sep 17 00:00:00 2001 From: Edgar Luque Date: Tue, 28 Apr 2026 18:11:00 +0200 Subject: [PATCH 2/2] fix(l1): read env.gas_limit for system-call reservoir setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review on PR #6546. Two issues: 1. prepare_execution read self.tx.gas_limit() to compute the reservoir. For system calls the tx is built from EIP1559Transaction::default(), so tx.gas_limit() = 0 and the reservoir was always 0 — the extra 601k we added to env.gas_limit just sat in gas_remaining as unreserved execution gas instead of being separated into state_gas_reservoir. Read env.gas_limit on the system-call branch (and keep tx.gas_limit() on the user-tx branch so eth_call/simulate paths, where env.gas_limit defaults to the block max, are unaffected). 2. SYSTEM_MAX_SSTORES_PER_CALL lived in gas_cost.rs while the 601_088 reservoir literal lived in constants.rs, decoupled. Move SYSTEM_MAX_SSTORES_PER_CALL to constants.rs and derive SYS_CALL_STATE_GAS_RESERVOIR symbolically so the two stay in lockstep. --- crates/vm/levm/src/constants.rs | 23 ++++++++++++++++------- crates/vm/levm/src/gas_cost.rs | 5 ----- crates/vm/levm/src/utils.rs | 13 ++++++++++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/crates/vm/levm/src/constants.rs b/crates/vm/levm/src/constants.rs index c9b334cdbc..600a1cdf3e 100644 --- a/crates/vm/levm/src/constants.rs +++ b/crates/vm/levm/src/constants.rs @@ -20,17 +20,26 @@ pub const MEMORY_EXPANSION_QUOTIENT: u64 = 512; // Dedicated gas limit for system calls according to EIPs 2935, 4788, 7002 and 7251 pub const SYS_CALL_GAS_LIMIT: u64 = 30000000; -// EIP-8037 (ethereum/EIPs#11573): per-fork system-call gas limit. From Amsterdam, system -// calls receive an extra `STATE_BYTES_PER_STORAGE_SET * CPSB * SYSTEM_MAX_SSTORES_PER_CALL` -// budget on top of the legacy 30M, placed in `state_gas_reservoir`. This preserves the +// EIP-8037 (ethereum/EIPs#11573): per-system-call upper bound on new storage slots +// written. Matches MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the largest per-block +// bound across the existing system contracts. +pub const SYSTEM_MAX_SSTORES_PER_CALL: u64 = 16; + +// EIP-8037: extra state-gas budget reserved for system contracts that perform up to +// SYSTEM_MAX_SSTORES_PER_CALL new SSTOREs per invocation. Derived symbolically from +// the EIP-8037 byte constant so the bound stays in lockstep with +// SYSTEM_MAX_SSTORES_PER_CALL. The CPSB factor (1174) is inlined here because +// `cost_per_state_byte` is a runtime fn; if CPSB ever changes, this constant and +// `cost_per_state_byte` must move together. +pub const SYS_CALL_STATE_GAS_RESERVOIR: u64 = + crate::gas_cost::STATE_BYTES_PER_STORAGE_SET * 1174 * SYSTEM_MAX_SSTORES_PER_CALL; + +// EIP-8037: From Amsterdam, system calls receive the legacy 30M execution budget plus +// SYS_CALL_STATE_GAS_RESERVOIR placed in `state_gas_reservoir`. This preserves the // existing 30M execution margin under the higher state-creation costs. // // 30_000_000 + 32 * 1174 * 16 = 30_601_088 pub const SYS_CALL_GAS_LIMIT_AMSTERDAM: u64 = SYS_CALL_GAS_LIMIT + SYS_CALL_STATE_GAS_RESERVOIR; -// Extra state-gas budget reserved for system contracts that perform up to -// SYSTEM_MAX_SSTORES_PER_CALL (=16) new SSTOREs per invocation (matches -// MAX_WITHDRAWAL_REQUESTS_PER_BLOCK from EIP-7002). -pub const SYS_CALL_STATE_GAS_RESERVOIR: u64 = 601_088; // 32 * 1174 * 16 // Transaction costs in gas pub const TX_BASE_COST: u64 = 21000; diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index 6f00368cbe..7c06032898 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -166,11 +166,6 @@ pub const STATE_BYTES_PER_NEW_ACCOUNT: u64 = 112; pub const STATE_BYTES_PER_STORAGE_SET: u64 = 32; pub const STATE_BYTES_PER_AUTH_BASE: u64 = 23; -// EIP-8037: Per-system-call upper bound on new storage slots written. Matches -// MAX_WITHDRAWAL_REQUESTS_PER_BLOCK (EIP-7002), the largest per-block bound across -// the existing system contracts. -pub const SYSTEM_MAX_SSTORES_PER_CALL: u64 = 16; - /// Cost per state byte for EIP-8037 (ethereum/EIPs#11573). /// /// Pinned at 1174, the value derived from a 100 GiB/year state-growth target at a diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 34056a6e3a..c0ce52bf1f 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -460,7 +460,18 @@ impl<'a> VM<'a> { // The caller (`generic_system_contract_levm`) sets `env.gas_limit` to the sum of both // plus TX_BASE_COST, so we just split `execution_gas` here. if self.env.config.fork >= Fork::Amsterdam { - let gas_limit = self.tx.gas_limit(); + // System calls have `tx.gas_limit() == 0` (the tx is built from + // `EIP1559Transaction::default()` in `generic_system_contract_levm`); their + // budget lives in `env.gas_limit` instead. For ordinary user txs the two + // agree (set together at the env-builder), but for `eth_call`/simulation + // paths `env.gas_limit` may default to the block max while the wrapping + // `tx` has `gas_limit = 0`. So we read whichever source carries the budget + // for the path we're on. + let gas_limit = if self.env.is_system_call { + self.env.gas_limit + } else { + self.tx.gas_limit() + }; let execution_gas = gas_limit.saturating_sub(total_gas); let reservoir = if self.env.is_system_call { execution_gas.saturating_sub(SYS_CALL_GAS_LIMIT)