diff --git a/core/state_transition.go b/core/state_transition.go index 12abcde071..bbeb163b16 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -674,11 +674,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ret []byte vmerr error // vm errors do not effect consensus and are therefore not assigned to err result vm.GasBudget - - // Capture the forwarded regular-gas amount BEFORE ForwardAll consumes - // it, so Absorb can back out state-gas spillover from UsedRegularGas - // per EIP-8037. - forwarded = st.gasRemaining.RegularGas ) if contractCreation { // Check whether the init code size has been exceeded. @@ -688,7 +683,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the transaction's creation. var creation bool ret, _, result, creation, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result, forwarded) + st.gasRemaining.Absorb(result) // If the contract creation failed, or the destination was pre-existing, // refund the account-creation state gas pre-charged in IntrinsicGas. @@ -712,7 +707,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's call. ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value) - st.gasRemaining.Absorb(result, forwarded) + st.gasRemaining.Absorb(result) } // Settle down the gas usage and refund the ETH back if any remaining diff --git a/core/vm/contract.go b/core/vm/contract.go index 7ee125470c..45dad42be0 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -163,9 +163,9 @@ func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.G } // refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state. -func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { +func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) { prior := c.Gas - c.Gas.Absorb(child, forwarded) + c.Gas.Absorb(child) if logger.HasGasHook() && reason != tracing.GasChangeIgnored { logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason) } diff --git a/core/vm/evm.go b/core/vm/evm.go index 17e3e8553c..15609a0205 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -265,7 +265,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { @@ -279,7 +279,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if _, ok := gas.ChargeRegular(wgas); !ok { evm.StateDB.RevertToSnapshot(snapshot) - return nil, gas.ExitHalt(reservoir), ErrOutOfGas + return nil, gas.ExitHalt(), ErrOutOfGas } } @@ -314,7 +314,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -350,7 +350,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -365,7 +365,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -396,7 +396,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -409,7 +409,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -443,7 +443,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // after all empty accounts were deleted, so this is not required. However, if we omit this, // then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json. // We could change this, but for now it's left for legacy reasons - snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas + snapshot := evm.StateDB.Snapshot() // We do an AddBalance of zero here, just in order to trigger a touch. // This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium, @@ -461,7 +461,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } // Calculate the remaining gas at the end of frame - exitGas := gas.Exit(err, reservoir) + exitGas := gas.Exit(err) if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { @@ -499,14 +499,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value } // Increment the caller's nonce after passing all validations evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) - reservoir := gas.StateGas // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas}) if !ok { - return nil, common.Address{}, gas.ExitHalt(reservoir), false, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas } if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck) @@ -527,7 +526,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) { - halt := gas.ExitHalt(reservoir) + halt := gas.ExitHalt() if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution) } @@ -556,7 +555,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - return nil, common.Address{}, gas.ExitHalt(reservoir), false, ErrOutOfGas + return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas } prior, _ := gas.Charge(GasCosts{RegularGas: consumed}) if evm.Config.Tracer.HasGasHook() { @@ -581,7 +580,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) - exit := contract.Gas.Exit(err, reservoir) + exit := contract.Gas.Exit(err) if err != ErrExecutionReverted { if evm.Config.Tracer.HasGasHook() { evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution) diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index b1756ab5fe..0364780576 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -65,6 +65,11 @@ type GasBudget struct { StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb) UsedRegularGas uint64 // gross regular gas consumed in this frame UsedStateGas int64 // signed net state-gas consumed in this frame + + // StateGasFromGasLeft tracks how much of this frame's regular gas (gas_left) + // has been borrowed to cover state-gas charges that exceeded the reservoir + // (the "spillover"). + StateGasFromGasLeft uint64 } // NewGasBudget initializes a fresh GasBudget for execution / forwarding, @@ -82,7 +87,7 @@ func (g GasBudget) Used(initial GasBudget) uint64 { // String returns a visual representation of the budget. func (g GasBudget) String() string { - return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas) + return fmt.Sprintf("<%v,%v,used=<%v,%v>,borrowed=%v>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas, g.StateGasFromGasLeft) } // CanAfford reports whether the running balance can cover the given cost. @@ -117,6 +122,10 @@ func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { spillover := cost.StateGas - g.StateGas g.StateGas = 0 g.RegularGas -= spillover + + // Record the regular gas borrowed to cover the overflowing state gas + // so a later inline refund can repay it before topping up the reservoir. + g.StateGasFromGasLeft += spillover } else { g.StateGas -= cost.StateGas } @@ -146,14 +155,27 @@ func (g *GasBudget) IsZero() bool { } // RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0). -// The reservoir is credited and the signed usage counter is decremented -// in lockstep, preserving the per-frame invariant: // -// StateGas + UsedStateGas == initialStateGas + spillover_so_far +// Per EIP-8037, the refund repays the regular gas previously borrowed for +// state-gas spillover (tracked by StateGasFromGasLeft) before crediting the +// reservoir: it is returned to RegularGas up to the outstanding borrowed +// amount, and only the remainder tops up StateGas. This keeps the regular and +// state pools from drifting into one another. // -// which the revert path relies on for the correct gross refund. +// The signed usage counter is decremented by the full refund regardless of the +// split, preserving the per-frame invariant: +// +// StateGas + UsedStateGas == initialStateGas + StateGasFromGasLeft +// +// which the revert and halt paths rely on for the correct gross refund. func (g *GasBudget) RefundState(s uint64) { - g.StateGas += s + // Repay the borrowed regular gas first, capped at the outstanding amount. + repay := min(s, g.StateGasFromGasLeft) + g.RegularGas += repay + g.StateGasFromGasLeft -= repay + + // Whatever is left tops up the reservoir. + g.StateGas += s - repay g.UsedStateGas -= int64(s) } @@ -201,53 +223,46 @@ func (g GasBudget) ExitSuccess() GasBudget { return g } -// ExitRevert produces the leftover for a REVERT exit. Per EIP-8037, all state -// gas charged by the reverted frame is refunded to the caller's reservoir: -// -// leftover.StateGas = StateGas + UsedStateGas -// -// UsedStateGas is reset since the frame's state changes are discarded. +// ExitRevert produces the leftover for a REVERT exit. The frame's state +// changes are discarded, so all state gas it charged is refilled to its origin +// (EIP-8037): up to StateGasFromGasLeft is returned to RegularGas (the regular +// gas it borrowed), and the remainder restores the reservoir. Because the +// borrowed regular gas is repaid first, the reservoir is made whole back to its +// start-of-frame value: func (g GasBudget) ExitRevert() GasBudget { - reservoir := int64(g.StateGas) + g.UsedStateGas + reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.StateGasFromGasLeft) if reservoir < 0 { // Reservoir should never be negative. By construction it equals - // the initial state-gas allocation plus any spillover to regular - // gas. + // the initial state-gas allocation. reservoir = 0 - log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas) + log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.StateGasFromGasLeft) } return GasBudget{ - RegularGas: g.RegularGas, + RegularGas: g.RegularGas + g.StateGasFromGasLeft, StateGas: uint64(reservoir), UsedRegularGas: g.UsedRegularGas, UsedStateGas: 0, } } -// ExitHalt produces the leftover for an exceptional halt. -// -// - state_gas_reservoir is reset back to its value at the start of the child frame -// - the gas_left initially given to the child is consumed (set to zero) -func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { - reservoir := int64(g.StateGas) + g.UsedStateGas +// ExitHalt produces the leftover for an exceptional halt. As with a revert, the +// frame's state changes are rolled back and its state gas is refilled to origin +// (EIP-8037); the difference is that the frame's gas_left is consumed rather +// than returned. The portion refilled to RegularGas is therefore burned along +// with the rest of gas_left, leaving only the reservoir portion to survive, +// which equals the reservoir's value at the start of the frame. +func (g GasBudget) ExitHalt() GasBudget { + reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.StateGasFromGasLeft) if reservoir < 0 { // Reservoir should never be negative. By construction it equals - // the initial state-gas allocation plus any spillover to regular - // gas. + // the initial state-gas allocation. reservoir = 0 - log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas) - } - // The portion of state gas charged from regular gas is also burned - // together with the regular gas, rather than being returned to the - // parent's state-gas reservoir. - var spilled uint64 - if uint64(reservoir) > initStateReservoir { - spilled = uint64(reservoir) - initStateReservoir + log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.StateGasFromGasLeft) } return GasBudget{ RegularGas: 0, - StateGas: initStateReservoir, - UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled, + StateGas: uint64(reservoir), + UsedRegularGas: g.UsedRegularGas + g.RegularGas + g.StateGasFromGasLeft, UsedStateGas: 0, } } @@ -258,17 +273,14 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget { // - err == nil → ExitSuccess // - err == ErrExecutionReverted → ExitRevert // - any other err → ExitHalt -// -// Soft validation failures (occurring BEFORE evm.Run) should call Preserved -// directly instead of going through this dispatcher. -func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget { +func (g GasBudget) Exit(err error) GasBudget { switch { case err == nil: return g.ExitSuccess() case err == ErrExecutionReverted: return g.ExitRevert() default: - return g.ExitHalt(initStateReservoir) + return g.ExitHalt() } } @@ -276,18 +288,12 @@ func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget { // budget. Additionally, it does an EIP-8037 spillover correction: // state-gas that spilled into the regular pool inside the child frame is // excluded from the UsedRegularGas. -// -// spillover = forwarded - child.RegularGas - child.UsedRegularGas -// -// forwarded is the regular-gas amount that was passed to the child at call -// entry (i.e., the regular initial of the child's GasBudget). -func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) { - spillover := forwarded - child.RegularGas - child.UsedRegularGas - +func (g *GasBudget) Absorb(child GasBudget) { g.UsedRegularGas -= child.RegularGas g.RegularGas += child.RegularGas g.StateGas = child.StateGas g.UsedStateGas += child.UsedStateGas - g.UsedRegularGas -= spillover + g.UsedRegularGas -= child.StateGasFromGasLeft + g.StateGasFromGasLeft += child.StateGasFromGasLeft } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 3088134e78..84d34e5aad 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -677,7 +677,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) // Refund the leftover gas back to current frame - scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) // Refund the state gas of account-creation if creation doesn't happen if evm.GetRules().IsAmsterdam && !creation { @@ -715,7 +715,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Stack.push(&stackvalue) // Refund the leftover gas back to current frame - scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) // Refund the state gas of account-creation if creation doesn't happen if evm.GetRules().IsAmsterdam && !creation { @@ -764,7 +764,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) // If the call frame reverts or halts exceptionally, the charged state-gas // is refilled back to the state reservoir in Amsterdam. @@ -805,7 +805,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -837,7 +837,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if err == nil || err == ErrExecutionReverted { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -870,7 +870,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil