Skip to content

EIP-8037 + EIP-7778: state gas reservoir and refund-free block gas accounting - #2876

Open
anvacaru wants to merge 11 commits into
amsterdam-foundationfrom
eip-8037
Open

EIP-8037 + EIP-7778: state gas reservoir and refund-free block gas accounting#2876
anvacaru wants to merge 11 commits into
amsterdam-foundationfrom
eip-8037

Conversation

@anvacaru

Copy link
Copy Markdown
Contributor

Implements EIP-8037 : state-creation costs move to a per-transaction state-gas reservoir; execution gas and state gas are tracked separately, and gasUsed is their max. The new #finalizeTx rules also implement EIP-7778: block gas is accumulated from pre-refund totals while sender/miner settlement still applies the refund. Aligned against the EELS reference implementation.

Blocked by #2834 (EIP-7825). This branch does not kompile on its own: #txStateGasReservoir is defined as the transaction gas above the EIP-7825 cap (maxInt(0, GLIMIT -Int Gmaxtxgaslimit < SCHED >)), and the per-transaction block-gas-remaining admission check bounds the limit by that same cap, so Gmaxtxgaslimit/Ghastxgaslimit must already be declared. #2834 has to merge first; CI here will fail until it does. Verified locally that plugin and llvm kompile clean once #2834 is merged in.

EIP-8038 is excluded for now until it is confirmed to be included in the Glamsterdam hardfork; so Amsterdam keeps Osaka's cold-access costs and Gsstorereset/Rsstoreclear values. Fixtures that assume the combined 8037+8038 pricing stay masked until EIP-8038 lands.

Changes:

  • Three new <callState> cells (stateGasReservoir, stateGasSpilled, newAccountCharged) with frame push/pop, CALL/CREATE/SELFDESTRUCT charge sites, and refill on create-tx failure.
  • Amsterdam SSTORE regular-gas restructure (cold XOR warm access + first-change write cost, simplified refunds).
  • State-creation constant increases; per-tx block-gas-remaining admission preserved on pre-state-gas forks.
  • All tests/specs/ closed-form <callState> configs gain the three cells.

anvacaru and others added 11 commits July 27, 2026 12:11
… (const part)

AMSTERDAM overrides Gsstoreset/Gnewaccount/Gcreate/Gtxcreate/Gcallvalue
per the state-creation-cost repricing. Gnewaccount's new value (183600)
would otherwise leak into the existing EIP-7702 "new account" refund rule
(<refund> += Gnewaccount - Gauthbase), so that rule is now gated by a new
Ghasauthbaserefund flag (true through Osaka, false at AMSTERDAM) until
Wave 1b's state-gas AUTH_BASE charge replaces it outright.

Docs record two scoping findings from implementing this and EIP-7954:
Amsterdam's full state-gas reservoir mechanism (which most devnet-7
fixtures exercise via tx gas limits above today's Gmaxtxgaslimit) is not
const-only and is re-scoped into its own "Wave 1b" plan item, with the
mapping doc gaining a fully-cited mechanics section for it.
Ghasauthbaserefund was added without a DEFAULT-schedule case, triggering
a non-exhaustive-match warning from kompile on every build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ate gas reservoir mechanism (Wave 1b)

Amsterdam splits gas into a regular and a state dimension. A per-tx
reservoir (nonzero only when tx.gas exceeds Gmaxtxgaslimit) is drawn down
first by state-gas-priced operations -- SSTORE zero-to-nonzero, new-account
creation on CALL/CREATE/SELFDESTRUCT, EIP-7702 authority processing, and
code-deposit -- spilling into regular gas once exhausted.

Two new per-frame cells (<stateGasReservoir>, <stateGasSpilled>) carry this
through the call stack for free via the existing #pushCallStack/#popCallStack
snapshot-restore, avoiding a fuller EELS-style running-total mirror. Two new
block-level cells (<gasUsedRegular>, <gasUsedState>) track the split
separately; <gasUsed> remains the single source of truth, updated to
max(regular, state) at #finalizeTx so the existing header-check rule needs
no changes. Tx admission replaces the old flat Gmaxtxgaslimit cap with dual
per-block budget checks, one per dimension.

