Skip to content

feat: EIP-8037 (State Creation Gas Cost Increase) — scaffolding + partial implementation#4285

Open
gabrocheleau wants to merge 5 commits intomasterfrom
feat/eip-8037
Open

feat: EIP-8037 (State Creation Gas Cost Increase) — scaffolding + partial implementation#4285
gabrocheleau wants to merge 5 commits intomasterfrom
feat/eip-8037

Conversation

@gabrocheleau
Copy link
Copy Markdown
Contributor

@gabrocheleau gabrocheleau commented May 6, 2026

Summary

Initial scaffolding and partial implementation of EIP-8037 (State Creation Gas Cost Increase) on top of the new snobal-devnet-6@v1.1.0 dev fixtures pulled in by #4284.

The EIP introduces a separate "state-gas" dimension with its own per-tx reservoir and a 2D block-level gas accounting model. This PR lands the foundational pieces; remaining items (CREATE/CALL state-gas, journal-based revert refund, 7702 auth state-gas, SELFDESTRUCT deferred refund, refund cap formula) are listed below for follow-up.

Commits

  1. common: scaffold EIP-8037 — register the EIP with required EIPs (2780, 6780, 7702, 7825, 7976, 7981) and add the parameter blocks (regular-gas overrides + new state-gas constants costPerStateByte, stateBytesPerStorageSet, stateBytesPerNewAccount, stateBytesPerAuthBase, systemMaxSstoresPerCall).
  2. vm/evm: reservoir plumbing + activate in Amsterdam — add stateGasReservoir and executionStateGasUsed to EVM / EVMInterface. runTx splits intrinsic gas into regular and state portions and applies the spec's reservoir formula:
    execution_gas      = tx.gas - intrinsic_gas
    regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic_regular_gas
    gas_left           = min(regular_gas_budget, execution_gas)
    reservoir          = execution_gas - gas_left
    
  3. evm/vm: SSTORE state-gas charge + refund + 7825 cap relaxation — add chargeStateGas / refillStateGasReservoir on the interpreter (reservoir-first, gas-left spill, OOG on overflow). SSTORE charges 32 × costPerStateByte on original=0, current=0, new!=0; refills on original=0, current!=0, new=0. Also relaxes the EIP-7825 tx-level cap so it applies to the regular-gas dimension only under 8037.
  4. vm: block-level 2D gas accounting — runBlock keeps independent block_regular_gas_used / block_state_gas_used accumulators and sets gas_used = max(...) for the block header. Per-tx pre-execution check uses the spec's two-dimension form.

Test results vs the snobal-devnet-6@v1.1.0 fixtures

Group Passing Total Was on master
state_gas_reservoir 18 57 0
state_gas_pricing 4 74 0

22/131 of the targeted dev fixtures pass on this branch (up from 0/131). The other ~109 failures all need pieces not implemented yet (see below) — they are not regressions, they are the known-unimplemented surface of the EIP.

Not yet implemented (follow-up commits)

Item Tests it should unblock
CREATE / CREATE2 frame-exit state-gas ((112+L)·costPerStateByte) create_state_gas_scales_with_cpsb, creation-tx reservoir tests
CALL* with value to non-existent account state-gas (112·costPerStateByte) call_new_account_state_gas_scales_with_cpsb and combos
Frame-revert / exceptional-halt journal hook for state-gas nested_failure_*, subcall_failure_*, top_level_failure_*, revert_discards_* (~20 tests)
EIP-7702 auth state-gas (intrinsic per-auth + non-empty refund) auth_state_gas_scales_with_cpsb
SELFDESTRUCT deferred refund selfdestruct_new_beneficiary_scales_with_cpsb
20% refund cap formula (tx.gas - gas_left - reservoir) refund_cap_includes_state_gas, sstore_refund_scales_with_cpsb
System call gas adjustment (30M + reservoir headroom) system-contract paths

Notes

  • The EIP spec is still moving; some fixtures are reportedly incorrect (per the R&D notes from Dragan referenced in our internal discussion). Triage of which failing fixtures are actually wrong vs. genuinely unimplemented should happen alongside the next round of work.
  • Depends on chore: point dev fixtures to snobal-devnet-6@v1.1.0 #4284 (the fixtures-bump PR) being merged first.

