Skip to content

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

Merged
rakita merged 28 commits into
mainfrom
glam-devnet-7
Jul 22, 2026
Merged

rakita merged 28 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

codspeed Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 4.76%

⚠️ 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

❌ 132 regressed benchmarks
✅ 49 untouched benchmarks
⏩ 1 skipped benchmark1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation transfer 14.2 µs 15.5 µs -8.58%
❌ Simulation transfer_finalize 17 µs 18.3 µs -7.15%
❌ Simulation JUMPDEST_50 16.1 µs 17.2 µs -6.41%
❌ Simulation transact_1000tx_commit_inner_every_40 2.3 ms 2.4 ms -6.08%
❌ Simulation JUMP_50 18.3 µs 19.4 µs -5.84%
❌ Simulation GASLIMIT_50 18.8 µs 19.9 µs -5.56%
❌ Simulation CHAINID_50 18.9 µs 20 µs -5.54%
❌ Simulation CALLDATASIZE_50 18.9 µs 20 µs -5.52%
❌ Simulation CALLER_50 19 µs 20.1 µs -5.5%
❌ Simulation PUSH5_50 19.1 µs 20.2 µs -5.48%
❌ Simulation MSTORE_50 19.2 µs 20.3 µs -5.44%
❌ Simulation GAS_50 18.7 µs 19.8 µs -5.43%
❌ Simulation PUSH1_50 18.8 µs 19.9 µs -5.42%
❌ Simulation PC_50 18.8 µs 19.9 µs -5.42%
❌ Simulation MSIZE_50 18.8 µs 19.9 µs -5.4%
❌ Simulation NUMBER_50 18.9 µs 19.9 µs -5.4%
❌ Simulation CODESIZE_50 18.9 µs 20 µs -5.4%
❌ Simulation RETURNDATASIZE_50 18.9 µs 20 µs -5.39%
❌ Simulation CALLVALUE_50 18.9 µs 20 µs -5.39%
❌ Simulation PUSH15_50 19.4 µs 20.5 µs -5.39%
... ... ... ... ... ...

ℹ️ 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 (4557e1e) with main (41e8b33)

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 7 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.
# Conflicts:
#	crates/handler/src/pre_execution.rs
Restore the InitialAndFloorGas parameter alongside the transaction-level
GasTracker so chain variants that meter the authorizations against the
intrinsic/floor gas keep their override point. The default implementation
does not use it.
@rakita
rakita merged commit f47a3e9 into main Jul 22, 2026
28 of 33 checks passed
This was referenced Jul 22, 2026
rakita added a commit to alloy-rs/evm2 that referenced this pull request Jul 30, 2026
)

Aligns Amsterdam to the **glamsterdam devnet-7** spec and the
`tests-glamsterdam-devnet@v7.2.0` fixtures. All Amsterdam `state_tests`
and `blockchain_tests` pass (interpreter + JIT + AOT); no regressions in
earlier-fork state suites or the unit/ee-test suite.

Ports bluealloy/revm#3795 to evm2's architecture.

## EIP-2780 runtime gas phase (ethereum/EIPs#11844)

State-dependent transaction charges move from the intrinsic phase to a
runtime phase applied as the first frame is entered. Running out of gas
there is an **included out-of-gas halt** consuming all regular gas, not
a transaction rejection.

- **Create transactions**: account-creation state gas is charged at the
create frame's entry (`execute_create_message`), conditional on the
destination not already existing. The intrinsic `create_state_gas` and
its refund path are removed.
- **EIP-7702**: the intrinsic per-auth charge drops to the
state-independent `REGULAR_PER_AUTH_BASE_COST` (7,816); `ACCOUNT_WRITE`,
new-account, and delegation-bytes state gas are charged per authority at
runtime on a transaction-level `GasTracker` (the `RuntimeAuthCharges`
accounting), stopping at the first unaffordable charge. A runtime
out-of-gas reverts the applied delegations and includes the tx as an OOG
halt.
- **Delegated recipient (depth 0)**: resolution is deferred into the
frame (`apply_eip2780_call_charges`) and the target load is gated on gas
(`skip_cold_load`, as nested calls do), so a cold, unafforded target
stays out of the EIP-7928 block access list.
`MessageResult::runtime_gas_oog` signals a recipient-charge OOG back to
the 7702 handler so it drops the delegations too.

