mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-03-09 20:59:02 +00:00
core/vm: fix EIP-8037 CALL state gas ordering + code-store OOG underflow (#33972)
## Summary Two EIP-8037 state gas accounting fixes: 1. **CALL state gas ordering** — Charge new-account state gas (112 × cpsb = 131,488) **before** the 63/64 child gas allocation, not after 2. **Code-store OOG underflow** — Do not inflate `TotalStateGasCharged` when `UseGas` fails for code deposit, preventing uint64 underflow in `blockGasUsed()` ## Bug 1: CALL state gas ordering (`operations_acl.go`) In `makeCallVariantGasCall`, the EIP-8037 state gas for new account creation was returned in the `GasCosts` struct and charged by the interpreter's `UseGas`/`Sub` **after** `callGas()` had already computed the 63/64 child gas allocation using the full (pre-state-gas) `contract.Gas.RegularGas`. When the state gas reservoir is empty (common case — reservoir only has gas when `tx.gasLimit` exceeds `TX_MAX_GAS_LIMIT - intrinsic`), state gas spills to regular gas. The spill amount (131,488) far exceeds the 1/64 retained gas (~15,600 at 1M gas), causing an underflow/OOG on CALLs that should succeed. **Fix:** Charge state gas directly before `callGas()` so the 63/64 calculation uses the reduced regular gas, then return `StateGas: 0` to avoid double-charging. This matches nethermind's implementation (`EvmInstructions.Call.cs:187-213`) and besu's approach. **Spec basis:** EIP-8037 says "State gas charges deduct from `state_gas_reservoir` first; when the reservoir is exhausted, from `gas_left`." The 63/64 rule applies to `gas_left`, so state gas spillover into `gas_left` must happen before the 63/64 computation. ## Bug 2: Code-store OOG underflow (`evm.go`) In `initNewContract`, when `UseGas` fails for code deposit (code-store OOG on valid code), the upstream code at `evm.go:666-672` added `createDataGas.StateGas` to `TotalStateGasCharged` without actually consuming any gas. For a 14KB init code output, this adds ~17.3M phantom state gas to TSC. This inflated TSC propagates through `blockGasUsed()`: ``` execRegularUsed := totalExecUsed - execStateUsed // uint64 underflow when TSC > totalExecUsed ``` The underflow produces a massive `txRegular` value, causing `ReturnGasAmsterdam` to reject the block with `gas limit reached`. **Fix:** Remove the TSC inflation on `UseGas` failure (lines 666-672). Also remove `ErrCodeStoreOutOfGas` from the `isCodeValidation` condition (line 621-623), so code-store OOG follows the normal exceptional halt path: all regular gas consumed, state gas reverted via `RevertStateGas()`. **Spec basis:** EIP-8037 §"Contract deployment cost calculation" explicitly lists code-store OOG as a failure path: > **Failure paths** (REVERT, OOG/invalid during initcode, **OOG during code deposit**, or `L > MAX_CODE_SIZE`): Do NOT charge `GAS_CODE_DEPOSIT * L` or `HASH_COST(L)` And §"Transaction-level gas accounting": > On child **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. ## Test plan - [x] geth+besu: 161 blocks (5+ epochs) with heavy spamoor load (eoatx 50 + evm-fuzz 25 + tx-fuzz 15), zero errors - [x] Without Bug 2 fix: geth+besu chain-split at block ~30 under load — `blockGasUsed` uint64 underflow - [x] geth+nethermind: 86+ blocks no-load, 162 blocks eoatx load (nethermind has separate BAL validation bug under fuzz) - [x] geth+nimbus: 150+ blocks evm-fuzz with nimbus CALL fix applied — gasUsed matches - [x] Verified cross-client: nethermind and besu both charge CALL state gas before 63/64 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51018a536f
commit
6489aab697
3 changed files with 27 additions and 11 deletions
|
|
@ -783,11 +783,13 @@ func (st *stateTransition) blockGasUsed(intrinsicGas, execGasStart vm.GasCosts)
|
|||
execStateUsed := st.gasRemaining.TotalStateGasCharged
|
||||
// Exclude state gas that was charged from regular gas but then reverted.
|
||||
// This gas was consumed from the regular pool but was for state operations
|
||||
// that didn't persist, so it shouldn't count in the regular dimension.
|
||||
// that didn't persist, so it shouldn't count in either dimension for block
|
||||
// accounting (invisible to the block). This matches nethermind's approach.
|
||||
execRegularUsed := totalExecUsed - execStateUsed - st.gasRemaining.RevertedStateGasSpill
|
||||
|
||||
txRegular := intrinsicGas.RegularGas + execRegularUsed
|
||||
txState := intrinsicGas.StateGas + execStateUsed - st.stateGasRefund
|
||||
|
||||
return txRegular, txState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -619,8 +619,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value *
|
|||
// State gas charges must persist for block 2D gas accounting even
|
||||
// though the state is reverted. Only zero regular gas as penalty.
|
||||
isCodeValidation := evm.chainRules.IsAmsterdam &&
|
||||
(errors.Is(err, ErrMaxCodeSizeExceeded) || errors.Is(err, ErrInvalidCode) ||
|
||||
(err == ErrCodeStoreOutOfGas && contract.Gas.TotalStateGasCharged > savedTotalStateGas))
|
||||
(errors.Is(err, ErrMaxCodeSizeExceeded) || errors.Is(err, ErrInvalidCode))
|
||||
if !isRevert {
|
||||
if isCodeValidation {
|
||||
contract.Gas.RegularGas = 0
|
||||
|
|
@ -663,13 +662,6 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
|
|||
}
|
||||
}
|
||||
if !contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) {
|
||||
if evm.chainRules.IsAmsterdam && codeErr == nil {
|
||||
// Valid code that merely ran out of gas: track state gas for
|
||||
// block accounting (EIP-8037).
|
||||
contract.Gas.TotalStateGasCharged += createDataGas.StateGas
|
||||
contract.Gas.RegularGas = 0
|
||||
contract.Gas.StateGas = 0
|
||||
}
|
||||
return ret, ErrCodeStoreOutOfGas
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -295,6 +295,21 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun
|
|||
}
|
||||
}
|
||||
|
||||
// EIP-8037: Charge state gas for new account creation BEFORE the 63/64
|
||||
// child gas allocation. State gas that spills from an empty reservoir to
|
||||
// regular gas must reduce the gas available for callGasTemp, otherwise
|
||||
// the Underflow check in UseGas will fail when the spillover exceeds the
|
||||
// tiny 1/64 remainder after child gas allocation.
|
||||
var stateGasCharged uint64
|
||||
if evm.chainRules.IsAmsterdam && oldStateful.StateGas > 0 {
|
||||
stateGasCharged = oldStateful.StateGas
|
||||
stateGasCost := GasCosts{StateGas: stateGasCharged}
|
||||
if contract.Gas.Underflow(stateGasCost) {
|
||||
return GasCosts{}, ErrOutOfGas
|
||||
}
|
||||
contract.Gas.Sub(stateGasCost)
|
||||
}
|
||||
|
||||
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, eip150BaseGas.RegularGas, stack.Back(0))
|
||||
if err != nil {
|
||||
return GasCosts{}, err
|
||||
|
|
@ -331,6 +346,13 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun
|
|||
return GasCosts{}, ErrGasUintOverflow
|
||||
}
|
||||
|
||||
return GasCosts{RegularGas: totalCost, StateGas: oldStateful.StateGas}, nil
|
||||
// If state gas was already charged directly (Amsterdam), don't include
|
||||
// it in the returned cost — it would be double-charged by the
|
||||
// interpreter's UseGas/Sub which increments TotalStateGasCharged again.
|
||||
returnedStateGas := oldStateful.StateGas
|
||||
if stateGasCharged > 0 {
|
||||
returnedStateGas = 0
|
||||
}
|
||||
return GasCosts{RegularGas: totalCost, StateGas: returnedStateGas}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue