core/vm: integrate Gary's call gas cost rework

This commit is contained in:
Jared Wasinger 2026-03-11 16:11:05 -04:00
parent e5d453c086
commit 97d643a677
4 changed files with 162 additions and 214 deletions

View file

@ -606,7 +606,6 @@ func enable8037(jt *JumpTable) {
jt[CREATE].dynamicGas = gasCreateEip8037
jt[CREATE2].constantGas = params.CreateGasAmsterdam
jt[CREATE2].dynamicGas = gasCreate2Eip8037
jt[CALL].dynamicGas = makeCallVariantGasCall(gasCall8037, gasCallStateless)
jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
jt[SSTORE].dynamicGas = gasSStore8037
}

View file

@ -49,6 +49,5 @@ func callGas(isEip150 bool, availableGas, base uint64, callCost *uint256.Int) (u
if !callCost.IsUint64() {
return 0, ErrGasUintOverflow
}
return callCost.Uint64(), nil
}

View file

@ -18,7 +18,6 @@ package vm
import (
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/params"
@ -378,33 +377,32 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
return GasCosts{RegularGas: gas}, nil
}
func gasCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
)
var (
gasCall = makeCallVariantGasCost(gasCallIntrinsic)
gasCallCode = makeCallVariantGasCost(gasCallCodeIntrinsic)
gasDelegateCall = makeCallVariantGasCost(gasDelegateCallIntrinsic)
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic)
)
if transfersValue {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
} else if !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsic.RegularGas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
gas, overflow := math.SafeAdd(intrinsic.RegularGas, evm.callGasTemp)
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
return GasCosts{RegularGas: gas}, nil
}
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return GasCosts{RegularGas: gas}, nil
}
func gasCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
@ -413,48 +411,54 @@ func gasCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
}
// Stateless check
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
transferGas = params.CallValueTransferGas
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return GasCosts{}, ErrGasUintOverflow
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
if contract.Gas.RegularGas < gas {
return GasCosts{}, ErrOutOfGas
}
// Stateful check
var (
stateGas uint64
accountCreationCost uint64
)
if evm.chainRules.IsAmsterdam {
accountCreationCost = params.AccountCreationSize * evm.Context.CostPerGasByte
} else {
accountCreationCost = params.CallNewAccountGas
}
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas
stateGas = accountCreationCost
}
} else if !evm.StateDB.Exist(address) {
gas += params.CallNewAccountGas
stateGas = accountCreationCost
}
return GasCosts{RegularGas: gas}, nil
}
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
stateless, err := gasCallStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
if evm.chainRules.IsAmsterdam {
return GasCosts{RegularGas: gas, StateGas: stateGas}, nil
}
stateful, err := gasCallStateful(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
gas, overflow := math.SafeAdd(stateless.RegularGas, stateful.RegularGas)
gas, overflow = math.SafeAdd(gas, stateGas)
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, gas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return GasCosts{RegularGas: gas}, nil
}
func gasCallCodeStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
return GasCosts{}, nil
}
func gasCallCodeStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@ -475,50 +479,7 @@ func gasCallCodeStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memor
return GasCosts{RegularGas: gas}, nil
}
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var overflow bool
gas, err := gasCallCodeStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, gas.RegularGas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
if gas.RegularGas, overflow = math.SafeAdd(gas.RegularGas, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
}
func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
err error
gas GasCosts
)
gas, err = gasDelegateCallStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, gas.RegularGas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
var overflow bool
if gas.RegularGas, overflow = math.SafeAdd(gas.RegularGas, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
}
func gasDelegateCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
return GasCosts{}, nil
}
func gasDelegateCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@ -526,7 +487,7 @@ func gasDelegateCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *M
return GasCosts{RegularGas: gas}, nil
}
func gasStaticCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
@ -534,27 +495,6 @@ func gasStaticCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Mem
return GasCosts{RegularGas: gas}, nil
}
func gasStaticCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
return GasCosts{}, nil
}
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
gas, err := gasStaticCallStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, gas.RegularGas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
var overflow bool
if gas.RegularGas, overflow = math.SafeAdd(gas.RegularGas, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
}
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
@ -628,26 +568,6 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
return GasCosts{RegularGas: gas + wordGas, StateGas: stateGas}, nil
}
// gasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037).
// It only returns the state-dependent gas (account creation as state gas).
// Memory gas, transfer gas, and callGas are handled by gasCallStateless and
// makeCallVariantGasCall.
func gasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
gas GasCosts
transfersValue = !stack.Back(2).IsZero()
address = common.Address(stack.Back(1).Bytes20())
)
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas.StateGas += params.AccountCreationSize * evm.Context.CostPerGasByte
}
} else if !evm.StateDB.Exist(address) {
gas.StateGas += params.AccountCreationSize * evm.Context.CostPerGasByte
}
return gas, nil
}
func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection

