Skip to content

feat(amsterdam): glamsterdam devnet-7 alignment (EIP-2780 runtime gas phase, fixtures v7.0.0)#3795

Open
rakita wants to merge 23 commits into
mainfrom
glam-devnet-7
Open

feat(amsterdam): glamsterdam devnet-7 alignment (EIP-2780 runtime gas phase, fixtures v7.0.0)#3795
rakita wants to merge 23 commits into
mainfrom
glam-devnet-7

Conversation

@rakita

@rakita rakita commented Jul 13, 2026

Copy link
Copy Markdown
Member

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:

  • Create transactions: new-account state gas is charged at runtime, only when the deployment target does not already exist; initcode revert/halt unwinds it LIFO.
  • EIP-7702: the pessimistic per-auth intrinsic charge is replaced by an intrinsic 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.
  • 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. The runtime halt returns the EIP-8037 reservoir instead of consuming it, matching the spec's top-frame halt settlement.
  • The phase is 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 only after the authorization charges are covered, so a runtime out-of-gas during authorization processing leaves it out of the BAL.
  • Depth-0 charge decisions live in the handler (apply_eip2780_runtime_gas) and travel to the first frame via FrameInit/InitialAndFloorGas::refundable_state_gas, recorded on the frame's gas tracker so they stay refillable on revert/halt.

Devnet-7 spec alignment

Other fixes

  • 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 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.
  • Superseded pre-devnet-7 create state-gas paths removed (each Amsterdam devnet supersedes the previous): the intrinsic create_state_gas fold, its last_frame_result refill, and target_was_alive on CreateFrame/CreateOutcome are gone; the CREATE refill is driven solely by the recorded charge.

rakita added 6 commits July 7, 2026 15:01
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.
@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 3.52%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 59 regressed benchmarks
✅ 122 untouched benchmarks
⏩ 1 skipped benchmark1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

Footnotes

  1. 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.

rakita added 6 commits July 13, 2026 10:33
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.
rakita added 9 commits July 14, 2026 04:07
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.
rakita added 2 commits July 14, 2026 22:24
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant