core: use source-based refund mechanism

This commit is contained in:
Gary Rong 2026-06-16 19:59:58 +08:00
parent 0b3dffcf00
commit 906488333c
5 changed files with 78 additions and 78 deletions

View file

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

View file

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

View file

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

View file

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

View file

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