From 04d9a7a8ccdf74825d4b313092487a5995344d80 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Fri, 15 Oct 2021 13:12:35 +0200 Subject: [PATCH 1/2] eth/tracers: add goja tracer engine eth/tracers: pass op to step eth/tracers: uncap field method names eth/tracers: fill in op methods eth/tracers: inject biginteger logic eth/tracers: push bigInt as argument eth/tracers: add memory wrapper for goja eth/tracers: add stack wrapper for goja eth/tracers: add db wrapper for goja eth/tracers: add contract wrapper for goja eth/tracers: add log field accessors eth/tracers: initial impl CaptureState and GetResult eth/tracers: impl CaptureStart & CaptureEnd eth/tracers: fixes eth/tracers: callTracerLegacy tests pass for goja eth/tracers: avoid ToValue on every step eth/tracers: avoid Export on every step eth/tracers: use goja.Object for log eth/tracers: use nested log fields via goja.Objects eth/tracers: fill-in missing methods eth/tracers: pre-compile bigIntegerJS eth/tracers: mv goja files to js dir eth/tracers: minor fixes goja: add stop method goja: register tracer, re-enable callTracer test impl CaptureFault improve err handling impl CaptureEnter impl CaptureExit Add test for call frame tracer goja Dont trace step when method not exposed Add license to goja files Fix nil frame value minor rename Impl some go funcs exposed to js env Make goja ctor private, add stack length method comment out some goja tests Test goja via duktape test cases add tx hooks update goja set goja as default for js tracers minor fix convert byte arrays to js buffer rewrite toBuffer wrap toBig calls drop unused fn set up builtins in a separate func set up converters in separate func refactor objects passed into js more refactor move aux types down in file move goja tracer struct private minor cleanups mv db objects setup fix buffer returned value from builtins explicit gas limit for runtime jstracer test drop deprecated test test recursion limit only for duktape drop goja test file minor fix console PrettyError test use goja interrupt minor fix halt test cases fix buf imported into go fix create2 addr --- console/console_test.go | 2 +- core/vm/runtime/runtime_test.go | 15 +- .../internal/tracetest/calltrace_test.go | 6 +- eth/tracers/js/goja.go | 840 ++++++++++++++++++ eth/tracers/js/internal/tracers/tracers.go | 40 +- eth/tracers/js/tracer.go | 53 +- eth/tracers/js/tracer_test.go | 116 ++- go.mod | 2 +- go.sum | 2 + 9 files changed, 1007 insertions(+), 69 deletions(-) create mode 100644 eth/tracers/js/goja.go 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 97673b490636..fcaa10f1c62c 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -752,7 +752,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 @@ -768,7 +768,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 @@ -781,7 +781,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 @@ -794,7 +794,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 @@ -806,7 +806,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 @@ -818,7 +818,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 @@ -859,7 +859,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 cf7c1e6c0d0e..11989b7a8f5c 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 := ioutil.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..0165f493398b --- /dev/null +++ b/eth/tracers/js/goja.go @@ -0,0 +1,840 @@ +// 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) + +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, buf goja.Value, allowString bool) ([]byte, error) { + switch exported := buf.Export().(type) { + case string: + if allowString { + return common.FromHex(exported), nil + } + return nil, fmt.Errorf("invalid argument") + case []byte: + return exported, nil + case map[string]interface{}: + // Uint8Array is parsed as map[string]interface{} by goja + // TODO: how to make sure we're not confusing it with a normal map + var b []byte + if err := vm.ExportTo(buf, &b); err != nil { + return nil, err + } + return b, nil + default: + return nil, fmt.Errorf("invalid argument") + } +} + +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 + 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} + 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 := 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 := 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 := 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 := 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 := fromBuf(vm, from, true) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + code, err := 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 := 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 := 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 + 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 +} + +func (do *dbObj) GetBalance(addrSlice goja.Value) goja.Value { + a, err := 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 := 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 := 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 := fromBuf(do.vm, addrSlice, false) + if err != nil { + panic(err) + } + addr := common.BytesToAddress(a) + h, err := 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 := 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..87a7f78509f1 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,15 @@ 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}`, }, } { if have, err := execTracer(tt.code); tt.want != string(have) || tt.fail != err { @@ -131,10 +148,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 +167,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 +195,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 +241,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 +269,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 +280,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 +316,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 689148c9d7d9..ca626edb49b2 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 4a2951e1f5d5..3c9b37d9e1b1 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,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= From 4061601fe3d7578b54b8a6654b823df7084d11fb Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Tue, 3 May 2022 16:42:30 +0200 Subject: [PATCH 2/2] rewrite fromBuf --- eth/tracers/js/goja.go | 77 ++++++++++++++++++++--------------- eth/tracers/js/tracer_test.go | 3 ++ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/eth/tracers/js/goja.go b/eth/tracers/js/goja.go index 0165f493398b..05611a61142c 100644 --- a/eth/tracers/js/goja.go +++ b/eth/tracers/js/goja.go @@ -51,6 +51,7 @@ 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. @@ -61,26 +62,32 @@ func toBuf(vm *goja.Runtime, bufType goja.Value, val []byte) (goja.Value, error) return vm.ToValue(res), nil } -func fromBuf(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) { - switch exported := buf.Export().(type) { - case string: - if allowString { - return common.FromHex(exported), 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 } - return nil, fmt.Errorf("invalid argument") - case []byte: - return exported, nil - case map[string]interface{}: - // Uint8Array is parsed as map[string]interface{} by goja - // TODO: how to make sure we're not confusing it with a normal map var b []byte if err := vm.ExportTo(buf, &b); err != nil { return nil, err } return b, nil - default: - return nil, fmt.Errorf("invalid argument") } + return nil, fmt.Errorf("invalid buffer type") } type gojaTracer struct { @@ -88,6 +95,7 @@ type gojaTracer struct { 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 @@ -196,7 +204,7 @@ 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} + 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") @@ -333,7 +341,7 @@ func (t *gojaTracer) setBuiltinFunctions() { vm := t.vm // TODO: load console from goja-nodejs vm.Set("toHex", func(v goja.Value) string { - b, err := fromBuf(vm, v, false) + b, err := t.fromBuf(vm, v, false) if err != nil { panic(err) } @@ -341,7 +349,7 @@ func (t *gojaTracer) setBuiltinFunctions() { }) vm.Set("toWord", func(v goja.Value) goja.Value { // TODO: add test with []byte len < 32 or > 32 - b, err := fromBuf(vm, v, true) + b, err := t.fromBuf(vm, v, true) if err != nil { panic(err) } @@ -353,7 +361,7 @@ func (t *gojaTracer) setBuiltinFunctions() { return res }) vm.Set("toAddress", func(v goja.Value) goja.Value { - a, err := fromBuf(vm, v, true) + a, err := t.fromBuf(vm, v, true) if err != nil { panic(err) } @@ -365,7 +373,7 @@ func (t *gojaTracer) setBuiltinFunctions() { return res }) vm.Set("toContract", func(from goja.Value, nonce uint) goja.Value { - a, err := fromBuf(vm, from, true) + a, err := t.fromBuf(vm, from, true) if err != nil { panic(err) } @@ -378,12 +386,12 @@ func (t *gojaTracer) setBuiltinFunctions() { return res }) vm.Set("toContract2", func(from goja.Value, salt string, initcode goja.Value) goja.Value { - a, err := fromBuf(vm, from, true) + a, err := t.fromBuf(vm, from, true) if err != nil { panic(err) } addr := common.BytesToAddress(a) - code, err := fromBuf(vm, initcode, true) + code, err := t.fromBuf(vm, initcode, true) if err != nil { panic(err) } @@ -397,7 +405,7 @@ func (t *gojaTracer) setBuiltinFunctions() { return res }) vm.Set("isPrecompiled", func(v goja.Value) bool { - a, err := fromBuf(vm, v, true) + a, err := t.fromBuf(vm, v, true) if err != nil { panic(err) } @@ -410,7 +418,7 @@ func (t *gojaTracer) setBuiltinFunctions() { return false }) vm.Set("slice", func(slice goja.Value, start, end int) goja.Value { - b, err := fromBuf(vm, slice, false) + b, err := t.fromBuf(vm, slice, false) if err != nil { panic(err) } @@ -452,6 +460,10 @@ func (t *gojaTracer) setTypeConverters() 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 } @@ -539,14 +551,15 @@ func (s *stackObj) setupObject() *goja.Object { } type dbObj struct { - db vm.StateDB - vm *goja.Runtime - toBig toBigFn - toBuf toBufFn + db vm.StateDB + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + fromBuf fromBufFn } func (do *dbObj) GetBalance(addrSlice goja.Value) goja.Value { - a, err := fromBuf(do.vm, addrSlice, false) + a, err := do.fromBuf(do.vm, addrSlice, false) if err != nil { panic(err) } @@ -560,7 +573,7 @@ func (do *dbObj) GetBalance(addrSlice goja.Value) goja.Value { } func (do *dbObj) GetNonce(addrSlice goja.Value) uint64 { - a, err := fromBuf(do.vm, addrSlice, false) + a, err := do.fromBuf(do.vm, addrSlice, false) if err != nil { panic(err) } @@ -569,7 +582,7 @@ func (do *dbObj) GetNonce(addrSlice goja.Value) uint64 { } func (do *dbObj) GetCode(addrSlice goja.Value) goja.Value { - a, err := fromBuf(do.vm, addrSlice, false) + a, err := do.fromBuf(do.vm, addrSlice, false) if err != nil { panic(err) } @@ -583,12 +596,12 @@ func (do *dbObj) GetCode(addrSlice goja.Value) goja.Value { } func (do *dbObj) GetState(addrSlice goja.Value, hashSlice goja.Value) goja.Value { - a, err := fromBuf(do.vm, addrSlice, false) + a, err := do.fromBuf(do.vm, addrSlice, false) if err != nil { panic(err) } addr := common.BytesToAddress(a) - h, err := fromBuf(do.vm, hashSlice, false) + h, err := do.fromBuf(do.vm, hashSlice, false) if err != nil { panic(err) } @@ -602,7 +615,7 @@ func (do *dbObj) GetState(addrSlice goja.Value, hashSlice goja.Value) goja.Value } func (do *dbObj) Exists(addrSlice goja.Value) bool { - a, err := fromBuf(do.vm, addrSlice, false) + a, err := do.fromBuf(do.vm, addrSlice, false) if err != nil { panic(err) } diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 87a7f78509f1..982cf7b3713c 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -140,6 +140,9 @@ func testTracer(t *testing.T, newTracer tracerCtor) { }, { 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 {