go-ethereum/eth/tracers/native/call.go
Alvaro Sevilla b1cec853be
eth/tracers: add position field for callTracer logs ()
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>
2023-11-03 10:28:27 +01:00

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)
}
}