diff --git a/console/console_test.go b/console/console_test.go index 1330f5a86deb..04ba91d1576a 100644 --- a/console/console_test.go +++ b/console/console_test.go @@ -285,7 +285,7 @@ func TestPrettyError(t *testing.T) { defer tester.Close(t) tester.console.Evaluate("throw 'hello'") - want := jsre.ErrorColor("hello") + "\n\tat :1:7(1)\n\n" + want := jsre.ErrorColor("hello") + "\n\tat :1:1(1)\n\n" if output := tester.output.String(); output != want { t.Fatalf("pretty error mismatch: have %s, want %s", output, want) } diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 81816df9d84c..0551737ba1af 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -761,7 +761,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.CREATE), byte(vm.POP), }, - results: []string{`"1,1,4294935775,6,12"`, `"1,1,4294935775,6,0"`}, + results: []string{`"1,1,952855,6,12"`, `"1,1,952855,6,0"`}, }, { // CREATE2 @@ -777,7 +777,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.CREATE2), byte(vm.POP), }, - results: []string{`"1,1,4294935766,6,13"`, `"1,1,4294935766,6,0"`}, + results: []string{`"1,1,952846,6,13"`, `"1,1,952846,6,0"`}, }, { // CALL @@ -790,7 +790,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.CALL), byte(vm.POP), }, - results: []string{`"1,1,4294964716,6,13"`, `"1,1,4294964716,6,0"`}, + results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`}, }, { // CALLCODE @@ -803,7 +803,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.CALLCODE), byte(vm.POP), }, - results: []string{`"1,1,4294964716,6,13"`, `"1,1,4294964716,6,0"`}, + results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`}, }, { // STATICCALL @@ -815,7 +815,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.STATICCALL), byte(vm.POP), }, - results: []string{`"1,1,4294964719,6,12"`, `"1,1,4294964719,6,0"`}, + results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`}, }, { // DELEGATECALL @@ -827,7 +827,7 @@ func TestRuntimeJSTracer(t *testing.T) { byte(vm.DELEGATECALL), byte(vm.POP), }, - results: []string{`"1,1,4294964719,6,12"`, `"1,1,4294964719,6,0"`}, + results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`}, }, { // CALL self-destructing contract @@ -868,7 +868,8 @@ func TestRuntimeJSTracer(t *testing.T) { t.Fatal(err) } _, _, err = Call(main, nil, &Config{ - State: statedb, + GasLimit: 1000000, + State: statedb, EVMConfig: vm.Config{ Debug: true, Tracer: tracer, diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index d4c25ee78103..0f3778b1c64a 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -134,6 +134,10 @@ func TestCallTracerNative(t *testing.T) { testCallTracer("callTracer", "call_tracer", t) } +func TestCallTracerLegacyDuktape(t *testing.T) { + testCallTracer("callTracerLegacyDuktape", "call_tracer_legacy", t) +} + func testCallTracer(tracerName string, dirPath string, t *testing.T) { files, err := os.ReadDir(filepath.Join("testdata", dirPath)) if err != nil { @@ -258,7 +262,7 @@ func BenchmarkTracers(b *testing.B) { if err := json.Unmarshal(blob, test); err != nil { b.Fatalf("failed to parse testcase: %v", err) } - benchTracer("callTracerNative", test, b) + benchTracer("callTracer", test, b) }) } } diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go new file mode 100644 index 000000000000..05611a61142c --- /dev/null +++ b/eth/tracers/js/goja.go @@ -0,0 +1,853 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . +package js + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/dop251/goja" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" + jsassets "github.com/ethereum/go-ethereum/eth/tracers/js/internal/tracers" + "github.com/ethereum/go-ethereum/log" +) + +var assetTracers = make(map[string]string) + +// init retrieves the JavaScript transaction tracers included in go-ethereum. +func init() { + var err error + assetTracers, err = jsassets.Load() + if err != nil { + panic(err) + } + tracers.RegisterLookup(true, newGojaTracer) +} + +// bigIntProgram is compiled once and the exported function mostly invoked to convert +// hex strings into big ints. +var bigIntProgram = goja.MustCompile("bigInt", bigIntegerJS, false) + +type toBigFn = func(vm *goja.Runtime, val string) (goja.Value, error) +type toBufFn = func(vm *goja.Runtime, val []byte) (goja.Value, error) +type fromBufFn = func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) + +func toBuf(vm *goja.Runtime, bufType goja.Value, val []byte) (goja.Value, error) { + // bufType is usually Uint8Array. This is equivalent to `new Uint8Array(val)` in JS. + res, err := vm.New(bufType, vm.ToValue(val)) + if err != nil { + return nil, err + } + return vm.ToValue(res), nil +} + +func fromBuf(vm *goja.Runtime, bufType goja.Value, buf goja.Value, allowString bool) ([]byte, error) { + obj := buf.ToObject(vm) + switch obj.ClassName() { + case "String": + if !allowString { + break + } + return common.FromHex(obj.String()), nil + case "Array": + var b []byte + if err := vm.ExportTo(buf, &b); err != nil { + return nil, err + } + return b, nil + + case "Object": + if !obj.Get("constructor").SameAs(bufType) { + break + } + var b []byte + if err := vm.ExportTo(buf, &b); err != nil { + return nil, err + } + return b, nil + } + return nil, fmt.Errorf("invalid buffer type") +} + +type gojaTracer struct { + vm *goja.Runtime + env *vm.EVM + toBig toBigFn // Converts a hex string into a JS bigint + toBuf toBufFn // Converts a []byte into a JS buffer + fromBuf fromBufFn // Converts an array, hex string or Uint8Array to a []byte + ctx map[string]goja.Value // KV-bag passed to JS in `result` + activePrecompiles []common.Address // List of active precompiles at current block + traceStep bool // True if tracer object exposes a `step()` method + traceFrame bool // True if tracer object exposes the `enter()` and `exit()` methods + gasLimit uint64 // Amount of gas bought for the whole tx + err error // Any error that should stop tracing + obj *goja.Object // Trace object + + // Methods exposed by tracer + result goja.Callable + fault goja.Callable + step goja.Callable + enter goja.Callable + exit goja.Callable + + // Underlying structs being passed into JS + log *steplog + frame *callframe + frameResult *callframeResult + + // Goja-wrapping of types prepared for JS consumption + logValue goja.Value + dbValue goja.Value + frameValue goja.Value + frameResultValue goja.Value +} + +func newGojaTracer(code string, ctx *tracers.Context) (tracers.Tracer, error) { + if c, ok := assetTracers[code]; ok { + code = c + } + vm := goja.New() + // By default field names are exported to JS as is, i.e. capitalized. + vm.SetFieldNameMapper(goja.UncapFieldNameMapper()) + t := &gojaTracer{ + vm: vm, + ctx: make(map[string]goja.Value), + } + if ctx == nil { + ctx = new(tracers.Context) + } + if ctx.BlockHash != (common.Hash{}) { + t.ctx["blockHash"] = vm.ToValue(ctx.BlockHash.Bytes()) + if ctx.TxHash != (common.Hash{}) { + t.ctx["txIndex"] = vm.ToValue(ctx.TxIndex) + t.ctx["txHash"] = vm.ToValue(ctx.TxHash.Bytes()) + } + } + + t.setTypeConverters() + t.setBuiltinFunctions() + ret, err := vm.RunString("(" + code + ")") + if err != nil { + return nil, err + } + // Check tracer's interface for required and optional methods. + obj := ret.ToObject(vm) + result, ok := goja.AssertFunction(obj.Get("result")) + if !ok { + return nil, errors.New("trace object must expose a function result()") + } + fault, ok := goja.AssertFunction(obj.Get("fault")) + if !ok { + return nil, errors.New("trace object must expose a function fault()") + } + step, ok := goja.AssertFunction(obj.Get("step")) + t.traceStep = ok + enter, hasEnter := goja.AssertFunction(obj.Get("enter")) + exit, hasExit := goja.AssertFunction(obj.Get("exit")) + if hasEnter != hasExit { + return nil, errors.New("trace object must expose either both or none of enter() and exit()") + } + t.traceFrame = hasEnter + t.obj = obj + t.step = step + t.enter = enter + t.exit = exit + t.result = result + t.fault = fault + // Setup objects carrying data to JS. These are created once and re-used. + t.log = &steplog{ + vm: vm, + op: &opObj{vm: vm}, + memory: &memoryObj{w: new(memoryWrapper), vm: vm, toBig: t.toBig, toBuf: t.toBuf}, + stack: &stackObj{w: new(stackWrapper), vm: vm, toBig: t.toBig}, + contract: &contractObj{vm: vm, toBig: t.toBig, toBuf: t.toBuf}, + } + t.frame = &callframe{vm: vm, toBig: t.toBig, toBuf: t.toBuf} + t.frameResult = &callframeResult{vm: vm, toBuf: t.toBuf} + t.frameValue = t.frame.setupObject() + t.frameResultValue = t.frameResult.setupObject() + t.logValue = t.log.setupObject() + return t, nil +} + +// CaptureTxStart implements the Tracer interface and is invoked at the beginning of +// transaction processing. +func (t *gojaTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +// CaptureTxStart implements the Tracer interface and is invoked at the end of +// transaction processing. +func (t *gojaTracer) CaptureTxEnd(restGas uint64) {} + +// CaptureStart implements the Tracer interface to initialize the tracing operation. +func (t *gojaTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + db := &dbObj{db: env.StateDB, vm: t.vm, toBig: t.toBig, toBuf: t.toBuf, fromBuf: t.fromBuf} + t.dbValue = db.setupObject() + if create { + t.ctx["type"] = t.vm.ToValue("CREATE") + } else { + t.ctx["type"] = t.vm.ToValue("CALL") + } + t.ctx["from"] = t.vm.ToValue(from.Bytes()) + t.ctx["to"] = t.vm.ToValue(to.Bytes()) + t.ctx["input"] = t.vm.ToValue(input) + t.ctx["gas"] = t.vm.ToValue(gas) + t.ctx["gasPrice"] = t.vm.ToValue(env.TxContext.GasPrice) + valueBig, err := t.toBig(t.vm, value.String()) + if err != nil { + t.err = err + return + } + t.ctx["value"] = valueBig + t.ctx["block"] = t.vm.ToValue(env.Context.BlockNumber.Uint64()) + // Update list of precompiles based on current block + rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil) + t.activePrecompiles = vm.ActivePrecompiles(rules) + t.ctx["intrinsicGas"] = t.vm.ToValue(t.gasLimit - gas) +} + +// CaptureState implements the Tracer interface to trace a single step of VM execution. +func (t *gojaTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + if !t.traceStep { + return + } + if t.err != nil { + return + } + + log := t.log + log.op.op = op + log.memory.w.memory = scope.Memory + log.stack.w.stack = scope.Stack + log.contract.contract = scope.Contract + log.pc = uint(pc) + log.gas = uint(gas) + log.cost = uint(cost) + log.depth = uint(depth) + log.err = err + if _, err := t.step(t.obj, t.logValue, t.dbValue); err != nil { + t.err = wrapError("step", err) + } +} + +// CaptureFault implements the Tracer interface to trace an execution fault +func (t *gojaTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + if t.err != nil { + return + } + // Other log fields have been already set as part of the last CaptureState. + t.log.err = err + if _, err := t.fault(t.obj, t.logValue, t.dbValue); err != nil { + t.err = wrapError("fault", err) + } +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *gojaTracer) CaptureEnd(output []byte, gasUsed uint64, duration time.Duration, err error) { + t.ctx["output"] = t.vm.ToValue(output) + t.ctx["time"] = t.vm.ToValue(duration.String()) + t.ctx["gasUsed"] = t.vm.ToValue(gasUsed) + if err != nil { + t.ctx["error"] = t.vm.ToValue(err.Error()) + } +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *gojaTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if !t.traceFrame { + return + } + if t.err != nil { + return + } + + t.frame.typ = typ.String() + t.frame.from = from + t.frame.to = to + t.frame.input = common.CopyBytes(input) + t.frame.gas = uint(gas) + t.frame.value = nil + if value != nil { + t.frame.value = new(big.Int).SetBytes(value.Bytes()) + } + + if _, err := t.enter(t.obj, t.frameValue); err != nil { + t.err = wrapError("enter", err) + } +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *gojaTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + if !t.traceFrame { + return + } + + t.frameResult.gasUsed = uint(gasUsed) + t.frameResult.output = common.CopyBytes(output) + t.frameResult.err = err + + if _, err := t.exit(t.obj, t.frameResultValue); err != nil { + t.err = wrapError("exit", err) + } +} + +// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error +func (t *gojaTracer) GetResult() (json.RawMessage, error) { + ctx := t.vm.ToValue(t.ctx) + res, err := t.result(t.obj, ctx, t.dbValue) + if err != nil { + return nil, wrapError("result", err) + } + encoded, err := json.Marshal(res) + if err != nil { + return nil, err + } + return json.RawMessage(encoded), t.err +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *gojaTracer) Stop(err error) { + t.vm.Interrupt(err) + t.env.Cancel() +} + +// setBuiltinFunctions injects Go functions which are available to tracers into the environment. +// It depends on type converters having been set up. +func (t *gojaTracer) setBuiltinFunctions() { + vm := t.vm + // TODO: load console from goja-nodejs + vm.Set("toHex", func(v goja.Value) string { + b, err := t.fromBuf(vm, v, false) + if err != nil { + panic(err) + } + return hexutil.Encode(b) + }) + vm.Set("toWord", func(v goja.Value) goja.Value { + // TODO: add test with []byte len < 32 or > 32 + b, err := t.fromBuf(vm, v, true) + if err != nil { + panic(err) + } + b = common.BytesToHash(b).Bytes() + res, err := t.toBuf(vm, b) + if err != nil { + panic(err) + } + return res + }) + vm.Set("toAddress", func(v goja.Value) goja.Value { + a, err := t.fromBuf(vm, v, true) + if err != nil { + panic(err) + } + a = common.BytesToAddress(a).Bytes() + res, err := t.toBuf(vm, a) + if err != nil { + panic(err) + } + return res + }) + vm.Set("toContract", func(from goja.Value, nonce uint) goja.Value { + a, err := t.fromBuf(vm, from, true) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + b := crypto.CreateAddress(addr, uint64(nonce)).Bytes() + res, err := t.toBuf(vm, b) + if err != nil { + panic(err) + } + return res + }) + vm.Set("toContract2", func(from goja.Value, salt string, initcode goja.Value) goja.Value { + a, err := t.fromBuf(vm, from, true) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + code, err := t.fromBuf(vm, initcode, true) + if err != nil { + panic(err) + } + code = common.CopyBytes(code) + codeHash := crypto.Keccak256(code) + b := crypto.CreateAddress2(addr, common.HexToHash(salt), codeHash).Bytes() + res, err := t.toBuf(vm, b) + if err != nil { + panic(err) + } + return res + }) + vm.Set("isPrecompiled", func(v goja.Value) bool { + a, err := t.fromBuf(vm, v, true) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + for _, p := range t.activePrecompiles { + if p == addr { + return true + } + } + return false + }) + vm.Set("slice", func(slice goja.Value, start, end int) goja.Value { + b, err := t.fromBuf(vm, slice, false) + if err != nil { + panic(err) + } + if start < 0 || start > end || end > len(b) { + log.Warn("Tracer accessed out of bound memory", "available", len(b), "offset", start, "size", end-start) + } + res, err := t.toBuf(vm, b[start:end]) + if err != nil { + panic(err) + } + return res + }) +} + +// setTypeConverters sets up utilities for converting Go types into those +// suitable for JS consumption. +func (t *gojaTracer) setTypeConverters() error { + // Inject bigint logic. + // TODO: To be replaced after goja adds support for native JS bigint. + toBigCode, err := t.vm.RunProgram(bigIntProgram) + if err != nil { + return err + } + // Used to create JS bigint objects from go. + toBigFn, ok := goja.AssertFunction(toBigCode) + if !ok { + return errors.New("failed to bind bigInt func") + } + toBigWrapper := func(vm *goja.Runtime, val string) (goja.Value, error) { + return toBigFn(goja.Undefined(), vm.ToValue(val)) + } + t.toBig = toBigWrapper + // NOTE: We need this workaround to create JS buffers because + // goja doesn't at the moment expose constructors for typed arrays. + // + // Cache uint8ArrayType once to be used every time for less overhead. + uint8ArrayType := t.vm.Get("Uint8Array") + toBufWrapper := func(vm *goja.Runtime, val []byte) (goja.Value, error) { + return toBuf(vm, uint8ArrayType, val) + } + t.toBuf = toBufWrapper + fromBufWrapper := func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) { + return fromBuf(vm, uint8ArrayType, buf, allowString) + } + t.fromBuf = fromBufWrapper + return nil +} + +type opObj struct { + vm *goja.Runtime + op vm.OpCode +} + +func (o *opObj) ToNumber() int { + return int(o.op) +} + +func (o *opObj) ToString() string { + return o.op.String() +} + +func (o *opObj) IsPush() bool { + return o.op.IsPush() +} + +func (o *opObj) setupObject() *goja.Object { + obj := o.vm.NewObject() + obj.Set("toNumber", o.vm.ToValue(o.ToNumber)) + obj.Set("toString", o.vm.ToValue(o.ToString)) + obj.Set("isPush", o.vm.ToValue(o.IsPush)) + return obj +} + +type memoryObj struct { + w *memoryWrapper + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn +} + +func (mo *memoryObj) Slice(begin, end int64) goja.Value { + b := mo.w.slice(begin, end) + res, err := mo.toBuf(mo.vm, b) + if err != nil { + panic(err) + } + return res +} + +func (mo *memoryObj) GetUint(addr int64) goja.Value { + value := mo.w.getUint(addr) + res, err := mo.toBig(mo.vm, value.String()) + if err != nil { + panic(err) + } + return res +} + +func (m *memoryObj) setupObject() *goja.Object { + o := m.vm.NewObject() + o.Set("slice", m.vm.ToValue(m.Slice)) + o.Set("getUint", m.vm.ToValue(m.GetUint)) + return o +} + +type stackObj struct { + w *stackWrapper + vm *goja.Runtime + toBig toBigFn +} + +func (s *stackObj) Peek(idx int) goja.Value { + value := s.w.peek(idx) + res, err := s.toBig(s.vm, value.String()) + if err != nil { + panic(err) + } + return res +} + +func (s *stackObj) Length() int { + return len(s.w.stack.Data()) +} + +func (s *stackObj) setupObject() *goja.Object { + o := s.vm.NewObject() + o.Set("peek", s.vm.ToValue(s.Peek)) + o.Set("length", s.vm.ToValue(s.Length)) + return o +} + +type dbObj struct { + db vm.StateDB + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + fromBuf fromBufFn +} + +func (do *dbObj) GetBalance(addrSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + value := do.db.GetBalance(addr) + res, err := do.toBig(do.vm, value.String()) + if err != nil { + panic(err) + } + return res +} + +func (do *dbObj) GetNonce(addrSlice goja.Value) uint64 { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + return do.db.GetNonce(addr) +} + +func (do *dbObj) GetCode(addrSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + code := do.db.GetCode(addr) + res, err := do.toBuf(do.vm, code) + if err != nil { + panic(err) + } + return res +} + +func (do *dbObj) GetState(addrSlice goja.Value, hashSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + h, err := do.fromBuf(do.vm, hashSlice, false) + if err != nil { + panic(err) + } + hash := common.BytesToHash(h) + state := do.db.GetState(addr, hash).Bytes() + res, err := do.toBuf(do.vm, state) + if err != nil { + panic(err) + } + return res +} + +func (do *dbObj) Exists(addrSlice goja.Value) bool { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + return do.db.Exist(addr) +} + +func (do *dbObj) setupObject() *goja.Object { + o := do.vm.NewObject() + o.Set("getBalance", do.vm.ToValue(do.GetBalance)) + o.Set("getNonce", do.vm.ToValue(do.GetNonce)) + o.Set("getCode", do.vm.ToValue(do.GetCode)) + o.Set("getState", do.vm.ToValue(do.GetState)) + o.Set("exists", do.vm.ToValue(do.Exists)) + return o +} + +type contractObj struct { + contract *vm.Contract + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn +} + +func (co *contractObj) GetCaller() goja.Value { + caller := co.contract.Caller().Bytes() + res, err := co.toBuf(co.vm, caller) + if err != nil { + panic(err) + } + return res +} + +func (co *contractObj) GetAddress() goja.Value { + addr := co.contract.Address().Bytes() + res, err := co.toBuf(co.vm, addr) + if err != nil { + panic(err) + } + return res +} + +func (co *contractObj) GetValue() goja.Value { + value := co.contract.Value() + res, err := co.toBig(co.vm, value.String()) + if err != nil { + panic(err) + } + return res +} + +func (co *contractObj) GetInput() goja.Value { + input := co.contract.Input + res, err := co.toBuf(co.vm, input) + if err != nil { + panic(err) + } + return res +} + +func (c *contractObj) setupObject() *goja.Object { + o := c.vm.NewObject() + o.Set("getCaller", c.vm.ToValue(c.GetCaller)) + o.Set("getAddress", c.vm.ToValue(c.GetAddress)) + o.Set("getValue", c.vm.ToValue(c.GetValue)) + o.Set("getInput", c.vm.ToValue(c.GetInput)) + return o +} + +type callframe struct { + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + + typ string + from common.Address + to common.Address + input []byte + gas uint + value *big.Int +} + +func (f *callframe) GetType() string { + return f.typ +} + +func (f *callframe) GetFrom() goja.Value { + from := f.from.Bytes() + res, err := f.toBuf(f.vm, from) + if err != nil { + panic(err) + } + return res +} + +func (f *callframe) GetTo() goja.Value { + to := f.to.Bytes() + res, err := f.toBuf(f.vm, to) + if err != nil { + panic(err) + } + return res +} + +func (f *callframe) GetInput() goja.Value { + input := f.input + res, err := f.toBuf(f.vm, input) + if err != nil { + panic(err) + } + return res +} + +func (f *callframe) GetGas() uint { + return f.gas +} + +func (f *callframe) GetValue() goja.Value { + if f.value == nil { + return goja.Undefined() + } + res, err := f.toBig(f.vm, f.value.String()) + if err != nil { + panic(err) + } + return res +} + +func (f *callframe) setupObject() *goja.Object { + o := f.vm.NewObject() + o.Set("getType", f.vm.ToValue(f.GetType)) + o.Set("getFrom", f.vm.ToValue(f.GetFrom)) + o.Set("getTo", f.vm.ToValue(f.GetTo)) + o.Set("getInput", f.vm.ToValue(f.GetInput)) + o.Set("getGas", f.vm.ToValue(f.GetGas)) + o.Set("getValue", f.vm.ToValue(f.GetValue)) + return o +} + +type callframeResult struct { + vm *goja.Runtime + toBuf toBufFn + + gasUsed uint + output []byte + err error +} + +func (r *callframeResult) GetGasUsed() uint { + return r.gasUsed +} + +func (r *callframeResult) GetOutput() goja.Value { + res, err := r.toBuf(r.vm, r.output) + if err != nil { + panic(err) + } + return res +} + +func (r *callframeResult) GetError() goja.Value { + if r.err != nil { + return r.vm.ToValue(r.err.Error()) + } + return goja.Undefined() + +} + +func (r *callframeResult) setupObject() *goja.Object { + o := r.vm.NewObject() + o.Set("getGasUsed", r.vm.ToValue(r.GetGasUsed)) + o.Set("getOutput", r.vm.ToValue(r.GetOutput)) + o.Set("getError", r.vm.ToValue(r.GetError)) + return o +} + +type steplog struct { + vm *goja.Runtime + + op *opObj + memory *memoryObj + stack *stackObj + contract *contractObj + + pc uint + gas uint + cost uint + depth uint + refund uint + err error +} + +func (l *steplog) GetPC() uint { + return l.pc +} + +func (l *steplog) GetGas() uint { + return l.gas +} + +func (l *steplog) GetCost() uint { + return l.cost +} + +func (l *steplog) GetDepth() uint { + return l.depth +} + +func (l *steplog) GetRefund() uint { + return l.refund +} + +func (l *steplog) GetError() goja.Value { + if l.err != nil { + return l.vm.ToValue(l.err.Error()) + } + return goja.Undefined() +} + +func (l *steplog) setupObject() *goja.Object { + o := l.vm.NewObject() + // Setup basic fields. + o.Set("getPC", l.vm.ToValue(l.GetPC)) + o.Set("getGas", l.vm.ToValue(l.GetGas)) + o.Set("getCost", l.vm.ToValue(l.GetCost)) + o.Set("getDepth", l.vm.ToValue(l.GetDepth)) + o.Set("getRefund", l.vm.ToValue(l.GetRefund)) + o.Set("getError", l.vm.ToValue(l.GetError)) + // Setup nested objects. + o.Set("op", l.op.setupObject()) + o.Set("stack", l.stack.setupObject()) + o.Set("memory", l.memory.setupObject()) + o.Set("contract", l.contract.setupObject()) + return o +} diff --git a/eth/tracers/js/internal/tracers/tracers.go b/eth/tracers/js/internal/tracers/tracers.go index 5a416d30e55b..6547f1b08804 100644 --- a/eth/tracers/js/internal/tracers/tracers.go +++ b/eth/tracers/js/internal/tracers/tracers.go @@ -17,7 +17,43 @@ // Package tracers contains the actual JavaScript tracer assets. package tracers -import "embed" +import ( + "embed" + "io/fs" + "strings" + "unicode" +) //go:embed *.js -var FS embed.FS +var files embed.FS + +// Load reads the built-in JS tracer files embedded in the binary and +// returns a mapping of tracer name to source. +func Load() (map[string]string, error) { + var assetTracers = make(map[string]string) + err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + b, err := fs.ReadFile(files, path) + if err != nil { + return err + } + name := camel(strings.TrimSuffix(path, ".js")) + assetTracers[name] = string(b) + return nil + }) + return assetTracers, err +} + +// 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, "") +} diff --git a/eth/tracers/js/tracer.go b/eth/tracers/js/tracer.go index dd68e52bd0f3..0ba8da476fde 100644 --- a/eth/tracers/js/tracer.go +++ b/eth/tracers/js/tracer.go @@ -21,56 +21,40 @@ import ( "encoding/json" "errors" "fmt" - "io/fs" "math/big" "strings" "sync/atomic" "time" - "unicode" "unsafe" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" - tracers2 "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/eth/tracers/js/internal/tracers" + "github.com/ethereum/go-ethereum/eth/tracers" + jsassets "github.com/ethereum/go-ethereum/eth/tracers/js/internal/tracers" "github.com/ethereum/go-ethereum/log" "gopkg.in/olebedev/go-duktape.v3" ) -// 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, "") -} - -var assetTracers = make(map[string]string) - // init retrieves the JavaScript transaction tracers included in go-ethereum. func init() { - err := fs.WalkDir(tracers.FS, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - b, err := fs.ReadFile(tracers.FS, path) - if err != nil { - return err - } - name := camel(strings.TrimSuffix(path, ".js")) - assetTracers[name] = string(b) - return nil - }) + assetTracers, err := jsassets.Load() if err != nil { panic(err) } - tracers2.RegisterLookup(true, newJsTracer) + // TODO: Either disable duktape or solve conflicts between goja and duktape + tracers.RegisterLookup(false, func(name string, ctx *tracers.Context) (tracers.Tracer, error) { + if !strings.HasSuffix(name, "Duktape") { + return nil, errors.New("only suffix Duktape supported") + } + name = strings.TrimSuffix(name, "Duktape") + code, ok := assetTracers[name] + if !ok { + return nil, errors.New("only pre-built tracers supported") + } + return newJsTracer(code, ctx) + }) } // makeSlice convert an unsafe memory pointer with the given type into a Go byte @@ -439,12 +423,9 @@ type jsTracer 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 newJsTracer(code string, ctx *tracers2.Context) (tracers2.Tracer, error) { - if c, ok := assetTracers[code]; ok { - code = c - } +func newJsTracer(code string, ctx *tracers.Context) (tracers.Tracer, error) { if ctx == nil { - ctx = new(tracers2.Context) + ctx = new(tracers.Context) } tracer := &jsTracer{ vm: duktape.New(), diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 9f4d6ddd4d51..982cf7b3713c 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "errors" "math/big" + "strings" "testing" "time" @@ -81,10 +82,20 @@ func runTrace(tracer tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCon return tracer.GetResult() } -func TestTracer(t *testing.T) { +type tracerCtor = func(string, *tracers.Context) (tracers.Tracer, error) + +func TestDuktapeTracer(t *testing.T) { + testTracer(t, newJsTracer) +} + +func TestGojaTracer(t *testing.T) { + testTracer(t, newGojaTracer) +} + +func testTracer(t *testing.T, newTracer tracerCtor) { execTracer := func(code string) ([]byte, string) { t.Helper() - tracer, err := newJsTracer(code, nil) + tracer, err := newTracer(code, nil) if err != nil { t.Fatal(err) } @@ -120,9 +131,18 @@ func TestTracer(t *testing.T) { }, { // tests intrinsic gas code: "{depths: [], step: function() {}, fault: function() {}, result: function(ctx) { return ctx.gasPrice+'.'+ctx.gasUsed+'.'+ctx.intrinsicGas; }}", want: `"100000.6.21000"`, - }, { // tests too deep object / serialization crash - code: "{step: function() {}, fault: function() {}, result: function() { var o={}; var x=o; for (var i=0; i<1000; i++){ o.foo={}; o=o.foo; } return x; }}", - fail: "RangeError: json encode recursion limit in server-side tracer function 'result'", + }, { + code: "{res: null, step: function(log) {}, fault: function() {}, result: function() { return toWord('0xffaa') }}", + want: `{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":255,"31":170}`, + }, { // test feeding a buffer back into go + code: "{res: null, step: function(log) { var address = log.contract.getAddress(); this.res = toAddress(address); }, fault: function() {}, result: function() { return this.res }}", + want: `{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0}`, + }, { + code: "{res: null, step: function(log) { var address = '0x0000000000000000000000000000000000000000'; this.res = toAddress(address); }, fault: function() {}, result: function() { return this.res }}", + want: `{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0}`, + }, { + code: "{res: null, step: function(log) { var address = Array.prototype.slice.call(log.contract.getAddress()); this.res = toAddress(address); }, fault: function() {}, result: function() { return this.res }}", + want: `{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0}`, }, } { if have, err := execTracer(tt.code); tt.want != string(have) || tt.fail != err { @@ -131,10 +151,18 @@ func TestTracer(t *testing.T) { } } -func TestHalt(t *testing.T) { +func TestHaltDuktape(t *testing.T) { t.Skip("duktape doesn't support abortion") + testHalt(t, newJsTracer) +} + +func TestHaltGoja(t *testing.T) { + testHalt(t, newGojaTracer) +} + +func testHalt(t *testing.T, newTracer tracerCtor) { timeout := errors.New("stahp") - tracer, err := newJsTracer("{step: function() { while(1); }, result: function() { return null; }, fault: function(){}}", nil) + tracer, err := newTracer("{step: function() { while(1); }, result: function() { return null; }, fault: function(){}}", nil) if err != nil { t.Fatal(err) } @@ -142,13 +170,21 @@ func TestHalt(t *testing.T) { time.Sleep(1 * time.Second) tracer.Stop(timeout) }() - if _, err = runTrace(tracer, testCtx(), params.TestChainConfig); err.Error() != "stahp in server-side tracer function 'step'" { + if _, err = runTrace(tracer, testCtx(), params.TestChainConfig); !strings.Contains(err.Error(), "stahp") { t.Errorf("Expected timeout error, got %v", err) } } -func TestHaltBetweenSteps(t *testing.T) { - tracer, err := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }}", nil) +func TestHaltBetweenStepsDuktape(t *testing.T) { + testHaltBetweenSteps(t, newJsTracer) +} + +func TestHaltBetweenStepsGoja(t *testing.T) { + testHaltBetweenSteps(t, newGojaTracer) +} + +func testHaltBetweenSteps(t *testing.T, newTracer tracerCtor) { + tracer, err := newTracer("{step: function() {}, fault: function() {}, result: function() { return null; }}", nil) if err != nil { t.Fatal(err) } @@ -162,17 +198,25 @@ func TestHaltBetweenSteps(t *testing.T) { tracer.Stop(timeout) tracer.CaptureState(0, 0, 0, 0, scope, nil, 0, nil) - if _, err := tracer.GetResult(); err.Error() != timeout.Error() { + if _, err := tracer.GetResult(); !strings.Contains(err.Error(), timeout.Error()) { t.Errorf("Expected timeout error, got %v", err) } } -// TestNoStepExec tests a regular value transfer (no exec), and accessing the statedb +func TestNoStepExecDuktape(t *testing.T) { + testNoStepExec(t, newJsTracer) +} + +func TestNoStepExecGoja(t *testing.T) { + testNoStepExec(t, newGojaTracer) +} + +// testNoStepExec tests a regular value transfer (no exec), and accessing the statedb // in 'result' -func TestNoStepExec(t *testing.T) { +func testNoStepExec(t *testing.T, newTracer tracerCtor) { execTracer := func(code string) []byte { t.Helper() - tracer, err := newJsTracer(code, nil) + tracer, err := newTracer(code, nil) if err != nil { t.Fatal(err) } @@ -200,13 +244,21 @@ func TestNoStepExec(t *testing.T) { } } -func TestIsPrecompile(t *testing.T) { +func TestIsPrecompileDuktape(t *testing.T) { + testIsPrecompile(t, newJsTracer) +} + +func TestIsPrecompileGoja(t *testing.T) { + testIsPrecompile(t, newGojaTracer) +} + +func testIsPrecompile(t *testing.T, newTracer tracerCtor) { chaincfg := ¶ms.ChainConfig{ChainID: big.NewInt(1), HomesteadBlock: big.NewInt(0), DAOForkBlock: nil, DAOForkSupport: false, EIP150Block: big.NewInt(0), EIP150Hash: common.Hash{}, EIP155Block: big.NewInt(0), EIP158Block: big.NewInt(0), ByzantiumBlock: big.NewInt(100), ConstantinopleBlock: big.NewInt(0), PetersburgBlock: big.NewInt(0), IstanbulBlock: big.NewInt(200), MuirGlacierBlock: big.NewInt(0), BerlinBlock: big.NewInt(300), LondonBlock: big.NewInt(0), TerminalTotalDifficulty: nil, Ethash: new(params.EthashConfig), Clique: nil} chaincfg.ByzantiumBlock = big.NewInt(100) chaincfg.IstanbulBlock = big.NewInt(200) chaincfg.BerlinBlock = big.NewInt(300) txCtx := vm.TxContext{GasPrice: big.NewInt(100000)} - tracer, err := newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, err := newTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) if err != nil { t.Fatal(err) } @@ -220,7 +272,7 @@ func TestIsPrecompile(t *testing.T) { t.Errorf("Tracer should not consider blake2f as precompile in byzantium") } - tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, _ = newTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) blockCtx = vm.BlockContext{BlockNumber: big.NewInt(250)} res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { @@ -231,16 +283,24 @@ func TestIsPrecompile(t *testing.T) { } } -func TestEnterExit(t *testing.T) { +func TestEnterExitDuktape(t *testing.T) { + testEnterExit(t, newJsTracer) +} + +func TestEnterExitGoja(t *testing.T) { + testEnterExit(t, newGojaTracer) +} + +func testEnterExit(t *testing.T, newTracer tracerCtor) { // test that either both or none of enter() and exit() are defined - if _, err := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}}", new(tracers.Context)); err == nil { + if _, err := newTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}}", new(tracers.Context)); err == nil { t.Fatal("tracer creation should've failed without exit() definition") } - if _, err := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}, exit: function() {}}", new(tracers.Context)); err != nil { + if _, err := newTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}, exit: function() {}}", new(tracers.Context)); err != nil { t.Fatal(err) } // test that the enter and exit method are correctly invoked and the values passed - tracer, err := newJsTracer("{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, step: function() {}, fault: function() {}, result: function() { return {enters: this.enters, exits: this.exits, enterGas: this.enterGas, gasUsed: this.gasUsed} }, enter: function(frame) { this.enters++; this.enterGas = frame.getGas(); }, exit: function(res) { this.exits++; this.gasUsed = res.getGasUsed(); }}", new(tracers.Context)) + tracer, err := newTracer("{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, step: function() {}, fault: function() {}, result: function() { return {enters: this.enters, exits: this.exits, enterGas: this.enterGas, gasUsed: this.gasUsed} }, enter: function(frame) { this.enters++; this.enterGas = frame.getGas(); }, exit: function(res) { this.exits++; this.gasUsed = res.getGasUsed(); }}", new(tracers.Context)) if err != nil { t.Fatal(err) } @@ -259,3 +319,20 @@ func TestEnterExit(t *testing.T) { t.Errorf("Number of invocations of enter() and exit() is wrong. Have %s, want %s\n", have, want) } } + +// Tests too deep object / serialization crash for duktape +func TestRecursionLimit(t *testing.T) { + code := "{step: function() {}, fault: function() {}, result: function() { var o={}; var x=o; for (var i=0; i<1000; i++){ o.foo={}; o=o.foo; } return x; }}" + fail := "RangeError: json encode recursion limit in server-side tracer function 'result'" + tracer, err := newJsTracer(code, nil) + if err != nil { + t.Fatal(err) + } + got := "" + if _, err := runTrace(tracer, testCtx(), params.TestChainConfig); err != nil { + got = err.Error() + } + if got != fail { + t.Errorf("expected error to be '%s' got '%s'\n", fail, got) + } +} diff --git a/go.mod b/go.mod index 71376f224fa6..d9a4557feeb1 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/deckarep/golang-set v1.8.0 github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf - github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48 + github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf github.com/edsrzf/mmap-go v1.0.0 github.com/fatih/color v1.7.0 github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c diff --git a/go.sum b/go.sum index 3cdd4c6bf60a..d0ca73cad35b 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf h1:sh8rkQZavChcmak github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48 h1:iZOop7pqsg+56twTopWgwCGxdB5SI2yDO8Ti7eTRliQ= github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf h1:Yt+4K30SdjOkRoRRm3vYNQgR+/ZIy0RmeUDZo7Y8zeQ= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=