diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index a6c9c1a860e8..c235d0a78094 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -54,31 +54,40 @@ type evmCallArgs struct { } // A CallType refers to a *CALL* [OpCode] / respective method on [EVM]. -type CallType uint8 +type CallType OpCode const ( - UnknownCallType CallType = iota - Call - CallCode - DelegateCall - StaticCall + Call = CallType(CALL) + CallCode = CallType(CALLCODE) + DelegateCall = CallType(DELEGATECALL) + StaticCall = CallType(STATICCALL) ) +func (t CallType) isValid() bool { + switch t { + case Call, CallCode, DelegateCall, StaticCall: + return true + default: + return false + } +} + // String returns a human-readable representation of the CallType. func (t CallType) String() string { - switch t { - case Call: - return "Call" - case CallCode: - return "CallCode" - case DelegateCall: - return "DelegateCall" - case StaticCall: - return "StaticCall" + if t.isValid() { + return t.OpCode().String() } return fmt.Sprintf("Unknown %T(%d)", t, t) } +// OpCode returns t's equivalent OpCode. +func (t CallType) OpCode() OpCode { + if t.isValid() { + return OpCode(t) + } + return INVALID +} + // run runs the [PrecompiledContract], differentiating between stateful and // regular types. func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index 59c07bbf9a7a..8c451ff66c89 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -17,6 +17,7 @@ package vm_test import ( "bytes" + "encoding/json" "fmt" "math/big" "reflect" @@ -33,6 +34,8 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/eth/tracers" + _ "github.com/ava-labs/libevm/eth/tracers/native" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/ethtest" "github.com/ava-labs/libevm/libevm/hookstest" @@ -338,7 +341,7 @@ func TestInheritReadOnly(t *testing.T) { rng := ethtest.NewPseudoRand(42) contractAddr := rng.Address() state.CreateAccount(contractAddr) - state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract)) + state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract...)) // (3) @@ -404,7 +407,7 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC } contract = append(contract, vm.PUSH20) - contract = append(contract, convertBytes[byte, vm.OpCode](dest[:])...) + contract = append(contract, convertBytes[byte, vm.OpCode](dest[:]...)...) contract = append(contract, p0, // gas @@ -417,7 +420,7 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC return contract } -func convertBytes[From ~byte, To ~byte](buf []From) []To { +func convertBytes[From ~byte, To ~byte](buf ...From) []To { out := make([]To, len(buf)) for i, b := range buf { out[i] = To(b) @@ -672,7 +675,7 @@ func TestPrecompileMakeCall(t *testing.T) { evm.Origin = eoa state.CreateAccount(caller) proxy := makeReturnProxy(t, sut, tt.incomingCallType) - state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy)) + state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy...)) got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0)) require.NoError(t, err) @@ -681,6 +684,49 @@ func TestPrecompileMakeCall(t *testing.T) { } } +func TestPrecompileCallWithTracer(t *testing.T) { + // The native pre-state tracer, when logging storage, assumes an invariant + // that is broken by a precompile calling another contract. This is a test + // of the fix, ensuring that an SLOADed value is properly handled by the + // tracer. + + rng := ethtest.NewPseudoRand(42 * 142857) + precompile := rng.Address() + contract := rng.Address() + + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + return env.Call(contract, nil, suppliedGas, uint256.NewInt(0)) + }), + }, + } + hooks.Register(t) + + state, evm := ethtest.NewZeroEVM(t) + evm.GasPrice = big.NewInt(1) + + state.CreateAccount(contract) + var zeroHash common.Hash + value := rng.Hash() + state.SetState(contract, zeroHash, value) + state.SetCode(contract, convertBytes[vm.OpCode, byte](vm.PC, vm.SLOAD)) + + const tracerName = "prestateTracer" + tracer, err := tracers.DefaultDirectory.New(tracerName, nil, nil) + require.NoErrorf(t, err, "tracers.DefaultDirectory.New(%q)", tracerName) + evm.Config.Tracer = tracer + + _, _, err = evm.Call(vm.AccountRef(rng.Address()), precompile, []byte{}, 1e6, uint256.NewInt(0)) + require.NoError(t, err, "evm.Call([precompile that calls regular contract])") + + gotJSON, err := tracer.GetResult() + require.NoErrorf(t, err, "%T.GetResult()", tracer) + var got map[common.Address]struct{ Storage map[common.Hash]common.Hash } + require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got) + require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD") +} + //nolint:testableexamples // Including output would only make the example more complicated and hide the true intent func ExamplePrecompileEnvironment() { // To determine the actual caller of a precompile, as against the effective diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go index 051a5ff142ba..1f87213e2ce6 100644 --- a/core/vm/environment.libevm.go +++ b/core/vm/environment.libevm.go @@ -90,7 +90,7 @@ func (e *environment) Call(addr common.Address, input []byte, gas uint64, value return e.callContract(Call, addr, input, gas, value, opts...) } -func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) { +func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retGas uint64, retErr error) { // Depth and read-only setting are handled by [EVMInterpreter.Run], which // isn't used for precompiles, so we need to do it ourselves to maintain the // expected invariants. @@ -122,11 +122,24 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by } } + if in.readOnly && value != nil && !value.IsZero() { + return nil, gas, ErrWriteProtection + } + if t := e.evm.Config.Tracer; t != nil { + var bigVal *big.Int + if value != nil { + bigVal = value.ToBig() + } + t.CaptureEnter(typ.OpCode(), caller.Address(), addr, input, gas, bigVal) + + startGas := gas + defer func() { + t.CaptureEnd(retData, startGas-retGas, retErr) + }() + } + switch typ { case Call: - if in.readOnly && !value.IsZero() { - return nil, gas, ErrWriteProtection - } return e.evm.Call(caller, addr, input, gas, value) case CallCode, DelegateCall, StaticCall: // TODO(arr4n): these cases should be very similar to CALL, hence the diff --git a/eth/tracers/native/prestate_libevm.go b/eth/tracers/native/prestate_libevm.go new file mode 100644 index 000000000000..c1a777153669 --- /dev/null +++ b/eth/tracers/native/prestate_libevm.go @@ -0,0 +1,37 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 native + +import ( + "math/big" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/vm" +) + +// CaptureEnter implements the [vm.EVMLogger] hook for entering a new scope (via +// CALL*, CREATE or SELFDESTRUCT). +func (t *prestateTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Although [prestateTracer.lookupStorage] expects + // [prestateTracer.lookupAccount] to have been called, the invariant is + // maintained by [prestateTracer.CaptureState] when it encounters an OpCode + // corresponding to scope entry. This, however, doesn't work when using a + // call method exposed by [vm.PrecompileEnvironment], and is restored by a + // call to this CaptureEnter implementation. Note that lookupAccount(x) is + // idempotent. + t.lookupAccount(to) +}