Test plan

  • npm run build (common, tx, evm, vm)
  • npm run test:est:dev:blockchain against state_gas_reservoir and state_gas_pricing subdirectories
  • Run the full test:est:dev:blockchain suite to confirm no regressions outside the EIP-8037 fixtures
  • Triage remaining failures against the EIP-8037 spec / Dragan's R&D notes

Adds the EIP entry and parameter blocks for EIP-8037 without
activating it on any hardfork yet.

- common: register EIP 8037 with required EIPs (2780, 6780, 7702,
  7825, 7976, 7981) and minimum hardfork Amsterdam
- evm/params: regular-gas overrides for state-creating opcodes
  (sstoreSetGas 2900, createGas 9000, callNewAccountGas 0,
  createDataGas 0, codeDepositHashWordGas 6) and the new state-gas
  constants (costPerStateByte 1174, stateBytesPerStorageSet 32,
  stateBytesPerNewAccount 112, stateBytesPerAuthBase 23,
  systemMaxSstoresPerCall 16)
- tx/params: EIP-7702 regular-gas overrides under 8037
  (perAuthBaseGas 7500, perEmptyAccountCost 0)

No behavior change — these only take effect when EIP-8037 is
activated, which lands in a follow-up commit alongside the
reservoir plumbing.
Step 2 of EIP-8037 (State Creation Gas Cost Increase):

- common/hardforks: include 8037 in the Amsterdam EIP list, activating
  the regular-gas overrides from the EIP-8037 params blocks (sstore set
  2900, create 9000, callNewAccount 0, perAuthBaseGas 7500,
  perEmptyAccountCost 0, txCreationGas 9000, createDataGas 0).
- evm: add `stateGasReservoir` and `executionStateGasUsed` to EVM and
  EVMInterface. These hold the per-tx reservoir state and are read /
  written by opcodes and frame-exit hooks in follow-up commits.
- tx/params: add `txCreationGas: 9000` under 8037.
- vm/runTx: split intrinsic gas into regular and state portions when
  8037 is active, applying the spec's reservoir formula:
    execution_gas      = tx.gas - intrinsic_gas
    regular_gas_budget = TX_MAX_GAS_LIMIT - intrinsic_regular_gas
    gas_left           = min(regular_gas_budget, execution_gas)
    reservoir          = execution_gas - gas_left
  The reservoir is installed on the EVM before runCall, the EVM runs
  with `gas_left` as its gasLimit, and totalGasSpent post-call accounts
  for any reservoir consumption.
- vm/runTx: corrects the 7702 intrinsic regular-gas computation under
  8037 (getDataGas charges per-auth via `perEmptyAccountCost`, which is
  0 under 8037; the per-auth `perAuthBaseGas` is added back here).
- vm/runTx: skip the legacy 7702 existing-authority refund when 8037
  is active (it would otherwise produce a negative regular-gas refund;
  the state-gas refund is added in a later commit).

State-gas is not yet charged on any opcode — those land in steps 3
and 4. The full state_gas_reservoir / state_gas_pricing fixture
groups still fail (57/57 reservoir, expected: tests require actual
state-gas accounting); this commit is plumbing only.
Step 3 of EIP-8037:

- evm/interpreter: add `chargeStateGas(amount, ctx)` and
  `refillStateGasReservoir(amount, ctx)` methods. `chargeStateGas` draws
  from the per-tx reservoir first, spilling to `gasLeft` (which can
  OOG) when the reservoir is exhausted. `refillStateGasReservoir`
  always refills the reservoir regardless of whether the original
  charge spilled, per spec.
- evm/opcodes/SSTORE: at the end of the opcode, when the slot was
  zero at the start of the transaction (`original == 0`):
    current=0, new!=0 -> charge stateBytesPerStorageSet * costPerStateByte
    current!=0, new=0 -> refill stateBytesPerStorageSet * costPerStateByte
  Other transitions produce no state-gas adjustment, per the EIP-8037
  table. The regular-gas portion of the SSTORE charge is still applied
  by the existing dynamic-gas function (sstoreSetGas = 2900 under
  8037, was 20000).
- tx/util: under 8037 the EIP-7825 transaction gas-limit cap applies
  to the regular-gas dimension only, so the tx-construction-time cap
  check is skipped. The runTx path validates
  `max(intrinsic_regular_gas, calldata_floor_gas_cost) <= TX_MAX_GAS_LIMIT`
  instead.
