Skip to content

evm/vm: EIP-8037 v7 fixtures (param bump + spill/auth/top-level-create refunds)#4293

Open
gabrocheleau wants to merge 6 commits into
masterfrom
eip-8037-v7-fixtures
Open

evm/vm: EIP-8037 v7 fixtures (param bump + spill/auth/top-level-create refunds)#4293
gabrocheleau wants to merge 6 commits into
masterfrom
eip-8037-v7-fixtures

Conversation

@gabrocheleau
Copy link
Copy Markdown
Contributor

@gabrocheleau gabrocheleau commented May 13, 2026

Summary

Updates EIP-8037 to track the v7 dev fixtures (amsterdam/v700_mixed_with_other_eips).

Param bump to match the v7 spec:

  • costPerStateByte: 1174 → 1530
  • stateBytesPerStorageSet: 32 → 64
  • stateBytesPerNewAccount: 112 → 120

The block-gas-limit-derived costPerStateByte formula is dropped; v7 treats it as a flat constant. Dev test scripts + consumeBal test repointed from snobal_devnet_6_v110_* to v700_mixed_with_other_eips.

Spill refund on frame revert — frame-revert snapshot pop already refunded the reservoir-paid portion of state-gas; v7 also refunds the portion that spilled into gas_left. Compute spilled amount at revert time and add it on top of the snapshot's reservoir value.

Auth refund consistency — v7 spec requires the existing-authority 7702 refund to also decrement execution_state_gas_used. Initialize executionStateGasUsed = -existingAuthStateGasRefund so later charges climb back from a negative baseline.

Top-level CREATE intrinsic refund on failure — capture isCreate at frame entry and, in the revert handler at depth=0 for a creation tx, add stateBytesPerNewAccount * costPerStateByte back to the reservoir.

Tx-create SELFDESTRUCT intrinsic refund — if a creation tx's initcode SELFDESTRUCTs the freshly-created contract in the same tx, refund the intrinsic in the state dimension only: block_state_gas_used reflects 0, but the sender still pays the gross intrinsic. Tracked in a new createdAccountIntrinsicStateGas map distinct from createdAccountStateGas.

Test plan

EIP-8037 group (amsterdam/v700_mixed_with_other_eips/eip8037_state_creation_gas_cost_increase):

Commit passing / total
baseline (master, post bal@v7.0.0) ~287 / 455
param bump 287 / 455
+ spill refund 317 / 455
+ auth-refund decrement 353 / 455
+ top-level CREATE refund 366 / 455
+ tx-create SELFDESTRUCT intrinsic refund 372 / 455

Full Amsterdam dev suite (v700_mixed_with_other_eips):

Branch passing / total
master 1181 / 2042
this PR 1672 / 2042 (+491)

Items not landed in this PR (deep dive)

Isolate SSTORE 0→x→0 refund on same-frame revert

After tracing several affected fixtures (revert_discards_descendant_storage_clear_credit_through_depth, subcall_revert_does_not_leak_grandchild_storage_clear_credit):

  • Our state-gas accounting already correctly isolates the SSTORE clear refund per-frame: the journal-based snapshot pops restore reservoir + executionStateGasUsed at frame entry, naturally discarding child-frame credits when a parent reverts. Debug logs confirm txStateGas, txRegularGas, and totalGasSpent all match expected fixture values exactly.
  • The actual mismatch is in the BAL (EIP-7928) hash: the failing assertion is generated block level access list correct. Diff shows our impl emits 5 EXTRA storageReads entries on reverted-child frames. Current bal.ts:160-203 preserves storage reads across reverts (and converts reverted storageChanges to reads); the v7 fixture expects these entries discarded.

This is an EIP-7928 spec/behavior question, not EIP-8037. The state-gas isolation the task names is already in place.

CREATE address-collision burned regular gas counted in block_regular_gas_used

Located the v7 test source at ethereum/execution-specs:devnets/bal/7/tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_create.py. The expected formula:

gas_at_create_after_state = gas_limit - intrinsic - factory_pre_create
                          - memory_expansion - create_base - new_account
retained = gas_at_create_after_state // 64
baseline_gas_used = gas_limit - retained - new_account + post_create_static

Decoded: under v7 the CREATE opcode pre-charges new_account = stateBytesPerNewAccount * costPerStateByte (= 183,600) of state-gas at opcode entry (before forwarding to inner frame). On address collision, that pre-charge is refunded (reservoir credit + executionStateGasUsed decrement). The user effectively pays gas_limit - retained - new_account + post_create_static.

Our current impl charges CREATE state-gas only at successful frame-exit (stateGasCreate in _executeCreate), so:

  • No pre-charge to refund → no new_account adjustment on collision
  • Forwarded gas is burned in full per legacy EIP-684 (executionGasUsed: message.gasLimit)
  • Result: 246,565 vs expected 65,834

Properly fixing this requires restructuring CREATE state-gas to charge at opcode entry instead of at frame-exit success, with mirrored refund paths on collision / revert / halt. That's a non-trivial change touching opcodes/gas.ts (CREATE dynamic gas), _executeCreate (charge timing), and the snapshot mechanism (the pre-charge needs to be in the per-frame snapshot so reverts properly unwind it). Out of scope for this PR.

This affects roughly 80 of the remaining 83 failing fixtures in the eip8037_state_creation_gas_cost_increase group; they all share the pattern "CREATE opcode runs, then halts/reverts/collides and the new_account state-gas should refund".