View file

@ -18,7 +18,6 @@ package vm
import (
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/tracing"
@ -155,12 +154,51 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
return GasCosts{RegularGas: 0}, nil
}
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
addr := common.Address(stack.Back(addressPosition).Bytes20())
// Check slot presence in the access list
warmAccess := evm.StateDB.AddressInAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
// the cost to charge for cold access, if any, is Cold - Warm
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
if !warmAccess {
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
// gas for call
if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Now call the old calculator, which takes into account
// - create new account
// - transfer value
// - memory expansion
// - 63/64ths rule
gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
if warmAccess || err != nil {
return gas, err
}
// In case of a cold access, we temporarily add the cold charge back, and also
// add it to the returned gas. By adding it to the return, it will be charged
// 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
var overflow bool
if gas.RegularGas, overflow = math.SafeAdd(gas.RegularGas, coldCost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return gas, nil
}
}
var (
// TODO: we can use the same functions already defined above for the 7702 gas handlers
gasCallEIP2929 = makeCallVariantGasCall(gasCallStateless, gasCallStateful)
gasDelegateCallEIP2929 = makeCallVariantGasCall(gasDelegateCallStateless, gasDelegateCallStateful)
gasStaticCallEIP2929 = makeCallVariantGasCall(gasStaticCallStateless, gasStaticCallStateful)
gasCallCodeEIP2929 = makeCallVariantGasCall(gasCallCodeStateless, gasCallCodeStateful)
gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall, 1)
gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall, 1)
gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall, 1)
gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode, 1)
gasSelfdestructEIP2929 = makeSelfdestructGasFn(true)
// gasSelfdestructEIP3529 implements the changes in EIP-3529 (no refunds)
gasSelfdestructEIP3529 = makeSelfdestructGasFn(false)
@ -222,76 +260,71 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
}
var (
gasCallEIP7702 = makeCallVariantGasCall(gasCallStateful, gasCallStateless)
gasDelegateCallEIP7702 = makeCallVariantGasCall(gasDelegateCallStateful, gasDelegateCallStateless)
gasStaticCallEIP7702 = makeCallVariantGasCall(gasStaticCallStateful, gasStaticCallStateless)
gasCallCodeEIP7702 = makeCallVariantGasCall(gasCallCodeStateful, gasCallCodeStateless)
innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic)
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
)
func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFunc) gasFunc {
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
// Return early if this call attempts to transfer value in a static context.
// Although it's checked in `gasCall`, EIP-7702 loads the target's code before
// to determine if it is resolving a delegation. This could incorrectly record
// the target in the block access list (BAL) if the call later fails.
transfersValue := !stack.Back(2).IsZero()
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
}
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip150BaseGas GasCosts // gas used for memory expansion, transfer costs -> input to the 63/64 bounding
eip7702Gas uint64
eip2929Gas uint64
addr = common.Address(stack.Back(1).Bytes20())
overflow bool
err error
eip2929Gas uint64
eip7702Gas uint64
addr = common.Address(stack.Back(1).Bytes20())
)
// Check slot presence in the access list
if evm.chainRules.IsEIP2929 && !evm.StateDB.AddressInAccessList(addr) {
// Perform EIP-2929 checks (stateless), checking address presence
// in the accessList and charge the cold access accordingly.
if !evm.StateDB.AddressInAccessList(addr) {
evm.StateDB.AddAddressToAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
// the cost to charge for cold access, if any, is Cold - Warm
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
// Charge the remaining difference here already, to correctly calculate available
// gas for call
if !contract.UseGas(GasCosts{RegularGas: coldCost}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form
// of a constant cost, so the cost to charge for cold access, if any,
// is Cold - Warm
eip2929Gas = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
// Charge the remaining difference here already, to correctly calculate
// available gas for call
if !contract.UseGas(GasCosts{RegularGas: eip2929Gas}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
eip2929Gas = coldCost
}
eip150BaseGas, err = oldCalculatorStateless(evm, contract, stack, mem, memorySize)
// Perform the intrinsic cost calculation including:
//
// - transfer value
// - memory expansion
// - create new account
intrinsicCost, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
// ensure the portion of the call cost which doesn't depend on state lookups
// is covered by the provided gas
if contract.Gas.RegularGas < eip150BaseGas.RegularGas {
if contract.Gas.RegularGas < intrinsicCost.RegularGas {
return GasCosts{}, ErrOutOfGas
}
oldStateful, err := oldCalculatorStateful(evm, contract, stack, mem, memorySize)
if err != nil {
return oldStateful, err
}
// this should cause BAL test failures if uncommented
baseCost, overflow := math.SafeAdd(eip150BaseGas.RegularGas, oldStateful.RegularGas)
if overflow {
return GasCosts{}, ErrGasUintOverflow
} else if contract.Gas.RegularGas < baseCost {
return GasCosts{}, ErrOutOfGas
}
if eip150BaseGas.RegularGas, overflow = math.SafeAdd(eip150BaseGas.RegularGas, oldStateful.RegularGas); overflow {
return GasCosts{}, ErrOutOfGas
}
if evm.chainRules.IsPrague {
// Check if code is a delegation and if so, charge for resolution.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
if evm.StateDB.AddressInAccessList(target) {
eip7702Gas = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
eip7702Gas = params.ColdAccountAccessCostEIP2929
}
if !contract.UseGas(GasCosts{RegularGas: eip7702Gas}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
// Check if code is a delegation and if so, charge for resolution.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
if evm.StateDB.AddressInAccessList(target) {
eip7702Gas = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
eip7702Gas = params.ColdAccountAccessCostEIP2929
}
if !contract.UseGas(GasCosts{RegularGas: eip7702Gas}, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
@ -301,8 +334,8 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun
// the Underflow check in UseGas will fail when the spillover exceeds the
// tiny 1/64 remainder after child gas allocation.
var stateGasCharged uint64
if evm.chainRules.IsAmsterdam && oldStateful.StateGas > 0 {
stateGasCharged = oldStateful.StateGas
if evm.chainRules.IsAmsterdam && intrinsicCost.StateGas > 0 {
stateGasCharged = intrinsicCost.StateGas
stateGasCost := GasCosts{StateGas: stateGasCharged}
if contract.Gas.Underflow(stateGasCost) {
return GasCosts{}, ErrOutOfGas
@ -311,15 +344,14 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun
contract.Gas.Sub(stateGasCost)
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, eip150BaseGas.RegularGas, stack.Back(0))
// Calculate the gas budget for the nested call. The costs defined by
// EIP-2929 and EIP-7702 have already been applied.
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, intrinsicCost.RegularGas, stack.Back(0))
if err != nil {
return GasCosts{}, err
}
// TODO: it's not clear what happens if there is enough gas to cover the stateless component
// but not enough to cover the whole call: do all the state reads happen in this case, and
// we fail at the very end?
var overflow bool
// Temporarily add the gas charge back to the contract and return value. By
// adding it to the return, it will be charged outside of this function, as
// part of the dynamic gas. This will ensure it is correctly reported to
@ -338,24 +370,22 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun
}
contract.GasUsed.RegularGasUsed -= eip7702Gas
// Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
// the CALL opcode itself, and the cost incurred by nested calls.
var totalCost uint64
totalCost, overflow = math.SafeAdd(eip2929Gas, eip7702Gas)
if overflow {
if totalCost, overflow = math.SafeAdd(eip2929Gas, eip7702Gas); overflow {
return GasCosts{}, ErrGasUintOverflow
}
totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp)
if overflow {
if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost.RegularGas); overflow {
return GasCosts{}, ErrGasUintOverflow
}
totalCost, overflow = math.SafeAdd(totalCost, eip150BaseGas.RegularGas)
if overflow {
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
// If state gas was already charged directly (Amsterdam), don't include
// it in the returned cost — it would be double-charged by the
// interpreter's UseGas/Sub which increments TotalStateGasCharged again.
returnedStateGas := oldStateful.StateGas
returnedStateGas := intrinsicCost.StateGas
if stateGasCharged > 0 {
returnedStateGas = 0
}