From 63bde1e5c491e037134e8a6041fa226fa097d7d3 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Tue, 15 Jul 2025 09:37:39 -0300 Subject: [PATCH] Return multi-gas in execution result This PR adds up the dynamic and constant opcode multi-gas cost in the EVM interpreter, and then return it in the ExecutionResult struct. Close NIT-3468 --- core/state_processor.go | 6 +++--- core/state_transition.go | 14 ++++++++++++-- core/vm/contract.go | 15 ++++++++++----- core/vm/evm.go | 16 ++++++++++------ core/vm/gas_table_test.go | 4 ++-- core/vm/instructions.go | 2 +- core/vm/interpreter.go | 6 +++++- core/vm/interpreter_test.go | 2 +- core/vm/runtime/runtime.go | 4 ++-- tests/state_test.go | 2 +- 10 files changed, 47 insertions(+), 24 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 7d5658256d..71fbfbca8f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -250,7 +250,7 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) - _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) + _, _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) evm.StateDB.Finalise(true) } @@ -274,7 +274,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { } evm.SetTxContext(NewEVMTxContext(msg)) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) - _, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) + _, _, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) if err != nil { panic(err) } @@ -313,7 +313,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, 30_000_000, common.U2560) + ret, _, _, err := evm.Call(msg.From, *msg.To, msg.Data, 30_000_000, common.U2560) evm.StateDB.Finalise(true) if err != nil { return fmt.Errorf("system call failed to execute: %v", err) diff --git a/core/state_transition.go b/core/state_transition.go index b020ec1672..904e8caf3c 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -22,6 +22,7 @@ import ( "math" "math/big" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/tracing" @@ -44,6 +45,8 @@ type ExecutionResult struct { ScheduledTxes types.Transactions // Arbitrum: the contract deployed from the top-level transaction, or nil if not a contract creation tx TopLevelDeployed *common.Address + // Arbitrum: total used multi-dimensional gas + UsedMultiGas *multigas.MultiGas } // Unwrap returns the internal evm error which allows us for further @@ -554,6 +557,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { Err: err, ReturnData: returnData, ScheduledTxes: st.evm.ProcessingHook.ScheduledTxes(), + UsedMultiGas: multigas.ZeroGas(), }, nil } @@ -583,6 +587,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil, st.evm.Context.Time, st.evm.Context.ArbOSVersion) contractCreation = msg.To == nil floorDataGas uint64 + usedMultiGas = multigas.ZeroGas() ) // Check clauses 4-5, subtract intrinsic gas if everything is correct @@ -671,7 +676,7 @@ 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, usedMultiGas, 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 @@ -679,7 +684,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { peakGasUsed := st.gasUsed() // Compute refund counter, capped to a refund quotient. - st.gasRemaining += st.calcRefund() + refund := st.calcRefund() + st.gasRemaining += refund if rules.IsPrague && st.evm.ProcessingHook.IsCalldataPricingIncreaseEnabled() { // After EIP-7623: Data-heavy transactions pay the floor gas. if st.gasUsed() < floorDataGas { @@ -695,6 +701,9 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } st.returnGas() + // Arbitrum: set the multigas refunds + usedMultiGas.SetRefund(refund) + effectiveTip := msg.GasPrice if rules.IsLondon { effectiveTip = new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee) @@ -742,6 +751,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ReturnData: ret, ScheduledTxes: st.evm.ProcessingHook.ScheduledTxes(), TopLevelDeployed: deployedContract, + UsedMultiGas: usedMultiGas, }, nil } diff --git a/core/vm/contract.go b/core/vm/contract.go index 3dc21f1d10..71fe47ce1d 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -17,6 +17,7 @@ package vm import ( + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/holiman/uint256" @@ -47,6 +48,9 @@ type Contract struct { Gas uint64 value *uint256.Int + + // Arbitrum: total used multi-dimensional gas + UsedMultiGas *multigas.MultiGas } // NewContract returns a new contract environment for the execution of EVM. @@ -56,11 +60,12 @@ func NewContract(caller common.Address, address common.Address, value *uint256.I jumpDests = make(map[common.Hash]bitvec) } return &Contract{ - caller: caller, - address: address, - jumpdests: jumpDests, - Gas: gas, - value: value, + caller: caller, + address: address, + jumpdests: jumpDests, + Gas: gas, + value: value, + UsedMultiGas: multigas.ZeroGas(), } } diff --git a/core/vm/evm.go b/core/vm/evm.go index cc4e3790d2..ea4ecc8f8d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -23,6 +23,7 @@ import ( "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" @@ -198,7 +199,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 uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { +func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, usedMultiGas *multigas.MultiGas, 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()) @@ -208,14 +209,15 @@ 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, multigas.ZeroGas(), ErrDepth } // Fail if we're trying to transfer more than the available balance if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { - return nil, gas, ErrInsufficientBalance + return nil, gas, multigas.ZeroGas(), ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) + usedMultiGas = multigas.ZeroGas() if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { @@ -223,14 +225,14 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g wgas := evm.AccessEvents.AddAccount(addr, false) if gas < wgas { evm.StateDB.RevertToSnapshot(snapshot) - return nil, 0, ErrOutOfGas + return nil, 0, multigas.ZeroGas(), ErrOutOfGas } gas -= wgas } if !isPrecompile && evm.chainRules.IsEIP158 && value.IsZero() { // Calling a non-existing account, don't do anything. - return nil, gas, nil + return nil, gas, usedMultiGas, nil } evm.StateDB.CreateAccount(addr) } @@ -258,6 +260,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g contract.SetCallCode(evm.resolveCodeHash(addr), code) ret, err = evm.interpreter.Run(contract, input, false) gas = contract.Gas + usedMultiGas, _ = usedMultiGas.SafeAdd(usedMultiGas, contract.UsedMultiGas) } } // When an error was returned by the EVM or when setting the creation code @@ -270,13 +273,14 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) } + usedMultiGas.SafeIncrement(multigas.ResourceKindComputation, gas) gas = 0 } // TODO: consider clearing up unused snapshots: //} else { // evm.StateDB.DiscardSnapshot(snapshot) } - return ret, gas, err + return ret, gas, usedMultiGas, err } // CallCode executes the contract associated with the addr with the given input diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go index 9699580763..f3028da4e0 100644 --- a/core/vm/gas_table_test.go +++ b/core/vm/gas_table_test.go @@ -97,7 +97,7 @@ func TestEIP2200(t *testing.T) { } evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}}) - _, gas, err := evm.Call(common.Address{}, address, nil, tt.gaspool, new(uint256.Int)) + _, gas, _, err := evm.Call(common.Address{}, address, nil, 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) } @@ -153,7 +153,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, startGas, new(uint256.Int)) + ret, gas, _, err := evm.Call(common.Address{}, address, nil, startGas, new(uint256.Int)) if err != nil { return false } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 55f2152937..eb993be917 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -770,7 +770,7 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt if !value.IsZero() { gas += params.CallStipend } - ret, returnGas, err := interpreter.evm.Call(scope.Contract.Address(), toAddr, args, gas, &value) + ret, returnGas, _, err := interpreter.evm.Call(scope.Contract.Address(), toAddr, args, gas, &value) if err != nil { temp.Clear() diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 0e5bef5313..1e40446a35 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -19,6 +19,7 @@ package vm import ( "fmt" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/state" @@ -267,6 +268,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } else { contract.Gas -= cost } + contract.UsedMultiGas.SafeIncrement(multigas.ResourceKindComputation, cost) // All ops with a dynamic memory usage also has a dynamic gas cost. var memorySize uint64 @@ -289,7 +291,8 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // Consume the gas and return an error if not enough gas is available. // cost is explicitly set so that the capture state defer method can get the proper cost var dynamicCost uint64 - _, dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize) + var multigasDynamicCost *multigas.MultiGas + multigasDynamicCost, dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize) cost += dynamicCost // for tracing if err != nil { return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err) @@ -300,6 +303,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( } else { contract.Gas -= dynamicCost } + contract.UsedMultiGas, _ = contract.UsedMultiGas.SafeAdd(contract.UsedMultiGas, multigasDynamicCost) } // Do tracing before potential memory expansion diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go index 0b93dd59e7..f6a1718ff5 100644 --- a/core/vm/interpreter_test.go +++ b/core/vm/interpreter_test.go @@ -53,7 +53,7 @@ func TestLoopInterrupt(t *testing.T) { timeout := make(chan bool) go func(evm *EVM) { - _, _, err := evm.Call(common.Address{}, address, nil, math.MaxUint64, new(uint256.Int)) + _, _, _, err := evm.Call(common.Address{}, address, nil, math.MaxUint64, new(uint256.Int)) errChannel <- err }(evm) diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index aa9a1efe1c..536d525fb0 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -141,7 +141,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) // Call the code with the given configuration. - ret, leftOverGas, err := vmenv.Call( + ret, leftOverGas, _, err := vmenv.Call( cfg.Origin, common.BytesToAddress([]byte("contract")), input, @@ -210,7 +210,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 ca6b8c7fdc..7fef0e30ef 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, msg.GasLimit, uint256.MustFromBig(msg.Value)) + _, leftOverGas, _, err := evm.Call(sender.Address(), *msg.To, msg.Data, msg.GasLimit, uint256.MustFromBig(msg.Value)) if err != nil { b.Error(err) return