Claude fixes for Glam-6

- AL key gas
- 7708 remove burn logs
- Refund auths fully
- 8037 halt mechanics
- 8037 spill mechanics
This commit is contained in:
Marius van der Wijden 2026-06-24 16:22:47 +02:00
parent 9c7bd2abe5
commit 3523721bd4
5 changed files with 78 additions and 57 deletions

View file

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

View file

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

View file

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

View file

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

View file

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