Skip to content

Commit aa13336

Browse files
authored
feat(eip8037): Amsterdam bal-devnet-7 (#3667)
* feat(gas): EIP-8037 issue #2 — 0→x→0 storage reservoir refill (#3581) * feat(gas): implement EIP-8037 issue #2 — 0→x→0 storage reservoir refill When a storage slot is restored to its original zero value within the same transaction, the state gas originally charged for the 0→x transition is now returned directly to the reservoir rather than routed through the capped (spent/5) refund counter. This preserves the EIP-8037 invariant that state gas consumption correlates with actual state creation. Changes: - `GasTracker.state_gas_spent` becomes `i64`. A child frame's count can legitimately go negative when it clears a slot that the parent had set; the net is reconciled on frame return. - New `GasTracker::refill_reservoir(amount)` and `Gas::refill_reservoir` wrappers: add to reservoir, subtract from `state_gas_spent`. - New `GasParams::sstore_state_gas_refill` returns `32 * CPSB` only on 0→x→0 restoration. - SSTORE instruction calls `refill_reservoir` when EIP-8037 is enabled and the restoration condition matches. - EIP-8037 table: `sstore_set_refund` drops from `32 * CPSB + 2800` to `2800`; the state portion now flows through the reservoir refill path rather than the refund counter. - `handle_reservoir_remaining_gas` (frame.rs) uses saturating i64 math on both success and revert/halt paths so the parent's matching 0→x charge is correctly netted out on success, and the combined `state_gas_spent + reservoir` is clamped to 0 on failure. - Top-frame reconciliation (handler.rs) and `build_result_gas` (post_execution.rs) clamp to 0 before the public `u64` surface. - Precompile + inspector plumbing updated for the i64 switch. Test updated: `test_eip8037_sstore_set_then_clear_refund` now asserts `state_gas_spent == 0` and that total gas spent matches the baseline (was asserting +200k and a strict `>` inequality). Golden JSON regenerated. * fix(ee-tests): adapt custom opcode test to new insert_instruction signature `insert_instruction` gained a `gas: u16` parameter in the instruction-table refactor (#3561), and `Instruction::new` now takes only the fn. Update the DOUBLE opcode registration in `revm_tests.rs` so the ee-tests crate compiles. * test(eip8037): add same-frame and cross-frame 0→x→0 restoration tests Covers the two EIP-8037 issue #2 code paths: - `test_eip8037_sstore_refill_same_frame`: 0→1→0 within one frame ends with `state_gas_spent == 0`; compared against a set-only variant that retains the full `STATE_GAS_SSTORE_SET` charge. - `test_eip8037_sstore_refill_cross_frame`: parent SSTORE(0,1), then DELEGATECALLs a child that does SSTORE(0,0). The refill fires in the child frame (driving child `state_gas_spent` negative); on frame return the parent's +200k and child's -200k net out, leaving only the CREATE + code-deposit state gas on the books. * feat(gas): EIP-7976 — increase calldata floor cost to 64/64 (#3597) * feat(gas): EIP-7976 — increase calldata floor cost to 16/64 gas per byte Introduces a new `tx_floor_token_zero_byte_weight` GasId so the floor tokens formula becomes `zero × floor_zero_weight + nonzero × tx_token_non_zero_byte_multiplier`. Under EIP-7623 (Prague) the weight stays at 1 — reproducing the existing `tokens_in_calldata`-based floor. Under EIP-7976 (Amsterdam) the weight is raised to `tx_token_non_zero_byte_multiplier`, which yields `floor_tokens_in_calldata = (zero + nonzero) × 4` and, together with the per-token bump from 10 to 16, a uniform 64 gas/byte floor. * docs: correct EIP-7976 floor cost comment (64/64, not 16/64) * perf(gas): skip zero-byte scan when floor weights match When (Amsterdam / EIP-7976), every calldata byte contributes the same number of floor tokens. In that case we can use `input.len() * weight` directly and skip the filter+count pass over the calldata. * refactor(gas): rename tx_floor_token_zero_byte_weight to _multiplier Matches the naming of `tx_token_non_zero_byte_multiplier`. No behavior change. * refactor(gas): reuse get_tokens_in_calldata in tx_floor_cost The EIP-7623 branch (zero-byte multiplier = 1) produces exactly `zero + nonzero * non_zero_multiplier`, which is what get_tokens_in_calldata already computes. Delegate to it instead of re-implementing the scan inline. The EIP-7976 uniform path keeps its `input.len() * multiplier` shortcut. * refactor(gas): extract tx_floor_cost_with_tokens helper * feat(gas): implement EIP-7981 access list cost increase (#3598) * feat(gas): EIP-7976 — increase calldata floor cost to 16/64 gas per byte Introduces a new `tx_floor_token_zero_byte_weight` GasId so the floor tokens formula becomes `zero × floor_zero_weight + nonzero × tx_token_non_zero_byte_multiplier`. Under EIP-7623 (Prague) the weight stays at 1 — reproducing the existing `tokens_in_calldata`-based floor. Under EIP-7976 (Amsterdam) the weight is raised to `tx_token_non_zero_byte_multiplier`, which yields `floor_tokens_in_calldata = (zero + nonzero) × 4` and, together with the per-token bump from 10 to 16, a uniform 64 gas/byte floor. * docs: correct EIP-7976 floor cost comment (64/64, not 16/64) * perf(gas): skip zero-byte scan when floor weights match When (Amsterdam / EIP-7976), every calldata byte contributes the same number of floor tokens. In that case we can use `input.len() * weight` directly and skip the filter+count pass over the calldata. * refactor(gas): rename tx_floor_token_zero_byte_weight to _multiplier Matches the naming of `tx_token_non_zero_byte_multiplier`. No behavior change. * refactor(gas): reuse get_tokens_in_calldata in tx_floor_cost The EIP-7623 branch (zero-byte multiplier = 1) produces exactly `zero + nonzero * non_zero_multiplier`, which is what get_tokens_in_calldata already computes. Delegate to it instead of re-implementing the scan inline. The EIP-7976 uniform path keeps its `input.len() * multiplier` shortcut. * refactor(gas): extract tx_floor_cost_with_tokens helper * feat(gas): implement EIP-7981 access list cost increase Folds the per-byte data charge into the per-item access-list cost and extends the EIP-7623/7976 floor to cover access-list bytes, both gated on AMSTERDAM. - tx_access_list_address_cost: 2400 -> 3680 (+20 * 64) - tx_access_list_storage_key_cost: 1900 -> 3948 (+32 * 64) - New GasId::tx_access_list_floor_byte_multiplier (= 4 at AMSTERDAM), surfaced via tx_floor_tokens_in_access_list; initial_tx_gas now adds the access-list contribution on top of the calldata floor. * feat(gas): EIP-8037 dynamic CPSB derived from block gas limit (#3603) * feat(gas): EIP-8037 dynamic cost_per_state_byte derived from block gas limit Thread CPSB through state-gas accounting so state-gas charges scale with the current block's gas limit per EIP-8037 instead of using a hard-coded 1174. State-gas table entries now store *byte counts*; helpers multiply by CPSB at charge time via new `Cfg::cpsb` / `Host::cpsb` methods and a `CfgEnv::cpsb_override` for tests and replay. Also refund the parent's upfront CREATE state gas to its reservoir when a child create reverts or halts, and split the EIP-7702 refund into separate regular and state-bytes components (new `tx_eip7702_auth_refund_state_bytes` GasId). * refactor(frame): move CREATE state-gas refund into return_create Drop the `state_gas_charged` field from `CreateOutcome` and handle the upfront CREATE state-gas refund inside `return_create` instead of `return_result`. The value is threaded through `CreateFrame`: `return_create` refills it into the reservoir on entry and re-records it on a successful commit, undoing the refund. Revert `test_eip8037_reverted_create_child` expectations (and the associated json testdata) back to the prior semantics, where the parent's upfront CREATE state gas is not refunded on child revert. * refactor(frame): derive CREATE state gas inside return_create `return_create` now takes `&mut impl ContextTr` and derives both the CPSB and the upfront `create_state_gas` charge from `cfg` + `block`, removing the need to thread `state_gas_charged` (and `cpsb`) as parameters. Drops the now-unused `state_gas_charged` fields from `CreateInputs` and `CreateFrame`, and the corresponding tracking in the CREATE opcode. * perf(context): cache EIP-8037 cpsb on LocalContext Adds a cpsb field to LocalContext exposed via LocalContextTr, populated at the start of every execution entry point (Handler::run, run_system_call, and their inspector variants) so the hot-path Host::cpsb is a single field read instead of recomputing cfg.cpsb(block.gas_limit()) on each call. * refactor(frame): use cached cpsb from local context in return_create Reads cpsb from LocalContext (set at tx entry) instead of recomputing from cfg+block, and simplifies the EIP-8037 gating now that the state-gas charge is always derivable from the cached value. * fix(frame): unwind 0→x→0 reservoir refund on sub-frame revert Per EIP-8037, when a sub-frame reverts, any 0→x→0 reservoir refund it performed must be rolled back. The previous formula `parent.reservoir = child.state_gas_spent + child.reservoir` left the parent's reservoir inflated when the child drained and refilled via a 0→x→0 restoration pattern. Cap the child's reservoir contribution at the parent's pre-call value (parent's reservoir isn't modified during the call, so the current value is the pre-call value) and clamp state_gas_spent to non-negative to preserve the parent's prior charges. * fix(post-execution): clamp reservoir when floor gas exceeds limit budget (#3607) * fix(post-execution): clamp reservoir when floor gas exceeds limit budget When EIP-7623's data floor clamps `gas_used` upward and `floor_gas + reservoir > gas.limit` (e.g. an EIP-7702 reservoir refund pushed the reservoir above `limit - floor_gas`), the plain `set_spent(floor_gas + reservoir)` saturates `remaining` to 0 but leaves the reservoir intact, so `reimbursable = remaining + reservoir + refund` overshoots by `floor_gas + reservoir - limit` gas and the caller is over-refunded by that amount. In that branch clamp the reservoir to `limit - floor_gas` and zero `remaining` instead. * feat(eip8037): refund state gas for CREATE+SELFDESTRUCT and restructure CREATE upfront charge Adds `JournalTr::eip8037_selfdestruct_state_gas_refund` (and the `JournalInner` implementation) which sums the state gas charged during the tx for every account that was both created and self-destructed (per EIP-6780) — account creation, code deposit, and 0→non-zero storage slot sets — and skips the CREATE-tx target whose creation gas is already in `initial_state_gas`. The handler invokes it at the start of post-execution and refills the reservoir before refund/reimbursement so the returned gas bypasses the 1/5 refund cap. Also moves the CREATE opcode's upfront `create_state_gas` refund out of `return_create` and into the CREATE opcode return path in the parent frame: the parent charged it and, on child failure (revert/halt/ early-fail with `address == None`), the parent now refunds it directly and undoes its `state_gas_spent`. The child frame is no longer allowed to borrow that upfront charge to cover code deposit. * refactor(journal): iterate selfdestructed_addresses set in EIP-8037 refund Replace full-state scan with a direct walk over the dedicated `selfdestructed_addresses` set, hoist the per-tx `sstore_set` constant out of the inner loop, and drop the redundant `original_value.is_zero()` check (locally-created accounts always start with empty storage, so every non-zero present slot was charged sstore_set). * fix(post-execution): zero reservoir when EIP-7623 floor wins Match execution-specs: when the floor is enforced, unused state gas is absorbed into the floor cost, not reimbursed separately. Without this, `reimburse_caller` (which sums `remaining + reservoir + refunded`) over-refunds the caller and `reward_beneficiary` under-pays the beneficiary by exactly `gas.reservoir()`. Also drop the prior commented-out "set_spent(floor + reservoir)" workaround — its premise that the reservoir must be added to spent conflicted with the spec's single-counter model. * fix(eip8037): correct CREATE state-gas refund propagation and unwind (#3614) * fix(post-execution): clamp reservoir when floor gas exceeds limit budget When EIP-7623's data floor clamps `gas_used` upward and `floor_gas + reservoir > gas.limit` (e.g. an EIP-7702 reservoir refund pushed the reservoir above `limit - floor_gas`), the plain `set_spent(floor_gas + reservoir)` saturates `remaining` to 0 but leaves the reservoir intact, so `reimbursable = remaining + reservoir + refund` overshoots by `floor_gas + reservoir - limit` gas and the caller is over-refunded by that amount. In that branch clamp the reservoir to `limit - floor_gas` and zero `remaining` instead. * feat(eip8037): refund state gas for CREATE+SELFDESTRUCT and restructure CREATE upfront charge Adds `JournalTr::eip8037_selfdestruct_state_gas_refund` (and the `JournalInner` implementation) which sums the state gas charged during the tx for every account that was both created and self-destructed (per EIP-6780) — account creation, code deposit, and 0→non-zero storage slot sets — and skips the CREATE-tx target whose creation gas is already in `initial_state_gas`. The handler invokes it at the start of post-execution and refills the reservoir before refund/reimbursement so the returned gas bypasses the 1/5 refund cap. Also moves the CREATE opcode's upfront `create_state_gas` refund out of `return_create` and into the CREATE opcode return path in the parent frame: the parent charged it and, on child failure (revert/halt/ early-fail with `address == None`), the parent now refunds it directly and undoes its `state_gas_spent`. The child frame is no longer allowed to borrow that upfront charge to cover code deposit. * refactor(journal): iterate selfdestructed_addresses set in EIP-8037 refund Replace full-state scan with a direct walk over the dedicated `selfdestructed_addresses` set, hoist the per-tx `sstore_set` constant out of the inner loop, and drop the redundant `original_value.is_zero()` check (locally-created accounts always start with empty storage, so every non-zero present slot was charged sstore_set). * fix(post-execution): zero reservoir when EIP-7623 floor wins Match execution-specs: when the floor is enforced, unused state gas is absorbed into the floor cost, not reimbursed separately. Without this, `reimburse_caller` (which sums `remaining + reservoir + refunded`) over-refunds the caller and `reward_beneficiary` under-pays the beneficiary by exactly `gas.reservoir()`. Also drop the prior commented-out "set_spent(floor + reservoir)" workaround — its premise that the reservoir must be added to spent conflicted with the spec's single-counter model. * fix(eip8037): correct CREATE/CALL state-gas refund propagation Three fixes to EIP-8037 reservoir accounting that together resolve 12 failing state tests under fixtures_bal_v570/state_tests/for_amsterdam/: 1. CREATE nonce overflow: the early-fail path returns `InstructionResult::Return` (ok) with `address == None`, but the upfront `create_state_gas` refund was gated on `!is_ok()`. Gate on `address.is_none() || !is_ok()` so the refund applies. 2. 0→x→0 refill unwind on parent revert/halt: add a per-frame `refill_amount` counter on `GasTracker` that accumulates `refill_reservoir` calls (and now the CREATE upfront-state-gas refund on child failure). On revert/halt, `handle_reservoir_remaining_gas` subtracts this from the propagated reservoir so refill-driven inflation does not leak up; on success the total is propagated so an ancestor revert can still unwind it. The CREATE-failed refund now uses `refill_reservoir` to participate in this tracking. 3. EIP-8037 selfdestruct refund: include the CREATE-tx contract in the iteration (the execution-specs reference iterates all created+ destroyed accounts) and cap the aggregate refund at `gas.state_gas_spent()` to match the per-address `min(refund, state_gas_used)` cap from the spec. * wip(eip8037): refactor reservoir handling and selfdestruct refund - Refactor handle_reservoir_remaining_gas to take is_success: bool directly - Drop is_created_locally / len != 0 guards in journal selfdestruct refund - Add is_ok_without_selfdestruct helper on InstructionResult - Stash WIP commented logic in handler for top-level CREATE failure refund * fix(merge): drop duplicated CREATE state-gas refund block The merge of origin/dev4-cpsb (00ca181) into functional kept both the upstream `set_reservoir`/`set_state_gas_spent` block and the local `refill_reservoir` block, causing every failed child CREATE to refund `create_state_gas` to the parent's reservoir twice. Keep only the `refill_reservoir` version: it handles the nonce-overflow path (address == None with is_ok()) and registers the refund in `refill_amount` so the parent's own revert/halt unwinds it correctly. * chore(scripts): bump devnet fixtures to bal@v5.7.0 and enable runs Also uncomment the devnet statetest run; the devnet btest line is left as-is. * test(ee-tests): update reverted CREATE child expected state gas On child revert, both the parent's upfront CREATE state gas and the child's SSTORE state gas are refunded to the reservoir, leaving state_gas_spent at 0. * fix: enable state refund on tx crate * refactor(eip8037): drop block_gas_limit from Cfg::cpsb CPSB is fixed at 1174 for Glamsterdam, so the block-gas-limit-derived formula is no longer needed. Deprecate cost_per_state_byte, expose CPSB_GLAMSTERDAM, and update callers and doc comments accordingly. * fix(eip8037): keep eip7702 reservoir refund out of gross tx state gas Per the spec, tx_state_gas = intrinsic_state_gas + execution_state_gas. The EIP-7702 reservoir refund is added back to the state gas reservoir at tx start, so it must not also be subtracted from the gross state gas reported in the result. * fix(eip7702): split per-auth refund into state and regular components Under EIP-8037 the per-auth refund for existing authorities is entirely state gas (NEW_ACCOUNT_BYTES * cpsb). apply_eip7702_auth_list was subtracting the full per-auth state *charge* ((NEW + AUTH_BASE) * cpsb) from initial_state_gas — overshooting the credit by AUTH_BASE_BYTES per auth — and returning the combined refund as if it were a regular-gas refund. Add tx_eip7702_auth_refund_{state,regular} helpers and use them to apply each portion to the right counter, with a guard for refund_per_auth == 0. Also refactors InitialAndFloorGas: replace `initial_total_gas` with an explicit `initial_regular_gas` + `initial_state_gas` pair, drop the `eip7702_reservoir_refund` field (state credit now flows through `initial_state_gas`), and add getter/setter/builder accessors. * refactor(eip7702): track per-auth refund via dedicated reservoir field Replace the in-place subtraction from `initial_state_gas` with a separate `initial_eip7702_refund` field on `InitialAndFloorGas`. The refund is now added to the reservoir directly in `initial_gas_and_reservoir`, while `initial_state_gas` keeps the gross intrinsic state gas. Reporting paths use a new `initial_state_gas_final()` accessor that subtracts the refund. * fix(eip7702): report gross initial_state_gas in result, not refund-net build_result_gas was summing `initial_state_gas_final()` into the reported state gas, but per the surrounding comment the EIP-7702 reservoir refund is credited to the reservoir at tx start and must not reduce the gross state gas spent. Switch to the raw `initial_state_gas` field. Update gas_params tests to use the renamed accessor. * refactor(eip8037): split halt/revert reservoir handling, rename is_error to is_halt Distinguish halt from revert in the reservoir reconciliation paths: - handle_reservoir_remaining_gas takes the full InstructionResult and treats success, revert, and halt distinctly. Halt no longer touches the parent reservoir from the child-merge path. - last_frame_result now receives the pre-execution reservoir; on halt it restores that value (child gas fully consumed, but the tx-level reservoir is the pre-call snapshot) while revert keeps the existing combined-recovery formula. State_gas_spent is zeroed on both halt and revert. - build_result_gas now combines initial_state_gas with execution state_gas_spent before clamping, so a transient negative state_gas_spent from refills doesn't underflow the addition. Also rename `InstructionResult::is_error` / `InterpreterResult::is_error` to `is_halt` (deprecated aliases kept) — the old name was confusable with revert. Inspector gas accounting only spends-all on halt now, matching the rename. Plumb the reservoir through the inspector last_frame_result call. * fix(eip7702): exclude initial state refund from block regular gas Track initial EIP-7702 state refund on ResultGas and subtract it from state_gas_spent when computing block_regular_gas_used so the refunded portion of intrinsic state gas isn't reclassified as regular execution gas. * fix(eip8037): regenerate snapshots and refund state gas on top-level halt - Add missing `refill_amount` field in `PrecompileOutput` literal in `precompile_provider.rs` test harness (build fix after merge). - In `last_frame_result`, fold halt into the same state-gas-refund path used by revert (`reservoir = state_gas_spent + reservoir`), capping at `original_reservoir + recovered` on halt to strip 0→x→0 refill inflation. Previously halt set `reservoir = original_reservoir`, dropping the rolled-back state gas. - Regenerate insta snapshots for `eip8037` and `revm_tests` to include the new `eip7702_state_refund` field. * simplify condition check * fix(eip8037): plumb new-account state-gas refund, bump CPSB to 1530, fix clippy Propagate `charged_new_account_state_gas` through CallInputs/CallOutcome so parent frames can refund the upfront EIP-8037 new-account state-gas charge on child revert/halt, mirroring the CREATE path. Update Glamsterdam constants (CPSB 1174→1530, SSTORE_SET_BYTES 32→64, NEW_ACCOUNT_BYTES 112→120), remove the deprecated `cost_per_state_byte` helper, and refresh tests/snapshots to use `state_gas_spent_final`. Also fix clippy warnings (missing-const-for-fn, needless-else, unused variable). * fix(eip8037): align top-level revert/halt reservoir refund with child-frame rule Use the same excess-over-baseline formula as handle_reservoir_remaining_gas for the top frame, so state gas spilled into regular gas is recovered while 0→x→0 refill inflation (both in-frame and propagated up from children) is stripped on revert/halt. Add regression tests for tx-Create collision and for a halt where state gas stayed entirely in the reservoir. * fix(eip8037): unwind grandchild storage-clear refund on revert/halt `handle_reservoir_remaining_gas` and `last_frame_result` discarded the negative branch of `state_gas_spent` when restoring the parent reservoir on revert/halt, so a 0→x→0 refill credit accrued by a grandchild and propagated through a successful child could survive into the parent even after the child reverted — the underlying storage clear no longer existed but the reservoir inflation persisted. Use the invariant `pre_call_reservoir = reservoir + state_gas_spent` (record_state_cost and refill_reservoir move the two symmetrically) via `saturating_add_signed`, so the negative contribution unwinds the leaked refill inflation. Fixes the subcall_revert and depth-chain leak tests from the EIP-8037 reservoir suite; full Amsterdam state-test failure count drops from 104 to 1 with no regressions. * fix(eip8037): align bal-devnet-7 system call gas limit (#3672) Update system calls to use the bal-devnet-7 gas limit formula instead of the legacy fixed 30M cap. Changes: - add SYSTEM_MAX_SSTORES_PER_CALL = 16 - derive SYSTEM_CALL_GAS_LIMIT as 30_000_000 + SSTORE_SET_BYTES * CPSB_GLAMSTERDAM * SYSTEM_MAX_SSTORES_PER_CALL - update the system_call test to assert the new limit - sync stale EIP-8037 docs/comments with the current bal-devnet-7 behavior This matches tests-bal@v7.0.0, where system calls receive a dedicated state-gas reservoir on top of the 30M regular-gas budget. * fix(eip8037): match EIP-7702 per-auth state refund to its charge The pessimistic per-auth state gas charged in `initial_tx_gas` is `(NEW_ACCOUNT_BYTES + AUTH_BASE_BYTES) * cpsb`, but the per-auth refund in `apply_eip7702_auth_list` only refunded `NEW_ACCOUNT_BYTES * cpsb`, leaving `AUTH_BASE_BYTES * cpsb` of state gas stranded on every existing-account authorization. Set `tx_eip7702_auth_refund_state_bytes` to `NEW_ACCOUNT_BYTES + AUTH_BASE_BYTES` so the refund fully recovers the pessimistic charge. * refactor(eip7702): fold state refund into state_gas_spent at result build Drop the `eip7702_state_refund` field on `ResultGas` and subtract the EIP-7702 per-authorization state-gas refund from `state_gas_spent` when building the result. The stored value is now already net of the refund, so `state_gas_spent_final()` just returns the field directly. * refactor(eip8037): drop journal-based selfdestruct state-gas refund Removes `eip8037_selfdestruct_state_gas_refund` from `JournalTr` / `JournalInner` / post-execution along with the `is_ok_without_selfdestruct` helper, and renames `InitialAndFloorGas::initial_eip7702_refund` to the more general `state_refund` since the same field is reused for any intrinsic state refund. The CREATE-failure unwind in `last_frame_result` now keys off `is_ok` again instead of the selfdestruct-aware variant. * docs: update top-level CREATE-failure comment after eip8037 refund removal * fix(eip8037): split EIP-7702 auth refund into account and bytecode portions Separate the per-auth state refund into two components: an account refund (NEW_ACCOUNT_BYTES * cpsb) credited per existing-account authorization and a bytecode refund (AUTH_BASE_BYTES * cpsb) credited per code-deposit authorization. apply_auth_list now returns both counts so the caller can apply each refund independently, and pre-Amsterdam paths continue to route the regular-gas refund through the standard counter. Also add AccountInfo::is_code_hash_empty_or_zero() helper used by the new bytecode-refund check and by is_empty(). * refactor(eip8037): consolidate EIP-7702 state-gas accessors Drop the dedicated `tx_eip7702_state_gas_new_account` / `tx_eip7702_auth_refund_state_bytes` slots — both equal already-existing state-byte constants, so reuse `new_account_state_gas` and a single new `tx_eip7702_state_gas_bytecode` slot (`AUTH_BASE_BYTES`). Collapse the per-auth and per-bytecode refund helpers into one `tx_eip7702_state_refund(refunded_accounts, refunded_bytecodes, cpsb)` that returns the total state refund, and rename `tx_eip7702_per_auth_state_gas` → `tx_eip7702_state_gas`. Always-run the regular per-auth refund (zero under EIP-8037) and unconditionally credit `state_refund` to the reservoir. * refactor(eip8037): drop is_eip8037 flag from initial_gas_and_reservoir Apply `tx_gas_limit_cap` against the full tx limit (then subtract `initial_regular_gas`) instead of branching on the EIP-8037 flag. The system-call zero-gas case still bypasses the cap, and the resulting `regular_gas_limit` / `reservoir` split is equivalent to the previous two paths. Drop the now-unused flag and unconditionalize the `initial_state_gas` deduction. * chore: bump devnet fixtures to bal@v7.1.1 and fix post-merge tests - scripts/run-tests.sh: pull devnet fixtures from execution-specs tests-bal@v7.1.1 release (new repo + tag format) - inspector_tests: rename gas().state_gas_spent() call to state_gas_spent_final() to match the renamed public accessor - eip8037 selfdestruct-after-sstore test: SSTORE state gas is no longer refunded after dropping the journal-based selfdestruct refund, so expect STATE_GAS_SSTORE_SET + NEW_ACCOUNT_BYTES and refresh the snapshot * docs: fix broken intra-doc links and unescaped backticks - gas_params: rewrite Cfg::cpsb link to super::Cfg::cpsb and split the tx_floor_cost formula across lines to balance backticks - result: point state_gas_spent_final() table entry and block_state_gas_used cross-reference at the public accessor instead of the private field - cfg: replace stale reference to primitives::eip8037::cost_per_state_byte (no such item) with the actual default constant CPSB_GLAMSTERDAM * chore: add typos to commands and fix lenght typo
1 parent 57c4561 commit aa13336

45 files changed

Lines changed: 1683 additions & 369 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ cargo nextest run --workspace --no-default-features
1515
cargo nextest run --workspace --all-features
1616
cargo clippy --workspace --all-targets --all-features
1717
cargo fmt --all --check
18+
typos
1819

1920
cargo check --target riscv32imac-unknown-none-elf --no-default-features
2021
cargo check --target riscv64imac-unknown-none-elf --no-default-features

crates/context/interface/src/cfg.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ pub trait Cfg {
9494
/// via the reservoir model. EIP-8037 specifies concrete gas values based on
9595
/// `cost_per_state_byte` and adds a hash cost for deployed bytecode.
9696
fn is_amsterdam_eip8037_enabled(&self) -> bool;
97+
98+
/// Returns the EIP-8037 `cost_per_state_byte` (CPSB).
99+
///
100+
/// When [`Cfg::is_amsterdam_eip8037_enabled`] is `false` this returns `0`.
101+
/// Otherwise, if an override is configured it is returned directly; otherwise
102+
/// the fixed Glamsterdam value [`primitives::eip8037::CPSB_GLAMSTERDAM`] is returned.
103+
fn cpsb(&self) -> u64;
97104
}
98105

99106
/// What bytecode analysis to perform

crates/context/interface/src/cfg/gas.rs

Lines changed: 150 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,18 @@ pub struct GasTracker {
1616
/// State gas reservoir (gas exceeding TX_MAX_GAS_LIMIT). Starts as `execution_gas - min(execution_gas, regular_gas_budget)`.
1717
/// When 0, all remaining gas is regular gas with hard cap at `TX_MAX_GAS_LIMIT`.
1818
reservoir: u64,
19-
/// Total state gas spent so far.
20-
state_gas_spent: u64,
19+
/// Net state gas spent so far.
20+
///
21+
/// Can be negative within a call frame when 0→x→0 storage restoration refills
22+
/// more state gas than the frame itself has charged (the parent previously
23+
/// charged the 0→x portion). The net is reconciled on frame return.
24+
state_gas_spent: i64,
25+
/// Cumulative reservoir refill amount from 0→x→0 storage restorations
26+
/// performed by this frame (EIP-8037 issue #2). Tracked so that on
27+
/// revert/halt the parent can subtract this inflation when propagating
28+
/// the child's reservoir, without confusing it with legitimate reservoir
29+
/// growth from grandchild halt/revert refunds.
30+
refill_amount: u64,
2131
/// Refunded gas. Used to refund the gas to the caller at the end of execution.
2232
refunded: i64,
2333
}
@@ -31,6 +41,7 @@ impl GasTracker {
3141
remaining,
3242
reservoir,
3343
state_gas_spent: 0,
44+
refill_amount: 0,
3445
refunded: 0,
3546
}
3647
}
@@ -79,13 +90,13 @@ impl GasTracker {
7990

8091
/// Returns the state gas spent.
8192
#[inline]
82-
pub const fn state_gas_spent(&self) -> u64 {
93+
pub const fn state_gas_spent(&self) -> i64 {
8394
self.state_gas_spent
8495
}
8596

8697
/// Sets the state gas spent.
8798
#[inline]
88-
pub const fn set_state_gas_spent(&mut self, val: u64) {
99+
pub const fn set_state_gas_spent(&mut self, val: i64) {
89100
self.state_gas_spent = val;
90101
}
91102

@@ -125,7 +136,7 @@ impl GasTracker {
125136
#[must_use = "In case of not enough gas, the interpreter should halt with an out-of-gas error"]
126137
pub const fn record_state_cost(&mut self, cost: u64) -> bool {
127138
if self.reservoir >= cost {
128-
self.state_gas_spent = self.state_gas_spent.saturating_add(cost);
139+
self.state_gas_spent = self.state_gas_spent.saturating_add(cost as i64);
129140
self.reservoir -= cost;
130141
return true;
131142
}
@@ -134,12 +145,43 @@ impl GasTracker {
134145

135146
let success = self.record_regular_cost(spill);
136147
if success {
137-
self.state_gas_spent = self.state_gas_spent.saturating_add(cost);
148+
self.state_gas_spent = self.state_gas_spent.saturating_add(cost as i64);
138149
self.reservoir = 0;
139150
}
140151
success
141152
}
142153

154+
/// Refills the reservoir with state gas that is returned by 0→x→0 storage
155+
/// restoration (EIP-8037 issue #2).
156+
///
157+
/// Per the spec, when a storage slot is restored to its original zero value
158+
/// within the same transaction, the state gas charged for the initial 0→x
159+
/// transition is directly restored to the reservoir rather than routed
160+
/// through the capped refund counter.
161+
///
162+
/// `state_gas_spent` is decremented by the same amount and may become
163+
/// negative if the matching 0→x charge was made by a parent frame. The
164+
/// parent's total is reconciled on frame return.
165+
#[inline]
166+
pub const fn refill_reservoir(&mut self, amount: u64) {
167+
self.reservoir = self.reservoir.saturating_add(amount);
168+
self.state_gas_spent = self.state_gas_spent.saturating_sub(amount as i64);
169+
self.refill_amount = self.refill_amount.saturating_add(amount);
170+
}
171+
172+
/// Returns cumulative reservoir refill amount from 0→x→0 restorations
173+
/// performed in this frame.
174+
#[inline]
175+
pub const fn refill_amount(&self) -> u64 {
176+
self.refill_amount
177+
}
178+
179+
/// Sets the refill amount.
180+
#[inline]
181+
pub const fn set_refill_amount(&mut self, val: u64) {
182+
self.refill_amount = val;
183+
}
184+
143185
/// Records a refund value.
144186
#[inline]
145187
pub const fn record_refund(&mut self, refund: i64) {
@@ -233,7 +275,7 @@ pub const NON_ZERO_BYTE_DATA_COST_ISTANBUL: u64 = 16;
233275
/// The multiplier for a non zero byte in calldata adjusted by [EIP-2028](https://eips.ethereum.org/EIPS/eip-2028).
234276
pub const NON_ZERO_BYTE_MULTIPLIER_ISTANBUL: u64 =
235277
NON_ZERO_BYTE_DATA_COST_ISTANBUL / STANDARD_TOKEN_COST;
236-
/// The cost floor per token as defined by EIP-2028.
278+
/// The cost floor per token as defined by [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623).
237279
pub const TOTAL_COST_FLOOR_PER_TOKEN: u64 = 10;
238280

239281
/// Gas cost for EOF CREATE instruction.
@@ -267,59 +309,125 @@ pub const CALL_STIPEND: u64 = 2300;
267309
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
268310
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269311
pub struct InitialAndFloorGas {
270-
/// Initial gas for transaction.
271-
pub initial_total_gas: u64,
272-
/// State gas component of initial_gas (subset of initial_total_gas).
312+
/// Regular (non-state) portion of the initial intrinsic gas.
313+
///
314+
/// Under EIP-8037, this is the part constrained by `TX_MAX_GAS_LIMIT`;
315+
/// state gas uses its own reservoir and is not subject to that cap.
316+
pub initial_regular_gas: u64,
317+
/// State gas component of the initial intrinsic gas.
273318
/// Under EIP-8037, this includes:
274319
/// - EIP-7702 auth list state gas (per-auth account creation + metadata costs)
275320
/// - For CREATE transactions: `create_state_gas` (account creation + contract metadata)
276321
/// - For CALL transactions: 0 (state gas is unpredictable at validation time)
277322
pub initial_state_gas: u64,
323+
/// EIP-7702 refund for existing authorities.
324+
/// This is the refund given when an authorization is applied to an already existing account.
325+
pub state_refund: u64,
278326
/// If transaction is a Call and Prague is enabled
279327
/// floor_gas is at least amount of gas that is going to be spent.
280328
pub floor_gas: u64,
281-
/// EIP-7702 state gas refund for existing authorities.
282-
/// Added to the reservoir after initial_state_gas is deducted.
283-
/// In the Python spec, set_delegation adds this back to state_gas_reservoir
284-
/// rather than reducing initial_state_gas, so the refunded gas stays as
285-
/// reservoir gas (not regular gas).
286-
pub eip7702_reservoir_refund: u64,
287329
}
288330

289331
impl InitialAndFloorGas {
332+
/***** Constructors *****/
333+
290334
/// Create a new InitialAndFloorGas instance.
291335
#[inline]
292-
pub const fn new(initial_total_gas: u64, floor_gas: u64) -> Self {
336+
pub const fn new(initial_regular_gas: u64, floor_gas: u64) -> Self {
293337
Self {
294-
initial_total_gas,
338+
initial_regular_gas,
295339
initial_state_gas: 0,
340+
state_refund: 0,
296341
floor_gas,
297-
eip7702_reservoir_refund: 0,
298342
}
299343
}
300344

301345
/// Create a new InitialAndFloorGas instance with state gas tracking.
302346
#[inline]
303347
pub const fn new_with_state_gas(
304-
initial_total_gas: u64,
348+
initial_regular_gas: u64,
305349
initial_state_gas: u64,
306350
floor_gas: u64,
307351
) -> Self {
308352
Self {
309-
initial_total_gas,
353+
initial_regular_gas,
310354
initial_state_gas,
355+
state_refund: 0,
311356
floor_gas,
312-
eip7702_reservoir_refund: 0,
313357
}
314358
}
315359

360+
/***** Simple getters *****/
361+
316362
/// Regular (non-state) portion of the initial intrinsic gas.
317363
///
318364
/// Under EIP-8037, this is the part constrained by `TX_MAX_GAS_LIMIT`;
319365
/// state gas uses its own reservoir and is not subject to that cap.
320366
#[inline]
321367
pub const fn initial_regular_gas(&self) -> u64 {
322-
self.initial_total_gas - self.initial_state_gas
368+
self.initial_regular_gas
369+
}
370+
371+
/// State gas component of the initial intrinsic gas.
372+
/// This is the state gas component of the initial intrinsic gas minus the EIP-7702 refund.
373+
#[inline]
374+
pub const fn initial_state_gas_final(&self) -> u64 {
375+
self.initial_state_gas - self.state_refund
376+
}
377+
378+
/// EIP-7623 floor gas.
379+
#[inline]
380+
pub const fn floor_gas(&self) -> u64 {
381+
self.floor_gas
382+
}
383+
384+
/// Total initial intrinsic gas: `initial_regular_gas + initial_state_gas`.
385+
#[inline]
386+
pub const fn initial_total_gas(&self) -> u64 {
387+
self.initial_regular_gas + self.initial_state_gas_final()
388+
}
389+
390+
/***** Simple setters *****/
391+
392+
/// Sets the `initial_regular_gas` field by mutable reference.
393+
#[inline]
394+
pub const fn set_initial_regular_gas(&mut self, initial_regular_gas: u64) {
395+
self.initial_regular_gas = initial_regular_gas;
396+
}
397+
398+
/// Sets the `initial_state_gas` field by mutable reference.
399+
#[inline]
400+
pub const fn set_initial_state_gas(&mut self, initial_state_gas: u64) {
401+
self.initial_state_gas = initial_state_gas;
402+
}
403+
404+
/// Sets the `floor_gas` field by mutable reference.
405+
#[inline]
406+
pub const fn set_floor_gas(&mut self, floor_gas: u64) {
407+
self.floor_gas = floor_gas;
408+
}
409+
410+
/***** Builder with_* methods *****/
411+
412+
/// Sets the `initial_regular_gas` field.
413+
#[inline]
414+
pub const fn with_initial_regular_gas(mut self, initial_regular_gas: u64) -> Self {
415+
self.initial_regular_gas = initial_regular_gas;
416+
self
417+
}
418+
419+
/// Sets the `initial_state_gas` field.
420+
#[inline]
421+
pub const fn with_initial_state_gas(mut self, initial_state_gas: u64) -> Self {
422+
self.initial_state_gas = initial_state_gas;
423+
self
424+
}
425+
426+
/// Sets the `floor_gas` field.
427+
#[inline]
428+
pub const fn with_floor_gas(mut self, floor_gas: u64) -> Self {
429+
self.floor_gas = floor_gas;
430+
self
323431
}
324432

325433
/// Computes the regular gas budget and reservoir for the initial call frame.
@@ -340,42 +448,36 @@ impl InitialAndFloorGas {
340448
&self,
341449
tx_gas_limit: u64,
342450
tx_gas_limit_cap: u64,
343-
is_eip8037: bool,
344451
) -> (u64, u64) {
345452
let execution_gas = tx_gas_limit - self.initial_regular_gas();
346453

347454
// System calls pass InitialAndFloorGas with all zeros and should not be
348455
// subject to the TX_MAX_GAS_LIMIT cap.
349-
let regular_gas_cap = if self.initial_total_gas == 0 {
456+
let tx_gas_limit_cap = if self.initial_total_gas() == 0 {
350457
u64::MAX
351-
} else if is_eip8037 {
352-
tx_gas_limit_cap.saturating_sub(self.initial_regular_gas())
353458
} else {
354459
tx_gas_limit_cap
355460
};
356461

357-
let mut gas_limit = core::cmp::min(execution_gas, regular_gas_cap);
358-
let mut reservoir = execution_gas - gas_limit;
462+
let mut regular_gas_limit = core::cmp::min(tx_gas_limit, tx_gas_limit_cap)
463+
.saturating_sub(self.initial_regular_gas());
464+
let mut reservoir = execution_gas - regular_gas_limit;
359465

360466
// Deduct initial state gas from the reservoir. When the reservoir is
361467
// insufficient, the deficit is charged from the regular gas budget.
362-
if self.initial_state_gas > 0 {
363-
if reservoir >= self.initial_state_gas {
364-
reservoir -= self.initial_state_gas;
365-
} else {
366-
gas_limit -= self.initial_state_gas - reservoir;
367-
reservoir = 0;
368-
}
468+
if reservoir >= self.initial_state_gas {
469+
reservoir -= self.initial_state_gas;
470+
} else {
471+
regular_gas_limit -= self.initial_state_gas - reservoir;
472+
reservoir = 0;
369473
}
370474

371475
// EIP-7702 state gas refund for existing authorities goes directly to
372476
// the reservoir. In the Python spec, set_delegation adds this refund to
373477
// state_gas_reservoir so it stays as state gas (not regular gas).
374-
if self.eip7702_reservoir_refund > 0 {
375-
reservoir += self.eip7702_reservoir_refund;
376-
}
478+
reservoir += self.state_refund;
377479

378-
(gas_limit, reservoir)
480+
(regular_gas_limit, reservoir)
379481
}
380482
}
381483

@@ -393,13 +495,15 @@ pub fn calculate_initial_tx_gas(
393495
access_list_accounts: u64,
394496
access_list_storages: u64,
395497
authorization_list_num: u64,
498+
cpsb: u64,
396499
) -> InitialAndFloorGas {
397500
GasParams::new_spec(spec_id).initial_tx_gas(
398501
input,
399502
is_create,
400503
access_list_accounts,
401504
access_list_storages,
402505
authorization_list_num,
506+
cpsb,
403507
)
404508
}
405509

@@ -410,7 +514,11 @@ pub fn calculate_initial_tx_gas(
410514
///
411515
/// - Intrinsic gas
412516
/// - Number of tokens in calldata
413-
pub fn calculate_initial_tx_gas_for_tx(tx: impl Transaction, spec: SpecId) -> InitialAndFloorGas {
517+
pub fn calculate_initial_tx_gas_for_tx(
518+
tx: impl Transaction,
519+
spec: SpecId,
520+
cpsb: u64,
521+
) -> InitialAndFloorGas {
414522
let mut accounts = 0;
415523
let mut storages = 0;
416524
// legacy is only tx type that does not have access list.
@@ -435,6 +543,7 @@ pub fn calculate_initial_tx_gas_for_tx(tx: impl Transaction, spec: SpecId) -> In
435543
accounts as u64,
436544
storages as u64,
437545
tx.authorization_list_len() as u64,
546+
cpsb,
438547
)
439548
}
440549

0 commit comments

Comments
 (0)