core/vm: refactor call gas pricing such that Geth will not perform state access if the component of the call price which is independent from the state is not sufficient to cover the provided gas.

This commit is contained in:
Jared Wasinger 2025-12-03 09:29:10 -08:00 committed by root
parent 0a8f3b0177
commit f61df31e1f
2 changed files with 200 additions and 48 deletions

View file

@ -368,12 +368,35 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memor
return gas, nil
}
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
)
if transfersValue && !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
}
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
func gasCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.Back(2).IsZero()
address = common.Address(stack.Back(1).Bytes20())
)
if evm.chainRules.IsEIP158 {
if transfersValue && evm.StateDB.Empty(address) {
gas += params.CallNewAccountGas
@ -381,15 +404,22 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
} else if !evm.StateDB.Exist(address) {
gas += params.CallNewAccountGas
}
if transfersValue && !evm.chainRules.IsEIP4762 {
gas += params.CallValueTransferGas
}
memoryGas, err := memoryGasCost(mem, memorySize)
return gas, nil
}
func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
stateless, err := gasCallStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
var overflow bool
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
stateful, err := gasCallStateful(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
gas, overflow := math.SafeAdd(stateless, stateful)
if overflow {
return 0, ErrGasUintOverflow
}
@ -404,7 +434,11 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize
return gas, nil
}
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasCallCodeStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return 0, nil
}
func gasCallCodeStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
@ -419,6 +453,16 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var overflow bool
gas, err := gasCallCodeStateless(evm, contract, stack, 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
@ -430,10 +474,16 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory
}
func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
var (
err error
gas uint64
)
gas, err = gasDelegateCallStateless(evm, contract, stack, 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
@ -445,11 +495,36 @@ func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
return gas, nil
}
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
func gasDelegateCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return 0, nil
}
func gasDelegateCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
return gas, nil
}
func gasStaticCallStateless(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
return gas, nil
}
func gasStaticCallStateful(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
return 0, nil
}
func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := gasStaticCallStateless(evm, contract, stack, 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

View file

@ -155,7 +155,7 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
return 0, nil
}
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
func makeCallVariantGasCallEIP2929(oldCalculatorStateful, oldCalculatorStateless gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
addr := common.Address(stack.Back(addressPosition).Bytes20())
// Check slot presence in the access list
@ -163,6 +163,10 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
// 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
var (
eip2929Cost uint64
overflow bool
)
if !warmAccess {
evm.StateDB.AddAddressToAccessList(addr)
// Charge the remaining difference here already, to correctly calculate available
@ -170,35 +174,61 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g
if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
}
eip2929Cost = coldCost
}
// 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
eip150BaseGas, err := oldCalculatorStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
// ensure the portion of the call cost which doesn't depend on state lookups
// is covered by the provided gas
if contract.Gas < eip150BaseGas {
return 0, ErrOutOfGas
}
oldStateful, err := oldCalculatorStateful(evm, contract, stack, mem, memorySize)
if err != nil {
return oldStateful, err
}
if eip150BaseGas, overflow = math.SafeAdd(eip150BaseGas, oldStateful); overflow {
return 0, ErrGasUintOverflow
}
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, eip150BaseGas, stack.Back(0))
if err != nil {
return 0, 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 += coldCost
var overflow bool
if gas, overflow = math.SafeAdd(gas, coldCost); overflow {
contract.Gas += eip2929Cost
var total uint64
if total, overflow = math.SafeAdd(evm.callGasTemp, eip150BaseGas); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
if total, overflow = math.SafeAdd(total, eip2929Cost); overflow {
return 0, ErrGasUintOverflow
}
return total, nil
}
}
var (
gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall, 1)
gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall, 1)
gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall, 1)
gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode, 1)
gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCallStateless, gasCallStateful, 1)
gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCallStateless, gasDelegateCallStateful, 1)
gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCallStateless, gasStaticCallStateful, 1)
gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCodeStateless, gasCallCodeStateful, 1)
gasSelfdestructEIP2929 = makeSelfdestructGasFn(true)
// gasSelfdestructEIP3529 implements the changes in EIP-3529 (no refunds)
gasSelfdestructEIP3529 = makeSelfdestructGasFn(false)
@ -252,17 +282,21 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
}
var (
gasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCall)
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCall)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCall)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCode)
gasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallStateful, gasCallStateless)
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallStateful, gasDelegateCallStateless)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallStateful, gasStaticCallStateless)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeStateful, gasCallCodeStateless)
)
func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc {
func makeCallVariantGasCallEIP7702(oldCalculatorStateful, oldCalculatorStateless gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
total uint64 // total dynamic gas used
addr = common.Address(stack.Back(1).Bytes20())
eip150BaseGas uint64 // 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
)
// Check slot presence in the access list
@ -276,44 +310,87 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc {
if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
}
total += coldCost
eip2929Gas = coldCost
}
eip150BaseGas, err = oldCalculatorStateless(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
// ensure the portion of the call cost which doesn't depend on state lookups
// is covered by the provided gas
if contract.Gas < eip150BaseGas {
return 0, ErrOutOfGas
}
// ^ TODO: I'm not totally sure this is compatible with the 63/64 gas reduction rule
oldStateful, err := oldCalculatorStateful(evm, contract, stack, mem, memorySize)
if err != nil {
return oldStateful, err
}
if eip150BaseGas, overflow = math.SafeAdd(eip150BaseGas, oldStateful); overflow {
return 0, ErrOutOfGas
}
// Check if code is a delegation and if so, charge for resolution.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
var cost uint64
if evm.StateDB.AddressInAccessList(target) {
cost = params.WarmStorageReadCostEIP2929
eip7702Gas = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
cost = params.ColdAccountAccessCostEIP2929
eip7702Gas = params.ColdAccountAccessCostEIP2929
}
if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
if !contract.UseGas(eip7702Gas, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return 0, ErrOutOfGas
}
total += cost
}
// Now call the old calculator, which takes into account
// - create new account
// - transfer value
// - memory expansion
// - 63/64ths rule
old, err := oldCalculator(evm, contract, stack, mem, memorySize)
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas, eip150BaseGas, stack.Back(0))
if err != nil {
return old, err
return 0, err
}
// TODO: this check is probably not necessary (?)
/*
if contract.Gas < evm.callGasTemp {
return 0, ErrOutOfGas
}
*/
// 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?
// 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
// tracers.
contract.Gas += total
var overflow bool
if total, overflow = math.SafeAdd(old, total); overflow {
contract.Gas, overflow = math.SafeAdd(contract.Gas, eip2929Gas)
if overflow {
return 0, ErrGasUintOverflow
}
return total, nil
contract.Gas, overflow = math.SafeAdd(contract.Gas, eip7702Gas)
if overflow {
return 0, ErrGasUintOverflow
}
var totalCost uint64
totalCost, overflow = math.SafeAdd(eip2929Gas, eip7702Gas)
if overflow {
return 0, ErrGasUintOverflow
}
totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp)
if overflow {
return 0, ErrGasUintOverflow
}
totalCost, overflow = math.SafeAdd(totalCost, eip150BaseGas)
if overflow {
return 0, ErrGasUintOverflow
}
return totalCost, nil
}
}