mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 02:40:45 +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].
|
// A CallType refers to a *CALL* [OpCode] / respective method on [EVM].
|
||||||
type CallType uint8
|
type CallType OpCode
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UnknownCallType CallType = iota
|
Call = CallType(CALL)
|
||||||
Call
|
CallCode = CallType(CALLCODE)
|
||||||
CallCode
|
DelegateCall = CallType(DELEGATECALL)
|
||||||
DelegateCall
|
StaticCall = CallType(STATICCALL)
|
||||||
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.
|
// String returns a human-readable representation of the CallType.
|
||||||
func (t CallType) String() string {
|
func (t CallType) String() string {
|
||||||
switch t {
|
if t.isValid() {
|
||||||
case Call:
|
return t.OpCode().String()
|
||||||
return "Call"
|
|
||||||
case CallCode:
|
|
||||||
return "CallCode"
|
|
||||||
case DelegateCall:
|
|
||||||
return "DelegateCall"
|
|
||||||
case StaticCall:
|
|
||||||
return "StaticCall"
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Unknown %T(%d)", t, t)
|
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
|
// run runs the [PrecompiledContract], differentiating between stateful and
|
||||||
// regular types.
|
// regular types.
|
||||||
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package vm_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -33,6 +34,8 @@ import (
|
||||||
"github.com/ava-labs/libevm/core/types"
|
"github.com/ava-labs/libevm/core/types"
|
||||||
"github.com/ava-labs/libevm/core/vm"
|
"github.com/ava-labs/libevm/core/vm"
|
||||||
"github.com/ava-labs/libevm/crypto"
|
"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"
|
||||||
"github.com/ava-labs/libevm/libevm/ethtest"
|
"github.com/ava-labs/libevm/libevm/ethtest"
|
||||||
"github.com/ava-labs/libevm/libevm/hookstest"
|
"github.com/ava-labs/libevm/libevm/hookstest"
|
||||||
|
|
@ -338,7 +341,7 @@ func TestInheritReadOnly(t *testing.T) {
|
||||||
rng := ethtest.NewPseudoRand(42)
|
rng := ethtest.NewPseudoRand(42)
|
||||||
contractAddr := rng.Address()
|
contractAddr := rng.Address()
|
||||||
state.CreateAccount(contractAddr)
|
state.CreateAccount(contractAddr)
|
||||||
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract))
|
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract...))
|
||||||
|
|
||||||
// (3)
|
// (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, vm.PUSH20)
|
||||||
contract = append(contract, convertBytes[byte, vm.OpCode](dest[:])...)
|
contract = append(contract, convertBytes[byte, vm.OpCode](dest[:]...)...)
|
||||||
|
|
||||||
contract = append(contract,
|
contract = append(contract,
|
||||||
p0, // gas
|
p0, // gas
|
||||||
|
|
@ -417,7 +420,7 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC
|
||||||
return contract
|
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))
|
out := make([]To, len(buf))
|
||||||
for i, b := range buf {
|
for i, b := range buf {
|
||||||
out[i] = To(b)
|
out[i] = To(b)
|
||||||
|
|
@ -672,7 +675,7 @@ func TestPrecompileMakeCall(t *testing.T) {
|
||||||
evm.Origin = eoa
|
evm.Origin = eoa
|
||||||
state.CreateAccount(caller)
|
state.CreateAccount(caller)
|
||||||
proxy := makeReturnProxy(t, sut, tt.incomingCallType)
|
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))
|
got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0))
|
||||||
require.NoError(t, err)
|
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
|
//nolint:testableexamples // Including output would only make the example more complicated and hide the true intent
|
||||||
func ExamplePrecompileEnvironment() {
|
func ExamplePrecompileEnvironment() {
|
||||||
// To determine the actual caller of a precompile, as against the effective
|
// 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...)
|
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
|
// 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
|
// isn't used for precompiles, so we need to do it ourselves to maintain the
|
||||||
// expected invariants.
|
// 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 {
|
switch typ {
|
||||||
case Call:
|
case Call:
|
||||||
if in.readOnly && !value.IsZero() {
|
|
||||||
return nil, gas, ErrWriteProtection
|
|
||||||
}
|
|
||||||
return e.evm.Call(caller, addr, input, gas, value)
|
return e.evm.Call(caller, addr, input, gas, value)
|
||||||
case CallCode, DelegateCall, StaticCall:
|
case CallCode, DelegateCall, StaticCall:
|
||||||
// TODO(arr4n): these cases should be very similar to CALL, hence the
|
// 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