From 3523721bd4e76258a2f72b00136facc2e8824d3d Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Wed, 24 Jun 2026 16:22:47 +0200 Subject: [PATCH] Claude fixes for Glam-6 - AL key gas - 7708 remove burn logs - Refund auths fully - 8037 halt mechanics - 8037 spill mechanics --- core/state_transition.go | 36 +++++++++++--------- core/vm/gas_table.go | 7 ++-- core/vm/gascosts.go | 71 ++++++++++++++++++++++++--------------- core/vm/instructions.go | 19 +++++------ params/protocol_params.go | 2 +- 5 files changed, 78 insertions(+), 57 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index 09a22a193b..85b519cfe1 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -122,14 +122,22 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set if accessList != nil { addresses := uint64(len(accessList)) storageKeys := uint64(accessList.StorageKeys()) - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses { + // EIP-8038 ties the access-list base costs to the cold-access costs + // (TX_ACCESS_LIST_ADDRESS = COLD_ACCOUNT_ACCESS = 3000, + // TX_ACCESS_LIST_STORAGE_KEY = COLD_STORAGE_ACCESS = 3000) in Amsterdam. + addressGas, storageKeyGas := params.TxAccessListAddressGas, params.TxAccessListStorageKeyGas + if rules.IsAmsterdam { + addressGas = params.ColdAccountAccessAmsterdam + storageKeyGas = params.ColdStorageAccessAmsterdam + } + if (math.MaxUint64-gas.RegularGas)/addressGas < addresses { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += addresses * params.TxAccessListAddressGas - if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys { + gas.RegularGas += addresses * addressGas + if (math.MaxUint64-gas.RegularGas)/storageKeyGas < storageKeys { return vm.GasCosts{}, ErrGasUintOverflow } - gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas + gas.RegularGas += storageKeys * storageKeyGas // EIP-7981: access list data is charged in addition to the base charge. if rules.IsAmsterdam { @@ -817,12 +825,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } } - // EIP-7708: Emit the ETH-burn logs - if rules.IsAmsterdam { - for _, log := range st.evm.StateDB.LogsForBurnAccounts() { - st.evm.StateDB.AddLog(log) - } - } return &ExecutionResult{ UsedGas: gasUsed, MaxUsedGas: peakUsed, @@ -999,12 +1001,14 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization) error { authority, err := st.validateAuthorization(auth) if err != nil { - // EIP-8037 (spec apply_authorization): an invalid authorization is - // skipped without any state-gas refund. The per-auth intrinsic state - // charge ((NEW_ACCOUNT + AUTH_BASE) * CPSB) was levied for every - // authorization in the list regardless of validity, and only a - // successfully-applied authorization that avoids creating new state - // earns a refund below. Invalid auths therefore pay in full. + if rules.IsAmsterdam { + // Spec set_delegation: an invalid authorization writes no state, so + // the full per-auth intrinsic charge is refilled — NEW_ACCOUNT + + // AUTH_BASE to the state reservoir and the worst-case ACCOUNT_WRITE + // to the regular refund counter. + st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte) + st.state.AddRefund(params.AccountWriteAmsterdam) + } return err } prevDelegation, curDelegated := types.ParseDelegation(st.state.GetCode(authority)) diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 1be270e8e0..061fd2d365 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -698,9 +698,10 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo cost.StateGas = stateSetGas } if current != newValue && original == newValue && original == (common.Hash{}) { - // Slot set then cleared in the same tx: refund the state gas directly - // to the reservoir (not the gas_used/5-capped refund counter). - contract.Gas.RefundState(stateSetGas) + // Slot set then cleared in the same tx: refund the state gas in LIFO + // order (regular gas up to the spilled amount, then the reservoir), + // not the gas_used/5-capped refund counter. + contract.Gas.CreditStateRefund(stateSetGas) } return cost, nil } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index f4469804f0..fd33329a67 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -65,6 +65,7 @@ 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 + SpilledStateGas uint64 // state gas that spilled from the reservoir into regular gas in this frame } // NewGasBudget initializes a fresh GasBudget for execution / forwarding, @@ -117,6 +118,7 @@ func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) { spillover := cost.StateGas - g.StateGas g.StateGas = 0 g.RegularGas -= spillover + g.SpilledStateGas += spillover } else { g.StateGas -= cost.StateGas } @@ -145,9 +147,10 @@ func (g *GasBudget) IsZero() bool { return g.RegularGas == 0 && g.StateGas == 0 } -// 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: +// RefundState applies an inline state-gas refund directly to the reservoir +// (e.g., a SetCode authorization on an already-existing leaf, or a top-level +// transaction refund). The reservoir is credited and the signed usage counter +// is decremented in lockstep, preserving the per-frame invariant: // // StateGas + UsedStateGas == initialStateGas + spillover_so_far // @@ -157,6 +160,24 @@ func (g *GasBudget) RefundState(s uint64) { g.UsedStateGas -= int64(s) } +// CreditStateRefund applies an inline state-gas refund in LIFO order, mirroring +// the spec's credit_state_gas_refund. State-gas charges draw from the reservoir +// first and from regular gas last, so a refill credits the pool charged last +// first: regular gas up to the amount previously spilled, then the reservoir. +// This restores the exact pools the original charge drew from, so a probe +// sub-call that can only observe the reservoir sees a refund only when the +// reservoir actually had headroom. +// +// Used for SSTORE 0->A->0 restorations and NEW_ACCOUNT refunds on failed +// CALL/CREATE sub-calls (spec storage.py / system.py credit_state_gas_refund). +func (g *GasBudget) CreditStateRefund(s uint64) { + fromRegular := min(s, g.SpilledStateGas) + g.RegularGas += fromRegular + g.SpilledStateGas -= fromRegular + g.StateGas += s - fromRegular + g.UsedStateGas -= int64(s) +} + // Forward drains `regular` regular gas and the entire state reservoir from // the parent's running budget and returns the initial GasBudget for a child // frame. The parent's UsedRegularGas is bumped by the forwarded amount so @@ -201,48 +222,44 @@ 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. It mirrors the spec's +// refill_frame_state_gas: the frame's state gas is rolled back in LIFO order — +// the spilled portion is credited back to regular gas and the remainder to the +// reservoir — so the pools the charges drew from are restored. No gas is burned +// on a revert. func (g GasBudget) ExitRevert() GasBudget { - reservoir := int64(g.StateGas) + g.UsedStateGas + reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.SpilledStateGas) if reservoir < 0 { - // Reservoir should never be negative. By construction it equals - // the initial state-gas allocation plus any spillover to regular - // gas. + // Reservoir should never be negative. By construction it equals the + // initial state-gas allocation (spillover is returned to regular gas). 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, "spilled", g.SpilledStateGas) } return GasBudget{ - RegularGas: g.RegularGas, + RegularGas: g.RegularGas + g.SpilledStateGas, StateGas: uint64(reservoir), UsedRegularGas: g.UsedRegularGas, UsedStateGas: 0, } } -// ExitHalt produces the leftover for an exceptional halt. -// -// Per the updated EIP-8037, only the regular gas_left is burned (folded into -// UsedRegularGas); the entire state-gas reservoir — including any portion that -// spilled into the regular pool during execution — is refunded to the caller's -// reservoir rather than reclassified as burned regular gas. +// ExitHalt produces the leftover for an exceptional halt. It mirrors the spec's +// refill_frame_state_gas followed by the gas_left burn: the frame's state gas +// is rolled back LIFO (spilled portion to regular gas, remainder to the +// reservoir), then ALL remaining regular gas — including the just-refilled +// spilled portion — is burned. Only the non-spilled reservoir survives. func (g GasBudget) ExitHalt() GasBudget { - reservoir := int64(g.StateGas) + g.UsedStateGas + reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.SpilledStateGas) if reservoir < 0 { - // Reservoir should never be negative. By construction it equals - // the initial state-gas allocation plus any spillover to regular - // gas. + // Reservoir should never be negative. By construction it equals the + // initial state-gas allocation (spillover is burned with regular gas). reservoir = 0 - log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas) + log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas, "spilled", g.SpilledStateGas) } return GasBudget{ RegularGas: 0, StateGas: uint64(reservoir), - UsedRegularGas: g.UsedRegularGas + g.RegularGas, + UsedRegularGas: g.UsedRegularGas + g.RegularGas + g.SpilledStateGas, UsedStateGas: 0, } } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 672a731358..2c2e78a8bd 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -681,9 +681,9 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // EIP-8037: no account was created on any failure path, so refund the // account-creation state gas charged before the opcode ran (gasCreateEip8037) - // back to the reservoir. + // in LIFO order (regular gas up to the spilled amount, then the reservoir). if evm.chainRules.IsAmsterdam && suberr != nil { - scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) + scope.Contract.Gas.CreditStateRefund(params.AccountCreationSize * evm.Context.CostPerStateByte) } if suberr == ErrExecutionReverted { @@ -722,9 +722,9 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // EIP-8037: no account was created on any failure path, so refund the // account-creation state gas charged before the opcode ran (gasCreate2Eip8037) - // back to the reservoir. + // in LIFO order (regular gas up to the spilled amount, then the reservoir). if evm.chainRules.IsAmsterdam && suberr != nil { - scope.Contract.Gas.RefundState(params.AccountCreationSize * evm.Context.CostPerStateByte) + scope.Contract.Gas.CreditStateRefund(params.AccountCreationSize * evm.Context.CostPerStateByte) } if suberr == ErrExecutionReverted { @@ -955,12 +955,11 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro evm.StateDB.SubBalance(this, balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary, balance, tracing.BalanceIncreaseSelfdestruct) } - if evm.chainRules.IsAmsterdam && !balance.IsZero() { - if this != beneficiary { - evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) - } else if newContract { - evm.StateDB.AddLog(types.EthBurnLog(this, balance)) - } + // EIP-7708: emit a transfer log when value moves to a distinct beneficiary. + // The current spec emits no burn log for a self-beneficiary selfdestruct + // (the balance is preserved and silently cleared at tx end). + if evm.chainRules.IsAmsterdam && !balance.IsZero() && this != beneficiary { + evm.StateDB.AddLog(types.EthTransferLog(this, beneficiary, balance)) } if tracer := evm.Config.Tracer; tracer != nil { diff --git a/params/protocol_params.go b/params/protocol_params.go index 1f7500a356..8b0560ba18 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -156,7 +156,7 @@ const ( MaxCodeSize = 24576 // Maximum bytecode to permit for a contract MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions - MaxCodeSizeAmsterdam = 32768 // Maximum bytecode to permit for a contract post Amsterdam + MaxCodeSizeAmsterdam = 65536 // Maximum bytecode to permit for a contract post Amsterdam (EIP-7954: 64 KiB) MaxInitCodeSizeAmsterdam = 2 * MaxCodeSizeAmsterdam // Maximum initcode to permit in a creation transaction and create instructions post Amsterdam // Precompiled contract gas prices