core/vm: fix EIP-8037 CALL state gas ordering

Charge new-account state gas BEFORE the 63/64 child gas allocation
rather than after. When state gas spills from an empty reservoir to
regular gas, it must reduce the gas available for callGasTemp.
Otherwise the spillover exceeds the 1/64 remainder after child gas
allocation, causing Underflow → OOG on CALLs that should succeed.

This matches nethermind's implementation which calls
ConsumeNewAccountCreation() before the 63/64 calculation
(EvmInstructions.Call.cs:187-213).

Verified: geth+besu in sync through 159 blocks with spamoor load,
geth+nethermind in sync through 50+ post-Amsterdam blocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
qu0b 2026-03-06 21:23:20 +00:00
parent 51018a536f
commit c33c10cb1b
2 changed files with 26 additions and 2 deletions

View file

@ -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
}

View file

@ -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
}
}