Also folds in the adjacent Amsterdam code-deposit repricing (flat
Gcodedeposit/byte cost becomes a keccak-per-word regular cost, plus a new
per-byte state-gas charge), since CREATE needed touching for the state-gas
charge anyway and every CREATE fixture needs both pieces correct together.

Verified against the full 5051-fixture Osaka+Prague regression suite (0
failures) and spot-checked against real Amsterdam EIP-8037 conformance
fixtures (SSTORE, CREATE, and CALL-family cases). A wider for_amsterdam
sweep surfaced 8 remaining edge-case fixtures (extreme-boundary gas
accounting on authority overspend, and a system-contract calling-convention
issue) documented as follow-up in the plan doc rather than blocking this
commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…7/8038)

The EIP-8037 work (commit 3ff7af55a) added the SSTORE state-gas half
(STORAGE_SET -> reservoir) but left the regular half in its Prague shape,
which charges the storage-access surcharge (Gcoldsload) AND a separate
Gsload operation cost. Amsterdam charges the access as cold XOR warm
(Gcoldsload=3000 if cold, else Gwarmstorageread=100, mutually exclusive)
plus STORAGE_WRITE (Gsstoreset=10000) only on the first real change, with a
simplified refund (a restore always refunds STORAGE_WRITE). Net effect of
the old shape: a cold no-op/dirty SSTORE was over-charged by 100 and a warm
first-change was under-charged by 100; both were masked wherever the
state-gas dimension dominated gasUsed=max(regular,state).

Restructure Csstore/Rsstore and the SSTORE #gasAccess surcharge with
Amsterdam-gated (Ghasstategas) variants matching the EELS reference; the
state-gas halves (SstoreStateGas/SstoreStateCredit) were already correct.
Unmask the two SLOTNUM sub-cases (value/slot_zero, gas_cost/out_of_gas)
that this fixes. Verified: osaka/prague sweep unchanged (5 fail / 5122
pass); Amsterdam SSTORE suites show the identical 9 pre-existing failures
before and after (no regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nd top-frame state-gas refill on create-tx failure

On the CREATE/CREATE2 opcode path the new-account state-gas charge is
lifted out of #create into #chargeCreateNewAccount, run before #create,
so an OOG there halts the calling frame cleanly instead of leaving a
dangling #codeDeposit that popped a never-pushed call frame. The charge
uses #chargeStateGasForCreate, whose out-of-gas rule drops the trailing
handler.

On create-transaction failure (init-code halt, code-deposit halt, and
oversized/invalid deployed code) #refillTopFrameStateGas discards the
top frame's refillable state gas (reservoir reset to the tx reservoir,
spilled folded into the burned regular gas), matching EELS
refill_frame_state_gas. It is gated to the top create-tx frame
(callDepth -1, empty 'to') and is inert on non-state-gas forks.

Unmasks the now-passing state_gas_create fixtures in failing.llvm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…efore the 63/64 split

