mirror of https://github.com/ethereum/go-ethereum
eth/tracers: support for golang tracers + add golang callTracer (#23708)
* eth/tracers: add basic native loader
* eth/tracers: add GetResult to tracer interface
* eth/tracers: add native call tracer
* eth/tracers: fix call tracer json result
* eth/tracers: minor fix
* eth/tracers: fix
* eth/tracers: fix benchTracer
* eth/tracers: test native call tracer
* eth/tracers: fix
* eth/tracers: rm extra make
Co-authored-by: Martin Holst Swende <martin@swende.se>
* eth/tracers: rm extra make
* eth/tracers: make callFrame private
* eth/tracers: clean-up and comments
* eth/tracers: add license
* eth/tracers: rework the model a bit
* eth/tracers: move tracecall tests to subpackage
* cmd/geth: load native tracers
* eth/tracers: minor fix
* eth/tracers: impl stop
* eth/tracers: add native noop tracer
* renamings
Co-authored-by: Martin Holst Swende <martin@swende.se>
* eth/tracers: more renamings
* eth/tracers: make jstracer non-exported, avoid cast
* eth/tracers, core/vm: rename vm.Tracer to vm.EVMLogger for clarity
* eth/tracers: minor comment fix
* eth/tracers/testing: lint nitpicks
* core,eth: cancel evm on nativecalltracer stop
* Revert "core,eth: cancel evm on nativecalltracer stop"
This reverts commit 01bb908790
.
* eth/tracers: linter nits
* eth/tracers: fix output on err
Co-authored-by: Martin Holst Swende <martin@swende.se>
pull/23861/head
parent
3bbeb94c1c
commit
8d7e6062ec
@ -0,0 +1,170 @@ |
||||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it 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 go-ethereum library is distributed in the hope that it 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 ( |
||||
"encoding/json" |
||||
"errors" |
||||
"math/big" |
||||
"strconv" |
||||
"strings" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/vm" |
||||
"github.com/ethereum/go-ethereum/eth/tracers" |
||||
) |
||||
|
||||
func init() { |
||||
tracers.RegisterNativeTracer("callTracerNative", NewCallTracer) |
||||
} |
||||
|
||||
type callFrame struct { |
||||
Type string `json:"type"` |
||||
From string `json:"from"` |
||||
To string `json:"to,omitempty"` |
||||
Value string `json:"value,omitempty"` |
||||
Gas string `json:"gas"` |
||||
GasUsed string `json:"gasUsed"` |
||||
Input string `json:"input"` |
||||
Output string `json:"output,omitempty"` |
||||
Error string `json:"error,omitempty"` |
||||
Calls []callFrame `json:"calls,omitempty"` |
||||
} |
||||
|
||||
type callTracer struct { |
||||
callstack []callFrame |
||||
interrupt uint32 // Atomic flag to signal execution interruption
|
||||
reason error // Textual reason for the interruption
|
||||
} |
||||
|
||||
// NewCallTracer returns a native go tracer which tracks
|
||||
// call frames of a tx, and implements vm.EVMLogger.
|
||||
func NewCallTracer() tracers.Tracer { |
||||
// First callframe contains tx context info
|
||||
// and is populated on start and end.
|
||||
t := &callTracer{callstack: make([]callFrame, 1)} |
||||
return t |
||||
} |
||||
|
||||
func (t *callTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { |
||||
t.callstack[0] = callFrame{ |
||||
Type: "CALL", |
||||
From: addrToHex(from), |
||||
To: addrToHex(to), |
||||
Input: bytesToHex(input), |
||||
Gas: uintToHex(gas), |
||||
Value: bigToHex(value), |
||||
} |
||||
if create { |
||||
t.callstack[0].Type = "CREATE" |
||||
} |
||||
} |
||||
|
||||
func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { |
||||
t.callstack[0].GasUsed = uintToHex(gasUsed) |
||||
if err != nil { |
||||
t.callstack[0].Error = err.Error() |
||||
if err.Error() == "execution reverted" && len(output) > 0 { |
||||
t.callstack[0].Output = bytesToHex(output) |
||||
} |
||||
} else { |
||||
t.callstack[0].Output = bytesToHex(output) |
||||
} |
||||
} |
||||
|
||||
func (t *callTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { |
||||
} |
||||
|
||||
func (t *callTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { |
||||
} |
||||
|
||||
func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { |
||||
// Skip if tracing was interrupted
|
||||
if atomic.LoadUint32(&t.interrupt) > 0 { |
||||
// TODO: env.Cancel()
|
||||
return |
||||
} |
||||
|
||||
call := callFrame{ |
||||
Type: typ.String(), |
||||
From: addrToHex(from), |
||||
To: addrToHex(to), |
||||
Input: bytesToHex(input), |
||||
Gas: uintToHex(gas), |
||||
Value: bigToHex(value), |
||||
} |
||||
t.callstack = append(t.callstack, call) |
||||
} |
||||
|
||||
func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) { |
||||
size := len(t.callstack) |
||||
if size <= 1 { |
||||
return |
||||
} |
||||
// pop call
|
||||
call := t.callstack[size-1] |
||||
t.callstack = t.callstack[:size-1] |
||||
size -= 1 |
||||
|
||||
call.GasUsed = uintToHex(gasUsed) |
||||
if err == nil { |
||||
call.Output = bytesToHex(output) |
||||
} else { |
||||
call.Error = err.Error() |
||||
if call.Type == "CREATE" || call.Type == "CREATE2" { |
||||
call.To = "" |
||||
} |
||||
} |
||||
t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call) |
||||
} |
||||
|
||||
func (t *callTracer) GetResult() (json.RawMessage, error) { |
||||
if len(t.callstack) != 1 { |
||||
return nil, errors.New("incorrect number of top-level calls") |
||||
} |
||||
res, err := json.Marshal(t.callstack[0]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.RawMessage(res), t.reason |
||||
} |
||||
|
||||
func (t *callTracer) Stop(err error) { |
||||
t.reason = err |
||||
atomic.StoreUint32(&t.interrupt, 1) |
||||
} |
||||
|
||||
func bytesToHex(s []byte) string { |
||||
return "0x" + common.Bytes2Hex(s) |
||||
} |
||||
|
||||
func bigToHex(n *big.Int) string { |
||||
if n == nil { |
||||
return "" |
||||
} |
||||
return "0x" + n.Text(16) |
||||
} |
||||
|
||||
func uintToHex(n uint64) string { |
||||
return "0x" + strconv.FormatUint(n, 16) |
||||
} |
||||
|
||||
func addrToHex(a common.Address) string { |
||||
return strings.ToLower(a.Hex()) |
||||
} |
@ -0,0 +1,46 @@ |
||||
package native |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"math/big" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/vm" |
||||
"github.com/ethereum/go-ethereum/eth/tracers" |
||||
) |
||||
|
||||
func init() { |
||||
tracers.RegisterNativeTracer("noopTracerNative", NewNoopTracer) |
||||
} |
||||
|
||||
type noopTracer struct{} |
||||
|
||||
func NewNoopTracer() tracers.Tracer { |
||||
return &noopTracer{} |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { |
||||
} |
||||
|
||||
func (t *noopTracer) CaptureExit(output []byte, gasUsed uint64, err error) { |
||||
} |
||||
|
||||
func (t *noopTracer) GetResult() (json.RawMessage, error) { |
||||
return json.RawMessage(`{}`), nil |
||||
} |
||||
|
||||
func (t *noopTracer) Stop(err error) { |
||||
} |
@ -0,0 +1,246 @@ |
||||
package testing |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io/ioutil" |
||||
"math/big" |
||||
"path/filepath" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
"unicode" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
"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/eth/tracers" |
||||
"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/native" |
||||
) |
||||
|
||||
type callContext struct { |
||||
Number math.HexOrDecimal64 `json:"number"` |
||||
Difficulty *math.HexOrDecimal256 `json:"difficulty"` |
||||
Time math.HexOrDecimal64 `json:"timestamp"` |
||||
GasLimit math.HexOrDecimal64 `json:"gasLimit"` |
||||
Miner common.Address `json:"miner"` |
||||
} |
||||
|
||||
// callTrace is the result of a callTracer run.
|
||||
type callTrace struct { |
||||
Type string `json:"type"` |
||||
From common.Address `json:"from"` |
||||
To common.Address `json:"to"` |
||||
Input hexutil.Bytes `json:"input"` |
||||
Output hexutil.Bytes `json:"output"` |
||||
Gas *hexutil.Uint64 `json:"gas,omitempty"` |
||||
GasUsed *hexutil.Uint64 `json:"gasUsed,omitempty"` |
||||
Value *hexutil.Big `json:"value,omitempty"` |
||||
Error string `json:"error,omitempty"` |
||||
Calls []callTrace `json:"calls,omitempty"` |
||||
} |
||||
|
||||
// callTracerTest defines a single test to check the call tracer against.
|
||||
type callTracerTest struct { |
||||
Genesis *core.Genesis `json:"genesis"` |
||||
Context *callContext `json:"context"` |
||||
Input string `json:"input"` |
||||
Result *callTrace `json:"result"` |
||||
} |
||||
|
||||
// Iterates over all the input-output datasets in the tracer test harness and
|
||||
// runs the JavaScript tracers against them.
|
||||
func TestCallTracerLegacy(t *testing.T) { |
||||
testCallTracer("callTracerLegacy", "call_tracer_legacy", t) |
||||
} |
||||
|
||||
func TestCallTracer(t *testing.T) { |
||||
testCallTracer("callTracer", "call_tracer", t) |
||||
} |
||||
|
||||
func TestCallTracerNative(t *testing.T) { |
||||
testCallTracer("callTracerNative", "call_tracer", t) |
||||
} |
||||
|
||||
func testCallTracer(tracerName string, dirPath string, t *testing.T) { |
||||
files, err := ioutil.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() |
||||
|
||||
var ( |
||||
test = new(callTracerTest) |
||||
tx = new(types.Transaction) |
||||
) |
||||
// Call tracer test found, read if from disk
|
||||
if blob, err := ioutil.ReadFile(filepath.Join("..", "testdata", dirPath, file.Name())); err != nil { |
||||
t.Fatalf("failed to read testcase: %v", err) |
||||
} else if err := json.Unmarshal(blob, test); err != nil { |
||||
t.Fatalf("failed to parse testcase: %v", err) |
||||
} |
||||
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil { |
||||
t.Fatalf("failed to parse testcase input: %v", err) |
||||
} |
||||
// Configure a blockchain with the given prestate
|
||||
var ( |
||||
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: new(big.Int).SetUint64(uint64(test.Context.Time)), |
||||
Difficulty: (*big.Int)(test.Context.Difficulty), |
||||
GasLimit: uint64(test.Context.GasLimit), |
||||
} |
||||
_, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) |
||||
) |
||||
tracer, err := tracers.New(tracerName, new(tracers.Context)) |
||||
if err != nil { |
||||
t.Fatalf("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 { |
||||
t.Fatalf("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 { |
||||
t.Fatalf("failed to execute transaction: %v", err) |
||||
} |
||||
// Retrieve the trace result and compare against the etalon
|
||||
res, err := tracer.GetResult() |
||||
if err != nil { |
||||
t.Fatalf("failed to retrieve trace result: %v", err) |
||||
} |
||||
ret := new(callTrace) |
||||
if err := json.Unmarshal(res, ret); err != nil { |
||||
t.Fatalf("failed to unmarshal trace result: %v", err) |
||||
} |
||||
|
||||
if !jsonEqual(ret, test.Result) { |
||||
// uncomment this for easier debugging
|
||||
//have, _ := json.MarshalIndent(ret, "", " ")
|
||||
//want, _ := json.MarshalIndent(test.Result, "", " ")
|
||||
//t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))
|
||||
t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
|
||||
// comparison
|
||||
func jsonEqual(x, y interface{}) bool { |
||||
xTrace := new(callTrace) |
||||
yTrace := new(callTrace) |
||||
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) |
||||
} |
||||
|
||||
// camel converts a snake cased input string into a camel cased output.
|
||||
func camel(str string) string { |
||||
pieces := strings.Split(str, "_") |
||||
for i := 1; i < len(pieces); i++ { |
||||
pieces[i] = string(unicode.ToUpper(rune(pieces[i][0]))) + pieces[i][1:] |
||||
} |
||||
return strings.Join(pieces, "") |
||||
} |
||||
func BenchmarkTracers(b *testing.B) { |
||||
files, err := ioutil.ReadDir(filepath.Join("..", "testdata", "call_tracer")) |
||||
if err != nil { |
||||
b.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
|
||||
b.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(b *testing.B) { |
||||
blob, err := ioutil.ReadFile(filepath.Join("..", "testdata", "call_tracer", file.Name())) |
||||
if err != nil { |
||||
b.Fatalf("failed to read testcase: %v", err) |
||||
} |
||||
test := new(callTracerTest) |
||||
if err := json.Unmarshal(blob, test); err != nil { |
||||
b.Fatalf("failed to parse testcase: %v", err) |
||||
} |
||||
benchTracer("callTracerNative", test, b) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { |
||||
// Configure a blockchain with the given prestate
|
||||
tx := new(types.Transaction) |
||||
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil { |
||||
b.Fatalf("failed to parse testcase input: %v", err) |
||||
} |
||||
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number))) |
||||
msg, err := tx.AsMessage(signer, nil) |
||||
if err != nil { |
||||
b.Fatalf("failed to prepare transaction for tracing: %v", err) |
||||
} |
||||
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: new(big.Int).SetUint64(uint64(test.Context.Time)), |
||||
Difficulty: (*big.Int)(test.Context.Difficulty), |
||||
GasLimit: uint64(test.Context.GasLimit), |
||||
} |
||||
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) |
||||
|
||||
b.ReportAllocs() |
||||
b.ResetTimer() |
||||
for i := 0; i < b.N; i++ { |
||||
tracer, err := tracers.New(tracerName, new(tracers.Context)) |
||||
if err != nil { |
||||
b.Fatalf("failed to create call tracer: %v", err) |
||||
} |
||||
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer}) |
||||
snap := statedb.Snapshot() |
||||
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) |
||||
if _, err = st.TransitionDb(); err != nil { |
||||
b.Fatalf("failed to execute transaction: %v", err) |
||||
} |
||||
if _, err = tracer.GetResult(); err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
statedb.RevertToSnapshot(snap) |
||||
} |
||||
} |
Loading…
Reference in new issue