mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-24 07:34:31 +00:00
fix: pre-state tracer logging storage after call from precompile (#64)
## Why this should be merged
Fixes tracing when a stateful precompile calls another contract that
itself accesses storage.
## How this works
The pre-state tracer from `eth/tracers/native` doesn't implement
`CaptureEnter()` (entry of a new context), instead relying on
`CaptureState()` (per-opcode tracing) to detect that a new contract has
been entered. In doing so, it [maintains an
invariant](cb7eb89341/eth/tracers/native/prestate.go (L160))
that is expected when `CaptureState(vm.SLOAD, ...)` is called—breaking
the invariant results in a panic due to a nil map.
The fix involves (a) maintaining the invariant as part of
`CaptureEnter()` (previously a no-op); and (b) calling said method
inside `vm.PrecompileEnvironment.Call()`. The latter has the added
benefit of properly handling all tracing involving an outbound call from
precompiles.
## How this was tested
New integration test demonstrates that the tracer can log the retrieved
storage value.
---------
Co-authored-by: Darioush Jalali <darioush.jalali@avalabs.org>
This commit is contained in:
parent
cb7eb89341
commit
12b8aa5c2e
4 changed files with 128 additions and 23 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
37
eth/tracers/native/prestate_libevm.go
Normal file
37
eth/tracers/native/prestate_libevm.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue