bf693228a3
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.
339 lines
14 KiB
Go
339 lines
14 KiB
Go
// Copyright 2017 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package js
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/state"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/eth/tracers"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
)
|
|
|
|
type account struct{}
|
|
|
|
func (account) SubBalance(amount *big.Int) {}
|
|
func (account) AddBalance(amount *big.Int) {}
|
|
func (account) SetAddress(common.Address) {}
|
|
func (account) Value() *big.Int { return nil }
|
|
func (account) SetBalance(*big.Int) {}
|
|
func (account) SetNonce(uint64) {}
|
|
func (account) Balance() *big.Int { return nil }
|
|
func (account) Address() common.Address { return common.Address{} }
|
|
func (account) SetCode(common.Hash, []byte) {}
|
|
func (account) ForEachStorage(cb func(key, value common.Hash) bool) {}
|
|
|
|
type dummyStatedb struct {
|
|
state.StateDB
|
|
}
|
|
|
|
func (*dummyStatedb) GetRefund() uint64 { return 1337 }
|
|
func (*dummyStatedb) GetBalance(addr common.Address) *big.Int { return new(big.Int) }
|
|
|
|
type vmContext struct {
|
|
blockCtx vm.BlockContext
|
|
txCtx vm.TxContext
|
|
}
|
|
|
|
func testCtx() *vmContext {
|
|
return &vmContext{blockCtx: vm.BlockContext{BlockNumber: big.NewInt(1)}, txCtx: vm.TxContext{GasPrice: big.NewInt(100000)}}
|
|
}
|
|
|
|
func runTrace(tracer tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainConfig) (json.RawMessage, error) {
|
|
var (
|
|
env = vm.NewEVM(vmctx.blockCtx, vmctx.txCtx, &dummyStatedb{}, chaincfg, vm.Config{Debug: true, Tracer: tracer})
|
|
gasLimit uint64 = 31000
|
|
startGas uint64 = 10000
|
|
value = big.NewInt(0)
|
|
contract = vm.NewContract(account{}, account{}, value, startGas)
|
|
)
|
|
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x1, 0x0}
|
|
|
|
tracer.CaptureTxStart(gasLimit)
|
|
tracer.CaptureStart(env, contract.Caller(), contract.Address(), false, []byte{}, startGas, value)
|
|
ret, err := env.Interpreter().Run(contract, []byte{}, false)
|
|
tracer.CaptureEnd(ret, startGas-contract.Gas, 1, err)
|
|
// Rest gas assumes no refund
|
|
tracer.CaptureTxEnd(startGas - contract.Gas)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tracer.GetResult()
|
|
}
|
|
|
|
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 := newTracer(code, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ret, err := runTrace(tracer, testCtx(), params.TestChainConfig)
|
|
if err != nil {
|
|
return nil, err.Error() // Stringify to allow comparison without nil checks
|
|
}
|
|
return ret, ""
|
|
}
|
|
for i, tt := range []struct {
|
|
code string
|
|
want string
|
|
fail string
|
|
}{
|
|
{ // tests that we don't panic on bad arguments to memory access
|
|
code: "{depths: [], step: function(log) { this.depths.push(log.memory.slice(-1,-2)); }, fault: function() {}, result: function() { return this.depths; }}",
|
|
want: `[{},{},{}]`,
|
|
}, { // tests that we don't panic on bad arguments to stack peeks
|
|
code: "{depths: [], step: function(log) { this.depths.push(log.stack.peek(-1)); }, fault: function() {}, result: function() { return this.depths; }}",
|
|
want: `["0","0","0"]`,
|
|
}, { // tests that we don't panic on bad arguments to memory getUint
|
|
code: "{ depths: [], step: function(log, db) { this.depths.push(log.memory.getUint(-64));}, fault: function() {}, result: function() { return this.depths; }}",
|
|
want: `["0","0","0"]`,
|
|
}, { // tests some general counting
|
|
code: "{count: 0, step: function() { this.count += 1; }, fault: function() {}, result: function() { return this.count; }}",
|
|
want: `3`,
|
|
}, { // tests that depth is reported correctly
|
|
code: "{depths: [], step: function(log) { this.depths.push(log.stack.length()); }, fault: function() {}, result: function() { return this.depths; }}",
|
|
want: `[0,1,2]`,
|
|
}, { // tests to-string of opcodes
|
|
code: "{opcodes: [], step: function(log) { this.opcodes.push(log.op.toString()); }, fault: function() {}, result: function() { return this.opcodes; }}",
|
|
want: `["PUSH1","PUSH1","STOP"]`,
|
|
}, { // tests intrinsic gas
|
|
code: "{depths: [], step: function() {}, fault: function() {}, result: function(ctx) { return ctx.gasPrice+'.'+ctx.gasUsed+'.'+ctx.intrinsicGas; }}",
|
|
want: `"100000.6.21000"`,
|
|
}, {
|
|
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 {
|
|
t.Errorf("testcase %d: expected return value to be '%s' got '%s', error to be '%s' got '%s'\n\tcode: %v", i, tt.want, string(have), tt.fail, err, tt.code)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 := newTracer("{step: function() { while(1); }, result: function() { return null; }, fault: function(){}}", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
tracer.Stop(timeout)
|
|
}()
|
|
if _, err = runTrace(tracer, testCtx(), params.TestChainConfig); !strings.Contains(err.Error(), "stahp") {
|
|
t.Errorf("Expected timeout error, got %v", err)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
env := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, vm.TxContext{GasPrice: big.NewInt(1)}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: tracer})
|
|
scope := &vm.ScopeContext{
|
|
Contract: vm.NewContract(&account{}, &account{}, big.NewInt(0), 0),
|
|
}
|
|
tracer.CaptureStart(env, common.Address{}, common.Address{}, false, []byte{}, 0, big.NewInt(0))
|
|
tracer.CaptureState(0, 0, 0, 0, scope, nil, 0, nil)
|
|
timeout := errors.New("stahp")
|
|
tracer.Stop(timeout)
|
|
tracer.CaptureState(0, 0, 0, 0, scope, nil, 0, nil)
|
|
|
|
if _, err := tracer.GetResult(); !strings.Contains(err.Error(), timeout.Error()) {
|
|
t.Errorf("Expected timeout error, got %v", err)
|
|
}
|
|
}
|
|
|
|
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, newTracer tracerCtor) {
|
|
execTracer := func(code string) []byte {
|
|
t.Helper()
|
|
tracer, err := newTracer(code, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, vm.TxContext{GasPrice: big.NewInt(100)}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: tracer})
|
|
tracer.CaptureStart(env, common.Address{}, common.Address{}, false, []byte{}, 1000, big.NewInt(0))
|
|
tracer.CaptureEnd(nil, 0, 1, nil)
|
|
ret, err := tracer.GetResult()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return ret
|
|
}
|
|
for i, tt := range []struct {
|
|
code string
|
|
want string
|
|
}{
|
|
{ // tests that we don't panic on accessing the db methods
|
|
code: "{depths: [], step: function() {}, fault: function() {}, result: function(ctx, db){ return db.getBalance(ctx.to)} }",
|
|
want: `"0"`,
|
|
},
|
|
} {
|
|
if have := execTracer(tt.code); tt.want != string(have) {
|
|
t.Errorf("testcase %d: expected return value to be %s got %s\n\tcode: %v", i, tt.want, string(have), tt.code)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 := 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)
|
|
}
|
|
|
|
blockCtx := vm.BlockContext{BlockNumber: big.NewInt(150)}
|
|
res, err := runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if string(res) != "false" {
|
|
t.Errorf("Tracer should not consider blake2f as precompile in byzantium")
|
|
}
|
|
|
|
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 {
|
|
t.Error(err)
|
|
}
|
|
if string(res) != "true" {
|
|
t.Errorf("Tracer should consider blake2f as precompile in istanbul")
|
|
}
|
|
}
|
|
|
|
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 := 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 := 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 := 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)
|
|
}
|
|
scope := &vm.ScopeContext{
|
|
Contract: vm.NewContract(&account{}, &account{}, big.NewInt(0), 0),
|
|
}
|
|
tracer.CaptureEnter(vm.CALL, scope.Contract.Caller(), scope.Contract.Address(), []byte{}, 1000, new(big.Int))
|
|
tracer.CaptureExit([]byte{}, 400, nil)
|
|
|
|
have, err := tracer.GetResult()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := `{"enters":1,"exits":1,"enterGas":1000,"gasUsed":400}`
|
|
if string(have) != want {
|
|
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)
|
|
}
|
|
}
|