From bf693228a347fc63d7bea01fef74e1cecec9522e Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi <1591639+s1na@users.noreply.github.com> Date: Wed, 18 May 2022 16:34:18 +0200 Subject: [PATCH] eth/tracers/js: goja tracer (#23773) This adds a JS tracer runtime environment based on the Goja VM. The new runtime replaces the duktape runtime, which will be removed soon. Goja is implemented in Go and is faster for cases where the Go <-> JS transition overhead dominates overall performance. It is faster because duktape is written in C, and the transition cost includes the cost of using cgo. Another reason for using Goja is that go-duktape is not maintained anymore. We expect the performace of JS tracing to be at least as good or better with this change. --- console/console_test.go | 2 +- core/vm/runtime/runtime_test.go | 15 +- .../internal/tracetest/calltrace_test.go | 6 +- eth/tracers/js/goja.go | 853 ++++++++++++++++++ eth/tracers/js/internal/tracers/tracers.go | 40 +- eth/tracers/js/tracer.go | 53 +- eth/tracers/js/tracer_test.go | 119 ++- go.mod | 2 +- go.sum | 2 + 9 files changed, 1023 insertions(+), 69 deletions(-) create mode 100644 eth/tracers/js/goja.go diff --git a/console/console_test.go b/console/console_test.go index 1330f5a86..04ba91d15 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 97673b490..fcaa10f1c 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 d4c25ee78..0f3778b1c 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 000000000..05611a611 --- /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 5a416d30e..6547f1b08 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 dd68e52bd..0ba8da476 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 9f4d6ddd4..982cf7b37 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 689148c9d..ca626edb4 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 4a2951e1f..3c9b37d9e 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=