cmd, core, eth/tracers: support fancier js tracing

This commit is contained in:
Péter Szilágyi 2017-11-18 14:23:23 +02:00
parent 4b939c23e4
commit b6467c167a
11 changed files with 589 additions and 33 deletions

View file

@ -19,6 +19,7 @@ package main
import (
"encoding/json"
"io"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
@ -35,6 +36,10 @@ func NewJSONLogger(cfg *vm.LogConfig, writer io.Writer) *JSONLogger {
return &JSONLogger{json.NewEncoder(writer), cfg}
}
func (l *JSONLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) error {
return nil
}
// CaptureState outputs state information on the logger.
func (l *JSONLogger) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *vm.Stack, contract *vm.Contract, depth int, err error) error {
log := vm.StructLog{

View file

@ -19,6 +19,7 @@ package vm
import (
"math/big"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
@ -171,7 +172,13 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
}
start := time.Now()
ret, err = run(evm, contract, input)
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
@ -181,6 +188,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
contract.UseGas(contract.Gas)
}
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}
return ret, contract.Gas, err
}
@ -338,7 +348,14 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, contractAddr, gas, nil
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, value)
}
start := time.Now()
ret, err = run(evm, contract, nil)
// check whether the max code size has been exceeded
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
// if the contract creation ran successfully and no errors were returned
@ -367,6 +384,9 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I
if maxCodeSizeExceeded && err == nil {
err = errMaxCodeSizeExceeded
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}
return ret, contractAddr, contract.Gas, err
}

View file

@ -84,6 +84,7 @@ func (s *StructLog) OpName() string {
// Note that reference types are actual VM data structures; make copies
// if you need to retain them beyond the current call.
type Tracer interface {
CaptureStart(from common.Address, to common.Address, call bool, input []byte, gas uint64, value *big.Int) error
CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error
CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) error
}
@ -111,6 +112,10 @@ func NewStructLogger(cfg *LogConfig) *StructLogger {
return logger
}
func (l *StructLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) error {
return nil
}
// CaptureState logs a new structured log message and pushes it out to the environment
//
// CaptureState also tracks SSTORE ops to track dirty values.

View file

