Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth/tracers: support for golang tracers + add golang callTracer #23708

Merged
merged 31 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
af67e1d
eth/tracers: add basic native loader
s1na Oct 11, 2021
57c993b
eth/tracers: add GetResult to tracer interface
s1na Oct 11, 2021
c9c1dc9
eth/tracers: add native call tracer
s1na Oct 11, 2021
340c773
eth/tracers: fix call tracer json result
s1na Oct 12, 2021
bf165bb
eth/tracers: minor fix
s1na Oct 12, 2021
4ea0d15
eth/tracers: fix
s1na Oct 12, 2021
75c6aff
eth/tracers: fix benchTracer
s1na Oct 13, 2021
7b10534
eth/tracers: test native call tracer
s1na Oct 13, 2021
ca5427a
eth/tracers: fix
s1na Oct 13, 2021
d9371f3
eth/tracers: rm extra make
s1na Oct 13, 2021
aff90d3
eth/tracers: rm extra make
s1na Oct 13, 2021
a2b0d3d
eth/tracers: make callFrame private
s1na Oct 13, 2021
18c4c17
eth/tracers: clean-up and comments
s1na Oct 13, 2021
0946623
eth/tracers: add license
s1na Oct 26, 2021
5db19c5
eth/tracers: rework the model a bit
holiman Oct 26, 2021
360e3ba
eth/tracers: move tracecall tests to subpackage
holiman Oct 28, 2021
c55a903
cmd/geth: load native tracers
s1na Nov 2, 2021
a9c2051
eth/tracers: minor fix
s1na Nov 2, 2021
c242c82
eth/tracers: impl stop
s1na Nov 2, 2021
60f5734
eth/tracers: add native noop tracer
s1na Nov 2, 2021
89bac76
renamings
s1na Nov 3, 2021
91a7c09
eth/tracers: more renamings
s1na Nov 3, 2021
2a69a37
eth/tracers: make jstracer non-exported, avoid cast
holiman Nov 3, 2021
4e2de46
eth/tracers, core/vm: rename vm.Tracer to vm.EVMLogger for clarity
holiman Nov 3, 2021
de953f5
eth/tracers: minor comment fix
s1na Nov 4, 2021
2af565b
eth/tracers/testing: lint nitpicks
holiman Nov 4, 2021
a839259
Merge branch 'master' into drop-in-plugin
holiman Nov 4, 2021
01bb908
core,eth: cancel evm on nativecalltracer stop
s1na Nov 4, 2021
2de2d6c
Revert "core,eth: cancel evm on nativecalltracer stop"
s1na Nov 4, 2021
a9b91d7
eth/tracers: linter nits
holiman Nov 4, 2021
8a5d5f0
eth/tracers: fix output on err
s1na Nov 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions eth/tracers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers/native"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
Expand Down Expand Up @@ -858,19 +859,23 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
return nil, err
}
}
// Constuct the JavaScript tracer to execute with
if tracer, err = New(*config.Tracer, txctx); err != nil {
return nil, err
}
// Handle timeouts and RPC cancellations
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
go func() {
<-deadlineCtx.Done()
if deadlineCtx.Err() == context.DeadlineExceeded {
tracer.(*Tracer).Stop(errors.New("execution timeout"))
// Native tracers take precedence
var ok bool
if tracer, ok = native.New(*config.Tracer); !ok {
if tracer, err = New(*config.Tracer, txctx); err != nil {
return nil, err
}
}()
defer cancel()
// TODO(s1na): do we need timeout for native tracers?
// Handle timeouts and RPC cancellations
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
go func() {
<-deadlineCtx.Done()
if deadlineCtx.Err() == context.DeadlineExceeded {
tracer.(*Tracer).Stop(errors.New("execution timeout"))
}
}()
defer cancel()
}

case config == nil:
tracer = vm.NewStructLogger(nil)
Expand Down Expand Up @@ -904,7 +909,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
}, nil

case *Tracer:
case native.Tracer:
return tracer.GetResult()

default:
Expand Down
135 changes: 135 additions & 0 deletions eth/tracers/native/call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package native
s1na marked this conversation as resolved.
Show resolved Hide resolved

