mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 09:23:48 +00:00
eth, internal/web3ext: rework trace API, concurrency, chain tracing
This commit is contained in:
parent
b6467c167a
commit
5fb85d4b7d
4 changed files with 615 additions and 269 deletions
241
eth/api.go
241
eth/api.go
|
|
@ -17,12 +17,10 @@
|
|||
package eth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -34,8 +32,6 @@ 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"
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/miner"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
|
|
@ -349,14 +345,6 @@ func NewPrivateDebugAPI(config *params.ChainConfig, eth *Ethereum) *PrivateDebug
|
|||
return &PrivateDebugAPI{config: config, eth: eth}
|
||||
}
|
||||
|
||||
// BlockTraceResult is the returned value when replaying a block to check for
|
||||
// consensus results and full VM trace logs for all included transactions.
|
||||
type BlockTraceResult struct {
|
||||
Validated bool `json:"validated"`
|
||||
StructLogs []ethapi.StructLogRes `json:"structLogs"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// TraceArgs holds extra parameters to trace functions
|
||||
type TraceArgs struct {
|
||||
*vm.LogConfig
|
||||
|
|
@ -364,235 +352,6 @@ type TraceArgs struct {
|
|||
Timeout *string
|
||||
}
|
||||
|
||||
// TraceBlock processes the given block'api RLP but does not import the block in to
|
||||
// the chain.
|
||||
func (api *PrivateDebugAPI) TraceBlock(blockRlp []byte, config *vm.LogConfig) BlockTraceResult {
|
||||
var block types.Block
|
||||
err := rlp.Decode(bytes.NewReader(blockRlp), &block)
|
||||
if err != nil {
|
||||
return BlockTraceResult{Error: fmt.Sprintf("could not decode block: %v", err)}
|
||||
}
|
||||
|
||||
validated, logs, err := api.traceBlock(&block, config)
|
||||
return BlockTraceResult{
|
||||
Validated: validated,
|
||||
StructLogs: ethapi.FormatLogs(logs),
|
||||
Error: formatError(err),
|
||||
}
|
||||
}
|
||||
|
||||
// TraceBlockFromFile loads the block'api RLP from the given file name and attempts to
|
||||
// process it but does not import the block in to the chain.
|
||||
func (api *PrivateDebugAPI) TraceBlockFromFile(file string, config *vm.LogConfig) BlockTraceResult {
|
||||
blockRlp, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return BlockTraceResult{Error: fmt.Sprintf("could not read file: %v", err)}
|
||||
}
|
||||
return api.TraceBlock(blockRlp, config)
|
||||
}
|
||||
|
||||
// TraceBlockByNumber processes the block by canonical block number.
|
||||
func (api *PrivateDebugAPI) TraceBlockByNumber(blockNr rpc.BlockNumber, config *vm.LogConfig) BlockTraceResult {
|
||||
// Fetch the block that we aim to reprocess
|
||||
var block *types.Block
|
||||
switch blockNr {
|
||||
case rpc.PendingBlockNumber:
|
||||
// Pending block is only known by the miner
|
||||
block = api.eth.miner.PendingBlock()
|
||||
case rpc.LatestBlockNumber:
|
||||
block = api.eth.blockchain.CurrentBlock()
|
||||
default:
|
||||
block = api.eth.blockchain.GetBlockByNumber(uint64(blockNr))
|
||||
}
|
||||
|
||||
if block == nil {
|
||||
return BlockTraceResult{Error: fmt.Sprintf("block #%d not found", blockNr)}
|
||||
}
|
||||
|
||||
validated, logs, err := api.traceBlock(block, config)
|
||||
return BlockTraceResult{
|
||||
Validated: validated,
|
||||
StructLogs: ethapi.FormatLogs(logs),
|
||||
Error: formatError(err),
|
||||
}
|
||||
}
|
||||
|
||||
// TraceBlockByHash processes the block by hash.
|
||||
func (api *PrivateDebugAPI) TraceBlockByHash(hash common.Hash, config *vm.LogConfig) BlockTraceResult {
|
||||
// Fetch the block that we aim to reprocess
|
||||
block := api.eth.BlockChain().GetBlockByHash(hash)
|
||||
if block == nil {
|
||||
return BlockTraceResult{Error: fmt.Sprintf("block #%x not found", hash)}
|
||||
}
|
||||
|
||||
validated, logs, err := api.traceBlock(block, config)
|
||||
return BlockTraceResult{
|
||||
Validated: validated,
|
||||
StructLogs: ethapi.FormatLogs(logs),
|
||||
Error: formatError(err),
|
||||
}
|
||||
}
|
||||
|
||||
// traceBlock processes the given block but does not save the state.
|
||||
func (api *PrivateDebugAPI) traceBlock(block *types.Block, logConfig *vm.LogConfig) (bool, []vm.StructLog, error) {
|
||||
// Validate and reprocess the block
|
||||
var (
|
||||
blockchain = api.eth.BlockChain()
|
||||
validator = blockchain.Validator()
|
||||
processor = blockchain.Processor()
|
||||
)
|
||||
|
||||
structLogger := vm.NewStructLogger(logConfig)
|
||||
|
||||
config := vm.Config{
|
||||
Debug: true,
|
||||
Tracer: structLogger,
|
||||
}
|
||||
if err := api.eth.engine.VerifyHeader(blockchain, block.Header(), true); err != nil {
|
||||
return false, structLogger.StructLogs(), err
|
||||
}
|
||||
statedb, err := blockchain.StateAt(blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1).Root())
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *trie.MissingNodeError:
|
||||
return false, structLogger.StructLogs(), fmt.Errorf("required historical state unavailable")
|
||||
default:
|
||||
return false, structLogger.StructLogs(), err
|
||||
}
|
||||
}
|
||||
|
||||
receipts, _, usedGas, err := processor.Process(block, statedb, config)
|
||||
if err != nil {
|
||||
return false, structLogger.StructLogs(), err
|
||||
}
|
||||
if err := validator.ValidateState(block, blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1), statedb, receipts, usedGas); err != nil {
|
||||
return false, structLogger.StructLogs(), err
|
||||
}
|
||||
return true, structLogger.StructLogs(), nil
|
||||
}
|
||||
|
||||
// formatError formats a Go error into either an empty string or the data content
|
||||
// of the error itself.
|
||||
func formatError(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (t *timeoutError) Error() string {
|
||||
return "Execution time exceeded"
|
||||
}
|
||||
|
||||
// TraceTransaction returns the structured logs created during the execution of EVM
|
||||
// and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceTransaction(ctx context.Context, txHash common.Hash, config *TraceArgs) (interface{}, error) {
|
||||
var (
|
||||
tracer vm.Tracer
|
||||
err error
|
||||
)
|
||||
if config != nil && config.Tracer != nil {
|
||||
timeout := defaultTraceTimeout
|
||||
if config.Timeout != nil {
|
||||
if timeout, err = time.ParseDuration(*config.Timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if tracer, ok := tracers.Tracer(*config.Tracer); ok {
|
||||
*config.Tracer = tracer
|
||||
}
|
||||
if tracer, err = ethapi.NewJavascriptTracer(*config.Tracer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Handle timeouts and RPC cancellations
|
||||
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
go func() {
|
||||
<-deadlineCtx.Done()
|
||||
tracer.(*ethapi.JavascriptTracer).Stop(&timeoutError{})
|
||||
}()
|
||||
defer cancel()
|
||||
} else if config == nil {
|
||||
tracer = vm.NewStructLogger(nil)
|
||||
} else {
|
||||
tracer = vm.NewStructLogger(config.LogConfig)
|
||||
}
|
||||
|
||||
// Retrieve the tx from the chain and the containing block
|
||||
tx, blockHash, _, txIndex := core.GetTransaction(api.eth.ChainDb(), txHash)
|
||||
if tx == nil {
|
||||
return nil, fmt.Errorf("transaction %x not found", txHash)
|
||||
}
|
||||
msg, context, statedb, err := api.computeTxEnv(blockHash, int(txIndex))
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *trie.MissingNodeError:
|
||||
return nil, fmt.Errorf("required historical state unavailable")
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Run the transaction with tracing enabled.
|
||||
vmenv := vm.NewEVM(context, statedb, api.config, vm.Config{Debug: true, Tracer: tracer})
|
||||
ret, gas, failed, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tracing failed: %v", err)
|
||||
}
|
||||
switch tracer := tracer.(type) {
|
||||
case *vm.StructLogger:
|
||||
return ðapi.ExecutionResult{
|
||||
Gas: gas,
|
||||
Failed: failed,
|
||||
ReturnValue: fmt.Sprintf("%x", ret),
|
||||
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
|
||||
}, nil
|
||||
case *ethapi.JavascriptTracer:
|
||||
return tracer.GetResult()
|
||||
default:
|
||||
panic(fmt.Sprintf("bad tracer type %T", tracer))
|
||||
}
|
||||
}
|
||||
|
||||
// computeTxEnv returns the execution environment of a certain transaction.
|
||||
func (api *PrivateDebugAPI) computeTxEnv(blockHash common.Hash, txIndex int) (core.Message, vm.Context, *state.StateDB, error) {
|
||||
// Create the parent state.
|
||||
block := api.eth.BlockChain().GetBlockByHash(blockHash)
|
||||
if block == nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("block %x not found", blockHash)
|
||||
}
|
||||
parent := api.eth.BlockChain().GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||||
if parent == nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("block parent %x not found", block.ParentHash())
|
||||
}
|
||||
statedb, err := api.eth.BlockChain().StateAt(parent.Root())
|
||||
if err != nil {
|
||||
return nil, vm.Context{}, nil, err
|
||||
}
|
||||
txs := block.Transactions()
|
||||
|
||||
// Recompute transactions up to the target index.
|
||||
signer := types.MakeSigner(api.config, block.Number())
|
||||
for idx, tx := range txs {
|
||||
// Assemble the transaction call message
|
||||
msg, _ := tx.AsMessage(signer)
|
||||
context := core.NewEVMContext(msg, block.Header(), api.eth.BlockChain(), nil)
|
||||
if idx == txIndex {
|
||||
return msg, context, statedb, nil
|
||||
}
|
||||
|
||||
vmenv := vm.NewEVM(context, statedb, api.config, vm.Config{})
|
||||
gp := new(core.GasPool).AddGas(tx.Gas())
|
||||
_, _, _, err := core.ApplyMessage(vmenv, msg, gp)
|
||||
if err != nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("tx %x failed: %v", tx.Hash(), err)
|
||||
}
|
||||
statedb.DeleteSuicides()
|
||||
}
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("tx index %d out of range for block %x", txIndex, blockHash)
|
||||
}
|
||||
|
||||
// Preimage is a debug API function that returns the preimage for a sha3 hash, if known.
|
||||
func (api *PrivateDebugAPI) Preimage(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) {
|
||||
db := core.PreimageTable(api.eth.ChainDb())
|
||||
|
|
|
|||
583
eth/api_tracer.go
Normal file
583
eth/api_tracer.go
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package eth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"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"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// txTraceResult is the result of a single transaction trace.
|
||||
type txTraceResult struct {
|
||||
Result interface{} `json:"result,omitempty"` // Trace results procuded by the tracer
|
||||
Error string `json:"error,omitempty"` // Trace failure produced by the tracer
|
||||
}
|
||||
|
||||
// blockTraceTask represents a single block trace task when an entire chain is
|
||||
// being traced.
|
||||
type blockTraceTask struct {
|
||||
statedb *state.StateDB // Intermediate state prepped for tracing
|
||||
block *types.Block // Block to trace the transactions from
|
||||
results []*txTraceResult // Trace results procudes by the task
|
||||
}
|
||||
|
||||
// blockTraceResult represets the results of tracing a single block when an entire
|
||||
// chain is being traced.
|
||||
type blockTraceResult struct {
|
||||
Block hexutil.Uint64 `json:"block"` // Block number corresponding to this trace
|
||||
Hash common.Hash `json:"hash"` // Block hash corresponding to this trace
|
||||
Traces []*txTraceResult `json:"traces"` // Trace results procuded by the task
|
||||
}
|
||||
|
||||
// txTraceTask represents a single transaction trace task when an entire block
|
||||
// is being traced.
|
||||
type txTraceTask struct {
|
||||
statedb *state.StateDB // Intermediate state prepped for tracing
|
||||
index int // Transaction offset in the block
|
||||
}
|
||||
|
||||
// ephemeralDatabase is a memory wrapper around a proper database, which acts as
|
||||
// an ephemeral write layer. This construct is used by the chain tracer to write
|
||||
// state tries for intermediate blocks without serializing to disk, but at the
|
||||
// same time to allow disk fallback for reads that do no hit the memory layer.
|
||||
type ephemeralDatabase struct {
|
||||
diskdb ethdb.Database // Persistent disk database to fall back to with reads
|
||||
memdb *ethdb.MemDatabase // Ephemeral memory database for primary reads and writes
|
||||
}
|
||||
|
||||
func (db *ephemeralDatabase) Put(key []byte, value []byte) error { return db.memdb.Put(key, value) }
|
||||
func (db *ephemeralDatabase) Delete(key []byte) error { panic("delete not supported") }
|
||||
func (db *ephemeralDatabase) Close() { db.memdb.Close() }
|
||||
func (db *ephemeralDatabase) NewBatch() ethdb.Batch {
|
||||
return db.memdb.NewBatch()
|
||||
}
|
||||
func (db *ephemeralDatabase) Has(key []byte) (bool, error) {
|
||||
if has, _ := db.memdb.Has(key); has {
|
||||
return has, nil
|
||||
}
|
||||
return db.diskdb.Has(key)
|
||||
}
|
||||
func (db *ephemeralDatabase) Get(key []byte) ([]byte, error) {
|
||||
if blob, _ := db.memdb.Get(key); blob != nil {
|
||||
return blob, nil
|
||||
}
|
||||
return db.diskdb.Get(key)
|
||||
}
|
||||
|
||||
// Prune does a state sync into a new memory write layer and replaces the old one.
|
||||
func (db *ephemeralDatabase) Prune(root common.Hash) {
|
||||
// Pull the still relevant state data into memory
|
||||
sync := state.NewStateSync(root, db.diskdb)
|
||||
for sync.Pending() > 0 {
|
||||
hash := sync.Missing(1)[0]
|
||||
|
||||
// Move the next trie node from the memory layer into a sync struct
|
||||
node, err := db.memdb.Get(hash[:])
|
||||
if err != nil {
|
||||
panic(err) // memdb must have the data
|
||||
}
|
||||
if _, _, err := sync.Process([]trie.SyncResult{{Hash: hash, Data: node}}); err != nil {
|
||||
panic(err) // it's not possible to fail processing a node
|
||||
}
|
||||
}
|
||||
// Discard the old memory layer and write a new one
|
||||
db.memdb, _ = ethdb.NewMemDatabaseWithCap(db.memdb.Len())
|
||||
if _, err := sync.Commit(db); err != nil {
|
||||
panic(err) // writing into a memdb cannot fail
|
||||
}
|
||||
}
|
||||
|
||||
// TraceChain returns the structured logs created during the execution of EVM
|
||||
// between two blocks (excluding start) and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceChain(ctx context.Context, start, end rpc.BlockNumber, config *TraceArgs) (*rpc.Subscription, error) {
|
||||
// Fetch the block interval that we want to trace
|
||||
var from, to *types.Block
|
||||
|
||||
switch start {
|
||||
case rpc.PendingBlockNumber:
|
||||
from = api.eth.miner.PendingBlock()
|
||||
case rpc.LatestBlockNumber:
|
||||
from = api.eth.blockchain.CurrentBlock()
|
||||
default:
|
||||
from = api.eth.blockchain.GetBlockByNumber(uint64(start))
|
||||
}
|
||||
switch end {
|
||||
case rpc.PendingBlockNumber:
|
||||
to = api.eth.miner.PendingBlock()
|
||||
case rpc.LatestBlockNumber:
|
||||
to = api.eth.blockchain.CurrentBlock()
|
||||
default:
|
||||
to = api.eth.blockchain.GetBlockByNumber(uint64(end))
|
||||
}
|
||||
// Trace the chain if we've found all our blocks
|
||||
if from == nil {
|
||||
return nil, fmt.Errorf("starting block #%d not found", start)
|
||||
}
|
||||
if to == nil {
|
||||
return nil, fmt.Errorf("end block #%d not found", end)
|
||||
}
|
||||
return api.traceChain(ctx, from, to, config)
|
||||
}
|
||||
|
||||
// traceChain 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 requestd tracer.
|
||||
func (api *PrivateDebugAPI) traceChain(ctx context.Context, start, end *types.Block, config *TraceArgs) (*rpc.Subscription, error) {
|
||||
// Tracing a chain is a **long** operation, only do with subscriptions
|
||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||
if !supported {
|
||||
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
|
||||
}
|
||||
sub := notifier.CreateSubscription()
|
||||
|
||||
// Ensure we have a valid starting state before doing any work
|
||||
memdb, _ := ethdb.NewMemDatabase()
|
||||
db := &ephemeralDatabase{
|
||||
diskdb: api.eth.ChainDb(),
|
||||
memdb: memdb,
|
||||
}
|
||||
statedb, err := state.New(start.Root(), state.NewDatabase(db))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Execute all the transaction contained within the chain concurrently for each block
|
||||
blocks := int(end.NumberU64() - start.NumberU64())
|
||||
|
||||
threads := runtime.NumCPU()
|
||||
if threads > blocks {
|
||||
threads = blocks
|
||||
}
|
||||
var (
|
||||
pend = new(sync.WaitGroup)
|
||||
tasks = make(chan *blockTraceTask, threads)
|
||||
results = make(chan *blockTraceTask, threads)
|
||||
)
|
||||
for th := 0; th < threads; th++ {
|
||||
pend.Add(1)
|
||||
go func() {
|
||||
defer pend.Done()
|
||||
|
||||
// Fetch and execute the next block trace tasks
|
||||
for task := range tasks {
|
||||
signer := types.MakeSigner(api.config, task.block.Number())
|
||||
|
||||
// Trace all the transactions contained within
|
||||
for i, tx := range task.block.Transactions() {
|
||||
msg, _ := tx.AsMessage(signer)
|
||||
vmctx := core.NewEVMContext(msg, task.block.Header(), api.eth.blockchain, nil)
|
||||
|
||||
res, err := api.traceTx(ctx, msg, vmctx, task.statedb, config)
|
||||
if err != nil {
|
||||
task.results[i] = &txTraceResult{Error: err.Error()}
|
||||
break
|
||||
}
|
||||
task.statedb.DeleteSuicides()
|
||||
task.results[i] = &txTraceResult{Result: res}
|
||||
}
|
||||
// Stream the result back to the user or abort on teardown
|
||||
select {
|
||||
case results <- task:
|
||||
case <-notifier.Closed():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Start a goroutine to feed all the blocks into the tracers
|
||||
begin := time.Now()
|
||||
complete := start.NumberU64()
|
||||
|
||||
go func() {
|
||||
var (
|
||||
logged time.Time
|
||||
number uint64
|
||||
traced uint64
|
||||
failed error
|
||||
)
|
||||
// Ensure everything is properly cleaned up on any exit path
|
||||
defer func() {
|
||||
close(tasks)
|
||||
pend.Wait()
|
||||
|
||||
switch {
|
||||
case failed != nil:
|
||||
log.Warn("Chain tracing failed", "start", start.NumberU64(), "end", end.NumberU64(), "transactions", traced, "elapsed", time.Since(begin), "err", failed)
|
||||
case number < end.NumberU64():
|
||||
log.Warn("Chain tracing aborted", "start", start.NumberU64(), "end", end.NumberU64(), "abort", number, "transactions", traced, "elapsed", time.Since(begin))
|
||||
default:
|
||||
log.Info("Chain tracing finished", "start", start.NumberU64(), "end", end.NumberU64(), "transactions", traced, "elapsed", time.Since(begin))
|
||||
}
|
||||
close(results)
|
||||
}()
|
||||
// Feed all the blocks both into the tracer, as well as fast process concurrently
|
||||
for number = start.NumberU64() + 1; number <= end.NumberU64(); number++ {
|
||||
// Stop tracing if interruption was requested
|
||||
select {
|
||||
case <-notifier.Closed():
|
||||
return
|
||||
default:
|
||||
}
|
||||
// Print progress logs if long enough time elapsed
|
||||
if time.Since(logged) > 8*time.Second {
|
||||
log.Info("Tracing chain segment", "start", start.NumberU64(), "end", end.NumberU64(), "current", number, "transactions", traced, "elapsed", time.Since(begin))
|
||||
logged = time.Now()
|
||||
}
|
||||
// Retrieve the next block to trace
|
||||
block := api.eth.blockchain.GetBlockByNumber(number)
|
||||
if block == nil {
|
||||
failed = fmt.Errorf("block #%d not found", number)
|
||||
break
|
||||
}
|
||||
// Send the block over to the concurrent tracers
|
||||
txs := block.Transactions()
|
||||
|
||||
select {
|
||||
case tasks <- &blockTraceTask{statedb: statedb.Copy(), block: block, results: make([]*txTraceResult, len(txs))}:
|
||||
case <-notifier.Closed():
|
||||
return
|
||||
}
|
||||
traced += uint64(len(txs))
|
||||
|
||||
// Generate the next state snapshot fast without tracing
|
||||
_, _, _, err := api.eth.blockchain.Processor().Process(block, statedb, vm.Config{})
|
||||
if err != nil {
|
||||
failed = err
|
||||
break
|
||||
}
|
||||
// Finalize the state so any modifications are written to the trie
|
||||
root, err := statedb.CommitTo(db, true)
|
||||
if err != nil {
|
||||
failed = err
|
||||
break
|
||||
}
|
||||
if err := statedb.Reset(root); err != nil {
|
||||
failed = err
|
||||
break
|
||||
}
|
||||
// After every N blocks, prune the database to only retain relevant data
|
||||
if (number-start.NumberU64())%4096 == 0 {
|
||||
// Wait until currently pending trace jobs finish
|
||||
for atomic.LoadUint64(&complete) != number {
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-notifier.Closed():
|
||||
return
|
||||
}
|
||||
}
|
||||
// No more concurrent access at this point, prune the database
|
||||
log.Info("Pruning tracer database", "nodes", db.memdb.Len())
|
||||
start := time.Now()
|
||||
db.Prune(root)
|
||||
log.Info("Tracer database pruned", "nodes", db.memdb.Len(), "elapsed", time.Since(start))
|
||||
|
||||
statedb, _ = state.New(root, state.NewDatabase(db))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep reading the trace results and stream the to the user
|
||||
go func() {
|
||||
var (
|
||||
done = make(map[uint64]*blockTraceResult)
|
||||
next = start.NumberU64() + 1
|
||||
)
|
||||
for res := range results {
|
||||
// Queue up next received result
|
||||
result := &blockTraceResult{
|
||||
Block: hexutil.Uint64(res.block.NumberU64()),
|
||||
Hash: res.block.Hash(),
|
||||
Traces: res.results,
|
||||
}
|
||||
done[uint64(result.Block)] = result
|
||||
|
||||
// Stream completed traces to the user, aborting on the first error
|
||||
for result, ok := done[next]; ok; result, ok = done[next] {
|
||||
if len(result.Traces) > 0 || next == end.NumberU64() {
|
||||
notifier.Notify(sub.ID, result)
|
||||
}
|
||||
atomic.StoreUint64(&complete, next)
|
||||
delete(done, next)
|
||||
next++
|
||||
}
|
||||
}
|
||||
}()
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// TraceBlockByNumber returns the structured logs created during the execution of
|
||||
// EVM and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceArgs) ([]*txTraceResult, error) {
|
||||
// Fetch the block that we want to trace
|
||||
var block *types.Block
|
||||
|
||||
switch number {
|
||||
case rpc.PendingBlockNumber:
|
||||
block = api.eth.miner.PendingBlock()
|
||||
case rpc.LatestBlockNumber:
|
||||
block = api.eth.blockchain.CurrentBlock()
|
||||
default:
|
||||
block = api.eth.blockchain.GetBlockByNumber(uint64(number))
|
||||
}
|
||||
// Trace the block if it was found
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("block #%d not found", number)
|
||||
}
|
||||
return api.traceBlock(ctx, block, config)
|
||||
}
|
||||
|
||||
// TraceBlockByHash returns the structured logs created during the execution of
|
||||
// EVM and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceBlockByHash(ctx context.Context, hash common.Hash, config *TraceArgs) ([]*txTraceResult, error) {
|
||||
block := api.eth.blockchain.GetBlockByHash(hash)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("block #%x not found", hash)
|
||||
}
|
||||
return api.traceBlock(ctx, block, config)
|
||||
}
|
||||
|
||||
// TraceBlock returns the structured logs created during the execution of EVM
|
||||
// and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceBlock(ctx context.Context, blob []byte, config *TraceArgs) ([]*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)
|
||||
}
|
||||
|
||||
// TraceBlockFromFile returns the structured logs created during the execution of
|
||||
// EVM and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceBlockFromFile(ctx context.Context, file string, config *TraceArgs) ([]*txTraceResult, error) {
|
||||
blob, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read file: %v", err)
|
||||
}
|
||||
return api.TraceBlock(ctx, blob, 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 requestd tracer.
|
||||
func (api *PrivateDebugAPI) traceBlock(ctx context.Context, block *types.Block, config *TraceArgs) ([]*txTraceResult, error) {
|
||||
// Create the parent state database
|
||||
if err := api.eth.engine.VerifyHeader(api.eth.blockchain, block.Header(), true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parent := api.eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||||
if parent == nil {
|
||||
return nil, fmt.Errorf("parent %x not found", block.ParentHash())
|
||||
}
|
||||
// Execute all the transaction contained within the block concurrently
|
||||
var (
|
||||
signer = types.MakeSigner(api.config, block.Number())
|
||||
|
||||
txs = block.Transactions()
|
||||
results = make([]*txTraceResult, len(txs))
|
||||
|
||||
pend = new(sync.WaitGroup)
|
||||
jobs = make(chan *txTraceTask, len(txs))
|
||||
)
|
||||
threads := runtime.NumCPU()
|
||||
if threads > len(txs) {
|
||||
threads = len(txs)
|
||||
}
|
||||
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, _ := txs[task.index].AsMessage(signer)
|
||||
vmctx := core.NewEVMContext(msg, block.Header(), api.eth.blockchain, nil)
|
||||
|
||||
res, err := api.traceTx(ctx, msg, vmctx, 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
|
||||
statedb, err := api.eth.blockchain.StateAt(parent.Root())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var failed error
|
||||
for i, tx := range txs {
|
||||
// Send the trace task over for execution
|
||||
jobs <- &txTraceTask{statedb: statedb.Copy(), index: i}
|
||||
|
||||
// Generate the next state snapshot fast without tracing
|
||||
msg, _ := tx.AsMessage(signer)
|
||||
vmctx := core.NewEVMContext(msg, block.Header(), api.eth.blockchain, nil)
|
||||
|
||||
vmenv := vm.NewEVM(vmctx, statedb, api.config, vm.Config{})
|
||||
if _, _, _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil {
|
||||
failed = err
|
||||
break
|
||||
}
|
||||
// Finalize the state so any modifications are written to the trie
|
||||
statedb.Finalise(true)
|
||||
}
|
||||
close(jobs)
|
||||
pend.Wait()
|
||||
|
||||
// If execution failed in between, abort
|
||||
if failed != nil {
|
||||
return nil, failed
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// TraceTransaction returns the structured logs created during the execution of EVM
|
||||
// and returns them as a JSON object.
|
||||
func (api *PrivateDebugAPI) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceArgs) (interface{}, error) {
|
||||
// Retrieve the transaction and assemble its EVM context
|
||||
tx, blockHash, _, index := core.GetTransaction(api.eth.ChainDb(), hash)
|
||||
if tx == nil {
|
||||
return nil, fmt.Errorf("transaction %x not found", hash)
|
||||
}
|
||||
msg, vmctx, statedb, err := api.computeTxEnv(blockHash, int(index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Trace the transaction and return
|
||||
return api.traceTx(ctx, msg, vmctx, statedb, config)
|
||||
}
|
||||
|
||||
// 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 *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, vmctx vm.Context, statedb *state.StateDB, config *TraceArgs) (interface{}, error) {
|
||||
// Assemble the structured logger or the JavaScript tracer
|
||||
var (
|
||||
tracer vm.Tracer
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case config != nil && config.Tracer != nil:
|
||||
// Define a meaningful timeout of a single transaction trace
|
||||
timeout := defaultTraceTimeout
|
||||
if config.Timeout != nil {
|
||||
if timeout, err = time.ParseDuration(*config.Timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Constuct the JavaScript tracer to execute with
|
||||
if tracer, ok := tracers.Tracer(*config.Tracer); ok {
|
||||
*config.Tracer = tracer
|
||||
}
|
||||
if tracer, err = ethapi.NewJavascriptTracer(*config.Tracer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Handle timeouts and RPC cancellations
|
||||
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
go func() {
|
||||
<-deadlineCtx.Done()
|
||||
tracer.(*ethapi.JavascriptTracer).Stop(errors.New("execution timeout"))
|
||||
}()
|
||||
defer cancel()
|
||||
|
||||
case config == nil:
|
||||
tracer = vm.NewStructLogger(nil)
|
||||
|
||||
default:
|
||||
tracer = vm.NewStructLogger(config.LogConfig)
|
||||
}
|
||||
// Run the transaction with tracing enabled.
|
||||
vmenv := vm.NewEVM(vmctx, statedb, api.config, vm.Config{Debug: true, Tracer: tracer})
|
||||
|
||||
ret, gas, failed, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tracing failed: %v", err)
|
||||
}
|
||||
// Depending on the tracer type, format and return the output
|
||||
switch tracer := tracer.(type) {
|
||||
case *vm.StructLogger:
|
||||
return ðapi.ExecutionResult{
|
||||
Gas: gas,
|
||||
Failed: failed,
|
||||
ReturnValue: fmt.Sprintf("%x", ret),
|
||||
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
|
||||
}, nil
|
||||
|
||||
case *ethapi.JavascriptTracer:
|
||||
return tracer.GetResult()
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("bad tracer type %T", tracer))
|
||||
}
|
||||
}
|
||||
|
||||
// computeTxEnv returns the execution environment of a certain transaction.
|
||||
func (api *PrivateDebugAPI) computeTxEnv(blockHash common.Hash, txIndex int) (core.Message, vm.Context, *state.StateDB, error) {
|
||||
// Create the parent state database
|
||||
block := api.eth.blockchain.GetBlockByHash(blockHash)
|
||||
if block == nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("block %x not found", blockHash)
|
||||
}
|
||||
parent := api.eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||||
if parent == nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("parent %x not found", block.ParentHash())
|
||||
}
|
||||
statedb, err := api.eth.blockchain.StateAt(parent.Root())
|
||||
if err != nil {
|
||||
return nil, vm.Context{}, nil, err
|
||||
}
|
||||
// Recompute transactions up to the target index.
|
||||
signer := types.MakeSigner(api.config, block.Number())
|
||||
|
||||
for idx, tx := range block.Transactions() {
|
||||
// Assemble the transaction call message and return if the requested offset
|
||||
msg, _ := tx.AsMessage(signer)
|
||||
context := core.NewEVMContext(msg, block.Header(), api.eth.blockchain, nil)
|
||||
if idx == txIndex {
|
||||
return msg, context, statedb, nil
|
||||
}
|
||||
// Not yet the searched for transaction, execute on top of the current state
|
||||
vmenv := vm.NewEVM(context, statedb, api.config, vm.Config{})
|
||||
if _, _, _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil {
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("tx %x failed: %v", tx.Hash(), err)
|
||||
}
|
||||
statedb.DeleteSuicides()
|
||||
}
|
||||
return nil, vm.Context{}, nil, fmt.Errorf("tx index %d out of range for block %x", txIndex, blockHash)
|
||||
}
|
||||
|
|
@ -37,6 +37,12 @@ func NewMemDatabase() (*MemDatabase, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func NewMemDatabaseWithCap(size int) (*MemDatabase, error) {
|
||||
return &MemDatabase{
|
||||
db: make(map[string][]byte, size),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *MemDatabase) Put(key []byte, value []byte) error {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
|
@ -74,14 +80,6 @@ func (db *MemDatabase) Keys() [][]byte {
|
|||
return keys
|
||||
}
|
||||
|
||||
/*
|
||||
func (db *MemDatabase) GetKeys() []*common.Key {
|
||||
data, _ := db.Get([]byte("KeyRing"))
|
||||
|
||||
return []*common.Key{common.NewKeyFromBytes(data)}
|
||||
}
|
||||
*/
|
||||
|
||||
func (db *MemDatabase) Delete(key []byte) error {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
|
@ -96,6 +94,8 @@ func (db *MemDatabase) NewBatch() Batch {
|
|||
return &memBatch{db: db}
|
||||
}
|
||||
|
||||
func (db *MemDatabase) Len() int { return len(db.db) }
|
||||
|
||||
type kv struct{ k, v []byte }
|
||||
|
||||
type memBatch struct {
|
||||
|
|
|
|||
|
|
@ -196,26 +196,6 @@ web3._extend({
|
|||
call: 'debug_setHead',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlock',
|
||||
call: 'debug_traceBlock',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockFromFile',
|
||||
call: 'debug_traceBlockFromFile',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockByNumber',
|
||||
call: 'debug_traceBlockByNumber',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockByHash',
|
||||
call: 'debug_traceBlockByHash',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'seedHash',
|
||||
call: 'debug_seedHash',
|
||||
|
|
@ -332,6 +312,30 @@ web3._extend({
|
|||
call: 'debug_writeMemProfile',
|
||||
params: 1
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlock',
|
||||
call: 'debug_traceBlock',
|
||||
params: 2,
|
||||
inputFormatter: [null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockFromFile',
|
||||
call: 'debug_traceBlockFromFile',
|
||||
params: 2,
|
||||
inputFormatter: [null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockByNumber',
|
||||
call: 'debug_traceBlockByNumber',
|
||||
params: 2,
|
||||
inputFormatter: [null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceBlockByHash',
|
||||
call: 'debug_traceBlockByHash',
|
||||
params: 2,
|
||||
inputFormatter: [null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'traceTransaction',
|
||||
call: 'debug_traceTransaction',
|
||||
|
|
|
|||
Loading…
Reference in a new issue