Skip to content

Commit 4384f3a

Browse files
ziogaschrs1na
authored andcommitted
eth/tracers: add native flatCallTracer (aka parity style tracer) (ethereum#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>
1 parent 40471f7 commit 4384f3a

37 files changed

+4870
-15
lines changed

eth/tracers/api.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,10 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
295295
for i, tx := range task.block.Transactions() {
296296
msg, _ := tx.AsMessage(signer, task.block.BaseFee())
297297
txctx := &Context{
298-
BlockHash: task.block.Hash(),
299-
TxIndex: i,
300-
TxHash: tx.Hash(),
298+
BlockHash: task.block.Hash(),
299+
BlockNumber: task.block.Number(),
300+
TxIndex: i,
301+
TxHash: tx.Hash(),
301302
}
302303
res, err := api.traceTx(ctx, msg, txctx, blockCtx, task.statedb, config)
303304
if err != nil {
@@ -629,9 +630,10 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
629630
// Generate the next state snapshot fast without tracing
630631
msg, _ := tx.AsMessage(signer, block.BaseFee())
631632
txctx := &Context{
632-
BlockHash: blockHash,
633-
TxIndex: i,
634-
TxHash: tx.Hash(),
633+
BlockHash: blockHash,
634+
BlockNumber: block.Number(),
635+
TxIndex: i,
636+
TxHash: tx.Hash(),
635637
}
636638
res, err := api.traceTx(ctx, msg, txctx, blockCtx, statedb, config)
637639
if err != nil {
@@ -671,9 +673,10 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
671673
for task := range jobs {
672674
msg, _ := txs[task.index].AsMessage(signer, block.BaseFee())
673675
txctx := &Context{
674-
BlockHash: blockHash,
675-
TxIndex: task.index,
676-
TxHash: txs[task.index].Hash(),
676+
BlockHash: blockHash,
677+
BlockNumber: block.Number(),
678+
TxIndex: task.index,
679+
TxHash: txs[task.index].Hash(),
677680
}
678681
res, err := api.traceTx(ctx, msg, txctx, blockCtx, task.statedb, config)
679682
if err != nil {
@@ -874,9 +877,10 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
874877
defer release()
875878

876879
txctx := &Context{
877-
BlockHash: blockHash,
878-
TxIndex: int(index),
879-
TxHash: hash,
880+
BlockHash: blockHash,
881+
BlockNumber: block.Number(),
882+
TxIndex: int(index),
883+
TxHash: hash,
880884
}
881885
return api.traceTx(ctx, msg, txctx, vmctx, statedb, config)
882886
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package tracetest
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"math/big"
7+
"os"
8+
"path/filepath"
9+
"reflect"
10+
"strings"
11+
"testing"
12+
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/ethereum/go-ethereum/common/hexutil"
15+
"github.com/ethereum/go-ethereum/core"
16+
"github.com/ethereum/go-ethereum/core/rawdb"
17+
"github.com/ethereum/go-ethereum/core/types"
18+
"github.com/ethereum/go-ethereum/core/vm"
19+
"github.com/ethereum/go-ethereum/rlp"
20+
"github.com/ethereum/go-ethereum/tests"
21+
22+
// Force-load the native, to trigger registration
23+
"github.com/ethereum/go-ethereum/eth/tracers"
24+
)
25+
26+
// flatCallTrace is the result of a callTracerParity run.
27+
type flatCallTrace struct {
28+
Action flatCallTraceAction `json:"action"`
29+
BlockHash common.Hash `json:"-"`
30+
BlockNumber uint64 `json:"-"`
31+
Error string `json:"error,omitempty"`
32+
Result flatCallTraceResult `json:"result,omitempty"`
33+
Subtraces int `json:"subtraces"`
34+
TraceAddress []int `json:"traceAddress"`
35+
TransactionHash common.Hash `json:"-"`
36+
TransactionPosition uint64 `json:"-"`
37+
Type string `json:"type"`
38+
Time string `json:"-"`
39+
}
40+
41+
type flatCallTraceAction struct {
42+
Author common.Address `json:"author,omitempty"`
43+
RewardType string `json:"rewardType,omitempty"`
44+
SelfDestructed common.Address `json:"address,omitempty"`
45+
Balance hexutil.Big `json:"balance,omitempty"`
46+
CallType string `json:"callType,omitempty"`
47+
CreationMethod string `json:"creationMethod,omitempty"`
48+
From common.Address `json:"from,omitempty"`
49+
Gas hexutil.Uint64 `json:"gas,omitempty"`
50+
Init hexutil.Bytes `json:"init,omitempty"`
51+
Input hexutil.Bytes `json:"input,omitempty"`
52+
RefundAddress common.Address `json:"refundAddress,omitempty"`
53+
To common.Address `json:"to,omitempty"`
54+
Value hexutil.Big `json:"value,omitempty"`
55+
}
56+
57+
type flatCallTraceResult struct {
58+
Address common.Address `json:"address,omitempty"`
59+
Code hexutil.Bytes `json:"code,omitempty"`
60+
GasUsed hexutil.Uint64 `json:"gasUsed,omitempty"`
61+
Output hexutil.Bytes `json:"output,omitempty"`
62+
}
63+
64+
// flatCallTracerTest defines a single test to check the call tracer against.
65+
type flatCallTracerTest struct {
66+
Genesis core.Genesis `json:"genesis"`
67+
Context callContext `json:"context"`
68+
Input string `json:"input"`
69+
TracerConfig json.RawMessage `json:"tracerConfig"`
70+
Result []flatCallTrace `json:"result"`
71+
}
72+
73+
func flatCallTracerTestRunner(tracerName string, filename string, dirPath string, t testing.TB) error {
74+
// Call tracer test found, read if from disk
75+
blob, err := os.ReadFile(filepath.Join("testdata", dirPath, filename))
76+
if err != nil {
77+
return fmt.Errorf("failed to read testcase: %v", err)
78+
}
79+
test := new(flatCallTracerTest)
80+
if err := json.Unmarshal(blob, test); err != nil {
81+
return fmt.Errorf("failed to parse testcase: %v", err)
82+
}
83+
// Configure a blockchain with the given prestate
84+
tx := new(types.Transaction)
85+
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
86+
return fmt.Errorf("failed to parse testcase input: %v", err)
87+
}
88+
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
89+
origin, _ := signer.Sender(tx)
90+
txContext := vm.TxContext{
91+
Origin: origin,
92+
GasPrice: tx.GasPrice(),
93+
}
94+
context := vm.BlockContext{
95+
CanTransfer: core.CanTransfer,
96+
Transfer: core.Transfer,
97+
Coinbase: test.Context.Miner,
98+
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
99+
Time: uint64(test.Context.Time),
100+
Difficulty: (*big.Int)(test.Context.Difficulty),
101+
GasLimit: uint64(test.Context.GasLimit),
102+
}
103+
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
104+
105+
// Create the tracer, the EVM environment and run it
106+
tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), test.TracerConfig)
107+
if err != nil {
108+
return fmt.Errorf("failed to create call tracer: %v", err)
109+
}
110+
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
111+
112+
msg, err := tx.AsMessage(signer, nil)
113+
if err != nil {
114+
return fmt.Errorf("failed to prepare transaction for tracing: %v", err)
115+
}
116+
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
117+
118+
if _, err = st.TransitionDb(); err != nil {
119+
return fmt.Errorf("failed to execute transaction: %v", err)
120+
}
121+
122+
// Retrieve the trace result and compare against the etalon
123+
res, err := tracer.GetResult()
124+
if err != nil {
125+
return fmt.Errorf("failed to retrieve trace result: %v", err)
126+
}
127+
ret := new([]flatCallTrace)
128+
if err := json.Unmarshal(res, ret); err != nil {
129+
return fmt.Errorf("failed to unmarshal trace result: %v", err)
130+
}
131+
if !jsonEqualFlat(ret, test.Result) {
132+
t.Logf("tracer name: %s", tracerName)
133+
134+
// uncomment this for easier debugging
135+
// have, _ := json.MarshalIndent(ret, "", " ")
136+
// want, _ := json.MarshalIndent(test.Result, "", " ")
137+
// t.Logf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))
138+
139+
// uncomment this for harder debugging <3 meowsbits
140+
// lines := deep.Equal(ret, test.Result)
141+
// for _, l := range lines {
142+
// t.Logf("%s", l)
143+
// t.FailNow()
144+
// }
145+
146+
t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result)
147+
}
148+
return nil
149+
}
150+
151+
// Iterates over all the input-output datasets in the tracer parity test harness and
152+
// runs the Native tracer against them.
153+
func TestFlatCallTracerNative(t *testing.T) {
154+
testFlatCallTracer("flatCallTracer", "call_tracer_flat", t)
155+
}
156+
157+
func testFlatCallTracer(tracerName string, dirPath string, t *testing.T) {
158+
files, err := os.ReadDir(filepath.Join("testdata", dirPath))
159+
if err != nil {
160+
t.Fatalf("failed to retrieve tracer test suite: %v", err)
161+
}
162+
for _, file := range files {
163+
if !strings.HasSuffix(file.Name(), ".json") {
164+
continue
165+
}
166+
file := file // capture range variable
167+
t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) {
168+
t.Parallel()
169+
170+
err := flatCallTracerTestRunner(tracerName, file.Name(), dirPath, t)
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
})
175+
}
176+
}
177+
178+
// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
179+
// comparison
180+
func jsonEqualFlat(x, y interface{}) bool {
181+
xTrace := new([]flatCallTrace)
182+
yTrace := new([]flatCallTrace)
183+
if xj, err := json.Marshal(x); err == nil {
184+
json.Unmarshal(xj, xTrace)
185+
} else {
186+
return false
187+
}
188+
if yj, err := json.Marshal(y); err == nil {
189+
json.Unmarshal(yj, yTrace)
190+
} else {
191+
return false
192+
}
193+
return reflect.DeepEqual(xTrace, yTrace)
194+
}
195+
196+
func BenchmarkFlatCallTracer(b *testing.B) {
197+
files, err := filepath.Glob("testdata/call_tracer_flat/*.json")
198+
if err != nil {
199+
b.Fatalf("failed to read testdata: %v", err)
200+
}
201+
202+
for _, file := range files {
203+
filename := strings.TrimPrefix(file, "testdata/call_tracer_flat/")
204+
b.Run(camel(strings.TrimSuffix(filename, ".json")), func(b *testing.B) {
205+
for n := 0; n < b.N; n++ {
206+
err := flatCallTracerTestRunner("flatCallTracer", filename, "call_tracer_flat", b)
207+
if err != nil {
208+
b.Fatal(err)
209+
}
210+
}
211+
})
212+
}
213+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"genesis": {
3+
"difficulty": "50486697699375",
4+
"extraData": "0xd783010406844765746887676f312e362e32856c696e7578",
5+
"gasLimit": "4788482",
6+
"hash": "0xf6bbc5bbe34d5c93fd5b4712cd498d1026b8b0f586efefe7fe30231ed6b8a1a5",
7+
"miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1",
8+
"mixHash": "0xabca93555584c0463ee5c212251dd002bb3a93a157e06614276f93de53d4fdb8",
9+
"nonce": "0xa64136fcb9c2d4ca",
10+
"number": "1719576",
11+
"stateRoot": "0xab5eec2177a92d633e282936af66c46e24cfa8f2fdc2b8155f33885f483d06f3",
12+
"timestamp": "1466150166",
13+
"totalDifficulty": "28295412423546970038",
14+
"alloc": {
15+
"0xf8bda96b67036ee48107f2a0695ea673479dda56": {
16+
"balance": "0x1529e844f9ecdeec",
17+
"nonce": "33",
18+
"code": "0x",
19+
"storage": {}
20+
}
21+
},
22+
"config": {
23+
"chainId": 1,
24+
"daoForkSupport": true,
25+
"eip150Block": 0,
26+
"eip150Hash": "0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d",
27+
"eip155Block": 3000000,
28+
"eip158Block": 0,
29+
"ethash": {},
30+
"homesteadBlock": 1150000,
31+
"byzantiumBlock": 8772000,
32+
"constantinopleBlock": 9573000,
33+
"petersburgBlock": 10500839,
34+
"istanbulBlock": 10500839
35+
}
36+
},
37+
"context": {
38+
"number": "1719577",
39+
"difficulty": "50486697732143",
40+
"timestamp": "1466150178",
41+
"gasLimit": "4788484",
42+
"miner": "0x2a65aca4d5fc5b5c859090a6c34d164135398226"
43+
},
44+
"input": "0xf874218504a817c800832318608080a35b620186a05a131560135760016020526000565b600080601f600039601f565b6000f31ba0575fa000a1f06659a7b6d3c7877601519a4997f04293f0dfa0eee6d8cd840c77a04c52ce50719ee2ff7a0c5753f4ee69c0340666f582dbb5148845a354ca726e4a",
45+
"result": [
46+
{
47+
"action": {
48+
"from": "0xf8bda96b67036ee48107f2a0695ea673479dda56",
49+
"gas": "0x22410c",
50+
"init": "0x5b620186a05a131560135760016020526000565b600080601f600039601f565b6000f3",
51+
"value": "0x0"
52+
},
53+
"blockNumber": 1719577,
54+
"result": {
55+
"address": "0xb2e6a2546c45889427757171ab05b8b438525b42",
56+
"code": "0x",
57+
"gasUsed": "0x219202"
58+
},
59+
"subtraces": 0,
60+
"traceAddress": [],
61+
"type": "create"
62+
}
63+
]
64+
}

0 commit comments

Comments
 (0)