- vm/runTx: use `tx.common.param('maxTransactionGasLimit')` for the
  cap (the param lives in the tx params block).

state_gas_reservoir tests: 18/57 passing (was 0/57). Remaining
failures expect account-creation state gas, block-level 2D gas
accounting, and revert-path refunds, which land in step 4.
Step 4 (partial) of EIP-8037:

- vm/types: add `txStateGas` and `txRegularGas` to RunTxResult so
  runBlock can keep two independent block-level accumulators.
- vm/runTx: under 8037, expose per-dimension breakdowns:
    tx_state_gas    = intrinsic_state_gas + execution_state_gas_used
    tx_regular_gas  = intrinsic_regular_gas + execution_regular_gas_used
  where execution_regular_gas_used backs out the slice of state-gas
  that spilled into gas_left from the executionGasUsed total.
- vm/runBlock: accumulate `block_regular_gas_used` and
  `block_state_gas_used` independently and set
    gas_used = max(block_regular_gas_used, block_state_gas_used)
  for the block header (per spec). The per-tx pre-execution check is
  also relaxed to the spec's two-dimension form:
    min(TX_MAX_GAS_LIMIT, tx.gas) <= regular_available
    tx.gas                         <= state_available

What still fails (tracked for the next commits, not yet implemented):

- CREATE / CREATE2 frame-exit state-gas charge for the new account
  and for code deposit ((stateBytesPerNewAccount + L) * costPerStateByte).
- CALL* with value to a non-existent account: frame-exit state-gas
  charge of stateBytesPerNewAccount * costPerStateByte.
- Frame-revert / exceptional-halt refund of state-gas charges to the
  parent reservoir (needs journal hook so SSTORE charges in a child
  frame are unwound on revert).
- EIP-7702 authorization state-gas: intrinsic per-auth charge of
  (stateBytesPerNewAccount + stateBytesPerAuthBase) * costPerStateByte and refund
  of stateBytesPerNewAccount * costPerStateByte when authority is non-empty.
- SELFDESTRUCT deferred refund at end-of-tx for accounts created and
  destructed in the same tx.
- 20% refund cap formula: still uses combined gas; spec uses
  tx.gas - gas_left - state_gas_reservoir for the cap base.
- System call gas adjustment (30M + reservoir headroom for system
  contracts).

Test status against the snobal-devnet-6@v1.1.0 fixtures:
- state_gas_reservoir: 18/57 passing (was 0/57 before this branch)
- state_gas_pricing:    4/74 passing (was 0/74 before this branch)
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 23.86364% with 134 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.55%. Comparing base (4479713) to head (571b864).

Additional details and impacted files

Impacted file tree graph

