Summary
On bal-devnet-3, erigon's parallel executor fails block 503 with an EIP-8037 state-gas accounting mismatch:
invalid block, block=503, gas used by execution: 6265830, in header: 6641318
Diff: −375,488 of state-gas. Reproduces deterministically on feat/eip-8159-eth71-bal-exchange after the CPSB formula fix (72f2c8e922), on a freshly initialised datadir.
Block details
- Chain: bal-devnet-3 (chain ID 7098917910), ≈43 min after Glamsterdam activation.
- Block hash:
0xa9c9b682e5573f24662e6c2145c3fedcdac55baa1590803a82cad0e93b34fa2b
gasLimit = 80_000_000, canonical gasUsed = 6_641_318.
- Tx 0: CREATE, 68-byte initcode
0x600b3803…, deploys 0x4195b465…e8d3. Canonical receipt gasUsed = 229_533; we produce 143_005 (diff −86_528).
- Tx 1: call into deployed contract with 1003 ETH value, 6_464-byte calldata (
0xc481aaf8…). Canonical receipt gasUsed = 7_120_000 (OOG); we match 7_120_000.
Canonical block.gasUsed is max(Σ blockRegular, Σ blockState) per execution/protocol/state_processor.go:44; state is the larger term here (difference is the EIP-8037 state-gas portion).
Two distinct discrepancies, both in state-gas accounting
A. Block-level: Σ blockState is 375,488 too low
Per-tx breakdown from an exec3 run with a diagnostic counter (added to useMdGas, not committed):
tx0 msgGas=300000 initRegular=225856 initState=74144
regConsumed=45 stateConsumed=37734 totalSpill=37734
blockRegular=31127 blockState=111878 blockRegularUsed=31127
txnGasUsed=143005 refund=0 vmerr=nil
tx1 msgGas=7120000 initRegular=7120000 initState=0
regConsumed=856372 stateConsumed=6153952 totalSpill=6153952
blockRegular=966048 blockState=6153952 blockRegularUsed=966048
txnGasUsed=7120000 refund=0 vmerr=\"out of gas\"
Aggregates — Σ blockState = 6_265_830 (we produce); 6_641_318 (canonical) → −375,488.
tx1.totalSpill == tx1.stateConsumed == 6_153_952. The reservoir is effectively 0 for every sub-cap tx because SplitTxnGasLimit returns reservoir=0 whenever executionGas ≤ regularGasBudget (true for any tx with gasLimit ≤ MaxTxnGasLimit = 16_777_216), so every state op goes through the spill path.
tx1 ends OOG. The OOG-triggering state op calls useMdGas, finds initial.State = 0, attempts to spill the full cost from initial.Regular, and fails. In that failure path (execution/vm/interpreter.go:140-144) we return ok=false and do not increment stateGasConsumed — the attempted state cost is lost.
Hypothesis (unverified): the attempted state cost of the final failed-spill op equals the 375,488 gap, and canonical counts it toward block.gasUsed even though the tx OOGs on it. To confirm, increment stateGasConsumed on the ok=false branch and rerun — if the diff becomes exactly zero, hypothesis holds.
B. Receipt-level: tx0.txnGasUsed is 86,528 too low
tx0 is successful (vmerr=nil), so nothing was thrown away in a failed useMdGas. We still produce txnGasUsed = 143_005 where canonical expects 229_533.
txnGasUsed = initialGas.Total() − gasRemaining.Total() + revertedSpillGas = 300_000 − 156_995 + 0 = 143_005.
So gasRemaining.Total() is 86,528 too high — we return too much unused gas. Given this is a CREATE that ran 68-byte initcode and deployed a near-empty runtime with 3 logs, likely leak sites:
- EIP-8037 CREATE state-gas for code deposit (200 gas per deployed byte, possibly split regular vs state in a way that's off by ~86 K).
- Initcode EIP-3860 accounting.
- A state-gas path inside execution/vm/evm.go:595-644 (CREATE finalisation).
Independent of (A) — tx0 does not OOG, so the failed-spill hypothesis does not apply. Same consensus-root category (state-gas charging), different root cause.
Where to look
Cross-references:
ethereum/execution-specs (src/ethereum/forks/amsterdam/vm/) — reference behaviour for failed-spill accounting
- Geth handling of OOG during a state op
origin/feature/lystopad/hoodi-amsterdam-fix — iterated on this area; do not bulk apply, but individual commits worth reading:
563e81a36e fix Amsterdam REVERT spill accounting — depth-0 REVERT spill handling; formula over-counts for OOG txs as measured
78ae4b789c blockRegular via mdGasUsed.Regular − RevertedSpillGas — formula reframe with the same over-count problem
Reproduction
From an erigon checkout with feat/eip-8159-eth71-bal-exchange (or any branch carrying 72f2c8e922 CPSB formula fix):
make erigon
rm -rf /home/erigon/bal-devnet-3/node-a/{chaindata,caplin,dev-beacon,downloader,logs,migrations,nodes,snapshots,temp,txpool,LOCK,nodekey}
./build/bin/erigon init --datadir=/home/erigon/bal-devnet-3/node-a /home/erigon/bal-devnet-3/genesis.json
Start with USE_STATE_CACHE=false and bal-devnet-3 bootnodes. Drive via Lighthouse checkpoint-sync from https://checkpoint-sync.bal-devnet-3.ethpandaops.io. Block 503 fails within ~30 s of CL sync engaging. (Setup auto-bootstrapped via .claude/skills/launch-bal-devnet-3/.)
To re-enable the diagnostic per-tx breakdown: add totalSpillGas uint64 to EVM, increment it at execution/vm/interpreter.go:142 alongside stateGasConsumed, and add a log.Warn(\"[amsterdam] tx gas breakdown\", …) at the end of the Amsterdam branch in execution/protocol/txn_executor.go:621. Diagnostic-only, not a fix.
Related
Summary
On bal-devnet-3, erigon's parallel executor fails block 503 with an EIP-8037 state-gas accounting mismatch:
Diff: −375,488 of state-gas. Reproduces deterministically on
feat/eip-8159-eth71-bal-exchangeafter the CPSB formula fix (72f2c8e922), on a freshly initialised datadir.Block details
0xa9c9b682e5573f24662e6c2145c3fedcdac55baa1590803a82cad0e93b34fa2bgasLimit = 80_000_000, canonicalgasUsed = 6_641_318.0x600b3803…, deploys0x4195b465…e8d3. Canonical receiptgasUsed = 229_533; we produce143_005(diff −86_528).0xc481aaf8…). Canonical receiptgasUsed = 7_120_000(OOG); we match 7_120_000.Canonical
block.gasUsedismax(Σ blockRegular, Σ blockState)per execution/protocol/state_processor.go:44; state is the larger term here (difference is the EIP-8037 state-gas portion).Two distinct discrepancies, both in state-gas accounting
A. Block-level: Σ blockState is 375,488 too low
Per-tx breakdown from an exec3 run with a diagnostic counter (added to
useMdGas, not committed):Aggregates — Σ blockState = 6_265_830 (we produce); 6_641_318 (canonical) → −375,488.
tx1.totalSpill == tx1.stateConsumed == 6_153_952. The reservoir is effectively 0 for every sub-cap tx becauseSplitTxnGasLimitreturns reservoir=0 wheneverexecutionGas ≤ regularGasBudget(true for any tx withgasLimit ≤ MaxTxnGasLimit = 16_777_216), so every state op goes through the spill path.tx1 ends OOG. The OOG-triggering state op calls
useMdGas, findsinitial.State = 0, attempts to spill the full cost frominitial.Regular, and fails. In that failure path (execution/vm/interpreter.go:140-144) we returnok=falseand do not incrementstateGasConsumed— the attempted state cost is lost.Hypothesis (unverified): the attempted state cost of the final failed-spill op equals the 375,488 gap, and canonical counts it toward
block.gasUsedeven though the tx OOGs on it. To confirm, incrementstateGasConsumedon theok=falsebranch and rerun — if the diff becomes exactly zero, hypothesis holds.B. Receipt-level: tx0.txnGasUsed is 86,528 too low
tx0 is successful (
vmerr=nil), so nothing was thrown away in a faileduseMdGas. We still producetxnGasUsed = 143_005where canonical expects229_533.txnGasUsed = initialGas.Total() − gasRemaining.Total() + revertedSpillGas = 300_000 − 156_995 + 0 = 143_005.So
gasRemaining.Total()is 86,528 too high — we return too much unused gas. Given this is a CREATE that ran 68-byte initcode and deployed a near-empty runtime with 3 logs, likely leak sites:Independent of (A) — tx0 does not OOG, so the failed-spill hypothesis does not apply. Same consensus-root category (state-gas charging), different root cause.
Where to look
SplitTxnGasLimit(is reservoir=0 for sub-cap txs intended per spec?)useMdGasincluding spill pathgasCreate,gasCreate2handleFrameRevert(spill accounting on child revert)Cross-references:
ethereum/execution-specs(src/ethereum/forks/amsterdam/vm/) — reference behaviour for failed-spill accountingorigin/feature/lystopad/hoodi-amsterdam-fix— iterated on this area; do not bulk apply, but individual commits worth reading:563e81a36efix Amsterdam REVERT spill accounting — depth-0 REVERT spill handling; formula over-counts for OOG txs as measured78ae4b789cblockRegular via mdGasUsed.Regular − RevertedSpillGas — formula reframe with the same over-count problemReproduction
From an erigon checkout with
feat/eip-8159-eth71-bal-exchange(or any branch carrying72f2c8e922CPSB formula fix):make erigon rm -rf /home/erigon/bal-devnet-3/node-a/{chaindata,caplin,dev-beacon,downloader,logs,migrations,nodes,snapshots,temp,txpool,LOCK,nodekey} ./build/bin/erigon init --datadir=/home/erigon/bal-devnet-3/node-a /home/erigon/bal-devnet-3/genesis.jsonStart with
USE_STATE_CACHE=falseand bal-devnet-3 bootnodes. Drive via Lighthouse checkpoint-sync fromhttps://checkpoint-sync.bal-devnet-3.ethpandaops.io. Block 503 fails within ~30 s of CL sync engaging. (Setup auto-bootstrapped via.claude/skills/launch-bal-devnet-3/.)To re-enable the diagnostic per-tx breakdown: add
totalSpillGas uint64toEVM, increment it at execution/vm/interpreter.go:142 alongsidestateGasConsumed, and add alog.Warn(\"[amsterdam] tx gas breakdown\", …)at the end of the Amsterdam branch in execution/protocol/txn_executor.go:621. Diagnostic-only, not a fix.Related