Skip to content

feat(specs): EIP-8037 state-delta counter (frame-diff alternative) [DRAFT]#2765

Closed
spencer-tb wants to merge 3 commits intoethereum:forks/amsterdamfrom
spencer-tb:eips/amsterdam/eip-8037-frame-diff
Closed

feat(specs): EIP-8037 state-delta counter (frame-diff alternative) [DRAFT]#2765
spencer-tb wants to merge 3 commits intoethereum:forks/amsterdamfrom
spencer-tb:eips/amsterdam/eip-8037-frame-diff

Conversation

@spencer-tb
Copy link
Copy Markdown
Contributor

Summary

Prototype implementation of EIP-8037 state gas charging via a per-frame state-delta byte counter on TransactionState, fresh from forks/amsterdam. Successor to PR #2683's diff-at-return approach.

Two commits:

  1. feat(specs): EIP-8037 diff-at-return state gas charging (735947d4) — initial port of feat(specs): EIP-8037 diff-at-call-return state gas charging [DRAFT] #2683's diff-at-return mechanism onto fresh forks/amsterdam. Computes a state diff at each process_message return.
  2. feat(specs): EIP-8037 state-delta counter on TransactionState (5f1f5fe1) — replaces the diff with a counter on TransactionState.state_delta_bytes. Hooked at the five low-level state funcs (set_account, set_storage, set_code, destroy_account, destroy_storage).

Final state is the counter approach; the diff commit is preserved in history for reviewability.

Why counter over diff

Two real problems with the diff approach surfaced during review:

  1. Code refund gap. destroy_account doesn't clean code_writes (codes keyed by hash, may be shared). The diff never sees code disappear on same-tx CREATE+SELFDESTRUCT, so we had to add an explicit refund block in fork.py recomputing account + storage + code by hand.
  2. Distance from clients. Clients meter via journals — every state change is recorded. fjl's framing in the #eip-8037 Discord thread was "meter the in-progress state diff", i.e. running tally not call-boundary diff.

Counter solves both — destroy_account explicitly debits -= len(code) (no gap), and the abstraction matches journal semantics.

Hook table

Op Δ bytes Where
Account None → Some +112 set_account
Account Some → None -112 set_account
Storage 0 → nonzero +32 set_storage
Storage nonzero → 0 -32 set_storage
Code deployed (fresh code_hash) +len(code) set_code
Storage destroyed -32 per non-zero slot destroy_storage
Code on account destruction -len(code) destroy_account

7702 auths run pre-EVM (before depth-0 snapshot), so their account creation is counted but absorbed into the snapshot — frame_delta doesn't see them. Intrinsic.state pre-charge + reservoir refund for existing-account auths handles this (unchanged from current behavior).

Charge at frame return

frame_delta_bytes = tx_state.state_delta_bytes - snapshot.state_delta_bytes
growth_cost = frame_delta_bytes * int(cost_per_state_byte)
already_paid = int(message.state_gas_reservoir) - int(evm.state_gas_left)
this_call_cost = growth_cost - already_paid
# (same reservoir/spillover/revert logic as diff-at-return)

The counter is stored on TransactionState, included in copy_tx_state and restore_tx_state — naturally rolls back on revert, no extra journal needed.

SELFDESTRUCT same-tx refund

Moved into depth-0 process_message, before the counter charge:

if message.depth == Uint(0) and not evm.error:
    for address in evm.accounts_to_delete:
        if address in tx_state.created_accounts:
            destroy_account(tx_state, address)

The destroy hooks (-112 account, -len(code) code, -32 per non-zero slot) credit tx_state.state_delta_bytes naturally. The subsequent frame_delta * cpsb calculation nets these credits against earlier charges. Removed:

  • The explicit account+storage+code refund block in fork.py:process_transaction (was added in commit 735947d4).
  • The for address in tx_output.accounts_to_delete: destroy_account(...) loop in fork.py (now done in process_message).

Files modified (final state, counter approach)

src/ethereum/forks/amsterdam/fork.py           |   removed selfdestruct refund block + destroy loop + 3 imports
src/ethereum/forks/amsterdam/state_tracker.py  |   +counter field, hooks in 5 funcs, removed 3 diff helpers
src/ethereum/forks/amsterdam/vm/interpreter.py |   replaced diff with counter, added depth-0 destroy loop

Open items

Related

Move the state_delta_bytes counter from TransactionState onto Evm and bump it inline at opcode/interpreter sites instead of inside state_tracker hooks. State_tracker stays pure (no Evm import, no gas-accounting concerns).

- Evm.state_delta_bytes is the per-frame byte counter; sums via incorporate_child_on_success.
- SSTORE bumps inline after set_storage based on 0 ↔ nonzero transition.
- CALL value-to-empty bumps in process_message after move_ether (on the child evm so revert handles it).
- CREATE/CREATE2 bumps in process_create_message on the success path: +112 + len(contract_code) after set_code.
- SELFDESTRUCT same-tx destruction: depth-0 destroy loop debits -112 - len(code) - 32×slots inline before destroy_account.
- COST_PER_STATE_BYTE replaces the unused dynamic-cpsb formula constants and state_gas_per_byte() function. validate_transaction/calculate_intrinsic_cost drop the gas_limit parameter.
@spencer-tb spencer-tb closed this Apr 28, 2026
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