Skip to content

Commit

Permalink
eth, internal/web3ext: rework trace API, concurrency, chain tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
karalabe committed Nov 24, 2017
1 parent b4eed22 commit e7a333f
Show file tree
Hide file tree
Showing 3 changed files with 520 additions and 251 deletions.
231 changes: 0 additions & 231 deletions eth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
package eth

import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"math/big"
"os"
"strings"
Expand All @@ -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"
Expand Down Expand Up @@ -349,240 +345,13 @@ 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
Tracer *string
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 {
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 {
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 &ethapi.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())
Expand Down
Loading

0 comments on commit e7a333f

Please sign in to comment.