diff --git a/core/gaspool.go b/core/gaspool.go index 3255e34835..7c85e36fde 100644 --- a/core/gaspool.go +++ b/core/gaspool.go @@ -87,11 +87,21 @@ func (gp *GasPool) ReturnGasAmsterdam(txGasLimit, txRegular, txState, receiptGas gp.cumulativeUsed += receiptGasUsed newUsed := max(gp.cumulativeRegular, gp.cumulativeState) blockGasIncrement := newUsed - oldUsed - returned := txGasLimit - blockGasIncrement - if gp.remaining > math.MaxUint64-returned { - return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned) + if blockGasIncrement > txGasLimit { + // State gas via reservoir model can push the dimensional cost above + // the tx gas limit. Consume the excess from the pool. + excess := blockGasIncrement - txGasLimit + if gp.remaining < excess { + return fmt.Errorf("%w: remaining: %d, excess: %d", ErrGasLimitReached, gp.remaining, excess) + } + gp.remaining -= excess + } else { + returned := txGasLimit - blockGasIncrement + if gp.remaining > math.MaxUint64-returned { + return fmt.Errorf("%w: remaining: %d, returned: %d", ErrGasLimitOverflow, gp.remaining, returned) + } + gp.remaining += returned } - gp.remaining += returned return nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index f1224c77e4..e8b18c3287 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -536,7 +536,6 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value * if err != nil { return nil, common.Address{}, gas, err } - // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) @@ -620,7 +619,8 @@ 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)) + (errors.Is(err, ErrMaxCodeSizeExceeded) || errors.Is(err, ErrInvalidCode) || + (err == ErrCodeStoreOutOfGas && contract.Gas.TotalStateGasCharged > savedTotalStateGas)) if !isRevert { if isCodeValidation { contract.Gas.RegularGas = 0 @@ -646,8 +646,13 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b return ret, err } - // Charge code storage gas BEFORE validation checks so that state gas is - // properly accounted for in block gas even if the code is invalid (EIP-7954). + // Check code validity once upfront. + codeErr := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))) + if codeErr == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { + codeErr = ErrInvalidCode + } + + // Charge code storage gas. if !evm.chainRules.IsEIP4762 { createDataGas := GasCosts{RegularGas: uint64(len(ret)) * params.CreateDataGas} if evm.chainRules.IsAmsterdam { @@ -658,6 +663,13 @@ 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 { @@ -668,14 +680,8 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } } - // Check whether the max code size has been exceeded, assign err if the case. - if err := CheckMaxCodeSize(&evm.chainRules, uint64(len(ret))); err != nil { - return ret, err - } - - // Reject code starting with 0xEF if EIP-3541 is enabled. - if len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { - return ret, ErrInvalidCode + if codeErr != nil { + return ret, codeErr } if len(ret) > 0 { diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 815bee9c98..8d720cce72 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -18,7 +18,6 @@ package vm import ( "errors" - "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -596,12 +595,13 @@ func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m if overflow { return GasCosts{}, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { - return GasCosts{}, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) - } - // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow - words := (size + 31) / 32 - stateGas := params.AccountCreationSize * evm.Context.CostPerGasByte * words + // Cap word gas at MaxInitCodeSizeAmsterdam to avoid overflow. + // The actual init code size check happens in create() for graceful failure. + wordSize := min(size, params.MaxInitCodeSizeAmsterdam) + words := (wordSize + 31) / 32 + // Account creation is a fixed state gas cost, not proportional to init code size. + // Code storage state gas is charged separately in initNewContract. + stateGas := params.AccountCreationSize * evm.Context.CostPerGasByte // CREATE uses InitCodeWordGas (EIP-3860); Keccak256WordGas is only for CREATE2. wordGas := params.InitCodeWordGas * words return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil @@ -616,12 +616,13 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, if overflow { return GasCosts{}, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { - return GasCosts{}, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) - } - // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow - words := (size + 31) / 32 - stateGas := params.AccountCreationSize * evm.Context.CostPerGasByte * words + // Cap word gas at MaxInitCodeSizeAmsterdam to avoid overflow. + // The actual init code size check happens in create() for graceful failure. + wordSize := min(size, params.MaxInitCodeSizeAmsterdam) + words := (wordSize + 31) / 32 + // Account creation is a fixed state gas cost, not proportional to init code size. + // Code storage state gas is charged separately in initNewContract. + stateGas := params.AccountCreationSize * evm.Context.CostPerGasByte // CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas (for address hashing). wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index d996735df6..bb7a1b2b32 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -28,13 +28,17 @@ func (g GasCosts) Sum() uint64 { return g.RegularGas + g.StateGas } -// Sub returns true if the operation would underflow +// Underflow returns true if the operation would underflow. +// When state gas exceeds the reservoir, the excess spills to regular gas. +// The check accounts for regular gas already consumed by b.RegularGas. func (g GasCosts) Underflow(b GasCosts) bool { if b.RegularGas > g.RegularGas { return true } if b.StateGas > g.StateGas { - if b.StateGas > g.RegularGas { + spillover := b.StateGas - g.StateGas + remainingRegular := g.RegularGas - b.RegularGas + if spillover > remainingRegular { return true } } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 600f77d25f..0bc126377e 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -17,6 +17,7 @@ package vm import ( + "errors" "math" "github.com/ethereum/go-ethereum/common" @@ -661,6 +662,15 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { gas.RegularGas -= gas.RegularGas / 64 } + // EIP-7954: check init code size after gas is charged (by the gas function) + // but before execution. This aborts the caller's execution, ensuring all + // regular gas is consumed while the state gas spill is tracked. + if evm.chainRules.IsAmsterdam { + if err := CheckMaxInitCodeSize(&evm.chainRules, uint64(len(input))); err != nil { + return nil, err + } + } + // reuse size int for stackvalue stackvalue := size @@ -684,6 +694,15 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) + // For inner CREATEs that fail code validation (EIP-7954 + EIP-8037): + // the state was reverted so no state growth occurred. Don't propagate + // the code storage state gas to the parent's block gas accounting. + // Restore the parent's state gas reservoir since it was not consumed. + if evm.chainRules.IsAmsterdam && (errors.Is(suberr, ErrMaxCodeSizeExceeded) || errors.Is(suberr, ErrInvalidCode)) { + returnGas.TotalStateGasCharged = 0 + returnGas.RevertedStateGasSpill = 0 + returnGas.StateGas = stateGas + } scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { @@ -708,6 +727,16 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Apply EIP150 gas.RegularGas -= gas.RegularGas / 64 + + // EIP-7954: check init code size after gas is charged (by the gas function) + // but before execution. This aborts the caller's execution, ensuring all + // regular gas is consumed while the state gas spill is tracked. + if evm.chainRules.IsAmsterdam { + if err := CheckMaxInitCodeSize(&evm.chainRules, uint64(len(input))); err != nil { + return nil, err + } + } + // Pass caller's state gas (reservoir) to child and zero it out to avoid // double-counting when the unused portion is refunded on return. stateGas := scope.Contract.Gas.StateGas @@ -724,6 +753,16 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) + + // For inner CREATEs that fail code validation (EIP-7954 + EIP-8037): + // the state was reverted so no state growth occurred. Don't propagate + // the code storage state gas to the parent's block gas accounting. + // Restore the parent's state gas reservoir since it was not consumed. + if evm.chainRules.IsAmsterdam && (errors.Is(suberr, ErrMaxCodeSizeExceeded) || errors.Is(suberr, ErrInvalidCode)) { + returnGas.TotalStateGasCharged = 0 + returnGas.RevertedStateGasSpill = 0 + returnGas.StateGas = stateGas + } scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted {