diff --git a/eth/api.go b/eth/api.go index caed002611..e170f78870 100644 --- a/eth/api.go +++ b/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()) diff --git a/eth/api_tracer.go b/eth/api_tracer.go new file mode 100644 index 0000000000..1d473f9f0f --- /dev/null +++ b/eth/api_tracer.go @@ -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 . + +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) +} diff --git a/ethdb/memory_database.go b/ethdb/memory_database.go index 699bd0c9fd..0dd93a2791 100644 --- a/ethdb/memory_database.go +++ b/ethdb/memory_database.go @@ -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 { diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index ef0d2b4e6e..e11aa402f5 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -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',