eth/tracers: initiate rework of tracer code to deduplicate block/tx tracer paths

This commit is contained in:
Jared Wasinger 2025-04-30 17:53:22 +08:00
parent 51b34efebc
commit 0808452e66

View file

@ -244,10 +244,6 @@ func (api *API) TraceChain(ctx context.Context, start, end rpc.BlockNumber, conf
// transaction, dependent on the requested tracer. // transaction, dependent on the requested tracer.
// The tracing procedure should be aborted in case the closed signal is received. // The tracing procedure should be aborted in case the closed signal is received.
func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed <-chan error) chan *blockTraceResult { func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed <-chan error) chan *blockTraceResult {
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
blocks := int(end.NumberU64() - start.NumberU64()) blocks := int(end.NumberU64() - start.NumberU64())
threads := runtime.NumCPU() threads := runtime.NumCPU()
if threads > blocks { if threads > blocks {
@ -259,7 +255,19 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
taskCh = make(chan *blockTraceTask, threads) taskCh = make(chan *blockTraceTask, threads)
resCh = make(chan *blockTraceTask, threads) resCh = make(chan *blockTraceTask, threads)
tracker = newStateTracker(maximumPendingTraceStates, start.NumberU64()) tracker = newStateTracker(maximumPendingTraceStates, start.NumberU64())
err error
) )
opt, err := traceExecOpt(config)
if err != nil {
log.Warn("invalid trace configuration", "err", err)
return nil
}
env := envFromTraceConfig(api, config)
instantiateTracer := func(ctx *Context) (*Tracer, error) {
return api.instantiateTracer(config, ctx)
}
for th := 0; th < threads; th++ { for th := 0; th < threads; th++ {
pend.Add(1) pend.Add(1)
go func() { go func() {
@ -267,26 +275,10 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
// Fetch and execute the block trace taskCh // Fetch and execute the block trace taskCh
for task := range taskCh { for task := range taskCh {
var ( task.results, err = api.traceBlock(ctx, task.block, env, opt, instantiateTracer)
signer = types.MakeSigner(api.backend.ChainConfig(), task.block.Number(), task.block.Time()) if err != nil {
blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil) log.Warn("Tracing failed", "err", err)
) break
// Trace all the transactions contained within
for i, tx := range task.block.Transactions() {
msg, _ := core.TransactionToMessage(tx, signer, task.block.BaseFee())
txctx := &Context{
BlockHash: task.block.Hash(),
BlockNumber: task.block.Number(),
TxIndex: i,
TxHash: tx.Hash(),
}
res, err := api.traceTx(ctx, tx, msg, txctx, blockCtx, task.statedb, config, nil)
if err != nil {
task.results[i] = &txTraceResult{TxHash: tx.Hash(), Error: err.Error()}
log.Warn("Tracing failed", "hash", tx.Hash(), "block", task.block.NumberU64(), "err", err)
break
}
task.results[i] = &txTraceResult{TxHash: tx.Hash(), Result: res}
} }
// Tracing state is used up, queue it for de-referencing. Note the // Tracing state is used up, queue it for de-referencing. Note the
// state is the parent state of trace block, use block.number-1 as // state is the parent state of trace block, use block.number-1 as
@ -373,22 +365,11 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
s1, s2, s3 := statedb.Database().TrieDB().Size() s1, s2, s3 := statedb.Database().TrieDB().Size()
preferDisk = s1+s2+s3 > defaultTracechainMemLimit preferDisk = s1+s2+s3 > defaultTracechainMemLimit
} }
statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, statedb, false, preferDisk) statedb, _, release, err = api.retrieveBlockState(ctx, block, opt, false, preferDisk)
if err != nil { if err != nil {
failed = err failed = err
break break
} }
// Insert block's parent beacon block root in the state
// as per EIP-4788.
context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(context, statedb, api.backend.ChainConfig(), vm.Config{})
if beaconRoot := next.BeaconRoot(); beaconRoot != nil {
core.ProcessBeaconBlockRoot(*beaconRoot, evm)
}
// Insert parent hash in history contract.
if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) {
core.ProcessParentBlockHash(next.ParentHash(), evm)
}
// Clean out any pending release functions of trace state. Note this // Clean out any pending release functions of trace state. Note this
// step must be done after constructing tracing state, because the // step must be done after constructing tracing state, because the
// tracing state of block next depends on the parent state and construction // tracing state of block next depends on the parent state and construction
@ -441,6 +422,18 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed
return retCh return retCh
} }
func (api *API) traceBlockCustomTracer(ctx context.Context, block *types.Block, config *TraceConfig) ([]*txTraceResult, error) {
instantiateTracer := func(ctx *Context) (*Tracer, error) {
return api.instantiateTracer(config, ctx)
}
env := envFromTraceConfig(api, config)
opt, err := traceExecOpt(config)
if err != nil {
return nil, err
}
return api.traceBlock(ctx, block, env, opt, instantiateTracer)
}
// TraceBlockByNumber returns the structured logs created during the execution of // TraceBlockByNumber returns the structured logs created during the execution of
// EVM and returns them as a JSON object. // EVM and returns them as a JSON object.
func (api *API) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceConfig) ([]*txTraceResult, error) { func (api *API) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceConfig) ([]*txTraceResult, error) {
@ -448,7 +441,7 @@ func (api *API) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber,
if err != nil { if err != nil {
return nil, err return nil, err
} }
return api.traceBlock(ctx, block, config) return api.traceBlockCustomTracer(ctx, block, config)
} }
// TraceBlockByHash returns the structured logs created during the execution of // TraceBlockByHash returns the structured logs created during the execution of
@ -458,7 +451,7 @@ func (api *API) TraceBlockByHash(ctx context.Context, hash common.Hash, config *
if err != nil { if err != nil {
return nil, err return nil, err
} }
return api.traceBlock(ctx, block, config) return api.traceBlockCustomTracer(ctx, block, config)
} }
// TraceBlock returns the structured logs created during the execution of EVM // TraceBlock returns the structured logs created during the execution of EVM
@ -468,7 +461,7 @@ func (api *API) TraceBlock(ctx context.Context, blob hexutil.Bytes, config *Trac
if err := rlp.DecodeBytes(blob, block); err != nil { if err := rlp.DecodeBytes(blob, block); err != nil {
return nil, fmt.Errorf("could not decode block: %v", err) return nil, fmt.Errorf("could not decode block: %v", err)
} }
return api.traceBlock(ctx, block, config) return api.traceBlockCustomTracer(ctx, block, config)
} }
// TraceBlockFromFile returns the structured logs created during the execution of // TraceBlockFromFile returns the structured logs created during the execution of
@ -489,10 +482,20 @@ func (api *API) TraceBadBlock(ctx context.Context, hash common.Hash, config *Tra
if block == nil { if block == nil {
return nil, fmt.Errorf("bad block %#x not found", hash) return nil, fmt.Errorf("bad block %#x not found", hash)
} }
return api.traceBlock(ctx, block, config) instantiateTracer := func(ctx *Context) (*Tracer, error) {
return api.instantiateTracer(config, ctx)
}
// TODO: investigate whether the canon field has different treatment when tracing bad blocks
env := envFromTraceConfig(api, config)
opt, err := traceExecOpt(config)
if err != nil {
return nil, err
}
return api.traceBlock(ctx, block, env, opt, instantiateTracer)
} }
// StandardTraceBlockToFile dumps the structured logs created during the // sstandardTraceBlockToFile dumps the structured logs created during the
// execution of EVM to the local file system and returns a list of files // execution of EVM to the local file system and returns a list of files
// to the caller. // to the caller.
func (api *API) StandardTraceBlockToFile(ctx context.Context, hash common.Hash, config *StdTraceConfig) ([]string, error) { func (api *API) StandardTraceBlockToFile(ctx context.Context, hash common.Hash, config *StdTraceConfig) ([]string, error) {
@ -578,30 +581,73 @@ func (api *API) StandardTraceBadBlockToFile(ctx context.Context, hash common.Has
return api.standardTraceBlockToFile(ctx, block, config) return api.standardTraceBlockToFile(ctx, block, config)
} }
// traceBlock configures a new tracer according to the provided configuration, and type traceExecOptions struct {
// executes all the transactions contained within. The return value will be one item txTimeout time.Duration
// per transaction, dependent on the requested tracer. reexec uint64
func (api *API) traceBlock(ctx context.Context, block *types.Block, config *TraceConfig) ([]*txTraceResult, error) { parallel bool
}
// chain configuration and flag for whether the config is canonical
type traceEnv struct {
canon bool
chainConfig *params.ChainConfig
}
func envFromStdConfig(api *API, config *StdTraceConfig) *traceEnv {
if config == nil {
return &traceEnv{
canon: true,
chainConfig: api.backend.ChainConfig(),
}
}
return envFromLoggerConfig(api, &config.Config)
}
func envFromTraceConfig(api *API, config *TraceConfig) *traceEnv {
if config == nil {
return &traceEnv{
canon: true,
chainConfig: api.backend.ChainConfig(),
}
}
return envFromLoggerConfig(api, config.Config)
}
func envFromLoggerConfig(api *API, config *logger.Config) *traceEnv {
if config == nil {
return &traceEnv{
canon: true,
chainConfig: api.backend.ChainConfig(),
}
}
chainConfig := api.backend.ChainConfig()
var canon bool
if config.Overrides != nil {
chainConfig, canon = overrideConfig(chainConfig, config.Overrides)
}
return &traceEnv{
canon,
chainConfig,
}
}
func (api *API) retrieveBlockState(ctx context.Context, block *types.Block, execOpt *traceExecOptions, readOnly, preferDisk bool) (db *state.StateDB, blockCtx *vm.BlockContext, stateRelease StateReleaseFunc, err error) {
if block.NumberU64() == 0 { if block.NumberU64() == 0 {
return nil, errors.New("genesis is not traceable") return nil, nil, nil, errors.New("genesis is not traceable")
} }
// Prepare base state // Prepare base state
parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash()) parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash())
if err != nil { if err != nil {
return nil, err return nil, nil, nil, err
} }
reexec := defaultTraceReexec statedb, release, err := api.backend.StateAtBlock(ctx, parent, execOpt.reexec, nil, readOnly, preferDisk)
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
if err != nil { if err != nil {
return nil, err return nil, nil, nil, err
} }
defer release()
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) // TODO: we can move these into execBlockTrace? it changes the internal semantics of traceChain but it shouldn't matter?
evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{}) bCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(bCtx, statedb, api.backend.ChainConfig(), vm.Config{})
if beaconRoot := block.BeaconRoot(); beaconRoot != nil { if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
core.ProcessBeaconBlockRoot(*beaconRoot, evm) core.ProcessBeaconBlockRoot(*beaconRoot, evm)
} }
@ -609,13 +655,12 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
core.ProcessParentBlockHash(block.ParentHash(), evm) core.ProcessParentBlockHash(block.ParentHash(), evm)
} }
// JS tracers have high overhead. In this case run a parallel return statedb, &bCtx, release, nil
// process that generates states in one thread and traces txes }
// in separate worker threads.
if config != nil && config.Tracer != nil && *config.Tracer != "" { func (api *API) execBlockTrace(ctx context.Context, block *types.Block, blockCtx *vm.BlockContext, statedb *state.StateDB, execOpt *traceExecOptions, instantiateTracer func(*Context) (*Tracer, error)) ([]*txTraceResult, error) {
if isJS := DefaultDirectory.IsJS(*config.Tracer); isJS { if execOpt.parallel {
return api.traceBlockParallel(ctx, block, statedb, config) return api.traceBlockParallel(ctx, block, statedb, execOpt.txTimeout, instantiateTracer)
}
} }
// Native tracers have low overhead // Native tracers have low overhead
var ( var (
@ -633,7 +678,11 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
TxIndex: i, TxIndex: i,
TxHash: tx.Hash(), TxHash: tx.Hash(),
} }
res, err := api.traceTx(ctx, tx, msg, txctx, blockCtx, statedb, config, nil) tracer, err := instantiateTracer(txctx)
if err != nil {
return nil, err
}
res, err := api.traceTx(ctx, tx, msg, txctx, *blockCtx, statedb, tracer, nil, execOpt.txTimeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -642,10 +691,22 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
return results, nil return results, nil
} }
// 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 *API) traceBlock(ctx context.Context, block *types.Block, env *traceEnv, execOpt *traceExecOptions, instantiateTracer func(*Context) (*Tracer, error)) ([]*txTraceResult, error) {
statedb, blockCtx, release, err := api.retrieveBlockState(ctx, block, execOpt, true, false)
if err != nil {
return nil, err
}
defer release()
return api.execBlockTrace(ctx, block, blockCtx, statedb, execOpt, instantiateTracer)
}
// traceBlockParallel is for tracers that have a high overhead (read JS tracers). One thread // 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. // runs along and executes txes without tracing enabled to generate their prestate.
// Worker threads take the tasks and the prestate and trace them. // 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) { func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, statedb *state.StateDB, txTimeout time.Duration, instantiateTracer func(*Context) (*Tracer, error)) ([]*txTraceResult, error) {
// Execute all the transaction contained within the block concurrently // Execute all the transaction contained within the block concurrently
var ( var (
txs = block.Transactions() txs = block.Transactions()
@ -658,6 +719,7 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
if threads > len(txs) { if threads > len(txs) {
threads = len(txs) threads = len(txs)
} }
jobs := make(chan *txTraceTask, threads) jobs := make(chan *txTraceTask, threads)
for th := 0; th < threads; th++ { for th := 0; th < threads; th++ {
pend.Add(1) pend.Add(1)
@ -672,12 +734,17 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
TxIndex: task.index, TxIndex: task.index,
TxHash: txs[task.index].Hash(), TxHash: txs[task.index].Hash(),
} }
tracer, err := instantiateTracer(txctx)
if err != nil {
results[task.index] = &txTraceResult{TxHash: txs[task.index].Hash(), Error: err.Error()}
continue
}
// Reconstruct the block context for each transaction // Reconstruct the block context for each transaction
// as the GetHash function of BlockContext is not safe for // as the GetHash function of BlockContext is not safe fs
// concurrent use. // concurrent use.
// See: https://github.com/ethereum/go-ethereum/issues/29114 // See: https://github.com/ethereum/go-ethereum/issues/29114
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
res, err := api.traceTx(ctx, txs[task.index], msg, txctx, blockCtx, task.statedb, config, nil) res, err := api.traceTx(ctx, txs[task.index], msg, txctx, blockCtx, task.statedb, tracer, nil, txTimeout)
if err != nil { if err != nil {
results[task.index] = &txTraceResult{TxHash: txs[task.index].Hash(), Error: err.Error()} results[task.index] = &txTraceResult{TxHash: txs[task.index].Hash(), Error: err.Error()}
continue continue
@ -725,108 +792,150 @@ txloop:
return results, nil return results, nil
} }
// standardTraceBlockToFile configures a new tracer which uses standard JSON output, func createTraceDumpFile(blockHash, txHash common.Hash, txIdx int, canon bool) (*os.File, error) {
// and traces either a full block or an individual transaction. The return value will // Generate a unique temporary file to dump it into
// be one filename per transaction traced. prefix := fmt.Sprintf("block_%#x-%d-%#x-", blockHash.Bytes()[:4], txIdx, txHash.Bytes()[:4])
func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block, config *StdTraceConfig) ([]string, error) { if !canon {
// If we're tracing a single transaction, make sure it's present prefix = fmt.Sprintf("%valt-", prefix)
if config != nil && config.TxHash != (common.Hash{}) {
if !containsTx(block, config.TxHash) {
return nil, fmt.Errorf("transaction %#x not found in block", config.TxHash)
}
} }
if block.NumberU64() == 0 { dump, err := os.CreateTemp(os.TempDir(), prefix)
return nil, errors.New("genesis is not traceable")
}
parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash())
if err != nil { if err != nil {
return nil, err return nil, err
} }
reexec := defaultTraceReexec return dump, nil
if config != nil && config.Reexec != nil { }
reexec = *config.Reexec
func (api *API) standardTraceTxToFile(ctx context.Context, block *types.Block, tx *types.Transaction, txIdx int, config *logger.Config, signer types.Signer, canon bool) ([]string, error) {
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
txctx := &Context{
BlockHash: block.Hash(),
BlockNumber: block.Number(),
TxIndex: txIdx,
TxHash: tx.Hash(),
} }
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) _, blockCtx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, txIdx, defaultTraceReexec)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer release() defer release()
var dumps []string
var dump *os.File
dump, err = createTraceDumpFile(block.Hash(), tx.Hash(), txIdx, canon)
if err != nil {
return nil, err
}
dumps = append(dumps, dump.Name())
// Swap out the noop logger to the standard traces
writer := bufio.NewWriter(dump)
var logConfig *logger.Config
if config != nil {
logConfig = config
}
logger := logger.NewJSONLogger(logConfig, writer)
tracer := Tracer{
Hooks: logger,
Stop: func(err error) {},
GetResult: func() (json.RawMessage, error) { return nil, nil },
}
traceTimeout := new(time.Duration)
*traceTimeout = 1 * time.Hour
_, err = api.traceTx(ctx, tx, msg, txctx, blockCtx, statedb, &tracer, nil, *traceTimeout)
if err != nil {
return nil, err
}
return dumps, nil
}
func traceCallExecOpt(config *TraceCallConfig) (*traceExecOptions, error) {
if config == nil {
return traceExecOpt(nil)
}
return traceExecOpt(&config.TraceConfig)
}
func traceExecOpt(config *TraceConfig) (*traceExecOptions, error) {
opt := &traceExecOptions{
defaultTraceTimeout,
defaultTraceReexec,
false,
}
if config != nil {
if config.Reexec != nil {
opt.reexec = *config.Reexec
}
if config.Timeout != nil {
timeout, err := time.ParseDuration(*config.Timeout)
if err != nil {
return nil, err
}
opt.txTimeout = timeout
}
// 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.
opt.parallel = config.Tracer != nil && *config.Tracer != "" && DefaultDirectory.IsJS(*config.Tracer)
}
return opt, nil
}
func standardTraceExecOpt(config *StdTraceConfig) *traceExecOptions {
opt := traceExecOptions{
defaultTraceTimeout,
defaultTraceReexec,
false,
}
if config != nil {
if config.Reexec != nil {
opt.reexec = *config.Reexec
}
}
return &opt
}
// standardTraceBlockToFile configures a new tracer which uses standard JSON output,
// and traces either a full block or an individual transaction. The return value will
// be one filename per transaction traced.
func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block, config *StdTraceConfig) ([]string, error) {
if block.NumberU64() == 0 {
return nil, errors.New("genesis is not traceable")
}
env := envFromStdConfig(api, config)
var signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time())
if config != nil && config.TxHash != (common.Hash{}) {
idx, ok := indexOf(block, config.TxHash)
if !ok {
return nil, fmt.Errorf("transaction %#x not found in block", config.TxHash)
}
return api.standardTraceTxToFile(ctx, block, block.Transactions()[idx], idx, &config.Config, signer, env.canon)
}
// Retrieve the tracing configurations, or use default values // Retrieve the tracing configurations, or use default values
var ( var (
logConfig logger.Config logConfig logger.Config
txHash common.Hash
) )
if config != nil { if config != nil {
logConfig = config.Config logConfig = config.Config
txHash = config.TxHash
} }
// Execute transaction, either tracing all or just the requested one var dumps []string
var ( // TODO: move this comment somewhere more relevant
dumps []string
signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time())
chainConfig = api.backend.ChainConfig()
vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
canon = true
)
// Check if there are any overrides: the caller may wish to enable a future // Check if there are any overrides: the caller may wish to enable a future
// fork when executing this block. Note, such overrides are only applicable to the // fork when executing this block. Note, such overrides are only applicable to the
// actual specified block, not any preceding blocks that we have to go through // actual specified block, not any preceding blocks that we have to go through
// in order to obtain the state. // in order to obtain the state.
// Therefore, it's perfectly valid to specify `"futureForkBlock": 0`, to enable `futureFork` // Therefore, it's perfectly valid to specify `"futureForkBlock": 0`, to enable `futureFork`
if config != nil && config.Overrides != nil {
// Note: This copies the config, to not screw up the main config
chainConfig, canon = overrideConfig(chainConfig, config.Overrides)
}
evm := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{}) var (
if beaconRoot := block.BeaconRoot(); beaconRoot != nil { dump *os.File
core.ProcessBeaconBlockRoot(*beaconRoot, evm) writer *bufio.Writer
} )
if chainConfig.IsPrague(block.Number(), block.Time()) { instantiateTracer := func(ctx *Context) (*Tracer, error) {
core.ProcessParentBlockHash(block.ParentHash(), evm) // cleanup previous tx trace if one just ran
}
for i, tx := range block.Transactions() {
// Prepare the transaction for un-traced execution
msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee())
if txHash != (common.Hash{}) && tx.Hash() != txHash {
// Process the tx to update state, but don't trace it.
_, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
if err != nil {
return dumps, err
}
// 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(evm.ChainConfig().IsEIP158(block.Number()))
continue
}
// The transaction should be traced.
// Generate a unique temporary file to dump it into.
prefix := fmt.Sprintf("block_%#x-%d-%#x-", block.Hash().Bytes()[:4], i, tx.Hash().Bytes()[:4])
if !canon {
prefix = fmt.Sprintf("%valt-", prefix)
}
var dump *os.File
dump, err := os.CreateTemp(os.TempDir(), prefix)
if err != nil {
return nil, err
}
dumps = append(dumps, dump.Name())
// Set up the tracer and EVM for the transaction.
var (
writer = bufio.NewWriter(dump)
tracer = logger.NewJSONLogger(&logConfig, writer)
evm = vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{
Tracer: tracer,
NoBaseFee: true,
})
)
// Execute the transaction and flush any traces to disk
statedb.SetTxContext(tx.Hash(), i)
if tracer.OnTxStart != nil {
tracer.OnTxStart(evm.GetVMContext(), tx, msg.From)
}
_, err = core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
if writer != nil { if writer != nil {
writer.Flush() writer.Flush()
} }
@ -834,30 +943,52 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
dump.Close() dump.Close()
log.Info("Wrote standard trace", "file", dump.Name()) log.Info("Wrote standard trace", "file", dump.Name())
} }
if err != nil {
return dumps, err
}
// 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(evm.ChainConfig().IsEIP158(block.Number()))
// If we've traced the transaction we were looking for, abort // instantiate a new logger for this tx which will dump the trace to a temp file
if tx.Hash() == txHash { dump, err := createTraceDumpFile(ctx.BlockHash, ctx.TxHash, ctx.TxIndex, env.canon)
break if err != nil {
return nil, err
} }
dumps = append(dumps, dump.Name())
// Swap out the noop logger to the standard tracer
writer = bufio.NewWriter(dump)
tracer := Tracer{
logger.NewJSONLogger(&logConfig, writer),
func() (json.RawMessage, error) {
return nil, nil
},
func(err error) {},
}
return &tracer, nil
}
execOpt := standardTraceExecOpt(config)
execOpt.txTimeout = 100 * time.Hour
if _, err := api.traceBlock(ctx, block, env, execOpt, instantiateTracer); err != nil {
return nil, err
}
// cleanup dumping of the last trace in the block
if writer != nil {
writer.Flush()
}
if dump != nil {
dump.Close()
log.Info("Wrote standard trace", "file", dump.Name())
} }
return dumps, nil return dumps, nil
} }
// containsTx reports whether the transaction with a certain hash // containsTx reports whether the transaction with a certain hash
// is contained within the specified block. // is contained within the specified block.
func containsTx(block *types.Block, hash common.Hash) bool { func indexOf(block *types.Block, hash common.Hash) (int, bool) {
for _, tx := range block.Transactions() { for i, tx := range block.Transactions() {
if tx.Hash() == hash { if tx.Hash() == hash {
return true return i, true
} }
} }
return false return 0, false
} }
// TraceTransaction returns the structured logs created during the execution of EVM // TraceTransaction returns the structured logs created during the execution of EVM
@ -876,15 +1007,15 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
if blockNumber == 0 { if blockNumber == 0 {
return nil, errors.New("genesis is not traceable") return nil, errors.New("genesis is not traceable")
} }
reexec := defaultTraceReexec opt, err := traceExecOpt(config)
if config != nil && config.Reexec != nil { if err != nil {
reexec = *config.Reexec return nil, err
} }
block, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(blockNumber), blockHash) block, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(blockNumber), blockHash)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index), reexec) tx, vmctx, statedb, release, err := api.backend.StateAtTransaction(ctx, block, int(index), opt.reexec)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -900,7 +1031,11 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
TxIndex: int(index), TxIndex: int(index),
TxHash: hash, TxHash: hash,
} }
return api.traceTx(ctx, tx, msg, txctx, vmctx, statedb, config, nil) tracer, err := api.instantiateTracer(config, txctx)
if err != nil {
return nil, err
}
return api.traceTx(ctx, tx, msg, txctx, vmctx, statedb, tracer, nil, opt.txTimeout)
} }
// TraceCall lets you trace a given eth_call. It collects the structured logs // TraceCall lets you trace a given eth_call. It collects the structured logs
@ -938,15 +1073,16 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
return nil, err return nil, err
} }
// try to recompute the state // try to recompute the state
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil { opt, err := traceCallExecOpt(config)
reexec = *config.Reexec if err != nil {
return nil, err
} }
if config != nil && config.TxIndex != nil { if config != nil && config.TxIndex != nil {
_, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex), reexec) _, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, int(*config.TxIndex), opt.reexec)
} else { } else {
statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, nil, true, false) statedb, release, err = api.backend.StateAtBlock(ctx, block, opt.reexec, nil, true, false)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -983,22 +1119,16 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 { if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
vmctx.BlobBaseFee = new(big.Int) vmctx.BlobBaseFee = new(big.Int)
} }
if config != nil { tracer, err := api.instantiateTracer(traceConfig, new(Context))
traceConfig = &config.TraceConfig if err != nil {
return nil, err
} }
return api.traceTx(ctx, tx, msg, new(Context), vmctx, statedb, traceConfig, precompiles) return api.traceTx(ctx, tx, msg, new(Context), vmctx, statedb, tracer, precompiles, opt.txTimeout)
} }
// traceTx configures a new tracer according to the provided configuration, and func (api *API) instantiateTracer(config *TraceConfig, txctx *Context) (*Tracer, error) {
// executes the given message in the provided environment. The return value will var tracer *Tracer
// be tracer dependent. var err error
func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig, precompiles vm.PrecompiledContracts) (interface{}, error) {
var (
tracer *Tracer
err error
timeout = defaultTraceTimeout
usedGas uint64
)
if config == nil { if config == nil {
config = &TraceConfig{} config = &TraceConfig{}
} }
@ -1011,23 +1141,30 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor
Stop: logger.Stop, Stop: logger.Stop,
} }
} else { } else {
tracer, err = DefaultDirectory.New(*config.Tracer, txctx, config.TracerConfig, api.backend.ChainConfig()) chainConfig := api.backend.ChainConfig()
if err != nil { if config.Config.Overrides != nil {
return nil, err chainConfig = config.Config.Overrides
} }
tracer, err = DefaultDirectory.New(*config.Tracer, txctx, config.TracerConfig, chainConfig)
} }
return tracer, err
}
// TODO: modify traceTx to take a tracer directly. move logic for instantiation from TraceConfig to a separate function.
// 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 *API) traceTx(ctx context.Context, tx *types.Transaction, message *core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, tracer *Tracer, precompiles vm.PrecompiledContracts, timeout time.Duration) (interface{}, error) {
var (
err error
usedGas uint64
)
tracingStateDB := state.NewHookedState(statedb, tracer.Hooks) tracingStateDB := state.NewHookedState(statedb, tracer.Hooks)
evm := vm.NewEVM(vmctx, tracingStateDB, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true}) evm := vm.NewEVM(vmctx, tracingStateDB, api.backend.ChainConfig(), vm.Config{Tracer: tracer.Hooks, NoBaseFee: true})
if precompiles != nil { if precompiles != nil {
evm.SetPrecompiles(precompiles) evm.SetPrecompiles(precompiles)
} }
// 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) deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
go func() { go func() {
<-deadlineCtx.Done() <-deadlineCtx.Done()