From e5d453c086c757c83efb08c9fa25b4c11ef42cbe Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Thu, 19 Mar 2026 14:52:15 +0100 Subject: [PATCH] core/vm: update to newest tests, rework gas mechanism --- build/checksums.txt | 6 ++-- core/state_processor.go | 6 ++-- core/state_transition.go | 20 +++++------ core/vm/contract.go | 20 ++++++----- core/vm/evm.go | 72 +++++++++++++++++++++---------------- core/vm/gas_table.go | 1 + core/vm/gas_table_test.go | 4 +-- core/vm/gascosts.go | 18 ++++++++-- core/vm/instructions.go | 41 +++++++++++++-------- core/vm/interpreter.go | 2 ++ core/vm/interpreter_test.go | 2 +- core/vm/operations_acl.go | 6 ++++ core/vm/runtime/runtime.go | 6 ++-- tests/state_test.go | 2 +- 14 files changed, 125 insertions(+), 81 deletions(-) diff --git a/build/checksums.txt b/build/checksums.txt index ceee947497..2f93b95662 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,10 +5,10 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz -# version:spec-tests-bal v5.4.0 +# version:spec-tests-bal v5.5.1 # https://github.com/ethereum/execution-spec-tests/releases -# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v5.4.0 -be300cbc5dcae213850bc5688d8fd0f572785ec64801f28653e1c01a03805127 fixtures_bal.tar.gz +# https://github.com/ethereum/execution-spec-tests/releases/download/bal%40v5.5.1 +79f81379bc456b9f05d4e7298eba939855d0147b525cd2cadd1206513284ab9e fixtures_bal.tar.gz # version:golang 1.25.7 # https://go.dev/dl/ diff --git a/core/state_processor.go b/core/state_processor.go index e6533ad05a..733ed3875b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -298,7 +298,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) bal.StateMutati } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) + _, _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) return evm.StateDB.Finalise(true) } @@ -322,7 +322,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) bal.StateMutation } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) + _, _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) if err != nil { panic(err) } @@ -362,7 +362,7 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(addr) - ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) + ret, _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.GasCosts{RegularGas: 30_000_000}, common.U2560) mut := evm.StateDB.Finalise(true) if err != nil { return nil, fmt.Errorf("system call failed to execute: %v", err) diff --git a/core/state_transition.go b/core/state_transition.go index 1ac580f3ef..04152b2e13 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -545,8 +545,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { vmerr error // vm errors do not effect consensus and are therefore not assigned to err authRefund uint64 ) + var execGasUsed vm.GasUsed if contractCreation { - ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) + ret, _, st.gasRemaining, execGasUsed, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -570,19 +571,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's call. - ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) + ret, st.gasRemaining, execGasUsed, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) } // Record the gas used excluding gas refunds. This value represents the actual // gas allowance required to complete execution. peakGasUsed := st.gasUsed() - // EIP-8037: Capture pre-refund remaining for 2D gas accounting. - var preRefundRemaining uint64 - if rules.IsAmsterdam { - preRefundRemaining = st.gasRemaining.Sum() - } - // Compute refund counter, capped to a refund quotient. st.gasRemaining.RegularGas += st.calcRefund() if rules.IsPrague { @@ -607,10 +602,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if rules.IsAmsterdam { // EIP-8037: 2D gas accounting for Amsterdam. - // tx_state = adjusted_intrinsic_state + exec_state_used (spec: set_delegation adjusts intrinsic) - // tx_regular = total_dimensional_used - tx_state - txState := (gas.StateGas - authRefund) + st.gasRemaining.StateGasCharged - txRegular := (msg.GasLimit - preRefundRemaining) - txState + // tx_regular = intrinsic_regular + exec_regular_gas_used + // tx_state = intrinsic_state (adjusted) + exec_state_gas_used + // These are tracked independently, not derived from remaining gas. + txState := (gas.StateGas - authRefund) + execGasUsed.StateGasCharged + txRegular := gas.RegularGas + execGasUsed.RegularGasUsed txRegular = max(txRegular, floorDataGas) if err := st.gp.ReturnGasAmsterdam(returned, txRegular, txState, st.gasUsed()); err != nil { return nil, err diff --git a/core/vm/contract.go b/core/vm/contract.go index 9aac8dcb03..beb4c3ff52 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -42,8 +42,9 @@ type Contract struct { IsDeployment bool IsSystemCall bool - Gas GasCosts - value *uint256.Int + Gas GasCosts + GasUsed GasUsed // EIP-8037: canonical per-frame gas usage accumulator + value *uint256.Int } // NewContract returns a new contract environment for the execution of EVM. @@ -133,19 +134,21 @@ func (c *Contract) UseGas(gas GasCosts, logger *tracing.Hooks, reason tracing.Ga if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { logger.OnGasChange(c.Gas.RegularGas, c.Gas.RegularGas-gas.RegularGas, reason) } + c.GasUsed.Add(gas) c.Gas.Sub(gas) return true } -// RefundGas refunds gas to the contract -func (c *Contract) RefundGas(err error, gas GasCosts, logger *tracing.Hooks, reason tracing.GasChangeReason) { +// RefundGas refunds gas to the contract. gasUsed carries the child frame's +// accumulated gas usage metrics (EIP-8037), incorporated on both success and error. +func (c *Contract) RefundGas(err error, gas GasCosts, gasUsed GasUsed, logger *tracing.Hooks, reason tracing.GasChangeReason) { // If the preceding call errored, return the state gas // to the parent call if err != nil { - gas.StateGas += gas.StateGasCharged - gas.StateGasCharged = 0 + gas.StateGas += gasUsed.StateGasCharged + gasUsed.StateGasCharged = 0 } - if gas.RegularGas == 0 && gas.StateGas == 0 && gas.StateGasCharged == 0 { + if gas.RegularGas == 0 && gas.StateGas == 0 && gasUsed.StateGasCharged == 0 && gasUsed.RegularGasUsed == 0 { return } if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { @@ -153,7 +156,8 @@ func (c *Contract) RefundGas(err error, gas GasCosts, logger *tracing.Hooks, rea } c.Gas.RegularGas += gas.RegularGas c.Gas.StateGas = gas.StateGas - c.Gas.StateGasCharged += gas.StateGasCharged + c.GasUsed.StateGasCharged += gasUsed.StateGasCharged + c.GasUsed.RegularGasUsed += gasUsed.RegularGasUsed } // Address returns the contracts address diff --git a/core/vm/evm.go b/core/vm/evm.go index 95b40eb37b..12aa6b11c9 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -238,7 +238,7 @@ func isSystemCall(caller common.Address) bool { // parameters. It also handles any necessary value transfer required and takse // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. -func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, err error) { +func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, gasUsed GasUsed, err error) { // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig()) @@ -248,37 +248,30 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas, GasUsed{}, ErrDepth } syscall := isSystemCall(caller) // Fail if we're trying to transfer more than the available balance. if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, gas, ErrInsufficientBalance + return nil, gas, GasUsed{}, ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { - // Add proof of absence to witness - // At this point, the read costs have already been charged, either because this - // is a direct tx call, in which case it's covered by the intrinsic gas, or because - // of a CALL instruction, in which case BASIC_DATA has been added to the access - // list in write mode. If there is enough gas paying for the addition of the code - // hash leaf to the access list, then account creation will proceed unimpaired. - // Thus, only pay for the creation of the code hash leaf here. wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false) if gas.RegularGas < wgas { evm.StateDB.RevertToSnapshot(snapshot) - return nil, GasCosts{}, ErrOutOfGas + return nil, GasCosts{}, GasUsed{}, ErrOutOfGas } gas.RegularGas -= wgas } if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non-existing account, don't do anything. - return nil, gas, nil + return nil, gas, GasUsed{}, nil } evm.StateDB.CreateAccount(addr) } @@ -294,7 +287,9 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if evm.chainRules.IsAmsterdam { stateDB = evm.StateDB } + gasBefore := gas.RegularGas ret, gas.RegularGas, err = RunPrecompiledContract(stateDB, p, addr, input, gas.RegularGas, evm.Config.Tracer) + gasUsed.RegularGasUsed += gasBefore - gas.RegularGas } else { // Initialise a new contract and set the code that is to be used by the EVM. code := evm.resolveCode(addr) @@ -307,6 +302,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g contract.SetCallCode(evm.resolveCodeHash(addr), code) ret, err = evm.Run(contract, input, false) gas = contract.Gas + gasUsed = contract.GasUsed } } // When an error was returned by the EVM or when setting the creation code @@ -319,10 +315,11 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + gasUsed.RegularGasUsed += gas.RegularGas gas.RegularGas = 0 } } - return ret, gas, err + return ret, gas, gasUsed, err } // CallCode executes the contract associated with the addr with the given input @@ -332,7 +329,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // // CallCode differs from Call in the sense that it executes the given address' // code with the caller as context. -func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, err error) { +func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, gasUsed GasUsed, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig()) @@ -342,14 +339,14 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas, GasUsed{}, ErrDepth } // Fail if we're trying to transfer more than the available balance // Note although it's noop to transfer X ether to caller itself. But // if caller doesn't have enough balance, it would be an error to allow // over-charging itself. So the check here is necessary. if !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, gas, ErrInsufficientBalance + return nil, gas, GasUsed{}, ErrInsufficientBalance } var snapshot = evm.StateDB.Snapshot() @@ -359,7 +356,9 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if evm.chainRules.IsAmsterdam { stateDB = evm.StateDB } + gasBefore := gas.RegularGas ret, gas.RegularGas, err = RunPrecompiledContract(stateDB, p, addr, input, gas.RegularGas, evm.Config.Tracer) + gasUsed.RegularGasUsed += gasBefore - gas.RegularGas } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -367,6 +366,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) gas = contract.Gas + gasUsed = contract.GasUsed } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -375,10 +375,11 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + gasUsed.RegularGasUsed += gas.RegularGas gas.RegularGas = 0 } } - return ret, gas, err + return ret, gas, gasUsed, err } // DelegateCall executes the contract associated with the addr with the given input @@ -386,7 +387,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // // DelegateCall differs from CallCode in the sense that it executes the given address' // code with the caller as context and the caller is set to the caller of the caller. -func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, err error) { +func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas GasCosts, value *uint256.Int) (ret []byte, leftOverGas GasCosts, gasUsed GasUsed, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { // DELEGATECALL inherits value from parent call @@ -397,7 +398,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas, GasUsed{}, ErrDepth } var snapshot = evm.StateDB.Snapshot() @@ -407,7 +408,9 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.chainRules.IsAmsterdam { stateDB = evm.StateDB } + gasBefore := gas.RegularGas ret, gas.RegularGas, err = RunPrecompiledContract(stateDB, p, addr, input, gas.RegularGas, evm.Config.Tracer) + gasUsed.RegularGasUsed += gasBefore - gas.RegularGas } else { // Initialise a new contract and make initialise the delegate values // @@ -416,6 +419,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) ret, err = evm.Run(contract, input, false) gas = contract.Gas + gasUsed = contract.GasUsed } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -424,17 +428,18 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + gasUsed.RegularGasUsed += gas.RegularGas gas.RegularGas = 0 } } - return ret, gas, err + return ret, gas, gasUsed, err } // StaticCall executes the contract associated with the addr with the given input // as parameters while disallowing any modifications to the state during the call. // Opcodes that attempt to perform such modifications will result in exceptions // instead of performing the modifications. -func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasCosts) (ret []byte, leftOverGas GasCosts, err error) { +func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas GasCosts) (ret []byte, leftOverGas GasCosts, gasUsed GasUsed, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil) @@ -444,7 +449,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b } // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { - return nil, gas, ErrDepth + return nil, gas, GasUsed{}, ErrDepth } // We take a snapshot here. This is a bit counter-intuitive, and could probably be skipped. // However, even a staticcall is considered a 'touch'. On mainnet, static calls were introduced @@ -464,7 +469,9 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if evm.chainRules.IsAmsterdam { stateDB = evm.StateDB } + gasBefore := gas.RegularGas ret, gas.RegularGas, err = RunPrecompiledContract(stateDB, p, addr, input, gas.RegularGas, evm.Config.Tracer) + gasUsed.RegularGasUsed += gasBefore - gas.RegularGas } else { // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. @@ -476,6 +483,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // when we're in Homestead this also counts for code storage gas errors. ret, err = evm.Run(contract, input, true) gas = contract.Gas + gasUsed = contract.GasUsed } if err != nil { evm.StateDB.RevertToSnapshot(snapshot) @@ -484,14 +492,15 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + gasUsed.RegularGasUsed += gas.RegularGas gas.RegularGas = 0 } } - return ret, gas, err + return ret, gas, gasUsed, err } // create creates a new contract using code as deployment code. -func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasCosts, err error) { +func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas GasCosts, gasUsed GasUsed, err error) { // Depth check execution. Fail if we're trying to execute above the // limit. var nonce uint64 @@ -516,13 +525,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value * }(gas) } if err != nil { - return nil, common.Address{}, gas, err + return nil, common.Address{}, gas, GasUsed{}, err } // Charge the contract creation init gas in verkle mode if evm.chainRules.IsEIP4762 { statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas) if statelessGas > gas.RegularGas { - return nil, common.Address{}, GasCosts{}, ErrOutOfGas + return nil, common.Address{}, GasCosts{}, GasUsed{}, ErrOutOfGas } if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, gas.RegularGas-statelessGas, tracing.GasChangeWitnessContractCollisionCheck) @@ -550,7 +559,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value * evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } gas.RegularGas = 0 - return nil, common.Address{}, gas, ErrContractAddressCollision + return nil, common.Address{}, gas, GasUsed{}, ErrContractAddressCollision } // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent @@ -573,7 +582,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value * if evm.chainRules.IsEIP4762 { consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas) if consumed < wanted { - return nil, common.Address{}, GasCosts{}, ErrOutOfGas + return nil, common.Address{}, GasCosts{}, GasUsed{}, ErrOutOfGas } if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas.RegularGas, gas.RegularGas-consumed, tracing.GasChangeWitnessContractInit) @@ -601,10 +610,11 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasCosts, value * if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(contract.Gas.RegularGas, 0, tracing.GasChangeCallFailedExecution) } + contract.GasUsed.RegularGasUsed += contract.Gas.RegularGas contract.Gas.RegularGas = 0 } } - return ret, address, contract.Gas, err + return ret, address, contract.Gas, contract.GasUsed, err } // initNewContract runs a new contract's creation code, performs checks on the @@ -661,7 +671,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b } // Create creates a new contract using code as deployment code. -func (evm *EVM) Create(caller common.Address, code []byte, gas GasCosts, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasCosts, err error) { +func (evm *EVM) Create(caller common.Address, code []byte, gas GasCosts, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasCosts, gasUsed GasUsed, err error) { contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller)) return evm.create(caller, code, gas, value, contractAddr, CREATE) } @@ -670,7 +680,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasCosts, value * // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. -func (evm *EVM) Create2(caller common.Address, code []byte, gas GasCosts, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasCosts, err error) { +func (evm *EVM) Create2(caller common.Address, code []byte, gas GasCosts, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas GasCosts, gasUsed GasUsed, err error) { inithash := crypto.Keccak256Hash(code) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 570c957fbd..399c1ed595 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -710,6 +710,7 @@ func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo if contract.Gas.Underflow(stateGas) { return GasCosts{}, errors.New("out of gas for state gas") } + contract.GasUsed.Add(stateGas) contract.Gas.Sub(stateGas) return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil } diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index cb932899d7..129d93bcfb 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -98,7 +98,7 @@ func TestEIP2200(t *testing.T) { } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) - _, gas, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: tt.gaspool}, new(uint256.Int)) + _, gas, _, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: tt.gaspool}, new(uint256.Int)) if !errors.Is(err, tt.failure) { t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure) } @@ -154,7 +154,7 @@ func TestCreateGas(t *testing.T) { evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, config) var startGas = uint64(testGas) - ret, gas, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: startGas}, new(uint256.Int)) + ret, gas, _, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: startGas}, new(uint256.Int)) if err != nil { return false } diff --git a/core/vm/gascosts.go b/core/vm/gascosts.go index 32f4455b36..6dbeeb103d 100644 --- a/core/vm/gascosts.go +++ b/core/vm/gascosts.go @@ -5,11 +5,24 @@ import "fmt" type GasCosts struct { RegularGas uint64 StateGas uint64 +} - // StateGasCharged tracks the cumulative state gas charged during execution. +// GasUsed tracks per-frame gas usage metrics for EIP-8037 2D block gas accounting. +type GasUsed struct { + // RegularGasUsed accumulates all gas charged via charge_gas() in the spec: + // State-gas spillover (when StateGas is exhausted and the excess + // is paid from RegularGas) does NOT increment RegularGasUsed. + RegularGasUsed uint64 + // StateGasCharged accumulates all gas charged via charge_state_gas() + // On child error the charged state gas is restored to the parent's state gas reservoir. StateGasCharged uint64 } +func (g *GasUsed) Add(cost GasCosts) { + g.RegularGasUsed += cost.RegularGas + g.StateGasCharged += cost.StateGas +} + func (g GasCosts) Max() uint64 { return max(g.RegularGas, g.StateGas) } @@ -38,11 +51,11 @@ func (g GasCosts) Underflow(b GasCosts) bool { // Sub doesn't check for underflows func (g *GasCosts) Sub(b GasCosts) { g.RegularGas -= b.RegularGas - g.StateGasCharged += b.StateGas if b.StateGas > g.StateGas { diff := b.StateGas - g.StateGas g.StateGas = 0 g.RegularGas -= diff + // Note: spillover does NOT increment RegularGasUsed, matching the spec } else { g.StateGas -= b.StateGas } @@ -52,7 +65,6 @@ func (g *GasCosts) Sub(b GasCosts) { func (g *GasCosts) Add(b GasCosts) { g.RegularGas += b.RegularGas g.StateGas += b.StateGas - g.StateGasCharged += b.StateGasCharged } func (g GasCosts) String() string { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 1092f779f6..27ec026ae2 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -674,8 +674,10 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue := size scope.Contract.UseGas(GasCosts{RegularGas: gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation) + // UseGas inflates RegularGasUsed, so we need to undo that here. + scope.Contract.GasUsed.RegularGasUsed -= gas.RegularGas - res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, GasCosts{RegularGas: gas.RegularGas, StateGas: scope.Contract.Gas.StateGas}, &value) + res, addr, returnGas, childGasUsed, suberr := evm.Create(scope.Contract.Address(), input, GasCosts{RegularGas: gas.RegularGas, StateGas: scope.Contract.Gas.StateGas}, &value) // Push item on the stack based on the returned error. If the ruleset is // homestead we must check for CodeStoreOutOfGasError (homestead only // rule) and treat as an error, if the ruleset is frontier we must @@ -689,7 +691,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(suberr, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(suberr, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -724,9 +726,11 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Contract.UseGas(GasCosts{RegularGas: gas.RegularGas}, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + // UseGas inflates RegularGasUsed, so we need to undo that here. + scope.Contract.GasUsed.RegularGasUsed -= gas.RegularGas // reuse size int for stackvalue stackvalue := size - res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, GasCosts{RegularGas: gas.RegularGas, StateGas: scope.Contract.Gas.StateGas}, + res, addr, returnGas, childGasUsed, suberr := evm.Create2(scope.Contract.Address(), input, GasCosts{RegularGas: gas.RegularGas, StateGas: scope.Contract.Gas.StateGas}, &endowment, &salt) // Push item on the stack based on the returned error. if suberr != nil { @@ -736,7 +740,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(suberr, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(suberr, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -764,7 +768,10 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } - ret, returnGas, err := evm.Call(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}, &value) + // We need to substract the gas here, otherwise its double counted by UseGas + scope.Contract.GasUsed.RegularGasUsed -= gas + + ret, returnGas, childGasUsed, err := evm.Call(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}, &value) if err != nil { temp.Clear() @@ -776,7 +783,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(err, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(err, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -797,8 +804,10 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if !value.IsZero() { gas += params.CallStipend } + // We need to substract the gas here, otherwise its double counted by UseGas + scope.Contract.GasUsed.RegularGasUsed -= gas - ret, returnGas, err := evm.CallCode(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}, &value) + ret, returnGas, childGasUsed, err := evm.CallCode(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}, &value) if err != nil { temp.Clear() } else { @@ -809,7 +818,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(err, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(err, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -827,8 +836,10 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - stateGas := scope.Contract.Gas.StateGas - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: stateGas}, scope.Contract.value) + // We need to substract the gas here, otherwise its double counted by UseGas + scope.Contract.GasUsed.RegularGasUsed -= gas + + ret, returnGas, childGasUsed, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}, scope.Contract.value) if err != nil { temp.Clear() } else { @@ -839,7 +850,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(err, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(err, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -857,8 +868,10 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - stateGas := scope.Contract.Gas.StateGas - ret, returnGas, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: stateGas}) + // We need to substract the gas here, otherwise its double counted by UseGas + scope.Contract.GasUsed.RegularGasUsed -= gas + + ret, returnGas, childGasUsed, err := evm.StaticCall(scope.Contract.Address(), toAddr, args, GasCosts{RegularGas: gas, StateGas: scope.Contract.Gas.StateGas}) if err != nil { temp.Clear() } else { @@ -869,7 +882,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(err, returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + scope.Contract.RefundGas(err, returnGas, childGasUsed, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index f2b37c8ce3..66d567cfeb 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -196,6 +196,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return nil, ErrOutOfGas } else { contract.Gas.RegularGas -= cost + contract.GasUsed.RegularGasUsed += cost // EIP-8037: track constant gas } // All ops with a dynamic memory usage also has a dynamic gas cost. @@ -228,6 +229,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte if contract.Gas.Underflow(dynamicCost) { return nil, ErrOutOfGas } else { + contract.GasUsed.Add(dynamicCost) contract.Gas.Sub(dynamicCost) } } diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 1c27b2c752..e92d8da3bf 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -55,7 +55,7 @@ func TestLoopInterrupt(t *testing.T) { timeout := make(chan bool) go func(evm *EVM) { - _, _, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: math.MaxUint64}, new(uint256.Int)) + _, _, _, err := evm.Call(common.Address{}, address, nil, GasCosts{RegularGas: math.MaxUint64}, new(uint256.Int)) errChannel <- err }(evm) diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index f087d93322..2c48db4061 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -307,6 +307,7 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun if contract.Gas.Underflow(stateGasCost) { return GasCosts{}, ErrOutOfGas } + contract.GasUsed.Add(stateGasCost) contract.Gas.Sub(stateGasCost) } @@ -327,10 +328,15 @@ func makeCallVariantGasCall(oldCalculatorStateful, oldCalculatorStateless gasFun if overflow { return GasCosts{}, ErrGasUintOverflow } + // EIP-8037: undo the RegularGasUsed increment from the direct UseGas + // charge, since this gas will be re-charged via the returned cost. + contract.GasUsed.RegularGasUsed -= eip2929Gas + contract.Gas.RegularGas, overflow = math.SafeAdd(contract.Gas.RegularGas, eip7702Gas) if overflow { return GasCosts{}, ErrGasUintOverflow } + contract.GasUsed.RegularGasUsed -= eip7702Gas var totalCost uint64 totalCost, overflow = math.SafeAdd(eip2929Gas, eip7702Gas) diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 49a7e809da..4a99ed4e39 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -142,7 +142,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { // set the receiver's (the executing contract) code for execution. cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, leftOverGas, _, err := vmenv.Call( cfg.Origin, common.BytesToAddress([]byte("contract")), input, @@ -177,7 +177,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { // - reset transient storage(eip 1153) cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - code, address, leftOverGas, err := vmenv.Create( + code, address, leftOverGas, _, err := vmenv.Create( cfg.Origin, input, vm.GasCosts{RegularGas: cfg.GasLimit}, @@ -211,7 +211,7 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er statedb.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, leftOverGas, _, err := vmenv.Call( cfg.Origin, address, input, diff --git a/tests/state_test.go b/tests/state_test.go index a1b71e95da..0ff343973c 100644 --- a/tests/state_test.go +++ b/tests/state_test.go @@ -316,7 +316,7 @@ func runBenchmark(b *testing.B, t *StateTest) { start := time.Now() // Execute the message. - _, leftOverGas, err := evm.Call(sender.Address(), *msg.To, msg.Data, vm.GasCosts{RegularGas: msg.GasLimit}, uint256.MustFromBig(msg.Value)) + _, leftOverGas, _, err := evm.Call(sender.Address(), *msg.To, msg.Data, vm.GasCosts{RegularGas: msg.GasLimit}, uint256.MustFromBig(msg.Value)) if err != nil { b.Error(err) return