The CALL/CREATE runtime charges live in the shared
`execute_call_message` / `execute_create_message` (gated on `depth == 0`
+ feature, not tx type), so **all transaction types** get them; only the
authorization-specific charges are in the 7702 handler.

## Devnet-7 spec alignment

- **ethereum/EIPs#11836**: calldata floor anchored on the decomposed
intrinsic base instead of flat `TX_BASE`.
- **ethereum/EIPs#11891**: 7702 `ACCOUNT_WRITE` charged once per
authority, skipped when already paid (sender / value-recipient / a
preceding valid authorization).
- **ethereum/EIPs#11858**: `CREATE`/`CREATE2` account-creation state gas
charged conditionally at access (endowment/nonce pre-check + a single
destination read), refilled on failure. Mirrored in the JIT builtin.
- **ethereum/EIPs#11854**: `SSTORE` covers the slot's access cost before
the implicit read; an unaffordable cold access skips the read
(`Evm::sstore` now honors `skip_cold_load`).
- **EIP-8282**: builder deposit/exit system-contract addresses updated.
- **EIP-7623** floor binds the block regular-gas component
(`TxResult::regular_gas_spent = max(total - state, floor)`).

## Cleanups

- Remove the superseded devnet-6 create accounting
(`create_initial_state_gas`, `refund_create_state_gas`,
`MessageResult::created_target_was_alive`, `settle_gas`'s `is_create`
path).
- Remove `rollback_failed_execution`: failed execution is already rolled
back to the message's own checkpoint (and halt gas zeroed) inside
`execute_message`, so it was redundant.

## Follow-up refactors

- **Message construction**: `Message` now carries its `destination` —
the CREATE/CREATE2 address is derived when the message is constructed
(`Message::derive_destination`) instead of being re-derived at frame
entry.
- **EIP-7702 authorization unification**: both gas regimes share one
`apply_auth_list` loop (validate → account → apply) driven by an
`AuthAccounting` strategy — `RuntimeAuthCharges` (EIP-2780 runtime
metering, abortable mid-list) and `AuthRefunds` (pre-Amsterdam
pessimistic-intrinsic refunds, owning the EIP-8037 gating). The handler
tail funnels every exit through a single `settle` closure (`settle_oog`
for the runtime OOG halts), and `initial_gas_and_reservoir` loses its
always-zero `state_refund` parameter — the pre-Amsterdam state refund is
credited straight to the reservoir at the call site, matching
execution-specs `set_delegation`. Crate constants are now `pub`.

The `Message`-carries-bytecode refactor (with the EIP-8037 create depth
check) and the regular→execution gas rename are split out into the
stacked PR #329.

## Fixtures / harness

- Bump devnet fixtures to v7.2.0 (setup script, CI, `AGENTS.md`).
- eest: honor an explicit per-transaction `chainId` (v7 fixtures test
type-0 `INVALID_CHAINID`).

## Testing

- Amsterdam `state_tests`: 406/406; `blockchain_tests`: 657/657.
- JIT + AOT Amsterdam state suites pass on v7.2.0.
- Unit + ee-tests: 2829/2829; Prague/Osaka/Cancun/Berlin/London/Shanghai
state suites unchanged.

Two revm changes were no-ops in evm2: EIP-7708 (already implemented) and
`JournalEntry::CodeChange` recording prior code (evm2's `AccountChange`
already snapshots the full prior `AccountInfo`).
RealiCZ added a commit to megaeth-labs/revm that referenced this pull request Sep 16, 2026
…net-7 alignment (EIP-2780 runtime gas phase, fixtures v7.0.0) (bluealloy#3795)

Conflict: MIGRATION_GUIDE.md only, 1 hunk / 24 lines. v112 has no Unreleased
preamble; took the upstream section verbatim. No source conflict.
@RealiCZ RealiCZ mentioned this pull request Sep 18, 2026
43 tasks done
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