From 2c6dda5ad7a720cccd957230f7978de0082ec8c7 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 10 Jan 2023 09:11:53 -0500 Subject: [PATCH] eth/tracers: use non-threaded tracechain (#24283) This makes non-JS tracers execute all block txs on a single goroutine. In the previous implementation, we used to prepare every tx pre-state on one goroutine, and then run the transactions again with tracing enabled. Native tracers are usually faster, so it is faster overall to use their output as the pre-state for tracing the next transaction. Co-authored-by: Sina Mahmoodi --- core/vm/runtime/runtime_test.go | 6 +- eth/tracers/api.go | 57 +++++++++++-- .../internal/tracetest/calltrace_test.go | 6 +- .../internal/tracetest/prestate_test.go | 2 +- eth/tracers/js/goja.go | 21 +++-- eth/tracers/native/4byte.go | 2 +- eth/tracers/native/call.go | 2 +- eth/tracers/native/mux.go | 4 +- eth/tracers/native/noop.go | 2 +- eth/tracers/native/prestate.go | 2 +- eth/tracers/native/tracer.go | 79 ------------------- eth/tracers/tracers.go | 67 ++++++++++------ 12 files changed, 121 insertions(+), 129 deletions(-) delete mode 100644 eth/tracers/native/tracer.go diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index ab77e284d..868d41e50 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -333,7 +333,7 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) cfg.GasLimit = gas if len(tracerCode) > 0 { - tracer, err := tracers.New(tracerCode, new(tracers.Context), nil) + tracer, err := tracers.DefaultDirectory.New(tracerCode, new(tracers.Context), nil) if err != nil { b.Fatal(err) } @@ -832,7 +832,7 @@ func TestRuntimeJSTracer(t *testing.T) { statedb.SetCode(common.HexToAddress("0xee"), calleeCode) statedb.SetCode(common.HexToAddress("0xff"), depressedCode) - tracer, err := tracers.New(jsTracer, new(tracers.Context), nil) + tracer, err := tracers.DefaultDirectory.New(jsTracer, new(tracers.Context), nil) if err != nil { t.Fatal(err) } @@ -868,7 +868,7 @@ func TestJSTracerCreateTx(t *testing.T) { code := []byte{byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN)} statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) - tracer, err := tracers.New(jsTracer, new(tracers.Context), nil) + tracer, err := tracers.DefaultDirectory.New(jsTracer, new(tracers.Context), nil) if err != nil { t.Fatal(err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 4436d1396..5a34d9d4a 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -593,6 +593,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac if block.NumberU64() == 0 { return nil, errors.New("genesis is not traceable") } + // Prepare base state parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash()) if err != nil { return nil, err @@ -607,23 +608,64 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac } defer release() + // JS tracers have high overhead. In this case run a parallel + // process that generates states in one thread and traces txes + // in separate worker threads. + if config != nil && config.Tracer != nil && *config.Tracer != "" { + if isJS := DefaultDirectory.IsJS(*config.Tracer); isJS { + return api.traceBlockParallel(ctx, block, statedb, config) + } + } + // Native tracers have low overhead + var ( + txs = block.Transactions() + blockHash = block.Hash() + is158 = api.backend.ChainConfig().IsEIP158(block.Number()) + blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) + results = make([]*txTraceResult, len(txs)) + ) + for i, tx := range txs { + // Generate the next state snapshot fast without tracing + msg, _ := tx.AsMessage(signer, block.BaseFee()) + txctx := &Context{ + BlockHash: blockHash, + TxIndex: i, + TxHash: tx.Hash(), + } + res, err := api.traceTx(ctx, msg, txctx, blockCtx, statedb, config) + if err != nil { + return nil, err + } + results[i] = &txTraceResult{Result: res} + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(is158) + } + return results, nil +} + +// traceBlockParallel is for tracers that have a high overhead (read JS tracers). One thread +// runs along and executes txes without tracing enabled to generate their prestate. +// Worker threads take the tasks and the prestate and trace them. +func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, statedb *state.StateDB, config *TraceConfig) ([]*txTraceResult, error) { // Execute all the transaction contained within the block concurrently var ( - signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) - txs = block.Transactions() - results = make([]*txTraceResult, len(txs)) - pend sync.WaitGroup + txs = block.Transactions() + blockHash = block.Hash() + blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) + results = make([]*txTraceResult, len(txs)) + pend sync.WaitGroup ) threads := runtime.NumCPU() if threads > len(txs) { threads = len(txs) } jobs := make(chan *txTraceTask, threads) - blockHash := block.Hash() for th := 0; th < threads; th++ { pend.Add(1) go func() { - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) defer pend.Done() // Fetch and execute the next transaction trace tasks for task := range jobs { @@ -645,7 +687,6 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac // Feed the transactions into the tracers and return var failed error - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) txloop: for i, tx := range txs { // Send the trace task over for execution @@ -923,7 +964,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex // Default tracer is the struct logger tracer = logger.NewStructLogger(config.Config) if config.Tracer != nil { - tracer, err = New(*config.Tracer, txctx, config.TracerConfig) + tracer, err = DefaultDirectory.New(*config.Tracer, txctx, config.TracerConfig) if err != nil { return nil, err } diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index 0827d3b40..5cfb5b33c 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -140,7 +140,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { } _, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) ) - tracer, err := tracers.New(tracerName, new(tracers.Context), test.TracerConfig) + tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), test.TracerConfig) if err != nil { t.Fatalf("failed to create call tracer: %v", err) } @@ -243,7 +243,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - tracer, err := tracers.New(tracerName, new(tracers.Context), nil) + tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), nil) if err != nil { b.Fatalf("failed to create call tracer: %v", err) } @@ -309,7 +309,7 @@ func TestZeroValueToNotExitCall(t *testing.T) { } _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) // Create the tracer, the EVM environment and run it - tracer, err := tracers.New("callTracer", nil, nil) + tracer, err := tracers.DefaultDirectory.New("callTracer", nil, nil) if err != nil { t.Fatalf("failed to create call tracer: %v", err) } diff --git a/eth/tracers/internal/tracetest/prestate_test.go b/eth/tracers/internal/tracetest/prestate_test.go index 9227aff94..2fee7d6fb 100644 --- a/eth/tracers/internal/tracetest/prestate_test.go +++ b/eth/tracers/internal/tracetest/prestate_test.go @@ -110,7 +110,7 @@ func testPrestateDiffTracer(tracerName string, dirPath string, t *testing.T) { } _, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) ) - tracer, err := tracers.New(tracerName, new(tracers.Context), test.TracerConfig) + tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), test.TracerConfig) if err != nil { t.Fatalf("failed to create call tracer: %v", err) } diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go index cf27acbb4..8e52f5b21 100644 --- a/eth/tracers/js/goja.go +++ b/eth/tracers/js/goja.go @@ -45,7 +45,16 @@ func init() { if err != nil { panic(err) } - tracers.RegisterLookup(true, newJsTracer) + type ctorFn = func(*tracers.Context, json.RawMessage) (tracers.Tracer, error) + lookup := func(code string) ctorFn { + return func(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + return newJsTracer(code, ctx, cfg) + } + } + for name, code := range assetTracers { + tracers.DefaultDirectory.Register(name, lookup(code), true) + } + tracers.DefaultDirectory.RegisterJSEval(newJsTracer) } // bigIntProgram is compiled once and the exported function mostly invoked to convert @@ -122,16 +131,14 @@ type jsTracer struct { frameResultValue goja.Value } -// newJsTracer instantiates a new JS tracer instance. code is either -// the name of a built-in JS tracer or a Javascript snippet which -// evaluates to an expression returning an object with certain methods. +// newJsTracer instantiates a new JS tracer instance. code is a +// Javascript snippet which evaluates to an expression returning +// an object with certain methods: +// // The methods `result` and `fault` are required to be present. // The methods `step`, `enter`, and `exit` are optional, but note that // `enter` and `exit` always go together. func newJsTracer(code string, ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { - if c, ok := assetTracers[code]; ok { - code = c - } vm := goja.New() // By default field names are exported to JS as is, i.e. capitalized. vm.SetFieldNameMapper(goja.UncapFieldNameMapper()) diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index afe70ea08..1b4649baa 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -28,7 +28,7 @@ import ( ) func init() { - register("4byteTracer", newFourByteTracer) + tracers.DefaultDirectory.Register("4byteTracer", newFourByteTracer, false) } // fourByteTracer searches for 4byte-identifiers, and collects them for post-processing. diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 24fd40639..5bf49e744 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -32,7 +32,7 @@ import ( //go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go func init() { - register("callTracer", newCallTracer) + tracers.DefaultDirectory.Register("callTracer", newCallTracer, false) } type callLog struct { diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index 878e2dc9d..db8ddd643 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -26,7 +26,7 @@ import ( ) func init() { - register("muxTracer", newMuxTracer) + tracers.DefaultDirectory.Register("muxTracer", newMuxTracer, false) } // muxTracer is a go implementation of the Tracer interface which @@ -47,7 +47,7 @@ func newMuxTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, er objects := make([]tracers.Tracer, 0, len(config)) names := make([]string, 0, len(config)) for k, v := range config { - t, err := tracers.New(k, ctx, v) + t, err := tracers.DefaultDirectory.New(k, ctx, v) if err != nil { return nil, err } diff --git a/eth/tracers/native/noop.go b/eth/tracers/native/noop.go index c1035bd1b..3beecd8ab 100644 --- a/eth/tracers/native/noop.go +++ b/eth/tracers/native/noop.go @@ -26,7 +26,7 @@ import ( ) func init() { - register("noopTracer", newNoopTracer) + tracers.DefaultDirectory.Register("noopTracer", newNoopTracer, false) } // noopTracer is a go implementation of the Tracer interface which diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index 10008699b..948d09ef7 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -32,7 +32,7 @@ import ( //go:generate go run github.com/fjl/gencodec -type account -field-override accountMarshaling -out gen_account_json.go func init() { - register("prestateTracer", newPrestateTracer) + tracers.DefaultDirectory.Register("prestateTracer", newPrestateTracer, false) } type state = map[common.Address]*account diff --git a/eth/tracers/native/tracer.go b/eth/tracers/native/tracer.go deleted file mode 100644 index f70d4b2af..000000000 --- a/eth/tracers/native/tracer.go +++ /dev/null @@ -1,79 +0,0 @@ -// 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 . - -// Package native is a collection of tracers written in go. -// -// In order to add a native tracer and have it compiled into the binary, a new -// file needs to be added to this folder, containing an implementation of the -// `eth.tracers.Tracer` interface. -// -// Aside from implementing the tracer, it also needs to register itself, using the -// `register` method -- and this needs to be done in the package initialization. -// -// Example: -// -// func init() { -// register("noopTracerNative", newNoopTracer) -// } -package native - -import ( - "encoding/json" - "errors" - - "github.com/ethereum/go-ethereum/eth/tracers" -) - -// init registers itself this packages as a lookup for tracers. -func init() { - tracers.RegisterLookup(false, lookup) -} - -// ctorFn is the constructor signature of a native tracer. -type ctorFn = func(*tracers.Context, json.RawMessage) (tracers.Tracer, error) - -/* -ctors is a map of package-local tracer constructors. - -We cannot be certain about the order of init-functions within a package, -The go spec (https://golang.org/ref/spec#Package_initialization) says - -> To ensure reproducible initialization behavior, build systems -> are encouraged to present multiple files belonging to the same -> package in lexical file name order to a compiler. - -Hence, we cannot make the map in init, but must make it upon first use. -*/ -var ctors map[string]ctorFn - -// register is used by native tracers to register their presence. -func register(name string, ctor ctorFn) { - if ctors == nil { - ctors = make(map[string]ctorFn) - } - ctors[name] = ctor -} - -// lookup returns a tracer, if one can be matched to the given name. -func lookup(name string, ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { - if ctors == nil { - ctors = make(map[string]ctorFn) - } - if ctor, ok := ctors[name]; ok { - return ctor(ctx, cfg) - } - return nil, errors.New("no tracer found") -} diff --git a/eth/tracers/tracers.go b/eth/tracers/tracers.go index 3d2d1256c..b93f7db6f 100644 --- a/eth/tracers/tracers.go +++ b/eth/tracers/tracers.go @@ -19,7 +19,6 @@ package tracers import ( "encoding/json" - "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -42,31 +41,55 @@ type Tracer interface { Stop(err error) } -type lookupFunc func(string, *Context, json.RawMessage) (Tracer, error) +type ctorFn func(*Context, json.RawMessage) (Tracer, error) +type jsCtorFn func(string, *Context, json.RawMessage) (Tracer, error) -var ( - lookups []lookupFunc -) +type elem struct { + ctor ctorFn + isJS bool +} -// RegisterLookup registers a method as a lookup for tracers, meaning that -// users can invoke a named tracer through that lookup. If 'wildcard' is true, -// then the lookup will be placed last. This is typically meant for interpreted -// engines (js) which can evaluate dynamic user-supplied code. -func RegisterLookup(wildcard bool, lookup lookupFunc) { - if wildcard { - lookups = append(lookups, lookup) - } else { - lookups = append([]lookupFunc{lookup}, lookups...) - } +// DefaultDirectory is the collection of tracers bundled by default. +var DefaultDirectory = directory{elems: make(map[string]elem)} + +// directory provides functionality to lookup a tracer by name +// and a function to instantiate it. It falls back to a JS code evaluator +// if no tracer of the given name exists. +type directory struct { + elems map[string]elem + jsEval jsCtorFn +} + +// Register registers a method as a lookup for tracers, meaning that +// users can invoke a named tracer through that lookup. +func (d *directory) Register(name string, f ctorFn, isJS bool) { + d.elems[name] = elem{ctor: f, isJS: isJS} +} + +// RegisterJSEval registers a tracer that is able to parse +// dynamic user-provided JS code. +func (d *directory) RegisterJSEval(f jsCtorFn) { + d.jsEval = f } // New returns a new instance of a tracer, by iterating through the -// registered lookups. -func New(code string, ctx *Context, cfg json.RawMessage) (Tracer, error) { - for _, lookup := range lookups { - if tracer, err := lookup(code, ctx, cfg); err == nil { - return tracer, nil - } +// registered lookups. Name is either name of an existing tracer +// or an arbitrary JS code. +func (d *directory) New(name string, ctx *Context, cfg json.RawMessage) (Tracer, error) { + if elem, ok := d.elems[name]; ok { + return elem.ctor(ctx, cfg) } - return nil, errors.New("tracer not found") + // Assume JS code + return d.jsEval(name, ctx, cfg) +} + +// IsJS will return true if the given tracer will evaluate +// JS code. Because code evaluation has high overhead, this +// info will be used in determining fast and slow code paths. +func (d *directory) IsJS(name string) bool { + if elem, ok := d.elems[name]; ok { + return elem.isJS + } + // JS eval will execute JS code + return true }