Skip to content

feat(specs): EIP-8037 diff-at-call-return state gas charging [DRAFT]#2683

Closed
spencer-tb wants to merge 30 commits intoethereum:eips/amsterdam/eip-8037from
spencer-tb:feat/eip-8037-diff-at-return
Closed

feat(specs): EIP-8037 diff-at-call-return state gas charging [DRAFT]#2683
spencer-tb wants to merge 30 commits intoethereum:eips/amsterdam/eip-8037from
spencer-tb:feat/eip-8037-diff-at-return

Conversation

@spencer-tb
Copy link
Copy Markdown
Contributor

@spencer-tb spencer-tb commented Apr 15, 2026

Summary

Prototype implementation of diff-at-call-return state gas charging for EIP-8037, as proposed in the #eip-8037 discord channel by @chfast.

Instead of charging state gas at each opcode (CREATE, CALL, SSTORE, SELFDESTRUCT), compute a single state diff at each call return point and charge based on actual state growth. This eliminates most edge cases from Maria's state gas accounting review (points 1-6).

State gas charges at call return draw from the reservoir first, when exhausted, the remainder spills into regular gas, preserving the existing spillover semantics.

Open Spec Questions & Resolution

1. Who pays state gas for value transfer creating a new account?

Resolution: the callee's call frame pays.

A calls B (non-existent address) with value = 1 wei.
The value transfer creates B's account inside B's call frame.
At B's call return, the state diff shows +1 new account.
State gas is deducted from the reservoir at B's return.
If B's gas budget cannot cover GAS_NEW_ACCOUNT, the call fails.
A sees failure (0 on stack).

Consequence: the caller must forward enough gas to cover state creation costs at the callee's return.

2. How does code deposit failure work?

Resolution: CREATE is all-or-nothing at call return.

CREATE deploys a contract with 10KB of code.
At CREATE's call return, the state diff shows:
  +1 new account
  +10240 bytes of deployed code

If the reservoir covers the full cost: CREATE succeeds.
If not: the entire CREATE reverts. No account, no code.

Consequence: no partial state where an account exists without code. The call either fully succeeds or fully reverts.

3. Can the reservoir grow beyond its initial value?

Resolution: yes, capped at transaction level.

Contract clears a storage slot (nonzero to zero).
At call return, the state diff shows -1 storage slot.
The reservoir is credited by the slot's state gas cost.

At transaction end: tx_gas_used = max(0, tx.gas - gas_left - reservoir).
The floor at zero prevents the user from being paid to execute.

Test changes

These tests assumed per-opcode state gas charging. With diff-at-return, state gas is computed from the net state diff at call return, which changes gas amounts, failure modes, and who pays.

test_block_gas_refund_eip7778_no_block_reduction: Updated expected gas_used. The set-then-restore SSTORE pattern (0 to 1 to 0) now produces zero state gas because the net state diff is zero. Previously, state gas was charged on the first write and partially refunded via refund_counter.

test_call_value_transfer_existing_account_no_state_gas: Changed target from empty account (amount=0) to alive account (amount=1) and forwarded all gas via Op.GAS. With diff-at-return, the callee pays state gas at its call return, so the callee needs enough gas to cover GAS_NEW_ACCOUNT if the target is not alive.

test_caller_reservoir_preserved_after_callee_diff_reverts: Replaces test_code_deposit_oog_preserves_parent_reservoir. The caller CALLs a factory that does CREATE with large code. The factory's state diff (account + code) exceeds its budget, so the factory reverts. The caller's reservoir is preserved and used for a subsequent SSTORE.

test_parent_reverts_when_create_diff_exceeds_budget: Replaces test_nested_create_code_deposit_cannot_borrow_parent_gas. A wrapper CALLs a factory that does CREATE deploying 15000 bytes. The factory's diff cost (~17.7M) exceeds its gas budget (~16.4M), so the factory reverts. The wrapper verifies the CALL returned 0 (failure).

166/166 blockchain_test passing (excluding pre-existing fork transition failures).

Related

