eth/tracers: rework the model a bit

This commit is contained in:
Martin Holst Swende
2021-10-26 14:34:59 +02:00
parent 0946623acc
commit 5db19c5922
8 changed files with 105 additions and 129 deletions

View File

@ -494,7 +494,7 @@ func BenchmarkSimpleLoop(b *testing.B) {
//Execute(loopingCode, nil, &Config{
// EVMConfig: vm.Config{
// Debug: true,
// Tracer: tracer,
// JSTracer: tracer,
// }})
// 100M gas
benchmarkNonModifyingCode(100000000, staticCallIdentity, "staticcall-identity-100M", "", b)

View File

@ -36,7 +36,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/native"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
@ -860,22 +859,17 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
}
}
// Native tracers take precedence
var ok bool
if tracer, ok = native.New(*config.Tracer); !ok {
if tracer, err = New(*config.Tracer, txctx); err != nil {
return nil, err
}
// TODO(s1na): do we need timeout for native tracers?
// 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()
if tracer, err = New(*config.Tracer, txctx); err != nil {
return nil, err
}
deadlineCtx, cancel := context.WithTimeout(ctx, timeout)
go func() {
<-deadlineCtx.Done()
if deadlineCtx.Err() == context.DeadlineExceeded {
tracer.(*JSTracer).Stop(errors.New("execution timeout"))
}
}()
defer cancel()
case config == nil:
tracer = vm.NewStructLogger(nil)
@ -909,7 +903,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
}, nil
case native.Tracer:
case Tracer:
return tracer.GetResult()
default:

View File

