Skip to content

Commit

Permalink
eth/tracers: add native flatCallTracer (aka parity style tracer) (eth…
Browse files Browse the repository at this point in the history
…ereum#26377)

Adds support for a native call tracer with the Parity format, which outputs call frames
in a flat array. This tracer accepts the following options:

- `convertParityErrors: true` will convert error messages to match those of Parity
- `includePrecompiles: true` will report all calls to precompiles. The default
  matches Parity's behavior where CALL and STATICCALLs to precompiles are excluded

Incompatibilities with Parity include:

- Parity removes the result object in case of failure. This behavior is maintained
  with the exception of reverts. Revert output usually contains useful information,
  i.e. Solidity revert reason.
- The `gasUsed` field accounts for intrinsic gas (e.g. 21000 for simple transfers)
  and refunds unlike Parity
- Block rewards are not reported

Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
  • Loading branch information
2 people authored and mmsqe committed Dec 7, 2023
1 parent 43a5c5d commit ce9b0a1
Show file tree
Hide file tree
Showing 37 changed files with 4,870 additions and 15 deletions.
28 changes: 16 additions & 12 deletions eth/tracers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,10 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
for i, tx := range task.block.Transactions() {
msg, _ := tx.AsMessage(signer, task.block.BaseFee())
txctx := &Context{
BlockHash: task.block.Hash(),
TxIndex: i,
TxHash: tx.Hash(),
BlockHash: task.block.Hash(),
BlockNumber: task.block.Number(),
TxIndex: i,
TxHash: tx.Hash(),
}
res, err := api.traceTx(ctx, msg, txctx, blockCtx, task.statedb, config)
if err != nil {
Expand Down Expand Up @@ -629,9 +630,10 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
// Generate the next state snapshot fast without tracing
msg, _ := tx.AsMessage(signer, block.BaseFee())
txctx := &Context{
BlockHash: blockHash,
TxIndex: i,
TxHash: tx.Hash(),
BlockHash: blockHash,
BlockNumber: block.Number(),
TxIndex: i,
TxHash: tx.Hash(),
}
res, err := api.traceTx(ctx, msg, txctx, blockCtx, statedb, config)
if err != nil {
Expand Down Expand Up @@ -671,9 +673,10 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
for task := range jobs {
msg, _ := txs[task.index].AsMessage(signer, block.BaseFee())
txctx := &Context{
BlockHash: blockHash,
TxIndex: task.index,
TxHash: txs[task.index].Hash(),
BlockHash: blockHash,
BlockNumber: block.Number(),
TxIndex: task.index,
TxHash: txs[task.index].Hash(),
}
res, err := api.traceTx(ctx, msg, txctx, blockCtx, task.statedb, config)
if err != nil {
Expand Down Expand Up @@ -874,9 +877,10 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
defer release()

txctx := &Context{
BlockHash: blockHash,
TxIndex: int(index),
TxHash: hash,
BlockHash: blockHash,
BlockNumber: block.Number(),
TxIndex: int(index),
TxHash: hash,
}
return api.traceTx(ctx, msg, txctx, vmctx, statedb, config)
}
Expand Down
213 changes: 213 additions & 0 deletions eth/tracers/internal/tracetest/flat_calltrace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package tracetest

import (
"encoding/json"
"fmt"
"math/big"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/tests"

// Force-load the native, to trigger registration
"github.com/ethereum/go-ethereum/eth/tracers"
)

// flatCallTrace is the result of a callTracerParity run.
type flatCallTrace struct {
Action flatCallTraceAction `json:"action"`
BlockHash common.Hash `json:"-"`
BlockNumber uint64 `json:"-"`
Error string `json:"error,omitempty"`
Result flatCallTraceResult `json:"result,omitempty"`
Subtraces int `json:"subtraces"`
TraceAddress []int `json:"traceAddress"`
TransactionHash common.Hash `json:"-"`
TransactionPosition uint64 `json:"-"`
Type string `json:"type"`
Time string `json:"-"`
}

type flatCallTraceAction struct {
Author common.Address `json:"author,omitempty"`
RewardType string `json:"rewardType,omitempty"`
SelfDestructed common.Address `json:"address,omitempty"`
Balance hexutil.Big `json:"balance,omitempty"`
CallType string `json:"callType,omitempty"`
CreationMethod string `json:"creationMethod,omitempty"`
From common.Address `json:"from,omitempty"`
Gas hexutil.Uint64 `json:"gas,omitempty"`
Init hexutil.Bytes `json:"init,omitempty"`
Input hexutil.Bytes `json:"input,omitempty"`
RefundAddress common.Address `json:"refundAddress,omitempty"`
To common.Address `json:"to,omitempty"`
Value hexutil.Big `json:"value,omitempty"`
}

type flatCallTraceResult struct {
Address common.Address `json:"address,omitempty"`
Code hexutil.Bytes `json:"code,omitempty"`
GasUsed hexutil.Uint64 `json:"gasUsed,omitempty"`
Output hexutil.Bytes `json:"output,omitempty"`
}

// flatCallTracerTest defines a single test to check the call tracer against.
type flatCallTracerTest struct {
Genesis core.Genesis `json:"genesis"`
Context callContext `json:"context"`
Input string `json:"input"`
TracerConfig json.RawMessage `json:"tracerConfig"`
Result []flatCallTrace `json:"result"`
}

func flatCallTracerTestRunner(tracerName string, filename string, dirPath string, t testing.TB) error {
// Call tracer test found, read if from disk
blob, err := os.ReadFile(filepath.Join("testdata", dirPath, filename))
if err != nil {
return fmt.Errorf("failed to read testcase: %v", err)
}
test := new(flatCallTracerTest)
if err := json.Unmarshal(blob, test); err != nil {
return fmt.Errorf("failed to parse testcase: %v", err)
}
// Configure a blockchain with the given prestate
tx := new(types.Transaction)
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
return fmt.Errorf("failed to parse testcase input: %v", err)
}
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
origin, _ := signer.Sender(tx)
txContext := vm.TxContext{
Origin: origin,
GasPrice: tx.GasPrice(),
}
context := vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
Coinbase: test.Context.Miner,
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
Time: uint64(test.Context.Time),
Difficulty: (*big.Int)(test.Context.Difficulty),
GasLimit: uint64(test.Context.GasLimit),
}
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)

// Create the tracer, the EVM environment and run it
tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), test.TracerConfig)
if err != nil {
return fmt.Errorf("failed to create call tracer: %v", err)
}
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})

