diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index 7af0e658a..9971c6728 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -25,38 +25,93 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" ) +//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"` +} + 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"` + Revertal 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 = common.Address{} + } + 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.Revertal = 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 config callTracerConfig + gasLimit uint64 interrupt uint32 // 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 @@ -75,39 +130,58 @@ func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, e // 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 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: to, + Input: common.CopyBytes(input), + Gas: gas, + 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) - } +func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, d time.Duration, err error) { + 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) { -} + // 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 > 0 { + return + } + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + return + } + switch op { + case vm.LOG0, vm.LOG1, vm.LOG2, vm.LOG3, vm.LOG4: + size := int(op - vm.LOG0) + + stack := scope.Stack + stackData := stack.Data() + + // 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()) + } -// 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) { + data := scope.Memory.GetCopy(int64(mStart.Uint64()), int64(mSize.Uint64())) + log := callLog{Address: scope.Contract.Address(), Topics: topics, Data: hexutil.Bytes(data)} + 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). @@ -117,17 +191,16 @@ func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common. } // Skip if tracing was interrupted if atomic.LoadUint32(&t.interrupt) > 0 { - t.env.Cancel() return } 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: to, + Input: common.CopyBytes(input), + Gas: gas, + Value: value, } t.callstack = append(t.callstack, call) } @@ -147,21 +220,22 @@ 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 (*callTracer) CaptureTxStart(gasLimit uint64) {} +func (t *callTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} -func (*callTracer) CaptureTxEnd(restGas uint64) {} +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`). @@ -182,6 +256,19 @@ func (t *callTracer) Stop(err error) { atomic.StoreUint32(&t.interrupt, 1) } +// 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 { return "0x" + common.Bytes2Hex(s) }