diff --git a/core/state_transition.go b/core/state_transition.go index 0443e8b655..0f22a48a2c 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -322,7 +322,7 @@ func (st *stateTransition) to() common.Address { return *st.msg.To } -func (st *stateTransition) buyGas() error { +func (st *stateTransition) buyGas() (uint64, error) { mgval := new(big.Int).SetUint64(st.msg.GasLimit) mgval.Mul(mgval, st.msg.GasPrice) balanceCheck := new(big.Int).Set(mgval) @@ -346,13 +346,10 @@ func (st *stateTransition) buyGas() error { } balanceCheckU256, overflow := uint256.FromBig(balanceCheck) if overflow { - return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) + return 0, fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) } if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { - return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) - } - if err := st.gp.SubGas(st.msg.GasLimit); err != nil { - return err + return 0, fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) } if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil { @@ -368,23 +365,23 @@ func (st *stateTransition) buyGas() error { st.gasRemaining = st.initialBudget.Copy() mgvalU256, _ := uint256.FromBig(mgval) st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) - return nil + return st.msg.GasLimit, nil } -func (st *stateTransition) preCheck() error { +func (st *stateTransition) preCheck() (uint64, error) { // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { // Make sure this transaction's nonce is correct. stNonce := st.state.GetNonce(msg.From) if msgNonce := msg.Nonce; stNonce < msgNonce { - return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooHigh, + return 0, fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooHigh, msg.From.Hex(), msgNonce, stNonce) } else if stNonce > msgNonce { - return fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooLow, + return 0, fmt.Errorf("%w: address %v, tx: %d state: %d", ErrNonceTooLow, msg.From.Hex(), msgNonce, stNonce) } else if stNonce+1 < stNonce { - return fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax, + return 0, fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax, msg.From.Hex(), stNonce) } } @@ -393,13 +390,13 @@ func (st *stateTransition) preCheck() error { if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. if !isAmsterdam && isOsaka && msg.GasLimit > params.MaxTxGas { - return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) + return 0, fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) } // Make sure the sender is an EOA code := st.state.GetCode(msg.From) _, delegated := types.ParseDelegation(code) if len(code) > 0 && !delegated { - return fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, msg.From.Hex(), len(code)) + return 0, fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, msg.From.Hex(), len(code)) } } // Make sure that transaction gasFeeCap is greater than the baseFee (post london) @@ -408,21 +405,21 @@ func (st *stateTransition) preCheck() error { skipCheck := st.evm.Config.NoBaseFee && msg.GasFeeCap.BitLen() == 0 && msg.GasTipCap.BitLen() == 0 if !skipCheck { if l := msg.GasFeeCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, + return 0, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", ErrFeeCapVeryHigh, msg.From.Hex(), l) } if l := msg.GasTipCap.BitLen(); l > 256 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, + return 0, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh, msg.From.Hex(), l) } if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 { - return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, + return 0, fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap, msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap) } // This will panic if baseFee is nil, but basefee presence is verified // as part of header validation. if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 { - return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, + return 0, fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow, msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee) } } @@ -433,17 +430,17 @@ func (st *stateTransition) preCheck() error { // has it as a non-nillable value, so any msg derived from blob transaction has it non-nil. // However, messages created through RPC (eth_call) don't have this restriction. if msg.To == nil { - return ErrBlobTxCreate + return 0, ErrBlobTxCreate } if len(msg.BlobHashes) == 0 { - return ErrMissingBlobHashes + return 0, ErrMissingBlobHashes } if isOsaka && len(msg.BlobHashes) > params.BlobTxMaxBlobs { - return ErrTooManyBlobs + return 0, ErrTooManyBlobs } for i, hash := range msg.BlobHashes { if !kzg4844.IsValidVersionedHash(hash[:]) { - return fmt.Errorf("blob %d has invalid hash version", i) + return 0, fmt.Errorf("blob %d has invalid hash version", i) } } } @@ -456,7 +453,7 @@ func (st *stateTransition) preCheck() error { // This will panic if blobBaseFee is nil, but blobBaseFee presence // is verified as part of header validation. if msg.BlobGasFeeCap.Cmp(st.evm.Context.BlobBaseFee) < 0 { - return fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, + return 0, fmt.Errorf("%w: address %v blobGasFeeCap: %v, blobBaseFee: %v", ErrBlobFeeCapTooLow, msg.From.Hex(), msg.BlobGasFeeCap, st.evm.Context.BlobBaseFee) } } @@ -465,10 +462,10 @@ func (st *stateTransition) preCheck() error { // Check that EIP-7702 authorization list signatures are well formed. if msg.SetCodeAuthorizations != nil { if msg.To == nil { - return fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From) + return 0, fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From) } if len(msg.SetCodeAuthorizations) == 0 { - return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) + return 0, fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } return st.buyGas() @@ -496,7 +493,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // 6. caller has enough balance to cover asset transfer for **topmost** call // Check clauses 1-3, buy gas if everything is correct - if err := st.preCheck(); err != nil { + gas, err := st.preCheck() + if err != nil { return nil, err } @@ -506,12 +504,33 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { contractCreation = msg.To == nil floorDataGas uint64 ) + + if !rules.IsAmsterdam { + if err := st.gp.SubGas(gas); err != nil { + return nil, err + } + } + // Check clauses 4-5, subtract intrinsic gas if everything is correct cost, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules, st.evm.Context.CostPerGasByte) if err != nil { return nil, err } + // Regular gas check for block inclusion post-amsterdam includes state gas. + if rules.IsAmsterdam { + subGasAmount := msg.GasLimit + if subGasAmount > cost.StateGas { + subGasAmount -= cost.StateGas + } else { + subGasAmount = 0 + } + subGasAmount = min(subGasAmount, params.MaxTxGas) + if err := st.gp.SubGas(subGasAmount); err != nil { + return nil, err + } + } + // Compute the floor data cost (EIP-7623), needed for both Prague and Amsterdam validation. if rules.IsPrague { floorDataGas, err = FloorDataGas(rules, msg.Data, msg.AccessList) diff --git a/core/vm/contract.go b/core/vm/contract.go index d2d9bb3087..3e7ba47bd5 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -141,22 +141,21 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G // RefundGas refunds gas to the contract. func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBudget, gasUsed GasUsed, logger *tracing.Hooks, reason tracing.GasChangeReason) { - // If the preceding call errored, return the child's state-gas reservoir - // and any net state-gas consumption to the parent. A negative - // gasUsed.StateGas (an unabsorbed SSTORE 0→x→0 refund) is an inflation - // to undo: the matching state mutation was reverted with the child, so - // the refund must not leak back to the parent. if err != nil { - if gasUsed.StateGas >= 0 { + if gasUsed.StateGas > 0 { gas.StateGas += uint64(gasUsed.StateGas) - } else if undo := uint64(-gasUsed.StateGas); gas.StateGas >= undo { - gas.StateGas -= undo - } else { - gas.StateGas = 0 } gasUsed.StateGas = 0 + if gas.StateGasRefund > 0 { + if gas.StateGas >= gas.StateGasRefund { + gas.StateGas -= gas.StateGasRefund + } else { + gas.StateGas = 0 + } + gas.StateGasRefund = 0 + } } - if gas.RegularGas == 0 && gas.StateGas == 0 && gasUsed.StateGas == 0 && gasUsed.RegularGas == 0 { + if gas.RegularGas == 0 && gas.StateGas == 0 && gasUsed.StateGas == 0 && gasUsed.RegularGas == 0 && gas.StateGasRefund == 0 { return } if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { @@ -164,6 +163,9 @@ func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBud } c.Gas.RegularGas += gas.RegularGas c.Gas.StateGas = gas.StateGas + // Propagate the child's applied inline refund so a later ancestor + // error can still undo the reservoir inflation. + c.Gas.StateGasRefund += gas.StateGasRefund c.GasUsed.StateGas += gasUsed.StateGas c.GasUsed.RegularGas = initialRegularGasUsed + gasUsed.RegularGas } @@ -172,6 +174,7 @@ func (c *Contract) RefundGas(err error, initialRegularGasUsed uint64, gas GasBud func (c *Contract) RefundCreateStateGas(refund uint64) { if refund > 0 { c.Gas.StateGas += refund + c.Gas.StateGasRefund += refund c.GasUsed.StateGas -= int64(refund) } } diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index ed2e4d4494..1905011381 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -699,9 +699,11 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1) // EIP-8037 point (2): refund state gas directly to the reservoir // at the SSTORE restoration point (0→x→0 in same tx); not to the - // refund counter, which is capped at gas_used/5. + // refund counter, which is capped at gas_used/5. StateGasRefund + // tracks the reservoir inflation so a frame error undoes it. stateRefund := params.StorageCreationSize * evm.Context.CostPerGasByte contract.Gas.StateGas += stateRefund + contract.Gas.StateGasRefund += stateRefund contract.GasUsed.StateGas -= int64(stateRefund) // Regular portion of the refund still goes through the refund counter. evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index b35e161680..7dc384f3b7 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -19,7 +19,7 @@ package vm import "fmt" // GasUsed is the per-frame accumulator for gas consumption. -// StateGas is signed because of 0 -> X -> 0 SSTORE refunds. +// StateGas is signed, because it can be negative in a 0 -> x -> 0 scenario. type GasUsed struct { RegularGas uint64 StateGas int64 @@ -55,6 +55,10 @@ func (g GasCosts) String() string { type GasBudget struct { RegularGas uint64 // The leftover gas for execution and state gas usage StateGas uint64 // The state gas reservoir + + // Tracks the gas refunds in this call frame. Needed so we can + // revert the refunds if the call frame reverts. + StateGasRefund uint64 } // NewGasBudgetReg creates a GasBudget with the given initial regular gas allowance.