msg, err := tx.AsMessage(signer, nil)
if err != nil {
return fmt.Errorf("failed to prepare transaction for tracing: %v", err)
}
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))

if _, err = st.TransitionDb(); err != nil {
return fmt.Errorf("failed to execute transaction: %v", err)
}

// Retrieve the trace result and compare against the etalon
res, err := tracer.GetResult()
if err != nil {
return fmt.Errorf("failed to retrieve trace result: %v", err)
}
ret := new([]flatCallTrace)
if err := json.Unmarshal(res, ret); err != nil {
return fmt.Errorf("failed to unmarshal trace result: %v", err)
}
if !jsonEqualFlat(ret, test.Result) {
t.Logf("tracer name: %s", tracerName)

// uncomment this for easier debugging
// have, _ := json.MarshalIndent(ret, "", " ")
// want, _ := json.MarshalIndent(test.Result, "", " ")
// t.Logf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))

// uncomment this for harder debugging <3 meowsbits
// lines := deep.Equal(ret, test.Result)
// for _, l := range lines {
// t.Logf("%s", l)
// t.FailNow()
// }

t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result)
}
return nil
}

// Iterates over all the input-output datasets in the tracer parity test harness and
// runs the Native tracer against them.
func TestFlatCallTracerNative(t *testing.T) {
testFlatCallTracer("flatCallTracer", "call_tracer_flat", t)
}