import (
"encoding/json"
"errors"
"math/big"
"strconv"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)

func init() {
register("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
}

// NewCallTracer returns a native go tracer which tracks
// call frames of a tx, and implements vm.Tracer.
func NewCallTracer() 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].Output = bytesToHex(output)
t.callstack[0].GasUsed = uintToHex(gasUsed)
if err != nil {
t.callstack[0].Error = err.Error()
}
}

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) {
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), nil
}

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())
}
35 changes: 35 additions & 0 deletions eth/tracers/native/native.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package native

import (
"encoding/json"

"github.com/ethereum/go-ethereum/core/vm"
)

// Tracer interface extends vm.Tracer and additionally
// allows collecting the tracing result.
type Tracer interface {
vm.Tracer
GetResult() (json.RawMessage, error)
}
s1na marked this conversation as resolved.
Show resolved Hide resolved

// constructor creates a new instance of a Tracer.
type constructor func() Tracer

var tracers map[string]constructor = make(map[string]constructor)

// register makes native tracers in this directory which adhere
// to the `Tracer` interface available to the rest of the codebase.
// It is typically invoked in the `init()` function.
func register(name string, fn constructor) {
tracers[name] = fn
}
s1na marked this conversation as resolved.
Show resolved Hide resolved

// New returns a new instance of a tracer, if one was
// registered under the given name.
func New(name string) (Tracer, bool) {
if fn, ok := tracers[name]; ok {
return fn(), true
}
return nil, false
}
72 changes: 52 additions & 20 deletions eth/tracers/tracers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/native"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/tests"
Expand Down Expand Up @@ -204,10 +205,42 @@ func TestPrestateTracerCreate2(t *testing.T) {
// 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)
newTracer := func() native.Tracer {
tracer, err := New("callTracerLegacy", new(Context))
if err != nil {
t.Fatalf("failed to create call tracer: %v", err)
}
return tracer
}

testCallTracer(newTracer, "call_tracer_legacy", t)
}

func TestCallTracer(t *testing.T) {
newTracer := func() native.Tracer {
tracer, err := New("callTracer", new(Context))
if err != nil {
t.Fatalf("failed to create call tracer: %v", err)
}
return tracer
}

testCallTracer(newTracer, "call_tracer", t)
}

func TestCallTracerNative(t *testing.T) {
newTracer := func() native.Tracer {
tracer, ok := native.New("callTracerNative")
if !ok {
t.Fatal("failed to create native call tracer")
}
return tracer
}

testCallTracer(newTracer, "call_tracer", t)
}

func testCallTracer(tracer string, dirPath string, t *testing.T) {
func testCallTracer(newTracer func() native.Tracer, 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)
Expand Down Expand Up @@ -251,11 +284,7 @@ func testCallTracer(tracer string, dirPath string, t *testing.T) {
}
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)

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

msg, err := tx.AsMessage(signer, nil)
Expand Down Expand Up @@ -287,10 +316,6 @@ func testCallTracer(tracer string, dirPath string, t *testing.T) {
}
}

func TestCallTracer(t *testing.T) {
testCallTracer("callTracer", "call_tracer", t)
}

// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
// comparison
func jsonEqual(x, y interface{}) bool {
Expand Down Expand Up @@ -406,12 +431,19 @@ func BenchmarkTracers(b *testing.B) {
if err := json.Unmarshal(blob, test); err != nil {
b.Fatalf("failed to parse testcase: %v", err)
}
benchTracer("callTracer", test, b)
newTracer := func() native.Tracer {
tracer, err := New("callTracer", new(Context))
if err != nil {
b.Fatalf("failed to create call tracer: %v", err)
}
return tracer
}
benchTracer(newTracer, test, b)
})
}
}

func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
func benchTracer(newTracer func() native.Tracer, 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 {
Expand All @@ -438,21 +470,21 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
}
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)

// Create the tracer, the EVM environment and run it
tracer, err := New(tracerName, new(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})

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tracer := newTracer()
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)
}
_, err := tracer.GetResult()
if err != nil {
b.Fatal(err)
}

statedb.RevertToSnapshot(snap)
}
}