Skip to content

feat(amsterdam): EIP-2780 runtime gas phase (ethereum/EIPs#11844) - #3789

Closed
rakita wants to merge 5 commits into
mainfrom
rakita/eip2780-runtime-gas-phase
Closed

rakita wants to merge 5 commits into
mainfrom
rakita/eip2780-runtime-gas-phase

Conversation

@rakita

@rakita rakita commented Jul 7, 2026

Copy link
Copy Markdown
Member

Implements the EIP-2780 update from ethereum/EIPs#11844 (head 9d2e4e17): state-dependent transaction charges move from the intrinsic phase to a runtime gas phase, applied as the first frame is entered. Intrinsic gas stays the sole input to the validity check.

Everything is gated on the existing CfgEnv::enable_amsterdam_eip2780; older forks and 2780-disabled Amsterdam keep the previous code paths (the eip8037.rs ee-tests, which disable the flag, pass unchanged).

Changes

  • Create transactions: create_state_gas (183,600) is no longer intrinsic. It is charged in make_create_frame at depth 0, on the frame's gas tracker, only when the deployment target does not already exist — so a create to a pre-existing (balance-only) target pays no new-account state gas, and an initcode revert/halt unwinds the charge LIFO via rollback_state_gas. The old top-level refill in last_frame_result is gated to the non-2780 path.
  • EIP-7702 decomposition: the pessimistic per-auth intrinsic charge (15,816 regular + 218,790 state) and its refunds are replaced by an intrinsic REGULAR_PER_AUTH_BASE_COST = 7,816 (new eip8038::EIP7702_PER_AUTH_BASE_REGULAR) plus runtime charges accumulated while applying the auth list (apply_auth_list_eip2780): a non-existent authority pays STATE_BYTES_PER_NEW_ACCOUNT × CPSB state + ACCOUNT_WRITE regular; net-new delegation bytes pay STATE_BYTES_PER_AUTH_BASE × CPSB state, at most once per authority. No refunds.
  • Delegated recipient: the delegation-target access follows the standard EIP-2929 warm/cold model (WARM_ACCESS if pre-warmed, e.g. by an access list) instead of a flat COLD_ACCOUNT_ACCESS, charged in the runtime phase (apply_eip2780_runtime_gas).
  • Runtime out-of-gas is a halt, not invalidity: apply_eip2780_runtime_gas checks affordability of the whole runtime phase (new InitialAndFloorGas::checked_initial_gas_and_reservoir). On OOG it reverts a checkpoint spanning the runtime phase — undoing applied delegations — and sets InitialAndFloorGas::runtime_oog; Handler::execution / inspect_execution then include the transaction as an out-of-gas halt consuming all gas without entering the first frame.
  • Affordable runtime charges are folded into initial_regular_gas / initial_state_gas, so they are deducted pre-frame and survive in-frame reverts (delegations persist on in-frame failure). The recipient new-account state gas stays on the frame tracker so a revert rolls it back with the transfer.

Spec notes / ambiguities

  • The PR body on ethereum/EIPs is stale (describes a warm-access-floor split); the head spec charges the recipient/authority at the cold rate unconditionally at intrinsic — implemented per the head.
  • The ACCOUNT_WRITE bullet's "(i.e. the authority differs from tx.to)" parenthetical conflicts with Test Case 8 and the Interactions section ("ACCOUNT_WRITE for a non-existent authority"); implemented as !existed, matching the old model's net charges.
  • Delegation set→clear→set on one authority within a tx charges the indicator bytes once, with no refund on clear ("at most once per authority").

Testing

  • 9 new ee-tests covering the reference cases: create to existing target (no state gas), included-as-halt runtime OOG for create/call/7702 (with delegation reverted), warm vs cold delegation target, new vs existing authority charges; plus a gas_params unit test for the intrinsic split.
  • cargo nextest run --workspace (default / all / no-default features), clippy, fmt, typos: clean.
  • Fixtures: pre-Amsterdam fork suites identical to main. for_amsterdam: 2663/2666 — the 3 failures (stInitCodeTest create-halt tests) encode the old devnet-6 refill-on-halt semantics; under the new spec the gas_left-drawn portion of the LIFO refill is consumed by the exceptional halt. Fixtures need regeneration for the updated spec.

Follow-up: op-revm needs alignment with the runtime_oog execution path.

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.
@codspeed

codspeed Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 4.07%

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

❌ 4 regressed benchmarks
✅ 177 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.3 ms 2.4 ms -5.01%
❌ Simulation transact_commit_1000txs 2.9 ms 3 ms -4.14%
❌ Simulation transfer_finalize 17.3 µs 18 µs -4.02%
❌ Simulation transfer 14.5 µs 14.9 µs -3.1%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing rakita/eip2780-runtime-gas-phase (a0b11be) with main (8a6b28a)

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 4 commits July 9, 2026 14:58
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.
@rakita

rakita commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Will move to glam-devnet-7 branch

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