Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: pre-state tracer logging storage after call from precompile #64

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
54 changes: 50 additions & 4 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package vm_test

import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"reflect"
Expand All @@ -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"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
21 changes: 17 additions & 4 deletions core/vm/environment.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions eth/tracers/native/prestate_libevm.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

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)
}