From 787260e501e8ea2a97e7b94a2c336c690689ab94 Mon Sep 17 00:00:00 2001 From: ryanmorphl2 <163962984+ryanmorphl2@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:54:51 +0800 Subject: [PATCH 1/5] feat(tracer): add withLog to callTracer --- core/state_transition.go | 7 + core/vm/access_list_tracer.go | 4 + core/vm/logger.go | 11 + core/vm/logger_json.go | 4 + core/vm/runtime/runtime_test.go | 6 +- eth/tracers/api.go | 18 +- .../internal/tracetest/calltrace_test.go | 6 +- eth/tracers/js/tracer.go | 6 +- eth/tracers/js/tracer_test.go | 26 +- eth/tracers/native/4byte.go | 37 +-- eth/tracers/native/call.go | 239 +++++++++++++----- eth/tracers/native/gen_account_json.go | 56 ++++ eth/tracers/native/gen_callframe_json.go | 107 ++++++++ eth/tracers/native/noop.go | 8 +- eth/tracers/native/prestate.go | 73 ++++-- eth/tracers/native/tracer.go | 50 ++-- eth/tracers/tracers.go | 6 +- rollup/tracing/mux_tracer.go | 12 + rollup/tracing/tracing.go | 2 +- 19 files changed, 501 insertions(+), 177 deletions(-) create mode 100644 eth/tracers/native/gen_account_json.go create mode 100644 eth/tracers/native/gen_callframe_json.go diff --git a/core/state_transition.go b/core/state_transition.go index 60aad1d2b..d46b6f4d2 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -355,6 +355,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { return nil, err } + if st.evm.Config.Debug { + st.evm.Config.Tracer.CaptureTxStart(st.initialGas) + defer func() { + st.evm.Config.Tracer.CaptureTxEnd(st.gas) + }() + } + var ( msg = st.msg sender = vm.AccountRef(msg.From()) diff --git a/core/vm/access_list_tracer.go b/core/vm/access_list_tracer.go index a5da0782a..b0f272e4b 100644 --- a/core/vm/access_list_tracer.go +++ b/core/vm/access_list_tracer.go @@ -175,6 +175,10 @@ func (*AccessListTracer) CaptureEnter(typ OpCode, from common.Address, to common func (*AccessListTracer) CaptureExit(output []byte, gasUsed uint64, err error) {} +func (t *AccessListTracer) CaptureTxStart(gasLimit uint64) {} + +func (t *AccessListTracer) CaptureTxEnd(restGas uint64) {} + // AccessList returns the current accesslist maintained by the tracer. func (a *AccessListTracer) AccessList() types.AccessList { return a.list.accessList() diff --git a/core/vm/logger.go b/core/vm/logger.go index f8b70538b..c587433ba 100644 --- a/core/vm/logger.go +++ b/core/vm/logger.go @@ -127,6 +127,9 @@ func (s *StructLog) ErrorString() string { // Note that reference types are actual VM data structures; make copies // if you need to retain them beyond the current call. type EVMLogger interface { + // Transaction level + CaptureTxStart(gasLimit uint64) + CaptureTxEnd(restGas uint64) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) CaptureState(pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error) CaptureStateAfter(pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error) @@ -340,6 +343,10 @@ func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { } +func (t *StructLogger) CaptureTxStart(gasLimit uint64) {} + +func (t *StructLogger) CaptureTxEnd(restGas uint64) {} + // UpdatedAccounts is used to collect all "touched" accounts func (l *StructLogger) UpdatedAccounts() map[common.Address]struct{} { return l.statesAffected @@ -487,6 +494,10 @@ func (t *mdLogger) CaptureEnter(typ OpCode, from common.Address, to common.Addre func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} +func (t *mdLogger) CaptureTxStart(gasLimit uint64) {} + +func (t *mdLogger) CaptureTxEnd(restGas uint64) {} + // FormatLogs formats EVM returned structured logs for json output func FormatLogs(logs []*StructLog) []*types.StructLogRes { formatted := make([]*types.StructLogRes, 0, len(logs)) diff --git a/core/vm/logger_json.go b/core/vm/logger_json.go index 61a3a656d..38ac2ecc6 100644 --- a/core/vm/logger_json.go +++ b/core/vm/logger_json.go @@ -98,3 +98,7 @@ func (l *JSONLogger) CaptureEnter(typ OpCode, from common.Address, to common.Add } func (l *JSONLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} + +func (t *JSONLogger) CaptureTxStart(gasLimit uint64) {} + +func (t *JSONLogger) CaptureTxEnd(restGas uint64) {} diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index d327d9d97..57ca64190 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -376,7 +376,7 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) cfg.GasLimit = gas if len(tracerCode) > 0 { - tracer, err := tracers.New(tracerCode, new(tracers.Context)) + tracer, err := tracers.New(tracerCode, new(tracers.Context), nil) if err != nil { b.Fatal(err) } @@ -877,7 +877,7 @@ func TestRuntimeJSTracer(t *testing.T) { statedb.SetCode(common.HexToAddress("0xee"), calleeCode) statedb.SetCode(common.HexToAddress("0xff"), depressedCode) - tracer, err := tracers.New(jsTracer, new(tracers.Context)) + tracer, err := tracers.New(jsTracer, new(tracers.Context), nil) if err != nil { t.Fatal(err) } @@ -912,7 +912,7 @@ func TestJSTracerCreateTx(t *testing.T) { code := []byte{byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.RETURN)} statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) - tracer, err := tracers.New(jsTracer, new(tracers.Context)) + tracer, err := tracers.New(jsTracer, new(tracers.Context), nil) if err != nil { t.Fatal(err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index e5fbf27a9..360abcbd3 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -20,6 +20,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -173,15 +174,15 @@ type TraceConfig struct { Tracer *string Timeout *string Reexec *uint64 + // Config specific to given tracer. Note struct logger + // config are historically embedded in main object. + TracerConfig json.RawMessage } // TraceCallConfig is the config for traceCall API. It holds one more // field to override the state for tracing. type TraceCallConfig struct { - *vm.LogConfig - Tracer *string - Timeout *string - Reexec *uint64 + TraceConfig StateOverrides *ethapi.StateOverride } @@ -898,12 +899,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc var traceConfig *TraceConfig if config != nil { - traceConfig = &TraceConfig{ - LogConfig: config.LogConfig, - Tracer: config.Tracer, - Timeout: config.Timeout, - Reexec: config.Reexec, - } + traceConfig = &config.TraceConfig } signer := types.MakeSigner(api.backend.ChainConfig(), block.Number()) @@ -936,7 +932,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex return nil, err } } - if t, err := New(*config.Tracer, txctx); err != nil { + if t, err := New(*config.Tracer, txctx, config.TracerConfig); err != nil { return nil, err } else { deadlineCtx, cancel := context.WithTimeout(ctx, timeout) diff --git a/eth/tracers/internal/tracetest/calltrace_test.go b/eth/tracers/internal/tracetest/calltrace_test.go index dccd5e629..8d9fb5e08 100644 --- a/eth/tracers/internal/tracetest/calltrace_test.go +++ b/eth/tracers/internal/tracetest/calltrace_test.go @@ -184,7 +184,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { } _, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) ) - tracer, err := tracers.New(tracerName, new(tracers.Context)) + tracer, err := tracers.New(tracerName, new(tracers.Context), nil) if err != nil { t.Fatalf("failed to create call tracer: %v", err) } @@ -302,7 +302,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - tracer, err := tracers.New(tracerName, new(tracers.Context)) + tracer, err := tracers.New(tracerName, new(tracers.Context), nil) if err != nil { b.Fatalf("failed to create call tracer: %v", err) } @@ -372,7 +372,7 @@ func TestZeroValueToNotExitCall(t *testing.T) { } _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) // Create the tracer, the EVM environment and run it - tracer, err := tracers.New("callTracer", nil) + tracer, err := tracers.New("callTracer", nil, nil) if err != nil { t.Fatalf("failed to create call tracer: %v", err) } diff --git a/eth/tracers/js/tracer.go b/eth/tracers/js/tracer.go index e305bf189..808852afd 100644 --- a/eth/tracers/js/tracer.go +++ b/eth/tracers/js/tracer.go @@ -425,7 +425,7 @@ 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) { +func newJsTracer(code string, ctx *tracers2.Context, cfg json.RawMessage) (tracers2.Tracer, error) { if c, ok := assetTracers[code]; ok { code = c } @@ -831,6 +831,10 @@ func (jst *jsTracer) CaptureExit(output []byte, gasUsed uint64, err error) { } } +func (t *jsTracer) CaptureTxStart(gasLimit uint64) {} + +func (t *jsTracer) CaptureTxEnd(restGas uint64) {} + // GetResult calls the Javascript 'result' function and returns its value, or any accumulated error func (jst *jsTracer) GetResult() (json.RawMessage, error) { // Transform the context into a JavaScript object and inject into the state diff --git a/eth/tracers/js/tracer_test.go b/eth/tracers/js/tracer_test.go index 571512d01..aeded0d34 100644 --- a/eth/tracers/js/tracer_test.go +++ b/eth/tracers/js/tracer_test.go @@ -80,7 +80,7 @@ func runTrace(tracer tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCon func TestTracer(t *testing.T) { execTracer := func(code string) ([]byte, string) { t.Helper() - tracer, err := newJsTracer(code, nil) + tracer, err := newJsTracer(code, nil, nil) if err != nil { t.Fatal(err) } @@ -130,7 +130,7 @@ func TestTracer(t *testing.T) { func TestHalt(t *testing.T) { t.Skip("duktape doesn't support abortion") timeout := errors.New("stahp") - tracer, err := newJsTracer("{step: function() { while(1); }, result: function() { return null; }, fault: function(){}}", nil) + tracer, err := newJsTracer("{step: function() { while(1); }, result: function() { return null; }, fault: function(){}}", nil, nil) if err != nil { t.Fatal(err) } @@ -144,7 +144,7 @@ func TestHalt(t *testing.T) { } func TestHaltBetweenSteps(t *testing.T) { - tracer, err := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }}", nil) + tracer, err := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }}", nil, nil) if err != nil { t.Fatal(err) } @@ -168,7 +168,7 @@ func TestHaltBetweenSteps(t *testing.T) { func TestNoStepExec(t *testing.T) { execTracer := func(code string) []byte { t.Helper() - tracer, err := newJsTracer(code, nil) + tracer, err := newJsTracer(code, nil, nil) if err != nil { t.Fatal(err) } @@ -203,7 +203,7 @@ func TestIsPrecompile(t *testing.T) { chaincfg.BerlinBlock = big.NewInt(300) chaincfg.ArchimedesBlock = big.NewInt(400) 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 := newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) if err != nil { t.Fatal(err) } @@ -217,7 +217,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, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) blockCtx = vm.BlockContext{BlockNumber: big.NewInt(250)} res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { @@ -228,7 +228,7 @@ func TestIsPrecompile(t *testing.T) { } // test sha disabled in archimedes - tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000002'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000002'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) blockCtx = vm.BlockContext{BlockNumber: big.NewInt(450)} res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { @@ -238,7 +238,7 @@ func TestIsPrecompile(t *testing.T) { t.Errorf("Tracer should not consider blake2f as precompile in archimedes") } - tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000003'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000003'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) blockCtx = vm.BlockContext{BlockNumber: big.NewInt(450)} res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { @@ -249,7 +249,7 @@ func TestIsPrecompile(t *testing.T) { } // test blake2f disabled in archimedes - tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000009'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { t.Error(err) @@ -259,7 +259,7 @@ func TestIsPrecompile(t *testing.T) { } // test ecrecover enabled in archimedes - tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000001'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil) + tracer, _ = newJsTracer("{addr: toAddress('0000000000000000000000000000000000000001'), res: null, step: function() { this.res = isPrecompiled(this.addr); }, fault: function() {}, result: function() { return this.res; }}", nil, nil) res, err = runTrace(tracer, &vmContext{blockCtx, txCtx}, chaincfg) if err != nil { t.Error(err) @@ -271,14 +271,14 @@ func TestIsPrecompile(t *testing.T) { func TestEnterExit(t *testing.T) { // 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 := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}}", new(tracers.Context), nil); 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 := newJsTracer("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}, exit: function() {}}", new(tracers.Context), nil); 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 := 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), nil) if err != nil { t.Fatal(err) } diff --git a/eth/tracers/native/4byte.go b/eth/tracers/native/4byte.go index a30fdf753..d184f1980 100644 --- a/eth/tracers/native/4byte.go +++ b/eth/tracers/native/4byte.go @@ -21,7 +21,6 @@ import ( "math/big" "strconv" "sync/atomic" - "time" "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/core/vm" @@ -47,20 +46,20 @@ func init() { // 0xc281d19e-0: 1 // } type fourByteTracer struct { - env *vm.EVM + noopTracer ids map[string]int // ids aggregates the 4byte ids found - interrupt uint32 // Atomic flag to signal execution interruption + interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption activePrecompiles []common.Address // Updated on CaptureStart based on given rules } // newFourByteTracer returns a native go tracer which collects // 4 byte-identifiers of a tx, and implements vm.EVMLogger. -func newFourByteTracer() tracers.Tracer { +func newFourByteTracer(ctx *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { t := &fourByteTracer{ ids: make(map[string]int), } - return t + return t, nil } // isPrecompiled returns whether the addr is a precompile. Logic borrowed from newJsTracer in eth/tracers/js/tracer.go @@ -81,8 +80,6 @@ func (t *fourByteTracer) store(id []byte, size int) { // CaptureStart implements the EVMLogger interface to initialize the tracing operation. func (t *fourByteTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - t.env = env - // Update list of precompiles based on current block rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Time.Uint64()) t.activePrecompiles = vm.ActivePrecompiles(rules) @@ -93,19 +90,10 @@ func (t *fourByteTracer) CaptureStart(env *vm.EVM, from common.Address, to commo } } -// CaptureState implements the EVMLogger interface to trace a single step of VM execution. -func (t *fourByteTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { -} - -// CaptureStateAfter for special needs, tracks SSTORE ops and records the storage change. -func (t *fourByteTracer) CaptureStateAfter(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { -} - // CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). func (t *fourByteTracer) CaptureEnter(op vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { // Skip if tracing was interrupted - if atomic.LoadUint32(&t.interrupt) > 0 { - t.env.Cancel() + if t.interrupt.Load() { return } if len(input) < 4 { @@ -123,19 +111,6 @@ func (t *fourByteTracer) CaptureEnter(op vm.OpCode, from common.Address, to comm t.store(input[0:4], len(input)-4) } -// CaptureExit is called when EVM exits a scope, even if the scope didn't -// execute any code. -func (t *fourByteTracer) CaptureExit(output []byte, gasUsed uint64, err error) { -} - -// CaptureFault implements the EVMLogger interface to trace an execution fault. -func (t *fourByteTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { -} - -// CaptureEnd is called after the call finishes to finalize the tracing. -func (t *fourByteTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { -} - // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *fourByteTracer) GetResult() (json.RawMessage, error) { @@ -149,5 +124,5 @@ func (t *fourByteTracer) GetResult() (json.RawMessage, error) { // Stop terminates execution of the tracer at the first opportune moment. func (t *fourByteTracer) Stop(err error) { t.reason = err - atomic.StoreUint32(&t.interrupt, 1) + t.interrupt.Store(true) } diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 9fe34f524..ff480bdcd 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -25,100 +25,203 @@ import ( "sync/atomic" "time" + "github.com/morph-l2/go-ethereum/accounts/abi" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" "github.com/morph-l2/go-ethereum/core/vm" "github.com/morph-l2/go-ethereum/eth/tracers" + "github.com/morph-l2/go-ethereum/log" ) +//go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go + func init() { register("callTracer", newCallTracer) } +type callLog struct { + Address common.Address `json:"address"` + Topics []common.Hash `json:"topics"` + Data hexutil.Bytes `json:"data"` + // Position of the log relative to subcalls within the same trace + // See https://github.com/ethereum/go-ethereum/pull/28389 for details + Position hexutil.Uint `json:"position"` +} + type callFrame struct { - Type string `json:"type"` - From string `json:"from"` - To string `json:"to,omitempty"` - Value string `json:"value,omitempty"` - Gas string `json:"gas"` - GasUsed string `json:"gasUsed"` - Input string `json:"input"` - Output string `json:"output,omitempty"` - Error string `json:"error,omitempty"` - Calls []callFrame `json:"calls,omitempty"` + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas uint64 `json:"gas"` + GasUsed uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input []byte `json:"input" rlp:"optional"` + Output []byte `json:"output,omitempty" rlp:"optional"` + Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` + Calls []callFrame `json:"calls,omitempty" rlp:"optional"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + // Placed at end on purpose. The RLP will be decoded to 0 instead of + // nil if there are non-empty elements after in the struct. + Value *big.Int `json:"value,omitempty" rlp:"optional"` +} + +func (f callFrame) TypeString() string { + return f.Type.String() +} + +func (f callFrame) failed() bool { + return len(f.Error) > 0 +} + +func (f *callFrame) processOutput(output []byte, err error) { + output = common.CopyBytes(output) + if err == nil { + f.Output = output + return + } + f.Error = err.Error() + if f.Type == vm.CREATE || f.Type == vm.CREATE2 { + f.To = nil + } + if !errors.Is(err, vm.ErrExecutionReverted) || len(output) == 0 { + return + } + f.Output = output + if len(output) < 4 { + return + } + if unpacked, err := abi.UnpackRevert(output); err == nil { + f.RevertReason = unpacked + } +} + +type callFrameMarshaling struct { + TypeString string `json:"type"` + Gas hexutil.Uint64 + GasUsed hexutil.Uint64 + Value *hexutil.Big + Input hexutil.Bytes + Output hexutil.Bytes } type callTracer struct { - env *vm.EVM + noopTracer callstack []callFrame - interrupt uint32 // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption + config callTracerConfig + gasLimit uint64 + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +type callTracerConfig struct { + OnlyTopCall bool `json:"onlyTopCall"` // If true, call tracer won't collect any subcalls + WithLog bool `json:"withLog"` // If true, call tracer will collect event logs } // newCallTracer returns a native go tracer which tracks // call frames of a tx, and implements vm.EVMLogger. -func newCallTracer() tracers.Tracer { +func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + var config callTracerConfig + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + } // First callframe contains tx context info // and is populated on start and end. - t := &callTracer{callstack: make([]callFrame, 1)} - return t + return &callTracer{callstack: make([]callFrame, 1), config: config}, nil } // CaptureStart implements the EVMLogger interface to initialize the tracing operation. func (t *callTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - t.env = env + toCopy := to t.callstack[0] = callFrame{ - Type: "CALL", - From: addrToHex(from), - To: addrToHex(to), - Input: bytesToHex(input), - Gas: uintToHex(gas), - Value: bigToHex(value), + Type: vm.CALL, + From: from, + To: &toCopy, + Input: common.CopyBytes(input), + Gas: t.gasLimit, + Value: value, } if create { - t.callstack[0].Type = "CREATE" + t.callstack[0].Type = vm.CREATE } } // CaptureEnd is called after the call finishes to finalize the tracing. func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { - t.callstack[0].GasUsed = uintToHex(gasUsed) - if err != nil { - t.callstack[0].Error = err.Error() - if err.Error() == "execution reverted" && len(output) > 0 { - t.callstack[0].Output = bytesToHex(output) - } - } else { - t.callstack[0].Output = bytesToHex(output) - } + t.callstack[0].processOutput(output, err) } // CaptureState implements the EVMLogger interface to trace a single step of VM execution. func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { -} + // skip if the previous op caused an error + if err != nil { + return + } + // Only logs need to be captured via opcode processing + if !t.config.WithLog { + return + } + // Avoid processing nested calls when only caring about top call + if t.config.OnlyTopCall && depth > 1 { + return + } + // Skip if tracing was interrupted + if t.interrupt.Load() { + return + } + switch op { + case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4: + size := int(op - vm.LOG0) -// CaptureStateAfter for special needs, tracks SSTORE ops and records the storage change. -func (t *callTracer) CaptureStateAfter(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { -} + stack := scope.Stack + stackData := stack.Data() -// CaptureFault implements the EVMLogger interface to trace an execution fault. -func (t *callTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { + // Don't modify the stack + mStart := stackData[len(stackData)-1] + mSize := stackData[len(stackData)-2] + topics := make([]common.Hash, size) + for i := 0; i < size; i++ { + topic := stackData[len(stackData)-2-(i+1)] + topics[i] = common.Hash(topic.Bytes32()) + } + + data, err := tracers.GetMemoryCopyPadded(scope.Memory, int64(mStart.Uint64()), int64(mSize.Uint64())) + if err != nil { + // mSize was unrealistically large + log.Warn("failed to copy CREATE2 input", "err", err, "tracer", "callTracer", "offset", mStart, "size", mSize) + return + } + + log := callLog{ + Address: scope.Contract.Address(), + Topics: topics, + Data: hexutil.Bytes(data), + Position: hexutil.Uint(len(t.callstack[len(t.callstack)-1].Calls)), + } + t.callstack[len(t.callstack)-1].Logs = append(t.callstack[len(t.callstack)-1].Logs, log) + } } // CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if t.config.OnlyTopCall { + return + } // Skip if tracing was interrupted - if atomic.LoadUint32(&t.interrupt) > 0 { - t.env.Cancel() + if t.interrupt.Load() { return } + toCopy := to call := callFrame{ - Type: typ.String(), - From: addrToHex(from), - To: addrToHex(to), - Input: bytesToHex(input), - Gas: uintToHex(gas), - Value: bigToHex(value), + Type: typ, + From: from, + To: &toCopy, + Input: common.CopyBytes(input), + Gas: gas, + Value: value, } t.callstack = append(t.callstack, call) } @@ -126,6 +229,9 @@ func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common. // CaptureExit is called when EVM exits a scope, even if the scope didn't // execute any code. func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + if t.config.OnlyTopCall { + return + } size := len(t.callstack) if size <= 1 { return @@ -135,24 +241,30 @@ func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) { t.callstack = t.callstack[:size-1] size -= 1 - call.GasUsed = uintToHex(gasUsed) - if err == nil { - call.Output = bytesToHex(output) - } else { - call.Error = err.Error() - if call.Type == "CREATE" || call.Type == "CREATE2" { - call.To = "" - } - } + call.GasUsed = gasUsed + call.processOutput(output, err) t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call) } +func (t *callTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +func (t *callTracer) CaptureTxEnd(restGas uint64) { + t.callstack[0].GasUsed = t.gasLimit - restGas + if t.config.WithLog { + // Logs are not emitted when the call fails + clearFailedLogs(&t.callstack[0], false) + } +} + // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *callTracer) GetResult() (json.RawMessage, error) { if len(t.callstack) != 1 { return nil, errors.New("incorrect number of top-level calls") } + res, err := json.Marshal(t.callstack[0]) if err != nil { return nil, err @@ -163,7 +275,20 @@ func (t *callTracer) GetResult() (json.RawMessage, error) { // Stop terminates execution of the tracer at the first opportune moment. func (t *callTracer) Stop(err error) { t.reason = err - atomic.StoreUint32(&t.interrupt, 1) + t.interrupt.Store(true) +} + +// clearFailedLogs clears the logs of a callframe and all its children +// in case of execution failure. +func clearFailedLogs(cf *callFrame, parentFailed bool) { + failed := cf.failed() || parentFailed + // Clear own logs + if failed { + cf.Logs = nil + } + for i := range cf.Calls { + clearFailedLogs(&cf.Calls[i], failed) + } } func bytesToHex(s []byte) string { diff --git a/eth/tracers/native/gen_account_json.go b/eth/tracers/native/gen_account_json.go new file mode 100644 index 000000000..602800f55 --- /dev/null +++ b/eth/tracers/native/gen_account_json.go @@ -0,0 +1,56 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package native + +import ( + "encoding/json" + "math/big" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" +) + +var _ = (*accountMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (a account) MarshalJSON() ([]byte, error) { + type account struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code hexutil.Bytes `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + } + var enc account + enc.Balance = (*hexutil.Big)(a.Balance) + enc.Code = a.Code + enc.Nonce = a.Nonce + enc.Storage = a.Storage + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (a *account) UnmarshalJSON(input []byte) error { + type account struct { + Balance *hexutil.Big `json:"balance,omitempty"` + Code *hexutil.Bytes `json:"code,omitempty"` + Nonce *uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + } + var dec account + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Balance != nil { + a.Balance = (*big.Int)(dec.Balance) + } + if dec.Code != nil { + a.Code = *dec.Code + } + if dec.Nonce != nil { + a.Nonce = *dec.Nonce + } + if dec.Storage != nil { + a.Storage = dec.Storage + } + return nil +} diff --git a/eth/tracers/native/gen_callframe_json.go b/eth/tracers/native/gen_callframe_json.go new file mode 100644 index 000000000..0267bf6bb --- /dev/null +++ b/eth/tracers/native/gen_callframe_json.go @@ -0,0 +1,107 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package native + +import ( + "encoding/json" + "math/big" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/core/vm" +) + +var _ = (*callFrameMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (c callFrame) MarshalJSON() ([]byte, error) { + type callFrame0 struct { + Type vm.OpCode `json:"-"` + From common.Address `json:"from"` + Gas hexutil.Uint64 `json:"gas"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input hexutil.Bytes `json:"input" rlp:"optional"` + Output hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error string `json:"error,omitempty" rlp:"optional"` + RevertReason string `json:"revertReason,omitempty"` + Calls []callFrame `json:"calls,omitempty" rlp:"optional"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + TypeString string `json:"type"` + } + var enc callFrame0 + enc.Type = c.Type + enc.From = c.From + enc.Gas = hexutil.Uint64(c.Gas) + enc.GasUsed = hexutil.Uint64(c.GasUsed) + enc.To = c.To + enc.Input = c.Input + enc.Output = c.Output + enc.Error = c.Error + enc.RevertReason = c.RevertReason + enc.Calls = c.Calls + enc.Logs = c.Logs + enc.Value = (*hexutil.Big)(c.Value) + enc.TypeString = c.TypeString() + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (c *callFrame) UnmarshalJSON(input []byte) error { + type callFrame0 struct { + Type *vm.OpCode `json:"-"` + From *common.Address `json:"from"` + Gas *hexutil.Uint64 `json:"gas"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + To *common.Address `json:"to,omitempty" rlp:"optional"` + Input *hexutil.Bytes `json:"input" rlp:"optional"` + Output *hexutil.Bytes `json:"output,omitempty" rlp:"optional"` + Error *string `json:"error,omitempty" rlp:"optional"` + RevertReason *string `json:"revertReason,omitempty"` + Calls []callFrame `json:"calls,omitempty" rlp:"optional"` + Logs []callLog `json:"logs,omitempty" rlp:"optional"` + Value *hexutil.Big `json:"value,omitempty" rlp:"optional"` + } + var dec callFrame0 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Type != nil { + c.Type = *dec.Type + } + if dec.From != nil { + c.From = *dec.From + } + if dec.Gas != nil { + c.Gas = uint64(*dec.Gas) + } + if dec.GasUsed != nil { + c.GasUsed = uint64(*dec.GasUsed) + } + if dec.To != nil { + c.To = dec.To + } + if dec.Input != nil { + c.Input = *dec.Input + } + if dec.Output != nil { + c.Output = *dec.Output + } + if dec.Error != nil { + c.Error = *dec.Error + } + if dec.RevertReason != nil { + c.RevertReason = *dec.RevertReason + } + if dec.Calls != nil { + c.Calls = dec.Calls + } + if dec.Logs != nil { + c.Logs = dec.Logs + } + if dec.Value != nil { + c.Value = (*big.Int)(dec.Value) + } + return nil +} diff --git a/eth/tracers/native/noop.go b/eth/tracers/native/noop.go index 48f3d2a45..33ab891cb 100644 --- a/eth/tracers/native/noop.go +++ b/eth/tracers/native/noop.go @@ -35,8 +35,8 @@ func init() { type noopTracer struct{} // newNoopTracer returns a new noop tracer. -func newNoopTracer() tracers.Tracer { - return &noopTracer{} +func newNoopTracer(ctx *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { + return &noopTracer{}, nil } // CaptureStart implements the EVMLogger interface to initialize the tracing operation. @@ -68,6 +68,10 @@ func (t *noopTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common. func (t *noopTracer) CaptureExit(output []byte, gasUsed uint64, err error) { } +func (t *noopTracer) CaptureTxStart(gasLimit uint64) {} + +func (t *noopTracer) CaptureTxEnd(restGas uint64) {} + // GetResult returns an empty json object. func (t *noopTracer) GetResult() (json.RawMessage, error) { return json.RawMessage(`{}`), nil diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go index b3981acb2..4218f0852 100644 --- a/eth/tracers/native/prestate.go +++ b/eth/tracers/native/prestate.go @@ -8,12 +8,15 @@ import ( "time" "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" "github.com/morph-l2/go-ethereum/core/vm" "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/eth/tracers" "github.com/morph-l2/go-ethereum/log" ) +//go:generate go run github.com/fjl/gencodec -type account -field-override accountMarshaling -out gen_account_json.go + func init() { register("prestateTracer", newPrestateTracer) } @@ -21,19 +24,10 @@ func init() { type state = map[common.Address]*account type account struct { - Balance *big.Int - Code []byte - Nonce uint64 - Storage map[common.Hash]common.Hash -} - -func (a *account) marshal() accountMarshaling { - return accountMarshaling{ - Balance: bigToHex(a.Balance), - Code: bytesToHex(a.Code), - Nonce: a.Nonce, - Storage: a.Storage, - } + Balance *big.Int `json:"balance,omitempty"` + Code []byte `json:"code,omitempty"` + Nonce uint64 `json:"nonce,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` } func (a *account) exists() bool { @@ -41,10 +35,8 @@ func (a *account) exists() bool { } type accountMarshaling struct { - Balance string `json:"balance,omitempty"` - Code string `json:"code,omitempty"` - Nonce uint64 `json:"nonce,omitempty"` - Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + Balance *hexutil.Big + Code hexutil.Bytes } type prestateTracer struct { @@ -54,20 +46,32 @@ type prestateTracer struct { post state create bool to common.Address - gasLimit uint64 // Amount of gas bought for the whole tx + gasLimit uint64 // Amount of gas bought for the whole tx + config prestateTracerConfig interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption created map[common.Address]bool deleted map[common.Address]bool } -func newPrestateTracer() tracers.Tracer { +type prestateTracerConfig struct { + DiffMode bool `json:"diffMode"` // If true, this tracer will return state modifications +} + +func newPrestateTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + var config prestateTracerConfig + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + } return &prestateTracer{ pre: state{}, post: state{}, + config: config, created: make(map[common.Address]bool), deleted: make(map[common.Address]bool), - } + }, nil } // CaptureStart implements the EVMLogger interface to initialize the tracing operation. @@ -92,10 +96,18 @@ func (t *prestateTracer) CaptureStart(env *vm.EVM, from common.Address, to commo fromBal.Add(fromBal, new(big.Int).Add(value, consumedGas)) t.pre[from].Balance = fromBal t.pre[from].Nonce-- + + if create && t.config.DiffMode { + t.created[to] = true + } } // CaptureEnd is called after the call finishes to finalize the tracing. -func (t *prestateTracer) CaptureEnd(output []byte, gasUsed uint64, d time.Duration, err error) { +func (t *prestateTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { + if t.config.DiffMode { + return + } + if t.create { // Keep existing account prior to contract creation at that address if s := t.pre[t.to]; s != nil && !s.exists() { @@ -157,6 +169,10 @@ func (t *prestateTracer) CaptureTxStart(gasLimit uint64) { } func (t *prestateTracer) CaptureTxEnd(restGas uint64) { + if !t.config.DiffMode { + return + } + for addr, state := range t.pre { // The deleted account's state is pruned from `post` but kept in `pre` if _, ok := t.deleted[addr]; ok { @@ -218,15 +234,20 @@ func (t *prestateTracer) CaptureTxEnd(restGas uint64) { // GetResult returns the json-encoded nested list of call traces, and any // error arising from the encoding or forceful termination (via `Stop`). func (t *prestateTracer) GetResult() (json.RawMessage, error) { - pre := make(map[string]accountMarshaling) - for addr, state := range t.pre { - pre[addrToHex(addr)] = state.marshal() + var res []byte + var err error + if t.config.DiffMode { + res, err = json.Marshal(struct { + Post state `json:"post"` + Pre state `json:"pre"` + }{t.post, t.pre}) + } else { + res, err = json.Marshal(t.pre) } - res, err := json.Marshal(pre) if err != nil { return nil, err } - return res, t.reason + return json.RawMessage(res), t.reason } // Stop terminates execution of the tracer at the first opportune moment. diff --git a/eth/tracers/native/tracer.go b/eth/tracers/native/tracer.go index 81289bb0e..857abd2e7 100644 --- a/eth/tracers/native/tracer.go +++ b/eth/tracers/native/tracer.go @@ -14,29 +14,24 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. -/* -Package native is a collection of tracers written in go. - -In order to add a native tracer and have it compiled into the binary, a new -file needs to be added to this folder, containing an implementation of the -`eth.tracers.Tracer` interface. - -Aside from implementing the tracer, it also needs to register itself, using the -`register` method -- and this needs to be done in the package initialization. - -Example: - -```golang - - func init() { - register("noopTracerNative", newNoopTracer) - } - -``` -*/ +// Package native is a collection of tracers written in go. +// +// In order to add a native tracer and have it compiled into the binary, a new +// file needs to be added to this folder, containing an implementation of the +// `eth.tracers.Tracer` interface. +// +// Aside from implementing the tracer, it also needs to register itself, using the +// `register` method -- and this needs to be done in the package initialization. +// +// Example: +// +// func init() { +// register("noopTracerNative", newNoopTracer) +// } package native import ( + "encoding/json" "errors" "github.com/morph-l2/go-ethereum/eth/tracers" @@ -47,6 +42,9 @@ func init() { tracers.RegisterLookup(false, lookup) } +// ctorFn is the constructor signature of a native tracer. +type ctorFn = func(*tracers.Context, json.RawMessage) (tracers.Tracer, error) + /* ctors is a map of package-local tracer constructors. @@ -59,23 +57,23 @@ The go spec (https://golang.org/ref/spec#Package_initialization) says Hence, we cannot make the map in init, but must make it upon first use. */ -var ctors map[string]func() tracers.Tracer +var ctors map[string]ctorFn // register is used by native tracers to register their presence. -func register(name string, ctor func() tracers.Tracer) { +func register(name string, ctor ctorFn) { if ctors == nil { - ctors = make(map[string]func() tracers.Tracer) + ctors = make(map[string]ctorFn) } ctors[name] = ctor } // lookup returns a tracer, if one can be matched to the given name. -func lookup(name string, ctx *tracers.Context) (tracers.Tracer, error) { +func lookup(name string, ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { if ctors == nil { - ctors = make(map[string]func() tracers.Tracer) + ctors = make(map[string]ctorFn) } if ctor, ok := ctors[name]; ok { - return ctor(), nil + return ctor(ctx, cfg) } return nil, errors.New("no tracer found") } diff --git a/eth/tracers/tracers.go b/eth/tracers/tracers.go index de6d25b4b..d2b9a6479 100644 --- a/eth/tracers/tracers.go +++ b/eth/tracers/tracers.go @@ -43,7 +43,7 @@ type Tracer interface { Stop(err error) } -type lookupFunc func(string, *Context) (Tracer, error) +type lookupFunc func(string, *Context, json.RawMessage) (Tracer, error) var ( lookups []lookupFunc @@ -63,9 +63,9 @@ func RegisterLookup(wildcard bool, lookup lookupFunc) { // New returns a new instance of a tracer, by iterating through the // registered lookups. -func New(code string, ctx *Context) (Tracer, error) { +func New(code string, ctx *Context, cfg json.RawMessage) (Tracer, error) { for _, lookup := range lookups { - if tracer, err := lookup(code, ctx); err == nil { + if tracer, err := lookup(code, ctx, cfg); err == nil { return tracer, nil } } diff --git a/rollup/tracing/mux_tracer.go b/rollup/tracing/mux_tracer.go index 585af1c7e..612f98582 100644 --- a/rollup/tracing/mux_tracer.go +++ b/rollup/tracing/mux_tracer.go @@ -67,3 +67,15 @@ func (t *MuxTracer) CaptureEnd(output []byte, gasUsed uint64, d time.Duration, e tracer.CaptureEnd(output, gasUsed, d, err) } } + +func (t *MuxTracer) CaptureTxStart(gasLimit uint64) { + for _, tracer := range t.tracers { + tracer.CaptureTxStart(gasLimit) + } +} + +func (t *MuxTracer) CaptureTxEnd(restGas uint64) { + for _, tracer := range t.tracers { + tracer.CaptureTxEnd(restGas) + } +} diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go index 52fccfb69..c349366d7 100644 --- a/rollup/tracing/tracing.go +++ b/rollup/tracing/tracing.go @@ -310,7 +310,7 @@ func (env *TraceEnv) getTxResult(state *state.StateDB, index int, block *types.B TxIndex: index, TxHash: tx.Hash(), } - callTracer, err := tracers.New("callTracer", &tracerContext) + callTracer, err := tracers.New("callTracer", &tracerContext, nil) if err != nil { return fmt.Errorf("failed to create callTracer: %w", err) } From 185c209cc896a0f9e6c1237a9554705faa084c25 Mon Sep 17 00:00:00 2001 From: ryanmorphl2 <163962984+ryanmorphl2@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:53:41 +0800 Subject: [PATCH 2/5] feat(tracer): add tracer logger --- common/hexutil/json.go | 45 ++ common/hexutil/json_test.go | 60 +++ eth/tracers/api.go | 82 ++-- .../internal/tracers/4byte_tracer_legacy.js | 2 +- .../js/internal/tracers/call_tracer_legacy.js | 2 +- eth/tracers/logger/access_list_tracer.go | 183 +++++++ eth/tracers/logger/gen_structlog.go | 118 +++++ eth/tracers/logger/logger.go | 459 ++++++++++++++++++ eth/tracers/logger/logger_json.go | 102 ++++ eth/tracers/logger/logger_test.go | 107 ++++ 10 files changed, 1106 insertions(+), 54 deletions(-) create mode 100644 eth/tracers/logger/access_list_tracer.go create mode 100644 eth/tracers/logger/gen_structlog.go create mode 100644 eth/tracers/logger/logger.go create mode 100644 eth/tracers/logger/logger_json.go create mode 100644 eth/tracers/logger/logger_test.go diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 50db20811..e0ac98f52 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -23,6 +23,8 @@ import ( "math/big" "reflect" "strconv" + + "github.com/holiman/uint256" ) var ( @@ -30,6 +32,7 @@ var ( bigT = reflect.TypeOf((*Big)(nil)) uintT = reflect.TypeOf(Uint(0)) uint64T = reflect.TypeOf(Uint64(0)) + u256T = reflect.TypeOf((*uint256.Int)(nil)) ) // Bytes marshals/unmarshals as a JSON string with 0x prefix. @@ -225,6 +228,48 @@ func (b *Big) UnmarshalGraphQL(input interface{}) error { return err } +// U256 marshals/unmarshals as a JSON string with 0x prefix. +// The zero value marshals as "0x0". +type U256 uint256.Int + +// MarshalText implements encoding.TextMarshaler +func (b U256) MarshalText() ([]byte, error) { + u256 := (*uint256.Int)(&b) + return []byte(u256.Hex()), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *U256) UnmarshalJSON(input []byte) error { + // The uint256.Int.UnmarshalJSON method accepts "dec", "0xhex"; we must be + // more strict, hence we check string and invoke SetFromHex directly. + if !isString(input) { + return errNonString(u256T) + } + // The hex decoder needs to accept empty string ("") as '0', which uint256.Int + // would reject. + if len(input) == 2 { + (*uint256.Int)(b).Clear() + return nil + } + err := (*uint256.Int)(b).SetFromHex(string(input[1 : len(input)-1])) + if err != nil { + return &json.UnmarshalTypeError{Value: err.Error(), Type: u256T} + } + return nil +} + +// UnmarshalText implements encoding.TextUnmarshaler +func (b *U256) UnmarshalText(input []byte) error { + // The uint256.Int.UnmarshalText method accepts "dec", "0xhex"; we must be + // more strict, hence we check string and invoke SetFromHex directly. + return (*uint256.Int)(b).SetFromHex(string(input)) +} + +// String returns the hex encoding of b. +func (b *U256) String() string { + return (*uint256.Int)(b).Hex() +} + // Uint64 marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint64 uint64 diff --git a/common/hexutil/json_test.go b/common/hexutil/json_test.go index ed7d6fad1..7cca30095 100644 --- a/common/hexutil/json_test.go +++ b/common/hexutil/json_test.go @@ -23,6 +23,8 @@ import ( "errors" "math/big" "testing" + + "github.com/holiman/uint256" ) func checkError(t *testing.T, input string, got, want error) bool { @@ -176,6 +178,64 @@ func TestUnmarshalBig(t *testing.T) { } } +var unmarshalU256Tests = []unmarshalTest{ + // invalid encoding + {input: "", wantErr: errJSONEOF}, + {input: "null", wantErr: errNonString(u256T)}, + {input: "10", wantErr: errNonString(u256T)}, + {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, u256T)}, + {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, u256T)}, + {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, u256T)}, + {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, u256T)}, + {input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, u256T)}, + { + input: `"0x10000000000000000000000000000000000000000000000000000000000000000"`, + wantErr: wrapTypeError(ErrBig256Range, u256T), + }, + + // valid encoding + {input: `""`, want: big.NewInt(0)}, + {input: `"0x0"`, want: big.NewInt(0)}, + {input: `"0x2"`, want: big.NewInt(0x2)}, + {input: `"0x2F2"`, want: big.NewInt(0x2f2)}, + {input: `"0X2F2"`, want: big.NewInt(0x2f2)}, + {input: `"0x1122aaff"`, want: big.NewInt(0x1122aaff)}, + {input: `"0xbBb"`, want: big.NewInt(0xbbb)}, + {input: `"0xfffffffff"`, want: big.NewInt(0xfffffffff)}, + { + input: `"0x112233445566778899aabbccddeeff"`, + want: referenceBig("112233445566778899aabbccddeeff"), + }, + { + input: `"0xffffffffffffffffffffffffffffffffffff"`, + want: referenceBig("ffffffffffffffffffffffffffffffffffff"), + }, + { + input: `"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"`, + want: referenceBig("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + }, +} + +func TestUnmarshalU256(t *testing.T) { + for _, test := range unmarshalU256Tests { + var v U256 + err := json.Unmarshal([]byte(test.input), &v) + if !checkError(t, test.input, err, test.wantErr) { + continue + } + if test.want == nil { + continue + } + want := new(uint256.Int) + want.SetFromBig(test.want.(*big.Int)) + have := (*uint256.Int)(&v) + if want.Cmp(have) != 0 { + t.Errorf("input %s: value mismatch: have %x, want %x", test.input, have, want) + continue + } + } +} + func BenchmarkUnmarshalBig(b *testing.B) { input := []byte(`"0x123456789abcdef123456789abcdef"`) for i := 0; i < b.N; i++ { diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 360abcbd3..f02bb4dcd 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -38,6 +38,7 @@ import ( "github.com/morph-l2/go-ethereum/core/state" "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/core/vm" + "github.com/morph-l2/go-ethereum/eth/tracers/logger" "github.com/morph-l2/go-ethereum/ethdb" "github.com/morph-l2/go-ethereum/internal/ethapi" "github.com/morph-l2/go-ethereum/log" @@ -917,75 +918,52 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig, l1DataFee *big.Int) (interface{}, error) { // Assemble the structured logger or the JavaScript tracer var ( - tracer vm.EVMLogger + tracer Tracer err error + timeout = defaultTraceTimeout txContext = core.NewEVMTxContext(message) ) - switch { - case config == nil: - tracer = vm.NewStructLogger(nil) - case config.Tracer != nil: - // Define a meaningful timeout of a single transaction trace - timeout := defaultTraceTimeout - if config.Timeout != nil { - if timeout, err = time.ParseDuration(*config.Timeout); err != nil { - return nil, err - } - } - if t, err := New(*config.Tracer, txctx, config.TracerConfig); err != nil { + if config == nil { + config = &TraceConfig{} + } + // Default tracer is the struct logger + tracer = logger.NewStructLogger(config.LogConfig) + if config.Tracer != nil { + tracer, err = New(*config.Tracer, txctx, config.TracerConfig) + if err != nil { return nil, err - } else { - deadlineCtx, cancel := context.WithTimeout(ctx, timeout) - go func() { - <-deadlineCtx.Done() - if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { - t.Stop(errors.New("execution timeout")) - } - }() - defer cancel() - tracer = t } - default: - tracer = vm.NewStructLogger(config.LogConfig) } // Run the transaction with tracing enabled. vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true}) + // Define a meaningful timeout of a single transaction trace + if config.Timeout != nil { + if timeout, err = time.ParseDuration(*config.Timeout); err != nil { + return nil, err + } + } + deadlineCtx, cancel := context.WithTimeout(ctx, timeout) + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + // Stop evm execution. Note cancellation is not necessarily immediate. + vmenv.Cancel() + } + }() + defer cancel() + // If gasPrice is 0, make sure that the account has sufficient balance to cover `l1DataFee`. if message.GasPrice().Cmp(big.NewInt(0)) == 0 { statedb.AddBalance(message.From(), l1DataFee) } - // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) - - result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), l1DataFee) - if err != nil { + if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), l1DataFee); err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } - - // Depending on the tracer type, format and return the output. - switch tracer := tracer.(type) { - case *vm.StructLogger: - // If the result contains a revert reason, return it. - returnVal := fmt.Sprintf("%x", result.Return()) - if len(result.Revert()) > 0 { - returnVal = fmt.Sprintf("%x", result.Revert()) - } - return &types.ExecutionResult{ - Gas: result.UsedGas, - Failed: result.Failed(), - ReturnValue: returnVal, - StructLogs: vm.FormatLogs(tracer.StructLogs()), - L1DataFee: (*hexutil.Big)(result.L1DataFee), - }, nil - - case Tracer: - return tracer.GetResult() - - default: - panic(fmt.Sprintf("bad tracer type %T", tracer)) - } + return tracer.GetResult() } // APIs return the collection of RPC services the tracer package offers. diff --git a/eth/tracers/js/internal/tracers/4byte_tracer_legacy.js b/eth/tracers/js/internal/tracers/4byte_tracer_legacy.js index 462b4ad4c..e4714b8bf 100644 --- a/eth/tracers/js/internal/tracers/4byte_tracer_legacy.js +++ b/eth/tracers/js/internal/tracers/4byte_tracer_legacy.js @@ -46,7 +46,7 @@ return false; }, - // store save the given indentifier and datasize. + // store save the given identifier and datasize. store: function(id, size){ var key = "" + toHex(id) + "-" + size; this.ids[key] = this.ids[key] + 1 || 1; diff --git a/eth/tracers/js/internal/tracers/call_tracer_legacy.js b/eth/tracers/js/internal/tracers/call_tracer_legacy.js index 3ca737773..054512735 100644 --- a/eth/tracers/js/internal/tracers/call_tracer_legacy.js +++ b/eth/tracers/js/internal/tracers/call_tracer_legacy.js @@ -220,7 +220,7 @@ return this.finalize(result); }, - // finalize recreates a call object using the final desired field oder for json + // finalize recreates a call object using the final desired field order for json // serialization. This is a nicety feature to pass meaningfully ordered results // to users who don't interpret it, just display it. finalize: function(call) { diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go new file mode 100644 index 000000000..ffc679fde --- /dev/null +++ b/eth/tracers/logger/access_list_tracer.go @@ -0,0 +1,183 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package logger + +import ( + "math/big" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/core/vm" +) + +// accessList is an accumulator for the set of accounts and storage slots an EVM +// contract execution touches. +type accessList map[common.Address]accessListSlots + +// accessListSlots is an accumulator for the set of storage slots within a single +// contract that an EVM contract execution touches. +type accessListSlots map[common.Hash]struct{} + +// newAccessList creates a new accessList. +func newAccessList() accessList { + return make(map[common.Address]accessListSlots) +} + +// addAddress adds an address to the accesslist. +func (al accessList) addAddress(address common.Address) { + // Set address if not previously present + if _, present := al[address]; !present { + al[address] = make(map[common.Hash]struct{}) + } +} + +// addSlot adds a storage slot to the accesslist. +func (al accessList) addSlot(address common.Address, slot common.Hash) { + // Set address if not previously present + al.addAddress(address) + + // Set the slot on the surely existent storage set + al[address][slot] = struct{}{} +} + +// equal checks if the content of the current access list is the same as the +// content of the other one. +func (al accessList) equal(other accessList) bool { + // Cross reference the accounts first + if len(al) != len(other) { + return false + } + // Given that len(al) == len(other), we only need to check that + // all the items from al are in other. + for addr := range al { + if _, ok := other[addr]; !ok { + return false + } + } + + // Accounts match, cross reference the storage slots too + for addr, slots := range al { + otherslots := other[addr] + + if len(slots) != len(otherslots) { + return false + } + // Given that len(slots) == len(otherslots), we only need to check that + // all the items from slots are in otherslots. + for hash := range slots { + if _, ok := otherslots[hash]; !ok { + return false + } + } + } + return true +} + +// accesslist converts the accesslist to a types.AccessList. +func (al accessList) accessList() types.AccessList { + acl := make(types.AccessList, 0, len(al)) + for addr, slots := range al { + tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}} + for slot := range slots { + tuple.StorageKeys = append(tuple.StorageKeys, slot) + } + acl = append(acl, tuple) + } + return acl +} + +// AccessListTracer is a tracer that accumulates touched accounts and storage +// slots into an internal set. +type AccessListTracer struct { + excl map[common.Address]struct{} // Set of account to exclude from the list + list accessList // Set of accounts and storage slots touched +} + +// NewAccessListTracer creates a new tracer that can generate AccessLists. +// An optional AccessList can be specified to occupy slots and addresses in +// the resulting accesslist. +func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompiles []common.Address) *AccessListTracer { + excl := map[common.Address]struct{}{ + from: {}, to: {}, + } + for _, addr := range precompiles { + excl[addr] = struct{}{} + } + list := newAccessList() + for _, al := range acl { + if _, ok := excl[al.Address]; !ok { + list.addAddress(al.Address) + } + for _, slot := range al.StorageKeys { + list.addSlot(al.Address, slot) + } + } + return &AccessListTracer{ + excl: excl, + list: list, + } +} + +func (a *AccessListTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +} + +// CaptureState captures all opcodes that touch storage or addresses and adds them to the accesslist. +func (a *AccessListTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + stack := scope.Stack + stackData := stack.Data() + stackLen := len(stackData) + if (op == vm.SLOAD || op == vm.SSTORE) && stackLen >= 1 { + slot := common.Hash(stackData[stackLen-1].Bytes32()) + a.list.addSlot(scope.Contract.Address(), slot) + } + if (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT) && stackLen >= 1 { + addr := common.Address(stackData[stackLen-1].Bytes20()) + if _, ok := a.excl[addr]; !ok { + a.list.addAddress(addr) + } + } + if (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE) && stackLen >= 5 { + addr := common.Address(stackData[stackLen-2].Bytes20()) + if _, ok := a.excl[addr]; !ok { + a.list.addAddress(addr) + } + } +} + +func (*AccessListTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +func (*AccessListTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {} + +func (*AccessListTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (*AccessListTracer) CaptureExit(output []byte, gasUsed uint64, err error) {} + +func (*AccessListTracer) CaptureTxStart(gasLimit uint64) {} + +func (*AccessListTracer) CaptureTxEnd(restGas uint64) {} + +// AccessList returns the current accesslist maintained by the tracer. +func (a *AccessListTracer) AccessList() types.AccessList { + return a.list.accessList() +} + +// Equal returns if the content of two access list traces are equal. +func (a *AccessListTracer) Equal(other *AccessListTracer) bool { + return a.list.equal(other.list) +} diff --git a/eth/tracers/logger/gen_structlog.go b/eth/tracers/logger/gen_structlog.go new file mode 100644 index 000000000..e67950ca7 --- /dev/null +++ b/eth/tracers/logger/gen_structlog.go @@ -0,0 +1,118 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package logger + +import ( + "encoding/json" + + "github.com/holiman/uint256" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/common/math" + "github.com/morph-l2/go-ethereum/core/vm" +) + +var _ = (*structLogMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (s StructLog) MarshalJSON() ([]byte, error) { + type StructLog struct { + Pc uint64 `json:"pc"` + Op vm.OpCode `json:"op"` + Gas math.HexOrDecimal64 `json:"gas"` + GasCost math.HexOrDecimal64 `json:"gasCost"` + Memory hexutil.Bytes `json:"memory,omitempty"` + MemorySize int `json:"memSize"` + Stack []hexutil.U256 `json:"stack"` + ReturnData hexutil.Bytes `json:"returnData,omitempty"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + Err error `json:"-"` + OpName string `json:"opName"` + ErrorString string `json:"error,omitempty"` + } + var enc StructLog + enc.Pc = s.Pc + enc.Op = s.Op + enc.Gas = math.HexOrDecimal64(s.Gas) + enc.GasCost = math.HexOrDecimal64(s.GasCost) + enc.Memory = s.Memory + enc.MemorySize = s.MemorySize + if s.Stack != nil { + enc.Stack = make([]hexutil.U256, len(s.Stack)) + for k, v := range s.Stack { + enc.Stack[k] = hexutil.U256(v) + } + } + enc.ReturnData = s.ReturnData + enc.Storage = s.Storage + enc.Depth = s.Depth + enc.RefundCounter = s.RefundCounter + enc.Err = s.Err + enc.OpName = s.OpName() + enc.ErrorString = s.ErrorString() + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (s *StructLog) UnmarshalJSON(input []byte) error { + type StructLog struct { + Pc *uint64 `json:"pc"` + Op *vm.OpCode `json:"op"` + Gas *math.HexOrDecimal64 `json:"gas"` + GasCost *math.HexOrDecimal64 `json:"gasCost"` + Memory *hexutil.Bytes `json:"memory,omitempty"` + MemorySize *int `json:"memSize"` + Stack []hexutil.U256 `json:"stack"` + ReturnData *hexutil.Bytes `json:"returnData,omitempty"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth *int `json:"depth"` + RefundCounter *uint64 `json:"refund"` + Err error `json:"-"` + } + var dec StructLog + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Pc != nil { + s.Pc = *dec.Pc + } + if dec.Op != nil { + s.Op = *dec.Op + } + if dec.Gas != nil { + s.Gas = uint64(*dec.Gas) + } + if dec.GasCost != nil { + s.GasCost = uint64(*dec.GasCost) + } + if dec.Memory != nil { + s.Memory = *dec.Memory + } + if dec.MemorySize != nil { + s.MemorySize = *dec.MemorySize + } + if dec.Stack != nil { + s.Stack = make([]uint256.Int, len(dec.Stack)) + for k, v := range dec.Stack { + s.Stack[k] = uint256.Int(v) + } + } + if dec.ReturnData != nil { + s.ReturnData = *dec.ReturnData + } + if dec.Storage != nil { + s.Storage = dec.Storage + } + if dec.Depth != nil { + s.Depth = *dec.Depth + } + if dec.RefundCounter != nil { + s.RefundCounter = *dec.RefundCounter + } + if dec.Err != nil { + s.Err = dec.Err + } + return nil +} diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go new file mode 100644 index 000000000..3d5d4dcf5 --- /dev/null +++ b/eth/tracers/logger/logger.go @@ -0,0 +1,459 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package logger + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/big" + "strings" + "sync/atomic" + "time" + + "github.com/holiman/uint256" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/hexutil" + "github.com/morph-l2/go-ethereum/common/math" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/core/vm" +) + +// Storage represents a contract's storage. +type Storage map[common.Hash]common.Hash + +// Copy duplicates the current storage. +func (s Storage) Copy() Storage { + cpy := make(Storage, len(s)) + for key, value := range s { + cpy[key] = value + } + return cpy +} + +//go:generate go run github.com/fjl/gencodec -type StructLog -field-override structLogMarshaling -out gen_structlog.go + +// StructLog is emitted to the EVM each cycle and lists information about the current internal state +// prior to the execution of the statement. +type StructLog struct { + Pc uint64 `json:"pc"` + Op vm.OpCode `json:"op"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Memory []byte `json:"memory,omitempty"` + MemorySize int `json:"memSize"` + Stack []uint256.Int `json:"stack"` + ReturnData []byte `json:"returnData,omitempty"` + Storage map[common.Hash]common.Hash `json:"-"` + Depth int `json:"depth"` + RefundCounter uint64 `json:"refund"` + Err error `json:"-"` +} + +// overrides for gencodec +type structLogMarshaling struct { + Gas math.HexOrDecimal64 + GasCost math.HexOrDecimal64 + Memory hexutil.Bytes + ReturnData hexutil.Bytes + Stack []hexutil.U256 + OpName string `json:"opName"` // adds call to OpName() in MarshalJSON + ErrorString string `json:"error,omitempty"` // adds call to ErrorString() in MarshalJSON +} + +// OpName formats the operand name in a human-readable format. +func (s *StructLog) OpName() string { + return s.Op.String() +} + +// ErrorString formats the log's error as a string. +func (s *StructLog) ErrorString() string { + if s.Err != nil { + return s.Err.Error() + } + return "" +} + +// StructLogger is an EVM state logger and implements EVMLogger. +// +// StructLogger can capture state based on the given Log configuration and also keeps +// a track record of modified storage which is used in reporting snapshots of the +// contract their storage. +type StructLogger struct { + cfg vm.LogConfig + env *vm.EVM + + storage map[common.Address]Storage + logs []StructLog + output []byte + err error + gasLimit uint64 + usedGas uint64 + + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +// NewStructLogger returns a new logger +func NewStructLogger(cfg *vm.LogConfig) *StructLogger { + logger := &StructLogger{ + storage: make(map[common.Address]Storage), + } + if cfg != nil { + logger.cfg = *cfg + } + return logger +} + +// Reset clears the data held by the logger. +func (l *StructLogger) Reset() { + l.storage = make(map[common.Address]Storage) + l.output = make([]byte, 0) + l.logs = l.logs[:0] + l.err = nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (l *StructLogger) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + l.env = env +} + +// CaptureState logs a new structured log message and pushes it out to the environment +// +// CaptureState also tracks SLOAD/SSTORE ops to track storage change. +func (l *StructLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + // If tracing was interrupted, set the error and stop + if l.interrupt.Load() { + return + } + // check if already accumulated the specified number of logs + if l.cfg.Limit != 0 && l.cfg.Limit <= len(l.logs) { + return + } + + memory := scope.Memory + stack := scope.Stack + contract := scope.Contract + // Copy a snapshot of the current memory state to a new buffer + var mem []byte + if l.cfg.EnableMemory { + mem = make([]byte, len(memory.Data())) + copy(mem, memory.Data()) + } + // Copy a snapshot of the current stack state to a new buffer + var stck []uint256.Int + if !l.cfg.DisableStack { + stck = make([]uint256.Int, len(stack.Data())) + for i, item := range stack.Data() { + stck[i] = item + } + } + stackData := stack.Data() + stackLen := len(stackData) + // Copy a snapshot of the current storage to a new container + var storage Storage + if !l.cfg.DisableStorage && (op == vm.SLOAD || op == vm.SSTORE) { + // initialise new changed values storage container for this contract + // if not present. + if l.storage[contract.Address()] == nil { + l.storage[contract.Address()] = make(Storage) + } + // capture SLOAD opcodes and record the read entry in the local storage + if op == vm.SLOAD && stackLen >= 1 { + var ( + address = common.Hash(stackData[stackLen-1].Bytes32()) + value = l.env.StateDB.GetState(contract.Address(), address) + ) + l.storage[contract.Address()][address] = value + storage = l.storage[contract.Address()].Copy() + } else if op == vm.SSTORE && stackLen >= 2 { + // capture SSTORE opcodes and record the written entry in the local storage. + var ( + value = common.Hash(stackData[stackLen-2].Bytes32()) + address = common.Hash(stackData[stackLen-1].Bytes32()) + ) + l.storage[contract.Address()][address] = value + storage = l.storage[contract.Address()].Copy() + } + } + var rdata []byte + if l.cfg.EnableReturnData { + rdata = make([]byte, len(rData)) + copy(rdata, rData) + } + // create a new snapshot of the EVM. + log := StructLog{pc, op, gas, cost, mem, memory.Len(), stck, rdata, storage, depth, l.env.StateDB.GetRefund(), err} + l.logs = append(l.logs, log) +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault +// while running an opcode. +func (l *StructLogger) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { + l.output = output + l.err = err + if l.cfg.Debug { + fmt.Printf("%#x\n", output) + if err != nil { + fmt.Printf(" error: %v\n", err) + } + } +} + +func (l *StructLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +// CaptureStateAfter for special needs, tracks SSTORE ops and records the storage change. +func (l *StructLogger) CaptureStateAfter(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { +} + +func (l *StructLogger) GetResult() (json.RawMessage, error) { + // Tracing aborted + if l.reason != nil { + return nil, l.reason + } + failed := l.err != nil + returnData := common.CopyBytes(l.output) + // Return data when successful and revert reason when reverted, otherwise empty. + returnVal := fmt.Sprintf("%x", returnData) + if failed && l.err != vm.ErrExecutionReverted { + returnVal = "" + } + return json.Marshal(&ExecutionResult{ + Gas: l.usedGas, + Failed: failed, + ReturnValue: returnVal, + StructLogs: formatLogs(l.StructLogs()), + }) +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (l *StructLogger) Stop(err error) { + l.reason = err + l.interrupt.Store(true) +} + +func (l *StructLogger) CaptureTxStart(gasLimit uint64) { + l.gasLimit = gasLimit +} + +func (l *StructLogger) CaptureTxEnd(restGas uint64) { + l.usedGas = l.gasLimit - restGas +} + +// StructLogs returns the captured log entries. +func (l *StructLogger) StructLogs() []StructLog { return l.logs } + +// Error returns the VM error captured by the trace. +func (l *StructLogger) Error() error { return l.err } + +// Output returns the VM return value captured by the trace. +func (l *StructLogger) Output() []byte { return l.output } + +// WriteTrace writes a formatted trace to the given writer +func WriteTrace(writer io.Writer, logs []StructLog) { + for _, log := range logs { + fmt.Fprintf(writer, "%-16spc=%08d gas=%v cost=%v", log.Op, log.Pc, log.Gas, log.GasCost) + if log.Err != nil { + fmt.Fprintf(writer, " ERROR: %v", log.Err) + } + fmt.Fprintln(writer) + + if len(log.Stack) > 0 { + fmt.Fprintln(writer, "Stack:") + for i := len(log.Stack) - 1; i >= 0; i-- { + fmt.Fprintf(writer, "%08d %s\n", len(log.Stack)-i-1, log.Stack[i].Hex()) + } + } + if len(log.Memory) > 0 { + fmt.Fprintln(writer, "Memory:") + fmt.Fprint(writer, hex.Dump(log.Memory)) + } + if len(log.Storage) > 0 { + fmt.Fprintln(writer, "Storage:") + for h, item := range log.Storage { + fmt.Fprintf(writer, "%x: %x\n", h, item) + } + } + if len(log.ReturnData) > 0 { + fmt.Fprintln(writer, "ReturnData:") + fmt.Fprint(writer, hex.Dump(log.ReturnData)) + } + fmt.Fprintln(writer) + } +} + +// WriteLogs writes vm logs in a readable format to the given writer +func WriteLogs(writer io.Writer, logs []*types.Log) { + for _, log := range logs { + fmt.Fprintf(writer, "LOG%d: %x bn=%d txi=%x\n", len(log.Topics), log.Address, log.BlockNumber, log.TxIndex) + + for i, topic := range log.Topics { + fmt.Fprintf(writer, "%08d %x\n", i, topic) + } + + fmt.Fprint(writer, hex.Dump(log.Data)) + fmt.Fprintln(writer) + } +} + +type mdLogger struct { + out io.Writer + cfg *vm.LogConfig + env *vm.EVM +} + +// NewMarkdownLogger creates a logger which outputs information in a format adapted +// for human readability, and is also a valid markdown table +func NewMarkdownLogger(cfg *vm.LogConfig, writer io.Writer) *mdLogger { + l := &mdLogger{out: writer, cfg: cfg} + if l.cfg == nil { + l.cfg = &vm.LogConfig{} + } + return l +} + +func (t *mdLogger) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + if !create { + fmt.Fprintf(t.out, "From: `%v`\nTo: `%v`\nData: `%#x`\nGas: `%d`\nValue `%v` wei\n", + from.String(), to.String(), + input, gas, value) + } else { + fmt.Fprintf(t.out, "From: `%v`\nCreate at: `%v`\nData: `%#x`\nGas: `%d`\nValue `%v` wei\n", + from.String(), to.String(), + input, gas, value) + } + + fmt.Fprintf(t.out, ` +| Pc | Op | Cost | Stack | RStack | Refund | +|-------|-------------|------|-----------|-----------|---------| +`) +} + +// CaptureState also tracks SLOAD/SSTORE ops to track storage change. +func (t *mdLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + stack := scope.Stack + fmt.Fprintf(t.out, "| %4d | %10v | %3d |", pc, op, cost) + + if !t.cfg.DisableStack { + // format stack + var a []string + for _, elem := range stack.Data() { + a = append(a, elem.Hex()) + } + b := fmt.Sprintf("[%v]", strings.Join(a, ",")) + fmt.Fprintf(t.out, "%10v |", b) + } + fmt.Fprintf(t.out, "%10v |", t.env.StateDB.GetRefund()) + fmt.Fprintln(t.out, "") + if err != nil { + fmt.Fprintf(t.out, "Error: %v\n", err) + } +} + +func (t *mdLogger) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + fmt.Fprintf(t.out, "\nError: at pc=%d, op=%v: %v\n", pc, op, err) +} + +func (t *mdLogger) CaptureEnd(output []byte, gasUsed uint64, err error) { + fmt.Fprintf(t.out, "\nOutput: `%#x`\nConsumed gas: `%d`\nError: `%v`\n", + output, gasUsed, err) +} + +func (t *mdLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} + +func (*mdLogger) CaptureTxStart(gasLimit uint64) {} + +func (*mdLogger) CaptureTxEnd(restGas uint64) {} + +// ExecutionResult groups all structured logs emitted by the EVM +// while replaying a transaction in debug mode as well as transaction +// execution status, the amount of gas used and the return value +type ExecutionResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"failed"` + ReturnValue string `json:"returnValue"` + StructLogs []StructLogRes `json:"structLogs"` +} + +// StructLogRes stores a structured log emitted by the EVM while replaying a +// transaction in debug mode +type StructLogRes struct { + Pc uint64 `json:"pc"` + Op string `json:"op"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Depth int `json:"depth"` + Error string `json:"error,omitempty"` + Stack *[]string `json:"stack,omitempty"` + ReturnData string `json:"returnData,omitempty"` + Memory *[]string `json:"memory,omitempty"` + Storage *map[string]string `json:"storage,omitempty"` + RefundCounter uint64 `json:"refund,omitempty"` +} + +// formatLogs formats EVM returned structured logs for json output +func formatLogs(logs []StructLog) []StructLogRes { + formatted := make([]StructLogRes, len(logs)) + for index, trace := range logs { + formatted[index] = StructLogRes{ + Pc: trace.Pc, + Op: trace.Op.String(), + Gas: trace.Gas, + GasCost: trace.GasCost, + Depth: trace.Depth, + Error: trace.ErrorString(), + RefundCounter: trace.RefundCounter, + } + if trace.Stack != nil { + stack := make([]string, len(trace.Stack)) + for i, stackValue := range trace.Stack { + stack[i] = stackValue.Hex() + } + formatted[index].Stack = &stack + } + if trace.ReturnData != nil && len(trace.ReturnData) > 0 { + formatted[index].ReturnData = hexutil.Bytes(trace.ReturnData).String() + } + if trace.Memory != nil { + memory := make([]string, 0, (len(trace.Memory)+31)/32) + for i := 0; i+32 <= len(trace.Memory); i += 32 { + memory = append(memory, fmt.Sprintf("%x", trace.Memory[i:i+32])) + } + formatted[index].Memory = &memory + } + if trace.Storage != nil { + storage := make(map[string]string) + for i, storageValue := range trace.Storage { + storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) + } + formatted[index].Storage = &storage + } + } + return formatted +} diff --git a/eth/tracers/logger/logger_json.go b/eth/tracers/logger/logger_json.go new file mode 100644 index 000000000..e4b553a48 --- /dev/null +++ b/eth/tracers/logger/logger_json.go @@ -0,0 +1,102 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package logger + +import ( + "encoding/json" + "io" + "math/big" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/common/math" + "github.com/morph-l2/go-ethereum/core/vm" +) + +type JSONLogger struct { + encoder *json.Encoder + cfg *vm.LogConfig + env *vm.EVM +} + +// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects +// into the provided stream. +func NewJSONLogger(cfg *vm.LogConfig, writer io.Writer) *JSONLogger { + l := &JSONLogger{encoder: json.NewEncoder(writer), cfg: cfg} + if l.cfg == nil { + l.cfg = &vm.LogConfig{} + } + return l +} + +func (l *JSONLogger) CaptureStart(env *vm.EVM, from, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + l.env = env +} + +func (l *JSONLogger) CaptureFault(pc uint64, op vm.OpCode, gas uint64, cost uint64, scope *vm.ScopeContext, depth int, err error) { + // TODO: Add rData to this interface as well + l.CaptureState(pc, op, gas, cost, scope, nil, depth, err) +} + +// CaptureState outputs state information on the logger. +func (l *JSONLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + memory := scope.Memory + stack := scope.Stack + + log := StructLog{ + Pc: pc, + Op: op, + Gas: gas, + GasCost: cost, + MemorySize: memory.Len(), + Depth: depth, + RefundCounter: l.env.StateDB.GetRefund(), + Err: err, + } + if l.cfg.EnableMemory { + log.Memory = memory.Data() + } + if !l.cfg.DisableStack { + log.Stack = stack.Data() + } + if l.cfg.EnableReturnData { + log.ReturnData = rData + } + l.encoder.Encode(log) +} + +// CaptureEnd is triggered at end of execution. +func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, err error) { + type endLog struct { + Output string `json:"output"` + GasUsed math.HexOrDecimal64 `json:"gasUsed"` + Err string `json:"error,omitempty"` + } + var errMsg string + if err != nil { + errMsg = err.Error() + } + l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), errMsg}) +} + +func (l *JSONLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (l *JSONLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} + +func (l *JSONLogger) CaptureTxStart(gasLimit uint64) {} + +func (l *JSONLogger) CaptureTxEnd(restGas uint64) {} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go new file mode 100644 index 000000000..4247e2311 --- /dev/null +++ b/eth/tracers/logger/logger_test.go @@ -0,0 +1,107 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package logger + +import ( + "encoding/json" + "errors" + "math/big" + "testing" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/state" + "github.com/morph-l2/go-ethereum/core/vm" + "github.com/morph-l2/go-ethereum/params" +) + +type dummyContractRef struct { + calledForEach bool +} + +func (dummyContractRef) Address() common.Address { return common.Address{} } +func (dummyContractRef) Value() *big.Int { return new(big.Int) } +func (dummyContractRef) SetCode(common.Hash, []byte) {} +func (d *dummyContractRef) ForEachStorage(callback func(key, value common.Hash) bool) { + d.calledForEach = true +} +func (d *dummyContractRef) SubBalance(amount *big.Int) {} +func (d *dummyContractRef) AddBalance(amount *big.Int) {} +func (d *dummyContractRef) SetBalance(*big.Int) {} +func (d *dummyContractRef) SetNonce(uint64) {} +func (d *dummyContractRef) Balance() *big.Int { return new(big.Int) } + +type dummyStatedb struct { + state.StateDB +} + +func (*dummyStatedb) GetRefund() uint64 { return 1337 } +func (*dummyStatedb) GetState(_ common.Address, _ common.Hash) common.Hash { return common.Hash{} } +func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {} + +func TestStoreCapture(t *testing.T) { + var ( + logger = NewStructLogger(nil) + env = vm.NewEVM(vm.BlockContext{}, vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger}) + contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 100000) + ) + contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} + var index common.Hash + logger.CaptureStart(env, common.Address{}, contract.Address(), false, nil, 0, nil) + _, err := env.Interpreter().Run(contract, []byte{}, false) + if err != nil { + t.Fatal(err) + } + if len(logger.storage[contract.Address()]) == 0 { + t.Fatalf("expected exactly 1 changed value on address %x, got %d", contract.Address(), + len(logger.storage[contract.Address()])) + } + exp := common.BigToHash(big.NewInt(1)) + if logger.storage[contract.Address()][index] != exp { + t.Errorf("expected %x, got %x", exp, logger.storage[contract.Address()][index]) + } +} + +// Tests that blank fields don't appear in logs when JSON marshalled, to reduce +// logs bloat and confusion. See https://github.com/ethereum/go-ethereum/issues/24487 +func TestStructLogMarshalingOmitEmpty(t *testing.T) { + tests := []struct { + name string + log *StructLog + want string + }{ + {"empty err and no fields", &StructLog{}, + `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, + {"with err", &StructLog{Err: errors.New("this failed")}, + `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP","error":"this failed"}`}, + {"with mem", &StructLog{Memory: make([]byte, 2), MemorySize: 2}, + `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memory":"0x0000","memSize":2,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, + {"with 0-size mem", &StructLog{Memory: make([]byte, 0)}, + `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + blob, err := json.Marshal(tt.log) + if err != nil { + t.Fatal(err) + } + if have, want := string(blob), tt.want; have != want { + t.Fatalf("mismatched results\n\thave: %v\n\twant: %v", have, want) + } + }) + } +} From 9422f8684c34e99580917fcf5189a7a944f2ced8 Mon Sep 17 00:00:00 2001 From: ryanmorphl2 <163962984+ryanmorphl2@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:29:33 +0800 Subject: [PATCH 3/5] StructLog: add L1DataFee --- eth/tracers/api.go | 8 +++++++- eth/tracers/logger/logger.go | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/eth/tracers/api.go b/eth/tracers/api.go index f02bb4dcd..845fb1452 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -960,9 +960,15 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex } // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) - if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), l1DataFee); err != nil { + result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), l1DataFee) + if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } + + l, ok := tracer.(*logger.StructLogger) + if ok { + l.ResultL1DataFee = result.L1DataFee + } return tracer.GetResult() } diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 3d5d4dcf5..e293bd435 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -107,6 +107,8 @@ type StructLogger struct { interrupt atomic.Bool // Atomic flag to signal execution interruption reason error // Textual reason for the interruption + + ResultL1DataFee *big.Int } // NewStructLogger returns a new logger @@ -245,6 +247,7 @@ func (l *StructLogger) GetResult() (json.RawMessage, error) { Failed: failed, ReturnValue: returnVal, StructLogs: formatLogs(l.StructLogs()), + L1DataFee: (*hexutil.Big)(l.ResultL1DataFee), }) } @@ -399,6 +402,7 @@ type ExecutionResult struct { Failed bool `json:"failed"` ReturnValue string `json:"returnValue"` StructLogs []StructLogRes `json:"structLogs"` + L1DataFee *hexutil.Big `json:"l1DataFee,omitempty"` } // StructLogRes stores a structured log emitted by the EVM while replaying a From b0dd86bbcc62c94921d83fa5f270ab3107f81c7d Mon Sep 17 00:00:00 2001 From: ryanmorphl2 <163962984+ryanmorphl2@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:42:27 +0800 Subject: [PATCH 4/5] revert tracer logger --- core/types/l2trace.go | 1 + core/vm/logger.go | 91 ++++- eth/tracers/api.go | 5 +- eth/tracers/logger/access_list_tracer.go | 183 --------- eth/tracers/logger/gen_structlog.go | 118 ------ eth/tracers/logger/logger.go | 463 ----------------------- eth/tracers/logger/logger_json.go | 102 ----- eth/tracers/logger/logger_test.go | 107 ------ 8 files changed, 92 insertions(+), 978 deletions(-) delete mode 100644 eth/tracers/logger/access_list_tracer.go delete mode 100644 eth/tracers/logger/gen_structlog.go delete mode 100644 eth/tracers/logger/logger.go delete mode 100644 eth/tracers/logger/logger_json.go delete mode 100644 eth/tracers/logger/logger_test.go diff --git a/core/types/l2trace.go b/core/types/l2trace.go index 2ef09b588..a125a7556 100644 --- a/core/types/l2trace.go +++ b/core/types/l2trace.go @@ -85,6 +85,7 @@ type StructLogRes struct { Depth int `json:"depth"` Error string `json:"error,omitempty"` Stack []string `json:"stack,omitempty"` + ReturnData string `json:"returnData,omitempty"` Memory []string `json:"memory,omitempty"` Storage map[string]string `json:"storage,omitempty"` RefundCounter uint64 `json:"refund,omitempty"` diff --git a/core/vm/logger.go b/core/vm/logger.go index c587433ba..f50f634ce 100644 --- a/core/vm/logger.go +++ b/core/vm/logger.go @@ -19,10 +19,12 @@ package vm import ( "bytes" "encoding/hex" + "encoding/json" "fmt" "io" "math/big" "strings" + "sync/atomic" "time" "github.com/holiman/uint256" @@ -165,6 +167,14 @@ type StructLogger struct { logs []*StructLog output []byte err error + + gasLimit uint64 + usedGas uint64 + + interrupt atomic.Bool // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption + + ResultL1DataFee *big.Int } // NewStructLogger returns a new logger @@ -217,6 +227,11 @@ func (l *StructLogger) CaptureStart(env *EVM, from common.Address, to common.Add // // CaptureState also tracks SLOAD/SSTORE ops to track storage change. func (l *StructLogger) CaptureState(pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, opErr error) { + // If tracing was interrupted, set the error and stop + if l.interrupt.Load() { + return + } + memory := scope.Memory stack := scope.Stack contract := scope.Contract @@ -343,9 +358,13 @@ func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { } -func (t *StructLogger) CaptureTxStart(gasLimit uint64) {} +func (l *StructLogger) CaptureTxStart(gasLimit uint64) { + l.gasLimit = gasLimit +} -func (t *StructLogger) CaptureTxEnd(restGas uint64) {} +func (l *StructLogger) CaptureTxEnd(restGas uint64) { + l.usedGas = l.gasLimit - restGas +} // UpdatedAccounts is used to collect all "touched" accounts func (l *StructLogger) UpdatedAccounts() map[common.Address]struct{} { @@ -374,6 +393,33 @@ func (l *StructLogger) Error() error { return l.err } // Output returns the VM return value captured by the trace. func (l *StructLogger) Output() []byte { return l.output } +func (l *StructLogger) GetResult() (json.RawMessage, error) { + // Tracing aborted + if l.reason != nil { + return nil, l.reason + } + failed := l.err != nil + returnData := common.CopyBytes(l.output) + // Return data when successful and revert reason when reverted, otherwise empty. + returnVal := fmt.Sprintf("%x", returnData) + if failed && l.err != ErrExecutionReverted { + returnVal = "" + } + return json.Marshal(&types.ExecutionResult{ + Gas: l.usedGas, + Failed: failed, + ReturnValue: returnVal, + StructLogs: formatLogs(l.StructLogs()), + L1DataFee: (*hexutil.Big)(l.ResultL1DataFee), + }) +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (l *StructLogger) Stop(err error) { + l.reason = err + l.interrupt.Store(true) +} + // WriteTrace writes a formatted trace to the given writer func WriteTrace(writer io.Writer, logs []*StructLog) { for _, log := range logs { @@ -522,3 +568,44 @@ func FormatLogs(logs []*StructLog) []*types.StructLogRes { } return formatted } + +// formatLogs formats EVM returned structured logs for json output +func formatLogs(logs []*StructLog) []*types.StructLogRes { + formatted := make([]*types.StructLogRes, len(logs)) + for index, trace := range logs { + formatted[index] = &types.StructLogRes{ + Pc: trace.Pc, + Op: trace.Op.String(), + Gas: trace.Gas, + GasCost: trace.GasCost, + Depth: trace.Depth, + Error: trace.ErrorString(), + RefundCounter: trace.RefundCounter, + } + if trace.Stack != nil { + stack := make([]string, len(trace.Stack)) + for i, stackValue := range trace.Stack { + stack[i] = stackValue.Hex() + } + formatted[index].Stack = stack + } + if trace.ReturnData.Len() > 0 { + formatted[index].ReturnData = hexutil.Bytes(trace.ReturnData.Bytes()).String() + } + if trace.Memory.Len() > 0 { + memory := make([]string, 0, (trace.Memory.Len()+31)/32) + for i := 0; i+32 <= trace.Memory.Len(); i += 32 { + memory = append(memory, fmt.Sprintf("%x", trace.Memory.Bytes()[i:i+32])) + } + formatted[index].Memory = memory + } + if trace.Storage != nil { + storage := make(map[string]string) + for i, storageValue := range trace.Storage { + storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) + } + formatted[index].Storage = storage + } + } + return formatted +} diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 845fb1452..ba40a0f85 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -38,7 +38,6 @@ import ( "github.com/morph-l2/go-ethereum/core/state" "github.com/morph-l2/go-ethereum/core/types" "github.com/morph-l2/go-ethereum/core/vm" - "github.com/morph-l2/go-ethereum/eth/tracers/logger" "github.com/morph-l2/go-ethereum/ethdb" "github.com/morph-l2/go-ethereum/internal/ethapi" "github.com/morph-l2/go-ethereum/log" @@ -927,7 +926,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex config = &TraceConfig{} } // Default tracer is the struct logger - tracer = logger.NewStructLogger(config.LogConfig) + tracer = vm.NewStructLogger(config.LogConfig) if config.Tracer != nil { tracer, err = New(*config.Tracer, txctx, config.TracerConfig) if err != nil { @@ -965,7 +964,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex return nil, fmt.Errorf("tracing failed: %w", err) } - l, ok := tracer.(*logger.StructLogger) + l, ok := tracer.(*vm.StructLogger) if ok { l.ResultL1DataFee = result.L1DataFee } diff --git a/eth/tracers/logger/access_list_tracer.go b/eth/tracers/logger/access_list_tracer.go deleted file mode 100644 index ffc679fde..000000000 --- a/eth/tracers/logger/access_list_tracer.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package logger - -import ( - "math/big" - - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/core/types" - "github.com/morph-l2/go-ethereum/core/vm" -) - -// accessList is an accumulator for the set of accounts and storage slots an EVM -// contract execution touches. -type accessList map[common.Address]accessListSlots - -// accessListSlots is an accumulator for the set of storage slots within a single -// contract that an EVM contract execution touches. -type accessListSlots map[common.Hash]struct{} - -// newAccessList creates a new accessList. -func newAccessList() accessList { - return make(map[common.Address]accessListSlots) -} - -// addAddress adds an address to the accesslist. -func (al accessList) addAddress(address common.Address) { - // Set address if not previously present - if _, present := al[address]; !present { - al[address] = make(map[common.Hash]struct{}) - } -} - -// addSlot adds a storage slot to the accesslist. -func (al accessList) addSlot(address common.Address, slot common.Hash) { - // Set address if not previously present - al.addAddress(address) - - // Set the slot on the surely existent storage set - al[address][slot] = struct{}{} -} - -// equal checks if the content of the current access list is the same as the -// content of the other one. -func (al accessList) equal(other accessList) bool { - // Cross reference the accounts first - if len(al) != len(other) { - return false - } - // Given that len(al) == len(other), we only need to check that - // all the items from al are in other. - for addr := range al { - if _, ok := other[addr]; !ok { - return false - } - } - - // Accounts match, cross reference the storage slots too - for addr, slots := range al { - otherslots := other[addr] - - if len(slots) != len(otherslots) { - return false - } - // Given that len(slots) == len(otherslots), we only need to check that - // all the items from slots are in otherslots. - for hash := range slots { - if _, ok := otherslots[hash]; !ok { - return false - } - } - } - return true -} - -// accesslist converts the accesslist to a types.AccessList. -func (al accessList) accessList() types.AccessList { - acl := make(types.AccessList, 0, len(al)) - for addr, slots := range al { - tuple := types.AccessTuple{Address: addr, StorageKeys: []common.Hash{}} - for slot := range slots { - tuple.StorageKeys = append(tuple.StorageKeys, slot) - } - acl = append(acl, tuple) - } - return acl -} - -// AccessListTracer is a tracer that accumulates touched accounts and storage -// slots into an internal set. -type AccessListTracer struct { - excl map[common.Address]struct{} // Set of account to exclude from the list - list accessList // Set of accounts and storage slots touched -} - -// NewAccessListTracer creates a new tracer that can generate AccessLists. -// An optional AccessList can be specified to occupy slots and addresses in -// the resulting accesslist. -func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompiles []common.Address) *AccessListTracer { - excl := map[common.Address]struct{}{ - from: {}, to: {}, - } - for _, addr := range precompiles { - excl[addr] = struct{}{} - } - list := newAccessList() - for _, al := range acl { - if _, ok := excl[al.Address]; !ok { - list.addAddress(al.Address) - } - for _, slot := range al.StorageKeys { - list.addSlot(al.Address, slot) - } - } - return &AccessListTracer{ - excl: excl, - list: list, - } -} - -func (a *AccessListTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { -} - -// CaptureState captures all opcodes that touch storage or addresses and adds them to the accesslist. -func (a *AccessListTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - stack := scope.Stack - stackData := stack.Data() - stackLen := len(stackData) - if (op == vm.SLOAD || op == vm.SSTORE) && stackLen >= 1 { - slot := common.Hash(stackData[stackLen-1].Bytes32()) - a.list.addSlot(scope.Contract.Address(), slot) - } - if (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT) && stackLen >= 1 { - addr := common.Address(stackData[stackLen-1].Bytes20()) - if _, ok := a.excl[addr]; !ok { - a.list.addAddress(addr) - } - } - if (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE) && stackLen >= 5 { - addr := common.Address(stackData[stackLen-2].Bytes20()) - if _, ok := a.excl[addr]; !ok { - a.list.addAddress(addr) - } - } -} - -func (*AccessListTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { -} - -func (*AccessListTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {} - -func (*AccessListTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { -} - -func (*AccessListTracer) CaptureExit(output []byte, gasUsed uint64, err error) {} - -func (*AccessListTracer) CaptureTxStart(gasLimit uint64) {} - -func (*AccessListTracer) CaptureTxEnd(restGas uint64) {} - -// AccessList returns the current accesslist maintained by the tracer. -func (a *AccessListTracer) AccessList() types.AccessList { - return a.list.accessList() -} - -// Equal returns if the content of two access list traces are equal. -func (a *AccessListTracer) Equal(other *AccessListTracer) bool { - return a.list.equal(other.list) -} diff --git a/eth/tracers/logger/gen_structlog.go b/eth/tracers/logger/gen_structlog.go deleted file mode 100644 index e67950ca7..000000000 --- a/eth/tracers/logger/gen_structlog.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by github.com/fjl/gencodec. DO NOT EDIT. - -package logger - -import ( - "encoding/json" - - "github.com/holiman/uint256" - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/common/hexutil" - "github.com/morph-l2/go-ethereum/common/math" - "github.com/morph-l2/go-ethereum/core/vm" -) - -var _ = (*structLogMarshaling)(nil) - -// MarshalJSON marshals as JSON. -func (s StructLog) MarshalJSON() ([]byte, error) { - type StructLog struct { - Pc uint64 `json:"pc"` - Op vm.OpCode `json:"op"` - Gas math.HexOrDecimal64 `json:"gas"` - GasCost math.HexOrDecimal64 `json:"gasCost"` - Memory hexutil.Bytes `json:"memory,omitempty"` - MemorySize int `json:"memSize"` - Stack []hexutil.U256 `json:"stack"` - ReturnData hexutil.Bytes `json:"returnData,omitempty"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth int `json:"depth"` - RefundCounter uint64 `json:"refund"` - Err error `json:"-"` - OpName string `json:"opName"` - ErrorString string `json:"error,omitempty"` - } - var enc StructLog - enc.Pc = s.Pc - enc.Op = s.Op - enc.Gas = math.HexOrDecimal64(s.Gas) - enc.GasCost = math.HexOrDecimal64(s.GasCost) - enc.Memory = s.Memory - enc.MemorySize = s.MemorySize - if s.Stack != nil { - enc.Stack = make([]hexutil.U256, len(s.Stack)) - for k, v := range s.Stack { - enc.Stack[k] = hexutil.U256(v) - } - } - enc.ReturnData = s.ReturnData - enc.Storage = s.Storage - enc.Depth = s.Depth - enc.RefundCounter = s.RefundCounter - enc.Err = s.Err - enc.OpName = s.OpName() - enc.ErrorString = s.ErrorString() - return json.Marshal(&enc) -} - -// UnmarshalJSON unmarshals from JSON. -func (s *StructLog) UnmarshalJSON(input []byte) error { - type StructLog struct { - Pc *uint64 `json:"pc"` - Op *vm.OpCode `json:"op"` - Gas *math.HexOrDecimal64 `json:"gas"` - GasCost *math.HexOrDecimal64 `json:"gasCost"` - Memory *hexutil.Bytes `json:"memory,omitempty"` - MemorySize *int `json:"memSize"` - Stack []hexutil.U256 `json:"stack"` - ReturnData *hexutil.Bytes `json:"returnData,omitempty"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth *int `json:"depth"` - RefundCounter *uint64 `json:"refund"` - Err error `json:"-"` - } - var dec StructLog - if err := json.Unmarshal(input, &dec); err != nil { - return err - } - if dec.Pc != nil { - s.Pc = *dec.Pc - } - if dec.Op != nil { - s.Op = *dec.Op - } - if dec.Gas != nil { - s.Gas = uint64(*dec.Gas) - } - if dec.GasCost != nil { - s.GasCost = uint64(*dec.GasCost) - } - if dec.Memory != nil { - s.Memory = *dec.Memory - } - if dec.MemorySize != nil { - s.MemorySize = *dec.MemorySize - } - if dec.Stack != nil { - s.Stack = make([]uint256.Int, len(dec.Stack)) - for k, v := range dec.Stack { - s.Stack[k] = uint256.Int(v) - } - } - if dec.ReturnData != nil { - s.ReturnData = *dec.ReturnData - } - if dec.Storage != nil { - s.Storage = dec.Storage - } - if dec.Depth != nil { - s.Depth = *dec.Depth - } - if dec.RefundCounter != nil { - s.RefundCounter = *dec.RefundCounter - } - if dec.Err != nil { - s.Err = dec.Err - } - return nil -} diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go deleted file mode 100644 index e293bd435..000000000 --- a/eth/tracers/logger/logger.go +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package logger - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "io" - "math/big" - "strings" - "sync/atomic" - "time" - - "github.com/holiman/uint256" - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/common/hexutil" - "github.com/morph-l2/go-ethereum/common/math" - "github.com/morph-l2/go-ethereum/core/types" - "github.com/morph-l2/go-ethereum/core/vm" -) - -// Storage represents a contract's storage. -type Storage map[common.Hash]common.Hash - -// Copy duplicates the current storage. -func (s Storage) Copy() Storage { - cpy := make(Storage, len(s)) - for key, value := range s { - cpy[key] = value - } - return cpy -} - -//go:generate go run github.com/fjl/gencodec -type StructLog -field-override structLogMarshaling -out gen_structlog.go - -// StructLog is emitted to the EVM each cycle and lists information about the current internal state -// prior to the execution of the statement. -type StructLog struct { - Pc uint64 `json:"pc"` - Op vm.OpCode `json:"op"` - Gas uint64 `json:"gas"` - GasCost uint64 `json:"gasCost"` - Memory []byte `json:"memory,omitempty"` - MemorySize int `json:"memSize"` - Stack []uint256.Int `json:"stack"` - ReturnData []byte `json:"returnData,omitempty"` - Storage map[common.Hash]common.Hash `json:"-"` - Depth int `json:"depth"` - RefundCounter uint64 `json:"refund"` - Err error `json:"-"` -} - -// overrides for gencodec -type structLogMarshaling struct { - Gas math.HexOrDecimal64 - GasCost math.HexOrDecimal64 - Memory hexutil.Bytes - ReturnData hexutil.Bytes - Stack []hexutil.U256 - OpName string `json:"opName"` // adds call to OpName() in MarshalJSON - ErrorString string `json:"error,omitempty"` // adds call to ErrorString() in MarshalJSON -} - -// OpName formats the operand name in a human-readable format. -func (s *StructLog) OpName() string { - return s.Op.String() -} - -// ErrorString formats the log's error as a string. -func (s *StructLog) ErrorString() string { - if s.Err != nil { - return s.Err.Error() - } - return "" -} - -// StructLogger is an EVM state logger and implements EVMLogger. -// -// StructLogger can capture state based on the given Log configuration and also keeps -// a track record of modified storage which is used in reporting snapshots of the -// contract their storage. -type StructLogger struct { - cfg vm.LogConfig - env *vm.EVM - - storage map[common.Address]Storage - logs []StructLog - output []byte - err error - gasLimit uint64 - usedGas uint64 - - interrupt atomic.Bool // Atomic flag to signal execution interruption - reason error // Textual reason for the interruption - - ResultL1DataFee *big.Int -} - -// NewStructLogger returns a new logger -func NewStructLogger(cfg *vm.LogConfig) *StructLogger { - logger := &StructLogger{ - storage: make(map[common.Address]Storage), - } - if cfg != nil { - logger.cfg = *cfg - } - return logger -} - -// Reset clears the data held by the logger. -func (l *StructLogger) Reset() { - l.storage = make(map[common.Address]Storage) - l.output = make([]byte, 0) - l.logs = l.logs[:0] - l.err = nil -} - -// CaptureStart implements the EVMLogger interface to initialize the tracing operation. -func (l *StructLogger) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - l.env = env -} - -// CaptureState logs a new structured log message and pushes it out to the environment -// -// CaptureState also tracks SLOAD/SSTORE ops to track storage change. -func (l *StructLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - // If tracing was interrupted, set the error and stop - if l.interrupt.Load() { - return - } - // check if already accumulated the specified number of logs - if l.cfg.Limit != 0 && l.cfg.Limit <= len(l.logs) { - return - } - - memory := scope.Memory - stack := scope.Stack - contract := scope.Contract - // Copy a snapshot of the current memory state to a new buffer - var mem []byte - if l.cfg.EnableMemory { - mem = make([]byte, len(memory.Data())) - copy(mem, memory.Data()) - } - // Copy a snapshot of the current stack state to a new buffer - var stck []uint256.Int - if !l.cfg.DisableStack { - stck = make([]uint256.Int, len(stack.Data())) - for i, item := range stack.Data() { - stck[i] = item - } - } - stackData := stack.Data() - stackLen := len(stackData) - // Copy a snapshot of the current storage to a new container - var storage Storage - if !l.cfg.DisableStorage && (op == vm.SLOAD || op == vm.SSTORE) { - // initialise new changed values storage container for this contract - // if not present. - if l.storage[contract.Address()] == nil { - l.storage[contract.Address()] = make(Storage) - } - // capture SLOAD opcodes and record the read entry in the local storage - if op == vm.SLOAD && stackLen >= 1 { - var ( - address = common.Hash(stackData[stackLen-1].Bytes32()) - value = l.env.StateDB.GetState(contract.Address(), address) - ) - l.storage[contract.Address()][address] = value - storage = l.storage[contract.Address()].Copy() - } else if op == vm.SSTORE && stackLen >= 2 { - // capture SSTORE opcodes and record the written entry in the local storage. - var ( - value = common.Hash(stackData[stackLen-2].Bytes32()) - address = common.Hash(stackData[stackLen-1].Bytes32()) - ) - l.storage[contract.Address()][address] = value - storage = l.storage[contract.Address()].Copy() - } - } - var rdata []byte - if l.cfg.EnableReturnData { - rdata = make([]byte, len(rData)) - copy(rdata, rData) - } - // create a new snapshot of the EVM. - log := StructLog{pc, op, gas, cost, mem, memory.Len(), stck, rdata, storage, depth, l.env.StateDB.GetRefund(), err} - l.logs = append(l.logs, log) -} - -// CaptureFault implements the EVMLogger interface to trace an execution fault -// while running an opcode. -func (l *StructLogger) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { -} - -// CaptureEnd is called after the call finishes to finalize the tracing. -func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { - l.output = output - l.err = err - if l.cfg.Debug { - fmt.Printf("%#x\n", output) - if err != nil { - fmt.Printf(" error: %v\n", err) - } - } -} - -func (l *StructLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { -} - -// CaptureStateAfter for special needs, tracks SSTORE ops and records the storage change. -func (l *StructLogger) CaptureStateAfter(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { -} - -func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { -} - -func (l *StructLogger) GetResult() (json.RawMessage, error) { - // Tracing aborted - if l.reason != nil { - return nil, l.reason - } - failed := l.err != nil - returnData := common.CopyBytes(l.output) - // Return data when successful and revert reason when reverted, otherwise empty. - returnVal := fmt.Sprintf("%x", returnData) - if failed && l.err != vm.ErrExecutionReverted { - returnVal = "" - } - return json.Marshal(&ExecutionResult{ - Gas: l.usedGas, - Failed: failed, - ReturnValue: returnVal, - StructLogs: formatLogs(l.StructLogs()), - L1DataFee: (*hexutil.Big)(l.ResultL1DataFee), - }) -} - -// Stop terminates execution of the tracer at the first opportune moment. -func (l *StructLogger) Stop(err error) { - l.reason = err - l.interrupt.Store(true) -} - -func (l *StructLogger) CaptureTxStart(gasLimit uint64) { - l.gasLimit = gasLimit -} - -func (l *StructLogger) CaptureTxEnd(restGas uint64) { - l.usedGas = l.gasLimit - restGas -} - -// StructLogs returns the captured log entries. -func (l *StructLogger) StructLogs() []StructLog { return l.logs } - -// Error returns the VM error captured by the trace. -func (l *StructLogger) Error() error { return l.err } - -// Output returns the VM return value captured by the trace. -func (l *StructLogger) Output() []byte { return l.output } - -// WriteTrace writes a formatted trace to the given writer -func WriteTrace(writer io.Writer, logs []StructLog) { - for _, log := range logs { - fmt.Fprintf(writer, "%-16spc=%08d gas=%v cost=%v", log.Op, log.Pc, log.Gas, log.GasCost) - if log.Err != nil { - fmt.Fprintf(writer, " ERROR: %v", log.Err) - } - fmt.Fprintln(writer) - - if len(log.Stack) > 0 { - fmt.Fprintln(writer, "Stack:") - for i := len(log.Stack) - 1; i >= 0; i-- { - fmt.Fprintf(writer, "%08d %s\n", len(log.Stack)-i-1, log.Stack[i].Hex()) - } - } - if len(log.Memory) > 0 { - fmt.Fprintln(writer, "Memory:") - fmt.Fprint(writer, hex.Dump(log.Memory)) - } - if len(log.Storage) > 0 { - fmt.Fprintln(writer, "Storage:") - for h, item := range log.Storage { - fmt.Fprintf(writer, "%x: %x\n", h, item) - } - } - if len(log.ReturnData) > 0 { - fmt.Fprintln(writer, "ReturnData:") - fmt.Fprint(writer, hex.Dump(log.ReturnData)) - } - fmt.Fprintln(writer) - } -} - -// WriteLogs writes vm logs in a readable format to the given writer -func WriteLogs(writer io.Writer, logs []*types.Log) { - for _, log := range logs { - fmt.Fprintf(writer, "LOG%d: %x bn=%d txi=%x\n", len(log.Topics), log.Address, log.BlockNumber, log.TxIndex) - - for i, topic := range log.Topics { - fmt.Fprintf(writer, "%08d %x\n", i, topic) - } - - fmt.Fprint(writer, hex.Dump(log.Data)) - fmt.Fprintln(writer) - } -} - -type mdLogger struct { - out io.Writer - cfg *vm.LogConfig - env *vm.EVM -} - -// NewMarkdownLogger creates a logger which outputs information in a format adapted -// for human readability, and is also a valid markdown table -func NewMarkdownLogger(cfg *vm.LogConfig, writer io.Writer) *mdLogger { - l := &mdLogger{out: writer, cfg: cfg} - if l.cfg == nil { - l.cfg = &vm.LogConfig{} - } - return l -} - -func (t *mdLogger) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - t.env = env - if !create { - fmt.Fprintf(t.out, "From: `%v`\nTo: `%v`\nData: `%#x`\nGas: `%d`\nValue `%v` wei\n", - from.String(), to.String(), - input, gas, value) - } else { - fmt.Fprintf(t.out, "From: `%v`\nCreate at: `%v`\nData: `%#x`\nGas: `%d`\nValue `%v` wei\n", - from.String(), to.String(), - input, gas, value) - } - - fmt.Fprintf(t.out, ` -| Pc | Op | Cost | Stack | RStack | Refund | -|-------|-------------|------|-----------|-----------|---------| -`) -} - -// CaptureState also tracks SLOAD/SSTORE ops to track storage change. -func (t *mdLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - stack := scope.Stack - fmt.Fprintf(t.out, "| %4d | %10v | %3d |", pc, op, cost) - - if !t.cfg.DisableStack { - // format stack - var a []string - for _, elem := range stack.Data() { - a = append(a, elem.Hex()) - } - b := fmt.Sprintf("[%v]", strings.Join(a, ",")) - fmt.Fprintf(t.out, "%10v |", b) - } - fmt.Fprintf(t.out, "%10v |", t.env.StateDB.GetRefund()) - fmt.Fprintln(t.out, "") - if err != nil { - fmt.Fprintf(t.out, "Error: %v\n", err) - } -} - -func (t *mdLogger) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { - fmt.Fprintf(t.out, "\nError: at pc=%d, op=%v: %v\n", pc, op, err) -} - -func (t *mdLogger) CaptureEnd(output []byte, gasUsed uint64, err error) { - fmt.Fprintf(t.out, "\nOutput: `%#x`\nConsumed gas: `%d`\nError: `%v`\n", - output, gasUsed, err) -} - -func (t *mdLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { -} - -func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} - -func (*mdLogger) CaptureTxStart(gasLimit uint64) {} - -func (*mdLogger) CaptureTxEnd(restGas uint64) {} - -// ExecutionResult groups all structured logs emitted by the EVM -// while replaying a transaction in debug mode as well as transaction -// execution status, the amount of gas used and the return value -type ExecutionResult struct { - Gas uint64 `json:"gas"` - Failed bool `json:"failed"` - ReturnValue string `json:"returnValue"` - StructLogs []StructLogRes `json:"structLogs"` - L1DataFee *hexutil.Big `json:"l1DataFee,omitempty"` -} - -// StructLogRes stores a structured log emitted by the EVM while replaying a -// transaction in debug mode -type StructLogRes struct { - Pc uint64 `json:"pc"` - Op string `json:"op"` - Gas uint64 `json:"gas"` - GasCost uint64 `json:"gasCost"` - Depth int `json:"depth"` - Error string `json:"error,omitempty"` - Stack *[]string `json:"stack,omitempty"` - ReturnData string `json:"returnData,omitempty"` - Memory *[]string `json:"memory,omitempty"` - Storage *map[string]string `json:"storage,omitempty"` - RefundCounter uint64 `json:"refund,omitempty"` -} - -// formatLogs formats EVM returned structured logs for json output -func formatLogs(logs []StructLog) []StructLogRes { - formatted := make([]StructLogRes, len(logs)) - for index, trace := range logs { - formatted[index] = StructLogRes{ - Pc: trace.Pc, - Op: trace.Op.String(), - Gas: trace.Gas, - GasCost: trace.GasCost, - Depth: trace.Depth, - Error: trace.ErrorString(), - RefundCounter: trace.RefundCounter, - } - if trace.Stack != nil { - stack := make([]string, len(trace.Stack)) - for i, stackValue := range trace.Stack { - stack[i] = stackValue.Hex() - } - formatted[index].Stack = &stack - } - if trace.ReturnData != nil && len(trace.ReturnData) > 0 { - formatted[index].ReturnData = hexutil.Bytes(trace.ReturnData).String() - } - if trace.Memory != nil { - memory := make([]string, 0, (len(trace.Memory)+31)/32) - for i := 0; i+32 <= len(trace.Memory); i += 32 { - memory = append(memory, fmt.Sprintf("%x", trace.Memory[i:i+32])) - } - formatted[index].Memory = &memory - } - if trace.Storage != nil { - storage := make(map[string]string) - for i, storageValue := range trace.Storage { - storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) - } - formatted[index].Storage = &storage - } - } - return formatted -} diff --git a/eth/tracers/logger/logger_json.go b/eth/tracers/logger/logger_json.go deleted file mode 100644 index e4b553a48..000000000 --- a/eth/tracers/logger/logger_json.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package logger - -import ( - "encoding/json" - "io" - "math/big" - - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/common/math" - "github.com/morph-l2/go-ethereum/core/vm" -) - -type JSONLogger struct { - encoder *json.Encoder - cfg *vm.LogConfig - env *vm.EVM -} - -// NewJSONLogger creates a new EVM tracer that prints execution steps as JSON objects -// into the provided stream. -func NewJSONLogger(cfg *vm.LogConfig, writer io.Writer) *JSONLogger { - l := &JSONLogger{encoder: json.NewEncoder(writer), cfg: cfg} - if l.cfg == nil { - l.cfg = &vm.LogConfig{} - } - return l -} - -func (l *JSONLogger) CaptureStart(env *vm.EVM, from, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - l.env = env -} - -func (l *JSONLogger) CaptureFault(pc uint64, op vm.OpCode, gas uint64, cost uint64, scope *vm.ScopeContext, depth int, err error) { - // TODO: Add rData to this interface as well - l.CaptureState(pc, op, gas, cost, scope, nil, depth, err) -} - -// CaptureState outputs state information on the logger. -func (l *JSONLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { - memory := scope.Memory - stack := scope.Stack - - log := StructLog{ - Pc: pc, - Op: op, - Gas: gas, - GasCost: cost, - MemorySize: memory.Len(), - Depth: depth, - RefundCounter: l.env.StateDB.GetRefund(), - Err: err, - } - if l.cfg.EnableMemory { - log.Memory = memory.Data() - } - if !l.cfg.DisableStack { - log.Stack = stack.Data() - } - if l.cfg.EnableReturnData { - log.ReturnData = rData - } - l.encoder.Encode(log) -} - -// CaptureEnd is triggered at end of execution. -func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, err error) { - type endLog struct { - Output string `json:"output"` - GasUsed math.HexOrDecimal64 `json:"gasUsed"` - Err string `json:"error,omitempty"` - } - var errMsg string - if err != nil { - errMsg = err.Error() - } - l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), errMsg}) -} - -func (l *JSONLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { -} - -func (l *JSONLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} - -func (l *JSONLogger) CaptureTxStart(gasLimit uint64) {} - -func (l *JSONLogger) CaptureTxEnd(restGas uint64) {} diff --git a/eth/tracers/logger/logger_test.go b/eth/tracers/logger/logger_test.go deleted file mode 100644 index 4247e2311..000000000 --- a/eth/tracers/logger/logger_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package logger - -import ( - "encoding/json" - "errors" - "math/big" - "testing" - - "github.com/morph-l2/go-ethereum/common" - "github.com/morph-l2/go-ethereum/core/state" - "github.com/morph-l2/go-ethereum/core/vm" - "github.com/morph-l2/go-ethereum/params" -) - -type dummyContractRef struct { - calledForEach bool -} - -func (dummyContractRef) Address() common.Address { return common.Address{} } -func (dummyContractRef) Value() *big.Int { return new(big.Int) } -func (dummyContractRef) SetCode(common.Hash, []byte) {} -func (d *dummyContractRef) ForEachStorage(callback func(key, value common.Hash) bool) { - d.calledForEach = true -} -func (d *dummyContractRef) SubBalance(amount *big.Int) {} -func (d *dummyContractRef) AddBalance(amount *big.Int) {} -func (d *dummyContractRef) SetBalance(*big.Int) {} -func (d *dummyContractRef) SetNonce(uint64) {} -func (d *dummyContractRef) Balance() *big.Int { return new(big.Int) } - -type dummyStatedb struct { - state.StateDB -} - -func (*dummyStatedb) GetRefund() uint64 { return 1337 } -func (*dummyStatedb) GetState(_ common.Address, _ common.Hash) common.Hash { return common.Hash{} } -func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {} - -func TestStoreCapture(t *testing.T) { - var ( - logger = NewStructLogger(nil) - env = vm.NewEVM(vm.BlockContext{}, vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Tracer: logger}) - contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 100000) - ) - contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} - var index common.Hash - logger.CaptureStart(env, common.Address{}, contract.Address(), false, nil, 0, nil) - _, err := env.Interpreter().Run(contract, []byte{}, false) - if err != nil { - t.Fatal(err) - } - if len(logger.storage[contract.Address()]) == 0 { - t.Fatalf("expected exactly 1 changed value on address %x, got %d", contract.Address(), - len(logger.storage[contract.Address()])) - } - exp := common.BigToHash(big.NewInt(1)) - if logger.storage[contract.Address()][index] != exp { - t.Errorf("expected %x, got %x", exp, logger.storage[contract.Address()][index]) - } -} - -// Tests that blank fields don't appear in logs when JSON marshalled, to reduce -// logs bloat and confusion. See https://github.com/ethereum/go-ethereum/issues/24487 -func TestStructLogMarshalingOmitEmpty(t *testing.T) { - tests := []struct { - name string - log *StructLog - want string - }{ - {"empty err and no fields", &StructLog{}, - `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, - {"with err", &StructLog{Err: errors.New("this failed")}, - `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP","error":"this failed"}`}, - {"with mem", &StructLog{Memory: make([]byte, 2), MemorySize: 2}, - `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memory":"0x0000","memSize":2,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, - {"with 0-size mem", &StructLog{Memory: make([]byte, 0)}, - `{"pc":0,"op":0,"gas":"0x0","gasCost":"0x0","memSize":0,"stack":null,"depth":0,"refund":0,"opName":"STOP"}`}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - blob, err := json.Marshal(tt.log) - if err != nil { - t.Fatal(err) - } - if have, want := string(blob), tt.want; have != want { - t.Fatalf("mismatched results\n\thave: %v\n\twant: %v", have, want) - } - }) - } -} From 099f66d42280ac9dae4f03ca9effd58f6dc934d3 Mon Sep 17 00:00:00 2001 From: ryanmorphl2 <163962984+ryanmorphl2@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:03:17 +0800 Subject: [PATCH 5/5] revert hexutil --- common/hexutil/json.go | 45 ---------------------------- common/hexutil/json_test.go | 60 ------------------------------------- 2 files changed, 105 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index e0ac98f52..50db20811 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -23,8 +23,6 @@ import ( "math/big" "reflect" "strconv" - - "github.com/holiman/uint256" ) var ( @@ -32,7 +30,6 @@ var ( bigT = reflect.TypeOf((*Big)(nil)) uintT = reflect.TypeOf(Uint(0)) uint64T = reflect.TypeOf(Uint64(0)) - u256T = reflect.TypeOf((*uint256.Int)(nil)) ) // Bytes marshals/unmarshals as a JSON string with 0x prefix. @@ -228,48 +225,6 @@ func (b *Big) UnmarshalGraphQL(input interface{}) error { return err } -// U256 marshals/unmarshals as a JSON string with 0x prefix. -// The zero value marshals as "0x0". -type U256 uint256.Int - -// MarshalText implements encoding.TextMarshaler -func (b U256) MarshalText() ([]byte, error) { - u256 := (*uint256.Int)(&b) - return []byte(u256.Hex()), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (b *U256) UnmarshalJSON(input []byte) error { - // The uint256.Int.UnmarshalJSON method accepts "dec", "0xhex"; we must be - // more strict, hence we check string and invoke SetFromHex directly. - if !isString(input) { - return errNonString(u256T) - } - // The hex decoder needs to accept empty string ("") as '0', which uint256.Int - // would reject. - if len(input) == 2 { - (*uint256.Int)(b).Clear() - return nil - } - err := (*uint256.Int)(b).SetFromHex(string(input[1 : len(input)-1])) - if err != nil { - return &json.UnmarshalTypeError{Value: err.Error(), Type: u256T} - } - return nil -} - -// UnmarshalText implements encoding.TextUnmarshaler -func (b *U256) UnmarshalText(input []byte) error { - // The uint256.Int.UnmarshalText method accepts "dec", "0xhex"; we must be - // more strict, hence we check string and invoke SetFromHex directly. - return (*uint256.Int)(b).SetFromHex(string(input)) -} - -// String returns the hex encoding of b. -func (b *U256) String() string { - return (*uint256.Int)(b).Hex() -} - // Uint64 marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint64 uint64 diff --git a/common/hexutil/json_test.go b/common/hexutil/json_test.go index 7cca30095..ed7d6fad1 100644 --- a/common/hexutil/json_test.go +++ b/common/hexutil/json_test.go @@ -23,8 +23,6 @@ import ( "errors" "math/big" "testing" - - "github.com/holiman/uint256" ) func checkError(t *testing.T, input string, got, want error) bool { @@ -178,64 +176,6 @@ func TestUnmarshalBig(t *testing.T) { } } -var unmarshalU256Tests = []unmarshalTest{ - // invalid encoding - {input: "", wantErr: errJSONEOF}, - {input: "null", wantErr: errNonString(u256T)}, - {input: "10", wantErr: errNonString(u256T)}, - {input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, u256T)}, - {input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, u256T)}, - {input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, u256T)}, - {input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, u256T)}, - {input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, u256T)}, - { - input: `"0x10000000000000000000000000000000000000000000000000000000000000000"`, - wantErr: wrapTypeError(ErrBig256Range, u256T), - }, - - // valid encoding - {input: `""`, want: big.NewInt(0)}, - {input: `"0x0"`, want: big.NewInt(0)}, - {input: `"0x2"`, want: big.NewInt(0x2)}, - {input: `"0x2F2"`, want: big.NewInt(0x2f2)}, - {input: `"0X2F2"`, want: big.NewInt(0x2f2)}, - {input: `"0x1122aaff"`, want: big.NewInt(0x1122aaff)}, - {input: `"0xbBb"`, want: big.NewInt(0xbbb)}, - {input: `"0xfffffffff"`, want: big.NewInt(0xfffffffff)}, - { - input: `"0x112233445566778899aabbccddeeff"`, - want: referenceBig("112233445566778899aabbccddeeff"), - }, - { - input: `"0xffffffffffffffffffffffffffffffffffff"`, - want: referenceBig("ffffffffffffffffffffffffffffffffffff"), - }, - { - input: `"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"`, - want: referenceBig("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - }, -} - -func TestUnmarshalU256(t *testing.T) { - for _, test := range unmarshalU256Tests { - var v U256 - err := json.Unmarshal([]byte(test.input), &v) - if !checkError(t, test.input, err, test.wantErr) { - continue - } - if test.want == nil { - continue - } - want := new(uint256.Int) - want.SetFromBig(test.want.(*big.Int)) - have := (*uint256.Int)(&v) - if want.Cmp(have) != 0 { - t.Errorf("input %s: value mismatch: have %x, want %x", test.input, have, want) - continue - } - } -} - func BenchmarkUnmarshalBig(b *testing.B) { input := []byte(`"0x123456789abcdef123456789abcdef"`) for i := 0; i < b.N; i++ {