From 39ddcdc9dc28ae3483b14e44f69cada98b0f8f9f Mon Sep 17 00:00:00 2001 From: i-norden Date: Wed, 1 Nov 2023 15:03:56 -0500 Subject: [PATCH] tracing API for traceBlock and traceCall --- pkg/eth/api.go | 4 + pkg/eth/tracing.go | 407 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 pkg/eth/tracing.go diff --git a/pkg/eth/api.go b/pkg/eth/api.go index 8709754f..10c0beb7 100644 --- a/pkg/eth/api.go +++ b/pkg/eth/api.go @@ -1007,6 +1007,10 @@ func (diff *StateOverride) Apply(state *ipld_direct_state.StateDB) error { } } } + // Now finalize the changes. Finalize is normally performed between transactions. + // By using finalize, the overrides are semantically behaving as + // if they were created in a transaction just before the tracing occur. + state.Finalise(false) return nil } diff --git a/pkg/eth/tracing.go b/pkg/eth/tracing.go new file mode 100644 index 00000000..9fa64d7c --- /dev/null +++ b/pkg/eth/tracing.go @@ -0,0 +1,407 @@ +package eth + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "runtime" + "sync" + "time" + + ipld_direct_state "github.com/cerc-io/ipld-eth-statedb/direct_by_leaf" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core" + "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/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" +) + +const ( + // defaultTraceTimeout is the amount of time a single transaction can execute + // by default before being forcefully aborted. + defaultTraceTimeout = 5 * time.Second +) + +// TracingAPI is the collection of tracing APIs exposed over the private debugging endpoint. +type TracingAPI struct { + backend *Backend + rpc *rpc.Client + ethClient *ethclient.Client + config APIConfig +} + +// NewTracingAPI creates a new TracingAPI with the provided underlying Backend +func NewTracingAPI(b *Backend, client *rpc.Client, config APIConfig) (*TracingAPI, error) { + if b == nil { + return nil, errors.New("ipld-eth-server must be configured with an ethereum backend") + } + if config.ForwardEthCalls && client == nil { + return nil, errors.New("ipld-eth-server is configured to forward eth_calls to proxy node but no proxy node is configured") + } + if config.ForwardGetStorageAt && client == nil { + return nil, errors.New("ipld-eth-server is configured to forward eth_getStorageAt to proxy node but no proxy node is configured") + } + if config.ProxyOnError && client == nil { + return nil, errors.New("ipld-eth-server is configured to forward all calls to proxy node on errors but no proxy node is configured") + } + var ethClient *ethclient.Client + if client != nil { + ethClient = ethclient.NewClient(client) + } + return &TracingAPI{ + backend: b, + rpc: client, + ethClient: ethClient, + config: config, + }, nil +} + +// TraceConfig holds extra parameters to trace functions. +type TraceConfig struct { + *logger.Config + Tracer *string + Timeout *string + Reexec *uint64 + // Config specific to given tracer. Note struct logger + // config are historically embedded in main object. + TracerConfig json.RawMessage +} + +// TraceCallConfig is the config for traceCall API. It holds one more +// field to override the state for tracing. +type TraceCallConfig struct { + TraceConfig + StateOverrides *StateOverride + BlockOverrides *BlockOverrides +} + +// TraceCall lets you trace a given eth_call. It collects the structured logs +// created during the execution of EVM if the given transaction was added on +// top of the provided block and returns them as a JSON object. +func (api *TracingAPI) TraceCall(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, config *TraceCallConfig) (interface{}, error) { + // Try to retrieve the specified block + var ( + err error + block *types.Block + ) + if hash, ok := blockNrOrHash.Hash(); ok { + block, err = api.blockByHash(ctx, hash) + } else if number, ok := blockNrOrHash.Number(); ok { + if number == rpc.PendingBlockNumber { + // We don't have access to the miner here. For tracing 'future' transactions, + // it can be done with block- and state-overrides instead, which offers + // more flexibility and stability than trying to trace on 'pending', since + // the contents of 'pending' is unstable and probably not a true representation + // of what the next actual block is likely to contain. + return nil, errors.New("tracing on top of pending is not supported") + } + block, err = api.blockByNumber(ctx, number) + } else { + return nil, errors.New("invalid arguments; neither block nor hash specified") + } + if err != nil { + return nil, err + } + + stateDB, _, err := api.backend.IPLDDirectStateDBAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithHash(block.Hash(), true)) + if err != nil { + return nil, err + } + vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + // Apply the customization rules if required. + if config != nil { + if err := config.StateOverrides.Apply(stateDB); err != nil { + return nil, err + } + config.BlockOverrides.Apply(&vmctx) + } + // Execute the trace + msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee()) + if err != nil { + return nil, err + } + + var traceConfig *TraceConfig + if config != nil { + traceConfig = &config.TraceConfig + } + return api.traceTx(ctx, msg, new(tracers.Context), vmctx, stateDB, traceConfig) +} + +// traceTx configures a new tracer according to the provided configuration, and +// executes the given message in the provided environment. The return value will +// be tracer dependent. +func (api *TracingAPI) traceTx(ctx context.Context, message *core.Message, txctx *tracers.Context, vmctx vm.BlockContext, statedb *ipld_direct_state.StateDB, config *TraceConfig) (interface{}, error) { + var ( + tracer tracers.Tracer + err error + timeout = defaultTraceTimeout + txContext = core.NewEVMTxContext(message) + ) + if config == nil { + config = &TraceConfig{} + } + // Default tracer is the struct logger + tracer = logger.NewStructLogger(config.Config) + if config.Tracer != nil { + tracer, err = tracers.DefaultDirectory.New(*config.Tracer, txctx, config.TracerConfig) + if err != nil { + return nil, err + } + } + vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Tracer: tracer, NoBaseFee: true}) + + // Define a meaningful timeout of a single transaction trace + if config.Timeout != nil { + if timeout, err = time.ParseDuration(*config.Timeout); err != nil { + return nil, err + } + } + deadlineCtx, cancel := context.WithTimeout(ctx, timeout) + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + // Stop evm execution. Note cancellation is not necessarily immediate. + vmenv.Cancel() + } + }() + defer cancel() + + // Call Prepare to clear out the statedb access list + statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) + if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.GasLimit)); err != nil { + return nil, fmt.Errorf("tracing failed: %w", err) + } + return tracer.GetResult() +} + +// chainContext constructs the context reader which is used by the evm for reading +// the necessary chain context. +func (api *TracingAPI) chainContext(ctx context.Context) core.ChainContext { + return &chainContext{api: api, ctx: ctx} +} + +// blockByNumber is the wrapper of the chain access function offered by the backend. +// It will return an error if the block is not found. +func (api *TracingAPI) blockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + block, err := api.backend.BlockByNumber(ctx, number) + if err != nil { + return nil, err + } + if block == nil { + return nil, fmt.Errorf("block #%d not found", number) + } + return block, nil +} + +// blockByHash is the wrapper of the chain access function offered by the backend. +// It will return an error if the block is not found. +func (api *TracingAPI) blockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + block, err := api.backend.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + if block == nil { + return nil, fmt.Errorf("block %s not found", hash.Hex()) + } + return block, nil +} + +// txTraceTask represents a single transaction trace task when an entire block +// is being traced. +type txTraceTask struct { + statedb *ipld_direct_state.StateDB // Intermediate state prepped for tracing + index int // Transaction offset in the block +} + +// txTraceResult is the result of a single transaction trace. +type txTraceResult struct { + Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer +} + +// TraceBlock returns the structured logs created during the execution of EVM +// and returns them as a JSON object. +func (api *TracingAPI) TraceBlock(ctx context.Context, blob hexutil.Bytes, config *TraceConfig) ([]*txTraceResult, error) { + block := new(types.Block) + if err := rlp.Decode(bytes.NewReader(blob), block); err != nil { + return nil, fmt.Errorf("could not decode block: %v", err) + } + return api.traceBlock(ctx, block, config) +} + +// traceBlock configures a new tracer according to the provided configuration, and +// executes all the transactions contained within. The return value will be one item +// per transaction, dependent on the requested tracer. +func (api *TracingAPI) traceBlock(ctx context.Context, block *types.Block, config *TraceConfig) ([]*txTraceResult, error) { + if block.NumberU64() == 0 { + return nil, errors.New("genesis is not traceable") + } + stateDB, _, err := api.backend.IPLDDirectStateDBAndHeaderByNumberOrHash(ctx, rpc.BlockNumberOrHashWithHash(block.ParentHash(), true)) + if err != nil { + return nil, err + } + + // 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 := tracers.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, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) + txctx := &tracers.Context{ + BlockHash: blockHash, + BlockNumber: block.Number(), + 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 *TracingAPI) traceBlockParallel(ctx context.Context, block *types.Block, statedb *ipld_direct_state.StateDB, config *TraceConfig) ([]*txTraceResult, error) { + // Execute all the transaction contained within the block concurrently + var ( + 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) + for th := 0; th < threads; th++ { + pend.Add(1) + go func() { + defer pend.Done() + // Fetch and execute the next transaction trace tasks + for task := range jobs { + msg, _ := core.TransactionToMessage(txs[task.index], signer, block.BaseFee()) + txctx := &tracers.Context{ + BlockHash: blockHash, + BlockNumber: block.Number(), + TxIndex: task.index, + TxHash: txs[task.index].Hash(), + } + res, err := api.traceTx(ctx, msg, txctx, blockCtx, task.statedb, config) + if err != nil { + results[task.index] = &txTraceResult{Error: err.Error()} + continue + } + results[task.index] = &txTraceResult{Result: res} + } + }() + } + + // Feed the transactions into the tracers and return + var failed error +txloop: + for i, tx := range txs { + // Send the trace task over for execution + task := &txTraceTask{statedb: statedb.Copy(), index: i} + select { + case <-ctx.Done(): + failed = ctx.Err() + break txloop + case jobs <- task: + } + + // Generate the next state snapshot fast without tracing + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) + statedb.SetTxContext(tx.Hash(), i) + vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit)); err != nil { + failed = err + break txloop + } + // 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(vmenv.ChainConfig().IsEIP158(block.Number())) + } + + close(jobs) + pend.Wait() + + // If execution failed in between, abort + if failed != nil { + return nil, failed + } + return results, nil +} + +// blockByNumberAndHash is the wrapper of the chain access function offered by +// the backend. It will return an error if the block is not found. +// +// Note this function is friendly for the light client which can only retrieve the +// historical(before the CHT) header/block by number. +func (api *TracingAPI) blockByNumberAndHash(ctx context.Context, number rpc.BlockNumber, hash common.Hash) (*types.Block, error) { + block, err := api.blockByNumber(ctx, number) + if err != nil { + return nil, err + } + if block.Hash() == hash { + return block, nil + } + return api.blockByHash(ctx, hash) +} + +type chainContext struct { + api *TracingAPI + ctx context.Context +} + +func (context *chainContext) Engine() consensus.Engine { + return context.api.backend.Engine() +} + +func (context *chainContext) GetHeader(hash common.Hash, number uint64) *types.Header { + header, err := context.api.backend.HeaderByNumber(context.ctx, rpc.BlockNumber(number)) + if err != nil { + return nil + } + if header.Hash() == hash { + return header + } + header, err = context.api.backend.HeaderByHash(context.ctx, hash) + if err != nil { + return nil + } + return header +}