-
Notifications
You must be signed in to change notification settings - Fork 20.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
eth/tracers: add native flatCallTracer (aka parity style tracer) (#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 <[email protected]>
- Loading branch information
Showing
37 changed files
with
4,870 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
eth/tracers/internal/tracetest/testdata/call_tracer_flat/big_slow.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} |
Oops, something went wrong.