Skip to content

Commit 12b8aa5

Browse files
ARR4NDarioush Jalali
andauthored
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](https://github.com/ava-labs/libevm/blob/cb7eb89341132f301680848d8faea6a5568dc326/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>
1 parent cb7eb89 commit 12b8aa5

File tree

4 files changed

+128
-23
lines changed

4 files changed

+128
-23
lines changed

core/vm/contracts.libevm.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,40 @@ type evmCallArgs struct {
5454
}
5555

5656
// A CallType refers to a *CALL* [OpCode] / respective method on [EVM].
57-
type CallType uint8
57+
type CallType OpCode
5858

5959
const (
60-
UnknownCallType CallType = iota
61-
Call
62-
CallCode
63-
DelegateCall
64-
StaticCall
60+
Call = CallType(CALL)
61+
CallCode = CallType(CALLCODE)
62+
DelegateCall = CallType(DELEGATECALL)
63+
StaticCall = CallType(STATICCALL)
6564
)
6665

66+
func (t CallType) isValid() bool {
67+
switch t {
68+
case Call, CallCode, DelegateCall, StaticCall:
69+
return true
70+
default:
71+
return false
72+
}
73+
}
74+
6775
// String returns a human-readable representation of the CallType.
6876
func (t CallType) String() string {
69-
switch t {
70-
case Call:
71-
return "Call"
72-
case CallCode:
73-
return "CallCode"
74-
case DelegateCall:
75-
return "DelegateCall"
76-
case StaticCall:
77-
return "StaticCall"
77+
if t.isValid() {
78+
return t.OpCode().String()
7879
}
7980
return fmt.Sprintf("Unknown %T(%d)", t, t)
8081
}
8182

83+
// OpCode returns t's equivalent OpCode.
84+
func (t CallType) OpCode() OpCode {
85+
if t.isValid() {
86+
return OpCode(t)
87+
}
88+
return INVALID
89+
}
90+
8291
// run runs the [PrecompiledContract], differentiating between stateful and
8392
// regular types.
8493
func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {

core/vm/contracts.libevm_test.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package vm_test
1717

1818
import (
1919
"bytes"
20+
"encoding/json"
2021
"fmt"
2122
"math/big"
2223
"reflect"
@@ -33,6 +34,8 @@ import (
3334
"github.com/ava-labs/libevm/core/types"
3435
"github.com/ava-labs/libevm/core/vm"
3536
"github.com/ava-labs/libevm/crypto"
37+
"github.com/ava-labs/libevm/eth/tracers"
38+
_ "github.com/ava-labs/libevm/eth/tracers/native"
3639
"github.com/ava-labs/libevm/libevm"
3740
"github.com/ava-labs/libevm/libevm/ethtest"
3841
"github.com/ava-labs/libevm/libevm/hookstest"
@@ -338,7 +341,7 @@ func TestInheritReadOnly(t *testing.T) {
338341
rng := ethtest.NewPseudoRand(42)
339342
contractAddr := rng.Address()
340343
state.CreateAccount(contractAddr)
341-
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract))
344+
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract...))
342345

343346
// (3)
344347

@@ -404,7 +407,7 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC
404407
}
405408

406409
contract = append(contract, vm.PUSH20)
407-
contract = append(contract, convertBytes[byte, vm.OpCode](dest[:])...)
410+
contract = append(contract, convertBytes[byte, vm.OpCode](dest[:]...)...)
408411

409412
contract = append(contract,
410413
p0, // gas
@@ -417,7 +420,7 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC
417420
return contract
418421
}
419422

420-
func convertBytes[From ~byte, To ~byte](buf []From) []To {
423+
func convertBytes[From ~byte, To ~byte](buf ...From) []To {
421424
out := make([]To, len(buf))
422425
for i, b := range buf {
423426
out[i] = To(b)
@@ -672,7 +675,7 @@ func TestPrecompileMakeCall(t *testing.T) {
672675
evm.Origin = eoa
673676
state.CreateAccount(caller)
674677
proxy := makeReturnProxy(t, sut, tt.incomingCallType)
675-
state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy))
678+
state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy...))
676679

677680
got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0))
678681
require.NoError(t, err)
@@ -681,6 +684,49 @@ func TestPrecompileMakeCall(t *testing.T) {
681684
}
682685
}
683686

