core/vm: rework gas measurement for call variants (#33648)

EIP-7928 brings state reads into consensus by recording accounts and storage accessed during execution in the block access list. As part of the spec, we need to check that there is enough gas available to cover the cost component which doesn't depend on looking up state. If this component can't be covered by the available gas, we exit immediately.

The portion of the call dynamic cost which doesn't depend on state look ups:

- EIP2929 call costs
- value transfer cost
- memory expansion cost

This PR:

- breaks up the "inner" gas calculation for each call variant into a pair of stateless/stateful cost methods
- modifies the gas calculation logic of calls to check stateless cost component first, and go out of gas immediately if it is not covered.

---------

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
This commit is contained in:
jwasinger 2026-03-19 12:02:49 -04:00 committed by GitHub
parent a3083ff5d0
commit fd859638bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 108 additions and 90 deletions

View file

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

View file

@ -373,7 +373,32 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
return gas, nil return gas, nil
} }
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { var (
gasCall = makeCallVariantGasCost(gasCallIntrinsic)
gasCallCode = makeCallVariantGasCost(gasCallCodeIntrinsic)
gasDelegateCall = makeCallVariantGasCost(gasDelegateCallIntrinsic)
gasStaticCall = makeCallVariantGasCost(gasStaticCallIntrinsic)
)
func makeCallVariantGasCost(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
intrinsic, err := intrinsicFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsic, stack.Back(0))
if err != nil {
return 0, err
}
gas, overflow := math.SafeAdd(intrinsic, evm.callGasTemp)
if overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
}
func gasCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var ( var (
gas uint64 gas uint64
transfersValue = !stack.Back(2).IsZero() transfersValue = !stack.Back(2).IsZero()
@ -382,38 +407,40 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
if evm.readOnly && transfersValue { if evm.readOnly && transfersValue {
return 0, ErrWriteProtection return 0, ErrWriteProtection
} }
// Stateless check
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas
}
} else if !evm.StateDB.Exist(address) {
gas += params.CallNewAccountGas
}
if transfersValue && !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
}
memoryGas, err := memoryGasCost(mem, memorySize) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
transferGas = params.CallValueTransferGas
}
var overflow bool var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
// Terminate the gas measurement if the leftover gas is not sufficient,
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0)) // it can effectively prevent accessing the states in the following steps.
if err != nil { if contract.Gas < gas {
return 0, err return 0, ErrOutOfGas
} }
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { // Stateful check
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
}
if gas, overflow = math.SafeAdd(gas, stateGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
return gas, nil return gas, nil
} }
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
memoryGas, err := memoryGasCost(mem, memorySize) memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil { if err != nil {
return 0, err return 0, err
@ -428,46 +455,15 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow { if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil return gas, nil
} }
func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize) return memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
} }
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasStaticCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize) return memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, gas, stack.Back(0))
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
} }
func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {

View file

@ -256,10 +256,10 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
} }
var ( var (
innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall) innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic)
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall) gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall) gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode) gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
) )
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
@ -274,62 +274,85 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize) return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
} }
func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { func makeCallVariantGasCallEIP7702(intrinsicFunc gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var ( var (
total uint64 // total dynamic gas used eip2929Cost uint64
addr = common.Address(stack.Back(1).Bytes20()) eip7702Cost uint64
addr = common.Address(stack.Back(1).Bytes20())
) )
// Perform EIP-2929 checks (stateless), checking address presence
// Check slot presence in the access list // in the accessList and charge the cold access accordingly.
if !evm.StateDB.AddressInAccessList(addr) { if !evm.StateDB.AddressInAccessList(addr) {
evm.StateDB.AddAddressToAccessList(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 // The WarmStorageReadCostEIP2929 (100) is already deducted in the form
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 // of a constant cost, so the cost to charge for cold access, if any,
// Charge the remaining difference here already, to correctly calculate available // is Cold - Warm
// gas for call eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
// Charge the remaining difference here already, to correctly calculate
// available gas for call
if !contract.UseGas(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas return 0, ErrOutOfGas
} }
total += coldCost }
// 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 0, err
}
// Terminate the gas measurement if the leftover gas is not sufficient,
// it can effectively prevent accessing the states in the following steps.
// It's an essential safeguard before any stateful check.
if contract.Gas < intrinsicCost {
return 0, ErrOutOfGas
} }
// Check if code is a delegation and if so, charge for resolution. // Check if code is a delegation and if so, charge for resolution.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok { if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
var cost uint64
if evm.StateDB.AddressInAccessList(target) { if evm.StateDB.AddressInAccessList(target) {
cost = params.WarmStorageReadCostEIP2929 eip7702Cost = params.WarmStorageReadCostEIP2929
} else { } else {
evm.StateDB.AddAddressToAccessList(target) evm.StateDB.AddAddressToAccessList(target)
cost = params.ColdAccountAccessCostEIP2929 eip7702Cost = params.ColdAccountAccessCostEIP2929
} }
if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { if !contract.UseGas(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas return 0, ErrOutOfGas
} }
total += cost
} }
// Calculate the gas budget for the nested call. The costs defined by
// Now call the old calculator, which takes into account // EIP-2929 and EIP-7702 have already been applied.
// - create new account evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, intrinsicCost, stack.Back(0))
// - transfer value
// - memory expansion
// - 63/64ths rule
old, err := oldCalculator(evm, contract, stack, mem, memorySize)
if err != nil { if err != nil {
return old, err return 0, err
} }
// Temporarily add the gas charge back to the contract and return value. By // 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 // 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 // part of the dynamic gas. This will ensure it is correctly reported to
// tracers. // tracers.
contract.Gas += total contract.Gas += eip2929Cost + eip7702Cost
var overflow bool // Aggregate the gas costs from all components, including EIP-2929, EIP-7702,
if total, overflow = math.SafeAdd(old, total); overflow { // the CALL opcode itself, and the cost incurred by nested calls.
var (
overflow bool
totalCost uint64
)
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return 0, ErrGasUintOverflow return 0, ErrGasUintOverflow
} }
return total, nil if totalCost, overflow = math.SafeAdd(totalCost, intrinsicCost); overflow {
return 0, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
return 0, ErrGasUintOverflow
}
return totalCost, nil
} }
} }