Flag Coverage Δ
block 87.33% <ø> (ø)
blockchain 88.82% <ø> (ø)
common 93.44% <ø> (ø)
evm 60.56% <27.08%> (-0.67%) ⬇️
mpt 89.64% <ø> (+0.08%) ⬆️
statemanager 78.04% <ø> (ø)
static 91.35% <ø> (ø)
tx 87.81% <100.00%> (ø)
util 80.83% <ø> (ø)
vm 53.53% <18.98%> (-1.74%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

📦 Bundle Size Analysis

Package Size (min+gzip) Δ
binarytree 17.5 KB ⚪ ±0%
block 43.8 KB 🔴 +0.0 KB (+0.09%)
blockchain 69.9 KB 🔴 +0.0 KB (+0.06%)
common 25.4 KB 🔴 +0.0 KB (+0.07%)
devp2p 17.7 KB ⚪ ±0%
e2store 87.3 KB 🔴 +0.0 KB (+0.04%)
ethash 61.8 KB 🔴 +0.0 KB (+0.07%)
evm 63.3 KB 🔴 +0.8 KB (+1.21%)
genesis 272.2 KB ⚪ ±0%
mpt 21.9 KB ⚪ ±0%
rlp 1.7 KB ⚪ ±0%
statemanager 41.3 KB 🔴 +0.0 KB (+0.04%)
testdata 43.8 KB ⚪ ±0%
tx 20.9 KB 🔴 +0.0 KB (+0.16%)
util 13.2 KB ⚪ ±0%
vm 156.1 KB 🔴 +1.3 KB (+0.83%)
wallet 15.0 KB ⚪ ±0%

Values are minified+gzipped bundles of each package entry. Workspace deps are bundled; external deps are excluded.

Generated by bundle-size workflow

@gabrocheleau gabrocheleau force-pushed the feat/eip-8037 branch 2 times, most recently from 52f744d to df77986 Compare May 6, 2026 05:48
gabrocheleau added a commit that referenced this pull request May 6, 2026
- evm: snapshot stateGasReservoir + executionStateGasUsed at every
  message-level journal.checkpoint(); on revert / exceptional halt
  restore them, refunding any state-gas charged within the reverted
  frame and undoing any in-frame reservoir refills (per spec). On
  commit drop the snapshot. The snapshot stack is reset at the start
  of each tx.
- evm: charge new-account + code-deposit state-gas at the end of a
  successful CREATE / CREATE2 frame:
    state-gas = (stateBytesPerNewAccount + L) * costPerStateByte
                                          (depth > 0 = CREATE opcode)
    state-gas = L * costPerStateByte
                                          (depth 0 = creation tx;
                                           the stateBytesPerNewAccount
                                           portion is intrinsic)
  The regular code-deposit cost is replaced under 8037 by
    6 * ceil(L/32) * codeDepositHashWordGas.
  State-gas is drawn from the reservoir first, spilling to gas_left;
  if the spill cannot fit in the frame's remaining budget the CREATE
  is converted to OOG and the snapshot is restored.
- evm: charge new-account state-gas on successful CALL* frame exit
  when the destination account did not exist (or was empty) before
  the frame and the call carried non-zero value (mirroring the
  existing callNewAccountGas trigger). delegatecall is excluded.
  Precompile addresses are excluded too — they are code-only entities,
  not real account state, so funding a precompile via CALL-with-value
  does not create new "stored" account state.
- evm/vm: EIP-8037 SELFDESTRUCT deferred refund (account + code-deposit
  portion). EVM tracks the state-gas charged per newly-created address;
  runTx, after the call returns, refunds those amounts directly to the
  reservoir for any address that was both created and SELFDESTRUCTed in
  the same tx (per EIP-6780 + EIP-8037). The refund is not subject to
  the 20% cap. Storage-slot per-account tracking is a follow-up.
- vm/runTx: clear the EVM's per-frame snapshot stack and the per-tx
  created-account state-gas map at the start of each tx so leftover
  state from a previous tx cannot leak.

Test results vs the snobal-devnet-6@v1.1.0 dev fixtures:

  Full Amsterdam dev suite      1215 / 1985 passing  (parent #4285: 1187)
  state_gas_reservoir            27 / 57            (parent: 18)
  state_gas_pricing               4 / 74            (parent: 4)

Net +28 vs parent across the full dev suite, 0 regressions in any
group. The journal-based revert refund unblocks `nested_failure_*`,
`subcall_revert_*`, and several `top_level_failure_*` cases. The
precompile bypass keeps `test_bal_precompile_funded` and
`test_transfer_to_special_address-precompile_*` passing. The
SELFDESTRUCT deferred refund keeps `test_create_selfdestruct_*` and
the same-tx `test_selfdestruct_to_*` cases passing.

`state_gas_pricing` is unchanged. The remaining pricing failures
appear to expect costPerStateByte to scale with the block gas limit
(test names include `_scales_with_cpsb`), while the EIP text I worked
from defines costPerStateByte as a fixed constant of 1174. Triage in
the next branch (could be a spec change since the reference, or one
of the incorrect-fixture cases Dragan flagged).

Still queued for the next branch:
- EIP-7702 authorization state-gas (intrinsic per-auth + non-empty refund)
- Storage-slot per-account tracking for SELFDESTRUCT refund
- 20% refund cap formula update (tx.gas - gas_left - reservoir)
- System-call gas adjustment (30M + reservoir headroom)
- costPerStateByte parametric vs constant triage
Still queued for the next branch:
- EIP-7702 authorization state-gas (intrinsic per-auth + non-empty refund)
- Storage-slot per-account tracking for SELFDESTRUCT refund
- 20% refund cap formula update (tx.gas - gas_left - reservoir)
- System-call gas adjustment (30M + reservoir headroom)
- costPerStateByte parametric vs constant triage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant