diff --git a/core/state_transition.go b/core/state_transition.go index 0092eaef51..c01718a5e0 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -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 } diff --git a/core/vm/evm.go b/core/vm/evm.go index e8b18c3287..0d4e8f3402 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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 { diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 65f2315c0f..f087d93322 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -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 } }