feat(amsterdam): glamsterdam devnet-7 alignment (EIP-2780 runtime gas phase, fixtures v7.0.0)#3795
Open
rakita wants to merge 23 commits into
Open
feat(amsterdam): glamsterdam devnet-7 alignment (EIP-2780 runtime gas phase, fixtures v7.0.0)#3795rakita wants to merge 23 commits into
rakita wants to merge 23 commits into
Conversation
Move the state-dependent transaction charges from the intrinsic phase to a runtime gas phase applied as the first frame is entered, per the EIP-2780 update in ethereum/EIPs#11844. Gated on CfgEnv::enable_amsterdam_eip2780; older forks and 2780-disabled configs are unchanged. - Create transactions: the new-account state gas moves out of intrinsic gas and is charged at runtime, only when the deployment target does not already exist. Initcode revert/halt unwinds it LIFO via rollback_state_gas. - EIP-7702: the pessimistic per-auth intrinsic charge and its refunds are replaced by an intrinsic REGULAR_PER_AUTH_BASE_COST (7,816) plus runtime charges per authority: new-account state gas and ACCOUNT_WRITE for a non-existent authority, and delegation-indicator state gas for net-new delegation bytes. - Delegated recipient: the delegation-target access follows the standard EIP-2929 warm/cold model instead of a flat COLD_ACCOUNT_ACCESS. - Running out of gas in the runtime phase no longer invalidates the transaction: it is included as an out-of-gas halt consuming all gas, with all runtime state changes (including applied delegations) reverted.
Aligns Amsterdam to the tests-glamsterdam-devnet@v7.0.0 fixtures. All state_tests and blockchain_tests pass (revme statetest/btest). Spec changes: - EIPs#11836: calldata floor anchored on the decomposed intrinsic base (TX_BASE + recipient/value charges) instead of flat TX_BASE. - EIPs#11891: 7702 ACCOUNT_WRITE charged on the first write to each authority, skipped only when already paid (sender, value-bearing recipient, or a preceding valid authorization). - EIPs#11858: CREATE/CREATE2 account-creation state gas charged conditionally at access in the creating frame before the 63/64 split, after the endowment/nonce pre-checks, only when the destination does not exist; refilled LIFO when the create fails. Top-frame charge for create transactions keeps the existing depth-0 path. - EIPs#11854: SSTORE must cover the slot's access cost before the implicit storage read; an unaffordable cold access skips the read so the slot stays out of the block access list. - EIP-8282: builder deposit/exit system-contract addresses updated. Runtime gas phase is now charged incrementally on a gas tracker and stops at the first unaffordable charge: later authorities and a cold delegation target are never loaded, keeping them out of the EIP-7928 BAL; the recipient is read up front so its BAL read survives a runtime out-of-gas. JournalEntry::CodeChange now records the previous code hash/bytecode and restores them on revert. The old revert-to-empty assumption breaks once 7702 delegation set/clear on already-delegated accounts becomes revertible; the corrupted account stayed untouched (state root correct) but leaked a phantom code write into the BAL. statetest runner updates for the v7 fixture format: explicit tx chainId (including type-0), optional secretKey (bad_v_r_s expects rejection), and >u64 nonces no longer panic the keep-going runner.
Move the depth-0 EIP-2780 state-gas decisions out of frame creation into the runtime gas phase (apply_eip2780_runtime_gas), reusing the existing runtime out-of-gas halt: - The recipient new-account charge (value transfer to an empty recipient) and the create transaction's account-creation charge (deployment target does not exist) are decided and affordability- checked at the runtime phase. An unaffordable charge sets runtime_oog and the handler includes the transaction as an out-of-gas halt without entering the frame; a create transaction still bumps the sender nonce on that path. - The decided charge travels to the first frame via FrameInit::state_gas_charge and InitialAndFloorGas::first_frame_state_gas and is recorded on the frame's gas tracker, keeping it refillable on revert/halt. make_call_frame/make_create_frame lose all cfg/journal/ depth logic and only apply the passed charge (defensive backstop on failure). - The runtime halt now returns the EIP-8037 reservoir instead of consuming it, matching the spec's top-frame halt settlement. The CREATE opcode and SSTORE cold-load check drop their EIP-2780 gate: the devnet-7 conditional account-creation charge and the access-cost-before-read ordering are now the sole EIP-8037 behavior, and the pre-devnet-7 unconditional-charge/refill path is removed, so the CREATE refill in return_result is gated purely on the recorded charge. The now-unused Host::is_amsterdam_eip2780_enabled is removed.
Each Amsterdam devnet supersedes the previous one, so the devnet-6 create accounting branches are removed rather than kept behind flag combinations: - The intrinsic `create_state_gas` fold for create transactions (and its `last_frame_result` refill on failed/alive-target creates) is gone; the charge is always decided at the runtime gas phase and recorded on the first frame's tracker. - `target_was_alive` is removed from `CreateFrame`/`CreateOutcome`; the CREATE refill is driven solely by `charged_create_state_gas`. - ee-tests and snapshots updated to the conditional-charge semantics. All glamsterdam devnet-7 v7.0.0 state and blockchain fixtures pass.
…_gas Renames InitialAndFloorGas::first_frame_state_gas and FrameInit::state_gas_charge to a single name, refundable_state_gas, stating the property that distinguishes it from initial_state_gas: it is recorded on the first frame's gas tracker and refunded when the frame reverts or halts, while initial_state_gas is consumed unconditionally.
Merging this PR will degrade performance by 3.52%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | transact_1000tx_commit_inner_every_40 |
2.4 ms | 2.5 ms | -7.21% |
| ❌ | Simulation | transfer |
14.5 µs | 15.5 µs | -6.51% |
| ❌ | Simulation | transfer_finalize |
17.3 µs | 18.4 µs | -5.79% |
| ❌ | Simulation | transact_commit_1000txs |
2.9 ms | 3.1 ms | -4.47% |
| ❌ | Simulation | JUMPDEST_50 |
16.7 µs | 17.3 µs | -3.85% |
| ❌ | Simulation | GASLIMIT_50 |
19.2 µs | 20 µs | -3.64% |
| ❌ | Simulation | JUMP_50 |
18.9 µs | 19.6 µs | -3.57% |
| ❌ | Simulation | MSTORE_50 |
19.7 µs | 20.4 µs | -3.56% |
| ❌ | Simulation | NUMBER_50 |
19.3 µs | 20 µs | -3.49% |
| ❌ | Simulation | CHAINID_50 |
19.3 µs | 20 µs | -3.48% |
| ❌ | Simulation | CALLVALUE_50 |
19.4 µs | 20.1 µs | -3.48% |
| ❌ | Simulation | CALLDATASIZE_50 |
19.4 µs | 20.1 µs | -3.47% |
| ❌ | Simulation | COINBASE_50 |
19.4 µs | 20.1 µs | -3.47% |
| ❌ | Simulation | ORIGIN_50 |
19.5 µs | 20.2 µs | -3.46% |
| ❌ | Simulation | PUSH3_50 |
19.5 µs | 20.2 µs | -3.45% |
| ❌ | Simulation | GASPRICE_50 |
19.5 µs | 20.2 µs | -3.45% |
| ❌ | Simulation | PUSH10_50 |
19.6 µs | 20.3 µs | -3.43% |
| ❌ | Simulation | PUSH14_50 |
19.8 µs | 20.5 µs | -3.41% |
| ❌ | Simulation | PUSH19_50 |
20 µs | 20.7 µs | -3.37% |
| ❌ | Simulation | PUSH24_50 |
20 µs | 20.7 µs | -3.37% |
| ... | ... | ... | ... | ... | ... |
ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing glam-devnet-7 (335a35d) with main (a094a93)
Footnotes
-
1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports. ↩
Drop the up-front recipient read from the EIP-2780 runtime gas phase: the recipient is now loaded only after the authorization charges are covered, so a runtime out-of-gas during authorization processing leaves the recipient out of the EIP-7928 block access list.
…andler Replace the FrameInit::refundable_state_gas plumbing with a handler-level deduct/settle pair around the execution loop: - deduct_refundable_state_gas takes the EIP-2780 first-frame charge out of the budget the first frame will see (reservoir first, deficit spilling into regular gas, mirroring record_state_cost) before first_frame_input. - settle_refundable_state_gas seeds the charge onto the frame result's state-gas counters after the run, so last_frame_result reports it on success and rolls it back on revert or halt. A create that succeeds without deploying (sender nonce overflow) rolls it back directly. make_call_frame/make_create_frame lose the charge recording, the early-halt backstop, the precompile re-apply special case, and the interpreter gas carry-over; FrameInit drops the field. The inspector handler mirrors the deduct/settle pair.
… gas table Fold the hardcoded EIP7702_PER_AUTH_BASE_REGULAR branch in initial_tx_gas into tx_eip7702_per_empty_account_cost: the Amsterdam table's tx_eip7702_regular_gas slot now holds the state-independent REGULAR_PER_AUTH_BASE_COST (7,816, previously assigned twice with the pessimistic 15,816), and the accessor returns the slot alone for every fork. The pessimistic per-auth model this replaces is superseded by the runtime gas phase, so its remnants go with it: the state-gas addition in the accessor and the dead tx_eip7702_state_gas/tx_eip7702_state_refund helpers, the Amsterdam tx_eip7702_regular_refund entry (zeroed — the runtime phase charges per authority instead of refunding), and the unused EIP7702_PER_EMPTY_ACCOUNT_REGULAR constants in eip8037/eip8038. The legacy branch of test_initial_tx_gas_eip2780_runtime_split asserts the bundled 25,000 cost on a PRAGUE table, since Amsterdam params no longer model the eip2780-disabled combination.
Add ee-tests covering the EIP-2780 refundable first-frame charge when it spills into the regular gas budget (zero reservoir): create initcode success/revert/halt, the reservoir-laundering DELEGATECALL dance that leaves the parent's state_gas_spilled counter stale, and value transfers to empty precompile recipients (success and halting failure). Regression net for any refactor of where the refundable charge is recorded.
This was referenced Jul 13, 2026
The calldata floor is not discounted by state gas, so block_regular_gas_used is max(total - state_gas, floor_gas). Also tidy frame.rs clear() call sites.
Create a transaction-level Gas right after validation and thread it through pre-execution, execution and frame settle: - The EIP-2780 runtime gas phase records its charges directly on the tx-level gas (snapshot/restore on out-of-gas) instead of folding them back into InitialAndFloorGas; runtime_oog becomes a None return and a dedicated Handler::runtime_oog_result builds the halt. - The refundable first-frame state gas stays deducted on the tx-level gas; only the decision travels as charged_* flags on the first frame inputs, and last_frame_result refunds it from the outcome, mirroring how EthFrame::return_result refunds inner CALL/CREATE state charges. - last_frame_result settles the first frame into the tx-level gas via handle_reservoir_remaining_gas, like a parent frame settling a child. - Drop deduct_refundable_state_gas / settle_refundable_state_gas and checked_initial_gas_and_reservoir; add Gas::new_with_remaining_and_reservoir.
…tionOutput Remove charged_refundable_state_gas from InitialAndFloorGas: it is a runtime-gas-phase decision, not validated gas. pre_execution and apply_eip7702_auth_list now return Option<PreExecutionOutput> bundling the EIP-7702 refund with the flag (None = runtime out-of-gas), and execution takes the flag directly instead of InitialAndFloorGas, which is immutable after validation again.
Move the refund decision to FrameResult::refundable_state_gas, shared by EthFrame::return_result and Handler::last_frame_result. The handler's explicit halt branch reduces to refill_reservoir + spend_all since the transaction-level remaining is zero on halt. Also replace the !is_ok_or_revert() checks with is_halt() — ok/revert/error fully partition InstructionResult.
…eation Remove apply_eip2780_runtime_gas: the authorization charges stay in pre-execution (apply_auth_list_eip2780), while the recipient and create-target charges move into create_init_frame, merging with the target loading it already performed and deciding the charged_* input flags in place. create_init_frame, first_frame_input and execution return Result<Option<..>> so any runtime charge can out-of-gas; the caller includes the transaction as an out-of-gas halt with pristine rebuilt gas. The runtime-phase journal checkpoint travels open in PreExecutionOutput and is settled by execution: committed once the first frame is created, reverted (dropping applied delegations) on runtime out-of-gas.
Replace the WARM_ACCESS affordability pre-check with an upfront warm charge and a COLD_ACCOUNT_ACCESS_ADDITIONAL charge after the load, matching the EIP-2929 idiom. Charges made before an out-of-gas bail need no undo since the runtime out-of-gas path rebuilds the transaction-level gas.
…ution apply_eip7702_auth_list returns just the EIP-7702 refund again (None on authorization out-of-gas); Handler::pre_execution opens the runtime gas phase checkpoint, reverts it when the authorization charges run out of gas, and packs PreExecutionOutput for execution to settle.
…nally PreExecutionOutput.checkpoint is no longer optional: pre_execution always opens it before the authorization list (pre-Amsterdam it is always committed since no runtime charges apply), execution always settles it, and run_system_call opens its own since it skips pre-execution.
This was referenced Jul 14, 2026
The handler never needs the memory-expansion half of Gas, so the transaction-level gas becomes a bare GasTracker threaded through pre-execution, execution and frame settle: - tx_gas returns GasTracker; handle_reservoir_remaining_gas and the runtime-gas-phase helpers (create_init_frame, apply_auth_list_eip2780) take &mut GasTracker, with frame call sites going through Gas::tracker_mut. Drop Gas::new_with_remaining_and_reservoir and re-export GasTracker from the interpreter root. - Move the forwarded-gas spend_all into last_frame_result: the settle consumes the forwarded regular gas and returns the frame's unused part, dropping the explicit calls in execution, inspect_execution and runtime_oog_result. - create_init_frame derives the create target from tx.nonce() instead of loading the caller (the account nonce is bumped only at frame creation), and the first-frame new-account/create charges no longer gate on is_eip8037 (its gas params are zero pre-8037).
The EIP-2780 first-frame delegate load metered the access correctly but read the bytecode from an account load that does not fetch code, so with a provider-style database (code only via code_by_hash) the delegated recipient executed as empty code. Found as the glamsterdam-devnet-7 block 6371 gas divergence (bug017, -3,267 gas): the delegated code's execution was skipped whenever the delegate was not already cached with its code. Load the delegate through the code-loading variant and add a regression test with a database whose basic() omits bytecode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Aligns Amsterdam to the glamsterdam devnet-7 spec and the tests-glamsterdam-devnet@v7.0.0 fixtures. All state_tests and blockchain_tests pass (
revme statetest/revme btest). Supersedes #3789.EIP-2780 runtime gas phase (ethereum/EIPs#11844)
State-dependent transaction charges move from the intrinsic phase to a runtime gas phase applied as the first frame is entered:
REGULAR_PER_AUTH_BASE_COST(7,816) plus runtime per-authority charges (new-account state gas,ACCOUNT_WRITE, delegation-indicator bytes). The delegation-target access follows the standard EIP-2929 warm/cold model.apply_eip2780_runtime_gas) and travel to the first frame viaFrameInit/InitialAndFloorGas::refundable_state_gas, recorded on the frame's gas tracker so they stay refillable on revert/halt.Devnet-7 spec alignment
TX_BASE+ recipient/value charges) instead of flatTX_BASE.ACCOUNT_WRITEcharged on the first write to each authority, skipped only when already paid (sender, value-bearing recipient, or a preceding valid authorization).CREATE/CREATE2account-creation state gas charged conditionally at access in the creating frame (before the 63/64 split, after the endowment/nonce pre-checks), only when the destination does not exist; refilled LIFO when the create fails.SSTOREmust cover the slot's access cost before the implicit storage read; an unaffordable cold access skips the read so the slot stays out of the block access list.Other fixes
JournalEntry::CodeChangenow records the previous code hash/bytecode and restores them on revert — the old revert-to-empty assumption breaks once 7702 delegation set/clear on already-delegated accounts becomes revertible (the corrupted account leaked a phantom code write into the BAL).chainId(including type-0), optionalsecretKey(bad_v_r_s expects rejection), and >u64 nonces no longer panic the keep-going runner.create_state_gasfold, itslast_frame_resultrefill, andtarget_was_aliveonCreateFrame/CreateOutcomeare gone; the CREATE refill is driven solely by the recorded charge.