EELS generic_create charges NEW_ACCOUNT state gas before computing
create_message_gas = max_message_call_gas(gas_left), so the charge draws from
the caller's full remaining gas. KEVM ran #allocateCreateGas first (from
NEW_ACCOUNT into regular gas hit a spurious OOG and burned the whole
transaction. Wrap the charge in #unallocateCreateGas / #allocateCreateGas so the
63/64 split is taken after it, matching the reference. The round trip is exact
(#allBut64th(G) +Int G /Int 64 ==Int G) and the charge is 0 without
Ghasstategas, so pre-Amsterdam behavior is unchanged; the early allocation is
kept because the #checkCreate failure paths refund <callGas>.

Also refill the top-frame state gas when a create transaction reverts: the
EVMC_REVERT #finishTx rule returned the spilled gas but left <stateGasSpilled>
set, so a reverted create kept its NEW_ACCOUNT charge in the state dimension.
The reference treats the prepare_dispatch charges as refillable -- they pay for
state that rolls back with the dispatched frame.

Unmask the four inner_create_succeeds_code_deposit_state_gas sub-cases, which
now report exactly 2 * 183600 + 1 * 1530 = 368730 state gas. The remaining
failed_create_tx_refills_top_frame_new_account[revert] sub-case stays masked:
its state gas is correctly 0 and the residual 256 gas is the calldata floor
(23320) versus the v7.1.0 fixture's pre-floor 23064 -- the known floor
placement drift, not a state-gas defect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…7702 auth charges, and CALL/SELFDESTRUCT charge sites with EELS

Per-frame spill: <stateGasSpilled> now resets at child-frame entry (#startFrameStateGas)
and #restoreStateGas accumulates the child's own spill into the parent on success, so
revert refunds (GAVAIL + spill) and LIFO state-gas credits stop double-counting ancestor
spill in nested chains.

Child-failure refunds: parent-side NEW_ACCOUNT charges (CALL value to dead account,
CREATE/CREATE2) are recorded in a new <newAccountCharged> callState flag and credited
back via #creditChargedNewAccount on every child failure path (return.exception/revert,
exceptional), matching EELS incorporate_child_on_error + credit_state_gas_refund.

Dispatch charges in-frame: the tx-level (callDepth -1) NEW_ACCOUNT charges for call and
create dispatch moved after #pushCallStack, so #finishTx's pop restores the post-auth
fold baseline exactly like EELS refill_frame_state_gas; #refillTopFrameStateGas is now
redundant and removed. The delegated-recipient dispatch access charge
(#chargeDispatchDelegationAccess, Amsterdam-only) implements EELS prepare_dispatch.

Auth charges: #loadAuthorities/#setDelegation/#addAuthority now thread EELS
set_delegation's written-accounts / no-auth-base / touched sets; nonexistent authorities
are charged Gnewaccount (state), first-writes Gaccountwrite (regular, sender and
value-bearing recipient exempt), and net-new delegations Gauthbase (state, once per
authority, pre-tx delegation exempt). Auth processing is bracketed by
now have OOG rules) rolls back applied delegations and burns exactly tx.gas.

Charge ordering: CALL charges extra regular gas, then NEW_ACCOUNT state gas, then
computes the 63/64 allocation from the remaining gas (#allocateCallGasStateGas), and
SELFDESTRUCT charges its state gas in the gas phase, so an OOG in either kills the
calling frame instead of being mis-caught by the inner #return pop or sticking on

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion on pre-state-gas forks

remaining (BLOCK_GAS_LIMIT - <gasUsed>) on schedules without Ghasstategas, matching EELS
check_transaction's gas_available test on every fork; the Amsterdam two-dimensional
admission branch is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…no-ops

Proof specs that omit or leave open <callState> get the three EIP-8037
cells framed as fresh symbolic variables, so `0 ~> #chargeStateGas`
nondeterministically branched across the charge/spill/OOG rules (the
`A <=Gas R` guard is undecidable for unconstrained R:Gas) and SSTORE
claims died on a spurious EVMC_OUT_OF_GAS. SstoreStateGas/Credit are
[concrete]-only, so symbolic storage arguments also wedged as stuck
gas terms even though both provably evaluate to 0 on pre-state-gas
schedules (Gstorageset is 0 there).

Add priority(40) no-op rules for literal-0 charges to #chargeStateGas,
#creditStateGas, #chargeStateGasForCreate, #chargeStateGasIntoCallGas,
and [simplification] rules reducing SstoreStateGas/SstoreStateCredit
to 0 when notBool Ghasstategas << SCHED >>.

Validated: STORAGE-SPEC.{myBool,setMyBool} and ERC20-SPEC.approve.success
(the two genuine prove-suite failures) now pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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