@ -26,10 +26,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers"
)
func init() {
register("callTracerNative", NewCallTracer)
tracers.RegisterNativeTracer("callTracerNative", NewCallTracer)
}
type callFrame struct {
@ -45,20 +46,20 @@ type callFrame struct {
Calls []callFrame `json:"calls,omitempty"`
}
type CallTracer struct {
type callTracer struct {
callstack []callFrame
}
// NewCallTracer returns a native go tracer which tracks
// call frames of a tx, and implements vm.Tracer.
func NewCallTracer() Tracer {
func NewCallTracer() tracers.Tracer {
// First callframe contains tx context info
// and is populated on start and end.
t := &CallTracer{callstack: make([]callFrame, 1)}
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) {
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),
@ -72,7 +73,7 @@ func (t *CallTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Ad
}
}
func (t *CallTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) {
func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) {
t.callstack[0].Output = bytesToHex(output)
t.callstack[0].GasUsed = uintToHex(gasUsed)
if err != nil {
@ -80,13 +81,13 @@ func (t *CallTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration,
}
}
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) 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) 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) {
func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
call := callFrame{
Type: typ.String(),
From: addrToHex(from),
@ -98,7 +99,7 @@ func (t *CallTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.
t.callstack = append(t.callstack, call)
}
func (t *CallTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
size := len(t.callstack)
if size <= 1 {
return
@ -120,7 +121,7 @@ func (t *CallTracer) CaptureExit(output []byte, gasUsed uint64, err error) {
t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call)
}
func (t *CallTracer) GetResult() (json.RawMessage, error) {
func (t *callTracer) GetResult() (json.RawMessage, error) {
if len(t.callstack) != 1 {
return nil, errors.New("incorrect number of top-level calls")
}
@ -131,6 +132,10 @@ func (t *CallTracer) GetResult() (json.RawMessage, error) {
return json.RawMessage(res), nil
}
func (t *callTracer) Stop(err error) {
// TODO
}
func bytesToHex(s []byte) string {
return "0x" + common.Bytes2Hex(s)
}

View File

@ -1,51 +0,0 @@
// 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"
"github.com/ethereum/go-ethereum/core/vm"
)
// Tracer interface extends vm.Tracer and additionally
// allows collecting the tracing result.
type Tracer interface {
vm.Tracer
GetResult() (json.RawMessage, error)
}
// constructor creates a new instance of a Tracer.
type constructor func() Tracer
var tracers map[string]constructor = make(map[string]constructor)
// register makes native tracers in this directory which adhere
// to the `Tracer` interface available to the rest of the codebase.
// It is typically invoked in the `init()` function.
func register(name string, fn constructor) {
tracers[name] = fn
}
// New returns a new instance of a tracer, if one was
// registered under the given name.
func New(name string) (Tracer, bool) {
if fn, ok := tracers[name]; ok {
return fn(), true
}
return nil, false
}

View File

@ -100,13 +100,13 @@ func (mw *memoryWrapper) slice(begin, end int64) []byte {
if end < begin || begin < 0 {
// TODO(karalabe): We can't js-throw from Go inside duktape inside Go. The Go
// runtime goes belly up https://github.com/golang/go/issues/15639.
log.Warn("Tracer accessed out of bound memory", "offset", begin, "end", end)
log.Warn("JSTracer accessed out of bound memory", "offset", begin, "end", end)
return nil
}
if mw.memory.Len() < int(end) {
// TODO(karalabe): We can't js-throw from Go inside duktape inside Go. The Go
// runtime goes belly up https://github.com/golang/go/issues/15639.
log.Warn("Tracer accessed out of bound memory", "available", mw.memory.Len(), "offset", begin, "size", end-begin)
log.Warn("JSTracer accessed out of bound memory", "available", mw.memory.Len(), "offset", begin, "size", end-begin)
return nil
}
return mw.memory.GetCopy(begin, end-begin)
@ -117,7 +117,7 @@ func (mw *memoryWrapper) getUint(addr int64) *big.Int {
if mw.memory.Len() < int(addr)+32 || addr < 0 {
// TODO(karalabe): We can't js-throw from Go inside duktape inside Go. The Go
// runtime goes belly up https://github.com/golang/go/issues/15639.
log.Warn("Tracer accessed out of bound memory", "available", mw.memory.Len(), "offset", addr, "size", 32)
log.Warn("JSTracer accessed out of bound memory", "available", mw.memory.Len(), "offset", addr, "size", 32)
return new(big.Int)
}
return new(big.Int).SetBytes(mw.memory.GetPtr(addr, 32))
@ -160,7 +160,7 @@ func (sw *stackWrapper) peek(idx int) *big.Int {
if len(sw.stack.Data()) <= idx || idx < 0 {
// TODO(karalabe): We can't js-throw from Go inside duktape inside Go. The Go
// runtime goes belly up https://github.com/golang/go/issues/15639.
log.Warn("Tracer accessed out of bound stack", "size", len(sw.stack.Data()), "index", idx)
log.Warn("JSTracer accessed out of bound stack", "size", len(sw.stack.Data()), "index", idx)
return new(big.Int)
}
return sw.stack.Back(idx).ToBig()
@ -365,7 +365,7 @@ func (r *frameResult) pushObject(vm *duktape.Context) {
// Tracer provides an implementation of Tracer that evaluates a Javascript
// function for each VM execution step.
type Tracer struct {
type JSTracer struct {
vm *duktape.Context // Javascript VM instance
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,
// which must evaluate to an expression returning an object with 'step', 'fault'
// and 'result' functions.
func New(code string, ctx *Context) (*Tracer, error) {
// Resolve any tracers by name and assemble the tracer object
if tracer, ok := tracer(code); ok {
code = tracer
}
tracer := &Tracer{
func newJsTracer(code string, ctx *Context) (*JSTracer, error) {
tracer := &JSTracer{
vm: duktape.New(),
ctx: make(map[string]interface{}),
opWrapper: new(opWrapper),
@ -522,7 +518,7 @@ func New(code string, ctx *Context) (*Tracer, error) {
if start < 0 || start > end || end > len(blob) {
// TODO(karalabe): We can't js-throw from Go inside duktape inside Go. The Go
// runtime goes belly up https://github.com/golang/go/issues/15639.
log.Warn("Tracer accessed out of bound memory", "available", len(blob), "offset", start, "size", size)
log.Warn("JSTracer accessed out of bound memory", "available", len(blob), "offset", start, "size", size)
ctx.PushFixedBuffer(0)
return 1
}
@ -566,7 +562,7 @@ func New(code string, ctx *Context) (*Tracer, error) {
tracer.traceCallFrames = hasEnter
tracer.traceSteps = hasStep
// Tracer is valid, inject the big int library to access large numbers
// JSTracer is valid, inject the big int library to access large numbers
tracer.vm.EvalString(bigIntegerJS)
tracer.vm.PutGlobalString("bigInt")
@ -627,14 +623,14 @@ func New(code string, ctx *Context) (*Tracer, error) {
}
// 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
atomic.StoreUint32(&jst.interrupt, 1)
}
// call executes a method on a JS object, catching any errors, formatting and
// 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
jst.vm.PushString(method)
for _, arg := range args {
@ -670,7 +666,7 @@ func wrapError(context string, err error) error {
}
// 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"
if create {
jst.ctx["type"] = "CREATE"
@ -700,7 +696,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.
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 {
return
}
@ -736,7 +732,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
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 {
return
}
@ -750,7 +746,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.
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["time"] = t.String()
jst.ctx["gasUsed"] = gasUsed
@ -761,7 +757,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).
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 {
return
}
@ -791,7 +787,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
// 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 {
return
}
@ -815,7 +811,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
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
obj := jst.vm.PushObject()
@ -837,7 +833,7 @@ func (jst *Tracer) GetResult() (json.RawMessage, error) {
}
// 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)
jst.vm.PutPropString(obj, key)
}

View File

@ -58,7 +58,7 @@ func testCtx() *vmContext {
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})
var (
startGas uint64 = 10000
@ -168,7 +168,7 @@ func TestHaltBetweenSteps(t *testing.T) {
// TestNoStepExec tests a regular value transfer (no exec), and accessing the statedb
// in 'result'
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})
startGas := uint64(10000)
contract := vm.NewContract(account{}, account{}, big.NewInt(0), startGas)
@ -223,7 +223,7 @@ func TestIsPrecompile(t *testing.T) {
t.Error(err)
}
if string(res) != "false" {
t.Errorf("Tracer should not consider blake2f as precompile in byzantium")
t.Errorf("JSTracer should not consider blake2f as precompile in byzantium")
}
tracer, _ = New("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", new(Context))
@ -233,7 +233,7 @@ func TestIsPrecompile(t *testing.T) {
t.Error(err)
}
if string(res) != "true" {
t.Errorf("Tracer should consider blake2f as precompile in istanbul")
t.Errorf("JSTracer should consider blake2f as precompile in istanbul")
}
}

View File

@ -18,14 +18,55 @@
package tracers
import (
"encoding/json"
"fmt"
"strings"
"unicode"
"github.com/ethereum/go-ethereum/core/vm"
"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)
// Tracer interface extends vm.JSTracer and additionally
// allows collecting the tracing result.
type Tracer interface {
vm.Tracer
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 in this directory which adhere
// to the `JSTracer` interface available to the rest of the codebase.
// It is typically invoked in the `init()` function.
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
}
panic(fmt.Sprintf("no native tracer %v found", code))
// 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.
func camel(str string) string {
@ -40,14 +81,6 @@ func camel(str string) string {
func init() {
for _, file := range tracers.AssetNames() {
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
}

View File

@ -35,7 +35,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/native"
//"github.com/ethereum/go-ethereum/eth/tracers/native"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/tests"
@ -205,7 +205,7 @@ 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) {
newTracer := func() native.Tracer {
newTracer := func() Tracer {
tracer, err := New("callTracerLegacy", new(Context))
if err != nil {
t.Fatalf("failed to create call tracer: %v", err)
@ -217,7 +217,7 @@ func TestCallTracerLegacy(t *testing.T) {
}
func TestCallTracer(t *testing.T) {
newTracer := func() native.Tracer {
newTracer := func() Tracer {
tracer, err := New("callTracer", new(Context))
if err != nil {
t.Fatalf("failed to create call tracer: %v", err)
@ -229,18 +229,17 @@ func TestCallTracer(t *testing.T) {
}
func TestCallTracerNative(t *testing.T) {
newTracer := func() native.Tracer {
tracer, ok := native.New("callTracerNative")
if !ok {
t.Fatal("failed to create native call tracer")
newTracer := func() Tracer {
tracer, err := New("callTracerNative", nil /* TODO? */)
if err != nil {
t.Fatalf("failed to create native call tracer: %v", err)
}
return tracer
}
testCallTracer(newTracer, "call_tracer", t)
}
func testCallTracer(newTracer func() native.Tracer, dirPath string, t *testing.T) {
func testCallTracer(newTracer func() Tracer, 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)
@ -431,7 +430,7 @@ func BenchmarkTracers(b *testing.B) {
if err := json.Unmarshal(blob, test); err != nil {
b.Fatalf("failed to parse testcase: %v", err)
}
newTracer := func() native.Tracer {
newTracer := func() Tracer {
tracer, err := New("callTracer", new(Context))
if err != nil {
b.Fatalf("failed to create call tracer: %v", err)
@ -443,7 +442,7 @@ func BenchmarkTracers(b *testing.B) {
}
}
func benchTracer(newTracer func() native.Tracer, test *callTracerTest, b *testing.B) {
func benchTracer(newTracer func() Tracer, 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 {