forked from cerc-io/plugeth
eth/tracers: support for golang tracers + add golang callTracer (#23708)
* eth/tracers: add basic native loader * eth/tracers: add GetResult to tracer interface * eth/tracers: add native call tracer * eth/tracers: fix call tracer json result * eth/tracers: minor fix * eth/tracers: fix * eth/tracers: fix benchTracer * eth/tracers: test native call tracer * eth/tracers: fix * eth/tracers: rm extra make Co-authored-by: Martin Holst Swende <martin@swende.se> * eth/tracers: rm extra make * eth/tracers: make callFrame private * eth/tracers: clean-up and comments * eth/tracers: add license * eth/tracers: rework the model a bit * eth/tracers: move tracecall tests to subpackage * cmd/geth: load native tracers * eth/tracers: minor fix * eth/tracers: impl stop * eth/tracers: add native noop tracer * renamings Co-authored-by: Martin Holst Swende <martin@swende.se> * eth/tracers: more renamings * eth/tracers: make jstracer non-exported, avoid cast * eth/tracers, core/vm: rename vm.Tracer to vm.EVMLogger for clarity * eth/tracers: minor comment fix * eth/tracers/testing: lint nitpicks * core,eth: cancel evm on nativecalltracer stop * Revert "core,eth: cancel evm on nativecalltracer stop" This reverts commit 01bb908790a369c1bb9d3937df9325c6857bf855. * eth/tracers: linter nits * eth/tracers: fix output on err Co-authored-by: Martin Holst Swende <martin@swende.se>
This commit is contained in:
parent
3bbeb94c1c
commit
8d7e6062ec
@ -96,7 +96,7 @@ type rejectedTx struct {
|
|||||||
// Apply applies a set of transactions to a pre-state
|
// Apply applies a set of transactions to a pre-state
|
||||||
func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
||||||
txs types.Transactions, miningReward int64,
|
txs types.Transactions, miningReward int64,
|
||||||
getTracerFn func(txIndex int, txHash common.Hash) (tracer vm.Tracer, err error)) (*state.StateDB, *ExecutionResult, error) {
|
getTracerFn func(txIndex int, txHash common.Hash) (tracer vm.EVMLogger, err error)) (*state.StateDB, *ExecutionResult, error) {
|
||||||
|
|
||||||
// Capture errors for BLOCKHASH operation, if we haven't been supplied the
|
// Capture errors for BLOCKHASH operation, if we haven't been supplied the
|
||||||
// required blockhashes
|
// required blockhashes
|
||||||
|
@ -89,10 +89,10 @@ func Transition(ctx *cli.Context) error {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
tracer vm.Tracer
|
tracer vm.EVMLogger
|
||||||
baseDir = ""
|
baseDir = ""
|
||||||
)
|
)
|
||||||
var getTracer func(txIndex int, txHash common.Hash) (vm.Tracer, error)
|
var getTracer func(txIndex int, txHash common.Hash) (vm.EVMLogger, error)
|
||||||
|
|
||||||
// If user specified a basedir, make sure it exists
|
// If user specified a basedir, make sure it exists
|
||||||
if ctx.IsSet(OutputBasedir.Name) {
|
if ctx.IsSet(OutputBasedir.Name) {
|
||||||
@ -119,7 +119,7 @@ func Transition(ctx *cli.Context) error {
|
|||||||
prevFile.Close()
|
prevFile.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
getTracer = func(txIndex int, txHash common.Hash) (vm.Tracer, error) {
|
getTracer = func(txIndex int, txHash common.Hash) (vm.EVMLogger, error) {
|
||||||
if prevFile != nil {
|
if prevFile != nil {
|
||||||
prevFile.Close()
|
prevFile.Close()
|
||||||
}
|
}
|
||||||
@ -131,7 +131,7 @@ func Transition(ctx *cli.Context) error {
|
|||||||
return vm.NewJSONLogger(logConfig, traceFile), nil
|
return vm.NewJSONLogger(logConfig, traceFile), nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getTracer = func(txIndex int, txHash common.Hash) (tracer vm.Tracer, err error) {
|
getTracer = func(txIndex int, txHash common.Hash) (tracer vm.EVMLogger, err error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ func runCmd(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tracer vm.Tracer
|
tracer vm.EVMLogger
|
||||||
debugLogger *vm.StructLogger
|
debugLogger *vm.StructLogger
|
||||||
statedb *state.StateDB
|
statedb *state.StateDB
|
||||||
chainConfig *params.ChainConfig
|
chainConfig *params.ChainConfig
|
||||||
|
@ -65,7 +65,7 @@ func stateTestCmd(ctx *cli.Context) error {
|
|||||||
EnableReturnData: !ctx.GlobalBool(DisableReturnDataFlag.Name),
|
EnableReturnData: !ctx.GlobalBool(DisableReturnDataFlag.Name),
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
tracer vm.Tracer
|
tracer vm.EVMLogger
|
||||||
debugger *vm.StructLogger
|
debugger *vm.StructLogger
|
||||||
)
|
)
|
||||||
switch {
|
switch {
|
||||||
|
@ -39,6 +39,10 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
|
||||||
|
// Force-load the native, to trigger registration
|
||||||
|
_ "github.com/ethereum/go-ethereum/eth/tracers/native"
|
||||||
|
|
||||||
"gopkg.in/urfave/cli.v1"
|
"gopkg.in/urfave/cli.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,11 +27,11 @@ import (
|
|||||||
|
|
||||||
// Config are the configuration options for the Interpreter
|
// Config are the configuration options for the Interpreter
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Debug bool // Enables debugging
|
Debug bool // Enables debugging
|
||||||
Tracer Tracer // Opcode logger
|
Tracer EVMLogger // Opcode logger
|
||||||
NoRecursion bool // Disables call, callcode, delegate call and create
|
NoRecursion bool // Disables call, callcode, delegate call and create
|
||||||
NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
|
NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
|
||||||
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
|
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
|
||||||
|
|
||||||
JumpTable [256]*operation // EVM instruction table, automatically populated if unset
|
JumpTable [256]*operation // EVM instruction table, automatically populated if unset
|
||||||
|
|
||||||
@ -152,9 +152,9 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
|
|||||||
pc = uint64(0) // program counter
|
pc = uint64(0) // program counter
|
||||||
cost uint64
|
cost uint64
|
||||||
// copies used by tracer
|
// copies used by tracer
|
||||||
pcCopy uint64 // needed for the deferred Tracer
|
pcCopy uint64 // needed for the deferred EVMLogger
|
||||||
gasCopy uint64 // for Tracer to log gas remaining before execution
|
gasCopy uint64 // for EVMLogger to log gas remaining before execution
|
||||||
logged bool // deferred Tracer should ignore already logged steps
|
logged bool // deferred EVMLogger should ignore already logged steps
|
||||||
res []byte // result of the opcode execution function
|
res []byte // result of the opcode execution function
|
||||||
)
|
)
|
||||||
// Don't move this deferrred function, it's placed before the capturestate-deferred method,
|
// Don't move this deferrred function, it's placed before the capturestate-deferred method,
|
||||||
|
@ -98,12 +98,12 @@ func (s *StructLog) ErrorString() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracer is used to collect execution traces from an EVM transaction
|
// EVMLogger is used to collect execution traces from an EVM transaction
|
||||||
// execution. CaptureState is called for each step of the VM with the
|
// execution. CaptureState is called for each step of the VM with the
|
||||||
// current VM state.
|
// current VM state.
|
||||||
// Note that reference types are actual VM data structures; make copies
|
// Note that reference types are actual VM data structures; make copies
|
||||||
// if you need to retain them beyond the current call.
|
// if you need to retain them beyond the current call.
|
||||||
type Tracer interface {
|
type EVMLogger interface {
|
||||||
CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int)
|
CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int)
|
||||||
CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error)
|
CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error)
|
||||||
CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int)
|
CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int)
|
||||||
@ -112,7 +112,7 @@ type Tracer interface {
|
|||||||
CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error)
|
CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StructLogger is an EVM state logger and implements Tracer.
|
// StructLogger is an EVM state logger and implements EVMLogger.
|
||||||
//
|
//
|
||||||
// StructLogger can capture state based on the given Log configuration and also keeps
|
// StructLogger can capture state based on the given Log configuration and also keeps
|
||||||
// a track record of modified storage which is used in reporting snapshots of the
|
// a track record of modified storage which is used in reporting snapshots of the
|
||||||
@ -145,7 +145,7 @@ func (l *StructLogger) Reset() {
|
|||||||
l.err = nil
|
l.err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaptureStart implements the Tracer interface to initialize the tracing operation.
|
// CaptureStart implements the EVMLogger interface to initialize the tracing operation.
|
||||||
func (l *StructLogger) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
func (l *StructLogger) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ func (l *StructLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost ui
|
|||||||
l.logs = append(l.logs, log)
|
l.logs = append(l.logs, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaptureFault implements the Tracer interface to trace an execution fault
|
// CaptureFault implements the EVMLogger interface to trace an execution fault
|
||||||
// while running an opcode.
|
// while running an opcode.
|
||||||
func (l *StructLogger) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, depth int, err error) {
|
func (l *StructLogger) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, depth int, err error) {
|
||||||
}
|
}
|
||||||
|
@ -862,12 +862,14 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
|
|||||||
func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig) (interface{}, error) {
|
func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig) (interface{}, error) {
|
||||||
// Assemble the structured logger or the JavaScript tracer
|
// Assemble the structured logger or the JavaScript tracer
|
||||||
var (
|
var (
|
||||||
tracer vm.Tracer
|
tracer vm.EVMLogger
|
||||||
err error
|
err error
|
||||||
txContext = core.NewEVMTxContext(message)
|
txContext = core.NewEVMTxContext(message)
|
||||||
)
|
)
|
||||||
switch {
|
switch {
|
||||||
case config != nil && config.Tracer != nil:
|
case config == nil:
|
||||||
|
tracer = vm.NewStructLogger(nil)
|
||||||
|
case config.Tracer != nil:
|
||||||
// Define a meaningful timeout of a single transaction trace
|
// Define a meaningful timeout of a single transaction trace
|
||||||
timeout := defaultTraceTimeout
|
timeout := defaultTraceTimeout
|
||||||
if config.Timeout != nil {
|
if config.Timeout != nil {
|
||||||
@ -875,23 +877,19 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Constuct the JavaScript tracer to execute with
|
if t, err := New(*config.Tracer, txctx); err != nil {
|
||||||
if tracer, err = New(*config.Tracer, txctx); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else {
|
||||||
|
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
go func() {
|
||||||
|
<-deadlineCtx.Done()
|
||||||
|
if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) {
|
||||||
|
t.Stop(errors.New("execution timeout"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer cancel()
|
||||||
|
tracer = t
|
||||||
}
|
}
|
||||||
// Handle timeouts and RPC cancellations
|
|
||||||
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
go func() {
|
|
||||||
<-deadlineCtx.Done()
|
|
||||||
if deadlineCtx.Err() == context.DeadlineExceeded {
|
|
||||||
tracer.(*Tracer).Stop(errors.New("execution timeout"))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
case config == nil:
|
|
||||||
tracer = vm.NewStructLogger(nil)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
tracer = vm.NewStructLogger(config.LogConfig)
|
tracer = vm.NewStructLogger(config.LogConfig)
|
||||||
}
|
}
|
||||||
@ -921,7 +919,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
|
|||||||
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
|
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *Tracer:
|
case Tracer:
|
||||||
return tracer.GetResult()
|
return tracer.GetResult()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
170
eth/tracers/native/call.go
Normal file
170
eth/tracers/native/call.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// Copyright 2021 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 native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
"github.com/ethereum/go-ethereum/eth/tracers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tracers.RegisterNativeTracer("callTracerNative", NewCallTracer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type callFrame struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
From string `json:"from"`
|
||||||
|
To string `json:"to,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Gas string `json:"gas"`
|
||||||
|
GasUsed string `json:"gasUsed"`
|
||||||
|
Input string `json:"input"`
|
||||||
|
Output string `json:"output,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Calls []callFrame `json:"calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type callTracer struct {
|
||||||
|
callstack []callFrame
|
||||||
|
interrupt uint32 // Atomic flag to signal execution interruption
|
||||||
|
reason error // Textual reason for the interruption
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCallTracer returns a native go tracer which tracks
|
||||||
|
// call frames of a tx, and implements vm.EVMLogger.
|
||||||
|
func NewCallTracer() tracers.Tracer {
|
||||||
|
// First callframe contains tx context info
|
||||||
|
// and is populated on start and end.
|
||||||
|
t := &callTracer{callstack: make([]callFrame, 1)}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||||
|
t.callstack[0] = callFrame{
|
||||||
|
Type: "CALL",
|
||||||
|
From: addrToHex(from),
|
||||||
|
To: addrToHex(to),
|
||||||
|
Input: bytesToHex(input),
|
||||||
|
Gas: uintToHex(gas),
|
||||||
|
Value: bigToHex(value),
|
||||||
|
}
|
||||||
|
if create {
|
||||||
|
t.callstack[0].Type = "CREATE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) {
|
||||||
|
t.callstack[0].GasUsed = uintToHex(gasUsed)
|
||||||
|
if err != nil {
|
||||||
|
t.callstack[0].Error = err.Error()
|
||||||
|
if err.Error() == "execution reverted" && len(output) > 0 {
|
||||||
|
t.callstack[0].Output = bytesToHex(output)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.callstack[0].Output = bytesToHex(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
|
// Skip if tracing was interrupted
|
||||||
|
if atomic.LoadUint32(&t.interrupt) > 0 {
|
||||||
|
// TODO: env.Cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call := callFrame{
|
||||||
|
Type: typ.String(),
|
||||||
|
From: addrToHex(from),
|
||||||
|
To: addrToHex(to),
|
||||||
|
Input: bytesToHex(input),
|
||||||
|
Gas: uintToHex(gas),
|
||||||
|
Value: bigToHex(value),
|
||||||
|
}
|
||||||
|
t.callstack = append(t.callstack, call)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
||||||
|
size := len(t.callstack)
|
||||||
|
if size <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// pop call
|
||||||
|
call := t.callstack[size-1]
|
||||||
|
t.callstack = t.callstack[:size-1]
|
||||||
|
size -= 1
|
||||||
|
|
||||||
|
call.GasUsed = uintToHex(gasUsed)
|
||||||
|
if err == nil {
|
||||||
|
call.Output = bytesToHex(output)
|
||||||
|
} else {
|
||||||
|
call.Error = err.Error()
|
||||||
|
if call.Type == "CREATE" || call.Type == "CREATE2" {
|
||||||
|
call.To = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) GetResult() (json.RawMessage, error) {
|
||||||
|
if len(t.callstack) != 1 {
|
||||||
|
return nil, errors.New("incorrect number of top-level calls")
|
||||||
|
}
|
||||||
|
res, err := json.Marshal(t.callstack[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.RawMessage(res), t.reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *callTracer) Stop(err error) {
|
||||||
|
t.reason = err
|
||||||
|
atomic.StoreUint32(&t.interrupt, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToHex(s []byte) string {
|
||||||
|
return "0x" + common.Bytes2Hex(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bigToHex(n *big.Int) string {
|
||||||
|
if n == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "0x" + n.Text(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uintToHex(n uint64) string {
|
||||||
|
return "0x" + strconv.FormatUint(n, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addrToHex(a common.Address) string {
|
||||||
|
return strings.ToLower(a.Hex())
|
||||||
|
}
|
46
eth/tracers/native/noop.go
Normal file
46
eth/tracers/native/noop.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
|
"github.com/ethereum/go-ethereum/eth/tracers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tracers.RegisterNativeTracer("noopTracerNative", NewNoopTracer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopTracer struct{}
|
||||||
|
|
||||||
|
func NewNoopTracer() tracers.Tracer {
|
||||||
|
return &noopTracer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) GetResult() (json.RawMessage, error) {
|
||||||
|
return json.RawMessage(`{}`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *noopTracer) Stop(err error) {
|
||||||
|
}
|
246
eth/tracers/testing/calltrace_test.go
Normal file
246
eth/tracers/testing/calltrace_test.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/common/math"
|
||||||
|
"github.com/ethereum/go-ethereum/core"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"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/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/tests"
|
||||||
|
|
||||||
|
// Force-load the native, to trigger registration
|
||||||
|
_ "github.com/ethereum/go-ethereum/eth/tracers/native"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callContext struct {
|
||||||
|
Number math.HexOrDecimal64 `json:"number"`
|
||||||
|
Difficulty *math.HexOrDecimal256 `json:"difficulty"`
|
||||||
|
Time math.HexOrDecimal64 `json:"timestamp"`
|
||||||
|
GasLimit math.HexOrDecimal64 `json:"gasLimit"`
|
||||||
|
Miner common.Address `json:"miner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// callTrace is the result of a callTracer run.
|
||||||
|
type callTrace struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
From common.Address `json:"from"`
|
||||||
|
To common.Address `json:"to"`
|
||||||
|
Input hexutil.Bytes `json:"input"`
|
||||||
|
Output hexutil.Bytes `json:"output"`
|
||||||
|
Gas *hexutil.Uint64 `json:"gas,omitempty"`
|
||||||
|
GasUsed *hexutil.Uint64 `json:"gasUsed,omitempty"`
|
||||||
|
Value *hexutil.Big `json:"value,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Calls []callTrace `json:"calls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// callTracerTest defines a single test to check the call tracer against.
|
||||||
|
type callTracerTest struct {
|
||||||
|
Genesis *core.Genesis `json:"genesis"`
|
||||||
|
Context *callContext `json:"context"`
|
||||||
|
Input string `json:"input"`
|
||||||
|
Result *callTrace `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterates over all the input-output datasets in the tracer test harness and
|
||||||
|
// runs the JavaScript tracers against them.
|
||||||
|
func TestCallTracerLegacy(t *testing.T) {
|
||||||
|
testCallTracer("callTracerLegacy", "call_tracer_legacy", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallTracer(t *testing.T) {
|
||||||
|
testCallTracer("callTracer", "call_tracer", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallTracerNative(t *testing.T) {
|
||||||
|
testCallTracer("callTracerNative", "call_tracer", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCallTracer(tracerName string, dirPath string, t *testing.T) {
|
||||||
|
files, err := ioutil.ReadDir(filepath.Join("..", "testdata", dirPath))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to retrieve tracer test suite: %v", err)
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasSuffix(file.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file := file // capture range variable
|
||||||
|
t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
test = new(callTracerTest)
|
||||||
|
tx = new(types.Transaction)
|
||||||
|
)
|
||||||
|
// Call tracer test found, read if from disk
|
||||||
|
if blob, err := ioutil.ReadFile(filepath.Join("..", "testdata", dirPath, file.Name())); err != nil {
|
||||||
|
t.Fatalf("failed to read testcase: %v", err)
|
||||||
|
} else if err := json.Unmarshal(blob, test); err != nil {
|
||||||
|
t.Fatalf("failed to parse testcase: %v", err)
|
||||||
|
}
|
||||||
|
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
|
||||||
|
t.Fatalf("failed to parse testcase input: %v", err)
|
||||||
|
}
|
||||||
|
// Configure a blockchain with the given prestate
|
||||||
|
var (
|
||||||
|
signer = types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
|
||||||
|
origin, _ = signer.Sender(tx)
|
||||||
|
txContext = vm.TxContext{
|
||||||
|
Origin: origin,
|
||||||
|
GasPrice: tx.GasPrice(),
|
||||||
|
}
|
||||||
|
context = vm.BlockContext{
|
||||||
|
CanTransfer: core.CanTransfer,
|
||||||
|
Transfer: core.Transfer,
|
||||||
|
Coinbase: test.Context.Miner,
|
||||||
|
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
|
||||||
|
Time: new(big.Int).SetUint64(uint64(test.Context.Time)),
|
||||||
|
Difficulty: (*big.Int)(test.Context.Difficulty),
|
||||||
|
GasLimit: uint64(test.Context.GasLimit),
|
||||||
|
}
|
||||||
|
_, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
|
||||||
|
)
|
||||||
|
tracer, err := tracers.New(tracerName, new(tracers.Context))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create call tracer: %v", err)
|
||||||
|
}
|
||||||
|
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
|
||||||
|
msg, err := tx.AsMessage(signer, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to prepare transaction for tracing: %v", err)
|
||||||
|
}
|
||||||
|
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
|
||||||
|
if _, err = st.TransitionDb(); err != nil {
|
||||||
|
t.Fatalf("failed to execute transaction: %v", err)
|
||||||
|
}
|
||||||
|
// Retrieve the trace result and compare against the etalon
|
||||||
|
res, err := tracer.GetResult()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to retrieve trace result: %v", err)
|
||||||
|
}
|
||||||
|
ret := new(callTrace)
|
||||||
|
if err := json.Unmarshal(res, ret); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal trace result: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jsonEqual(ret, test.Result) {
|
||||||
|
// uncomment this for easier debugging
|
||||||
|
//have, _ := json.MarshalIndent(ret, "", " ")
|
||||||
|
//want, _ := json.MarshalIndent(test.Result, "", " ")
|
||||||
|
//t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))
|
||||||
|
t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
|
||||||
|
// comparison
|
||||||
|
func jsonEqual(x, y interface{}) bool {
|
||||||
|
xTrace := new(callTrace)
|
||||||
|
yTrace := new(callTrace)
|
||||||
|
if xj, err := json.Marshal(x); err == nil {
|
||||||
|
json.Unmarshal(xj, xTrace)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if yj, err := json.Marshal(y); err == nil {
|
||||||
|
json.Unmarshal(yj, yTrace)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(xTrace, yTrace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// camel converts a snake cased input string into a camel cased output.
|
||||||
|
func camel(str string) string {
|
||||||
|
pieces := strings.Split(str, "_")
|
||||||
|
for i := 1; i < len(pieces); i++ {
|
||||||
|
pieces[i] = string(unicode.ToUpper(rune(pieces[i][0]))) + pieces[i][1:]
|
||||||
|
}
|
||||||
|
return strings.Join(pieces, "")
|
||||||
|
}
|
||||||
|
func BenchmarkTracers(b *testing.B) {
|
||||||
|
files, err := ioutil.ReadDir(filepath.Join("..", "testdata", "call_tracer"))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("failed to retrieve tracer test suite: %v", err)
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasSuffix(file.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file := file // capture range variable
|
||||||
|
b.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(b *testing.B) {
|
||||||
|
blob, err := ioutil.ReadFile(filepath.Join("..", "testdata", "call_tracer", file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("failed to read testcase: %v", err)
|
||||||
|
}
|
||||||
|
test := new(callTracerTest)
|
||||||
|
if err := json.Unmarshal(blob, test); err != nil {
|
||||||
|
b.Fatalf("failed to parse testcase: %v", err)
|
||||||
|
}
|
||||||
|
benchTracer("callTracerNative", test, b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
|
||||||
|
// Configure a blockchain with the given prestate
|
||||||
|
tx := new(types.Transaction)
|
||||||
|
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
|
||||||
|
b.Fatalf("failed to parse testcase input: %v", err)
|
||||||
|
}
|
||||||
|
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
|
||||||
|
msg, err := tx.AsMessage(signer, nil)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("failed to prepare transaction for tracing: %v", err)
|
||||||
|
}
|
||||||
|
origin, _ := signer.Sender(tx)
|
||||||
|
txContext := vm.TxContext{
|
||||||
|
Origin: origin,
|
||||||
|
GasPrice: tx.GasPrice(),
|
||||||
|
}
|
||||||
|
context := vm.BlockContext{
|
||||||
|
CanTransfer: core.CanTransfer,
|
||||||
|
Transfer: core.Transfer,
|
||||||
|
Coinbase: test.Context.Miner,
|
||||||
|
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
|
||||||
|
Time: new(big.Int).SetUint64(uint64(test.Context.Time)),
|
||||||
|
Difficulty: (*big.Int)(test.Context.Difficulty),
|
||||||
|
GasLimit: uint64(test.Context.GasLimit),
|
||||||
|
}
|
||||||
|
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
tracer, err := tracers.New(tracerName, new(tracers.Context))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("failed to create call tracer: %v", err)
|
||||||
|
}
|
||||||
|
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
|
||||||
|
snap := statedb.Snapshot()
|
||||||
|
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
|
||||||
|
if _, err = st.TransitionDb(); err != nil {
|
||||||
|
b.Fatalf("failed to execute transaction: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = tracer.GetResult(); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
statedb.RevertToSnapshot(snap)
|
||||||
|
}
|
||||||
|
}
|
@ -363,9 +363,9 @@ func (r *frameResult) pushObject(vm *duktape.Context) {
|
|||||||
vm.PutPropString(obj, "getError")
|
vm.PutPropString(obj, "getError")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracer provides an implementation of Tracer that evaluates a Javascript
|
// jsTracer provides an implementation of Tracer that evaluates a Javascript
|
||||||
// function for each VM execution step.
|
// function for each VM execution step.
|
||||||
type Tracer struct {
|
type jsTracer struct {
|
||||||
vm *duktape.Context // Javascript VM instance
|
vm *duktape.Context // Javascript VM instance
|
||||||
|
|
||||||
tracerObject int // Stack index of the tracer JavaScript object
|
tracerObject int // Stack index of the tracer JavaScript object
|
||||||
@ -409,12 +409,8 @@ type Context struct {
|
|||||||
// New instantiates a new tracer instance. code specifies a Javascript snippet,
|
// New instantiates a new tracer instance. code specifies a Javascript snippet,
|
||||||
// which must evaluate to an expression returning an object with 'step', 'fault'
|
// which must evaluate to an expression returning an object with 'step', 'fault'
|
||||||
// and 'result' functions.
|
// and 'result' functions.
|
||||||
func New(code string, ctx *Context) (*Tracer, error) {
|
func newJsTracer(code string, ctx *Context) (*jsTracer, error) {
|
||||||
// Resolve any tracers by name and assemble the tracer object
|
tracer := &jsTracer{
|
||||||
if tracer, ok := tracer(code); ok {
|
|
||||||
code = tracer
|
|
||||||
}
|
|
||||||
tracer := &Tracer{
|
|
||||||
vm: duktape.New(),
|
vm: duktape.New(),
|
||||||
ctx: make(map[string]interface{}),
|
ctx: make(map[string]interface{}),
|
||||||
opWrapper: new(opWrapper),
|
opWrapper: new(opWrapper),
|
||||||
@ -620,14 +616,14 @@ func New(code string, ctx *Context) (*Tracer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop terminates execution of the tracer at the first opportune moment.
|
// Stop terminates execution of the tracer at the first opportune moment.
|
||||||
func (jst *Tracer) Stop(err error) {
|
func (jst *jsTracer) Stop(err error) {
|
||||||
jst.reason = err
|
jst.reason = err
|
||||||
atomic.StoreUint32(&jst.interrupt, 1)
|
atomic.StoreUint32(&jst.interrupt, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// call executes a method on a JS object, catching any errors, formatting and
|
// call executes a method on a JS object, catching any errors, formatting and
|
||||||
// returning them as error objects.
|
// returning them as error objects.
|
||||||
func (jst *Tracer) call(noret bool, method string, args ...string) (json.RawMessage, error) {
|
func (jst *jsTracer) call(noret bool, method string, args ...string) (json.RawMessage, error) {
|
||||||
// Execute the JavaScript call and return any error
|
// Execute the JavaScript call and return any error
|
||||||
jst.vm.PushString(method)
|
jst.vm.PushString(method)
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
@ -663,7 +659,7 @@ func wrapError(context string, err error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureStart implements the Tracer interface to initialize the tracing operation.
|
// CaptureStart implements the Tracer interface to initialize the tracing operation.
|
||||||
func (jst *Tracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
func (jst *jsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
|
||||||
jst.ctx["type"] = "CALL"
|
jst.ctx["type"] = "CALL"
|
||||||
if create {
|
if create {
|
||||||
jst.ctx["type"] = "CREATE"
|
jst.ctx["type"] = "CREATE"
|
||||||
@ -693,7 +689,7 @@ func (jst *Tracer) CaptureStart(env *vm.EVM, from common.Address, to common.Addr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureState implements the Tracer interface to trace a single step of VM execution.
|
// CaptureState implements the Tracer interface to trace a single step of VM execution.
|
||||||
func (jst *Tracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
func (jst *jsTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
|
||||||
if !jst.traceSteps {
|
if !jst.traceSteps {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -729,7 +725,7 @@ func (jst *Tracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureFault implements the Tracer interface to trace an execution fault
|
// CaptureFault implements the Tracer interface to trace an execution fault
|
||||||
func (jst *Tracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
|
func (jst *jsTracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
|
||||||
if jst.err != nil {
|
if jst.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -743,7 +739,7 @@ func (jst *Tracer) CaptureFault(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureEnd is called after the call finishes to finalize the tracing.
|
// CaptureEnd is called after the call finishes to finalize the tracing.
|
||||||
func (jst *Tracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) {
|
func (jst *jsTracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) {
|
||||||
jst.ctx["output"] = output
|
jst.ctx["output"] = output
|
||||||
jst.ctx["time"] = t.String()
|
jst.ctx["time"] = t.String()
|
||||||
jst.ctx["gasUsed"] = gasUsed
|
jst.ctx["gasUsed"] = gasUsed
|
||||||
@ -754,7 +750,7 @@ func (jst *Tracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct).
|
// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct).
|
||||||
func (jst *Tracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
func (jst *jsTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
|
||||||
if !jst.traceCallFrames {
|
if !jst.traceCallFrames {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -784,7 +780,7 @@ func (jst *Tracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Ad
|
|||||||
|
|
||||||
// CaptureExit is called when EVM exits a scope, even if the scope didn't
|
// CaptureExit is called when EVM exits a scope, even if the scope didn't
|
||||||
// execute any code.
|
// execute any code.
|
||||||
func (jst *Tracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
func (jst *jsTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
||||||
if !jst.traceCallFrames {
|
if !jst.traceCallFrames {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -808,7 +804,7 @@ func (jst *Tracer) CaptureExit(output []byte, gasUsed uint64, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error
|
// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error
|
||||||
func (jst *Tracer) GetResult() (json.RawMessage, error) {
|
func (jst *jsTracer) GetResult() (json.RawMessage, error) {
|
||||||
// Transform the context into a JavaScript object and inject into the state
|
// Transform the context into a JavaScript object and inject into the state
|
||||||
obj := jst.vm.PushObject()
|
obj := jst.vm.PushObject()
|
||||||
|
|
||||||
@ -830,7 +826,7 @@ func (jst *Tracer) GetResult() (json.RawMessage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addToObj pushes a field to a JS object.
|
// addToObj pushes a field to a JS object.
|
||||||
func (jst *Tracer) addToObj(obj int, key string, val interface{}) {
|
func (jst *jsTracer) addToObj(obj int, key string, val interface{}) {
|
||||||
pushValue(jst.vm, val)
|
pushValue(jst.vm, val)
|
||||||
jst.vm.PutPropString(obj, key)
|
jst.vm.PutPropString(obj, key)
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func testCtx() *vmContext {
|
|||||||
return &vmContext{blockCtx: vm.BlockContext{BlockNumber: big.NewInt(1)}, txCtx: vm.TxContext{GasPrice: big.NewInt(100000)}}
|
return &vmContext{blockCtx: vm.BlockContext{BlockNumber: big.NewInt(1)}, txCtx: vm.TxContext{GasPrice: big.NewInt(100000)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTrace(tracer *Tracer, vmctx *vmContext, chaincfg *params.ChainConfig) (json.RawMessage, error) {
|
func runTrace(tracer Tracer, vmctx *vmContext, chaincfg *params.ChainConfig) (json.RawMessage, error) {
|
||||||
env := vm.NewEVM(vmctx.blockCtx, vmctx.txCtx, &dummyStatedb{}, chaincfg, vm.Config{Debug: true, Tracer: tracer})
|
env := vm.NewEVM(vmctx.blockCtx, vmctx.txCtx, &dummyStatedb{}, chaincfg, vm.Config{Debug: true, Tracer: tracer})
|
||||||
var (
|
var (
|
||||||
startGas uint64 = 10000
|
startGas uint64 = 10000
|
||||||
@ -168,7 +168,7 @@ func TestHaltBetweenSteps(t *testing.T) {
|
|||||||
// TestNoStepExec tests a regular value transfer (no exec), and accessing the statedb
|
// TestNoStepExec tests a regular value transfer (no exec), and accessing the statedb
|
||||||
// in 'result'
|
// in 'result'
|
||||||
func TestNoStepExec(t *testing.T) {
|
func TestNoStepExec(t *testing.T) {
|
||||||
runEmptyTrace := func(tracer *Tracer, vmctx *vmContext) (json.RawMessage, error) {
|
runEmptyTrace := func(tracer Tracer, vmctx *vmContext) (json.RawMessage, error) {
|
||||||
env := vm.NewEVM(vmctx.blockCtx, vmctx.txCtx, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: tracer})
|
env := vm.NewEVM(vmctx.blockCtx, vmctx.txCtx, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: tracer})
|
||||||
startGas := uint64(10000)
|
startGas := uint64(10000)
|
||||||
contract := vm.NewContract(account{}, account{}, big.NewInt(0), startGas)
|
contract := vm.NewContract(account{}, account{}, big.NewInt(0), startGas)
|
||||||
|
@ -18,14 +18,53 @@
|
|||||||
package tracers
|
package tracers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
"github.com/ethereum/go-ethereum/eth/tracers/internal/tracers"
|
"github.com/ethereum/go-ethereum/eth/tracers/internal/tracers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// all contains all the built in JavaScript tracers by name.
|
// Tracer interface extends vm.EVMLogger and additionally
|
||||||
var all = make(map[string]string)
|
// allows collecting the tracing result.
|
||||||
|
type Tracer interface {
|
||||||
|
vm.EVMLogger
|
||||||
|
GetResult() (json.RawMessage, error)
|
||||||
|
// Stop terminates execution of the tracer at the first opportune moment.
|
||||||
|
Stop(err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nativeTracers map[string]func() Tracer = make(map[string]func() Tracer)
|
||||||
|
jsTracers = make(map[string]string)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterNativeTracer makes native tracers which adhere
|
||||||
|
// to the `Tracer` interface available to the rest of the codebase.
|
||||||
|
// It is typically invoked in the `init()` function, e.g. see the `native/call.go`.
|
||||||
|
func RegisterNativeTracer(name string, ctor func() Tracer) {
|
||||||
|
nativeTracers[name] = ctor
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new instance of a tracer,
|
||||||
|
// 1. If 'code' is the name of a registered native tracer, then that tracer
|
||||||
|
// is instantiated and returned
|
||||||
|
// 2. If 'code' is the name of a registered js-tracer, then that tracer is
|
||||||
|
// instantiated and returned
|
||||||
|
// 3. Otherwise, the code is interpreted as the js code of a js-tracer, and
|
||||||
|
// is evaluated and returned.
|
||||||
|
func New(code string, ctx *Context) (Tracer, error) {
|
||||||
|
// Resolve native tracer
|
||||||
|
if fn, ok := nativeTracers[code]; ok {
|
||||||
|
return fn(), nil
|
||||||
|
}
|
||||||
|
// Resolve js-tracers by name and assemble the tracer object
|
||||||
|
if tracer, ok := jsTracers[code]; ok {
|
||||||
|
code = tracer
|
||||||
|
}
|
||||||
|
return newJsTracer(code, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// camel converts a snake cased input string into a camel cased output.
|
// camel converts a snake cased input string into a camel cased output.
|
||||||
func camel(str string) string {
|
func camel(str string) string {
|
||||||
@ -40,14 +79,6 @@ func camel(str string) string {
|
|||||||
func init() {
|
func init() {
|
||||||
for _, file := range tracers.AssetNames() {
|
for _, file := range tracers.AssetNames() {
|
||||||
name := camel(strings.TrimSuffix(file, ".js"))
|
name := camel(strings.TrimSuffix(file, ".js"))
|
||||||
all[name] = string(tracers.MustAsset(file))
|
jsTracers[name] = string(tracers.MustAsset(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracer retrieves a specific JavaScript tracer by name.
|
|
||||||
func tracer(name string) (string, bool) {
|
|
||||||
if tracer, ok := all[name]; ok {
|
|
||||||
return tracer, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
@ -20,23 +20,18 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
"github.com/ethereum/go-ethereum/core"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
"github.com/ethereum/go-ethereum/core/vm"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/tests"
|
"github.com/ethereum/go-ethereum/tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,22 +99,6 @@ type callTrace struct {
|
|||||||
Calls []callTrace `json:"calls,omitempty"`
|
Calls []callTrace `json:"calls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type callContext struct {
|
|
||||||
Number math.HexOrDecimal64 `json:"number"`
|
|
||||||
Difficulty *math.HexOrDecimal256 `json:"difficulty"`
|
|
||||||
Time math.HexOrDecimal64 `json:"timestamp"`
|
|
||||||
GasLimit math.HexOrDecimal64 `json:"gasLimit"`
|
|
||||||
Miner common.Address `json:"miner"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// callTracerTest defines a single test to check the call tracer against.
|
|
||||||
type callTracerTest struct {
|
|
||||||
Genesis *core.Genesis `json:"genesis"`
|
|
||||||
Context *callContext `json:"context"`
|
|
||||||
Input string `json:"input"`
|
|
||||||
Result *callTrace `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestZeroValueToNotExitCall tests the calltracer(s) on the following:
|
// TestZeroValueToNotExitCall tests the calltracer(s) on the following:
|
||||||
// Tx to A, A calls B with zero value. B does not already exist.
|
// Tx to A, A calls B with zero value. B does not already exist.
|
||||||
// Expected: that enter/exit is invoked and the inner call is shown in the result
|
// Expected: that enter/exit is invoked and the inner call is shown in the result
|
||||||
@ -280,96 +259,6 @@ func TestPrestateTracerCreate2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterates over all the input-output datasets in the tracer test harness and
|
|
||||||
// runs the JavaScript tracers against them.
|
|
||||||
func TestCallTracerLegacy(t *testing.T) {
|
|
||||||
testCallTracer("callTracerLegacy", "call_tracer_legacy", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCallTracer(tracer string, dirPath string, t *testing.T) {
|
|
||||||
files, err := ioutil.ReadDir(filepath.Join("testdata", dirPath))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to retrieve tracer test suite: %v", err)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if !strings.HasSuffix(file.Name(), ".json") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
file := file // capture range variable
|
|
||||||
t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Call tracer test found, read if from disk
|
|
||||||
blob, err := ioutil.ReadFile(filepath.Join("testdata", dirPath, file.Name()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read testcase: %v", err)
|
|
||||||
}
|
|
||||||
test := new(callTracerTest)
|
|
||||||
if err := json.Unmarshal(blob, test); err != nil {
|
|
||||||
t.Fatalf("failed to parse testcase: %v", err)
|
|
||||||
}
|
|
||||||
// Configure a blockchain with the given prestate
|
|
||||||
tx := new(types.Transaction)
|
|
||||||
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
|
|
||||||
t.Fatalf("failed to parse testcase input: %v", err)
|
|
||||||
}
|
|
||||||
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
|
|
||||||
origin, _ := signer.Sender(tx)
|
|
||||||
txContext := vm.TxContext{
|
|
||||||
Origin: origin,
|
|
||||||
GasPrice: tx.GasPrice(),
|
|
||||||
}
|
|
||||||
context := vm.BlockContext{
|
|
||||||
CanTransfer: core.CanTransfer,
|
|
||||||
Transfer: core.Transfer,
|
|
||||||
Coinbase: test.Context.Miner,
|
|
||||||
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
|
|
||||||
Time: new(big.Int).SetUint64(uint64(test.Context.Time)),
|
|
||||||
Difficulty: (*big.Int)(test.Context.Difficulty),
|
|
||||||
GasLimit: uint64(test.Context.GasLimit),
|
|
||||||
}
|
|
||||||
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
|
|
||||||
|
|
||||||
// Create the tracer, the EVM environment and run it
|
|
||||||
tracer, err := New(tracer, new(Context))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create call tracer: %v", err)
|
|
||||||
}
|
|
||||||
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
|
|
||||||
|
|
||||||
msg, err := tx.AsMessage(signer, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to prepare transaction for tracing: %v", err)
|
|
||||||
}
|
|
||||||
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
|
|
||||||
if _, err = st.TransitionDb(); err != nil {
|
|
||||||
t.Fatalf("failed to execute transaction: %v", err)
|
|
||||||
}
|
|
||||||
// Retrieve the trace result and compare against the etalon
|
|
||||||
res, err := tracer.GetResult()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to retrieve trace result: %v", err)
|
|
||||||
}
|
|
||||||
ret := new(callTrace)
|
|
||||||
if err := json.Unmarshal(res, ret); err != nil {
|
|
||||||
t.Fatalf("failed to unmarshal trace result: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !jsonEqual(ret, test.Result) {
|
|
||||||
// uncomment this for easier debugging
|
|
||||||
//have, _ := json.MarshalIndent(ret, "", " ")
|
|
||||||
//want, _ := json.MarshalIndent(test.Result, "", " ")
|
|
||||||
//t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))
|
|
||||||
t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCallTracer(t *testing.T) {
|
|
||||||
testCallTracer("callTracer", "call_tracer", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
|
// jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to
|
||||||
// comparison
|
// comparison
|
||||||
func jsonEqual(x, y interface{}) bool {
|
func jsonEqual(x, y interface{}) bool {
|
||||||
@ -466,73 +355,3 @@ func BenchmarkTransactionTrace(b *testing.B) {
|
|||||||
tracer.Reset()
|
tracer.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkTracers(b *testing.B) {
|
|
||||||
files, err := ioutil.ReadDir(filepath.Join("testdata", "call_tracer"))
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to retrieve tracer test suite: %v", err)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if !strings.HasSuffix(file.Name(), ".json") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
file := file // capture range variable
|
|
||||||
b.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(b *testing.B) {
|
|
||||||
blob, err := ioutil.ReadFile(filepath.Join("testdata", "call_tracer", file.Name()))
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to read testcase: %v", err)
|
|
||||||
}
|
|
||||||
test := new(callTracerTest)
|
|
||||||
if err := json.Unmarshal(blob, test); err != nil {
|
|
||||||
b.Fatalf("failed to parse testcase: %v", err)
|
|
||||||
}
|
|
||||||
benchTracer("callTracer", test, b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
|
|
||||||
// Configure a blockchain with the given prestate
|
|
||||||
tx := new(types.Transaction)
|
|
||||||
if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
|
|
||||||
b.Fatalf("failed to parse testcase input: %v", err)
|
|
||||||
}
|
|
||||||
signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)))
|
|
||||||
msg, err := tx.AsMessage(signer, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to prepare transaction for tracing: %v", err)
|
|
||||||
}
|
|
||||||
origin, _ := signer.Sender(tx)
|
|
||||||
txContext := vm.TxContext{
|
|
||||||
Origin: origin,
|
|
||||||
GasPrice: tx.GasPrice(),
|
|
||||||
}
|
|
||||||
context := vm.BlockContext{
|
|
||||||
CanTransfer: core.CanTransfer,
|
|
||||||
Transfer: core.Transfer,
|
|
||||||
Coinbase: test.Context.Miner,
|
|
||||||
BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
|
|
||||||
Time: new(big.Int).SetUint64(uint64(test.Context.Time)),
|
|
||||||
Difficulty: (*big.Int)(test.Context.Difficulty),
|
|
||||||
GasLimit: uint64(test.Context.GasLimit),
|
|
||||||
}
|
|
||||||
_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
|
|
||||||
|
|
||||||
// Create the tracer, the EVM environment and run it
|
|
||||||
tracer, err := New(tracerName, new(Context))
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to create call tracer: %v", err)
|
|
||||||
}
|
|
||||||
evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer})
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
snap := statedb.Snapshot()
|
|
||||||
st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
|
|
||||||
if _, err = st.TransitionDb(); err != nil {
|
|
||||||
b.Fatalf("failed to execute transaction: %v", err)
|
|
||||||
}
|
|
||||||
statedb.RevertToSnapshot(snap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user