From 87a4f9c67454d6073e6f9d6c8e830299cd10f33b Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Wed, 29 Apr 2026 01:50:27 +0200 Subject: [PATCH] core: misc fixes (claude) --- core/state/journal.go | 37 +++++++++---- core/state_transition.go | 71 ++++++++++++++++++------- core/vm/eips.go | 6 +++ core/vm/evm.go | 109 +++++++++++++++++++++++++++++++++----- core/vm/gas_table.go | 36 ++++++++----- core/vm/gascosts.go | 28 ++++++++-- core/vm/instructions.go | 46 +++++++++++++--- core/vm/interpreter.go | 2 + core/vm/operations_acl.go | 17 +++++- 9 files changed, 287 insertions(+), 65 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index 903deacc79..b3f97655dd 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -19,6 +19,7 @@ package state import ( "fmt" "maps" + "os" "slices" "sort" @@ -251,7 +252,7 @@ func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]* var subcallBytes int64 visit := func(e journalEntry) { switch e := e.(type) { - case createContractChange: + case createObjectChange: created[e.account] = true case codeChange: codeChanged[e.account] = true @@ -262,19 +263,27 @@ func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]* } } } - pos := rev.journalIndex - for _, child := range rev.closedChildren { - for ; pos < child.start; pos++ { + if excludeSubcalls { + // Walk only this frame's own entries, skipping closed child ranges. + pos := rev.journalIndex + for _, child := range rev.closedChildren { + for ; pos < child.start; pos++ { + visit(j.entries[pos]) + } + pos = child.end + } + for ; pos < len(j.entries); pos++ { visit(j.entries[pos]) } - if !excludeSubcalls { - // Add the cached cost for this subcall. - subcallBytes += j.stateBytesCharged[child.start] + } else { + // Walk all entries (including subcall ranges) to compute the full + // diff for this frame. The caller is responsible for subtracting + // `already_paid` (= state_gas_used so far) before charging the + // difference, matching the spec's `this_call_cost = growth_cost - + // already_paid` formula. + for pos := rev.journalIndex; pos < len(j.entries); pos++ { + visit(j.entries[pos]) } - pos = child.end - } - for ; pos < len(j.entries); pos++ { - visit(j.entries[pos]) } var totalBytes int64 @@ -319,6 +328,12 @@ func (j *journal) stateChangedBytes(revid int, stateObjects map[common.Address]* // Cache so the parent can look up this frame's total cost. j.stateBytesCharged[rev.journalIndex] = totalBytes + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " scb(rev=%d,idx=%d,!s=%v): cre=%d sl=%d cc=%d sub=%d tot=%d cls=%v\n", + revid, rev.journalIndex, excludeSubcalls, + len(created), len(slots), len(codeChanged), + subcallBytes, totalBytes, rev.closedChildren) + } return totalBytes } diff --git a/core/state_transition.go b/core/state_transition.go index e9b5b1f83f..a03be12b00 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -21,6 +21,7 @@ import ( "fmt" "math" "math/big" + "os" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" @@ -557,6 +558,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if !sufficient { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas) } + if rules.IsAmsterdam { + // EIP-8037: intrinsic state gas is deducted from the reservoir but + // does NOT count toward StateGasUsed (it is added back later via + // tx_state_gas = intrinsic_state + state_gas_used). Reset only the + // state-gas tracker; regular intrinsic stays accounted. + st.gasRemaining.StateGasUsed -= cost.StateGas + } if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { if rules.IsAmsterdam { t.OnGasChange(msg.GasLimit, st.gasRemaining.RegularGas+st.gasRemaining.StateGas, tracing.GasChangeTxIntrinsicGas) @@ -599,23 +607,31 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) - // Take a snapshot for gas calculation - outerSnapshot := st.state.Snapshot() - - var execGasUsed vm.GasUsed - if contractCreation { - ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) - } else { + if !contractCreation { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) - // Apply EIP-7702 authorizations. + // Apply EIP-7702 authorizations BEFORE the outer state-gas snapshot + // so that the authorization code-write does not appear in the diff + // (it's already paid for by the per-auth intrinsic state gas). if msg.SetCodeAuthorizations != nil { for _, auth := range msg.SetCodeAuthorizations { // Note errors are ignored, we simply skip invalid authorizations here. st.applyAuthorization(rules, &auth) } } + } + + // Take a snapshot for gas calculation. For CREATE txs, the account + // creation in evm.Create() will be inside this snapshot and bubble up + // via cached child frames; we subtract the intrinsic-covered portion + // at frame end. For CALL txs, auths have already been applied so their + // code changes are not in the diff. + outerSnapshot := st.state.Snapshot() + + if contractCreation { + ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) + } else { // Perform convenience warming of sender's delegation target. Although the // sender is already warmed in Prepare(..), it's possible a delegation to @@ -636,12 +652,23 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { outerBytes := st.state.StateChangedBytes(outerSnapshot, false) // Refund state gas for selfdestructed accounts. outerBytes -= st.state.SelfDestructRefundBytes() - st.gasRemaining.Charge(vm.GasCosts{StateGas: outerBytes * int64(st.evm.Context.CostPerStateByte)}) - } else { - if execGasUsed.StateGas > 0 { - st.gasRemaining.StateGas += uint64(execGasUsed.StateGas) + // For contract-creation txs the intrinsic already paid for the + // account creation; subtract it so we don't double-charge. + if contractCreation { + outerBytes -= int64(params.AccountCreationSize) } - execGasUsed.StateGas = 0 + // EIP-8037 spec: this_call_cost = growth_cost - already_paid. + alreadyPaid := st.gasRemaining.StateGasUsed + thisCallCost := outerBytes*int64(st.evm.Context.CostPerStateByte) - alreadyPaid + st.gasRemaining.Charge(vm.GasCosts{StateGas: thisCallCost}) + } else { + // On top-level error, restore state-gas reservoir and reset + // the state-gas-used counter; state changes are reverted, no + // state was grown (matches execution-specs fork.py:1055). + if st.gasRemaining.StateGasUsed > 0 { + st.gasRemaining.StateGas += uint64(st.gasRemaining.StateGasUsed) + } + st.gasRemaining.StateGasUsed = 0 } } @@ -672,12 +699,20 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { returned := st.returnGas() if rules.IsAmsterdam { - // EIP-8037: 2D gas accounting for Amsterdam. - // tx_regular = initialBudget.RegularGas - gasRemaining.RegularGas - // tx_state = initialBudget.StateGas - gasRemaining.StateGas - txRegular := st.initialBudget.RegularGas - st.gasRemaining.RegularGas + // EIP-8037: tx_regular = intrinsic_regular + execution_regular_gas_used + // (RegularGasUsed already includes intrinsic since it stays counted). + // tx_state = intrinsic_state + execution_state_gas_used (StateGasUsed + // excludes intrinsic — it was undone after the intrinsic Charge). + txRegular := st.gasRemaining.RegularGasUsed txRegular = max(txRegular, floorDataGas) - txState := st.initialBudget.StateGas - st.gasRemaining.StateGas + txState := uint64(int64(cost.StateGas) + st.gasRemaining.StateGasUsed) + if int64(cost.StateGas)+st.gasRemaining.StateGasUsed < 0 { + txState = 0 + } + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, "state_transition: txRegular=%d, txState=%d (cost.StateGas=%d, StateGasUsed=%d), gasUsed=%d\n", + txRegular, txState, cost.StateGas, st.gasRemaining.StateGasUsed, st.gasUsed()) + } if err := st.gp.ReturnGasAmsterdam(txRegular, txState, st.gasUsed()); err != nil { return nil, err } diff --git a/core/vm/eips.go b/core/vm/eips.go index aa03453cd8..f8a0f39809 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -173,8 +173,14 @@ func enable3529(jt *JumpTable) { // enable8037 enables EIP-8037 SSTORE repricing: the regular-gas portion of // new slot creation and same-tx 0→X→0 reset is reduced; the state-gas // portion is charged/refunded at frame-end via the journal. +// +// Also reprices CREATE/CREATE2 base regular gas from GAS_CREATE (32,000) to +// CREATE_BASE_AMSTERDAM (9,000); the 112 × CPSB state-gas portion is charged +// at frame end via the journal walker. func enable8037(jt *JumpTable) { jt[SSTORE].dynamicGas = gasSStoreEIP8037 + jt[CREATE].constantGas = params.CreateGasAmsterdam + jt[CREATE2].constantGas = params.CreateGasAmsterdam } // enable3198 applies EIP-3198 (BASEFEE Opcode) diff --git a/core/vm/evm.go b/core/vm/evm.go index a0250bfdc0..ea10591a36 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -18,7 +18,9 @@ package vm import ( "errors" + "fmt" "math/big" + "os" "sync/atomic" "github.com/ethereum/go-ethereum/common" @@ -318,11 +320,31 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } gas.Exhaust() } + // EIP-8037: on error, return the child's state-gas-used to its + // reservoir (state was rolled back, no state was grown), and reset + // state_gas_used to 0 so it isn't propagated to the parent. + if evm.chainRules.IsAmsterdam && gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + gas.StateGasUsed = 0 + } } else { if evm.chainRules.IsAmsterdam { // Charge callee's state changes to the callee's gas. + // EIP-8037 spec: this_call_cost = growth_cost - already_paid, + // where already_paid is the sum of state_gas_used by successful + // descendants (tracked in gas.StateGasUsed). + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, "Call to %v depth=%d: closing snapshot2=%d (gas before charge: %v, StateGasUsed=%d)\n", + addr, evm.depth, snapshot2, gas, gas.StateGasUsed) + } bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + alreadyPaid := gas.StateGasUsed + thisCallCost := bytesCharged*int64(evm.Context.CostPerStateByte) - alreadyPaid + stateGasCost := GasCosts{StateGas: thisCallCost} + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " bytesCharged=%d, alreadyPaid=%d, thisCallCost=%d, canAfford=%v\n", + bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) + } if !gas.CanAfford(stateGasCost) { evm.StateDB.RevertToSnapshot(snapshot1) gas.Exhaust() @@ -366,7 +388,10 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } - var snapshot = evm.StateDB.Snapshot() + // EIP-8037: two-snapshot pattern (matches Call/DelegateCall) to avoid + // double counting subcall state-gas in the parent's frame computation. + snapshot1 := evm.StateDB.Snapshot() + snapshot2 := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -380,24 +405,45 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt gas = contract.Gas } if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } + // EIP-8037: on error, return state-gas-used to reservoir. + if evm.chainRules.IsAmsterdam && gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + gas.StateGasUsed = 0 + } } else { if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, "CallCode to %v depth=%d (gas before charge: %v, StateGasUsed=%d)\n", + addr, evm.depth, gas, gas.StateGasUsed) + } + bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) + alreadyPaid := gas.StateGasUsed + thisCallCost := bytesCharged*int64(evm.Context.CostPerStateByte) - alreadyPaid + stateGasCost := GasCosts{StateGas: thisCallCost} + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " bytesCharged=%d, alreadyPaid=%d, thisCallCost=%d, canAfford=%v\n", + bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) + } if !gas.CanAfford(stateGasCost) { gas.Exhaust() return ret, gas, ErrOutOfGas } gas.Charge(stateGasCost) } - evm.StateDB.CloseSnapshot(snapshot) + evm.StateDB.CloseSnapshot(snapshot2) + if evm.chainRules.IsAmsterdam { + // Cache snapshot1's own bytes (no setup work for callcode), + // excluding the snapshot2 subcall which was already charged. + evm.StateDB.StateChangedBytes(snapshot1, true) + } + evm.StateDB.CloseSnapshot(snapshot1) } return ret, gas, err } @@ -420,7 +466,11 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - var snapshot = evm.StateDB.Snapshot() + // EIP-8037: two-snapshot pattern (matches Call). snapshot1 covers + // any caller-frame setup (none for delegatecall, kept for symmetry); + // snapshot2 covers the callee execution. + snapshot1 := evm.StateDB.Snapshot() + snapshot2 := evm.StateDB.Snapshot() // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { @@ -435,24 +485,45 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, gas = contract.Gas } if err != nil { - evm.StateDB.RevertToSnapshot(snapshot) + evm.StateDB.RevertToSnapshot(snapshot1) if err != ErrExecutionReverted { if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } gas.Exhaust() } + // EIP-8037: on error, return state-gas-used to reservoir. + if evm.chainRules.IsAmsterdam && gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + gas.StateGasUsed = 0 + } } else { if evm.chainRules.IsAmsterdam { - bytesCharged := evm.StateDB.StateChangedBytes(snapshot, false) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, "DelegateCall to %v depth=%d (gas before charge: %v, StateGasUsed=%d)\n", + addr, evm.depth, gas, gas.StateGasUsed) + } + bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) + alreadyPaid := gas.StateGasUsed + thisCallCost := bytesCharged*int64(evm.Context.CostPerStateByte) - alreadyPaid + stateGasCost := GasCosts{StateGas: thisCallCost} + if os.Getenv("DEBUG_8037") != "" { + fmt.Fprintf(os.Stderr, " bytesCharged=%d, alreadyPaid=%d, thisCallCost=%d, canAfford=%v\n", + bytesCharged, alreadyPaid, thisCallCost, gas.CanAfford(stateGasCost)) + } if !gas.CanAfford(stateGasCost) { gas.Exhaust() return ret, gas, ErrOutOfGas } gas.Charge(stateGasCost) } - evm.StateDB.CloseSnapshot(snapshot) + evm.StateDB.CloseSnapshot(snapshot2) + if evm.chainRules.IsAmsterdam { + // Cache snapshot1's own bytes (= 0 for delegatecall, no setup), + // excluding the snapshot2 subcall which was already charged. + evm.StateDB.StateChangedBytes(snapshot1, true) + } + evm.StateDB.CloseSnapshot(snapshot1) } return ret, gas, err @@ -509,6 +580,10 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } gas.Exhaust() } + if evm.chainRules.IsAmsterdam && gas.StateGasUsed > 0 { + gas.StateGas += uint64(gas.StateGasUsed) + gas.StateGasUsed = 0 + } } else { evm.StateDB.CloseSnapshot(snapshot) } @@ -619,11 +694,21 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value if err != ErrExecutionReverted { contract.UseGas(GasCosts{RegularGas: contract.Gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } + // EIP-8037: on error, return the child contract's state-gas-used + // to its reservoir (state was rolled back). Don't propagate + // state_gas_used to the parent. + if evm.chainRules.IsAmsterdam && contract.Gas.StateGasUsed > 0 { + contract.Gas.StateGas += uint64(contract.Gas.StateGasUsed) + contract.Gas.StateGasUsed = 0 + } } else { if evm.chainRules.IsAmsterdam { // Charge initcode's state changes to the created contract's gas. + // EIP-8037 spec: this_call_cost = growth_cost - already_paid. bytesCharged := evm.StateDB.StateChangedBytes(snapshot2, false) - stateGasCost := GasCosts{StateGas: bytesCharged * int64(evm.Context.CostPerStateByte)} + alreadyPaid := contract.Gas.StateGasUsed + thisCallCost := bytesCharged*int64(evm.Context.CostPerStateByte) - alreadyPaid + stateGasCost := GasCosts{StateGas: thisCallCost} if !contract.Gas.CanAfford(stateGasCost) { evm.StateDB.RevertToSnapshot(snapshot1) contract.Gas.Exhaust() diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index b3259b2ec7..b9300111fe 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -430,16 +430,21 @@ func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m return GasCosts{}, ErrOutOfGas } // Stateful check - var stateGas uint64 - if evm.chainRules.IsEIP158 { - if transfersValue && evm.StateDB.Empty(address) { + // EIP-8037: under Amsterdam the regular-gas portion of GAS_NEW_ACCOUNT + // is removed (covered by CALL_VALUE 9000); the state-gas portion of + // 112 × CPSB is charged at frame end via the journal walker. + if !evm.chainRules.IsAmsterdam { + var stateGas uint64 + if evm.chainRules.IsEIP158 { + if transfersValue && evm.StateDB.Empty(address) { + stateGas += params.CallNewAccountGas + } + } else if !evm.StateDB.Exist(address) { stateGas += params.CallNewAccountGas } - } else if !evm.StateDB.Exist(address) { - stateGas += params.CallNewAccountGas - } - if gas, overflow = math.SafeAdd(gas, stateGas); overflow { - return GasCosts{}, ErrGasUintOverflow + if gas, overflow = math.SafeAdd(gas, stateGas); overflow { + return GasCosts{}, ErrGasUintOverflow + } } return GasCosts{RegularGas: gas}, nil } @@ -489,13 +494,18 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me gas = params.SelfdestructGasEIP150 var address = common.Address(stack.Back(0).Bytes20()) - if evm.chainRules.IsEIP158 { - // if empty and transfers value - if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + // EIP-8037: CreateBySelfdestructGas (25000 regular) is removed under + // Amsterdam — account creation is now charged via state gas at frame + // end via the journal walker. + if !evm.chainRules.IsAmsterdam { + if evm.chainRules.IsEIP158 { + // if empty and transfers value + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas += params.CreateBySelfdestructGas + } + } else if !evm.StateDB.Exist(address) { gas += params.CreateBySelfdestructGas } - } else if !evm.StateDB.Exist(address) { - gas += params.CreateBySelfdestructGas } } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 4f8f60f63b..48752cad52 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -59,6 +59,13 @@ type GasBudget struct { // Tracks the gas refunds in this call frame. Needed so we can // revert the refunds if the call frame reverts. StateGasRefund uint64 + + // EIP-8037 per-dimension usage trackers. RegularGasUsed is incremented + // only for regular-gas charges; StateGasUsed is incremented for the + // full state-gas portion of a charge regardless of whether it was + // satisfied from the reservoir or spilled into RegularGas. + RegularGasUsed uint64 + StateGasUsed int64 } // NewGasBudgetReg creates a GasBudget with the given initial regular gas allowance. @@ -76,13 +83,22 @@ func (g GasBudget) Used(initial GasBudget) uint64 { return (initial.RegularGas + initial.StateGas) - (g.RegularGas + g.StateGas) } -// Exhaust burns the remaining regular gas on exceptional halt. +// Exhaust burns the remaining regular gas on exceptional halt. The full +// remaining regular gas is moved to RegularGasUsed so the per-tx tally +// reflects the burn (per spec process_message exceptional-halt branch). func (g *GasBudget) Exhaust() { + g.RegularGasUsed += g.RegularGas g.RegularGas = 0 } func (g *GasBudget) Copy() GasBudget { - return GasBudget{RegularGas: g.RegularGas, StateGas: g.StateGas} + return GasBudget{ + RegularGas: g.RegularGas, + StateGas: g.StateGas, + StateGasRefund: g.StateGasRefund, + RegularGasUsed: g.RegularGasUsed, + StateGasUsed: g.StateGasUsed, + } } // String returns a visual representation of the gas budget vector. @@ -117,6 +133,8 @@ func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { return prior, false } g.RegularGas -= cost.RegularGas + g.RegularGasUsed += cost.RegularGas + g.StateGasUsed += cost.StateGas if cost.StateGas < 0 { g.StateGas -= uint64(cost.StateGas) return prior, true @@ -132,10 +150,14 @@ func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) { } // Refund adds the given gas budget back. It returns the pre-refund regular gas -// value and whether the budget was actually changed. +// value and whether the budget was actually changed. Used trackers from the +// other budget are accumulated so child-frame state-gas usage propagates to +// the caller. func (g *GasBudget) Refund(other GasBudget) (uint64, bool) { prior := g.RegularGas g.RegularGas += other.RegularGas g.StateGas += other.StateGas + g.RegularGasUsed += other.RegularGasUsed + g.StateGasUsed += other.StateGasUsed return prior, other.RegularGas != 0 || other.StateGas != 0 } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index d4d342281d..85755b96f3 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -669,7 +669,12 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) - res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudgetReg(gas), &value) + // EIP-8037: gas given to child via Create will be tracked there and + // propagated via RefundGas. Uncount here to avoid double-counting. + scope.Contract.Gas.RegularGasUsed -= gas + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 + res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, NewGasBudget(gas, childStateGas), &value) // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -708,9 +713,14 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Apply EIP150 gas -= gas / 64 scope.Contract.UseGas(GasCosts{RegularGas: gas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + // EIP-8037: gas given to child via Create2 will be tracked there and + // propagated via RefundGas. Uncount here to avoid double-counting. + scope.Contract.Gas.RegularGasUsed -= gas + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 // reuse size int for stackvalue stackvalue := size - res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudgetReg(gas), + res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, NewGasBudget(gas, childStateGas), &endowment, &salt) // Push item on the stack based on the returned error. if suberr != nil { @@ -744,10 +754,24 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if evm.readOnly && !value.IsZero() { return nil, ErrWriteProtection } + // EIP-8037: callGasTemp was already added to RegularGasUsed via the + // dynamicGas mechanism, but the child will track its own usage and + // propagate via RefundGas. Uncount it here to avoid double-counting. + scope.Contract.Gas.RegularGasUsed -= evm.callGasTemp if !value.IsZero() { gas += params.CallStipend + // EIP-8037: the stipend is "free" gas given to the child; it is + // not charged to the caller's regular_gas_used. The child will + // nevertheless track stipend usage via charge_gas and propagate + // via RefundGas, so subtract it here so the caller doesn't pay + // for stipend usage (matches spec MessageCallGas semantics). + scope.Contract.Gas.RegularGasUsed -= params.CallStipend } - ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), &value) + // EIP-8037: pass the parent's full state-gas reservoir to the child; + // reset parent's reservoir to zero. Leftover comes back via RefundGas. + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 + ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, NewGasBudget(gas, childStateGas), &value) if err != nil { temp.Clear() @@ -777,11 +801,15 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) + scope.Contract.Gas.RegularGasUsed -= evm.callGasTemp if !value.IsZero() { gas += params.CallStipend + scope.Contract.Gas.RegularGasUsed -= params.CallStipend } - ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), &value) + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 + ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, NewGasBudget(gas, childStateGas), &value) if err != nil { temp.Clear() } else { @@ -810,7 +838,10 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas), scope.Contract.value) + scope.Contract.Gas.RegularGasUsed -= evm.callGasTemp + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 + ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, NewGasBudget(gas, childStateGas), scope.Contract.value) if err != nil { temp.Clear() } else { @@ -839,7 +870,10 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudgetReg(gas)) + scope.Contract.Gas.RegularGasUsed -= evm.callGasTemp + childStateGas := scope.Contract.Gas.StateGas + scope.Contract.Gas.StateGas = 0 + ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, NewGasBudget(gas, childStateGas)) if err != nil { temp.Clear() } else { diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 9dc0a0b787..3c2fa9fd19 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -196,6 +196,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, ErrOutOfGas } else { contract.Gas.RegularGas -= cost + contract.Gas.RegularGasUsed += cost } // All ops with a dynamic memory usage also has a dynamic gas cost. @@ -229,6 +230,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, ErrOutOfGas } else { contract.Gas.RegularGas -= dynamicCost.RegularGas + contract.Gas.RegularGasUsed += dynamicCost.RegularGas } } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 87941a5ae1..733f2c865a 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -186,6 +186,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g // outside of this function, as part of the dynamic gas, and that will make it // also become correctly reported to tracers. contract.Gas.RegularGas += coldCost + // Also undo the RegularGasUsed bookkeeping so coldCost isn't + // double-counted (it will be re-added by the dynamicGas + // mechanism via the returned cost). + contract.Gas.RegularGasUsed -= coldCost gas := gasCost.RegularGas var overflow bool @@ -318,8 +322,13 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { } } // if empty and transfers value - if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { - gas += params.CreateBySelfdestructGas + // EIP-8037: under Amsterdam the regular-gas portion of + // CreateBySelfdestructGas (25,000) is removed; account creation + // is charged via state gas at frame end. + if !evm.chainRules.IsAmsterdam { + if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 { + gas += params.CreateBySelfdestructGas + } } if refundsEnabled && !evm.StateDB.HasSelfDestructed(contract.Address()) { evm.StateDB.AddRefund(params.SelfdestructRefundGas) @@ -411,6 +420,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc { // part of the dynamic gas. This will ensure it is correctly reported to // tracers. contract.Gas.RegularGas += eip2929Cost + eip7702Cost + // Also undo the RegularGasUsed bookkeeping so eip2929/7702 costs aren't + // double-counted (they will be re-added by the dynamicGas mechanism via + // the returned cost). + contract.Gas.RegularGasUsed -= eip2929Cost + eip7702Cost // Aggregate the gas costs from all components, including EIP-2929, EIP-7702, // the CALL opcode itself, and the cost incurred by nested calls.