func testFlatCallTracer(tracerName string, dirPath string, t *testing.T) {
files, err := os.ReadDir(filepath.Join("testdata", dirPath))
if err != nil {
t.Fatalf("failed to retrieve tracer test suite: %v", err)
}
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") {
continue
}
file := file // capture range variable
t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) {
t.Parallel()

err := flatCallTracerTestRunner(tracerName, file.Name(), dirPath, t)
if err != nil {
t.Fatal(err)
}
})
}
}

// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
// comparison
func jsonEqualFlat(x, y interface{}) bool {
xTrace := new([]flatCallTrace)
yTrace := new([]flatCallTrace)
if xj, err := json.Marshal(x); err == nil {
json.Unmarshal(xj, xTrace)
} else {
return false
}
if yj, err := json.Marshal(y); err == nil {
json.Unmarshal(yj, yTrace)
} else {
return false
}
return reflect.DeepEqual(xTrace, yTrace)
}

func BenchmarkFlatCallTracer(b *testing.B) {
files, err := filepath.Glob("testdata/call_tracer_flat/*.json")
if err != nil {
b.Fatalf("failed to read testdata: %v", err)
}

for _, file := range files {
filename := strings.TrimPrefix(file, "testdata/call_tracer_flat/")
b.Run(camel(strings.TrimSuffix(filename, ".json")), func(b *testing.B) {
for n := 0; n < b.N; n++ {
err := flatCallTracerTestRunner("flatCallTracer", filename, "call_tracer_flat", b)
if err != nil {
b.Fatal(err)
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"genesis": {
"difficulty": "50486697699375",
"extraData": "0xd783010406844765746887676f312e362e32856c696e7578",
"gasLimit": "4788482",
"hash": "0xf6bbc5bbe34d5c93fd5b4712cd498d1026b8b0f586efefe7fe30231ed6b8a1a5",
"miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1",
"mixHash": "0xabca93555584c0463ee5c212251dd002bb3a93a157e06614276f93de53d4fdb8",
"nonce": "0xa64136fcb9c2d4ca",
"number": "1719576",
"stateRoot": "0xab5eec2177a92d633e282936af66c46e24cfa8f2fdc2b8155f33885f483d06f3",
"timestamp": "1466150166",
"totalDifficulty": "28295412423546970038",
"alloc": {
"0xf8bda96b67036ee48107f2a0695ea673479dda56": {
"balance": "0x1529e844f9ecdeec",
"nonce": "33",
"code": "0x",
"storage": {}
}
},
"config": {
"chainId": 1,
"daoForkSupport": true,
"eip150Block": 0,
"eip150Hash": "0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d",
"eip155Block": 3000000,
"eip158Block": 0,
"ethash": {},
"homesteadBlock": 1150000,
"byzantiumBlock": 8772000,
"constantinopleBlock": 9573000,
"petersburgBlock": 10500839,
"istanbulBlock": 10500839
}
},
"context": {
"number": "1719577",
"difficulty": "50486697732143",
"timestamp": "1466150178",
"gasLimit": "4788484",
"miner": "0x2a65aca4d5fc5b5c859090a6c34d164135398226"
},
"input": "0xf874218504a817c800832318608080a35b620186a05a131560135760016020526000565b600080601f600039601f565b6000f31ba0575fa000a1f06659a7b6d3c7877601519a4997f04293f0dfa0eee6d8cd840c77a04c52ce50719ee2ff7a0c5753f4ee69c0340666f582dbb5148845a354ca726e4a",
"result": [
{
"action": {
"from": "0xf8bda96b67036ee48107f2a0695ea673479dda56",
"gas": "0x22410c",
"init": "0x5b620186a05a131560135760016020526000565b600080601f600039601f565b6000f3",
"value": "0x0"
},
"blockNumber": 1719577,
"result": {
"address": "0xb2e6a2546c45889427757171ab05b8b438525b42",
"code": "0x",
"gasUsed": "0x219202"
},
"subtraces": 0,
"traceAddress": [],
"type": "create"
}
]
}
Loading

0 comments on commit ce9b0a1

Please sign in to comment.