@ -34,6 +34,7 @@ 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"
@ -488,21 +489,23 @@ func (t *timeoutError) Error() string {
// 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
var (
tracer vm.Tracer
err error
)
if config != nil && config.Tracer != nil {
timeout := defaultTraceTimeout
if config.Timeout != nil {
var err error
if timeout, err = time.ParseDuration(*config.Timeout); err != nil {
return nil, err
}
}
var err error
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() {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,117 @@
// callTracer is a full blown transaction tracer that extracts and reports all
// the internal calls made by a transaction, along with any useful information.
{
// invocations is a nested data structure containing all the internal contract
// calls executed by a transaction.
invocations: [],
// callstack is the current recursive call stack of the EVM execution.
callstack: [],
// descended tracks whether we've just descended from an outer transaction into
// an inner call.
descended: false,
// step is invoked for every opcode that the VM executes.
step: function(log, db) {
// We only care about system opcodes, faster if we pre-check once
var syscall = (log.op.toNumber() & 0xf0) == 0xf0;
if (syscall) {
var op = log.op.toString();
}
// If a new method invocation is being done, add to the call stack
if (syscall && op != "RETURN" && op != "REVERT") {
// Skip any pre-compile invocations, those are just fancy opcodes
var to = toAddress(log.stack.peek(1).Bytes());
if (isPrecompiled(to)) {
return
}
// Otherwise gather some internal call details
var off = (op == 'DELEGATECALL' ? 0 : 1);
var inOff = log.stack.peek(2 + off).Int64();
var inEnd = inOff + log.stack.peek(3 + off).Int64();
// Assemble the internal call report and store for completion
var call = {
type: op,
from: log.account,
to: to,
input: toHex(log.memory.slice(inOff, inEnd)),
gasDiff: log.gas - log.cost,
outOff: log.stack.peek(4 + off).Int64(),
outLen: log.stack.peek(5 + off).Int64()
};
if (op != 'DELEGATECALL') {
call.value = '0x' + log.stack.peek(2).Text(16);
}
this.callstack.push(call);
this.descended = true
return;
}
// If we've just descended into an inner call, retrieve it's true allowance. We
// need to extract if from within the call as there may be funky gas dynamics
// with regard to requested and actually given gas (2300 stipend, 63/64 rule).
if (this.descended) {
if (log.depth > this.callstack.length) {
this.callstack[this.callstack.length - 1].gas = log.gas;
} else {
this.callstack[this.callstack.length - 1].gas = 2300; // TODO (karalabe): Erm...
}
this.descended = false;
}
// If an existing call is returning, pop off the call stack
if (syscall && op == 'REVERT') {
call.revert = true;
return;
}
if (log.depth == this.callstack.length) {
// Pop off the last call and get the execution results
var call = this.callstack.pop();
call.gasUsed = '0x' + big.NewInt(call.gas - log.gas + call.gasDiff).Text(16);
call.gas = '0x' + big.NewInt(call.gas).Text(16);
delete call.gasDiff;
call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen));
delete call.outOff;
delete call.outLen;
// Append to the invocation list if topmost
var left = this.callstack.length;
if (left == 0) {
this.invocations.push(call);
return
}
// Apparently it was a nested call, inject into the previous one
if (this.callstack[left-1].calls === undefined) {
this.callstack[left-1].calls = [];
}
this.callstack[left-1].calls.push(call);
}
},
// result is invoked when all the opcodes have been iterated over and returns
// the final result of the tracing.
result: function(ctx) {
var result = {
type: ctx.type,
from: toAddress(ctx.from),
to: toAddress(ctx.to),
value: '0x' + ctx.value.Text(16),
gas: '0x' + big.NewInt(ctx.gas).Text(16),
gasUsed: '0x' + big.NewInt(ctx.gasUsed).Text(16),
input: toHex(ctx.input),
output: toHex(ctx.output),
time: ctx.time,
};
if (this.invocations.length > 0) {
result.calls = this.invocations;
}
if (ctx.error !== undefined) {
result.error = ctx.error;
}
return result;
}
}

View file

@ -0,0 +1,10 @@
// noopTracer is just the barebone boilerplate code required from a JavaScript
// object to be usable as a transaction tracer.
{
// step is invoked for every opcode that the VM executes.
step: function(log, db) { },
// result is invoked when all the opcodes have been iterated over and returns
// the final result of the tracing.
result: function(ctx) { }
}

View file

@ -0,0 +1,13 @@
// opcountTracer is a sample tracer that just counts the number of instructions
// executed by the EVM before the transaction terminated.
{
// count tracks the number of EVM instructions executed.
count: 0,
// step is invoked for every opcode that the VM executes.
step: function(log, db) { this.count++ },
// result is invoked when all the opcodes have been iterated over and returns
// the final result of the tracing.
result: function(ctx) { return this.count }
}

View file

@ -0,0 +1,21 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
//go:generate go-bindata -nometadata -o assets.go -pkg tracers -ignore ((tracers)|(assets)).go ./...
//go:generate gofmt -s -w assets.go
// Package tracers contains the actual JavaScript tracer assets.
package tracers

51
eth/tracers/tracers.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package tracers is a collection of JavaScript transaction tracers.
package tracers
import (
"strings"
"unicode"
"github.com/ethereum/go-ethereum/eth/tracers/internal/tracers"
)
// all contains all the built in JavaScript tracers by name.
var all = make(map[string]string)
// init retrieves the JavaScript transaction tracers included in go-ethereum.
func init() {
for _, file := range tracers.AssetNames() {
// Convert the underscored tracer file name into a camelcase tracer name
pieces := strings.Split(strings.TrimSuffix(file, ".js"), "_")
for i := 1; i < len(pieces); i++ {
pieces[i] = string(unicode.ToUpper(rune(pieces[i][0]))) + pieces[i][1:]
}
name := strings.Join(pieces, "")
// Retrieve and store the tracer
all[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
}

View file

@ -37,23 +37,23 @@ func (fb *fakeBig) NewInt(x int64) *big.Int {
return big.NewInt(x)
}
// OpCodeWrapper provides a JavaScript-friendly wrapper around OpCode, to convince Otto to treat it
// opCodeWrapper provides a JavaScript-friendly wrapper around OpCode, to convince Otto to treat it
// as an object, instead of a number.
type opCodeWrapper struct {
op vm.OpCode
}
// toNumber returns the ID of this opcode as an integer
// toNumber returns the ID of this opcode as an integer.
func (ocw *opCodeWrapper) toNumber() int {
return int(ocw.op)
}
// toString returns the string representation of the opcode
// toString returns the string representation of the opcode.
func (ocw *opCodeWrapper) toString() string {
return ocw.op.String()
}
// isPush returns true if the op is a Push
// isPush returns true if the op is a Push.
func (ocw *opCodeWrapper) isPush() bool {
return ocw.op.IsPush()
}
@ -200,8 +200,11 @@ func (c *contractWrapper) toValue(vm *otto.Otto) otto.Value {
// JavascriptTracer provides an implementation of Tracer that evaluates a
// Javascript function for each VM execution step.
type JavascriptTracer struct {
inited bool // Flag whether the context was already inited from the EVM
vm *otto.Otto // Javascript VM instance
traceobj *otto.Object // User-supplied object to call
ctx map[string]interface{} // Map for the execution `ctx` arg to `step`
op *opCodeWrapper // Wrapper around the VM opcode
log map[string]interface{} // (Reusable) map for the `log` arg to `step`
logvalue otto.Value // JS view of `log`
@ -218,14 +221,19 @@ type JavascriptTracer struct {
// code specifies a Javascript snippet, which must evaluate to an expression
// returning an object with 'step' and 'result' functions.
func NewJavascriptTracer(code string) (*JavascriptTracer, error) {
vm := otto.New()
vm.Interrupt = make(chan func(), 1)
jsvm := otto.New()
jsvm.Interrupt = make(chan func(), 1)
// Set up builtins for this environment
vm.Set("big", &fakeBig{})
vm.Set("toHex", hexutil.Encode)
jsvm.Set("big", &fakeBig{})
jsvm.Set("toHex", hexutil.Encode)
jsvm.Set("toAddress", common.BytesToAddress)
jsvm.Set("isPrecompiled", func(addr []byte) bool {
_, ok := vm.PrecompiledContractsByzantium[common.BytesToAddress(addr)]
return ok
})
jstracer, err := vm.Object("(" + code + ")")
jstracer, err := jsvm.Object("(" + code + ")")
if err != nil {
return nil, err
}
@ -254,23 +262,24 @@ func NewJavascriptTracer(code string) (*JavascriptTracer, error) {
contract = new(contractWrapper)
)
log := map[string]interface{}{
"op": op.toValue(vm),
"memory": mem.toValue(vm),
"stack": stack.toValue(vm),
"contract": contract.toValue(vm),
"op": op.toValue(jsvm),
"memory": mem.toValue(jsvm),
"stack": stack.toValue(jsvm),
"contract": contract.toValue(jsvm),
}
logvalue, _ := vm.ToValue(log)
logvalue, _ := jsvm.ToValue(log)
return &JavascriptTracer{
vm: vm,
vm: jsvm,
traceobj: jstracer,
ctx: make(map[string]interface{}),
op: op,
log: log,
logvalue: logvalue,
memory: mem,
stack: stack,
db: db,
dbvalue: db.toValue(vm),
dbvalue: db.toValue(jsvm),
contract: contract,
err: nil,
}, nil
@ -317,8 +326,27 @@ func wrapError(context string, err error) error {
return fmt.Errorf("%v in server-side tracer function '%v'", message, context)
}
// CaptureState implements the Tracer interface to trace a single step of VM execution
// CaptureState implements the Tracer interface to initialize the tracing operation.
func (jst *JavascriptTracer) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) error {
jst.ctx["type"] = "CALL"
if create {
jst.ctx["type"] = "CREATE"
}
jst.ctx["from"] = from
jst.ctx["to"] = to
jst.ctx["input"] = input
jst.ctx["gas"] = gas
jst.ctx["value"] = value
return nil
}
// CaptureState implements the Tracer interface to trace a single step of VM execution.
func (jst *JavascriptTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode, gas, cost uint64, memory *vm.Memory, stack *vm.Stack, contract *vm.Contract, depth int, err error) error {
if !jst.inited {
jst.ctx["block"] = env.BlockNumber.Uint64()
jst.inited = true
}
if jst.err == nil {
jst.op.op = op
jst.memory.memory = memory
@ -344,21 +372,23 @@ func (jst *JavascriptTracer) CaptureState(env *vm.EVM, pc uint64, op vm.OpCode,
return nil
}
// CaptureEnd is called after the call finishes
// CaptureEnd is called after the call finishes to finalize the tracing.
func (jst *JavascriptTracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) error {
//TODO! @Arachnid please figure out of there's anything we can use this method for
jst.ctx["output"] = output
jst.ctx["gasUsed"] = gasUsed
jst.ctx["time"] = t.String()
if err != nil {
jst.ctx["error"] = err.Error()
}
ctxvalue, _ := jst.vm.ToValue(jst.ctx)
jst.result, jst.err = jst.callSafely("result", ctxvalue, jst.dbvalue)
if jst.err != nil {
jst.err = wrapError("result", jst.err)
}
return nil
}
// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error
func (jst *JavascriptTracer) GetResult() (result interface{}, err error) {
if jst.err != nil {
return nil, jst.err
}
result, err = jst.callSafely("result")
if err != nil {
err = wrapError("result", err)
}
return
return jst.result, jst.err
}