429 lines
15 KiB
Go
429 lines
15 KiB
Go
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/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
|
|
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")
|
|
}
|
|
return &TracingAPI{
|
|
backend: b,
|
|
rpc: client,
|
|
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) {
|
|
trace, err := api.localTraceCall(ctx, args, blockNrOrHash, config)
|
|
if trace != nil && err == nil {
|
|
return trace, nil
|
|
}
|
|
if api.config.ProxyOnError {
|
|
var res interface{}
|
|
if err := api.rpc.CallContext(ctx, &res, "debug_traceCall", args, blockNrOrHash, config); res != nil && err == nil {
|
|
return res, nil
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func (api *TracingAPI) localTraceCall(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) {
|
|
trace, err := api.localTraceBlock(ctx, blob, config)
|
|
if trace != nil && err == nil {
|
|
return trace, nil
|
|
}
|
|
if api.config.ProxyOnError {
|
|
var res []*txTraceResult
|
|
if err := api.rpc.CallContext(ctx, &res, "debug_traceBlock", blob, config); res != nil && err == nil {
|
|
return res, nil
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func (api *TracingAPI) localTraceBlock(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
|
|
}
|