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/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/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..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" @@ -127,6 +129,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) @@ -162,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 @@ -214,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 @@ -340,6 +358,14 @@ func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) { } +func (l *StructLogger) CaptureTxStart(gasLimit uint64) { + l.gasLimit = gasLimit +} + +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{} { return l.statesAffected @@ -367,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 { @@ -487,6 +540,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)) @@ -511,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/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..ba40a0f85 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()) @@ -921,75 +917,58 @@ 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); err != nil { + if config == nil { + config = &TraceConfig{} + } + // Default tracer is the struct logger + tracer = vm.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 { 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)) + l, ok := tracer.(*vm.StructLogger) + if ok { + l.ResultL1DataFee = result.L1DataFee } + return tracer.GetResult() } // APIs return the collection of RPC services the tracer package offers. 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/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/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 . -/* -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) }