refactor(eip8037): defer state-gas to frame return via NewStateTracker#3634
refactor(eip8037): defer state-gas to frame return via NewStateTracker#3634rakita wants to merge 7 commits intosnobal-devnet-4from
Conversation
State-creating opcodes (SSTORE 0→x, CREATE/CREATE2, CALL/SELFDESTRUCT to an empty account, code deposit) no longer charge the reservoir inline. Instead they bump a simple per-frame counter on the Interpreter (`new_state`). At frame return — in `EthFrame::process_next_action` — the totals are reconciled with the gas tracker: * on ok: `state_gas_refunded` is added to the reservoir first, then `state_gas` is charged from it (may OOG, in which case the result is flipped to OutOfGas before journal commit) * on revert/halt: the counters are dropped; state work didn't happen, so no reservoir change `NewStateTracker` is intentionally minimal — two `u64` counters and `add_storage` / `remove_storage` / `add_create_account` / `remove_create_account` / `add_call_account` / `add_code_deposit_bytes` / `merge` / `clear` — and uses hardcoded Glamsterdam EIP-8037 values (`bytes_per_unit × CPSB_GLAMSTERDAM`) for now. `return_create` no longer commits/reverts the journal; it validates the deployed code, charges the regular code-deposit + hash gas, bumps `add_code_deposit_bytes`, and lets `process_next_action` apply state-gas and finalize the journal (set_code on success). `handle_reservoir_remaining_gas` is simplified: with the deferred model, the child's reservoir on revert/halt is just the parent pre-call value plus successful-grandchild contributions — the old `logical_refund / baseline / excess` math compensating for spill-on-revert is no longer necessary. Removes the `state_gas!` macro.
Merging this PR will degrade performance by 4.84%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | REVERT_50 |
109 µs | 113.1 µs | -3.59% |
| ❌ | Simulation | SLOAD_50 |
24.3 µs | 25.4 µs | -4.3% |
| ❌ | Simulation | SSTORE_50 |
30.2 µs | 31.5 µs | -4.08% |
| ❌ | Simulation | subcall_1000_transfer_1wei |
1.2 ms | 1.3 ms | -4.84% |
| ❌ | Simulation | subcall_1000_same_account |
1.1 ms | 1.2 ms | -3.3% |
| ❌ | Simulation | transact_1000tx_commit_inner_every_40 |
2.2 ms | 2.4 ms | -4.37% |
| ❌ | Simulation | subcall_1000_nested |
2.2 ms | 2.2 ms | -3.43% |
| ❌ | Simulation | transact_commit_1000txs |
2.8 ms | 2.9 ms | -3.97% |
Comparing dev5-new-way (05c0d7a) with main (bd89862)2
Footnotes
-
1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports. ↩
-
No successful run was found on
snobal-devnet-4(d9b5182) during the generation of this report, somain(bd89862) was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
Following dev4-frame-oog: return_create now owns all create-specific work (validation, code-deposit gas, state-gas counter bump, and set_code on success) and returns a `bool commit`. process_next_action becomes create-agnostic — it only forwards to return_create or treats a Call as committed when the interpreter returned ok, then runs the generic state-gas reconciliation and journal commit/revert that's shared by both paths.
…voir_remaining_gas
…ccount Gating purely on is_amsterdam_eip8037_enabled meant every CALL bumped the per-frame state-gas counter, so a frame making N non-empty-target CALLs would charge N × NEW_ACCOUNT_STATE_GAS at frame return. Restoring the state_gas_cost > 0 guard — non-zero only when EIP-8037 is enabled AND the CALL actually materializes a new empty account.
…t/halt When a frame reverts or halts, the per-frame state-gas counters are dropped (state work didn't happen). Add the reverted state_gas back to the frame's reservoir so the parent's revert/halt branch in handle_reservoir_remaining_gas accounts for it correctly.
Summary
Switches EIP-8037 state-gas from immediate charging to a deferred model. State-creating opcodes bump a simple per-frame counter on the
Interpreter(new_state); the totals are reconciled with the gas tracker at frame return insideEthFrame::process_next_action.state_gas_refundedadded to the reservoir first, thenstate_gascharged (may OOG → result flipped toOutOfGasbefore journal commit)Notes
NewStateTracker(crates/context/interface/src/cfg/new_state_tracker.rs) is intentionally minimal: twou64counters and a small set ofadd_*/remove_*methods. Uses hardcoded Glamsterdam EIP-8037 values (bytes_per_unit × CPSB_GLAMSTERDAM) for now — does not readGasParamsat runtime.host.rs, SELFDESTRUCT inhost.rs, CREATE/CREATE2 incontract.rs, CALL-with-value incall_helpers.rs) to bumpinterpreter.new_state.add_*(). Removes thestate_gas!macro.return_createno longer commits/reverts the journal; it validates code, charges regular code-deposit + hash gas, bumpsadd_code_deposit_bytes, and letsprocess_next_actionapply state-gas and finalize (set_codeon success).handle_reservoir_remaining_gasis simplified — the deferred model removes the spill-on-revert distortion, so the oldlogical_refund / baseline / excessmath is no longer needed.return_resultCreate-branch: on child failure →interpreter.new_state.remove_create_account()(replacesrefill_reservoir(create_state_gas)).Test plan
cargo build --workspacecleanrevm-ee-testscurrently fail because the test suite overridessstore_set_state_gasetc. viaGasParams(e.g.200_000) but the newNewStateTrackerignores those — the numbers it produces are EIP-8037 Glamsterdam defaults (e.g. SSTORE =32 × 1174 = 37_568). Need to either update test expectations, switchNewStateTrackerto readGasParams/cpsb, or both.