fb3a081c7e
* eth/tracers: refactor traceTx to separate out struct logging review fix Update eth/tracers/api.go Co-authored-by: Martin Holst Swende <martin@swende.se> Mv ExecutionResult type to logger package review fix impl GetResult for StructLogger make formatLogs private confused exit and end.. account for intrinsicGas in structlogger, fix TraceCall test Add Stop method to logger Simplify traceTx Fix test rm logger from blockchain test account for refund in structLogger * use tx hooks in struct logger * minor * avoid executionResult in struct logger * revert blockchain test changes
465 lines
15 KiB
Go
465 lines
15 KiB
Go
// Copyright 2015 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/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/common/math"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/holiman/uint256"
|
|
)
|
|
|
|
// 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)
|
|
for key, value := range s {
|
|
cpy[key] = value
|
|
}
|
|
return cpy
|
|
}
|
|
|
|
// Config are the configuration options for structured logger the EVM
|
|
type Config struct {
|
|
EnableMemory bool // enable memory capture
|
|
DisableStack bool // disable stack capture
|
|
DisableStorage bool // disable storage capture
|
|
EnableReturnData bool // enable return data capture
|
|
Debug bool // print output during capture end
|
|
Limit int // maximum length of output, but zero means unlimited
|
|
// Chain overrides, can be used to execute a trace using future fork rules
|
|
Overrides *params.ChainConfig `json:"overrides,omitempty"`
|
|
}
|
|
|
|
//go:generate 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
|
|
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 Config
|
|
env *vm.EVM
|
|
|
|
storage map[common.Address]Storage
|
|
logs []StructLog
|
|
output []byte
|
|
err error
|
|
gasLimit uint64
|
|
usedGas uint64
|
|
|
|
interrupt uint32 // Atomic flag to signal execution interruption
|
|
reason error // Textual reason for the interruption
|
|
}
|
|
|
|
// NewStructLogger returns a new logger
|
|
func NewStructLogger(cfg *Config) *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 atomic.LoadUint32(&l.interrupt) > 0 {
|
|
l.env.Cancel()
|
|
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, t time.Duration, err error) {
|
|
l.output = output
|
|
l.err = err
|
|
if l.cfg.Debug {
|
|
fmt.Printf("0x%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) {
|
|
}
|
|
|
|
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
|
|
atomic.StoreUint32(&l.interrupt, 1)
|
|
}
|
|
|
|
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 *Config
|
|
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 *Config, writer io.Writer) *mdLogger {
|
|
l := &mdLogger{out: writer, cfg: cfg}
|
|
if l.cfg == nil {
|
|
l.cfg = &Config{}
|
|
}
|
|
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: `0x%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: `0x%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, tm time.Duration, err error) {
|
|
fmt.Fprintf(t.out, "\nOutput: `0x%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"`
|
|
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.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
|
|
}
|