687+
func TestPrecompileCallWithTracer(t *testing.T) {
688+
// The native pre-state tracer, when logging storage, assumes an invariant
689+
// that is broken by a precompile calling another contract. This is a test
690+
// of the fix, ensuring that an SLOADed value is properly handled by the
691+
// tracer.
692+
693+
rng := ethtest.NewPseudoRand(42 * 142857)
694+
precompile := rng.Address()
695+
contract := rng.Address()
696+
697+
hooks := &hookstest.Stub{
698+
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
699+
precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
700+
return env.Call(contract, nil, suppliedGas, uint256.NewInt(0))
701+
}),
702+
},
703+
}
704+
hooks.Register(t)
705+
706+
state, evm := ethtest.NewZeroEVM(t)
707+
evm.GasPrice = big.NewInt(1)
708+
709+
state.CreateAccount(contract)
710+
var zeroHash common.Hash
711+
value := rng.Hash()
712+
state.SetState(contract, zeroHash, value)
713+
state.SetCode(contract, convertBytes[vm.OpCode, byte](vm.PC, vm.SLOAD))
714+
715+
const tracerName = "prestateTracer"
716+
tracer, err := tracers.DefaultDirectory.New(tracerName, nil, nil)
717+
require.NoErrorf(t, err, "tracers.DefaultDirectory.New(%q)", tracerName)
718+
evm.Config.Tracer = tracer
719+
720+
_, _, err = evm.Call(vm.AccountRef(rng.Address()), precompile, []byte{}, 1e6, uint256.NewInt(0))
721+
require.NoError(t, err, "evm.Call([precompile that calls regular contract])")
722+
723+
gotJSON, err := tracer.GetResult()
724+
require.NoErrorf(t, err, "%T.GetResult()", tracer)
725+
var got map[common.Address]struct{ Storage map[common.Hash]common.Hash }
726+
require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got)
727+
require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD")
728+
}
729+
684730
//nolint:testableexamples // Including output would only make the example more complicated and hide the true intent
685731
func ExamplePrecompileEnvironment() {
686732
// To determine the actual caller of a precompile, as against the effective

core/vm/environment.libevm.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (e *environment) Call(addr common.Address, input []byte, gas uint64, value
9090
return e.callContract(Call, addr, input, gas, value, opts...)
9191
}
9292

93-
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) {
93+
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retGas uint64, retErr error) {
9494
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
9595
// isn't used for precompiles, so we need to do it ourselves to maintain the
9696
// expected invariants.
@@ -122,11 +122,24 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by
122122
}
123123
}
124124

125+
if in.readOnly && value != nil && !value.IsZero() {
126+
return nil, gas, ErrWriteProtection
127+
}
128+
if t := e.evm.Config.Tracer; t != nil {
129+
var bigVal *big.Int
130+
if value != nil {
131+
bigVal = value.ToBig()
132+
}
133+
t.CaptureEnter(typ.OpCode(), caller.Address(), addr, input, gas, bigVal)
134+
135+
startGas := gas
136+
defer func() {
137+
t.CaptureEnd(retData, startGas-retGas, retErr)
138+
}()
139+
}
140+
125141
switch typ {
126142
case Call:
127-
if in.readOnly && !value.IsZero() {
128-
return nil, gas, ErrWriteProtection
129-
}
130143
return e.evm.Call(caller, addr, input, gas, value)
131144
case CallCode, DelegateCall, StaticCall:
132145
// TODO(arr4n): these cases should be very similar to CALL, hence the
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2024 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package native
18+
19+
import (
20+
"math/big"
21+
22+
"github.com/ava-labs/libevm/common"
23+
"github.com/ava-labs/libevm/core/vm"
24+
)
25+
26+
// CaptureEnter implements the [vm.EVMLogger] hook for entering a new scope (via
27+
// CALL*, CREATE or SELFDESTRUCT).
28+
func (t *prestateTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
29+
// Although [prestateTracer.lookupStorage] expects
30+
// [prestateTracer.lookupAccount] to have been called, the invariant is
31+
// maintained by [prestateTracer.CaptureState] when it encounters an OpCode
32+
// corresponding to scope entry. This, however, doesn't work when using a
33+
// call method exposed by [vm.PrecompileEnvironment], and is restored by a
34+
// call to this CaptureEnter implementation. Note that lookupAccount(x) is
35+
// idempotent.
36+
t.lookupAccount(to)
37+
}

0 commit comments

Comments
 (0)