diff --git a/core/vm/contract.go b/core/vm/contract.go index 165ca833f88..0a4fd7bb6c1 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -18,7 +18,6 @@ package vm import ( "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/holiman/uint256" ) @@ -125,29 +124,6 @@ func (c *Contract) Caller() common.Address { return c.caller } -// UseGas attempts the use gas and subtracts it and returns true on success -func (c *Contract) UseGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) (ok bool) { - if c.Gas < gas { - return false - } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(c.Gas, c.Gas-gas, reason) - } - c.Gas -= gas - return true -} - -// RefundGas refunds gas to the contract -func (c *Contract) RefundGas(gas uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) { - if gas == 0 { - return - } - if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored { - logger.OnGasChange(c.Gas, c.Gas+gas, reason) - } - c.Gas += gas -} - // Address returns the contracts address func (c *Contract) Address() common.Address { return c.address diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 21307ff5ace..a033b5bf6ec 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -257,14 +257,12 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - the returned bytes, // - the _remaining_ gas, // - any error that occurred -func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { +func RunPrecompiledContract[TS TracingSwitch](p PrecompiledContract, input []byte, suppliedGas uint64, tracer tracer[TS]) (ret []byte, remainingGas uint64, err error) { gasCost := p.RequiredGas(input) if suppliedGas < gasCost { return nil, 0, ErrOutOfGas } - if logger != nil && logger.OnGasChange != nil { - logger.OnGasChange(suppliedGas, suppliedGas-gasCost, tracing.GasChangeCallPrecompiledContract) - } + tracer.OnGasChange(suppliedGas, suppliedGas-gasCost, tracing.GasChangeCallPrecompiledContract) suppliedGas -= gasCost output, err := p.Run(input) return output, suppliedGas, err diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 1e5cc800747..7b10423389f 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -36,7 +36,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(p, input, gas, nil) + RunPrecompiledContract(p, input, gas, NewTracer[TracingDisabled](nil)) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index da44e250e1e..c4e1ef1548b 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -99,7 +99,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(p, in, gas, nil); err != nil { + if res, _, err := RunPrecompiledContract(p, in, gas, NewTracer[TracingDisabled](nil)); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -121,7 +121,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := p.RequiredGas(in) - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(p, in, gas, NewTracer[TracingDisabled](nil)) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -138,7 +138,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(p, in, gas, NewTracer[TracingDisabled](nil)) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -170,7 +170,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { bench.ResetTimer() for i := 0; i < bench.N; i++ { copy(data, in) - res, _, err = RunPrecompiledContract(p, data, reqGas, nil) + res, _, err = RunPrecompiledContract(p, data, reqGas, NewTracer[TracingDisabled](nil)) } bench.StopTimer() elapsed := uint64(time.Since(start)) diff --git a/core/vm/eips.go b/core/vm/eips.go index 10ca1fe9abb..138919a3f62 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -358,7 +358,7 @@ func opExtCodeCopyEIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, er code := evm.StateDB.GetCode(addr) paddedCodeCopy, copyOffset, nonPaddedCopyLength := getDataAndAdjustedBounds(code, uint64CodeOffset, length.Uint64()) consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(addr, copyOffset, nonPaddedCopyLength, uint64(len(code)), false, scope.Contract.Gas) - scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) + evm.UseGas(scope.Contract, consumed, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -384,7 +384,7 @@ func opPush1EIP4762(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // advanced past this boundary. contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, *pc+1, uint64(1), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(wanted, evm.Config.Tracer, tracing.GasChangeUnspecified) + evm.UseGas(scope.Contract, wanted, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } @@ -412,7 +412,7 @@ func makePushEIP4762(size uint64, pushByteSize int) executionFunc { if !scope.Contract.IsDeployment && !scope.Contract.IsSystemCall { contractAddr := scope.Contract.Address() consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(contractAddr, uint64(start), uint64(pushByteSize), uint64(len(scope.Contract.Code)), false, scope.Contract.Gas) - scope.Contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeUnspecified) + evm.UseGas(scope.Contract, consumed, tracing.GasChangeUnspecified) if consumed < wanted { return nil, ErrOutOfGas } diff --git a/core/vm/evm.go b/core/vm/evm.go index e360187f7b2..75cde24a15d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -125,6 +125,9 @@ type EVM struct { // jumpDests stores results of JUMPDEST analysis. jumpDests JumpDestCache + // tracer provides a nil-safe access to a tracing.Hooks + tracer tracer[TracingEnabled] + hasher crypto.KeccakState // Keccak256 hasher instance shared across opcodes hasherBuf common.Hash // Keccak256 hasher result array shared across opcodes @@ -145,6 +148,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), jumpDests: newMapJumpDests(), hasher: crypto.NewKeccakState(), + tracer: NewTracer[TracingEnabled](config.Tracer), } evm.precompiles = activePrecompiledContracts(evm.chainRules) @@ -240,12 +244,10 @@ func isSystemCall(caller common.Address) bool { // 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) { // 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()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) - }(gas) - } + evm.captureBegin(evm.depth, CALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth @@ -283,7 +285,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.Context.Transfer(evm.StateDB, caller, addr, value) if isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.tracer) } else { // Initialise a new contract and set the code that is to be used by the EVM. code := evm.resolveCode(addr) @@ -304,9 +306,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) - } + evm.tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) gas = 0 } @@ -326,12 +326,10 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // code with the caller as context. func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, 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()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) - }(gas) - } + evm.captureBegin(evm.depth, CALLCODE, caller, addr, input, gas, value.ToBig()) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth @@ -347,7 +345,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.tracer) } 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. @@ -359,9 +357,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) - } + evm.tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) gas = 0 } } @@ -375,13 +371,11 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // 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 uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame - if evm.Config.Tracer != nil { - // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) - }(gas) - } + // DELEGATECALL inherits value from parent call + evm.captureBegin(evm.depth, DELEGATECALL, caller, addr, input, gas, value.ToBig()) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth @@ -390,7 +384,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.tracer) } else { // Initialise a new contract and make initialise the delegate values // @@ -403,9 +397,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) - } + evm.tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) gas = 0 } } @@ -418,12 +410,10 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // instead of performing the modifications. func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, 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) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) - }(gas) - } + evm.captureBegin(evm.depth, STATICCALL, caller, addr, input, gas, nil) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) // Fail if we're trying to execute above the call depth limit if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth @@ -442,7 +432,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b evm.StateDB.AddBalance(addr, new(uint256.Int), tracing.BalanceChangeTouchAccount) if p, isPrecompile := evm.precompile(addr); isPrecompile { - ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, gas, err = RunPrecompiledContract(p, input, gas, evm.tracer) } 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. @@ -458,9 +448,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b if err != nil { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) - } + evm.tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) gas = 0 } @@ -470,12 +458,10 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // create creates a new contract using code as deployment code. func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, leftOverGas uint64, err error) { - if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) - defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) - }(gas) - } + evm.captureBegin(evm.depth, typ, caller, address, code, gas, value.ToBig()) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { @@ -496,9 +482,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui if statelessGas > gas { return nil, common.Address{}, 0, ErrOutOfGas } - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, gas-statelessGas, tracing.GasChangeWitnessContractCollisionCheck) - } + evm.tracer.OnGasChange(gas, gas-statelessGas, tracing.GasChangeWitnessContractCollisionCheck) gas = gas - statelessGas } @@ -517,9 +501,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code (storageRoot != (common.Hash{}) && storageRoot != types.EmptyRootHash) { // non-empty storage - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) - } + evm.tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) return nil, common.Address{}, 0, ErrContractAddressCollision } // Create a new account on the state only if the object was not present. @@ -544,9 +526,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui if consumed < wanted { return nil, common.Address{}, 0, ErrOutOfGas } - if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gas, gas-consumed, tracing.GasChangeWitnessContractInit) - } + evm.tracer.OnGasChange(gas, gas-consumed, tracing.GasChangeWitnessContractInit) gas = gas - consumed } evm.Context.Transfer(evm.StateDB, caller, address, value) @@ -564,7 +544,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { - contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) + evm.UseGas(contract, contract.Gas, tracing.GasChangeCallFailedExecution) } } return ret, address, contract.Gas, err @@ -590,12 +570,12 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b if !evm.chainRules.IsEIP4762 { createDataGas := uint64(len(ret)) * params.CreateDataGas - if !contract.UseGas(createDataGas, evm.Config.Tracer, tracing.GasChangeCallCodeStorage) { + if !evm.UseGas(contract, createDataGas, tracing.GasChangeCallCodeStorage) { return ret, ErrCodeStoreOutOfGas } } else { consumed, wanted := evm.AccessEvents.CodeChunksRangeGas(address, 0, uint64(len(ret)), uint64(len(ret)), true, contract.Gas) - contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + evm.UseGas(contract, consumed, tracing.GasChangeWitnessCodeChunk) if len(ret) > 0 && (consumed < wanted) { return ret, ErrCodeStoreOutOfGas } @@ -654,19 +634,13 @@ func (evm *EVM) resolveCodeHash(addr common.Address) common.Hash { func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to common.Address, input []byte, startGas uint64, value *big.Int) { - tracer := evm.Config.Tracer - if tracer.OnEnter != nil { - tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) - } - if tracer.OnGasChange != nil { - tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance) - } + evm.tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value) + evm.tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance) } func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) { - tracer := evm.Config.Tracer - if leftOverGas != 0 && tracer.OnGasChange != nil { - tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned) + if leftOverGas != 0 { + evm.tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned) } var reverted bool if err != nil { @@ -675,9 +649,7 @@ func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret [ if !evm.chainRules.IsHomestead && errors.Is(err, ErrCodeStoreOutOfGas) { reverted = false } - if tracer.OnExit != nil { - tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted) - } + evm.tracer.OnExit(depth, ret, startGas-leftOverGas, VMErrorFromErr(err), reverted) } // GetVMContext provides context about the block being executed as well as state diff --git a/core/vm/instructions.go b/core/vm/instructions.go index fffa65fd6a9..d1a59d8160b 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -442,9 +442,7 @@ func opBlockhash(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { if witness := evm.StateDB.Witness(); witness != nil { witness.AddBlockHash(num64) } - if tracer := evm.Config.Tracer; tracer != nil && tracer.OnBlockHashRead != nil { - tracer.OnBlockHashRead(num64, res) - } + evm.tracer.OnBlockHashRead(num64, res) num.SetBytes(res[:]) } else { num.Clear() @@ -669,7 +667,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // reuse size int for stackvalue stackvalue := size - scope.Contract.UseGas(gas, evm.Config.Tracer, tracing.GasChangeCallContractCreation) + evm.UseGas(scope.Contract, gas, tracing.GasChangeCallContractCreation) res, addr, returnGas, suberr := evm.Create(scope.Contract.Address(), input, gas, &value) // Push item on the stack based on the returned error. If the ruleset is @@ -685,7 +683,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -709,7 +707,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Apply EIP150 gas -= gas / 64 - scope.Contract.UseGas(gas, evm.Config.Tracer, tracing.GasChangeCallContractCreation2) + evm.UseGas(scope.Contract, gas, tracing.GasChangeCallContractCreation2) // reuse size int for stackvalue stackvalue := size res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, gas, @@ -721,7 +719,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { stackvalue.SetBytes(addr.Bytes()) } scope.Stack.push(&stackvalue) - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) if suberr == ErrExecutionReverted { evm.returnData = res // set REVERT data to return data buffer @@ -761,7 +759,7 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -794,7 +792,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -823,7 +821,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -852,7 +850,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret) } - scope.Contract.RefundGas(returnGas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded) + evm.RefundGas(scope.Contract, returnGas, tracing.GasChangeCallLeftOverRefunded) evm.returnData = ret return ret, nil @@ -889,14 +887,8 @@ func opSelfdestruct(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { balance := evm.StateDB.GetBalance(scope.Contract.Address()) evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct) evm.StateDB.SelfDestruct(scope.Contract.Address()) - if tracer := evm.Config.Tracer; tracer != nil { - if tracer.OnEnter != nil { - tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig()) - } - if tracer.OnExit != nil { - tracer.OnExit(evm.depth, []byte{}, 0, nil, false) - } - } + evm.tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig()) + evm.tracer.OnExit(evm.depth, []byte{}, 0, nil, false) return nil, errStopToken } @@ -909,14 +901,8 @@ func opSelfdestruct6780(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, erro evm.StateDB.SubBalance(scope.Contract.Address(), balance, tracing.BalanceDecreaseSelfdestruct) evm.StateDB.AddBalance(beneficiary.Bytes20(), balance, tracing.BalanceIncreaseSelfdestruct) evm.StateDB.SelfDestruct6780(scope.Contract.Address()) - if tracer := evm.Config.Tracer; tracer != nil { - if tracer.OnEnter != nil { - tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig()) - } - if tracer.OnExit != nil { - tracer.OnExit(evm.depth, []byte{}, 0, nil, false) - } - } + evm.tracer.OnEnter(evm.depth, byte(SELFDESTRUCT), scope.Contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance.ToBig()) + evm.tracer.OnExit(evm.depth, []byte{}, 0, nil, false) return nil, errStopToken } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index a0637a6800e..7a14545b28e 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -151,11 +151,11 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte if err == nil { return } - if !logged && evm.Config.Tracer.OnOpcode != nil { - evm.Config.Tracer.OnOpcode(pcCopy, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) + if !logged { + evm.tracer.OnOpcode(pcCopy, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) } - if logged && evm.Config.Tracer.OnFault != nil { - evm.Config.Tracer.OnFault(pcCopy, byte(op), gasCopy, cost, callContext, evm.depth, VMErrorFromErr(err)) + if logged { + evm.tracer.OnFault(pcCopy, byte(op), gasCopy, cost, callContext, evm.depth, VMErrorFromErr(err)) } }() } @@ -175,7 +175,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // associated costs. contractAddr := contract.Address() consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas) - contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk) + evm.UseGas(contract, consumed, tracing.GasChangeWitnessCodeChunk) if consumed < wanted { return nil, ErrOutOfGas } @@ -235,13 +235,9 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte // Do tracing before potential memory expansion if debug { - if evm.Config.Tracer.OnGasChange != nil { - evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode) - } - if evm.Config.Tracer.OnOpcode != nil { - evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) - logged = true - } + evm.tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode) + evm.tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err)) + logged = true } if memorySize > 0 { mem.Resize(memorySize) @@ -261,3 +257,26 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte return res, err } + +// UseGas attempts the use gas and subtracts it and returns true on success +func (evm *EVM) UseGas(c *Contract, gas uint64, reason tracing.GasChangeReason) (ok bool) { + if c.Gas < gas { + return false + } + if reason != tracing.GasChangeIgnored { + evm.tracer.OnGasChange(c.Gas, c.Gas-gas, reason) + } + c.Gas -= gas + return true +} + +// RefundGas refunds gas to the contract +func (evm *EVM) RefundGas(c *Contract, gas uint64, reason tracing.GasChangeReason) { + if gas == 0 { + return + } + if reason != tracing.GasChangeIgnored { + evm.tracer.OnGasChange(c.Gas, c.Gas+gas, reason) + } + c.Gas += gas +} diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 085b018e4c4..9de2f741c33 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -164,7 +164,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) g evm.StateDB.AddAddressToAccessList(addr) // Charge the remaining difference here already, to correctly calculate available // gas for call - if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !evm.UseGas(contract, coldCost, tracing.GasChangeCallStorageColdAccess) { return 0, ErrOutOfGas } } @@ -265,7 +265,7 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 // Charge the remaining difference here already, to correctly calculate available // gas for call - if !contract.UseGas(coldCost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !evm.UseGas(contract, coldCost, tracing.GasChangeCallStorageColdAccess) { return 0, ErrOutOfGas } total += coldCost @@ -280,7 +280,7 @@ func makeCallVariantGasCallEIP7702(oldCalculator gasFunc) gasFunc { evm.StateDB.AddAddressToAccessList(target) cost = params.ColdAccountAccessCostEIP2929 } - if !contract.UseGas(cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { + if !evm.UseGas(contract, cost, tracing.GasChangeCallStorageColdAccess) { return 0, ErrOutOfGas } total += cost diff --git a/core/vm/tracer.go b/core/vm/tracer.go new file mode 100644 index 00000000000..2ed7a6bf585 --- /dev/null +++ b/core/vm/tracer.go @@ -0,0 +1,188 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package vm + +import ( + "math/big" + "unsafe" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +const tracingEnabledSize = 1 + +type TracingEnabled byte +type TracingDisabled uint16 + +type TracingSwitch interface { + TracingEnabled | TracingDisabled +} + +// tracer is a wrapper that gives nil-safe access to a tracing.Hooks +// and also enables VM tracing to be disabled at compile time +type tracer[TS TracingSwitch] struct { + hooks tracing.Hooks +} + +func NewTracer[TS TracingSwitch](hooks *tracing.Hooks) tracer[TS] { + var t tracer[TS] + if hooks != nil { + t.hooks = *hooks + } + return t +} + +func (t *tracer[TS]) OnTxStart(vm *tracing.VMContext, tx *types.Transaction, from common.Address) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnTxStart != nil { + t.hooks.OnTxStart(vm, tx, from) + } +} + +func (t *tracer[TS]) OnTxEnd(receipt *types.Receipt, err error) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnTxEnd != nil { + t.hooks.OnTxEnd(receipt, err) + } +} + +func (t *tracer[TS]) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnEnter != nil { + t.hooks.OnEnter(depth, typ, from, to, input, gas, value) + } +} + +func (t *tracer[TS]) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnExit != nil { + t.hooks.OnExit(depth, output, gasUsed, err, reverted) + } +} + +func (t *tracer[TS]) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnOpcode != nil { + t.hooks.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) + } +} + +func (t *tracer[TS]) OnFault(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnFault != nil { + t.hooks.OnFault(pc, op, gas, cost, scope, depth, err) + } +} + +func (t *tracer[TS]) OnGasChange(oldGas, newGas uint64, reason tracing.GasChangeReason) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnGasChange != nil { + t.hooks.OnGasChange(oldGas, newGas, reason) + } +} + +func (t *tracer[TS]) OnBlockchainInit(chainConfig *params.ChainConfig) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnBlockchainInit != nil { + t.hooks.OnBlockchainInit(chainConfig) + } +} + +func (t *tracer[TS]) OnClose() { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnClose != nil { + t.hooks.OnClose() + } +} + +func (t *tracer[TS]) OnBlockStart(event tracing.BlockEvent) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnBlockStart != nil { + t.hooks.OnBlockStart(event) + } +} + +func (t *tracer[TS]) OnBlockEnd(err error) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnBlockEnd != nil { + t.hooks.OnBlockEnd(err) + } +} + +func (t *tracer[TS]) OnSkippedBlock(event tracing.BlockEvent) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnSkippedBlock != nil { + t.hooks.OnSkippedBlock(event) + } +} + +func (t *tracer[TS]) OnGenesisBlock(genesis *types.Block, alloc types.GenesisAlloc) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnGenesisBlock != nil { + t.hooks.OnGenesisBlock(genesis, alloc) + } +} + +func (t *tracer[TS]) OnSystemCallStart() { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnSystemCallStart != nil { + t.hooks.OnSystemCallStart() + } +} + +func (t *tracer[TS]) OnSystemCallStartV2(vm *tracing.VMContext) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnSystemCallStartV2 != nil { + t.hooks.OnSystemCallStartV2(vm) + } +} + +func (t *tracer[TS]) OnSystemCallEnd() { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnSystemCallEnd != nil { + t.hooks.OnSystemCallEnd() + } +} + +func (t *tracer[TS]) OnBalanceChange(addr common.Address, prevBalance, newBalance *big.Int, reason tracing.BalanceChangeReason) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnBalanceChange != nil { + t.hooks.OnBalanceChange(addr, prevBalance, newBalance, reason) + } +} + +func (t *tracer[TS]) OnNonceChange(addr common.Address, prevNonce, newNonce uint64) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnNonceChange != nil { + t.hooks.OnNonceChange(addr, prevNonce, newNonce) + } +} + +func (t *tracer[TS]) OnNonceChangeV2(addr common.Address, prevNonce, newNonce uint64, reason tracing.NonceChangeReason) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnNonceChangeV2 != nil { + t.hooks.OnNonceChangeV2(addr, prevNonce, newNonce, reason) + } +} + +func (t *tracer[TS]) OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnCodeChange != nil { + t.hooks.OnCodeChange(addr, prevCodeHash, prevCode, codeHash, code) + } +} + +func (t *tracer[TS]) OnStorageChange(addr common.Address, slot common.Hash, prevValue, newValue common.Hash) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnStorageChange != nil { + t.hooks.OnStorageChange(addr, slot, prevValue, newValue) + } +} + +func (t *tracer[TS]) OnLog(log *types.Log) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnLog != nil { + t.hooks.OnLog(log) + } +} + +func (t *tracer[TS]) OnBlockHashRead(blockNumber uint64, hash common.Hash) { + if unsafe.Sizeof(*new(TS)) == tracingEnabledSize && t.hooks.OnBlockHashRead != nil { + t.hooks.OnBlockHashRead(blockNumber, hash) + } +}