@gabrocheleau gabrocheleau changed the base branch from feat/eip-8037 to master May 13, 2026 20:01
Update the EIP-8037 state-gas constants to match the v7 dev fixtures
(`amsterdam/v700_mixed_with_other_eips`):

  costPerStateByte:       1174 -> 1530
  stateBytesPerStorageSet:  32 ->   64
  stateBytesPerNewAccount: 112 ->  120
  stateBytesPerAuthBase:    23 (unchanged)

The v7 spec also drops the block-gas-limit-derived costPerStateByte
formula and treats it as a flat constant again. Replace the parametric
helper with a thin wrapper that reads common.param('costPerStateByte').

Point the dev blockchain test scripts and the consumeBal test at the
new fixture directory v700_mixed_with_other_eips (was
snobal_devnet_6_v110_mixed_with_other_eips). The fixtures submodule is
bumped separately on this branch.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 9.52381% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.11%. Comparing base (535e435) to head (a764ca0).

Additional details and impacted files

Impacted file tree graph

Flag Coverage Δ
block 87.33% <ø> (ø)
blockchain 88.82% <ø> (ø)
common 93.44% <ø> (ø)
evm 59.84% <10.20%> (-0.28%) ⬇️
mpt 89.73% <ø> (+0.08%) ⬆️
statemanager 78.04% <ø> (ø)
static 91.35% <ø> (ø)
tx 87.81% <ø> (ø)
util 80.83% <ø> (ø)
vm 52.91% <7.14%> (-0.12%) ⬇️

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.

The frame-revert path was popping the reservoir snapshot, which
restores the reservoir-paid portion of state-gas, but the spilled
portion (state-gas that overflowed into gas_left via useGas) was
not credited back. Per the v7 spec, the FULL state_gas_used on a
reverted/halted frame must refund to the parent's reservoir (or to
the tx-level reservoir at depth 0).

Compute the spilled portion at revert time as
  usedThisFrame - reservoirPaidThisFrame
and add it back to the post-pop reservoir. The parent's useGas of
the child's executionGasUsed (which includes the spilled state-gas)
is now offset by the reservoir credit, so the net cost to the user
is just the regular gas.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

📦 Bundle Size Analysis

Package Size (min+gzip) Δ
binarytree 17.5 KB ⚪ ±0%
block 43.8 KB ⚪ ±0%
blockchain 69.9 KB ⚪ ±0%
common 25.4 KB ⚪ ±0%
devp2p 17.7 KB ⚪ ±0%
e2store 87.3 KB ⚪ ±0%
ethash 61.8 KB ⚪ ±0%
evm 63.8 KB 🔴 +0.1 KB (+0.09%)
genesis 272.2 KB ⚪ ±0%
mpt 21.9 KB ⚪ ±0%
rlp 1.7 KB ⚪ ±0%
statemanager 41.3 KB ⚪ ±0%
testdata 43.8 KB ⚪ ±0%
tx 20.9 KB ⚪ ±0%
util 13.2 KB ⚪ ±0%
vm 156.8 KB 🔴 +0.1 KB (+0.08%)
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

Per the v7 spec, the existing-authority auth refund both:
  - refunds STATE_BYTES_PER_NEW_ACCOUNT * costPerStateByte to the
    state_gas_reservoir, AND
  - decreases execution_state_gas_used by the same amount.

Previously we only did the reservoir refund. Now execution_state_gas_used
is initialized to -existingAuthStateGasRefund at the start of the tx so
that subsequent SSTORE / CREATE state-gas charges climb back from a
negative baseline. tx_state_gas = intrinsicState + executionStateGasUsed
ends up reduced by the refund, which makes block.state_gas_used
consistent with the per-tx tx_gas_used accounting.

+36 tests in eip8037 group (317 -> 353).
…ATE failure

When a creation tx fails at the top level (initcode revert / OOG /
exceptional halt / address collision / oversized code), the intrinsic
stateBytesPerNewAccount * costPerStateByte that runTx paid up-front
was not refunded. The frame-revert snapshot pop only refunds state-gas
CHARGED during the frame; the intrinsic was paid before EVM execution
started and so isn't tracked in any snapshot.

Capture isCreate at frame entry (before _generateAddress sets
message.to) and, in the revert handler at depth=0, add the intrinsic
amount back to the reservoir and decrement execution_state_gas_used.
…te SELFDESTRUCT

If a creation tx's initcode SELFDESTRUCTs the freshly-created contract in
the same tx, refund the intrinsic stateBytesPerNewAccount * costPerStateByte
in the state dimension so block_state_gas_used reflects that no new account
persists. Per the v7 spec the sender STILL pays the gross intrinsic on
their balance (the reservoir is NOT credited), so receipt cumulativeGasUsed
and the sender debit remain at the pre-refund value — this is purely a
state-dim accounting adjustment.

Track the intrinsic separately in a new createdAccountIntrinsicStateGas
map (populated at depth=0 CREATE success). The deferred-refund path in
runTx now distinguishes the two pools:
  - createdAccountStateGas        (frame-exit charges):
      credit reservoir + decrement execution_state_gas_used
  - createdAccountIntrinsicStateGas (tx-create intrinsic):
      accumulate into txCreateIntrinsicStateGasRefund, applied ONLY to
      txStateGas at final accounting time (no reservoir credit, no
      mid-computation executionStateGasUsed mutation that would skew
      execution_regular_gas_used).

+6 tests in eip8037 group (366 -> 372).
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