b1cec853be
Currently, one can use the "withLogs" parameter to include logs in the callTracer results, which allows the user to see at which trace level was each log emitted. This commit adds a position field to the logs which determine the exact ordering of a call's logs and its subcalls. This would be useful e.g. for explorers wishing to display the flow of execution. Co-authored-by: jsvisa <delweng@gmail.com>
290 lines
8.5 KiB
Go
290 lines
8.5 KiB
Go
// 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 native
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"math/big"
|
|
"sync/atomic"
|
|
|
|
"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"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
//go:generate go run github.com/fjl/gencodec -type callFrame -field-override callFrameMarshaling -out gen_callframe_json.go
|
|
|
|
func init() {
|
|
tracers.DefaultDirectory.Register("callTracer", newCallTracer, false)
|
|
}
|
|
|
|
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 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 {
|
|
noopTracer
|
|
callstack []callFrame
|
|
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(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.
|
|
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) {
|
|
toCopy := to
|
|
t.callstack[0] = callFrame{
|
|
Type: vm.CALL,
|
|
From: from,
|
|
To: &toCopy,
|
|
Input: common.CopyBytes(input),
|
|
Gas: t.gasLimit,
|
|
Value: value,
|
|
}
|
|
if 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, 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) {
|
|
// 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 > 0 {
|
|
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)
|
|
|
|
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())
|
|
}
|
|
|
|
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 t.interrupt.Load() {
|
|
return
|
|
}
|
|
|
|
toCopy := to
|
|
call := callFrame{
|
|
Type: typ,
|
|
From: from,
|
|
To: &toCopy,
|
|
Input: common.CopyBytes(input),
|
|
Gas: gas,
|
|
Value: value,
|
|
}
|
|
t.callstack = append(t.callstack, call)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// pop call
|
|
call := t.callstack[size-1]
|
|
t.callstack = t.callstack[:size-1]
|
|
size -= 1
|
|
|
|
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
|
|
}
|
|
return json.RawMessage(res), t.reason
|
|
}
|
|
|
|
// Stop terminates execution of the tracer at the first opportune moment.
|
|
func (t *callTracer) Stop(err error) {
|
|
t.reason = err
|
|
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)
|
|
}
|
|
}
|