diff --git a/cmd/erigon/node/config_snapshot_test.go b/cmd/erigon/node/config_snapshot_test.go index daef72bea6d..91bdb790867 100644 --- a/cmd/erigon/node/config_snapshot_test.go +++ b/cmd/erigon/node/config_snapshot_test.go @@ -116,7 +116,7 @@ func TestConfigDefaults(t *testing.T) { require.Equal(t, uint64(1), snap.NetworkID, "default network should be mainnet") require.True(t, snap.StateStream, "state stream should be enabled by default") require.True(t, snap.InternalCL, "internal CL (Caplin) is on by default") - require.False(t, snap.ExperimentalBAL, "experimental BAL should be off by default") + require.True(t, snap.ExperimentalBAL, "experimental BAL should be on by default on bal-devnet branches") require.False(t, snap.KeepExecutionProofs, "keep execution proofs should be off by default") // Snapshot defaults @@ -210,6 +210,6 @@ func TestConfigSnapshotStability(t *testing.T) { require.True(t, snap.StateStream) require.True(t, snap.SnapProduceE2) require.True(t, snap.SnapProduceE3) - require.False(t, snap.ExperimentalBAL) + require.True(t, snap.ExperimentalBAL) require.False(t, snap.NoDownloader) } diff --git a/common/dbg/experiments.go b/common/dbg/experiments.go index 16de27177cf..179cf68914e 100644 --- a/common/dbg/experiments.go +++ b/common/dbg/experiments.go @@ -77,7 +77,7 @@ var ( CaplinSyncedDataMangerDeadlockDetection = EnvBool("CAPLIN_SYNCED_DATA_MANAGER_DEADLOCK_DETECTION", false) - Exec3Parallel = EnvBool("EXEC3_PARALLEL", false) + Exec3Parallel = EnvBool("EXEC3_PARALLEL", true) numWorkers = runtime.NumCPU() / 2 Exec3Workers = EnvInt("EXEC3_WORKERS", numWorkers) ExecTerseLoggerLevel = EnvInt("EXEC_TERSE_LOGGER_LEVEL", int(log.LvlWarn)) diff --git a/docs/plans/20260427-eip8037-journal-state-gas-spec.md b/docs/plans/20260427-eip8037-journal-state-gas-spec.md new file mode 100644 index 00000000000..30669cd4e5c --- /dev/null +++ b/docs/plans/20260427-eip8037-journal-state-gas-spec.md @@ -0,0 +1,724 @@ +# EIP-8037 Journal-Walk State-Gas Accounting (bal-devnet-4 v2) + +## Context + +EIP-8037 (Amsterdam) introduced multi-dimensional gas: a regular-gas dimension and a state-gas dimension that prices state growth. The current implementation charges state gas eagerly at every state-touching opcode (SSTORE, CREATE, CALL-to-empty, SELFDESTRUCT, code deposit) and unwinds those charges via ad-hoc refund rules: + +- SSTORE 0→X→0 inline credit via `evm.CreditStateGasRefund`, with cross-frame plumbing through `state_gas_refund_pending` for DELEGATECALL/CALLCODE chains (PR #2733). +- CREATE silent-failure refunds of `GAS_NEW_ACCOUNT` on insufficient balance, nonce overflow, depth, and address collision (PR #2704). +- Code-deposit halt unwind that rolls back initcode state-gas charges (PR #2595). +- Same-tx SELFDESTRUCT refund driven by `IntraBlockState.SameTxSelfDestructedNewAccounts()` at tx finalize. +- Frame revert/halt accounting in `evm.handleFrameRevert` that restores spill, refund-tracker, and reservoir at depth-dependent boundaries. + +This sprawl has produced bugs the snøbal-devnet-4 test suite has surfaced, and there is concern that more cases are lurking. The new design eliminates per-opcode state-gas charging entirely. State gas is computed once per call/create frame at commit time by walking the journal segment that frame produced. On revert/halt the journal undoes everything → no state gas charged for that frame. All historical refund paths collapse into one rule: each frame's net contribution is `walk_total − committed_children_total`; charge if positive, credit if negative. + +## Design Decisions (confirmed) + +1. **Charge timing**: pure frame-end. Opcodes charge regular gas only. +2. **Reservoir model**: keep `mdgas.MdGas{Regular, State}`, `SplitTxnGasLimit`, full-reservoir-pass-to-child, and spill-into-`gas_left` when reservoir exhausted. +3. **Per-frame delta accounting**: each frame at commit walks its journal segment and computes `delta = walk_total − committed_children_total`. If `delta > 0`, charge `delta × cpsb` from the frame's reservoir (with spill into `gas_left`; OOG → revert). If `delta < 0`, credit `|delta| × cpsb` back to the frame's reservoir AND decrement `evm.executionStateGas`. This single mechanism handles every cross-frame interaction (SSTORE 0→X→0 across siblings/DELEGATECALL chains, create-then-destroy across frames, etc.) without any refund plumbing. The walk uses **`storageChange.originalValue`** (= tx-entry value, captured at SetState time) as the "new slot" determinant, matching spec PR 11573's rule and ensuring OOG attribution at the spec-correct frame. +4. **SELFDESTRUCT filter applied only at the top-frame walk**: sub-frames walk *without* the `.newlyCreated && .selfdestructed` filter (so EIP-6780 net-0 cases temporarily over-count and OOG mid-execution exactly as the spec wants — state IS allocated transiently). At the top frame's commit (depth==0), the walk DOES apply the filter, which makes the top-frame walk `< committed_children_total`, producing the negative delta that credits the over-charge back. Multiple selfdestructs of the same account are deduplicated naturally because the walk keys on address (first occurrence per address). +5. **Top-level CREATE tx**: pass `excludeAddr` so the contract address (already covered by the 112×cpsb intrinsic) is not double-counted by the top-frame walk. Snapshot stays where it is; the top-level `createObjectChange` is just skipped in the walk. The exclusion is derived in-place from `depth == 0` inside `evm.create()` (where the contract address is already a local parameter) — no coordination from TxnExecutor needed, no field on the EVM struct. +6. **Per-depth `committedChildBytes` accumulator on the EVM**, populated by each child as it commits. No journal-range bookkeeping (`frameChildRanges` is unnecessary because the running total of children's contributions, combined with the parent's full-segment walk and credit-on-negative-delta, is strictly more correct than range exclusion — the latter would miss cross-frame SSTORE 0→X→0). +7. **System calls (EIP-2935 / EIP-4788 / EIP-7002 / EIP-7251 etc.) charge state gas internally via the journal walk, but with a dedicated pre-sized reservoir** (per [EIPs PR 11573 commit `d2a0230`](https://github.com/ethereum/EIPs/pull/11573/changes/d2a023056187fb17c94e9477cadd076a0f817760)). The system-call MdGas is initialised as `{Regular: 30_000_000, State: STATE_BYTES_PER_STORAGE_SET × CPSB × SYSTEM_MAX_SSTORES_PER_CALL}` where `SYSTEM_MAX_SSTORES_PER_CALL = 16`. Total `SYSTEM_CALL_GAS_LIMIT = 30_000_000 + 32 × 1_174 × 16 = 30_601_088`. System calls remain not subject to `TX_MAX_GAS_LIMIT`, do not count against the block gas limit, and do not contribute to either `block_regular_gas_used` or `block_state_gas_used` — but they DO walk the journal at frame commit and DO charge state gas from their dedicated reservoir (so each system call can SSTORE up to 16 fresh slots without OOG'ing on state gas). + +## Algorithm + +At each call/create frame: + +``` +Frame entry (evm.call / evm.create): + frameStart = ibs.JournalLength() + push 0 onto evm.committedChildBytes // per-depth accumulator + +Frame body runs (regular gas charged inline as today; no state-gas charges). + +Frame commit (success path, IsAmsterdam, !RestoreState): + frameEnd = ibs.JournalLength() + applyFilt = (depth == 0) // filter only at top frame + excludeC = address if (in evm.create() && depth == 0) else NilAddress + // address is the local param of evm.create(); + // for evm.call() it's always NilAddress + walkTotal = ibs.ComputeFrameStateBytes(frameStart, frameEnd, applyFilt, excludeC) + childTotal = evm.committedChildBytes[depth] + delta = int64(walkTotal) - int64(childTotal) + + if delta > 0: + stateGas = uint64(delta) * cpsb + deduct stateGas from gas.State first, spill into gas.Regular + if insufficient → err = ErrOutOfGas (then handleFrameRevert path) + else evm.executionStateGas += stateGas + elif delta < 0: + creditGas = uint64(-delta) * cpsb + gas.State += creditGas // credit current frame + evm.executionStateGas -= creditGas // shrink tx total + // delta == 0 → nothing to do + + pop evm.committedChildBytes[depth] + if depth > 0: + evm.committedChildBytes[depth-1] += walkTotal // propagate to parent + +Frame revert/halt: + poppedChildBytes = pop evm.committedChildBytes[depth] // do NOT add to parent + if IsAmsterdam && !RestoreState && poppedChildBytes > 0: + // EIP-8037: restore all state gas consumed by committed descendants + // to this frame's reservoir (which is then propagated to the parent + // via restoreChildGas). Spec: "On child revert or exceptional halt, + // all state gas consumed by the child, both from the reservoir and + // any that spilled into gas_left, is restored to the parent's + // reservoir." At depth==0 this naturally zeroes evm.executionStateGas + // because the top frame's accumulator holds the sum of all charged + // bytes for the tx. + restoreGas = poppedChildBytes * cpsb + gas.State += restoreGas + evm.executionStateGas -= restoreGas // saturate at 0 + RevertToSnapshot // journal undoes everything + burn regular gas on exceptional halt (same as today) + +Tx finalize (TxnExecutor.Execute): + blockStateGas = intrinsicStateGas + evm.executionStateGas + // No refund logic in TxnExecutor — credits/charges already baked in. +``` + +`ComputeFrameStateBytes(start, end, applyFilter, excludeCreate)` walks `journal.entries[start:end]` linearly (no range exclusions). It accumulates: + +- **Account creations** — `createObjectChange` and `resetObjectChange`, first occurrence per address. Skip if `excludeCreate == account`. Skip if stateObject is nil. If `applyFilter` AND stateObject is `.newlyCreated && .selfdestructed`, skip. Otherwise +112. +- **Code deposit** — `codeChange`, first per address. If `applyFilter` AND `.newlyCreated && .selfdestructed`, skip. When `prevhash` is empty AND current `stateObject.code` is non-empty, +`len(code)`. +- **Storage 0→non-zero** — `storageChange`, dedup by `(address, key)`. If `applyFilter` AND `.newlyCreated && .selfdestructed`, skip. **Use the entry's `originalValue` field (= tx-entry value, captured at SetState time via `GetCommittedState`).** When `originalValue.IsZero()` AND current value (`stateObject.GetState(key)`) is non-zero, +32. +- All other entry types (`selfdestructChange`, `balanceChange`, `nonceChange`, `refundChange`, `addLogChange`, `transientStorageChange`, `accessList*Change`, `touchAccount`, `balanceIncrease*`, `fakeStorageChange`) are skipped. + +**Why `originalValue` (tx-entry) instead of first-prev-in-segment**: matches spec PR 11573's "new slot" rule, which references the slot's tx-entry value (not frame-entry). Without this, a parent that wrote 0→X and then a child that wrote X→Y would over-charge the parent and under-charge the child relative to the spec, producing the same tx-level total but different per-frame OOG attribution. With `originalValue`, the deepest committed observer that sees `originalValue=0 AND current!=0` is the one that charges, and OOG fires at the spec-correct frame. + +## Why this resolves the existing edge cases + +| Old edge case | New behavior | +|---|---| +| SSTORE 0→X→0 same frame | Walk: `originalValue=0 AND current=0` → 0 bytes. No refund needed. | +| SSTORE 0→X→0 across DELEGATECALL/CALLCODE chains, or sibling frames | Creator frame sees `originalValue=0 AND current=non-zero` at its commit time → +32 charge. Clearer frame and parent see `originalValue=0 AND current=0` → 0. Parent's `committedChildBytes` includes the creator's +32, parent's walk yields 0 → parent.delta=−32 → credit. `state_gas_refund_pending` plumbing deleted entirely. | +| CREATE silent failure (collision, balance, depth, nonce) | No journal entries produced (snapshot pushed/popped) → 0 bytes for that frame. No `CreditStateGasRefund` needed. | +| Code-deposit halt | `SetCode` not called when deposit OOGs → no `codeChange` in journal → 0 code bytes. Plus regular-gas rollback (still needed). | +| Same-tx SELFDESTRUCT (within or cross frame, single or multiple times) | Sub-frames walk without filter → may charge transiently (spec-correct: state was real during execution). At top-frame commit the filter applies, making top.walk < committed_children_total → negative top.delta → credit. Per-address dedup is automatic (walk keys on address; multiple selfdestructs of same account → 1 skip). | +| Frame OOG state-gas restoration to parent | Frame revert → journal undoes → no commit-time charge fired. Nothing to restore. | +| Top-level revert state-gas restoration | `evm.executionStateGas = 0` on top-level revert. Tx-state-gas = intrinsic only. | +| Per-frame OOG attribution matches spec | `originalValue` (tx-entry) drives "new slot" check; the deepest committed observer with `originalValue=0 AND current!=0` charges and OOGs, matching spec PR 11573. | + +## Alignment with EIP-8037 PR 11573 + +PR [ethereum/EIPs#11573](https://github.com/ethereum/EIPs/pull/11573) ("Update EIP-8037: fixed CPSB + frame accounting") rewrites EIP-8037 to drop per-opcode charging in favour of frame-end accounting. Our plan is consistent with the PR's intent and produces matching tx-level totals, but the per-frame mechanism differs. + +### Where we match +- **Constant CPSB = 1174** (PR's quantization-removed table). Already hardcoded in `eip8037.go`. +- **Frame-end charging only** — opcodes do not emit state-gas charges. +- **Reverted/halted frames produce no debits or credits.** +- **SELFDESTRUCT accounting deferred to top-frame commit** (spec calls it "transaction end"; for us this is `evm.create()` / `evm.call()` at `depth==0`, which is the same moment). +- **Each created/cleared slot or account accounted for exactly once across the call stack** (we get this property automatically from the first-prevalue-in-segment rule + `committedChildBytes` delta math). +- All previously-needed refund machinery (SSTORE 0→X→0 inline credit, CREATE silent-failure refund, code-deposit halt unwind, same-tx SELFDESTRUCT refund at TxnExecutor) is removed in both designs. + +### Where the mechanism differs +The spec emits per-slot charges and refunds at every frame commit by examining three reference values (`tx-entry`, `frame-entry`, `frame-exit`). Our plan emits per-frame *deltas* (charge or credit) where the walk uses `tx-entry` (via `storageChange.originalValue`) as the new-slot determinant, combined with `committedChildBytes` accumulator + delta math. + +The two approaches produce the same per-frame charge attribution for the **new slot** rule (which is the rule that matters for OOG), and the same tx-level totals for every scenario. + +What we don't emit explicitly: +- The spec's **"cleared slot, zero at tx start" refund** (`frame-entry != 0 AND frame-exit == 0 AND tx-entry == 0`) is not emitted as a separate refund step. In our plan, the slot simply isn't charged in the first place (the walk's new-slot check requires `current != 0`), so there's nothing to refund. The spec's literal text reads "Refund STATE_BYTES_PER_STORAGE_SET × CPSB" which would create gas without a prior charge in some traces — we read this as the spec intending a matched-pair (refund cancels a charge), so omitting both keeps the net total correct. + +### OOG attribution +With `originalValue` driving the new-slot rule, our plan attributes the per-frame charge to the same frame the spec does — the deepest committed observer with `originalValue=0 AND frame-exit!=0`. Tight-reservoir OOG fires at the spec-correct frame. No consensus divergence on storage-slot OOG. + +For **accounts and code**, no equivalent tx-entry field is needed because the EVM's existing collision rule (`evm.create()` checks `nonce != 0 || !contractHash.IsEmpty() || hasStorage`) ensures at most one `createObjectChange` (or `resetObjectChange`) and at most one main-frame `codeChange` per address per tx: +- Pre-existing accounts (EIP-161 prunes empties) collide on CREATE. +- Newly-created → SELFDESTRUCT → re-CREATE is blocked: SELFDESTRUCT leaves nonce/code untouched until tx finalize, so the second CREATE collides on `nonce != 0`. +- EIP-7702 auth-list `codeChange` happens before the top-frame snapshot (intrinsic processing) so it's outside every frame's walk segment. + +So the walk's "first createObjectChange/codeChange per address" rule is spec-correct without any tx-entry comparison. No remaining divergence. + +### Recent spec changes (PR 11573 commits after `d2a0230`) + +The PR has continued to evolve. Each commit and its impact on our plan: + +| Commit | Subject | Impact on plan | +|---|---|---| +| `3f190787` | Add gas used rules | None — formalises `execution_regular_gas_used` and `execution_state_gas_used` as per-tx counters that increase on charges and decrease on refunds. Already matches our `evm.regularGasConsumed` and `evm.executionStateGas`. | +| `b8193df` | Fix errors in eip | Two adjustments: (a) cleared-slot regular-gas refund of `GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS = 2800` to `refund_counter` (we already emit `SstoreSetGasEIP8037 - WarmStorageReadCostEIP2929 = 2900-100 = 2800` ✓); (b) **EIP-7702 auth refund now also decreases `execution_state_gas_used`** — required code update, see below. Also clarifies system-tx gas formula and CREATE2 hashing cost (both already aligned). | +| `8daeab6` | Fix gas used error | None — `execution_*_gas_used` initialised to 0, not to intrinsic. Already matches. | +| `421279b` | Fix additional errors | None — receipt = `tx_gas_used` post-refund post-floor (matches); CPSB now formally a "fixed parameter" (matches); EIP-7825 contract-size limit applies "when CPSB = 1174". | +| `46faf2a` | Jochem's review | None — wording. | +| `3535f03` | Small fixes from Jochem review | None — `requires:` list updated to `2780, 6780, 7702, 7825, 7976, 7981, 8038`; SELFDESTRUCT explicitly aligned with EIP-6780 (matches). | +| `731a276` | Add ERC-4337 interaction | None on consensus. New informational subsection: bundlers/EntryPoint must account for state-gas explicitly because `GAS` opcode returns `gas_left` only and cannot observe `state_gas_reservoir`. Cross-user-operation subsidy risk noted. **No execution-client behavior change** — purely a recommendation for ERC-4337 implementations layered on top. | + +**Required code change (commit `b8193df`)**: Spec now states "`execution_state_gas_used` decreases by the corresponding amount" when an EIP-7702 authority is non-empty. Previously our impl added the 112×cpsb refund to `gas_remaining.State` (reservoir replenish) but left `blockStateGasUsed = imdGas.State + executionStateGas` at the worst-case value. Per the new spec, `block_state_gas_used` must also drop by `stateIgasRefund`. Fix applied in `execution/protocol/txn_executor.go` at the Amsterdam refund branch and the gasBailout/no-refund Amsterdam branch: +```go +st.blockStateGasUsed = imdGas.State + st.evm.ExecutionStateGas() - stateIgasRefund +``` +Subtraction is safe: by construction `stateIgasRefund = 112 × cpsb × num_existing_auths` and `imdGas.State ≥ 135 × cpsb × num_auths ≥ stateIgasRefund`. + +**Open question (commit `b8193df`)**: The spec text says the state-gas refund happens "in parallel with EIP-7702's `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` regular-gas refund", suggesting both refunds fire under Amsterdam. Under our current impl the regular-gas refund is only applied pre-Amsterdam (the `else` branch of `verifyAuthorities`). With Amsterdam values (`PER_AUTH_BASE_COST_EIP8037 = 7500`, intrinsic regular per auth = 7500), refunding `25000 - 12500 = 12500` regular gas would exceed the per-auth charge, producing negative net regular cost. This appears to be ambiguous spec wording rather than intended behavior — leaving the impl unchanged on this point pending clarification with spec authors. + +## Test status with `snobal-devnet-5` fixtures + +After the fixture submodule was bumped to `snobal-devnet-5` (the user's manual update aimed at the latest spec), running `TestExecutionSpecBlockchainDevnet` with `-count=1`: + +| Stage | File-level failures | Notes | +|---|---|---| +| Initial (after fixture bump) | 146 | Mostly cross-fork tests (Prague/Cancun/etc.) running under Amsterdam | +| After empty-account skip in `liveAccount` | 52 | Net improvement of 94 file-level (1+ k sub-test) failures | + +**Empty-account skip** added in `IntraBlockState.ComputeFrameStateBytes` `liveAccount`: +```go +if applyFilter && so.data.Empty() { + return so, false +} +``` +Rationale: at top-frame walk, an account that is empty per EIP-161 (`nonce==0 && balance==0 && codeHash==EmptyCodeHash`) will be pruned at tx finalize, so it must not be counted as a created account. This catches `AddBalance(X, 0)` to a non-existent X, which TouchAccount-creates an empty stateObject that emits `createObjectChange{X}` even though no real new account persists. + +### Remaining 52 file-level failures (post-fix) + +| Category | Count | Description | +|---|---|---| +| `amsterdam/eip8037` | 16 | Direct EIP-8037 spec tests (state-gas accounting edge cases) | +| `cancun/eip6780_selfdestruct` | 12 | EIP-6780 same-tx selfdestruct | +| `frontier/opcodes` | 3 | Basic opcode gas under Amsterdam | +| `amsterdam/eip7928` | 3 | Block Access List interactions | +| Other | 18 | spread across many forks/EIPs | + +**Recurring failure pattern (eip8037)**: state-gas off by exactly `32×CPSB` (one storage slot) or `112×CPSB` (one new account). Sample: `sstore_restoration_sub_frame_revert[CALL]` gives 76,137 vs expected 38,570 (diff = 37,567 ≈ 32×CPSB). + +**Root cause: chargeFrameStateGas OOG burned gas as exceptional halt.** When the frame-end state-gas charge can't cover (reservoir + remaining regular gas < required), our impl returned `ErrOutOfGas` which routes through `handleFrameRevert`'s exceptional-halt path — burning the frame's remaining `gas.Regular` and adding it to `evm.regularGasConsumed`. That's where the spurious +37,567 came from. + +**EELS reference behavior** (verified by tracing `tests/amsterdam/eip8037_state_creation_gas_cost_increase/test_state_gas_sstore.py::test_sstore_restoration_sub_frame_revert` in `ethereum/execution-specs@devnets/snobal/5`): +- `apply_frame_state_gas` (`vm/interpreter.py`) sets `evm.error = OutOfGasError()` directly without raising `ExceptionalHalt`. +- Process_message's `try/except` only matches `ExceptionalHalt` for the gas-burn path. So apply_frame_state_gas's OOG bypasses it — `evm.gas_left` is preserved. +- `incorporate_child_on_error` returns the child's `gas_left` to the parent reservoir. +- Net: a frame-state-gas OOG behaves like a REVERT (state rolled back, gas returned to parent), NOT like a true exceptional halt. + +**Fix applied** in `execution/vm/`: +- New error: `ErrFrameStateGasOOG` (in `errors.go`). +- `chargeFrameStateGas` returns `ErrFrameStateGasOOG` (was `ErrOutOfGas`). +- `handleFrameRevert` treats `ErrFrameStateGasOOG` like `ErrExecutionReverted` — preserves `gas.Regular`, no burn into `regularGasConsumed`. + +**Test impact**: `TestExecutionSpecBlockchainDevnet` cache-busted: 52 → **31 file-level failures**. EIP-8037 direct scope: 16 → **2 failures**. 14 EIP-8037 tests fixed by this single change. Cancun/EIP-6780 selfdestruct cluster (12 tests) and Frontier/Constantinople tests (~10 tests) still failing — those have a different root cause (likely related to selfdestruct gas accounting, separate investigation). + +### Fix #2: excludeCreate refund for same-tx CREATE+SELFDESTRUCT + +**Test traced**: `test_selfdestruct_in_create_tx_initcode` (top-level CREATE tx where initcode SELFDESTRUCTs to a fresh beneficiary). Expected `gas_used = 131,488`; our impl produced `262,976` (extra 112×CPSB). + +**Root cause via EELS comparison**: +- Intrinsic charges 112×CPSB for the contract address. +- EELS's `compute_state_byte_diff` at frame end gives `byte_delta = +112` (only the SELFDESTRUCT beneficiary; the contract is `existed_at_frame_entry=true` since the snapshot was taken AFTER `move_ether` and `mark_account_created`). +- EELS's `process_message_call` then refunds 112×CPSB (and code bytes if any) for accounts in `accounts_to_delete ∩ created_accounts`. Net execution_state = 0. +- Our impl: walk excludes the contract via `excludeCreate` and counts the beneficiary `+112`. No corresponding refund mechanism for the contract → over-counts. + +**Fix applied** in `IntraBlockState.ComputeFrameStateBytes` and `evm.chargeFrameStateGas`: +- New `excludeCreateRefund` second return value from `ComputeFrameStateBytes`. When `applyFilter && excludeCreate ≠ NilAddress` and the contract is `newlyCreated && selfdestructed`, returns 112 (+ len(code) if present) — mirroring EELS's `accounts_to_delete` refund. +- `chargeFrameStateGas` subtracts `excludeCreateRefund` from the frame's positive delta; if the result is negative it credits back. + +**Test impact**: 31 → **30 file-level failures**. EIP-8037 direct scope: 2 → **1 failure**. Remaining EIP-8037 case (`sstore_restoration_charge_in_ancestor`) is now a *receipt hash mismatch* (not gas-used), indicating receipt field divergence in CALLCODE/DELEGATECALL variants — separate issue from gas accounting. + +### Remaining 30 failures — need per-cluster investigation + +Three fixes brought 146 → 30. The remaining 30 cluster as follows: + +| Cluster | Tests | Pattern | Status | +|---|---|---|---| +| `cancun/eip6780_selfdestruct/*` | 12 | Diff = 1×112×CPSB (or other). **Verified test-isolation bug**: each failing variant PASSES when run truly alone (single `-run` regex), but FAILS when sibling tests in the same fixture file run first. Confirmed for `selfdestruct_not_created_in_same_tx_with_revert.json` — variant 2 fails but variants 1, 2, 3 individually all pass. The cross-contamination is at the subtest level (`t.Run` within a single fixture file), and persists even with `-parallel 1`. The 81-byte SD code our impl shows in failing runs (with CREATE at pc=42, deploying 5 phantom contracts) is NOT the SD contract code for the failing test — it matches `test_recursive_contract_creation_and_selfdestruct`'s SD code. Each subtest creates a fresh `ExecModuleTester` (own tmpdir DB) but some package-level state (sync.Pool? cache?) leaks between them. **Defensive reset of `stateObjectPool` returned objects did NOT fix the bug.** | **Not fixed**. Needs deeper investigation of which package-level state persists between subtests. | +| `frontier/opcodes` + `create` | 5 | Various opcode/create gas patterns under Amsterdam fork. Different from cancun pattern. | Untraced | +| `prague/{6110,7002,7251}` | 3 | System requests (deposits, withdrawals, consolidations). | Untraced | +| `tangerine_whistle/eip150_selfdestruct` | 2 | Diff varies; `gas used 25943, header 37568` (under-counts) AND `gas used 157316, header 37803` (over-counts). Mixed pattern. | Untraced | +| `constantinople/eip1052_extcodehash` | 2 | Diff = 131488 (1×112×CPSB) and 135010 (115×CPSB, irregular). | Untraced | +| `amsterdam/eip8037 sstore_restoration_charge_in_ancestor` | 1 | Receipt hash mismatch (not gas), CALLCODE/DELEGATECALL variants. | Untraced | +| `amsterdam/eip7708 selfdestruct_to_system_address` | 1 | Selfdestruct to system address (`0xff...fe`). | Untraced | +| Others | 4 | byzantium staticcall, cancun create, shanghai warm_coinbase, frontier scenarios | Untraced | + +Each cluster needs: +1. Generate EELS trace via `cd /tmp/execution-specs && uv run fill -v --fork Amsterdam --traces --evm-dump-dir=/tmp/traces`. +2. Compare `result.json`'s gasUsed with our impl's output. +3. Identify which addresses/storage events EELS counts vs ours. +4. Fix root cause in walk or chargeFrameStateGas. + +The tooling is set up at `/tmp/execution-specs` (`devnets/snobal/5` branch) with `uv` deps installed. Each trace iteration takes ~30 seconds. + +## Session end state + +- **146 → 19 file-level failures** (87% reduction). +- EIP-8037 direct scope: **17 → 1 failure** (`sstore_restoration_charge_in_ancestor`, receipt hash mismatch — separate from gas accounting). +- 5 fixes applied, all backed by EELS reference-impl traces. +- `make lint` clean. + +### Fix #4: resetObjectChange does not contribute +112 to walk total + +**Root cause** (verified against EELS `compute_state_byte_diff` in +`forks/amsterdam/state_tracker.py`): EELS only adds +112 for an account when +`account_now != None && !existed_at_frame_entry && !existed_at_tx_entry`. A +pre-existing account being deployed to (Erigon's `resetObjectChange`) fails +the third condition — the account record already counted toward block-state +at the prior funding tx, so the new CREATE's frame-end byte_delta does NOT +re-charge 112. + +**Fix applied** in `IntraBlockState.ComputeFrameStateBytes`: +- `resetObjectChange` does NOT add +112 to `total` (only `createObjectChange` + does — that's the `account didn't exist at tx entry` case). +- The address is still tracked in `acctData` so the EIP-6780 refund path + (see fix #5) refunds 112 unconditionally for `accounts_to_delete ∩ + created_accounts`. + +### Fix #5: explicit per-tx EIP-6780 SELFDESTRUCT refund at top frame + +**Root cause** (verified by EELS trace of +`test_create_selfdestruct_same_tx[selfdestruct_contract_initial_balance_100000-single_call-CREATE]`): +EELS's `compute_state_byte_diff` at top-frame end charges +112 for D ONLY +in balance_0 (D fresh). For balance_100000 (D existed at tx entry), no +112 +is charged. EELS THEN refunds 112 + len(code) + non_zero_storage_bytes +unconditionally for any address in `accounts_to_delete ∩ created_accounts` +(via `process_message_call`'s top-level refund loop). This produces: + + | balance | top-frame +112 for D | refund 181 for D | net for D | + |---------|----------------------|-------------------|-----------| + | 0 | yes (charged) | yes (refunded) | 0 | + | 100_000 | no | yes (-112 offset) | -112 | + +The -112 in balance_100000 offsets some other charge (e.g., the intrinsic +112 for the top-level CREATE contract C), giving the test's expected +`block_state_gas_used = max(0, intrinsic + execution) = max(0, 112 + (-112) + +slots) = (slots) bytes`. + +Our previous walk applied an EIP-6780 filter at the top frame (skipping +walk entries for `newlyCreated && selfdestructed` addresses). That filter +worked for balance_0 (D's bytes effectively cancelled in walk total) but +NOT for balance_100000, because the walk-and-delta math always sums to the +top-frame walkTotal — and with the filter, both cases produced the same +total. We needed to drive net balance_100000 NEGATIVE. + +**Fix applied**: +1. `ComputeFrameStateBytes` now returns `(total, accountRefund)`. Total no + longer applies the EIP-6780 filter — it counts everything per the rules + (createObjectChange, codeChange, storageChange). +2. At top frame (`applyFilter`), `accountRefund` sums per-address + `accountBytes + codeBytes + storageBytes` for each address in + `newlyCreated && selfdestructed`. This mirrors EELS's + `accounts_to_delete ∩ created_accounts` refund. +3. `chargeFrameStateGas` subtracts `accountRefund` from delta: + `delta = walkTotal - childTotal - accountRefund`. Negative delta credits + `gas.State` and decrements `evm.executionStateGas`. +4. `excludeCreate` (top-level CREATE C's address) is pre-tracked in `acctData` + with `accountBytes=112` because C's `createObjectChange` lands BEFORE the + top frame's `frameStart` (the snapshot is pushed AFTER `CreateAccount`) + and so isn't seen by the walk. Without pre-tracking, the unified refund + couldn't refund C. + +### Fix #6: signed `executionStateGas` — allow negative for refund offset + +**Root cause**: with the new explicit-refund mechanism (fix #5), the credit +at top frame can exceed the per-tx running `executionStateGas` total. Under +the old `uint64` semantics, the credit saturated at 0, leaving the +intrinsic-only block-state-gas charged. EELS's `state_gas_used` is signed +(can go negative), and `tx_state_gas = max(0, intrinsic_state_gas + +state_gas_used)` clamps at the block-level uint64 counter. + +**Fix applied** in `execution/vm/evm.go`: +- `evm.executionStateGas` field changed from `uint64` to `int64`. +- `ExecutionStateGas()` now returns `int64`. +- Credit / restore paths in `chargeFrameStateGas` and `handleFrameRevert` + no longer saturate at 0 — they subtract `int64(creditGas)` directly. +- `useMdGas` casts `originalGas` to `int64` when adding. +- `txn_executor.go`'s `blockStateGasUsed` calc now does `max(0, int64(imdGas.State) + + executionStateGas)` before assigning to the `uint64` block counter. + +### Fix #7: nullify `versionedWrites[AddressPath]` on `createObjectChange.revert` (2026-04-29) + +**Root cause** (verified by tracing +`tests/cancun/eip6780_selfdestruct/test_selfdestruct_revert.py::test_selfdestruct_created_in_same_tx_with_revert[outer_selfdestruct_after_inner_call-same_tx]`): + +When `createObjectChange{addr}` is reverted (frame revert pops it from +the journal), `revert()` removes `addr` from `sdb.stateObjects` but +leaves the `versionedWrites[addr][AddressPath]` entry that +`createObject()` emitted via `versionWritten`. A subsequent same-tx +read of `addr` via `getStateObject → getVersionedAccount → versionedRead` +finds the stale `WriteSetRead` value (an empty `*accounts.Account`), +treats `addr` as existing, and calls `stateObjectForAccount` which +re-adds `addr` to `stateObjects` *without* firing a new +`createObjectChange`. The next non-reverted SELFDESTRUCT to that addr +then writes a `balanceChange` with `wasCommited=false` and tags the +stateObject as `newlyCreated=false`. Walk misses the +112 byte charge +even though EELS counts the address as freshly created at frame end. + +**Symptom**: under-charge by exactly 112 bytes × CPSB on a family of +selfdestruct-with-revert tests (~130 file-level failures across the +suite, but only the cancun selfdestruct_revert pair shows the 112-byte +diff cleanly; the rest exhibit downstream effects on the +versionedWrites cleanup at finalize). + +**Fix applied** in `execution/state/journal.go`, +`createObjectChange.revert()`: +```go +if s.versionMap != nil { + s.versionedWrites.UpdateVal(ch.account, AccountKey{Path: AddressPath}, (*accounts.Account)(nil)) +} +``` +Nullifying (not deleting) preserves the entry in `versionedWrites` so +that `MakeWriteSet`'s parallel-mode cleanup loop (line 2304) still +sees the address as reverted and clears stale entries from the global +versionMap. The `nil` value causes subsequent versionedRead to return +nil for AddressPath, so `getStateObject` doesn't materialize a phantom +stateObject. CodeHashPath stays as the reverted-empty-account +EmptyCodeHash, which is the correct value for a non-existent address. + +### Fixes summary table + +| Fix | What | Tests fixed | +|---|---|---| +| 1 (earlier) | empty-account skip in `liveAccount` | ~94 file-level | +| 2 (earlier) | `ErrFrameStateGasOOG` (soft OOG) | 14 EIP-8037 tests | +| 3 (earlier) | excludeCreateRefund (initcode SELFDESTRUCT) | 1 EIP-8037 | +| 4 (earlier) | resetObjectChange → no +112 in total | balance_100000 cancun cluster | +| 5 (earlier) | unified EIP-6780 refund at top frame | most of cancun cluster | +| 6 (earlier) | int64 executionStateGas | enables fix #5's negative offset | +| 7 (prior session) | nullify versionedWrites[AddressPath] on createObjectChange.revert | ~130 file-level failures across forks | +| 8 (this session) | EIP-161 empty-account filter at ALL frames in walk | frontier under-charges (constant_gas, scenarios, value_transfer); tangerine_whistle eip150 selfdestruct cluster (16); byzantium eip214 staticcall_nested_call_to_precompile; shanghai warm_coinbase; constantinople eip1052; prague system contract under-charges (3 files) | +| 9 (this session) | EIP-8037 cleared-slot rule in walk + signed walkTotal/childTotal + propagate only positive walkTotal | cancun create_oog_from_eoa_refunds (6 receipt-hash); cancun selfdestruct receipt-hash family (3); amsterdam sstore_restoration_charge_in_ancestor (6 receipt-hash) | +| 10 (this session) | clamp underflow when gasRemaining > initialGas (state-gas credits) | enables fix #9 receipt computation; recursive selfdestruct receipt mismatches | +| 11 (this session) | resetObjectChange charges +112 when `prev.original.Empty()` | enables 0xff..fe address creation when system call's TouchAccount-then-prune sequence preceded | +| 12 (this session) | getStateObject treats EIP-161-empty versionMap accounts as non-existent | selfdestruct_to_system_address | + +Result: 146 → **0 file-level failures** (100% reduction). All 16,389 subtests pass. + +### Fixes 8–12 (this session) — root causes and rationale + +**Fix #8: EIP-161 empty-account filter at ALL frames** (`IntraBlockState.ComputeFrameStateBytes` `liveAccount`) + +The fix #1 empty-account skip was guarded by `applyFilter` (top frame only). EELS's `compute_state_byte_diff` applies the filter at every frame: `account_now != None` uses EIP-161 "exists" semantics (empty accounts are None). Removing the `applyFilter &&` guard fixes: + +- STATICCALL to a non-existent address: `evm.call(STATICCALL, …, addr=c0f6dc, …)` does `AddBalance(addr, 0)` to trigger a touch — emits `createObjectChange{c0f6dc}` followed by `touchAccount{c0f6dc}` BEFORE the inner frame's `frameStart`. At the inner frame's commit (depth=1), the walk previously charged +112 for the empty `c0f6dc` (filter off at sub-frames), forcing a state-gas OOG that reverted the parent's effects (e.g., the SSTORE 0→1 that the test was verifying). With the filter at all frames, `c0f6dc` is correctly skipped, the inner frame doesn't OOG, and the SSTORE survives. Fixed `frontier/opcodes/value_transfer_gas_calculation` and the entire frontier/tangerine/shanghai/constantinople/prague-system-contract cluster. + +**Fix #9: EIP-8037 cleared-slot rule + signed walkTotal/childTotal + positive-only propagation** (`IntraBlockState.ComputeFrameStateBytes` storageChange handler; `EVM.committedChildBytes`/`chargeFrameStateGas`/`propagateChildBytes`) + +EELS's `compute_state_byte_diff` returns a SIGNED byte_delta — positive for new state, negative for cleared state (`value_now == 0 && frame_entry != 0 && tx_entry == 0` → −32). Erigon's walk previously returned uint64 and only emitted +32 for new slots; the cleared-slot −32 case wasn't recognised. Three coordinated changes: + +1. `ComputeFrameStateBytes` returns `int64` total. The storageChange handler now also emits `total -= 32` when `originalValue == 0 && prevalue != 0 && current == 0` (the cleared-slot rule). Because `seenSlot` dedups to the FIRST entry per (addr, key) in the segment, `e.prevalue` is the slot's value at frame-entry (for sub-frames) or tx-entry (for top frames). + +2. `EVM.committedChildBytes` and `propagateChildBytes` use signed `int64` to carry the negative walkTotal up. `chargeFrameStateGas` does signed delta math: `delta = walkTotal − childTotal − accountRefund`; `delta > 0` charges, `delta < 0` credits. + +3. `propagateChildBytes` propagates ONLY positive walkTotals to the parent. EELS's `apply_frame_state_gas` updates `state_gas_used` only on positive `this_call_cost`; negative path credits the reservoir but leaves `state_gas_used` unchanged. Mirroring this in Erigon: a child with a negative walkTotal credits its own gas.State (via the `delta < 0` path) and the credit propagates to the parent through the gas return mechanism, but the parent's `committedChildBytes` (= EELS `already_paid`) stays at 0 for that child. Otherwise the parent over-charges by the absolute value of the child's credit. + +Crucial subtlety inside `chargeFrameStateGas` for the credit path: `executionStateGas` is decremented by the `accountRefund` portion of the credit (capped by `creditGas`), NOT by the full credit. The cleared-slot portion of the credit goes to the reservoir (state_gas_reservoir in EELS terms) but doesn't decrement the per-tx `executionStateGas` counter. EIP-6780 same-tx-CREATE+SELFDESTRUCT of an account that EXISTED at tx-entry needs `executionStateGas` to go negative to offset intrinsic_state — that path is preserved via the accountRefund decrement. + +Together these fixed `cancun/create/create_oog_from_eoa_refunds` (6 sub-tests, all `sstore_callcode/sstore_delegatecall × no_oog/oog_*` variants), the `cancun/eip6780_selfdestruct/recursive_contract_creation_and_selfdestruct` and `recreate_self_destructed_contract_different_txs` receipt-hash failures, and `amsterdam/eip8037 sstore_restoration_charge_in_ancestor` (6 CALLCODE/DELEGATECALL variants). + +**Fix #10: clamp `txnGasUsedB4Refunds` underflow when state-gas credits exceed consumption** (`TxnExecutor` Apply and Execute paths) + +`txnGasUsedB4Refunds = initialGas.Total() - gasRemaining.Total()` underflowed when EIP-6780 same-tx-CREATE+SELFDESTRUCT credits (and now cleared-slot credits) made `gasRemaining.State` exceed `initialGas.State`. The receipt's `cumulativeGasUsed` ended up as `2^64 - small_number`. Clamp to 0 when remaining exceeds initial; the downstream `max(FloorGasCost, …)` ensures a valid receipt gas value. Applied at both ApplyMessage paths in `txn_executor.go`. + +**Fix #11: resetObjectChange charges +112 when `prev.original.Empty()`** + +The walk's resetObjectChange handler previously skipped the +112 charge always, on the rationale that `resetObjectChange` corresponds to a previously-existing account. But `previous` may be a stateObject for an account that was effectively non-existent at tx-entry — e.g., a system address that an earlier system call's TouchAccount materialized as an empty stateObject, then EIP-161-pruned at the system call's FinalizeTx. From EELS's `compute_state_byte_diff` perspective such accounts have `existed_at_tx_entry = False` and qualify for +112 on creation. `prev.original.Empty()` is the correct signal: `original` is preserved across multiple resets and reflects the state at the very first creation, so an empty `original` indicates the account didn't exist when this stateObject's lineage began. + +**Fix #12: getStateObject treats EIP-161-empty versionMap accounts as non-existent** + +In the parallel-executor path, `getStateObject` calls `getVersionedAccount` to read the address from the versionMap. If a system call earlier in the same block had touched the address (creating an empty stateObject that survives into the versionMap), `getVersionedAccount` returns a non-nil empty Account. Previously Erigon called `stateObjectForAccount(addr, account)` to materialise it as a stateObject WITHOUT firing any journal entry, then `GetOrNewStateObject` saw a non-deleted stateObject and skipped `createObject`. The walk never saw a `createObjectChange` for an address that EELS counts as freshly-created at the frame end, missing +112×CPSB. + +Fix: in `getStateObject`, when `getVersionedAccount` returns a non-nil but EIP-161-empty Account, cache it in `nilAccounts` and return nil. Subsequent accesses see the cached nil; `AddBalance(addr, non_zero)` flows through `GetOrNewStateObject → createObject(addr, nil)` → fires `createObjectChange`. Fixes `amsterdam/eip7708 selfdestruct_to_system_address` (the test's SELFDESTRUCT transfers value to 0xff…fe, which prior block-init system calls had emptied; with the fix Erigon properly counts +112 for the system address re-creation, matching EELS's expected `block_state_gas_used = 131,488`). + +## Found test challenges + +After implementing the plan and running `TestExecutionSpecBlockchainDevnet` from `execution/tests/eest_devnet/`, **1,845 of 15,429 subtests fail** (~12%). The implementation is consistent with EIP-8037 PR 11573, but the EEST fixtures pre-date PR 11573 and were generated against the old opcode-time state-gas-charging semantics. (Initial run before adding the revert-time state-gas restoration was 1,857; the restoration fix moved 12 tests from FAIL to PASS.) + +The fixture submodule at `execution/tests/execution-spec-tests` is pinned to commit `6c2af7fc95d1c0aa781898b1a7ad78769a536d7f` ("add snolbal-devnet-4 fixtures with static cpsb"), which is from **before** PR 11573's "fixed CPSB + frame accounting" rewrite. As a result the fixtures expect: + +- State-gas charges to fire at the SSTORE/CREATE/CALL-to-empty opcodes inline, spilling into `gas_left` when `state_gas_reservoir == 0`. +- Receipt gas to subtract the `stateGasConsumed` on top-level revert/halt (effectively refunding the spilled regular gas). + +Our implementation (per PR 11573): +- Charges state gas only at frame commit via journal walk. +- On top-level revert/halt, the commit-time charge never fires → state gas = 0; remaining regular gas is fully burned. + +The math on a representative failure confirms the divergence comes from this single source. + +### Pattern verification: `dupn_stack_underflow.json` + +Test path: `for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/dupn/dupn_stack_underflow.json` → `test_dupn_stack_underflow[fork_Amsterdam-blockchain_test_from_state_test-dupn_underflow_imm_0]`. + +Tx gas limit: 1,000,000. Bytecode: `PUSH1 1 PUSH1 0 SSTORE` then DUPN underflow (exceptional halt at top frame). + +| Quantity | Old EIP-8037 (fixture) | PR 11573 (our impl) | +|---|---|---| +| SSTORE 0→1 regular gas | 5,000 | 5,000 | +| SSTORE 0→1 state gas | 32 × 1,174 = 37,568 (charged inline, spills to `gas_left`) | 0 (deferred to frame commit) | +| Frame commit fires? | n/a (charged inline) | No — exceptional halt aborts | +| Remaining gas at halt | 979,000 − 5,000 − 37,568 = 936,432 | 979,000 − 5,000 = 974,000 | +| Burned on halt | 936,432 | 974,000 | +| `regularGasConsumed` | 5,000 + 936,432 = 941,432 | 5,000 + 974,000 = 979,000 | +| Receipt subtracts `stateGasConsumed`? | 37,568 (yes) | 0 (no) | +| Block `gas_used` (header) | 21,000 + 941,432 = **962,432** | 21,000 + 979,000 = **1,000,000** | + +Difference: exactly 37,568 = 32 × CPSB (one slot-set worth of state gas). + +### Failing test categories + +All five failing categories live under `for_amsterdam/amsterdam/`: + +| Category | Representative failing tests | +|---|---| +| `eip8024_dupn_swapn_exchange` (DUPN/SWAPN/EXCHANGE) | `dupn/dupn_stack_underflow.json::test_dupn_stack_underflow[*]` (all 6 imm variants) | +| `eip7954_increase_max_contract_size` (max code size) | `max_code_size/max_code_size_deposit_gas.json::test_max_code_size_deposit_gas[short_one_gas]` | +| `eip7928_block_level_access_lists` (BAL — block-level access lists) | `block_access_lists_opcodes/bal_create_oog_code_deposit.json`; `bal_create_contract_init_revert.json`; `bal_create2_collision.json`; `bal_create_and_oog.json[CREATE/CREATE2 × oog_before/after_target_access]`; `block_access_lists/bal_net_zero_balance_transfer.json[zero_balance_zero_transfer_selfdestruct]`; `bal_nonexistent_account_access_read_only.json[staticcall]`; `bal_aborted_storage_access.json[invalid]`; `bal_precompile_call.json[0x01..0x100]` | +| `eip7708_eth_transfer_logs` (ETH transfer logs) | `transfer_logs/zero_value_operations_no_log.json[selfdestruct]`; `transfer_logs/selfdestruct_to_system_address.json`; `transfer_logs/failed_create_with_value_no_log.json[initcode_invalid]`; `transfer_logs/create_collision_no_log.json[CREATE/CREATE2]`; `transfer_logs/create_out_of_gas_no_log.json[create_out_of_gas_code_deposit]` | +| `eip8037_state_creation_gas_cost_increase` (the EIP we refactored) | broad coverage; many subtests across the EIP-8037 fixture set | + +Common failure mode across all categories: the fixture's expected `gasUsed` reflects the OLD EIP-8037 state-gas spillover behaviour; under our PR-11573-aligned model, `gasUsed` is higher by some multiple of 32 × CPSB or 112 × CPSB depending on what state-gas charges the test exercises. + +### Other test signals + +- `make lint` — clean. +- `make test-short` — passes for all packages. +- `make erigon` and `make integration` — both build clean. +- Pre-Amsterdam fork tests in `TestExecutionSpecBlockchain` (and the per-fork `*Cancun*` / `*Prague*` / `*Osaka*` variants) — pass; no divergence outside Amsterdam. + +### Recommended next step + +Per the `erigon-implement-eip` skill ("question the tests — do not silently fix them"): the EEST fixtures need to be regenerated against the PR-11573-aligned python-spec implementation. Until that lands upstream, the 1,845 Amsterdam-EIP fixture failures are expected and should be documented in the PR description rather than worked around in the implementation. + +If a sooner check is needed, options are: +1. Wait for upstream EEST regeneration against PR 11573 and re-pin the submodule. +2. Hand-craft per-test expected values in a local override layer (fragile and high-maintenance). +3. Hold the implementation behind a temporary fork-rules feature flag while both spec lines stabilise (defers the divergence rather than resolving it). + +## Edge cases (worked traces) + +The "originalValue (tx-entry) + current-value-at-commit" rule, combined with the per-frame `delta = walk − committedChildBytes` charge/credit, handles these without any opcode-level refund plumbing. All scenarios assume EIP-8037 (`IsAmsterdam`) and `cpsb = cost_per_state_byte`. Since `storageChange.originalValue` is the slot's value at tx start (captured via `GetCommittedState`), pre-S=X scenarios short-circuit to 0 bytes immediately — the slot was already non-zero at tx start, so no rule can identify it as "new state". + +### Storage-slot scenarios + +For each scenario: `S` is a single storage slot on some address. Pre-tx-committed value is stated. Frame nesting is given as `T → F` (T calls F as child). Siblings means the parent calls them in sequence. + +| # | Scenario | Per-frame charges/credits | Net executionStateGas | +|---|---|---|---| +| 1 | Pre-S=0; F: 0→X→0 (same frame) | F.walk: originalValue=0, curr=0 → 0; F.delta=0 | 0 | +| 2 | Pre-S=0; T → F1 (0→X) → F2 (X→0) | F2: originalValue=0, curr=0 → 0; F1.walk: originalValue=0, curr=0 → 0; T: 0 | 0 | +| 3 | Pre-S=X; T → F1 (X→0) → F2 (0→Y) | originalValue=X non-zero everywhere → 0 bytes at every frame; no charges or credits | 0 | +| 4 | Pre-S=0; T → F1 (0→X) → F2 (X→Y) | F2.walk: originalValue=0, curr=Y → +32 charge (deepest observer); F1.walk: originalValue=0, curr=Y → +32, committedChildBytes=32, delta=0; T: same, delta=0 | 32 (charged at F2, matches spec) | +| 5 | Pre-S=X; T → F1 (X→Y) → F2 (Y→Z) | originalValue=X non-zero → 0 everywhere | 0 | +| 6 | Pre-S=0; T → F1 (0→X) (siblings) F2 (X→0) F3 (0→Y) | F1: originalValue=0, curr=X → +32 charge; F2: originalValue=0, curr=0 → 0; F3: originalValue=0, curr=Y → +32 charge; T.committedChildBytes=64; T.walk: originalValue=0, curr=Y → +32; delta=32−64=−32 credit | 32 | +| 7 | Pre-S=X; T → F1 (X→0) → F2 (0→Y) → F3 (Y→Z) | originalValue=X non-zero → 0 everywhere | 0 | +| 8 | Pre-S=0; F1 (0→X), F2 sibling reverts after starting an SSTORE | F2 reverts → its journal entry gone; F1: originalValue=0, curr=X → +32 charge; T: same, delta=32−32=0 | 32 | +| 9 | Pre-S=X; T → DELEGATECALL F1 (X→0) → DELEGATECALL F2 (0→0)¹ | F2: noop (no journal entry²); F1: originalValue=X, curr=0 → 0; T: same → 0 | 0 | +| 10 | Pre-S=0; T → DELEGATECALL chain F1→F2→F3, each does 0→X then X→0 across the chain | originalValue=0, curr at each frame's commit reflects intermediate state; net 0 cancels at top | 0 | +| 11 | Pre-S=0; T → A_1 (0→X) → A_2 child (X→0) → A_3 grandchild (0→Y) | A_3.walk: originalValue=0, curr=Y → +32 charge (deepest observer); A_2.walk: originalValue=0, curr=Y → +32, committedChildBytes=32, delta=0; A_1.walk: originalValue=0, curr=Y → +32, committedChildBytes=32, delta=0; T: same, delta=0 | 32 (charged once at A_3, matches spec) | +| 12 | Pre-S=0; same as #11 but A_3 reverts | A_3 journal entries gone; A_2/A_1: originalValue=0, curr=0 (S reverted to 0) → 0; T: 0 | 0 | +| 13 | Pre-S=X; T → A_1 (X→0) → A_2 (0→Y) → A_3 (Y→0) | originalValue=X non-zero → 0 everywhere | 0 (slot deletes back to 0; no new state) | +| 14 | Pre-S=0; T calls F1 (0→X), F2 (X→0), F3 (0→Y) as **sequential siblings** | F1: +32 charge; F2: originalValue=0, curr=0 → 0; F3: +32 charge; T.committedChildBytes=64; T.walk: originalValue=0, curr=Y → +32; delta=32−64=−32 credit | 32 (slot ends at Y from 0) | +| 15 | Pre-S=0; SSTOREs by deepest frames under **different parents**: T→F1→F1a (0→X); T→F2→F2a (X→0); T→F3→F3a (0→Y) | F1a: +32 charge; F2a: 0; F3a: +32 charge; F1/F2/F3 walks see same originalValue=0/curr → +32/0/+32, but committedChildBytes covers them so delta=0/0/0; T.committedChildBytes=64; T.walk: originalValue=0, curr=Y → +32; delta=−32 credit | 32 | +| 16 | Pre-S=0; **mixed nesting + siblings**: T→F1 (F1 SSTORE 0→X then F1→F1a does X→0; F1 commits); T→F2 (does 0→Y) | F1a: originalValue=0, curr=0 → 0; F1.walk: originalValue=0, curr=0 (F1a cleared) → 0, delta=0; F2: +32 charge; T: originalValue=0, curr=Y → +32, delta=32−32=0 | 32 | +| 17 | Pre-S=0; **sibling cancellation + parent direct write**: T→F1 (0→X), T→F2 (X→0), T→F3 (0→Y), then T directly SSTORE Y→Z | F1+32, F2:0, F3+32; T.walk: originalValue=0, curr=Z → +32; T.committedChildBytes=64; delta=−32 credit | 32 (slot ends at Z from 0) | +| 18 | Pre-S=0; **same-tree across siblings**: T→F1 (which has F1a doing 0→X and F1b doing X→0 as siblings under F1); T→F2 (does 0→Y) | F1a: +32 charge; F1b: 0; F1.walk: originalValue=0, curr=0 → 0, delta=0−32=−32 credit; F2: +32 charge; T: originalValue=0, curr=Y → +32, delta=32−(0+32)=0 | 32 | + +#### Case A: pre-set slot ending cleared (pre-S=X, final=0). All net **0** state gas — slot is being deleted, no new state. + +With `originalValue=X` (non-zero) on every storageChange entry for S, the new-slot rule never triggers (it requires `originalValue.IsZero()`). Every frame's walk yields 0 bytes for S. No charges, no credits, no per-frame delta. Total: **0** for all variants below. + +| # | Scenario | Trace | +|---|---|---| +| 19 | Pre-S=X; **sequential siblings**: T→F1 (X→0), F2 (0→Y), F3 (Y→0) | originalValue=X non-zero → 0 everywhere | +| 20 | Pre-S=X; **different parents**: T→F1→F1a (X→0); T→F2→F2a (0→Y); T→F3→F3a (Y→0) | originalValue=X non-zero → 0 everywhere | +| 21 | Pre-S=X; **mixed nesting+siblings**: T→F1 (F1: X→0 then F1→F1a does 0→Y; F1 commits); T→F2 SSTORE Y→0 | originalValue=X non-zero → 0 everywhere | +| 22 | Pre-S=X; **same-tree across siblings**: T→F1 (which has F1a:X→0 and F1b:0→Y); T→F2 (Y→0) | originalValue=X non-zero → 0 everywhere | + +#### Case B: pre-set slot ending still-set (pre-S=X, final=Y non-zero). All net **0** state gas — slot was already non-zero, no new state. + +Same short-circuit as Case A: `originalValue=X` non-zero never triggers the new-slot rule. + +| # | Scenario | Trace | +|---|---|---| +| 23 | Pre-S=X; **3-deep nested**: T→A_1 (X→Y') →A_2 child (Y'→Z') →A_3 grandchild (Z'→Y) | originalValue=X non-zero → 0 everywhere | +| 24 | Pre-S=X; **sequential siblings**: T→F1 (X→Y'), F2 (Y'→0), F3 (0→Y) | originalValue=X non-zero → 0 everywhere | +| 25 | Pre-S=X; **different parents**: T→F1→F1a (X→Y'); T→F2→F2a (Y'→0); T→F3→F3a (0→Y) | originalValue=X non-zero → 0 everywhere | +| 26 | Pre-S=X; **mixed nesting+siblings**: T→F1 (F1: X→Y' then F1→F1a does Y'→Z'; F1 commits); T→F2 SSTORE Z'→Y | originalValue=X non-zero → 0 everywhere | +| 27 | Pre-S=X; **same-tree across siblings**: T→F1 (F1a:X→Y', F1b:Y'→0); T→F2 (0→Y) | originalValue=X non-zero → 0 everywhere | + +¹ DELEGATECALL/CALLCODE write to caller's storage, so `storageChange.account` keys to the storage owner — same dedup rules apply. +² A 0→0 SSTORE is a no-op and emits no journal entry (existing IBS behavior). + +### Account-creation / SELFDESTRUCT scenarios + +| # | Scenario | Per-frame charges/credits | Net executionStateGas | +|---|---|---|---| +| 28 | F creates account X with no code, no slots; F commits | F.walk: createObjectChange{X} → +112 charge; T (filter): X exists, not selfdestructed → +112, delta=0 | 112 | +| 29 | F creates X with code length L and N non-zero slots; F commits | F.walk: 112 + L + 32N charge; T (filter): same, delta=0 | 112 + L + 32N | +| 30 | F creates X then SELFDESTRUCTs X (same frame) | F.walk (no filter): 112+L+32N charge; T.walk (filter applied, X is `.newlyCreated && .selfdestructed`) → 0; delta=−(112+L+32N) credit | 0 | +| 31 | T → F1 creates X; T → F2 (sibling) SELFDESTRUCTs X | F1 charges 112 (or 112+L+32N); F2: 0; T (filter): X skipped → 0; delta=−(112+...) credit | 0 | +| 32 | T → F1 creates X; T → F1 then SELFDESTRUCTs X (creator destroys) | F1.walk (no filter): 112+... charge; T (filter): X skipped → 0, credit | 0 | +| 33 | T → F1 → F2 creates X; F1 SELFDESTRUCTs X (parent destroys child's creation) | F2 charges 112+...; F1.walk (no filter): 112+... (its own segment includes F2's createObjectChange), delta=112+...−(112+...)=0; T (filter): 0, delta=−(112+...) credit | 0 | +| 34 | Multiple SELFDESTRUCT of same newly-created X (e.g. F1 creates X; F2, F3, F4 each SELFDESTRUCT X) | F1 charges 112+...; F2/F3/F4: 0 each; T.walk (filter): X skipped exactly once (per-address dedup); credit fires once | 0 | + +#### Cross-frame CREATE+SELFDESTRUCT variants (mirror of storage cases #11–#18) + +| # | Scenario | Per-frame charges/credits | Net | +|---|---|---|---| +| 35 | **3-deep nested**: T→F1→F1a creates X; F1a→F1ab SELFDESTRUCTs X | F1ab: selfdestructChange (skip) → 0; F1a.walk: createObjectChange (no filter), selfdestructChange (skip) → +112 charge; F1.walk: same → +112, delta=112−112=0; T.committedChildBytes=112; T (filter): X skipped → 0; delta=−112 credit | 0 | +| 36 | **Different parents**: T→F1→F1a creates X; T→F2→F2a SELFDESTRUCTs X | F1a: +112 charge; F1.walk: +112, delta=0; F2a: 0; F2: 0; T.committedChildBytes=112; T (filter): X skipped → 0; delta=−112 credit | 0 | +| 37 | **Mixed nesting+siblings**: T→F1 (F1 creates X then F1→F1a SELFDESTRUCTs X; F1 commits); T→F2 (does something else not touching X) | F1a: 0; F1.walk: createObjectChange (its own), selfdestructChange (F1a's) → +112 charge; F2: 0; T (filter): X skipped → 0; T.delta=−112 credit | 0 | +| 38 | **Same-tree across siblings**: T→F1 (F1a creates X, F1b SELFDESTRUCTs X as siblings under F1); T→F2 (no-op for X) | F1a: +112 charge; F1b: 0; F1.walk: createObjectChange (F1a's), selfdestructChange (F1b's) → +112, delta=112−112=0; F2: 0; T (filter): X skipped → 0; delta=−112 credit | 0 | +| 39 | **Creator nested, destroyer at top-level sibling**: T→F1→F1a creates X; F1 returns to T; T→F2 SELFDESTRUCTs X | F1a: +112 charge; F1.walk: +112, delta=0, totalForParent=112; F2: 0; T.committedChildBytes=112; T (filter): X skipped → 0; delta=−112 credit | 0 | +| 40 | **Spread destroys**: T→F1 creates X; T→F2 SELFDESTRUCTs X; T→F3 SELFDESTRUCTs X again | F1: +112 charge; F2: 0; F3: 0 (selfdestructChange{prev=true} just skipped like all selfdestructChange); T.committedChildBytes=112; T (filter): X skipped once by per-address dedup → 0; delta=−112 credit | 0 | + +#### CREATE/CALL boundary scenarios + +| # | Scenario | Per-frame charges/credits | Net executionStateGas | +|---|---|---|---| +| 41 | CREATE silent failure (collision / depth / balance / nonce overflow) at depth ≥ 1 | Frame's snapshot pushed and immediately popped on failure → no journal entries → walks see nothing | 0 | +| 42 | Code-deposit OOG: initcode runs to completion, but `len(code) × cpsb + Keccak256WordGas × ⌈L/32⌉` exceeds remaining gas | `SetCode` not called → no `codeChange` in journal; `createObjectChange` is in journal but EIP-3541/etc. handling will revert via `handleFrameRevert` if applicable. If the create-frame still commits with `code = []` (Homestead pre-fork rules): walk counts +112 for the account but NOT len(code) since current code is empty | 112 (no code charge) | +| 43 | Top-level CREATE tx, contract address C: intrinsic charges 112×cpsb for C; execution then runs initcode and deploys L bytes | Top-frame walk uses `excludeCreate=C` so createObjectChange{C} skipped; codeChange{C} counts L bytes; intrinsic + execution = 112 + L | 112×cpsb (intrinsic) + L×cpsb (execution) | +| 44 | Frame mid-execution OOG (regular gas exhausted before frame commit) | Frame reverts via existing path; no commit-time walk fires → no charge | 0 (for that subtree) | +| 45 | Top-level revert (tx OOG at top frame, or top REVERT) | `evm.executionStateGas = 0` set on top-level revert path; tx-state-gas = intrinsic only | 0 (intrinsic only) | +| 46 | SystemAddress sys-call (e.g. EIP-2935 history-storage update) | SysCallContract initialises `gasRemaining = {Regular: 30_000_000, State: 32 × CPSB × 16 = 601_088}` per the SYSTEM_CALL_GAS_LIMIT formula. The frame-commit walk fires (gated on `IsAmsterdam && !RestoreState` only — no special flag) and the per-frame state-gas charge draws from the dedicated reservoir (covers up to 16 fresh storage writes; e.g. 16-slot history buffer for EIP-2935). System calls do not contribute to `block_regular_gas_used` or `block_state_gas_used` — TxnExecutor is not involved, so the EVM's accumulated `executionStateGas`/`regularGasConsumed` are simply discarded after the call returns. | not contributing to block gas; sys call's own reservoir covers up to `SYSTEM_MAX_SSTORES_PER_CALL` slot writes | + +### Mixed-dimension scenarios + +| # | Scenario | Behavior | +|---|---|---| +| 47 | EIP-7702 auth list at tx start; auth target later does normal SSTORE inside main frame | Auth processing runs before top-frame `Call()` snapshot → auth's codeChange/nonceChange are at journal indices < `frameStart`; top-frame walk doesn't see them. Intrinsic_state_gas already charges 23×cpsb per auth. Main-frame SSTORE counted as usual. **No double-count.** | +| 48 | CALL to non-existent account, value=0 (post-Spurious Dragon) | EIP-161: short-circuit, account not materialized → no journal entry → 0 bytes | +| 49 | CALL to non-existent account, value>0 | Inside `evm.call()` the snapshot is pushed FIRST, then `CreateAccount(addr, false)` fires → `createObjectChange` lands in the **called** frame's segment. The called frame's walk picks up +112. Matches spec (deepest committed observer charges). | +| 50 | SELFDESTRUCT to non-existent beneficiary with value transfer | `AddBalance(beneficiary, balance)` runs inside the destroying frame's body → `createObjectChange` in that frame's segment → frame's walk attributes +112 to the destroying frame. Matches spec. | + +## Files to Modify + +### `execution/state/intra_block_state.go` +- Add `JournalLength() int` returning `len(sdb.journal.entries)`. +- Add `ComputeFrameStateBytes(start, end int, applyFilter bool, excludeCreate accounts.Address) uint64` — implements the walk above. +- **Delete** `SameTxSelfDestructedNewAccounts()` and the `SelfDestructedNewAccount` struct (lines 1441–1473) — no longer needed; the filter at top-frame walk subsumes it. + +### `execution/state/journal.go` +- **Add `originalValue uint256.Int` field to `storageChange` struct.** This is the slot's value at tx start (captured via `GetCommittedState` when the SSTORE happens). Used by `ComputeFrameStateBytes` to determine whether a write creates new state per spec. +- Same field on `fakeStorageChange` (debug path); production walk skips fakeStorageChange anyway, so not strictly required. +- Update `storageChange.revert(...)` — no behavior change; originalValue is just stored, not used in revert. +- Add a typed switch helper `(*journal) walkSegment(start, end int, fn func(idx int, e journalEntry))` if useful for keeping the walk logic in one place. Optional polish. + +### `execution/state/state_object.go` (and/or `intra_block_state.go`) +- In `stateObject.SetState(key, value)` (the journal-emitting path): when appending `storageChange{...}`, populate `originalValue` from `sdb.GetCommittedState(addr, key)`. Erigon's IBS already caches committed values via `originStorage`, so this lookup is O(1) after first hit per `(addr, key)` per tx. +- The existing journal-emit site is in `intra_block_state.go` around line 268 (`sdb.journal.append(storageChange{...})`). Add the lookup there. + +### `execution/protocol/params/protocol.go` +- Add `SystemMaxSstoresPerCall = 16` (per [PR 11573 commit `d2a0230`](https://github.com/ethereum/EIPs/pull/11573/changes/d2a023056187fb17c94e9477cadd076a0f817760)) — upper bound on fresh storage slots a single system call writes. +- Add a helper `SystemCallGasLimit(cpsb uint64) uint64` returning `30_000_000 + StateBytesPerStorageSlot × cpsb × SystemMaxSstoresPerCall` (or use the existing `SysCallGasLimit = 30_000_000` and a separate `SystemCallStateReservoir(cpsb uint64) uint64 = 32 × cpsb × SystemMaxSstoresPerCall` to keep the regular and state portions clearly separated). + +### `execution/vm/evm.go` +- Add to `EVM` struct: + - `executionStateGas uint64` (replaces `stateGasConsumed` for block accounting). + - `committedChildBytes []uint64` (per-depth stack of running children-bytes accumulators). +- **No `chargeStateGas` flag.** Under PR 11573 commit `d2a0230`, system calls also charge state gas via the journal walk (with a pre-sized reservoir, see `SysCallContract` below). The walk's gate is just `IsAmsterdam && !RestoreState`. Block-accounting exemption for system calls is automatic because they don't go through `TxnExecutor`. +- **Remove**: `stateGasConsumed`, `revertedSpillGas`, `stateGasRefund` fields and their accessors. Remove `CreditStateGasRefund(...)` and `RefundTxStateGas(...)` methods entirely. +- In `evm.call()` (CALL/CALLCODE/DELEGATECALL/STATICCALL) and `evm.create()`: + - At entry: capture `frameStart := ibs.JournalLength()`; push `0` onto `committedChildBytes`. + - On success commit (`IsAmsterdam && !RestoreState`): compute `frameEnd`, walk via `ComputeFrameStateBytes(frameStart, frameEnd, depth==0, excludeAddr)`. Compute `delta = int64(walkTotal) - int64(committedChildBytes[depth])`. If positive, charge from `gas.State` (with spill); on insufficient set `err = ErrOutOfGas`. If negative, credit `gas.State` and decrement `executionStateGas`. Pop self; if `depth > 0`, add `walkTotal` to `committedChildBytes[depth-1]`. + - On revert/halt: pop self; do not propagate to parent. Top-level revert (`depth==0 && err != nil`) sets `evm.executionStateGas = 0`. +- Strip `handleFrameRevert`'s state-gas branches (lines 246–299). Keep only `RevertToSnapshot` and the regular-gas burn on exceptional halt. +- In `evm.create()`: + - Remove the inline `useMdGas(... stateGas := len(ret) * cpsb ...)` at line 685–687 — `codeChange` from `SetCode` is picked up by the walk. + - Simplify the `preDepositGas`/`preDepositStateGasConsumed` rollback to just `gasRemaining = preDepositGas` (regular only). + - Drop `savedStateGasConsumed`, `savedStateGasRefund`, `initialChildState` locals (and matching ones in `evm.call()`). + +### `execution/vm/operations_acl.go` +- `makeGasSStoreFunc`: + - Line 76–80 (create slot): drop the `State: 32 * cpsb` from the Amsterdam branch — return regular only. + - Line 100–105 (X→0 reset): drop `evm.CreditStateGasRefund(callContext, 32 * cpsb)`. Keep `AddRefund(...)` for regular-gas refund counter. +- `makeSelfdestructGasFn` (lines 295–301): drop the Amsterdam state-gas branch. + +### `execution/vm/gas_table.go` +- `statefulGasCall` empty-account 112×cpsb branch (around lines 514–518): drop. The `createObjectChange` from `AddBalance` to a non-existent account is picked up by the walk. + +### `execution/vm/instructions.go` +- `execCreate` (lines 1024–1086): delete the `accountStateGas` pre-deduction block (1027–1038). Delete the `CreditStateGasRefund` on failure (1076–1078). +- `opSelfdestruct6780` (lines 1341–1385): no state-gas changes needed in the opcode itself; the gas table changes above are sufficient. +- `opCall`/`opCallCode`/`opDelegateCall`/`opStaticCall`: no signature changes. The CallStipend regular-gas correction at lines 1126–1128 / 1177–1179 stays as-is. + +### `execution/vm/interpreter.go` +- `useMdGas` for `mdgas.StateGas`: make the spillover-into-`Regular` path **transactional**. If neither `State` alone nor `State + spilled-Regular` can cover the requested amount, return `(initial, false)` **without mutating** `initial.State` (don't pre-zero it). This is required so that the revert-time restoration in `evm.call()`/`evm.create()` (which adds `committedChildBytes × cpsb` back to `gas.State`) reflects the correct pre-charge state. Without this fix, a failed-charge attempt would silently lose `State` gas during the half-applied deduction, causing the parent's reservoir to be under-restored on the subsequent revert. + +### `execution/protocol/txn_executor.go` +- No need to communicate the top-level contract address — `evm.create()` derives it from `depth == 0` and the local `address` parameter at commit time. +- No need to set a `chargeStateGas` flag — the walk is gated on `IsAmsterdam && !RestoreState` only. +- In the refund/finalize block (around lines 608–697 area): + - Replace `evm.StateGasConsumed()` with `evm.ExecutionStateGas()`. + - Drop `evm.RevertedSpillGas()` from the receipt-gas formula. + - **Delete the `SameTxSelfDestructedNewAccounts`-driven refund block entirely** — handled inside the EVM via the top-frame credit. +- `block_state_gas_used = intrinsic_state_gas + evm.ExecutionStateGas()`. +- `block_regular_gas_used = intrinsic_regular_gas + evm.regularGasConsumed`. +- `ApplyFrame` (RIP-7560 path): same treatment, ensure `executionStateGas` and `committedChildBytes` are reset between AA passes. + +### `execution/protocol/block_exec.go` (`SysCallContract` / `SysCallContractWithBlockContext`) +- No flag to set — the walk fires automatically on Amsterdam (gated only on `IsAmsterdam && !RestoreState`). What changes is the gas split: +- Initialise `mdGas` with the new SYSTEM_CALL_GAS_LIMIT split: + ```go + cpsb := blockContext.CostPerStateByte + mdGas := mdgas.MdGas{ + Regular: params.SysCallGasLimit, // 30_000_000 + State: 32 * cpsb * params.SystemMaxSstoresPerCall, // dedicated reservoir + } + ``` + This matches `SYSTEM_CALL_GAS_LIMIT = 30_000_000 + STATE_BYTES_PER_STORAGE_SET × CPSB × SYSTEM_MAX_SSTORES_PER_CALL` from PR 11573 commit `d2a0230`. With CPSB=1174 the reservoir is `32 × 1174 × 16 = 601_088` state gas, enough to cover up to 16 fresh storage writes per call (sufficient for EIP-2935 history-buffer updates, EIP-4788 beacon-root, EIP-7002 / EIP-7251 system-contract operations). +- For pre-Amsterdam forks the `State: 0` initialisation stays (no state-gas dimension before EIP-8037). Gate the reservoir initialisation on `chainConfig.Rules(blockContext...)` having `IsAmsterdam`. +- System calls remain not subject to `TX_MAX_GAS_LIMIT`, do not count against the block gas limit, and do not contribute to either `block_regular_gas_used` or `block_state_gas_used` — automatic in our model because `SysCallContract` does not invoke `TxnExecutor`, so there is no add-step for the EVM's `executionStateGas`/`regularGasConsumed`. After the call returns, the EVM is discarded and its counters are dropped. + +### `execution/tracing/hooks.go` +- Add `GasChangeFrameStateGas` to the `GasChangeReason` enum. +- Regenerate `gen_gas_change_reason_stringer.go` (`go generate ./execution/tracing/`). +- The frame-commit `useMdGas` call passes this reason. Credits emit a positive `OnGasChange(gas.State, gas.State+credit, reason)` event. + +### Removed code paths (tracking) +- `evm.CreditStateGasRefund`, `evm.RefundTxStateGas`, `evm.stateGasRefund`, `evm.revertedSpillGas`, `evm.stateGasConsumed`, `evm.StateGasConsumed`, `evm.RevertedSpillGas`. +- All Amsterdam state-gas charge paths in `gas_table.go`, `operations_acl.go`, `instructions.go`. +- The `savedStateGas*` save/restore plumbing in `evm.call()`/`evm.create()` and `handleFrameRevert`. +- `IntraBlockState.SameTxSelfDestructedNewAccounts()` and `SelfDestructedNewAccount` struct. +- `evm.chargeStateGas` field and `evm.SetChargeStateGas` setter — the walk is gated solely on `IsAmsterdam && !RestoreState`. + +## Critical files +- `execution/state/intra_block_state.go` +- `execution/state/journal.go` +- `execution/vm/evm.go` +- `execution/vm/instructions.go` +- `execution/vm/operations_acl.go` +- `execution/vm/gas_table.go` +- `execution/protocol/txn_executor.go` +- `execution/tracing/hooks.go` + +## Verification + +### Unit tests to add/update +1. `execution/protocol/misc/eip8037_test.go` — reactivate; add cases for: + - Single-frame CREATE tx: intrinsic 112×cpsb only; execution = 0. + - CREATE with code deposit: intrinsic 112×cpsb + execution `len(code)*cpsb`. + - CREATE that fails (collision, balance, OOG, REVERT): intrinsic only. + - SSTORE 0→X (same frame): execution +32×cpsb. + - SSTORE 0→X→0 same frame: execution = 0 (single-frame walk handles). + - SSTORE 0→X→0 across DELEGATECALL chain (1, 2, 3 hops): execution = 0 via top-frame negative-delta credit. + - SSTORE 0→X→0 across sibling frames: same — top-frame credit. + - CREATE-then-SELFDESTRUCT same frame: F charges 112+code+slots, T's filtered walk yields negative delta → credit; net 0. + - Cross-frame CREATE-then-SELFDESTRUCT (sibling destroys): same — net 0. + - Multiple SELFDESTRUCTs of same newly-created account: credit fires once (per-address dedup at top walk). + - Frame-end OOG: positive delta exceeds reservoir+gas_left → frame reverts; tx-state-gas = intrinsic only. + - SystemAddress sysCall (EIP-2935 history-buffer ring update writes 1 slot per block; EIP-4788 beacon-root similar): SysCallContract initialises reservoir = `32 × cpsb × 16 = 601_088`, walk fires (gated only on `IsAmsterdam && !RestoreState`) and charges state gas for the slot writes (well within the dedicated reservoir budget). Verify the call does not contribute to `block_*_gas_used`. +2. `execution/vm/gas_table_test.go` — drop assertions on state-gas charges for SSTORE/CALL/SELFDESTRUCT (now zero by design). +3. `execution/protocol/txn_executor_test.go` — keep gas-pool tests; add frame-end-OOG regression. +4. Tracer fixtures expecting per-opcode `GasChangeCallCodeStorage` for state-gas events must be updated to expect `GasChangeFrameStateGas` once per frame (potentially with a credit direction at parent commits). + +### Pre-merge checklist +- `make lint` clean. +- `make test-short` passes. +- `make test-all` passes. + +### Alignment with Mario's TODO test list + +The page at https://notes.ethereum.org/@marioevz/eip-8037-todo-tests lists three test cases that the EIP-8037 implementation must satisfy. All three are handled correctly by the journal-walk algorithm without code changes: + +1. **"TX spending all balance from an account should not refund account destroy refund"** — A pre-existing account whose entire balance is drained by a tx must NOT trigger a 112×cpsb refund (it was never charged in the first place). In our walk: only `createObjectChange`, `resetObjectChange`, `codeChange`, and `storageChange` contribute bytes. `balanceChange` entries are skipped, and the SELFDESTRUCT filter only fires on accounts with `.newlyCreated && .selfdestructed` — a pre-existing account drained to 0 does not match that predicate, so the top-frame walk does not skip it (and there's nothing to skip, since no `createObjectChange` was emitted for it). Net effect: 0 charge, 0 refund. + +2. **"Contract creation -> return successfully -> gas to sstore"** — A top-level CREATE tx that deploys code AND writes a storage slot. Covered by edge case #43: the intrinsic charges 112×cpsb (account); the top-frame walk uses `excludeCreate=C` to skip the contract's `createObjectChange` (avoiding double-count); `codeChange` contributes `len(code)` bytes; `storageChange` contributes 32 bytes. Total state gas: `112×cpsb (intrinsic) + (len(code) + 32)×cpsb (execution)`. + +3. **"Not enough gas to pay for account creation at the end of the tx, but still got refund from a self-destruct"** — A frame creates account A and then SELFDESTRUCTs A within the same tx; even if the test runs near OOG limits, the create + self-destruct pair must net to 0 state gas. Covered by edge cases #30, #31, #34, #35–40: the deepest creator frame charges 112+code+slots at its commit (no filter); the top-frame walk applies the filter, sees A is `.newlyCreated && .selfdestructed`, and skips it; this makes `top.walkTotal < top.committedChildBytes`, producing a negative delta that credits the over-charge back. Per-address dedup at the walk level ensures the credit fires exactly once even if A is SELFDESTRUCTed multiple times. + +The four design questions on Mario's page are also resolved by the implementation: +- **Which frame is charged for account creation?** The frame in which `createObjectChange` lands. For `CALL` to non-existent address with value, that's the called frame (snapshot is pushed before `CreateAccount`). For top-level CREATE tx, the contract account is intrinsic-charged (with `excludeCreate` to prevent double-count). For nested `CREATE`, the create-frame charges via its own `createObjectChange`. +- **Code deposit charge frame?** Always the create-frame, because `codeChange` is emitted by the create-frame's `SetCode` call right before commit. +- **Double-charging via intrinsic_state_cost?** No — `excludeCreate` parameter to `ComputeFrameStateBytes` skips the top-level CREATE's contract account from the walk. + +## Rollout +All changes are gated on `chainRules.IsAmsterdam` — pre-Amsterdam paths are untouched. Cutover is automatic at the fork-transition block, exercising both code paths in a single devnet run. diff --git a/execution/engineapi/engineapitester/engine_api_tester.go b/execution/engineapi/engineapitester/engine_api_tester.go index 34137cf47e3..d20eed49cca 100644 --- a/execution/engineapi/engineapitester/engine_api_tester.go +++ b/execution/engineapi/engineapitester/engine_api_tester.go @@ -90,7 +90,7 @@ func DefaultEngineApiTesterGenesis(t *testing.T) (*types.Genesis, *ecdsa.Private Config: &chainConfig, Coinbase: coinbaseAddr, Difficulty: merge.ProofOfStakeDifficulty, - GasLimit: 1_000_000_000, + GasLimit: 100_000_000, Alloc: types.GenesisAlloc{ coinbaseAddr: { Balance: new(big.Int).Exp(big.NewInt(10), big.NewInt(21), nil), // 1_000 ETH diff --git a/execution/execmodule/exec_module_test.go b/execution/execmodule/exec_module_test.go index 5059c2efb60..dbcd51c5f96 100644 --- a/execution/execmodule/exec_module_test.go +++ b/execution/execmodule/exec_module_test.go @@ -1108,10 +1108,12 @@ func drainHeaders(t *testing.T, ch <-chan [][]byte, timeout time.Duration) { // TestAssembleBlockStateGasLimit verifies that the builder respects the EIP-8037 // block validity invariant: gas_used = max(regular, state) <= gas_limit. // -// Contract creations have high intrinsic state gas (~131K per create at -// CostPerStateByte=1174) but low regular gas (~30K). With a 500K gas limit, -// about 4 creates would push state gas past the limit even though regular gas -// has room. Without the fix the builder would produce an invalid block. +// Under EIP-8037's dynamic cost_per_state_byte, ~730 creates (at any +// gas limit) are needed to fill intrinsic state gas via account creation +// alone. To exercise the check in a small number of txns we pad each create +// with large initcode so that code deposit state gas (L × cpsb) dominates. +// With a 120M gas limit (cpsb = 1174) and ~12 KiB initcode each create costs +// ~14.5M state gas, so ~8 fit before state gas trips the block limit. func TestAssembleBlockStateGasLimit(t *testing.T) { t.Parallel() ctx := t.Context() @@ -1122,7 +1124,7 @@ func TestAssembleBlockStateGasLimit(t *testing.T) { genesis := &types.Genesis{ Config: chain.AllProtocolChanges, - GasLimit: 500_000, // low limit so state gas from a few creates exceeds it + GasLimit: 120_000_000, // cpsb = 1174 at this limit Alloc: types.GenesisAlloc{ senderAddr: {Balance: new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)}, }, @@ -1144,13 +1146,32 @@ func TestAssembleBlockStateGasLimit(t *testing.T) { require.NoError(t, err) // Submit 10 contract creation txns to the pool. - // Each has ~131K intrinsic state gas but only ~30K regular gas. + // Each deploys a ~12 KiB contract so state gas (code deposit + account + // creation) per create is ~14.5M at cpsb=1174. + // Initcode layout: 12-byte prefix (CODECOPY+RETURN) that deploys the + // `runtimeSize` bytes of JUMPDEST that follow it. + const runtimeSize = 12 * 1024 + initCode := make([]byte, 12+runtimeSize) + initCode[0] = 0x61 // PUSH2 + initCode[1] = byte(runtimeSize >> 8) // size hi + initCode[2] = byte(runtimeSize & 0xff) // size lo + initCode[3] = 0x80 // DUP1 + initCode[4] = 0x60 // PUSH1 + initCode[5] = 0x0c // srcOffset = 12 + initCode[6] = 0x60 // PUSH1 + initCode[7] = 0x00 // dstOffset = 0 + initCode[8] = 0x39 // CODECOPY + initCode[9] = 0x60 // PUSH1 + initCode[10] = 0x00 // retOffset = 0 + initCode[11] = 0xf3 // RETURN + for i := 12; i < len(initCode); i++ { + initCode[i] = 0x5b // JUMPDEST — valid-as-code filler; value doesn't matter for state gas + } baseFee := chainPack.TopBlock.BaseFee().Uint64() - deployCode := []byte{0x60, 0x00} // PUSH1 0x00 — minimal contract rlpTxs := make([][]byte, 10) for i := range rlpTxs { tx, txErr := types.SignTx( - types.NewContractCreation(uint64(i), uint256.NewInt(0), 200_000, uint256.NewInt(baseFee), deployCode), + types.NewContractCreation(uint64(i), uint256.NewInt(0), 16_000_000, uint256.NewInt(baseFee), initCode), *types.LatestSignerForChainID(m.ChainConfig.ChainID), privKey, ) require.NoError(t, txErr) @@ -1198,11 +1219,11 @@ func TestAssembleBlockStateGasLimit(t *testing.T) { // invariant for execution-time state gas (SSTOREs), as opposed to intrinsic // state gas (contract creations tested above). // -// A deployed contract writes 4 new storage slots per call (~150K execution -// state gas, ~41K regular gas, 0 intrinsic state gas). The txpool cannot -// filter these by state gas — only the check inside applyTransaction -// (between ApplyMessage and FinalizeTx) prevents the block from exceeding -// gas_limit. +// A deployed contract writes 4 new storage slots per call. At cpsb=1174 +// (120M gas limit) that costs ~150K execution state gas per call. The +// txpool cannot filter these by state gas — only the check inside +// applyTransaction (between ApplyMessage and FinalizeTx) prevents the +// block from exceeding gas_limit. func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) { t.Parallel() ctx := t.Context() @@ -1213,7 +1234,7 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) { genesis := &types.Genesis{ Config: chain.AllProtocolChanges, - GasLimit: 500_000, + GasLimit: 120_000_000, Alloc: types.GenesisAlloc{ senderAddr: {Balance: new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)}, }, @@ -1227,17 +1248,49 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) { exec := m.ExecModule txpool := m.TxPoolGrpcServer - // Deploy a contract whose runtime writes to 4 storage slots per call. - // Runtime: base = calldataload(0); sstore(base+i, 1) for i in 0..3. - deployCode, err := hex.DecodeString( - "601d600c600039601d6000f3" + // initcode: deploy 29-byte runtime - "6000356001815560018160010155600181600201556001816003015500") // runtime - require.NoError(t, err) + // Deploy a contract whose runtime writes to `slotsPerCall` NEW storage + // slots per call. Slot index is base + calldataload(0), so different + // callers/calldata write to non-overlapping regions. At cpsb=1174 each + // slot costs 32 × 1174 = 37,568 state gas. + const slotsPerCall = 100 + // Runtime layout (unrolled): + // PUSH1 0; CALLDATALOAD → stack: [base] + // repeat slotsPerCall times: + // DUP1; PUSH2 i; ADD; PUSH1 1; SWAP1; SSTORE // write 1 at base+i + // STOP + runtime := []byte{0x60, 0x00, 0x35} // PUSH1 0; CALLDATALOAD + for i := 0; i < slotsPerCall; i++ { + runtime = append(runtime, + 0x80, // DUP1 (base) + 0x61, byte(i>>8), byte(i&0xff), // PUSH2 i + 0x01, // ADD + 0x60, 0x01, // PUSH1 1 (value) + 0x90, // SWAP1 (swap value and key) + 0x55, // SSTORE + ) + } + runtime = append(runtime, 0x00) // STOP + runtimeSize := len(runtime) + // Initcode prefix: CODECOPY the runtime into memory then RETURN it. + deployCode := make([]byte, 12+runtimeSize) + deployCode[0] = 0x61 // PUSH2 + deployCode[1] = byte(runtimeSize >> 8) // size hi + deployCode[2] = byte(runtimeSize & 0xff) // size lo + deployCode[3] = 0x80 // DUP1 + deployCode[4] = 0x60 // PUSH1 + deployCode[5] = 0x0c // srcOffset = 12 + deployCode[6] = 0x60 // PUSH1 + deployCode[7] = 0x00 // dstOffset = 0 + deployCode[8] = 0x39 // CODECOPY + deployCode[9] = 0x60 // PUSH1 + deployCode[10] = 0x00 // retOffset = 0 + deployCode[11] = 0xf3 // RETURN + copy(deployCode[12:], runtime) signer := *types.LatestSignerForChainID(m.ChainConfig.ChainID) baseFee := m.Genesis.BaseFee().Uint64() deployTx, err := types.SignTx( - types.NewContractCreation(0, uint256.NewInt(0), 300_000, uint256.NewInt(baseFee), deployCode), + types.NewContractCreation(0, uint256.NewInt(0), 16_000_000, uint256.NewInt(baseFee), deployCode), signer, privKey, ) require.NoError(t, err) @@ -1250,18 +1303,20 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) { err = m.InsertChain(chainPack) require.NoError(t, err) - // Submit 10 call txns. Each writes 4 new slots (~150K state gas, ~41K - // regular gas). With a 500K gas limit, 3 calls fit (~451K state gas) - // but the 4th would push to ~601K. Intrinsic state gas is 0 for all - // calls, so the txpool's regular-gas filter lets them all through — - // the applyTransaction check is the only defense. + // Submit 50 call txns. Each writes 100 new slots (~3.76M state gas at + // cpsb=1174, negligible regular gas compared to state). With a 120M + // gas limit, ~32 calls fit before state gas trips the block limit. + // Intrinsic state gas is 0 for all calls, so the txpool's regular-gas + // filter lets them all through — the applyTransaction check is the + // only defense. + const txCount = 50 baseFee = chainPack.TopBlock.BaseFee().Uint64() - rlpTxs := make([][]byte, 10) + rlpTxs := make([][]byte, txCount) for i := range rlpTxs { var calldata [32]byte - binary.BigEndian.PutUint64(calldata[24:], uint64(i*4)) + binary.BigEndian.PutUint64(calldata[24:], uint64(i*slotsPerCall)) tx, txErr := types.SignTx( - types.NewTransaction(uint64(i+1), contractAddr, uint256.NewInt(0), 300_000, uint256.NewInt(baseFee), calldata[:]), + types.NewTransaction(uint64(i+1), contractAddr, uint256.NewInt(0), 16_000_000, uint256.NewInt(baseFee), calldata[:]), signer, privKey, ) require.NoError(t, txErr) @@ -1291,9 +1346,9 @@ func TestAssembleBlockStateGasLimitSSTORE(t *testing.T) { block, err := getAssembledBlock(ctx, exec, payloadId) require.NoError(t, err) - txCount := len(block.Transactions()) - require.Greater(t, txCount, 0, "block should contain at least one tx") - require.Less(t, txCount, 10, "builder should stop before all 10 calls fit") + actualTxCount := len(block.Transactions()) + require.Greater(t, actualTxCount, 0, "block should contain at least one tx") + require.Less(t, actualTxCount, txCount, "builder should stop before all calls fit") // EIP-8037 invariant: gas_used <= gas_limit. require.LessOrEqual(t, block.GasUsed(), block.GasLimit(), diff --git a/execution/execmodule/execmoduletester/exec_module_tester.go b/execution/execmodule/execmoduletester/exec_module_tester.go index b77e1245700..ded2642fc04 100644 --- a/execution/execmodule/execmoduletester/exec_module_tester.go +++ b/execution/execmodule/execmoduletester/exec_module_tester.go @@ -366,7 +366,7 @@ func applyOptions(opts []Option) options { pruneMode: &defaultPruneMode, blockBufferSize: 128, chainConfig: chain.TestChainBerlinConfig, - experimentalBAL: false, + experimentalBAL: true, } for _, o := range opts { o(&opt) diff --git a/execution/protocol/block_exec.go b/execution/protocol/block_exec.go index e6b25e54eda..6f7175c5c9a 100644 --- a/execution/protocol/block_exec.go +++ b/execution/protocol/block_exec.go @@ -270,9 +270,18 @@ func SysCallContractWithBlockContext(contract accounts.Address, data []byte, cha txContext = NewEVMTxContext(msg) } evm := vm.NewEVM(blockContext, txContext, ibs, chainConfig, vmConfig) + // EIP-8037 (PR 11573 commit d2a0230): system calls are not subject to the + // TX_MAX_GAS_LIMIT cap, do not count against the block gas limit, and do + // not contribute to either block_regular_gas_used or block_state_gas_used. + // On Amsterdam, they get a dedicated state-gas reservoir sized for up to + // SystemMaxSstoresPerCall (=16) fresh storage slots; the regular budget + // (msg.Gas() = SysCallGasLimit = 30M) sits alongside it as gas_left. mdGas := mdgas.MdGas{ Regular: msg.Gas(), - State: 0, // state gas reservoir will consume from regular gas for sys calls + State: 0, + } + if evm.ChainRules().IsAmsterdam { + mdGas.State = params.StateBytesPerStorageSlot * evm.Context.CostPerStateByte * params.SystemMaxSstoresPerCall } ret, _, err := evm.Call( msg.From(), diff --git a/execution/protocol/mdgas/intrinsic_gas.go b/execution/protocol/mdgas/intrinsic_gas.go index 542bd83aed5..008986319fe 100644 --- a/execution/protocol/mdgas/intrinsic_gas.go +++ b/execution/protocol/mdgas/intrinsic_gas.go @@ -37,6 +37,7 @@ type IntrinsicGasCalcArgs struct { IsEIP3860 bool IsEIP7623 bool IsEIP7976 bool + IsEIP7981 bool IsEIP8037 bool IsAATxn bool } @@ -83,10 +84,10 @@ func CalcIntrinsicGas(args IntrinsicGasCalcArgs) (IntrinsicGasCalcResult, bool) result.RegularGas = params.TxGas } result.FloorGasCost = params.TxGas + nz := args.DataNonZeroLen // Bump the required gas by the amount of transactional data if dataLen > 0 { // Zero and non-zero bytes are priced differently - nz := args.DataNonZeroLen // Make sure we don't exceed uint64 for all data combinations nonZeroGas := params.TxDataNonZeroGasFrontier if args.IsEIP2028 { @@ -124,56 +125,119 @@ func CalcIntrinsicGas(args IntrinsicGasCalcArgs) (IntrinsicGasCalcResult, bool) return IntrinsicGasCalcResult{}, true } } + } + if args.AccessListLen > 0 { + product, overflow := math.SafeMul(args.AccessListLen, params.TxAccessListAddressGas) + if overflow { + return IntrinsicGasCalcResult{}, true + } + result.RegularGas, overflow = math.SafeAdd(result.RegularGas, product) + if overflow { + return IntrinsicGasCalcResult{}, true + } + + product, overflow = math.SafeMul(args.StorageKeysLen, params.TxAccessListStorageKeyGas) + if overflow { + return IntrinsicGasCalcResult{}, true + } + result.RegularGas, overflow = math.SafeAdd(result.RegularGas, product) + if overflow { + return IntrinsicGasCalcResult{}, true + } + } - if args.IsEIP7623 { - var tokenLen uint64 - var costPerToken uint64 - if args.IsEIP7976 { - // EIP-7976: floor_tokens = total_bytes * 4, cost_per_token = 16 - // => 64 gas per byte (both zero and non-zero) - var overflow bool - tokenLen, overflow = math.SafeMul(dataLen, 4) - if overflow { - return IntrinsicGasCalcResult{}, true - } - costPerToken = params.TxTotalCostFloorPerTokenEIP7976 - } else { - // EIP-7623: tokens = zero_bytes + 4*nonzero_bytes = dataLen + 3*nz - nzTokens, overflow := math.SafeMul(3, nz) - if overflow { - return IntrinsicGasCalcResult{}, true - } - tokenLen, overflow = math.SafeAdd(dataLen, nzTokens) - if overflow { - return IntrinsicGasCalcResult{}, true - } - costPerToken = params.TxTotalCostFloorPerToken + // Floor data gas cost. + // + // Three EIPs layer here — each covers an independent contribution, and they + // can be combined (EIP-7976 + EIP-7981 both activate at Glamsterdam): + // - EIP-7623 (Prague): legacy calldata floor using zero/non-zero byte + // tokens (zero_bytes + 4*nonzero_bytes) at 10 gas/token. + // - EIP-7976 (Glamsterdam): supersedes the EIP-7623 calldata formula — + // every byte of calldata counts as 4 tokens regardless of value, priced + // at 16 gas/token. + // - EIP-7981 (Glamsterdam): extends floor coverage to access-list data + // (addresses + storage keys) on top of the calldata floor, using the + // same 4 tokens-per-byte / 16 gas-per-token rate. Also charges the + // access-list data cost at floor rate in the regular intrinsic gas path + // so access lists cannot be used to bypass calldata floor pricing. + // + // Compute calldata floor tokens (EIP-7623 or EIP-7976) independently from + // access-list floor tokens (EIP-7981), then combine at the appropriate cost + // per token so both EIPs are exercised when both are active. + var ( + calldataFloorTokens uint64 + accessListFloorTokens uint64 + costPerToken uint64 + ) + // EIP-7976 and EIP-7981 share the same per-token rate (16 gas/token), and + // EIP-7981 spec requires EIP-7976 as a precondition. Selecting the rate on + // either flag keeps the access-list floor surcharge (charged at floor rate + // in RegularGas) consistent with the FloorGasCost rate. + if args.IsEIP7976 || args.IsEIP7981 { + costPerToken = params.TxTotalCostFloorPerTokenEIP7976 + } else { + costPerToken = params.TxTotalCostFloorPerToken + } + if args.IsEIP7623 && dataLen > 0 { + if args.IsEIP7976 { + var overflow bool + calldataFloorTokens, overflow = math.SafeMul(dataLen, params.TxStandardTokensPerByte) + if overflow { + return IntrinsicGasCalcResult{}, true } - dataGas, overflow := math.SafeMul(tokenLen, costPerToken) + } else { + nzTokens, overflow := math.SafeMul(3, nz) if overflow { return IntrinsicGasCalcResult{}, true } - result.FloorGasCost, overflow = math.SafeAdd(result.FloorGasCost, dataGas) + calldataFloorTokens, overflow = math.SafeAdd(dataLen, nzTokens) if overflow { return IntrinsicGasCalcResult{}, true } } } - if args.AccessListLen > 0 { - product, overflow := math.SafeMul(args.AccessListLen, params.TxAccessListAddressGas) + if args.IsEIP7981 { + accessListBytes, overflow := math.SafeMul(args.AccessListLen, params.TxAccessListAddressBytes) if overflow { return IntrinsicGasCalcResult{}, true } - result.RegularGas, overflow = math.SafeAdd(result.RegularGas, product) + storageKeyBytes, overflow := math.SafeMul(args.StorageKeysLen, params.TxAccessListStorageKeyBytes) + if overflow { + return IntrinsicGasCalcResult{}, true + } + accessListBytes, overflow = math.SafeAdd(accessListBytes, storageKeyBytes) + if overflow { + return IntrinsicGasCalcResult{}, true + } + accessListFloorTokens, overflow = math.SafeMul(accessListBytes, params.TxStandardTokensPerByte) if overflow { return IntrinsicGasCalcResult{}, true } - product, overflow = math.SafeMul(args.StorageKeysLen, params.TxAccessListStorageKeyGas) + // Always charge the access list data cost in the standard intrinsic gas + // path so access list data is charged at floor rate regardless of + // execution level. + if accessListFloorTokens > 0 { + accessListDataGas, overflow := math.SafeMul(accessListFloorTokens, costPerToken) + if overflow { + return IntrinsicGasCalcResult{}, true + } + result.RegularGas, overflow = math.SafeAdd(result.RegularGas, accessListDataGas) + if overflow { + return IntrinsicGasCalcResult{}, true + } + } + } + totalFloorTokens, overflow := math.SafeAdd(calldataFloorTokens, accessListFloorTokens) + if overflow { + return IntrinsicGasCalcResult{}, true + } + if totalFloorTokens > 0 { + dataGas, overflow := math.SafeMul(totalFloorTokens, costPerToken) if overflow { return IntrinsicGasCalcResult{}, true } - result.RegularGas, overflow = math.SafeAdd(result.RegularGas, product) + result.FloorGasCost, overflow = math.SafeAdd(result.FloorGasCost, dataGas) if overflow { return IntrinsicGasCalcResult{}, true } diff --git a/execution/protocol/mdgas/intrinsic_gas_test.go b/execution/protocol/mdgas/intrinsic_gas_test.go index 1db71cf22e5..bf2d3af64c0 100644 --- a/execution/protocol/mdgas/intrinsic_gas_test.go +++ b/execution/protocol/mdgas/intrinsic_gas_test.go @@ -109,9 +109,10 @@ func TestZeroDataIntrinsicGas(t *testing.T) { assert.Equal(params.TxGas, result.FloorGasCost) } +// TestEIP7976FloorCost covers EIP-7976 (Increase Calldata Floor Cost): every +// calldata byte (zero or non-zero) contributes TxStandardTokensPerByte tokens, +// each costing TxTotalCostFloorPerTokenEIP7976 gas. func TestEIP7976FloorCost(t *testing.T) { - // EIP-7976 floor: 64 gas per byte (both zero and non-zero), - // computed as floor_tokens = total_bytes * 4, cost_per_token = 16. cases := map[string]struct { dataLen uint64 dataNonZeroLen uint64 @@ -126,25 +127,23 @@ func TestEIP7976FloorCost(t *testing.T) { // 32 zero bytes: floor_tokens = 32*4 = 128, floor = 128*16 = 2048 dataLen: 32, dataNonZeroLen: 0, - expectedFloor: params.TxGas + 32*4*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 2048 = 23048 + expectedFloor: params.TxGas + 32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 2048 = 23048 }, "all non-zero bytes": { - // 32 non-zero bytes: floor_tokens = 32*4 = 128, floor = 128*16 = 2048 - // Key property: same floor as all-zero (byte value doesn't matter) + // Same floor as all-zero (byte value doesn't matter) dataLen: 32, dataNonZeroLen: 32, - expectedFloor: params.TxGas + 32*4*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 2048 = 23048 + expectedFloor: params.TxGas + 32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, }, "mixed bytes": { - // 20 zero + 12 non-zero = 32 total: floor_tokens = 32*4 = 128, floor = 128*16 = 2048 dataLen: 32, dataNonZeroLen: 12, - expectedFloor: params.TxGas + 32*4*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 2048 = 23048 + expectedFloor: params.TxGas + 32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, }, "single byte non-zero": { dataLen: 1, dataNonZeroLen: 1, - expectedFloor: params.TxGas + 1*4*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 64 = 21064 + expectedFloor: params.TxGas + 1*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, // 21000 + 64 = 21064 }, } @@ -168,10 +167,9 @@ func TestEIP7976FloorCost(t *testing.T) { } } +// TestEIP7976VsEIP7623Floor verifies EIP-7976 floors are strictly greater +// than EIP-7623 floors for the same calldata. func TestEIP7976VsEIP7623Floor(t *testing.T) { - // Compare EIP-7976 vs EIP-7623 floor costs for the same data. - // EIP-7976 is strictly greater than EIP-7623 for any non-empty calldata - // (64 > 10 for zero bytes, 64 > 40 for non-zero bytes). assert := assert.New(t) // 32 non-zero bytes: @@ -196,13 +194,11 @@ func TestEIP7976VsEIP7623Floor(t *testing.T) { }) assert.False(overflow) - assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerToken, result7623.FloorGasCost) // 21000+1280=22280 - assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerTokenEIP7976, result7976.FloorGasCost) // 21000+2048=23048 + assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerToken, result7623.FloorGasCost) + assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerTokenEIP7976, result7976.FloorGasCost) assert.Greater(result7976.FloorGasCost, result7623.FloorGasCost) // 32 zero bytes: - // EIP-7623: tokens = 32 + 3*0 = 32, floor = 32*10 = 320 - // EIP-7976: tokens = 32*4 = 128, floor = 128*16 = 2048 result7623z, overflow := CalcIntrinsicGas(IntrinsicGasCalcArgs{ Data: make([]byte, 32), DataNonZeroLen: 0, @@ -222,11 +218,137 @@ func TestEIP7976VsEIP7623Floor(t *testing.T) { }) assert.False(overflow) - assert.Equal(params.TxGas+32*params.TxTotalCostFloorPerToken, result7623z.FloorGasCost) // 21000+320=21320 - assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerTokenEIP7976, result7976z.FloorGasCost) // 21000+2048=23048 + assert.Equal(params.TxGas+32*params.TxTotalCostFloorPerToken, result7623z.FloorGasCost) + assert.Equal(params.TxGas+128*params.TxTotalCostFloorPerTokenEIP7976, result7976z.FloorGasCost) assert.Greater(result7976z.FloorGasCost, result7623z.FloorGasCost) - // Standard gas should be the same regardless of EIP-7976 assert.Equal(result7623.RegularGas, result7976.RegularGas) assert.Equal(result7623z.RegularGas, result7976z.RegularGas) } + +// TestEIP7981IntrinsicGas covers EIP-7981 (Increase Access List Cost): +// access list data contributes to the floor calculation, and the access +// list data cost is always charged in the standard intrinsic gas path. +func TestEIP7981IntrinsicGas(t *testing.T) { + cases := map[string]struct { + dataLen uint64 + dataNonZeroLen uint64 + accessListLen uint64 + storageKeysLen uint64 + expectedRegularGas uint64 + expectedFloorGasCost uint64 + }{ + "no data no access list": { + dataLen: 0, + dataNonZeroLen: 0, + accessListLen: 0, + storageKeysLen: 0, + expectedRegularGas: params.TxGas, + expectedFloorGasCost: params.TxGas, + }, + "only access list address": { + dataLen: 0, + dataNonZeroLen: 0, + accessListLen: 1, + storageKeysLen: 0, + // regular = 21000 + 2400 + (20*4)*16 = 21000 + 2400 + 1280 + expectedRegularGas: params.TxGas + + params.TxAccessListAddressGas + + params.TxAccessListAddressBytes*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + // floor = 21000 + (20*4)*16 = 21000 + 1280 + expectedFloorGasCost: params.TxGas + + params.TxAccessListAddressBytes*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + }, + "access list with storage keys": { + dataLen: 0, + dataNonZeroLen: 0, + accessListLen: 1, + storageKeysLen: 2, + // access_list_bytes = 1*20 + 2*32 = 84; floor_tokens = 84*4 = 336; data gas = 336*16 = 5376 + // regular = 21000 + 2400 + 2*1900 + 5376 = 32576 + expectedRegularGas: params.TxGas + + params.TxAccessListAddressGas + + 2*params.TxAccessListStorageKeyGas + + (params.TxAccessListAddressBytes+2*params.TxAccessListStorageKeyBytes)*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + expectedFloorGasCost: params.TxGas + + (params.TxAccessListAddressBytes+2*params.TxAccessListStorageKeyBytes)*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + }, + "calldata only all non-zero": { + dataLen: 32, + dataNonZeroLen: 32, + accessListLen: 0, + storageKeysLen: 0, + // regular = 21000 + 32*16 = 21512 + expectedRegularGas: params.TxGas + 32*params.TxDataNonZeroGasEIP2028, + // floor = 21000 + (32*4)*16 = 21000 + 2048 + expectedFloorGasCost: params.TxGas + 32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + }, + "calldata only all zero bytes": { + dataLen: 32, + dataNonZeroLen: 0, + accessListLen: 0, + storageKeysLen: 0, + // regular = 21000 + 32*4 = 21128 + expectedRegularGas: params.TxGas + 32*params.TxDataZeroGas, + // EIP-7976: zero bytes also cost 4 tokens each for the floor => 21000 + (32*4)*16 = 23048 + expectedFloorGasCost: params.TxGas + 32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + }, + "calldata and access list": { + dataLen: 32, + dataNonZeroLen: 32, + accessListLen: 1, + storageKeysLen: 2, + // access_list_bytes = 84, access list data gas = 84*4*16 = 5376 + // regular = 21000 + 32*16 + 2400 + 2*1900 + 5376 = 33088 + expectedRegularGas: params.TxGas + + 32*params.TxDataNonZeroGasEIP2028 + + params.TxAccessListAddressGas + + 2*params.TxAccessListStorageKeyGas + + (params.TxAccessListAddressBytes+2*params.TxAccessListStorageKeyBytes)*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + // floor tokens = 32*4 + 84*4 = 128 + 336 = 464; floor = 21000 + 464*16 = 28424 + expectedFloorGasCost: params.TxGas + + (32+params.TxAccessListAddressBytes+2*params.TxAccessListStorageKeyBytes)*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + result, overflow := CalcIntrinsicGas(IntrinsicGasCalcArgs{ + Data: make([]byte, c.dataLen), + DataNonZeroLen: c.dataNonZeroLen, + AccessListLen: c.accessListLen, + StorageKeysLen: c.storageKeysLen, + IsEIP2: true, + IsEIP2028: true, + IsEIP7623: true, + IsEIP7976: true, + IsEIP7981: true, + }) + assert.False(t, overflow) + assert.Equal(t, c.expectedRegularGas, result.RegularGas, "RegularGas mismatch") + assert.Equal(t, c.expectedFloorGasCost, result.FloorGasCost, "FloorGasCost mismatch") + }) + } +} + +// TestEIP7981NotActive verifies that when IsEIP7981 is false (but EIP-7976 is on), +// the EIP-7976 floor formula is used and the access list is NOT included in the +// floor or added to the standard intrinsic gas. +func TestEIP7981NotActive(t *testing.T) { + result, overflow := CalcIntrinsicGas(IntrinsicGasCalcArgs{ + Data: make([]byte, 32), + DataNonZeroLen: 32, + AccessListLen: 1, + StorageKeysLen: 2, + IsEIP2: true, + IsEIP2028: true, + IsEIP7623: true, + IsEIP7976: true, + IsEIP7981: false, + }) + assert.False(t, overflow) + // Regular: 21000 + 32*16 + 2400 + 2*1900 = 27712 (no access list data floor charge) + assert.Equal(t, params.TxGas+32*params.TxDataNonZeroGasEIP2028+params.TxAccessListAddressGas+2*params.TxAccessListStorageKeyGas, result.RegularGas) + // Floor (EIP-7976, access list not included): 21000 + (32*4)*16 = 23048 + assert.Equal(t, params.TxGas+32*params.TxStandardTokensPerByte*params.TxTotalCostFloorPerTokenEIP7976, result.FloorGasCost) +} diff --git a/execution/protocol/misc/eip8037.go b/execution/protocol/misc/eip8037.go index 5a01dfb6a13..490a39cd646 100644 --- a/execution/protocol/misc/eip8037.go +++ b/execution/protocol/misc/eip8037.go @@ -16,29 +16,27 @@ package misc -import ( - "math" - "math/bits" - - "github.com/erigontech/erigon/execution/protocol/params" -) - +// CostPerStateByte derives the per-byte price for new state using the block +// gas limit as per EIP-8037. +// +// raw = ceil((gas_limit * BLOCKS_PER_YEAR) / (2 * TARGET_STATE_GROWTH_PER_YEAR)) +// shifted = raw + CPSB_OFFSET +// shift = max(bit_length(shifted) - CPSB_SIGNIFICANT_BITS, 0) +// quantized = (shifted >> shift) << shift +// cost_per_state_byte = quantized - CPSB_OFFSET, floored at 1 func CostPerStateByte(gasLimit uint64) uint64 { - // TODO this should be removed after bal-devnet-3 (we use hardcoded cspb=1174 for now) - const balDevnet3Spec = true - if balDevnet3Spec { - return 1174 - } - //raw = ceil((gas_limit * 2_628_000) / (2 * TARGET_STATE_GROWTH_PER_YEAR)) - //shifted = raw + CPSB_OFFSET - //shift = max(bit_length(shifted) - CPSB_SIGNIFICANT_BITS, 0) - //cost_per_state_byte = max(((shifted >> shift) << shift) - CPSB_OFFSET, 1) - raw := uint64(math.Ceil(float64(gasLimit*2_628_000) / float64(2*params.TargetStateGrowthPerYear))) - shifted := raw + params.CpsbOffset - shift := max(bits.Len64(shifted)-params.CpsbSignificantBits, 0) - rounded := (shifted >> shift) << shift - if rounded <= params.CpsbOffset { - return 1 - } - return rounded - params.CpsbOffset + // + // TODO clean up all changes related to the dynamic nature of this. Simplify to static val references. + // + // it was decided to stick to static gas + return 1174 + //denominator := 2 * params.TargetStateGrowthPerYear + //raw := (gasLimit*params.BlocksPerYear + denominator - 1) / denominator + //shifted := raw + params.CpsbOffset + //shift := max(bits.Len64(shifted)-params.CpsbSignificantBits, 0) + //quantized := (shifted >> shift) << shift + //if quantized <= params.CpsbOffset { + // return 1 + //} + //return quantized - params.CpsbOffset } diff --git a/execution/protocol/misc/eip8037_test.go b/execution/protocol/misc/eip8037_test.go new file mode 100644 index 00000000000..0a2baa23566 --- /dev/null +++ b/execution/protocol/misc/eip8037_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package misc + +// +//import ( +// "testing" +//) +// +//func TestCostPerStateByte(t *testing.T) { +// // Reference values computed by the EIP-8037 Python reference at devnets/bal/4 +// // (see src/ethereum/forks/amsterdam/vm/gas.py::state_gas_per_byte). +// tests := []struct { +// gasLimit uint64 +// want uint64 +// }{ +// {gasLimit: 1_000_000, want: 1}, +// {gasLimit: 30_000_000, want: 150}, +// {gasLimit: 36_000_000, want: 150}, +// {gasLimit: 60_000_000, want: 662}, +// {gasLimit: 100_000_000, want: 1174}, +// {gasLimit: 120_000_000, want: 1174}, +// {gasLimit: 200_000_000, want: 2198}, +// {gasLimit: 300_000_000, want: 3222}, +// {gasLimit: 500_000_000, want: 5782}, +// {gasLimit: 1_000_000_000, want: 11926}, +// } +// for _, tt := range tests { +// got := CostPerStateByte(tt.gasLimit) +// if got != tt.want { +// t.Errorf("CostPerStateByte(%d) = %d, want %d", tt.gasLimit, got, tt.want) +// } +// } +//} diff --git a/execution/protocol/params/protocol.go b/execution/protocol/params/protocol.go index 4ca4dac192e..e8f0155c469 100644 --- a/execution/protocol/params/protocol.go +++ b/execution/protocol/params/protocol.go @@ -99,7 +99,10 @@ const ( TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxTotalCostFloorPerToken uint64 = 10 // Per token of calldata in a transaction, as a minimum the txn must pay (EIP-7623) - TxTotalCostFloorPerTokenEIP7976 uint64 = 16 // Per token of calldata floor cost (EIP-7976), replaces EIP-7623 value + TxTotalCostFloorPerTokenEIP7976 uint64 = 16 // Per token of calldata floor cost (EIP-7976, reused by EIP-7981) + TxAccessListAddressBytes uint64 = 20 // Byte length of an access list address (EIP-7981) + TxAccessListStorageKeyBytes uint64 = 32 // Byte length of an access list storage key (EIP-7981) + TxStandardTokensPerByte uint64 = 4 // Tokens per byte for EIP-7976 / EIP-7981 floor calculation // These have been changed during the course of the chain CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction. @@ -211,6 +214,7 @@ const ( // EIP-8037: State Creation Gas Cost Increase TargetStateGrowthPerYear uint64 = 107_374_182_400 // 100 × 1024^3 bytes + BlocksPerYear uint64 = 2_628_000 // blocks per year at 12s slot time CpsbOffset = 9_578 // cost_per_state_byte_offset (for quantization) CpsbSignificantBits = 5 // cost_per_state_byte_significant_bits (for quantization) CreateGasEIP8037 = CallValueTransferGas @@ -219,6 +223,15 @@ const ( PerAuthBaseCostEIP8037 = 7_500 StateBytesNewAccount = 112 // bytes per new account creation StateBytesAuthBase = 23 // bytes per authorization base cost + StateBytesPerStorageSlot = 32 // bytes per fresh non-zero storage slot + // SystemMaxSstoresPerCall is the upper bound on the number of new storage + // slots a single system call (EIP-2935 / EIP-4788 / EIP-7002 / EIP-7251 etc.) + // is expected to write. Per EIPs PR 11573 commit d2a0230, the system call's + // state_gas_reservoir is sized as + // StateBytesPerStorageSlot × CostPerStateByte × SystemMaxSstoresPerCall, so + // a single system call can SSTORE up to this many fresh slots without + // running out of state gas. + SystemMaxSstoresPerCall uint64 = 16 ) // EIP-7702: Set EOA account code diff --git a/execution/protocol/txn_executor.go b/execution/protocol/txn_executor.go index ca73c3b0613..fe0ffcffd97 100644 --- a/execution/protocol/txn_executor.go +++ b/execution/protocol/txn_executor.go @@ -405,13 +405,17 @@ func (st *TxnExecutor) ApplyFrame() (*evmtypes.ExecutionResult, error) { State: intrinsicGasResult.StateGas, } st.gasRemaining = mdgas.SplitTxnGasLimit(st.msg.Gas(), imdGas, rules) - // EIP-8037 × EIP-7702: authority-exists refund moves from intrinsic state - // gas into the reservoir so execution-time state ops can draw from it. + st.initialGas = st.gasRemaining.Plus(imdGas) + // EIP-8037 × EIP-7702: intrinsic_state_gas is worst-case (assumes all auths + // create new accounts). For each existing-account auth, 112 × cpsb is + // refunded to the reservoir so execution can draw from it. Per EIP-7778 + // the block-level state gas keeps the worst-case value (no refund + // subtraction); the receipt-level deduction surfaces naturally because + // txnGasUsedB4Refunds = initialGas.Total() - gasRemaining.Total() and + // gasRemaining.Total() includes the refunded portion. if stateIgasRefund > 0 && rules.IsAmsterdam { - imdGas.State -= stateIgasRefund st.gasRemaining.State += stateIgasRefund } - st.initialGas = st.gasRemaining.Plus(imdGas) // Execute the preparatory steps for txn execution which includes: // - prepare accessList(post-berlin; eip-7702) @@ -423,6 +427,8 @@ func (st *TxnExecutor) ApplyFrame() (*evmtypes.ExecutionResult, error) { vmerr error // vm errors do not affect consensus and are therefore not assigned to err ) + st.evm.ResetGasConsumed() + ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), st.data, st.gasRemaining, st.value, false) result := &evmtypes.ExecutionResult{ @@ -552,11 +558,10 @@ func (st *TxnExecutor) Execute(refunds bool, gasBailout bool) (result *evmtypes. State: intrinsicGasResult.StateGas, } st.gasRemaining = mdgas.SplitTxnGasLimit(st.msg.Gas(), imdGas, rules) + st.initialGas = st.gasRemaining.Plus(imdGas) if rules.IsAmsterdam && stateIgasRefund > 0 { - imdGas.State -= stateIgasRefund st.gasRemaining.State += stateIgasRefund } - st.initialGas = st.gasRemaining.Plus(imdGas) if t := st.evm.Config().Tracer; t != nil && t.OnGasChange != nil { t.OnGasChange(st.initialGas.Total(), st.gasRemaining.Total(), tracing.GasChangeTxIntrinsicGas) @@ -610,18 +615,30 @@ func (st *TxnExecutor) Execute(refunds bool, gasBailout bool) (result *evmtypes. refundQuotient = params.RefundQuotientEIP3529 } if rules.IsAmsterdam { - // EIP-8037 + EIP-7778: Block gas accounting uses two dimensions. - // stateGasConsumed tracks ALL state gas charges (including spill to regular gas). - // regularGasConsumed tracks only regular-dimension opcode gas. - blockState := imdGas.State + st.evm.StateGasConsumed() - blockRegular := imdGas.Regular + st.evm.RegularGasConsumed() - st.blockRegularGasUsed = max(blockRegular, intrinsicGasResult.FloorGasCost) - st.blockStateGasUsed = blockState - // Receipt gasUsed: EIP-8037 formula tx.gas - gas_left - reservoir. - // Use Total()-level subtraction to avoid per-component uint64 underflow - // when gasRemaining.State > initialGas.State (reservoir grew via child reverts). - st.txnGasUsedB4Refunds = st.initialGas.Total() - st.gasRemaining.Total() + st.evm.RevertedSpillGas() - refund := min(st.txnGasUsedB4Refunds/refundQuotient, st.state.GetRefund().Total()) + st.blockRegularGasUsed = max(imdGas.Regular+st.evm.RegularGasConsumed(), intrinsicGasResult.FloorGasCost) + // EIP-7778: block_state_gas_used does NOT subtract refunds (including + // the EIP-7702 auth-list refund). intrinsic_state_gas is worst-case + // (assumes all auths create new accounts) and stays in the block-level + // total even when authority is non-empty; only the receipt-level + // txnGasUsedB4Refunds reflects the reservoir replenish (via lower + // gasRemaining usage). + // executionStateGas is signed (can be negative due to EIP-6780 refunds); + // clamp the sum to 0 since block_state_gas_used is a uint64 counter. + if execState := st.evm.ExecutionStateGas(); int64(imdGas.State)+execState > 0 { + st.blockStateGasUsed = uint64(int64(imdGas.State) + execState) + } else { + st.blockStateGasUsed = 0 + } + // txnGasUsedB4Refunds: Total can underflow if EIP-6780 same-tx + // selfdestruct credits on gas.State (via chargeFrameStateGas) make + // gasRemaining.Total() exceed initialGas.Total(). Clamp at 0; the + // FloorGasCost max below ensures a valid receipt gas value. + if st.gasRemaining.Total() > st.initialGas.Total() { + st.txnGasUsedB4Refunds = 0 + } else { + st.txnGasUsedB4Refunds = st.initialGas.Total() - st.gasRemaining.Total() + } + refund := min(st.txnGasUsedB4Refunds/refundQuotient, st.state.GetRefund().Regular) st.txnGasUsed = max(intrinsicGasResult.FloorGasCost, st.txnGasUsedB4Refunds-refund) } else if rules.IsPrague { st.txnGasUsedB4Refunds = st.initialGas.Regular - st.gasRemaining.Regular @@ -636,11 +653,23 @@ func (st *TxnExecutor) Execute(refunds bool, gasBailout bool) (result *evmtypes. } st.refundGas() } else if rules.IsAmsterdam { - blockState := imdGas.State + st.evm.StateGasConsumed() + executionStateGas := st.evm.ExecutionStateGas() + // Clamp signed execution state gas to non-negative when summing with + // intrinsic for the block-level uint64 counter. + var blockState uint64 + if int64(imdGas.State)+executionStateGas > 0 { + blockState = uint64(int64(imdGas.State) + executionStateGas) + } blockRegular := imdGas.Regular + st.evm.RegularGasConsumed() st.blockRegularGasUsed = max(blockRegular, intrinsicGasResult.FloorGasCost) st.blockStateGasUsed = blockState - st.txnGasUsedB4Refunds = st.initialGas.Total() - st.gasRemaining.Total() + st.evm.RevertedSpillGas() + // txnGasUsedB4Refunds: clamp underflow when state-gas credits cause + // gasRemaining to exceed initialGas (see refunds branch above). + if st.gasRemaining.Total() > st.initialGas.Total() { + st.txnGasUsedB4Refunds = 0 + } else { + st.txnGasUsedB4Refunds = st.initialGas.Total() - st.gasRemaining.Total() + } st.txnGasUsed = max(st.txnGasUsedB4Refunds, intrinsicGasResult.FloorGasCost) } else { // No-refund path: gasBailout (trace_call) or !refunds. @@ -844,6 +873,7 @@ func (st *TxnExecutor) calcIntrinsicGas(contractCreation bool, auths []types.Aut IsEIP3860: vmConfig.HasEip3860(rules), IsEIP7623: rules.IsPrague, IsEIP7976: rules.IsAmsterdam, + IsEIP7981: rules.IsAmsterdam, IsEIP8037: rules.IsAmsterdam, }) } diff --git a/execution/state/intra_block_state.go b/execution/state/intra_block_state.go index 527512c0582..453a0f5b5bc 100644 --- a/execution/state/intra_block_state.go +++ b/execution/state/intra_block_state.go @@ -1519,6 +1519,18 @@ func (sdb *IntraBlockState) getStateObject(addr accounts.Address, recordRead boo } if account != nil { + if account.Empty() { + // EIP-161 + EIP-8037: an empty account loaded from versionMap + // (e.g., system address touched by a system call earlier in the + // block, or an account whose only state is a TouchAccount) was + // pruned by EIP-161 at its source tx's FinalizeTx. Treat as + // non-existent so that subsequent writes (AddBalance, SSTORE) + // fire a createObjectChange via createObject. This matches EELS + // compute_state_byte_diff's existed_at_tx_entry semantics, which + // uses EIP-161 "exists" (account_now != None). + sdb.nilAccounts[addr] = struct{}{} + return nil, nil + } return sdb.stateObjectForAccount(addr, account), nil } @@ -1856,6 +1868,233 @@ func (sdb *IntraBlockState) PushSnapshot() int { return sdb.revisions.snapshot(sdb.journal) } +// JournalLength returns the current number of journal entries. Used by EIP-8037 +// frame-end state-gas accounting to capture frame-segment boundaries. +func (sdb *IntraBlockState) JournalLength() int { + return sdb.journal.length() +} + +// ComputeFrameStateBytes walks the journal segment journal[start:end] and +// returns the total state bytes attributable to that frame, plus a refund +// for accounts created and selfdestructed in the same tx (EIP-6780). +// +// Rules: +// - createObjectChange (account didn't exist pre-tx), first per address: +// skip if account == excludeCreate, skip if stateObject is nil or empty +// (EIP-161 prune). Otherwise +StateBytesNewAccount. +// - resetObjectChange (account existed pre-tx with balance only): never +// contributes +112 — the account record was already paid for at the +// prior tx that funded it. (EELS compute_state_byte_diff: the +112 +// condition requires not existed_at_tx_entry, which fails for any +// pre-existing account.) +// - Code deposits (codeChange), first per address: skip if not alive. +// If prevhash was empty AND current stateObject.code is non-empty, +// +len(code). +// - Storage 0→non-zero (storageChange), first per (address, key): skip +// if not alive. Use the entry's originalValue (= tx-entry value) — when +// originalValue.IsZero() AND the current value is non-zero, +32. +// +// At the top frame (applyFilter=true), accountRefund mirrors EELS's +// process_message_call refund for accounts in (created_accounts ∩ +// accounts_to_delete): for each address that's newlyCreated && selfdestructed, +// 112 + code_bytes + non_zero_storage_bytes. The chargeFrameStateGas caller +// subtracts this from the per-frame delta. This applies to both excludeCreate +// (top-level CREATE C) and nested addresses uniformly. +// +// excludeCreate is the contract address of a top-level CREATE tx (already +// covered by the 112×CPSB intrinsic). Pass NilAddress otherwise. Bytes for +// excludeCreate are not added to total, but ARE included in accountRefund +// when the address is newlyCreated && selfdestructed (so that the intrinsic- +// charged 112+code+storage gets refunded against the negative delta). +func (sdb *IntraBlockState) ComputeFrameStateBytes( + start, end int, + applyFilter bool, + excludeCreate accounts.Address, +) (total int64, accountRefund uint64) { + if start >= end { + return 0, 0 + } + type slotKey struct { + addr accounts.Address + key accounts.StorageKey + } + seenAccount := make(map[accounts.Address]struct{}) + seenCode := make(map[accounts.Address]struct{}) + seenSlot := make(map[slotKey]struct{}) + + // Per-address tracking for the top-frame refund. Only populated when + // applyFilter is true. The refund mirrors EELS's process_message_call + // `accounts_to_delete ∩ created_accounts` refund. + type acctTrack struct { + accountBytes uint64 // 112 if a create/reset entry was seen for this addr + codeBytes uint64 + storageBytes uint64 + } + var acctData map[accounts.Address]*acctTrack + if applyFilter { + acctData = make(map[accounts.Address]*acctTrack) + } + track := func(addr accounts.Address) *acctTrack { + if a, ok := acctData[addr]; ok { + return a + } + a := &acctTrack{} + acctData[addr] = a + return a + } + + // Pre-track excludeCreate at the top frame: its createObjectChange is + // emitted by evm.create() BEFORE frameStart is captured (the snapshot is + // pushed after CreateAccount), so the walk never sees it. The intrinsic + // charged 112×CPSB for this address; if it gets selfdestructed in the same + // tx, the refund needs to include 112 (matching EELS's accounts_to_delete + // refund). Code/storage are picked up normally by the walk if their + // journal entries fire after frameStart. + if applyFilter && excludeCreate != accounts.NilAddress { + track(excludeCreate).accountBytes = params.StateBytesNewAccount + } + + // liveAccount returns the stateObject for addr and whether it should be + // counted, applying the EIP-161 filter (empty accounts get pruned at tx + // finalize, so they shouldn't be charged for creation — e.g. + // AddBalance(X, 0) on a non-existent X creates a phantom empty + // stateObject). The filter applies at ALL frames, matching EELS's + // compute_state_byte_diff which uses `account_now != None` (EIP-161 + // "exists" semantics). The EIP-6780 filter (newlyCreated && + // selfdestructed) is NOT applied here — instead, those accounts are + // refunded explicitly via accountRefund (matching EELS's + // accounts_to_delete refund path). + liveAccount := func(addr accounts.Address) (*stateObject, bool) { + so := sdb.stateObjects[addr] + if so == nil { + return nil, false + } + if so.data.Empty() { + return so, false + } + return so, true + } + + for i := start; i < end && i < len(sdb.journal.entries); i++ { + switch e := sdb.journal.entries[i].(type) { + case createObjectChange: + if _, ok := seenAccount[e.account]; ok { + continue + } + seenAccount[e.account] = struct{}{} + _, alive := liveAccount(e.account) + if !alive { + continue + } + if e.account != excludeCreate { + total += int64(params.StateBytesNewAccount) + } + if applyFilter { + track(e.account).accountBytes = params.StateBytesNewAccount + } + case resetObjectChange: + if _, ok := seenAccount[e.account]; ok { + continue + } + seenAccount[e.account] = struct{}{} + _, alive := liveAccount(e.account) + if !alive { + continue + } + // EIP-8037 +112 rule: charge for accounts that didn't exist at + // tx entry. resetObjectChange is fired by createObject when + // `previous != nil` — but `previous` may be a stateObject + // representing a DELETED account (e.g., system address pruned by + // EIP-161 in a prior tx, or any address whose only prior + // existence was an EIP-161-empty stateObject). Use + // `prev.original.Empty()` to detect when the address was + // effectively non-existent at tx start: original is the account + // state before any modifications, preserved across multiple + // resets in the same tx. If empty, the address did not exist at + // tx entry → charge +112 (matches EELS rule + // `account_now != None && !existed_at_frame_entry && + // !existed_at_tx_entry`). + if e.prev != nil && e.prev.original.Empty() && e.account != excludeCreate { + total += int64(params.StateBytesNewAccount) + } + if applyFilter { + track(e.account).accountBytes = params.StateBytesNewAccount + } + case codeChange: + if _, ok := seenCode[e.account]; ok { + continue + } + seenCode[e.account] = struct{}{} + so, alive := liveAccount(e.account) + if !alive { + continue + } + if e.prevhash.IsEmpty() && len(so.code) > 0 { + codeLen := uint64(len(so.code)) + total += int64(codeLen) + if applyFilter { + track(e.account).codeBytes = codeLen + } + } + case storageChange: + k := slotKey{addr: e.account, key: e.key} + if _, ok := seenSlot[k]; ok { + continue + } + seenSlot[k] = struct{}{} + so, alive := liveAccount(e.account) + if !alive { + continue + } + if !e.originalValue.IsZero() { + // tx-entry value is non-zero: no rule applies (neither + // new-slot nor cleared-slot-with-tx-entry-zero). + continue + } + // EIP-8037 four-case rule (matching EELS compute_state_byte_diff): + // - New slot: current != 0 && frame_entry == 0 && tx_entry == 0 → +32 + // - Cleared slot, zero at tx start: current == 0 && frame_entry != 0 + // && tx_entry == 0 → -32 + // - Other transitions: 0 + // + // e.prevalue is the slot's value just before this storageChange. + // Because seenSlot dedups to the FIRST entry in the segment, + // e.prevalue is the slot's value at frame-entry (for sub-frames) + // or tx-entry (for top frame). + current, _ := so.GetState(e.key) + if !current.IsZero() && e.prevalue.IsZero() { + total += 32 + if applyFilter { + track(e.account).storageBytes += 32 + } + } else if current.IsZero() && !e.prevalue.IsZero() { + // Cleared slot. Credit -32 to walkTotal (signed), so + // chargeFrameStateGas returns the gas to the reservoir. + // Mirrors EELS: a slot set in an ancestor frame and cleared + // in this frame nets out to 0 state bytes. + total -= 32 + } + default: + // All other entry types are not state-bytes-relevant. + } + } + + if applyFilter { + for addr, a := range acctData { + so := sdb.stateObjects[addr] + if so == nil { + continue + } + if !(so.newlyCreated && so.selfdestructed) { + continue + } + accountRefund += a.accountBytes + a.codeBytes + a.storageBytes + } + } + + return total, accountRefund +} + func (sdb *IntraBlockState) PopSnapshot(snapshot int) { sdb.revisions.returnSnapshot(snapshot) } diff --git a/execution/state/journal.go b/execution/state/journal.go index e2720d6bf44..6440b3aae36 100644 --- a/execution/state/journal.go +++ b/execution/state/journal.go @@ -144,10 +144,16 @@ type ( wasCommited bool } storageChange struct { - account accounts.Address - key accounts.StorageKey - prevalue uint256.Int - wasCommited bool + account accounts.Address + key accounts.StorageKey + prevalue uint256.Int + // originalValue is the slot's value at transaction start (committed + // state). Captured at SetState time via GetCommittedState. Used by + // IntraBlockState.ComputeFrameStateBytes to determine whether a write + // creates new state per EIP-8037: a slot is "new" if originalValue == 0 + // and the current value at frame commit is non-zero. + originalValue uint256.Int + wasCommited bool } fakeStorageChange struct { account accounts.Address @@ -200,6 +206,17 @@ func (ch createObjectChange) revert(s *IntraBlockState) error { } delete(s.stateObjects, ch.account) delete(s.stateObjectsDirty, ch.account) + // Nullify the AddressPath versionedWrite that createObject emitted. Without + // this, a subsequent versionedRead in the same tx would see the stale + // write and re-materialize a stateObject for this address via + // stateObjectForAccount (no journal entry), causing EIP-8037 to miss the + // +112 byte charge when the address is then re-created in a later + // (non-reverted) frame. Keep the entry in the WriteSet (Val=nil) so that + // MakeWriteSet's parallel-mode cleanup can still see this address as + // reverted and clear stale entries from the global versionMap. + if s.versionMap != nil { + s.versionedWrites.UpdateVal(ch.account, AccountKey{Path: AddressPath}, (*accounts.Account)(nil)) + } return nil } diff --git a/execution/state/state_object.go b/execution/state/state_object.go index 39239ef1f48..8ac10cff12b 100644 --- a/execution/state/state_object.go +++ b/execution/state/state_object.go @@ -264,12 +264,22 @@ func (so *stateObject) SetState(key accounts.StorageKey, value uint256.Int, forc return false, nil } + // EIP-8037: capture the slot's value at transaction start (committed + // state). Used by ComputeFrameStateBytes to identify new slots. + // GetCommittedState caches via originStorage so this is O(1) after the + // first call per (addr, key) per tx. + originalValue, err := so.GetCommittedState(key) + if err != nil { + return false, err + } + // New value is different, update and journal the change so.db.journal.append(storageChange{ - account: so.address, - key: key, - prevalue: prev, - wasCommited: commited, + account: so.address, + key: key, + prevalue: prev, + originalValue: originalValue, + wasCommited: commited, }) if so.db.tracingHooks != nil && so.db.tracingHooks.OnStorageChange != nil { diff --git a/execution/tests/eest_devnet/block_test.go b/execution/tests/eest_devnet/block_test.go index 71db81f237c..c25876d8fba 100644 --- a/execution/tests/eest_devnet/block_test.go +++ b/execution/tests/eest_devnet/block_test.go @@ -54,257 +54,6 @@ func TestExecutionSpecBlockchainDevnet(t *testing.T) { // static — tested in state test format by TestState bt.SkipLoad(`^for_amsterdam/ported_static/`) - // TODO fix failing tests - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/finalization_burn_log_single_account_multiple_transfers.json`) // block=1, gas used by execution: 288804, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/finalization_burn_logs.json`) // block=1, gas used by execution: 652744, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/finalization_burn_logs_multi_account_ordering.json`) // block=1, gas used by execution: 918068, in header: 149961 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/selfdestruct_finalization_after_priority_fee.json`) // block=1, gas used by execution: 265324, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/selfdestruct_same_tx_via_call.json`) // block=1, gas used by execution: 157316, in header: 44709 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/selfdestruct_to_different_address_same_tx.json`) // block=1, gas used by execution: 131488, in header: 37625 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/burn_logs/selfdestruct_to_self_same_tx.json`) // block=1, gas used by execution: 131488, in header: 35024 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/create_collision_no_log.json`) // block=1, gas used by execution: 131488, in header: 67911 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/create_insufficient_balance_no_log.json`) // block=1, gas used by execution: 131488, in header: 32226 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/create_out_of_gas_no_log.json`) // block=1, receiptHash mismatch: 009438deb1de46992abb88fe0ae9a0ddac9f51e271e187f90e8ee24ba2cb5ad0 != dd8803e13b8cb71811d62735df0b9b37d8c2526eae428f0c655cc8a4c6ff3126, headerNum=1, 64d6033a2a26d14031c313d27413f94c127a47314a50ed3a9150b7091dab3be4 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/failed_create_with_value_no_log.json`) // block=1, receiptHash mismatch: 0287aff45fdc09786733241c6048608415aa4f99b6e873b72be6cd8d861ef799 != 6a84ba66d7c74c68e9eee3ae1f5bdff1983a21b6bb5d4fcac2194132e0c4023a, headerNum=1, f7943b7f9af73920eb176bf0934826a430f25675eecb396660d2f5dbced1d997 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/transfer_with_all_tx_types.json`) // block=1, gas used by execution: 40000, in header: 56128 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/multiple_refund_types_in_one_tx.json`) // block=1, gas used by execution: 270020, in header: 1584900 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/simple_gas_accounting.json`) // block=1, gas used by execution: 270020, in header: 1584900 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/varying_calldata_costs.json`) // block=1, gas used by execution: 31240, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_2930_account_listed_but_untouched.json`) // block=1, gas used by execution: 25300, in header: 28628 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_2930_slot_listed_and_unlisted_reads.json`) // block=1, gas used by execution: 27506, in header: 30834 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_2930_slot_listed_and_unlisted_writes.json`) // block=1, receiptHash mismatch: 4445caea7a2485f5e2b50b1c31521f6425b9644ef51180f55d59d70e8bfa5e0f != 7433756f583f4418305ba1bbdda7a9eaf89f1efdac632ed914e53919083aa8ce, headerNum=1, 11ec9293c65bd3e0fe4d23c088d7fcf1deddadd241312b4fdbc85991fac97f9f - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_2930_slot_listed_but_untouched.json`) // block=1, gas used by execution: 25309, in header: 28637 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_aborted_storage_access.json`) // block=1, receiptHash mismatch: 0574ca632811062d8709db6085aef9953a58cc20c1f2a9f2fd58973ee9fc43c5 != 3436c842cadfa64946a5dee36dd6ceaab7cbaab05b7ddcb3e8069b72bcf8620a, headerNum=1, 1efe1b367f53fbddf9e77735a144532932bd5aec9d496bccf4adc5ba0c4fe78f - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists/bal_all_transaction_types.json`) // block=1, gas used by execution: 214842, in header: 346330 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7002/bal_7002_request_invalid.json`) // block=1, gas used by execution: 150272, in header: 56234 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_delegation_clear.json`) // block=1, gas used by execution: 57000, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_delegation_create.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_delegation_update.json`) // block=1, gas used by execution: 57000, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_double_auth_reset.json`) // block=1, gas used by execution: 54004, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_double_auth_swap.json`) // block=1, gas used by execution: 54004, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_7702_null_address_delegation_no_code_change.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_selfdestruct_to_7702_delegation.json`) // block=1, gas used by execution: 59724, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/bal_withdrawal_to_7702_delegation.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_call_7702_delegation_and_oog.json`) // block=1 (hash=0xb43d86b6cd3e1d8b8f7f4b684987fa47a4a669edcc6143787a9b72c62b53aff2): block access list mismatch; debug dumps in /Volumes/erigontests/mock-sentry-1584648681/bal - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_call_no_delegation_and_oog_before_target_access.json`) // block=1, gas used by execution: 23521, in header: 24800 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_call_with_value_in_static_context.json`) // block=1, gas used by execution: 1036027, in header: 1037307 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_callcode_7702_delegation_and_oog.json`) // block=1 (hash=0xd7ec8d3e0ee01f1a8e0a0e40b96cf75648113f08acc5879ec5b8967cba48eda4): block access list mismatch; debug dumps in /Volumes/erigontests/mock-sentry-944926237/bal - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_callcode_no_delegation_and_oog_before_target_access.json`) // block=1, gas used by execution: 23521, in header: 24800 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_create2_collision.json`) // block=1, receiptHash mismatch: 58c109854ad17a7e6cc36919cc07464496eba4562546763c5719a54dbcf83572 != c0f65f206dc904c838752c6d10e9e70f72efefaefb9f01c5793a730fc4d0ef43, headerNum=1, c650b7b61e4cee14205def343f64864259bcbeb9b127686d448f73dd086cd04b - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_create_and_oog.json`) // block=1, gas used by execution: 131488, in header: 30023 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_create_early_failure.json`) // block=1, gas used by execution: 131488, in header: 35026 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_create_oog_code_deposit.json`) // block=1, receiptHash mismatch: 432aa57d091596e6f080cb880aa2265c360922292588ee56480677ce29c931a1 != fd591027210a4da480975eab0be900db75960133ac29c6e4f479df76fe730315, headerNum=1, f0e3464870d4f197c97ef93cb229adb7fe0898b9333aaeb9da79f3095d8b38e8 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_create_selfdestruct_to_self_with_call.json`) // block=1, gas used by execution: 244192, in header: 75136 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_delegatecall_7702_delegation_and_oog.json`) // block=1 (hash=0xaebf8f0426fa03d66ccc61a55b6bdd38c00126d3f9e7c2a2270382de77ca22ec): block access list mismatch; debug dumps in /Volumes/erigontests/mock-sentry-4136835721/bal - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_delegatecall_no_delegation_and_oog_before_target_access.json`) // block=1, gas used by execution: 23518, in header: 24797 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_selfdestruct_in_static_context.json`) // block=1, gas used by execution: 1036027, in header: 1037307 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_staticcall_7702_delegation_and_oog.json`) // block=1 (hash=0x8ef752f47ac4f89d34f8bfa2155254ef393d74b486d3f3866312f183fe9afb29): block access list mismatch; debug dumps in /Volumes/erigontests/mock-sentry-2349312826/bal - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_opcodes/bal_staticcall_no_delegation_and_oog_before_target_access.json`) // block=1, gas used by execution: 23518, in header: 24797 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7954_increase_max_contract_size/max_code_size/max_code_size_via_create.json`) // block=1, receiptHash mismatch: 764a45a52f65e30545bc1acec647b68bef0de17812d850a420716c19b6c19f37 != f2baac99015cfaaf4245fccad11e911bf1bba7f51678131b54269bf903cb6316, headerNum=1, 68ec0f21c9fdd7438f527c256bed133b73eb921c5db72957fa0516467b688547 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/additional_coverage/exact_threshold_boundary.json`) // block=1, gas used by execution: 25928, in header: 27208 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/eip_mainnet/eip_7976.json`) // block=1, gas used by execution: 31176, in header: 34504 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/transaction_validity/transaction_validity_type_1_type_2.json`) // block=1, gas used by execution: 596488, in header: 814088 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/transaction_validity/transaction_validity_type_3.json`) // block=1, gas used by execution: 596488, in header: 814088 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/transaction_validity/transaction_validity_type_4.json`) // block=1, gas used by execution: 2809672, in header: 3027272 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/access_list_cost/access_list_floor_cost_with_calldata.json`) // block=1, gas used by execution: 27400, in header: 30728 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/access_list_cost/access_list_token_calculation.json`) // block=1, gas used by execution: 25300, in header: 28628 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/access_list_cost/duplicate_access_list_entries.json`) // block=1, gas used by execution: 29600, in header: 36256 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/access_list_cost/large_access_list_cost.json`) // block=1, gas used by execution: 80500, in header: 138100 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/eip_mainnet/access_list_data_cost_edge_cases.json`) // block=1, gas used by execution: 32900, in header: 44420 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/eip_mainnet/access_list_gas_cost.json`) // block=1, gas used by execution: 29600, in header: 36256 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/transaction_validity/mixed_zero_nonzero_bytes_floor_cost.json`) // block=1, gas used by execution: 126400, in header: 230080 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip7981_increase_access_list_cost/transaction_validity/valid_gas_limits_with_access_list.json`) // block=1, gas used by execution: 25300, in header: 28628 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/dupn/dupn_stack_underflow.json`) // block=1, receiptHash mismatch: 6ebeb82e2fd4ad8ef581ba011ed8590752fbb658e86bb4f29d186cba3f7b1357 != 496ca8c76f744094f9ba323bd2996f088f7609d26fc17a648298dac6203189a0, headerNum=1, a9c78e38550bc1045539456afe1a06c8086c5a708bac1d17b51976ea05acf949 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/exchange/exchange_invalid_immediate_aborts.json`) // block=1, receiptHash mismatch: 6ebeb82e2fd4ad8ef581ba011ed8590752fbb658e86bb4f29d186cba3f7b1357 != 496ca8c76f744094f9ba323bd2996f088f7609d26fc17a648298dac6203189a0, headerNum=1, 3942d6681902bdc203aaac09522f161eca9fcf30c763f74f3307264d99d8f5f9 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/exchange/exchange_stack_underflow.json`) // block=1, receiptHash mismatch: 6ebeb82e2fd4ad8ef581ba011ed8590752fbb658e86bb4f29d186cba3f7b1357 != 496ca8c76f744094f9ba323bd2996f088f7609d26fc17a648298dac6203189a0, headerNum=1, 648868b84582031adf8c7ca3a0b40b14879f1cf2b8c55dc1ad8b57c89f51acf4 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/pc_advancement/dupn_multiple_consecutive_pc_advancement.json`) // block=1, gas used by execution: 112704, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/pc_advancement/dupn_pc_advances_by_2.json`) // block=1, gas used by execution: 112704, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/pc_advancement/exchange_pc_advances_by_2.json`) // block=1, gas used by execution: 112704, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/pc_advancement/mixed_opcodes_pc_advancement.json`) // block=1, gas used by execution: 112704, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8024_dupn_swapn_exchange/pc_advancement/swapn_pc_advances_by_2.json`) // block=1, gas used by execution: 112704, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/block_2d_gas_accounting/block_2d_gas_boundary_exact_fit.json`) // block=1, gas used by execution: 375680, in header: 460300 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/block_2d_gas_accounting/block_gas_refund_eip7778_no_block_reduction.json`) // block=1, gas used by execution: 112704, in header: 78336 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_call/call_value_to_self_destructed_burns_value.json`) // block=1, gas used by execution: 131488, in header: 41864 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_call/call_value_to_self_destructed_header_gas_used.json`) // block=1, gas used by execution: 131488, in header: 44465 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_call/call_value_to_self_destructed_same_tx_account.json`) // block=1, gas used by execution: 169056, in header: 46874 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_call/call_zero_value_to_self_destructed_same_tx_account.json`) // block=1, gas used by execution: 131488, in header: 35173 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_call/create_insufficient_balance_returns_reservoir.json`) // block=1, gas used by execution: 169056, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/code_deposit_halt_discards_initcode_state_gas.json`) // block=1, receiptHash mismatch: 6109c57d8983f520f4b510024a1760aff8d920bb293bbc00c30abdac956b3b2f != c7cd454c1e2d8f590ccd8205e653d2649edb81e2ee1259782fb3af690d5648d8, headerNum=1, 89a4abe5fd3c9d1b72b7dc5e1a8aba9fd3164ec844156307f23ceac5b92181a5 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/code_deposit_oog_preserves_parent_reservoir.json`) // block=1, receiptHash mismatch: 87eb1d5efeccbf21568dc8ba422e793d3dc3ca7bea2c064adcd6d4bf89c1f636 != 13caceefb2a53e479514f660e5d50b365ead011387defffccca23e4d8d03c5f4, headerNum=1, 59aa99f406a68008b33cc2807b9bd2988a55798b270b671b549c2923b703baad - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create2_address_collision.json`) // block=1, receiptHash mismatch: ed982053122348c512e18c1ac8827d4e10ef91c2705616cd872dc2ad2a0b03dd != 7637ffd9bbf631be43879032b61a2d3ece5335e64500422749a7ea9001482b68, headerNum=1, c148d12a917d4fc0c5dafc6ffa352ee44d216546824653e3b697b3ac7dbf9ac1 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_child_halt_refunds_state_gas.json`) // block=1, gas used by execution: 1555201, in header: 1536420 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_child_revert_refunds_state_gas.json`) // block=1, gas used by execution: 169056, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_code_deposit_oog_refunds_state_gas.json`) // block=1, gas used by execution: 1555201, in header: 1536420 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_collision_burned_gas_counted_in_block_regular.json`) // block=1, gas used by execution: 131488, in header: 117132 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_collision_refunds_state_gas.json`) // block=1, gas used by execution: 1555201, in header: 1536420 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_mixed_success_and_failure_block_accounting.json`) // block=1, gas used by execution: 262976, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_revert_no_code_deposit_state_gas.json`) // block=1, gas used by execution: 131488, in header: 32232 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/create_silent_failure_refunds_state_gas.json`) // block=1, gas used by execution: 169056, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/failed_create_header_gas_used.json`) // block=1, receiptHash mismatch: 9adb08ce7ee8cdea800360deb4ac97ab74ab228a85c1c3756ad1b8c6b675adf3 != 769baff2fb1b33f057f8234087ba691afc2a7e92d81d7dc023a960fc22378d16, headerNum=1, 5c835fc83a5400f48feeb928f38e7f3dc05966214a9dbeee905b3bc0e6089fd6 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/inner_create_fail_refunds_in_creation_tx.json`) // block=1, gas used by execution: 525952, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/inner_create_succeeds_code_deposit_state_gas.json`) // block=1, gas used by execution: 264150, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/nested_create_code_deposit_cannot_borrow_parent_gas.json`) // block=1, gas used by execution: 131488, in header: 30620 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/nested_create_fail_parent_revert_state_gas.json`) // block=1, receiptHash mismatch: 7bcf94219e25ac0732ab6b16e86421b8ca83921986485c99782a0be5579d3a87 != c55cbaff662c04ece0a9c550a57a05b480590f30131afd89fab8b99a29f685a5, headerNum=1, ea6f529ce05e9a22fe8b59ace8401967a32ff6bfaf0f80a9a3a044b3cdd1cc29 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_create/selfdestruct_in_create_tx_initcode.json`) // block=1, gas used by execution: 262976, in header: 131488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/auth_state_gas_scales_with_cpsb.json`) // block=1, gas used by execution: 102138, in header: 233626 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/call_new_account_state_gas_scales_with_cpsb.json`) // block=1, gas used by execution: 169056, in header: 1717344 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/cpsb_underflow_boundary.json`) // block=1, gas used by execution: 37568, in header: 26006 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/create_state_gas_scales_with_cpsb.json`) // block=1, gas used by execution: 169056, in header: 1717344 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/pricing_at_various_gas_limits.json`) // block=1, gas used by execution: 37568, in header: 381632 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/pricing_changes_with_block_gas_limit.json`) // block=1, gas used by execution: 37568, in header: 26006 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/pricing_minimum_cpsb_floor.json`) // block=1, gas used by execution: 37568, in header: 26006 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/refund_cap_includes_state_gas.json`) // block=1, gas used by execution: 37568, in header: 26112 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/refund_with_reservoir_state_gas.json`) // block=1, gas used by execution: 37568, in header: 26112 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/selfdestruct_new_beneficiary_scales_with_cpsb.json`) // block=1, gas used by execution: 169056, in header: 1717344 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_pricing/sstore_refund_scales_with_cpsb.json`) // block=1, gas used by execution: 37568, in header: 26112 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/access_list_gas_is_regular_not_state.json`) // block=1, gas used by execution: 23400, in header: 24680 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/access_list_warm_savings_stay_regular.json`) // block=1, gas used by execution: 25506, in header: 28834 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/creation_tx_failure_preserves_intrinsic_state_gas.json`) // block=1, receiptHash mismatch: 655ba40a763440741644e1cc9308e3161cadd08a84571fb49ff3311789fb61e2 != 34f408ef6c0c284659b1f6f2bb262a45afacfb0ada3448163a7c3bf0520d86ee, headerNum=1, 2ab3e39efd48616245f9037e31450045ef402c07744f16f7d11d674505c802fc - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/creation_tx_regular_check_subtracts_intrinsic_state.json`) // intrinsic gas too low: have 46800, want 161488 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/top_level_failure_refunds_execution_state_gas.json`) // block=1, receiptHash mismatch: 45cbf3c1eacc7b09beaf9eb699a979e045c93d6e3857920c0b33a39b3727d43d != 6109c57d8983f520f4b510024a1760aff8d920bb293bbc00c30abdac956b3b2f, headerNum=1, 6682826169809fc2384bda92bdbe0e432f1d81c87618bc7f72acd977649b6812 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/top_level_failure_refunds_spilled_state_gas.json`) // block=1, gas used by execution: 37568, in header: 26012 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/top_level_failure_refunds_state_gas_propagated_from_child.json`) // block=1, gas used by execution: 37568, in header: 28634 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_reservoir/top_level_failure_zeros_block_state_gas.json`) // block=1, receiptHash mismatch: 45cbf3c1eacc7b09beaf9eb699a979e045c93d6e3857920c0b33a39b3727d43d != 6109c57d8983f520f4b510024a1760aff8d920bb293bbc00c30abdac956b3b2f, headerNum=1, e322f9371ab2c919e6f5252c0c58509f16ae2cf1b41fb0e6db0463823b0eaa14 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/create_selfdestruct_code_deposit_refund_header_check.json`) // block=1, gas used by execution: 432032, in header: 38088 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/create_selfdestruct_no_double_refund_with_sstore_restoration.json`) // block=1, gas used by execution: 169056, in header: 40139 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/create_selfdestruct_refunds_account_and_storage.json`) // block=1, gas used by execution: 319328, in header: 60057 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/create_selfdestruct_refunds_code_deposit_state_gas.json`) // block=1, gas used by execution: 248888, in header: 38655 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/selfdestruct_to_self_in_create_tx.json`) // block=1, gas used by execution: 131488, in header: 35027 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_selfdestruct/selfdestruct_via_delegatecall_chain.json`) // block=1, gas used by execution: 247714, in header: 75136 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/auth_refund_block_gas_accounting.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/auth_refund_bypasses_one_fifth_cap.json`) // block=1, gas used by execution: 139706, in header: 271194 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/auth_with_calldata_and_access_list.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/auth_with_multiple_sstores.json`) // block=1, gas used by execution: 214842, in header: 346330 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/authorization_exact_state_gas_boundary.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/authorization_to_precompile_address.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/authorization_with_sstore.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/duplicate_signer_authorizations.json`) // block=1, gas used by execution: 185492, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/existing_account_auth_header_gas_used_uses_worst_case.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/existing_account_refund.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/existing_account_refund_enables_sstore.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/existing_auth_with_reverted_execution_preserves_intrinsic.json`) // block=1, gas used by execution: 64570, in header: 158490 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/many_authorizations_state_gas.json`) // block=1, gas used by execution: 270020, in header: 1584900 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/mixed_auths_header_gas_used_uses_worst_case.json`) // block=1, gas used by execution: 185492, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/mixed_new_and_existing_auths.json`) // block=1, gas used by execution: 54004, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/mixed_valid_and_invalid_auths.json`) // block=1, gas used by execution: 185492, in header: 316980 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/multi_tx_block_auth_refund_and_sstore.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_ancestor_revert.json`) // block=1, gas used by execution: 75136, in header: 78759 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_block_state_gas_zero.json`) // block=1, gas used by execution: 1878400, in header: 276600 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_charge_in_ancestor.json`) // block=1, gas used by execution: 76131, in header: 75136 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_create_init_revert.json`) // block=1, gas used by execution: 75136, in header: 85168 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_create_init_success.json`) // block=1, gas used by execution: 206624, in header: 169056 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_intermediate_values.json`) // block=1, gas used by execution: 37568, in header: 26218 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_mixed_with_genuine_sstore.json`) // block=1, gas used by execution: 75136, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_refund.json`) // block=1, gas used by execution: 37568, in header: 26112 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_reservoir_replenished_inline.json`) // block=1, gas used by execution: 75136, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_reservoir_spillover.json`) // block=1, gas used by execution: 37568, in header: 26112 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_sub_frame_revert.json`) // block=1, gas used by execution: 75136, in header: 76137 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_restoration_then_reset.json`) // block=1, gas used by execution: 75136, in header: 37568 - bt.SkipLoad(`^for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_sstore/sstore_state_gas_all_tx_types.json`) // block=1, gas used by execution: 45006, in header: 61134 - bt.SkipLoad(`^for_amsterdam/berlin/eip2929_gas_cost_increases/create/create_insufficient_balance.json`) // block=1, gas used by execution: 169056, in header: 47918 - bt.SkipLoad(`^for_amsterdam/berlin/eip2929_gas_cost_increases/create/create_nonce_overflow.json`) // block=1, gas used by execution: 169056, in header: 47918 - bt.SkipLoad(`^for_amsterdam/berlin/eip2930_access_list/acl/account_storage_warm_cold_state.json`) // block=1, gas used by execution: 37568, in header: 38391 - bt.SkipLoad(`^for_amsterdam/berlin/eip2930_access_list/acl/repeated_address_acl.json`) // block=1, receiptHash mismatch: fc6a9666511f1769ef9170ad67316857f70034c974f82e8bb29d8718785410f9 != 925c49d754f5ae98a0f4b9809b1b6374183eb23049041353208a309fd19cbf87, headerNum=1, 65b22b0f8a3bb6791b7a17829222c06cf3c3cea1120923c461f2a50e64d01d41 - bt.SkipLoad(`^for_amsterdam/berlin/eip2930_access_list/acl/transaction_intrinsic_gas_cost.json`) // block=1, gas used by execution: 29600, in header: 36256 - bt.SkipLoad(`^for_amsterdam/berlin/eip2930_access_list/tx_intrinsic_gas/tx_intrinsic_gas.json`) // block=1, gas used by execution: 134676, in header: 231040 - bt.SkipLoad(`^for_amsterdam/cancun/create/create_oog_from_eoa_refunds/create_oog_from_eoa_refunds.json`) // block=1, receiptHash mismatch: ba9991e573821b0c234e361e68d2fe4b0fc940f8380d61fc18ee6ceb963b7094 != 91e4445a3ed8b70b41017281d14d817b16413ae3d96c8afaabd680ec95ff42d9, headerNum=1, 0d4c5d4b752c49e59f005ae61edeb5fadca9a7609e9d100e14097e7f492b1ff5 - bt.SkipLoad(`^for_amsterdam/cancun/eip1153_tstore/tstorage_create_contexts/tstore_rollback_on_failed_create.json`) // block=1, gas used by execution: 16776219, in header: 16776220 - bt.SkipLoad(`^for_amsterdam/cancun/eip1153_tstore/tstorage_selfdestruct/reentrant_selfdestructing_call.json`) // block=1, gas used by execution: 335764, in header: 112704 - bt.SkipLoad(`^for_amsterdam/cancun/eip4788_beacon_root/beacon_root_contract/beacon_root_contract_timestamps.json`) // block=1, receiptHash mismatch: a08468bbc451b952113931bb879542c55e3d7b2ccf4deb042d0b94f8c57f86df != e7ccd1a812b8e285ebd71350997600f2cb445cbc4418c19232a5392071516914, headerNum=1, 310f42900fe908a6ae29313e40fdd04368834e2dabb4431167929c4e7c718344 - bt.SkipLoad(`^for_amsterdam/cancun/eip4788_beacon_root/beacon_root_contract/beacon_root_equal_to_timestamp.json`) // block=1, receiptHash mismatch: a08468bbc451b952113931bb879542c55e3d7b2ccf4deb042d0b94f8c57f86df != e7ccd1a812b8e285ebd71350997600f2cb445cbc4418c19232a5392071516914, headerNum=1, d7f435eb5a5dc52f308311a30cd9db80eda1320c56953dbafc55bb5c25890950 - bt.SkipLoad(`^for_amsterdam/cancun/eip4788_beacon_root/beacon_root_contract/tx_to_beacon_root_contract.json`) // block=1, gas used by execution: 27660, in header: 33036 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/blob_txs/blob_gas_subtraction_tx.json`) // block=1, receiptHash mismatch: 6a54bbdc096ab24565de7bf4759b6f7be19d810de95cf864ee13dd4b708adde5 != 5dff71537f505e5214e9fa0e1ad143f08fe119b94a30f008761c18c57ffa621e, headerNum=1, 1385b20a9eb8dd45b665fea39481e78ea47dbc1d81acb82673d9288ad7910d71 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/blob_txs/sufficient_balance_blob_tx.json`) // block=1, gas used by execution: 27200, in header: 32576 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/blob_txs/sufficient_balance_blob_tx_pre_fund_tx.json`) // block=1, gas used by execution: 48200, in header: 53576 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/blobhash_opcode/blobhash_invalid_blob_index.json`) // block=1, gas used by execution: 413248, in header: 88332 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/blobhash_opcode/blobhash_scenarios.json`) // block=1, gas used by execution: 450816, in header: 225408 - bt.SkipLoad(`^for_amsterdam/cancun/eip4844_blobs/point_evaluation_precompile/tx_entry_point.json`) // block=1, gas used by execution: 94148, in header: 105668 - bt.SkipLoad(`^for_amsterdam/cancun/eip5656_mcopy/mcopy_memory_expansion/mcopy_huge_memory_expansion.json`) // block=1, receiptHash mismatch: 56fe7a0c09434e88468b961fffa5d328dd45d99b0a8d254178c45b1c33ab7a12 != 98ff002f51d977b4e85bd9a688d348d42c26f3b2f31fbfe9f005555adf3e5027, headerNum=1, f5309ec689b572ac095a551de1ab894b6782605b03012a48469f0f961cdd3e94 - bt.SkipLoad(`^for_amsterdam/cancun/eip5656_mcopy/mcopy_memory_expansion/mcopy_memory_expansion.json`) // block=1, gas used by execution: 47635, in header: 59154 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/dynamic_create2_selfdestruct_collision/dynamic_create2_selfdestruct_collision.json`) // block=1, gas used by execution: 795972, in header: 473083 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/dynamic_create2_selfdestruct_collision/dynamic_create2_selfdestruct_collision_multi_tx.json`) // block=1, gas used by execution: 570564, in header: 500666 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/dynamic_create2_selfdestruct_collision/dynamic_create2_selfdestruct_collision_two_different_transactions.json`) // block=1, gas used by execution: 896936, in header: 702052 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/calling_from_new_contract_to_pre_existing_contract.json`) // block=1, gas used by execution: 565868, in header: 358070 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/create_and_destroy_multiple_contracts_same_tx.json`) // block=1, gas used by execution: 696182, in header: 282934 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/create_multiple_contracts_destroy_one_then_destroy_other_next_tx.json`) // block=1, gas used by execution: 605784, in header: 378028 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/create_selfdestruct_same_tx.json`) // block=1, gas used by execution: 786580, in header: 545910 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/create_selfdestruct_same_tx_increased_nonce.json`) // block=1, gas used by execution: 1435802, in header: 1171652 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/parent_creates_child_selfdestruct_one.json`) // block=1, gas used by execution: 740794, in header: 535344 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/recreate_self_destructed_contract_different_txs.json`) // block=1, gas used by execution: 500124, in header: 101168 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/recursive_contract_creation_and_selfdestruct.json`) // block=1, gas used by execution: 693834, in header: 488384 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/self_destructing_initcode.json`) // block=1, gas used by execution: 376854, in header: 207798 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct/self_destructing_initcode_create_tx.json`) // block=1, gas used by execution: 169056, in header: 131488 - bt.SkipLoad(`^for_amsterdam/cancun/eip6780_selfdestruct/selfdestruct_revert/selfdestruct_created_in_same_tx_with_revert.json`) // block=1, gas used by execution: 612828, in header: 338112 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1014_create2/create2_revert/create2_revert_preserves_balance.json`) // block=1, gas used by execution: 131488, in header: 40175 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1014_create2/create_returndata/create2_return_data.json`) // block=1, gas used by execution: 206624, in header: 78097 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1052_extcodehash/extcodehash/extcodehash_after_selfdestruct.json`) // block=1, gas used by execution: 359244, in header: 225408 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1052_extcodehash/extcodehash/extcodehash_created_and_deleted.json`) // block=1, gas used by execution: 360418, in header: 225408 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1052_extcodehash/extcodehash/extcodehash_created_and_deleted_recheck_outer.json`) // block=1, gas used by execution: 473122, in header: 338112 - bt.SkipLoad(`^for_amsterdam/constantinople/eip1052_extcodehash/extcodehash/extcodehash_subcall_selfdestruct.json`) // block=1, gas used by execution: 474296, in header: 300544 - bt.SkipLoad(`^for_amsterdam/frontier/create/create_deposit_oog/create_deposit_oog.json`) // block=1, gas used by execution: 131488, in header: 70238 - bt.SkipLoad(`^for_amsterdam/frontier/create/create_one_byte/create_one_byte.json`) // block=1, gas used by execution: 43577706, in header: 43446218 - bt.SkipLoad(`^for_amsterdam/frontier/create/create_suicide_during_init/create_suicide_during_transaction_create.json`) // block=1, gas used by execution: 169056, in header: 46953 - bt.SkipLoad(`^for_amsterdam/frontier/create/create_suicide_store/create_suicide_store.json`) // block=1, gas used by execution: 413248, in header: 112704 - bt.SkipLoad(`^for_amsterdam/frontier/opcodes/all_opcodes/cover_revert.json`) // block=1, gas used by execution: 169056, in header: 131488 - bt.SkipLoad(`^for_amsterdam/frontier/scenarios/scenarios/scenarios.json`) // block=21, gas used by execution: 131488, in header: 50495 - bt.SkipLoad(`^for_amsterdam/istanbul/eip1344_chainid/chainid/chainid.json`) // block=1, gas used by execution: 45005, in header: 61133 - bt.SkipLoad(`^for_amsterdam/osaka/eip7934_block_rlp_limit/max_block_rlp_size/block_rlp_size_at_limit_with_all_typed_transactions.json`) // block=1, gas used by execution: 537261968, in header: 537278096 - bt.SkipLoad(`^for_amsterdam/paris/eip7610_create_collision/collision_selfdestruct/selfdestruct_after_create2_collision.json`) // block=1, gas used by execution: 499019, in header: 409422 - bt.SkipLoad(`^for_amsterdam/paris/eip7610_create_collision/initcollision/init_collision_create_opcode.json`) // block=1, receiptHash mismatch: 524db87016bef6d465f58a76b2d203a5aa7530ed58655fc1a222fc976da98830 != 0ef8df661f25e664b1912098fbf14882312c23e5318ea1af9948df4ad71304eb, headerNum=1, 441aaedd3994f8ef204e166eefeb7cfc63b2250c0423d68737a120ebba7f117e - bt.SkipLoad(`^for_amsterdam/paris/eip7610_create_collision/revert_in_create/create2_collision_storage.json`) // block=1, receiptHash mismatch: c5beba07298d35464a539c2062ecf900c130d2400e66393b762a20377e6af894 != e019c41e16c55b80810e927d1bba60fc149d3da257bd5a4f83260142b1debc8f, headerNum=1, 2c115de7d2427ab2aac6d6d93890448d389878a11e1cb6dec62a5c7ac91e75ab - bt.SkipLoad(`^for_amsterdam/paris/security/selfdestruct_balance_bug/tx_selfdestruct_balance_bug.json`) // block=1, gas used by execution: 266498, in header: 128408 - bt.SkipLoad(`^for_amsterdam/prague/eip6110_deposits/deposits/deposit.json`) // block=1, gas used by execution: 112704, in header: 95374 - bt.SkipLoad(`^for_amsterdam/prague/eip7002_el_triggerable_withdrawals/withdrawal_requests/withdrawal_requests.json`) // block=1, gas used by execution: 225408, in header: 150272 - bt.SkipLoad(`^for_amsterdam/prague/eip7251_consolidations/consolidations/consolidation_requests.json`) // block=1, gas used by execution: 262976, in header: 187840 - bt.SkipLoad(`^for_amsterdam/prague/eip7623_increase_calldata_cost/transaction_validity/transaction_validity_type_3.json`) // block=1, gas used by execution: 596488, in header: 814088 - bt.SkipLoad(`^for_amsterdam/prague/eip7623_increase_calldata_cost/transaction_validity/transaction_validity_type_4.json`) // block=1, gas used by execution: 2809672, in header: 3027272 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/gas/account_warming.json`) // block=1, gas used by execution: 185492, in header: 316980 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/creating_delegation_designation_contract.json`) // block=1, receiptHash mismatch: eb0a2410285068047569b15f5785dacab88b92c4551f532189e41037162268dc != f5610820eb298919377f6d68f2eb097e952382f07a765552fb25d2a0b8bdad59, headerNum=1, c839439445443752f8b49facf7f0eff1f457461e8346d022e9b5683713ffd258 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/delegation_clearing.json`) // block=1, gas used by execution: 102138, in header: 233626 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/delegation_clearing_and_set.json`) // block=1, gas used by execution: 91572, in header: 354548 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/delegation_clearing_failing_tx.json`) // block=1, gas used by execution: 28506, in header: 158490 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/delegation_clearing_tx_to.json`) // block=1, gas used by execution: 28500, in header: 158490 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/eoa_tx_after_set_code.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/ext_code_on_chain_delegating_set_code.json`) // block=1, gas used by execution: 448468, in header: 579956 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/ext_code_on_self_delegating_set_code.json`) // block=1, gas used by execution: 177274, in header: 308762 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/ext_code_on_self_set_code.json`) // block=1, gas used by execution: 177274, in header: 308762 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/ext_code_on_set_code.json`) // block=1, gas used by execution: 177274, in header: 308762 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/many_delegations.json`) // block=1, gas used by execution: 2737768, in header: 15886568 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/nonce_overflow_after_first_authorization.json`) // block=1, gas used by execution: 260628, in header: 392116 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/nonce_validity.json`) // block=1, gas used by execution: 102138, in header: 233626 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/reset_code.json`) // block=1, gas used by execution: 129140, in header: 392116 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/self_code_on_set_code.json`) // block=1, gas used by execution: 139706, in header: 271194 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/self_sponsored_set_code.json`) // block=1, receiptHash mismatch: 64d5287109ecfacbe2249911a87ae7f100a59abe2238541e248a4a1c000f8602 != 6714a2b04db021c4cd8e2e9df55ad5e53d2828b4878d1ae2286b8011f9b032f6, headerNum=1, a0fce57f98c8613a01fa50846ce1e6f05644b726cd9c64d87f55d5070eef0ff5 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_max_depth_call_stack.json`) // block=1, gas used by execution: 257711, in header: 258015 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce.json`) // block=1, gas used by execution: 439076, in header: 1622468 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce_self_sponsored.json`) // block=1, gas used by execution: 307588, in header: 1622468 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_log.json`) // block=1, gas used by execution: 29149, in header: 158490 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_non_empty_storage_non_zero_nonce.json`) // block=1, gas used by execution: 33512, in header: 158490 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_self_destruct.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_self_destructing_account_deployed_in_same_tx.json`) // block=1, gas used by execution: 509516, in header: 308762 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_sstore.json`) // block=1, receiptHash mismatch: da5d7c1fbb98154096d9cb65b43fa5562977b8f344e909589ab32fe0ac29e542 != 3b00e115c1357b094c8e9b40820126ab7984c7cc3603cbc6afbe4ea6b1b3af46, headerNum=1, 279196e38455deb9929c5fb6287da6d58d9109a9b244ce5640761b24f6f6f035 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_sstore_then_sload.json`) // block=1, gas used by execution: 260628, in header: 392116 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/set_code_to_system_contract.json`) // block=1, gas used by execution: 139706, in header: 271194 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/call_pointer_to_created_from_create_after_oog_call_again.json`) // block=1, gas used by execution: 319328, in header: 450816 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/call_to_precompile_in_pointer_context.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/contract_storage_to_pointer_with_storage.json`) // block=1, gas used by execution: 44175, in header: 158490 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/delegation_replacement_call_previous_contract.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/double_auth.json`) // block=1, gas used by execution: 91572, in header: 354548 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_measurements.json`) // block=2, gas used by execution: 646874, in header: 778362 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_normal.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_reentry.json`) // block=1, gas used by execution: 477818, in header: 609306 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_resets_an_empty_code_account_with_storage.json`) // block=1, gas used by execution: 427336, in header: 821800 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_reverts.json`) // block=1, gas used by execution: 177274, in header: 308762 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_to_precompile.json`) // block=1, gas used by execution: 64570, in header: 196058 - bt.SkipLoad(`^for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/pointer_to_static_reentry.json`) // block=1, gas used by execution: 140147, in header: 233626 - bt.SkipLoad(`^for_amsterdam/shanghai/eip3855_push0/push0/push0_contracts.json`) // block=1, receiptHash mismatch: 777f1c1c378807634128348e4f0eeca6a0e7f516ea411690ca04266323f671a4 != 2f13c48591f063e30a792a180116e5ef2611efc62565ec81f9cbe853e23bc631, headerNum=1, a89980a26041d5d56ba7192f619f4dc13d8bed40c41fff9f8be25b15b5a207eb - bt.SkipLoad(`^for_amsterdam/shanghai/eip3860_initcode/initcode/create2_oversized_initcode_with_insufficient_balance.json`) // block=1, gas used by execution: 169056, in header: 37568 - bt.SkipLoad(`^for_amsterdam/tangerine_whistle/eip150_operation_gas_costs/eip150_selfdestruct/initcode_selfdestruct_to_self.json`) // block=1, gas used by execution: 131488, in header: 35040 - bt.SkipLoad(`^for_amsterdam/tangerine_whistle/eip150_operation_gas_costs/eip150_selfdestruct/selfdestruct_state_access_boundary.json`) // block=1, gas used by execution: 31024, in header: 32304 - bt.SkipLoad(`^for_amsterdam/tangerine_whistle/eip150_operation_gas_costs/eip150_selfdestruct/selfdestruct_to_precompile_state_access_boundary.json`) // block=1, gas used by execution: 31024, in header: 32304 - bt.SkipLoad(`^for_amsterdam/tangerine_whistle/eip150_operation_gas_costs/eip150_selfdestruct/selfdestruct_to_self.json`) // block=1, gas used by execution: 133836, in header: 35188 - bt.SkipLoad(`^for_amsterdam/tangerine_whistle/eip150_operation_gas_costs/eip150_selfdestruct/selfdestruct_to_system_contract.json`) // block=1, gas used by execution: 31024, in header: 32304 - bt.Walk(t, dir, func(t *testing.T, name string, test *testutil.BlockTest) { // import pre accounts & construct test genesis block & state root test.ExperimentalBAL = true // TODO eventually remove this from BlockTest and run normally diff --git a/execution/tests/execution-spec-tests b/execution/tests/execution-spec-tests index ec66995f0de..b1241d947fd 160000 --- a/execution/tests/execution-spec-tests +++ b/execution/tests/execution-spec-tests @@ -1 +1 @@ -Subproject commit ec66995f0debb19fb1e0e4499cf7e4a172b46028 +Subproject commit b1241d947fdd611ddce263d3c70cf12b987df68f diff --git a/execution/tests/testutil/transaction_test_util.go b/execution/tests/testutil/transaction_test_util.go index a0a4b712b93..cb378539469 100644 --- a/execution/tests/testutil/transaction_test_util.go +++ b/execution/tests/testutil/transaction_test_util.go @@ -91,6 +91,7 @@ func (tt *TransactionTest) Run(chainID *big.Int) error { IsEIP3860: rules.IsShanghai, IsEIP7623: rules.IsPrague, IsEIP7976: rules.IsAmsterdam, + IsEIP7981: rules.IsAmsterdam, IsEIP8037: rules.IsAmsterdam, }) requiredGas := intrinsicGasResult.RegularGas diff --git a/execution/tracing/gen_gas_change_reason_stringer.go b/execution/tracing/gen_gas_change_reason_stringer.go index 2047f5959b2..3b5dac2d54e 100644 --- a/execution/tracing/gen_gas_change_reason_stringer.go +++ b/execution/tracing/gen_gas_change_reason_stringer.go @@ -24,21 +24,22 @@ func _() { _ = x[GasChangeCallStorageColdAccess-13] _ = x[GasChangeCallFailedExecution-14] _ = x[GasChangeDelegatedDesignation-15] + _ = x[GasChangeFrameStateGas-16] _ = x[GasChangeIgnored-255] } const ( - _GasChangeReason_name_0 = "GasChangeUnspecifiedGasChangeTxInitialBalanceGasChangeTxIntrinsicGasGasChangeTxRefundsGasChangeTxLeftOverReturnedGasChangeCallInitialBalanceGasChangeCallLeftOverReturnedGasChangeCallLeftOverRefundedGasChangeCallContractCreationGasChangeCallContractCreation2GasChangeCallCodeStorageGasChangeCallOpCodeGasChangeCallPrecompiledContractGasChangeCallStorageColdAccessGasChangeCallFailedExecutionGasChangeDelegatedDesignation" + _GasChangeReason_name_0 = "GasChangeUnspecifiedGasChangeTxInitialBalanceGasChangeTxIntrinsicGasGasChangeTxRefundsGasChangeTxLeftOverReturnedGasChangeCallInitialBalanceGasChangeCallLeftOverReturnedGasChangeCallLeftOverRefundedGasChangeCallContractCreationGasChangeCallContractCreation2GasChangeCallCodeStorageGasChangeCallOpCodeGasChangeCallPrecompiledContractGasChangeCallStorageColdAccessGasChangeCallFailedExecutionGasChangeDelegatedDesignationGasChangeFrameStateGas" _GasChangeReason_name_1 = "GasChangeIgnored" ) var ( - _GasChangeReason_index_0 = [...]uint16{0, 20, 45, 68, 86, 113, 140, 169, 198, 227, 257, 281, 300, 332, 362, 390, 419} + _GasChangeReason_index_0 = [...]uint16{0, 20, 45, 68, 86, 113, 140, 169, 198, 227, 257, 281, 300, 332, 362, 390, 419, 441} ) func (i GasChangeReason) String() string { switch { - case i <= 15: + case i <= 16: return _GasChangeReason_name_0[_GasChangeReason_index_0[i]:_GasChangeReason_index_0[i+1]] case i == 255: return _GasChangeReason_name_1 diff --git a/execution/tracing/hooks.go b/execution/tracing/hooks.go index 666a0c857ee..6e338cd9af4 100644 --- a/execution/tracing/hooks.go +++ b/execution/tracing/hooks.go @@ -297,6 +297,11 @@ const ( GasChangeCallFailedExecution GasChangeReason = 14 // GasChangeDelegatedDesignation is the amount of gas that will be charged for resolution of delegated designation. GasChangeDelegatedDesignation GasChangeReason = 15 + // GasChangeFrameStateGas is the EIP-8037 per-frame state-gas charge or + // credit emitted at frame commit, computed via journal walk. + // Positive (charge) reduces gas.State (with spill into gas.Regular). + // Negative (credit) increases gas.State and decrements executionStateGas. + GasChangeFrameStateGas GasChangeReason = 16 // GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as // it will be "manually" tracked by a direct emit of the gas change event. diff --git a/execution/types/aa_transaction.go b/execution/types/aa_transaction.go index 4cecd93c04a..137a9a689ba 100644 --- a/execution/types/aa_transaction.go +++ b/execution/types/aa_transaction.go @@ -502,6 +502,7 @@ func (tx *AccountAbstractionTransaction) PreTransactionGasCost(rules *chain.Rule IsEIP3860: hasEIP3860, IsEIP7623: rules.IsPrague, IsEIP7976: rules.IsAmsterdam, + IsEIP7981: rules.IsAmsterdam, IsEIP8037: rules.IsAmsterdam, IsAATxn: true, }) diff --git a/execution/vm/errors.go b/execution/vm/errors.go index 164f201e384..5e7b56d117a 100644 --- a/execution/vm/errors.go +++ b/execution/vm/errors.go @@ -46,6 +46,13 @@ var ( ErrReturnStackExceeded = errors.New("return stack limit reached") ErrInvalidCode = errors.New("invalid code") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + // ErrFrameStateGasOOG is a soft OOG raised by EIP-8037 frame-end state-gas + // accounting when the combined state-gas reservoir + remaining regular gas + // cannot cover the per-frame state-gas charge. Per the EELS reference, this + // rolls back state changes for the frame but PRESERVES the frame's remaining + // gas (it is returned to the parent), unlike a true exceptional halt which + // burns it. Treated as a REVERT-equivalent in handleFrameRevert. + ErrFrameStateGasOOG = errors.New("frame state gas out of gas") // errStopToken is an internal token indicating interpreter loop termination, // never returned to outside callers. diff --git a/execution/vm/evm.go b/execution/vm/evm.go index eb6e0e394f8..18bdc77c79d 100644 --- a/execution/vm/evm.go +++ b/execution/vm/evm.go @@ -95,9 +95,21 @@ type EVM struct { readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse - stateGasConsumed uint64 // total state gas charged during tx execution (restored on depth>0 revert, kept on depth-0) regularGasConsumed uint64 // total regular gas charged during tx execution (for block-level accounting) - revertedSpillGas uint64 // state gas that spilled to regular and was restored on depth-0 revert + // executionStateGas accumulates the EIP-8037 state-gas charges (and credits) + // across all frame commits in this tx. Charged at frame-commit time via the + // journal-walk-and-delta algorithm (see IntraBlockState.ComputeFrameStateBytes). + // Reset to 0 on top-level revert (depth==0 && err != nil). Signed because + // per-tx refunds (EIP-6780 same-tx selfdestruct, EIP-7702 auth-list refund) + // can drive this counter negative; the block-level computation clamps to 0 + // (block_state_gas_used = max(0, intrinsic_state + executionStateGas)). + executionStateGas int64 + // committedChildBytes is a per-depth stack of running totals — when a child + // frame commits, its walkTotal is added to the parent's slot. Used at + // frame-commit to compute delta = walkTotal - committedChildBytes[depth]. + // Signed because cleared-slot rules (EIP-8037) and child frames that net + // shrink state can produce negative walkTotals that propagate to parents. + committedChildBytes []int64 } // NewEVM returns a new EVM. The returned EVM is not thread safe and should @@ -158,36 +170,180 @@ func (evm *EVM) Cancel() { evm.abort.Store(true) } // Cancelled returns true if Cancel has been called func (evm *EVM) Cancelled() bool { return evm.abort.Load() } -// StateGasConsumed returns the total state gas charged during tx execution. -// Restored on depth>0 revert (so parent frame sees correct value), kept on depth-0 revert -// (so block gas accounting includes reverted state gas). -func (evm *EVM) StateGasConsumed() uint64 { return evm.stateGasConsumed } +// ExecutionStateGas returns the EIP-8037 execution state gas accumulated across +// all frame commits in the current tx. Reset to 0 on top-level revert. +func (evm *EVM) ExecutionStateGas() int64 { return evm.executionStateGas } // RegularGasConsumed returns the total regular gas charged during tx execution (for block-level accounting) func (evm *EVM) RegularGasConsumed() uint64 { return evm.regularGasConsumed } -// RevertedSpillGas returns state gas that spilled to regular and was restored on depth-0 revert -func (evm *EVM) RevertedSpillGas() uint64 { return evm.revertedSpillGas } - // ResetGasConsumed resets the gas consumed counters for a new transaction func (evm *EVM) ResetGasConsumed() { - evm.stateGasConsumed = 0 evm.regularGasConsumed = 0 - evm.revertedSpillGas = 0 + evm.executionStateGas = 0 + evm.committedChildBytes = evm.committedChildBytes[:0] +} + +// pushFrameAccumulator pushes a 0 onto the per-depth committedChildBytes stack +// at frame entry. Pair with popFrameAccumulator at frame exit (commit or revert). +func (evm *EVM) pushFrameAccumulator() { + evm.committedChildBytes = append(evm.committedChildBytes, 0) } -// handleFrameRevert handles the full error path for a call or create frame: -// state revert, regular gas burning on exceptional halt, and EIP-8037 state -// gas accounting (spill restoration, depth-dependent reservoir preservation). -func (evm *EVM) handleFrameRevert(gas *mdgas.MdGas, err error, depth int, - snapshot int, - savedStateGasConsumed, initialChildState uint64) { +// popFrameAccumulator pops the top of committedChildBytes and returns its value. +// At commit time the caller passes the popped value to the parent via +// propagateChildBytes. On revert the caller discards it. +func (evm *EVM) popFrameAccumulator() int64 { + n := len(evm.committedChildBytes) + if n == 0 { + return 0 + } + top := evm.committedChildBytes[n-1] + evm.committedChildBytes = evm.committedChildBytes[:n-1] + return top +} + +// propagateChildBytes adds walkTotal to the parent's committedChildBytes slot +// after a child frame commits. If we are at depth 0 (no parent) this is a no-op. +// +// Negative walkTotals (from EIP-8037 cleared-slot credits) are NOT propagated: +// matching EELS, the reservoir credit happens at the child frame, while the +// parent's `already_paid` (committedChildBytes) tracks only positive charges. +// Reservoir gas itself is propagated via the gas return mechanism (the parent +// receives the child's leftover gas, which includes any credited state gas). +func (evm *EVM) propagateChildBytes(walkTotal int64) { + n := len(evm.committedChildBytes) + if n == 0 { + return + } + if walkTotal > 0 { + evm.committedChildBytes[n-1] += walkTotal + } +} +// adjustGasStateAndExecution applies a signed `restoreGas` adjustment to the +// frame's state-gas reservoir (gas.State) and the running per-tx +// `executionStateGas` counter. Used on frame revert to undo the per-byte effects +// committed by descendants: positive `restoreGas` returns previously-charged +// gas to the reservoir (and decrements executionStateGas); negative +// `restoreGas` reverses a previously-credited descendant (e.g. EIP-8037 +// cleared-slot) and decrements the reservoir. +func adjustGasStateAndExecution(gas *mdgas.MdGas, executionStateGas *int64, restoreGas int64) { + if restoreGas >= 0 { + gas.State += uint64(restoreGas) + } else { + sub := uint64(-restoreGas) + if gas.State >= sub { + gas.State -= sub + } else { + gas.State = 0 + } + } + *executionStateGas -= restoreGas +} + +// chargeFrameStateGas runs the EIP-8037 frame-end state-gas accounting for a +// successful call/create commit, when Amsterdam is active and the EVM is not +// in RestoreState mode. It walks the journal segment [frameStart, frameEnd), +// computes delta = walkTotal - committedChildBytes[depth] - accountRefund, and: +// - delta > 0 → charge delta×CPSB from gas.State (with spill into gas.Regular). +// On insufficient gas, returns ErrFrameStateGasOOG (caller treats as REVERT). +// - delta < 0 → credit |delta|×CPSB back to gas.State and decrement +// evm.executionStateGas. +// +// Returns the walkTotal so the caller can propagate it to the parent's +// accumulator. The caller is responsible for pushing/popping the per-depth +// accumulator slot via pushFrameAccumulator/popFrameAccumulator. +// +// At depth==0 (top frame), the walk computes accountRefund — bytes for accounts +// in (newlyCreated && selfdestructed), mirroring EELS's accounts_to_delete ∩ +// created_accounts refund. This naturally subsumes the per-tx +// CREATE+SELFDESTRUCT case (e.g. test_selfdestruct_in_create_tx_initcode) and +// the cross-frame CREATE-then-SELFDESTRUCT cases (cancun/eip6780_selfdestruct). +// +// excludeCreate is the contract address for top-level CREATE txs (depth==0 +// inside evm.create) — its 112-byte account record is intrinsic-charged so +// the walk skips it. NilAddress in all other cases. +func (evm *EVM) chargeFrameStateGas( + gas *mdgas.MdGas, + frameStart int, + depth int, + excludeCreate accounts.Address, +) (walkTotal int64, err error) { + frameEnd := evm.intraBlockState.JournalLength() + applyFilter := depth == 0 + var accountRefund uint64 + walkTotal, accountRefund = evm.intraBlockState.ComputeFrameStateBytes(frameStart, frameEnd, applyFilter, excludeCreate) + + n := len(evm.committedChildBytes) + var childTotal int64 + if n > 0 { + childTotal = evm.committedChildBytes[n-1] + } + + cpsb := evm.Context.CostPerStateByte + delta := walkTotal - childTotal - int64(accountRefund) + if delta == 0 { + return walkTotal, nil + } + + if delta > 0 { + stateGas := uint64(delta) * cpsb + var ok bool + *gas, ok = useMdGas(evm, *gas, stateGas, mdgas.StateGas, evm.config.Tracer, tracing.GasChangeFrameStateGas) + if !ok { + return walkTotal, ErrFrameStateGasOOG + } + return walkTotal, nil + } + creditGas := uint64(-delta) * cpsb + if evm.config.Tracer != nil && evm.config.Tracer.OnGasChange != nil { + evm.config.Tracer.OnGasChange(gas.State, gas.State+creditGas, tracing.GasChangeFrameStateGas) + } + gas.State += creditGas + // EIP-8037: when delta < 0 (cleared-slot credits, EIP-6780 same-tx + // selfdestruct refunds) we credit the reservoir but DO NOT decrement + // executionStateGas. This matches EELS's apply_frame_state_gas, where the + // negative this_call_cost path only updates state_gas_reservoir and leaves + // state_gas_used unchanged. The reservoir credit propagates to the parent + // via the gas return mechanism, while the per-tx executionStateGas counter + // only tracks positive charges (which determine block_state_gas_used). + // + // Exception: account-refund (EIP-6780 same-tx-CREATE+SELFDESTRUCT of an + // account that EXISTED at tx-entry) DOES need to drive executionStateGas + // negative to offset the intrinsic_state contribution. That refund is in + // `accountRefund`; the cleared-slot credit is the part beyond accountRefund. + if int64(accountRefund) > 0 { + // Decrement executionStateGas only by the accountRefund portion of + // the credit (capped by total credit). The remainder of the credit + // (cleared-slot rules) does not affect executionStateGas. + acctCreditGas := accountRefund * cpsb + if acctCreditGas > creditGas { + acctCreditGas = creditGas + } + evm.executionStateGas -= int64(acctCreditGas) + } + return walkTotal, nil +} + +// handleFrameRevert handles the error path for a call or create frame: +// it reverts journal state and burns remaining regular gas on exceptional halt. +// +// EIP-8037 state-gas accounting is journal-walk-based at frame commit, so a +// reverted/halted frame contributes nothing — there's nothing to roll back +// here for state gas (the frame-end charge never fired). +// +// ErrFrameStateGasOOG is treated like REVERT: state is rolled back but the +// frame's remaining gas is preserved (returned to parent), matching EELS +// where apply_frame_state_gas's OOG sets evm.error directly without raising +// ExceptionalHalt. +func (evm *EVM) handleFrameRevert(gas *mdgas.MdGas, err error, depth int, snapshot int) { // 1. Revert state changes. evm.intraBlockState.RevertToSnapshot(snapshot, err) - // 2. On exceptional halt (not REVERT), burn remaining regular gas. - if err != ErrExecutionReverted { + // 2. On exceptional halt (not REVERT, not soft OOG from frame-state-gas), + // burn remaining regular gas. + if err != ErrExecutionReverted && err != ErrFrameStateGasOOG { if evm.chainRules.IsAmsterdam { evm.regularGasConsumed += gas.Regular } @@ -196,50 +352,6 @@ func (evm *EVM) handleFrameRevert(gas *mdgas.MdGas, err error, depth int, } gas.Regular = 0 } - - // 3. EIP-8037: state gas revert accounting. - if !evm.chainRules.IsAmsterdam { - return - } - childStateConsumed := evm.stateGasConsumed - savedStateGasConsumed - - // For child frames (depth > 0), restore stateGasConsumed so the parent - // frame sees the correct value. At depth 0 there is no parent, and we - // keep the full value for block gas accounting. - if depth > 0 { - evm.stateGasConsumed = savedStateGasConsumed - } - - // EIP-8037: "On child revert or exceptional halt, all state gas - // consumed by the child, both from the reservoir and any that spilled - // into gas_left, is restored to the parent's reservoir." - if depth == 0 { - if err == ErrExecutionReverted { - // Top-level REVERT: restore spill to gas_left for refund - // accounting; track it for receipt gas calculation. - // Spill = state gas that was charged from gas_left (regular) - // because the reservoir was insufficient. When gas.State > - // initialChildState the reservoir grew via sub-child reverts — - // no reservoir was consumed net, so all childStateConsumed - // was spilled. - var reservoirUsed uint64 - if initialChildState > gas.State { - reservoirUsed = initialChildState - gas.State - } - spill := childStateConsumed - reservoirUsed - gas.Regular += spill - evm.revertedSpillGas += spill - } - // Top-level exceptional halt: gas.Regular already zeroed in step 2; - // reservoir stays as-is for block gas accounting. - } else { - // Child frame (depth > 0): restore all consumed state gas - // (reservoir-sourced + spill) to the reservoir, preserving any - // sub-child restorations already in the reservoir. - gas.State += childStateConsumed - // Regular gas: REVERT preserves it (step 2 doesn't apply); - // exceptional halt burns it (step 2 zeroed gas.Regular). - } } // CallGasTemp returns the callGasTemp for the EVM @@ -362,8 +474,11 @@ func (evm *EVM) call(typ OpCode, caller accounts.Address, callerAddress accounts evm.intraBlockState.AddBalance(addr, u256.Num0, tracing.BalanceChangeTouchAccount) } - savedStateGasConsumed := evm.stateGasConsumed - initialChildState := gas.State + // EIP-8037 frame-end state-gas accounting: capture the journal index at + // frame entry, push a per-depth committed-children accumulator slot. + frameStart := evm.intraBlockState.JournalLength() + evm.pushFrameAccumulator() + frameAccumulatorPopped := false // It is allowed to call precompiles, even via delegatecall if isPrecompile { @@ -382,6 +497,7 @@ func (evm *EVM) call(typ OpCode, caller accounts.Address, callerAddress accounts var codeHash accounts.CodeHash codeHash, err = evm.intraBlockState.ResolveCodeHash(addr) if err != nil { + evm.popFrameAccumulator() return nil, mdgas.MdGas{}, fmt.Errorf("%w: %w", ErrIntraBlockStateFailed, err) } var contract Contract @@ -416,11 +532,46 @@ func (evm *EVM) call(typ OpCode, caller accounts.Address, callerAddress accounts } ret, gas, err = evm.Run(contract, gas, input, readOnly) } + + // EIP-8037 frame commit (success path, Amsterdam, not in RestoreState): + // run the journal-walk-and-charge before we evaluate the error path so + // that an OOG at the frame-end charge correctly triggers a revert. + if err == nil && !evm.config.RestoreState && evm.chainRules.IsAmsterdam { + walkTotal, chargeErr := evm.chargeFrameStateGas(&gas, frameStart, depth, accounts.NilAddress) + if chargeErr != nil { + err = chargeErr + } else { + // Commit succeeded: pop self, propagate walkTotal to parent's accumulator. + evm.popFrameAccumulator() + frameAccumulatorPopped = true + evm.propagateChildBytes(walkTotal) + } + } + // When an error was returned by the EVM or when setting the creation code // above we revert to the snapshot and consume any gas remaining. Additionally // when we're in Homestead this also counts for code storage gas errors. if err != nil || evm.config.RestoreState { - evm.handleFrameRevert(&gas, err, depth, snapshot, savedStateGasConsumed, initialChildState) + if !frameAccumulatorPopped { + poppedChildBytes := evm.popFrameAccumulator() + // EIP-8037: on child revert or exceptional halt, all state gas + // consumed by committed descendants — both from the reservoir and + // any that spilled into gas_left — is restored to this frame's + // reservoir (which is then propagated to the parent via + // restoreChildGas). Skip in RestoreState mode (caller discards + // results anyway). At depth==0 this also naturally zeroes + // evm.executionStateGas because the top frame's accumulator holds + // the sum of all charged bytes for the tx. + if !evm.config.RestoreState && evm.chainRules.IsAmsterdam && poppedChildBytes != 0 { + restoreGas := poppedChildBytes * int64(evm.Context.CostPerStateByte) + adjustGasStateAndExecution(&gas, &evm.executionStateGas, restoreGas) + } + } + evm.handleFrameRevert(&gas, err, depth, snapshot) + } else if !frameAccumulatorPopped { + // Pre-Amsterdam: no state-gas charging happens in this branch; pop the + // accumulator without propagating. + evm.popFrameAccumulator() } return ret, gas, err @@ -597,8 +748,11 @@ func (evm *EVM) create(caller accounts.Address, codeAndHash *codeAndHash, gasRem return nil, address, gasRemaining, nil } - savedStateGasConsumed := evm.stateGasConsumed - initialChildState := gasRemaining.State + // EIP-8037 frame-end state-gas accounting: capture the journal index at + // frame entry, push the per-depth committed-children accumulator slot. + frameStart := evm.intraBlockState.JournalLength() + evm.pushFrameAccumulator() + frameAccumulatorPopped := false ret, gasRemaining, err = evm.Run(contract, gasRemaining, nil, false) @@ -611,47 +765,38 @@ func (evm *EVM) create(caller accounts.Address, codeAndHash *codeAndHash, gasRem err = ErrInvalidCode } // If the contract creation ran successfully and no errors were returned, - // calculate the gas required to store the code. If the code could not - // be stored due to not enough gas, set an error when we're in Homestead and let it be handled - // by the error checking condition below. + // calculate the regular gas required to store the code. If the code could + // not be stored due to not enough gas, set an error when we're in Homestead + // and let it be handled by the error checking condition below. + // + // EIP-8037: state gas for code deposit is NOT charged here — it falls out + // of the journal-walk at frame commit (via the codeChange entry SetCode + // emits below). Only the regular gas for code deposit is charged inline. if err == nil { - // EIP-8037: GAS_CODE_DEPOSIT = cpsb/byte (state) + 6*ceil(len/32) (regular) - // Pre-Amsterdam: GAS_CODE_DEPOSIT = 200/byte (regular only) preDepositGas := gasRemaining - preDepositStateGasConsumed := evm.stateGasConsumed - // Charge state gas (Amsterdam only). - stateGasOk := true + var regularGas uint64 if evm.chainRules.IsAmsterdam { - stateGas := uint64(len(ret)) * evm.Context.CostPerStateByte - gasRemaining, stateGasOk = useMdGas(evm, gasRemaining, stateGas, mdgas.StateGas, evm.Config().Tracer, tracing.GasChangeCallCodeStorage) + // EIP-8037 "Contract deployment cost calculation", success path: + // HASH_COST(L) = 6*ceil(L/32). The state component (cpsb*L) is + // derived at frame commit from the codeChange journal entry. + regularGas = params.Keccak256WordGas * ToWordSize(uint64(len(ret))) + } else { + regularGas = uint64(len(ret)) * params.CreateDataGas } - - // Charge regular gas. var regularGasOk bool - if stateGasOk { - var regularGas uint64 - if evm.chainRules.IsAmsterdam { - // EIP-8037 "Contract deployment cost calculation", success path: - // HASH_COST(L) = 6*ceil(L/32); the state component (cpsb*L) is charged above. - regularGas = params.Keccak256WordGas * ToWordSize(uint64(len(ret))) - } else { - regularGas = uint64(len(ret)) * params.CreateDataGas - } - gasRemaining, regularGasOk = useMdGas(evm, gasRemaining, regularGas, mdgas.RegularGas, evm.Config().Tracer, tracing.GasChangeCallCodeStorage) - } + gasRemaining, regularGasOk = useMdGas(evm, gasRemaining, regularGas, mdgas.RegularGas, evm.Config().Tracer, tracing.GasChangeCallCodeStorage) - if stateGasOk && regularGasOk { + if regularGasOk { evm.intraBlockState.SetCode(address, ret) } else { if evm.chainRules.IsAmsterdam { - // Code deposit failed: per EIP-8037 the failure cost is - // GAS_CREATE + initcode_execution_cost only; code deposit - // gas (both state and regular) is excluded. Undo the - // charges so that handleFrameRevert and block-level gas - // accounting see the correct values. + // Code-deposit OOG: per EIP-8037 the failure cost is + // GAS_CREATE + initcode_execution_cost only; code-deposit + // gas is excluded. Restore regular gas to pre-deposit state + // (state gas needs no rollback — SetCode hasn't fired so + // the codeChange isn't in the journal). gasRemaining = preDepositGas - evm.stateGasConsumed = preDepositStateGasConsumed } // If we run out of gas, we do not store the code: the returned code must be empty. ret = []byte{} @@ -661,11 +806,46 @@ func (evm *EVM) create(caller accounts.Address, codeAndHash *codeAndHash, gasRem } } + // EIP-8037 frame commit (success path, Amsterdam, not in RestoreState): + // run the journal-walk-and-charge before the error path so that an OOG at + // the frame-end charge correctly triggers a revert. excludeCreate is the + // contract address when this is the top-level CREATE tx (depth==0); the + // 112×CPSB intrinsic already covers it, so the walk must skip it to avoid + // double-counting. + if err == nil && !evm.config.RestoreState && evm.chainRules.IsAmsterdam { + excludeCreate := accounts.NilAddress + if depth == 0 { + excludeCreate = address + } + walkTotal, chargeErr := evm.chargeFrameStateGas(&gasRemaining, frameStart, depth, excludeCreate) + if chargeErr != nil { + err = chargeErr + } else { + evm.popFrameAccumulator() + frameAccumulatorPopped = true + evm.propagateChildBytes(walkTotal) + } + } + // When an error was returned by the EVM or when setting the creation code // above, we revert to the snapshot and consume any gas remaining. Additionally, // when we're in Homestead, this also counts for code storage gas errors. if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { - evm.handleFrameRevert(&gasRemaining, err, depth, snapshot, savedStateGasConsumed, initialChildState) + if !frameAccumulatorPopped { + poppedChildBytes := evm.popFrameAccumulator() + // EIP-8037: on child revert or exceptional halt, restore all state + // gas consumed by committed descendants to this frame's reservoir. + // See evm.call() for the full rationale. + if !evm.config.RestoreState && evm.chainRules.IsAmsterdam && poppedChildBytes != 0 { + restoreGas := poppedChildBytes * int64(evm.Context.CostPerStateByte) + adjustGasStateAndExecution(&gasRemaining, &evm.executionStateGas, restoreGas) + } + } + evm.handleFrameRevert(&gasRemaining, err, depth, snapshot) + } else if !frameAccumulatorPopped { + // Pre-Amsterdam or RestoreState: pop the accumulator without + // propagating (no state-gas charging happens in this branch). + evm.popFrameAccumulator() } return ret, address, gasRemaining, err diff --git a/execution/vm/gas_table.go b/execution/vm/gas_table.go index 78defa838f7..882099dfade 100644 --- a/execution/vm/gas_table.go +++ b/execution/vm/gas_table.go @@ -498,7 +498,7 @@ func statelessGasCall(evm *EVM, callContext *CallContext, availableGas mdgas.MdG } func statefulGasCall(evm *EVM, callContext *CallContext, gas mdgas.MdGas, availableGas mdgas.MdGas, transfersValue bool) (mdgas.MdGas, error) { - var accountGas, stateGas uint64 + var accountGas uint64 var address = accounts.InternAddress(callContext.Stack.Back(1).Bytes20()) rules := evm.ChainRules() if rules.IsSpuriousDragon { @@ -510,12 +510,13 @@ func statefulGasCall(evm *EVM, callContext *CallContext, gas mdgas.MdGas, availa // tracking unconditionally, since the read happens regardless of // whether the CALL proceeds or transfers value. evm.IntraBlockState().MarkAddressAccess(address, false) - if transfersValue && empty { - if rules.IsAmsterdam { - stateGas = params.StateBytesNewAccount * evm.Context.CostPerStateByte - } else { - accountGas = params.CallNewAccountGas - } + // EIP-8037: under Amsterdam, the state gas for materializing a new + // account via CALL-with-value is no longer charged inline. The + // CreateAccount call inside evm.call() emits a createObjectChange + // after the called frame's snapshot, so the called frame's commit + // walk picks up +112. Pre-Amsterdam keeps the regular CallNewAccountGas. + if transfersValue && empty && !rules.IsAmsterdam { + accountGas = params.CallNewAccountGas } } else { exists, err := evm.IntraBlockState().Exist(address) @@ -541,13 +542,6 @@ func statefulGasCall(evm *EVM, callContext *CallContext, gas mdgas.MdGas, availa evm.intraBlockState.BlockNumber(), evm.intraBlockState.TxIndex(), evm.intraBlockState.Incarnation(), accountGas) } - if stateGas > 0 { - gas.State, overflow = math.SafeAdd(gas.State, stateGas) - if overflow { - return mdgas.MdGas{}, ErrGasUintOverflow - } - } - return gas, nil } diff --git a/execution/vm/instructions.go b/execution/vm/instructions.go index b861f88184e..414576f0cfe 100644 --- a/execution/vm/instructions.go +++ b/execution/vm/instructions.go @@ -29,7 +29,6 @@ import ( "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/hexutil" "github.com/erigontech/erigon/common/log/v3" - "github.com/erigontech/erigon/execution/protocol/mdgas" "github.com/erigontech/erigon/execution/protocol/misc" "github.com/erigontech/erigon/execution/protocol/params" "github.com/erigontech/erigon/execution/tracing" @@ -1023,16 +1022,11 @@ func opCreate2(pc uint64, evm *EVM, scope *CallContext) (uint64, []byte, error) // execCreate is the shared implementation for opCreate (salt == nil) and opCreate2 (salt != nil). func execCreate(pc uint64, evm *EVM, scope *CallContext, value uint256.Int, input []byte, salt *uint256.Int) (uint64, []byte, error) { - if evm.ChainRules().IsAmsterdam { - // EIP-8037: charge state gas for account creation after the static-context - // check so that it is not consumed on early failures where no state is - // created (per execution-specs#2608). - stateGas := uint64(params.StateBytesNewAccount) * evm.Context.CostPerStateByte - if !scope.useMdGas(evm, stateGas, mdgas.StateGas, evm.Config().Tracer, tracing.GasChangeIgnored) { - return pc, nil, ErrOutOfGas - } - } - + // EIP-8037: state gas for account creation is no longer pre-charged here. + // On success, the createObjectChange emitted by evm.Create's CreateAccount + // is picked up by the frame-commit walk in evm.create. On silent failure + // (collision/balance/depth/nonce overflow), the snapshot is pushed and + // immediately popped so no journal entry persists → 0 bytes counted. gas := scope.Gas() if evm.ChainRules().IsTangerineWhistle { gas.Regular -= gas.Regular / 64 diff --git a/execution/vm/interpreter.go b/execution/vm/interpreter.go index 0f65a9802f1..7996165e036 100644 --- a/execution/vm/interpreter.go +++ b/execution/vm/interpreter.go @@ -175,22 +175,32 @@ func useMdGas(evm *EVM, initial mdgas.MdGas, gas uint64, t mdgas.MdGasType, trac var ok bool switch t { case mdgas.StateGas: + // EIP-8037 frame-end charging: state gas is consumed at frame commit + // via chargeFrameStateGas, which calls useMdGas. The consumed amount + // is added to evm.executionStateGas for tx-level block accounting. originalGas := gas initial.State, ok = useGas(initial.State, gas, tracer, reason) if ok { if evm != nil { - evm.stateGasConsumed += originalGas + evm.executionStateGas += int64(originalGas) } return initial, true } - // otherwise use up all remaining state gas and try to use some from the regular gas - gas = gas - initial.State + // State alone is insufficient. Check if spilling into Regular can + // cover the remainder before mutating anything — useMdGas must be + // transactional so that on insufficient gas the frame's reservoir + // is left untouched (so revert-time restoration of consumed-by- + // children state gas works correctly per EIP-8037). + needFromRegular := gas - initial.State + if initial.Regular < needFromRegular { + return initial, false + } initial.State = 0 - initial.Regular, ok = useGas(initial.Regular, gas, tracer, reason) - if ok && evm != nil { - evm.stateGasConsumed += originalGas + initial.Regular, _ = useGas(initial.Regular, needFromRegular, tracer, reason) + if evm != nil { + evm.executionStateGas += int64(originalGas) } - return initial, ok + return initial, true case mdgas.RegularGas: initial.Regular, ok = useGas(initial.Regular, gas, tracer, reason) if ok && evm != nil { diff --git a/execution/vm/operations_acl.go b/execution/vm/operations_acl.go index f56ccda8b68..bf661a4a237 100644 --- a/execution/vm/operations_acl.go +++ b/execution/vm/operations_acl.go @@ -74,10 +74,13 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { if original.Eq(¤t) { if original.IsZero() { // create slot (2.1.1) if rules.IsAmsterdam { - return mdgas.MdGas{Regular: cost + params.SstoreSetGasEIP8037, State: 32 * evm.Context.CostPerStateByte}, nil - } else { - return mdgas.MdGas{Regular: cost + params.SstoreSetGasEIP2200}, nil + // EIP-8037: state gas for the new slot is no longer charged + // inline. It is computed at frame commit by the journal walk + // (see IntraBlockState.ComputeFrameStateBytes). Only the + // regular-gas component changes here. + return mdgas.MdGas{Regular: cost + params.SstoreSetGasEIP8037}, nil } + return mdgas.MdGas{Regular: cost + params.SstoreSetGasEIP2200}, nil } if value.IsZero() { // delete slot (2.1.2b) evm.IntraBlockState().AddRefund(clearingRefund) @@ -98,8 +101,12 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { // EIP 2200 Original clause: //evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200) if rules.IsAmsterdam { + // EIP-8037: regular-gas refund still flows through the 20%-capped + // refund_counter. The state-gas component is no longer credited + // inline — the slot's net contribution is computed at frame + // commit via the journal walk, so a 0→X→0 within the same tx + // naturally produces 0 state gas without any explicit refund. evm.IntraBlockState().AddRefund(params.SstoreSetGasEIP8037 - params.WarmStorageReadCostEIP2929) - evm.IntraBlockState().AddStateRefund(32 * evm.Context.CostPerStateByte) } else { evm.IntraBlockState().AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929) } @@ -289,12 +296,12 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { if balance.IsZero() && address != callContext.Address() { evm.IntraBlockState().MarkNewReadsInternal(address, beneficiaryReadsBefore) } - if empty && !balance.IsZero() { - if evm.chainRules.IsAmsterdam { - gas.State = params.StateBytesNewAccount * evm.Context.CostPerStateByte - } else { - gas.Regular += params.CreateBySelfdestructGas - } + // EIP-8037: under Amsterdam, the state gas for a new beneficiary account + // is no longer charged inline — the AddBalance call inside opSelfdestruct + // emits a createObjectChange that's picked up by the frame-commit walk. + // Pre-Amsterdam still charges CreateBySelfdestructGas (regular only). + if empty && !balance.IsZero() && !evm.chainRules.IsAmsterdam { + gas.Regular += params.CreateBySelfdestructGas } hasSelfdestructed, err := evm.IntraBlockState().HasSelfdestructed(callContext.Address()) diff --git a/node/cli/flags.go b/node/cli/flags.go index e429068eec8..dbe601f20a9 100644 --- a/node/cli/flags.go +++ b/node/cli/flags.go @@ -98,7 +98,7 @@ var ( ExperimentalBALFlag = cli.BoolFlag{ Name: "experimental.bal", Usage: "generate block access list", - Value: false, + Value: true, } // Throttling Flags diff --git a/node/ethconfig/config.go b/node/ethconfig/config.go index 0ed826ab9ec..a34a52a18a7 100644 --- a/node/ethconfig/config.go +++ b/node/ethconfig/config.go @@ -114,7 +114,7 @@ var Defaults = Config{ FcuTimeout: 1 * time.Second, FcuBackgroundPrune: true, FcuBackgroundCommit: false, // to enable, we need to 1) have rawdb API go via execctx and 2) revive Coherent cache for rpcdaemon - ExperimentalBAL: false, + ExperimentalBAL: true, } const DefaultChainDBPageSize = 16 * datasize.KB diff --git a/txnprovider/txpool/pool.go b/txnprovider/txpool/pool.go index 1312be19ea2..f819b50ecea 100644 --- a/txnprovider/txpool/pool.go +++ b/txnprovider/txpool/pool.go @@ -795,6 +795,7 @@ func (p *TxPool) best(ctx context.Context, n int, txns *TxnsRlp, onTopOf uint64, IsEIP3860: isEIP3860, IsEIP7623: isEIP7623, IsEIP7976: isAmsterdam, + IsEIP7981: isAmsterdam, IsEIP8037: isAmsterdam, IsAATxn: isAATxn, }) @@ -989,6 +990,7 @@ func (p *TxPool) validateTx(txn *TxnSlot, isLocal bool, stateCache kvcache.Cache IsEIP3860: isEIP3860, IsEIP7623: isPrague, IsEIP7976: isAmsterdam, + IsEIP7981: isAmsterdam, IsEIP8037: isAmsterdam, IsAATxn: isAATxn, })