spencer-tb and others added 28 commits April 10, 2026 14:50
…se (ethereum#2363)

* feat(spec-specs): update EIP-8037 to match latest spec revision

* feat(tests): add EIP-8037 state creation gas cost increase tests
Co-authored-by: Ben Adams <thundercat@illyriad.co.uk>
Add sstore_state_gas(), code_deposit_state_gas(), and create_state_gas()
calculator methods to Fork. Replace Spec constants and manual gas
arithmetic across all EIP-8037 tests with dynamic fork method calls.
Remove redundant Op.STOP, hardcoded numbers from docstrings, and add
fork: Fork parameter to all test functions that use fork methods.
Test CREATE with max initcode size using proper regular/state gas
split via the reservoir. Verifies gas boundary behavior with EIP-8037
two-dimensional metering.
Align EIP-8037 gas constant references with upstream renames:
- GAS_STORAGE_UPDATE → GAS_COLD_STORAGE_WRITE
- GAS_COLD_SLOAD → GAS_COLD_STORAGE_ACCESS
- GAS_WARM_ACCOUNT_ACCESS → GAS_WARM_ACCESS

Bump gas_limit on tests added to upstream after EIP-8037 branched,
which now need extra gas for EIP-8037 state gas costs.
… format runs in withdrawal request contract tests (ethereum#2532)

* fix(tests): prevent tx_gas_limit double-accumulation across fixture format runs in withdrawal request contract tests

* fix: mypy

* fix: ruff

* fix: ruff

* chore(tests): refactor fix to fork-aware transactions to prevent mutation

* chore(test): add a warning to all tests that could mutate vars; address in later PR

Add a lightweight repr-based snapshot hook to the filler plugin that warns whenever
any ``pytest.param`` value is mutated during a test run.

A subsequent PR could address this by returning values instead of mutating, then
flipping the hook to a hard failure.

---------

Co-authored-by: fselmo <fselmo2@gmail.com>
… gas validity test (ethereum#2583)

Co-authored-by: Stefan <22667037+qu0b@users.noreply.github.com>
Conditionally increase tx gas_limit (and env gas_limit where needed)
when fork >= Amsterdam to account for EIP-8037 state creation gas costs.

137 files, 9 with env gas_limit bumps. Headroom: 2,000,000.
Move MAX_CODE_SIZE check before gas charges, charge keccak hash
cost (regular gas) before code deposit state gas, and add tests
for over-max code size and reservoir preservation after OOG.
On CREATE/CREATE2 address collision the 63/64 gas allocation is
burned but was not added to regular_gas_used, leaving it invisible
to 2D block gas accounting. Per EIP-684 collision behaves as an
immediate exceptional halt, so the burned gas belongs in the regular
dimension.
…and subcall pattern

Co-authored-by: Mario Vega <marioevz@gmail.com>
The static test skip list and conftest were a temporary workaround for
EIP-8037 gas failures. The ported static tests in tests/ported_static/
replace this approach; failures are tracked in ethereum#2601.
…thereum#2603)

* fix(execute): use --sender-fund-refund-gas-limit for all funding txs

On EIP-8037 networks, simple value transfers to new accounts require
more than 21000 gas due to GAS_NEW_ACCOUNT state gas (112 * cpsb).
The default Transaction gas_limit of 21000 causes 'intrinsic gas too
low' errors during test setup.

Changes:
- contracts.py: Use 200000 gas for deterministic factory deployer funding
- pre_alloc.py: Pass sender_fund_refund_gas_limit to Alloc and use it
  for simple EOA funding transactions

Usage: --sender-fund-refund-gas-limit 200000

* chore: additional fixes for execute remote funds w/ higher gas limits

* fix: bump all gas limits to 200k for EIP-8037 state creation costs

- sender.py: bump --sender-fund-refund-gas-limit default 21000 → 200000
- pre_alloc.py: bump funding_gas_limit default 21000 → 200000
- pre_alloc.py: use configurable gas limit for per-test refund txs
- execute_recover.py: bump recovery refund gas limit 21000 → 200000

EIP-8037 charges GAS_NEW_ACCOUNT (112 × cost_per_state_byte = 131488)
for transfers to new accounts, making 21000 gas insufficient for all
funding, refund, and recovery transactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Felipe Selmo <fselmo2@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… gas (ethereum#2595)

Co-authored-by: spencer-tb <spencer.tb@ethereum.org>
…ode size validation (ethereum#2608)

* fix(spec): charge CREATE state gas after initcode size validation

Move charge_state_gas(STATE_BYTES_PER_NEW_ACCOUNT) from create()/create2()
into generic_create(), after the MAX_INIT_CODE_SIZE check.

Previously, state gas was charged before the initcode size check, so a
CREATE with oversized initcode would persist state_gas_used equal to the
account creation state gas cost (STATE_BYTES_PER_NEW_ACCOUNT *
cost_per_state_byte) even though no account was ever created and no state
was touched.

Reported by @AskDragan (reth): ethereum#2578 (comment)

* fix(spec): check static context before gas in CREATE/CREATE2

Move the is_static check from generic_create() into create() and
create2(), before stack pops and charge_gas(). This is consistent
with SSTORE, CALL, and SELFDESTRUCT which all check static context
before any gas charging.
@spencer-tb spencer-tb force-pushed the feat/eip-8037-diff-at-return branch 2 times, most recently from 659ad2f to 23602a0 Compare April 15, 2026 11:48
Replace per-opcode state gas charging with a single state diff
computed at each call return point. State gas is charged based on
actual state growth (new accounts, storage slots, deployed code)
rather than attempted operations.

Changes:
- Add compute_state_growth_cost() to state_tracker.py
- Add diff-at-return logic in process_message() with spillover
- Remove charge_state_gas from CREATE, CALL, SSTORE, SELFDESTRUCT
- Remove code deposit state gas charge from process_create_message
- Simplify incorporate_child_on_error (no state gas restore needed)
- Handle negative diffs (state removed credits reservoir)
- Saturating subtraction in tx accounting to prevent underflow
- test_block_gas_refund_eip7778_no_block_reduction: set-then-restore
  SSTORE has net zero state diff, update expected gas_used
- test_call_value_transfer_existing_account_no_state_gas: use alive
  target (amount=1) and forward all gas (Op.GAS) since callee pays
  state gas at return
- test_caller_reservoir_preserved_after_callee_diff_reverts: replaces
  test_code_deposit_oog_preserves_parent_reservoir, tests caller
  reservoir preserved when callee reverts due to expensive state diff
- test_parent_reverts_when_create_diff_exceeds_budget: replaces
  test_nested_create_code_deposit_cannot_borrow_parent_gas, tests
  all-or-nothing CREATE revert when diff exceeds budget
@spencer-tb spencer-tb force-pushed the feat/eip-8037-diff-at-return branch from 23602a0 to 422e17e Compare April 15, 2026 11:49
@felix314159 felix314159 force-pushed the eips/amsterdam/eip-8037 branch from 9755dba to 3e1d7c4 Compare April 16, 2026 10:34
@spencer-tb spencer-tb force-pushed the eips/amsterdam/eip-8037 branch from 3e1d7c4 to 44b47cc Compare April 17, 2026 16:01
@marioevz marioevz force-pushed the eips/amsterdam/eip-8037 branch from cc2cd47 to 44c4c15 Compare April 20, 2026 07:53
@spencer-tb
Copy link
Copy Markdown
Contributor Author

Closing as no longer using this spec change!

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.

4 participants