eth/tracers: refactor traceTx to separate out struct logging (#24326)

* 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
This commit is contained in:
Sina Mahmoodi 2022-04-06 09:34:18 +02:00 committed by GitHub
parent 0654014652
commit fb3a081c7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 139 deletions

View File

@ -862,71 +862,46 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
// executes the given message in the provided environment. The return value will // executes the given message in the provided environment. The return value will
// be tracer dependent. // be tracer dependent.
func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig) (interface{}, error) { func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Context, vmctx vm.BlockContext, statedb *state.StateDB, config *TraceConfig) (interface{}, error) {
// Assemble the structured logger or the JavaScript tracer
var ( var (
tracer vm.EVMLogger tracer Tracer
err error err error
timeout = defaultTraceTimeout
txContext = core.NewEVMTxContext(message) txContext = core.NewEVMTxContext(message)
) )
switch { if config == nil {
case config == nil: config = &TraceConfig{}
tracer = logger.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 {
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 = logger.NewStructLogger(config.Config)
} }
// Default tracer is the struct logger
tracer = logger.NewStructLogger(config.Config)
if config.Tracer != nil {
tracer, err = New(*config.Tracer, txctx)
if err != nil {
return nil, err
}
}
// 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"))
}
}()
defer cancel()
// Run the transaction with tracing enabled. // Run the transaction with tracing enabled.
vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true}) vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
// Call Prepare to clear out the statedb access list // Call Prepare to clear out the statedb access list
statedb.Prepare(txctx.TxHash, txctx.TxIndex) statedb.Prepare(txctx.TxHash, txctx.TxIndex)
if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas())); err != nil {
result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()))
if err != nil {
return nil, fmt.Errorf("tracing failed: %w", err) return nil, fmt.Errorf("tracing failed: %w", err)
} }
return tracer.GetResult()
// Depending on the tracer type, format and return the output.
switch tracer := tracer.(type) {
case *logger.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 &ethapi.ExecutionResult{
Gas: result.UsedGas,
Failed: result.Failed(),
ReturnValue: returnVal,
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
}, nil
case Tracer:
return tracer.GetResult()
default:
panic(fmt.Sprintf("bad tracer type %T", tracer))
}
} }
// APIs return the collection of RPC services the tracer package offers. // APIs return the collection of RPC services the tracer package offers.

View File

@ -39,6 +39,7 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
@ -213,11 +214,11 @@ func TestTraceCall(t *testing.T) {
}, },
config: nil, config: nil,
expectErr: nil, expectErr: nil,
expect: &ethapi.ExecutionResult{ expect: &logger.ExecutionResult{
Gas: params.TxGas, Gas: params.TxGas,
Failed: false, Failed: false,
ReturnValue: "", ReturnValue: "",
StructLogs: []ethapi.StructLogRes{}, StructLogs: []logger.StructLogRes{},
}, },
}, },
// Standard JSON trace upon the head, plain transfer. // Standard JSON trace upon the head, plain transfer.
@ -230,11 +231,11 @@ func TestTraceCall(t *testing.T) {
}, },
config: nil, config: nil,
expectErr: nil, expectErr: nil,
expect: &ethapi.ExecutionResult{ expect: &logger.ExecutionResult{
Gas: params.TxGas, Gas: params.TxGas,
Failed: false, Failed: false,
ReturnValue: "", ReturnValue: "",
StructLogs: []ethapi.StructLogRes{}, StructLogs: []logger.StructLogRes{},
}, },
}, },
// Standard JSON trace upon the non-existent block, error expects // Standard JSON trace upon the non-existent block, error expects
@ -259,11 +260,11 @@ func TestTraceCall(t *testing.T) {
}, },
config: nil, config: nil,
expectErr: nil, expectErr: nil,
expect: &ethapi.ExecutionResult{ expect: &logger.ExecutionResult{
Gas: params.TxGas, Gas: params.TxGas,
Failed: false, Failed: false,
ReturnValue: "", ReturnValue: "",
StructLogs: []ethapi.StructLogRes{}, StructLogs: []logger.StructLogRes{},
}, },
}, },
// Standard JSON trace upon the pending block // Standard JSON trace upon the pending block
@ -276,11 +277,11 @@ func TestTraceCall(t *testing.T) {
}, },
config: nil, config: nil,
expectErr: nil, expectErr: nil,
expect: &ethapi.ExecutionResult{ expect: &logger.ExecutionResult{
Gas: params.TxGas, Gas: params.TxGas,
Failed: false, Failed: false,
ReturnValue: "", ReturnValue: "",
StructLogs: []ethapi.StructLogRes{}, StructLogs: []logger.StructLogRes{},
}, },
}, },
} }
@ -299,8 +300,12 @@ func TestTraceCall(t *testing.T) {
t.Errorf("Expect no error, get %v", err) t.Errorf("Expect no error, get %v", err)
continue continue
} }
if !reflect.DeepEqual(result, testspec.expect) { var have *logger.ExecutionResult
t.Errorf("Result mismatch, want %v, get %v", testspec.expect, result) if err := json.Unmarshal(result.(json.RawMessage), &have); err != nil {
t.Errorf("failed to unmarshal result %v", err)
}
if !reflect.DeepEqual(have, testspec.expect) {
t.Errorf("Result mismatch, want %v, get %v", testspec.expect, have)
} }
} }
} }
@ -329,11 +334,15 @@ func TestTraceTransaction(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Failed to trace transaction %v", err) t.Errorf("Failed to trace transaction %v", err)
} }
if !reflect.DeepEqual(result, &ethapi.ExecutionResult{ var have *logger.ExecutionResult
if err := json.Unmarshal(result.(json.RawMessage), &have); err != nil {
t.Errorf("failed to unmarshal result %v", err)
}
if !reflect.DeepEqual(have, &logger.ExecutionResult{
Gas: params.TxGas, Gas: params.TxGas,
Failed: false, Failed: false,
ReturnValue: "", ReturnValue: "",
StructLogs: []ethapi.StructLogRes{}, StructLogs: []logger.StructLogRes{},
}) { }) {
t.Error("Transaction tracing result is different") t.Error("Transaction tracing result is different")
} }

View File

@ -18,10 +18,12 @@ package logger
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"math/big" "math/big"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -108,10 +110,15 @@ type StructLogger struct {
cfg Config cfg Config
env *vm.EVM env *vm.EVM
storage map[common.Address]Storage storage map[common.Address]Storage
logs []StructLog logs []StructLog
output []byte output []byte
err error 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 // NewStructLogger returns a new logger
@ -142,13 +149,19 @@ func (l *StructLogger) CaptureStart(env *vm.EVM, from common.Address, to common.
// //
// CaptureState also tracks SLOAD/SSTORE ops to track storage change. // 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) { func (l *StructLogger) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) {
memory := scope.Memory // If tracing was interrupted, set the error and stop
stack := scope.Stack if atomic.LoadUint32(&l.interrupt) > 0 {
contract := scope.Contract l.env.Cancel()
return
}
// check if already accumulated the specified number of logs // check if already accumulated the specified number of logs
if l.cfg.Limit != 0 && l.cfg.Limit <= len(l.logs) { if l.cfg.Limit != 0 && l.cfg.Limit <= len(l.logs) {
return return
} }
memory := scope.Memory
stack := scope.Stack
contract := scope.Contract
// Copy a snapshot of the current memory state to a new buffer // Copy a snapshot of the current memory state to a new buffer
var mem []byte var mem []byte
if l.cfg.EnableMemory { if l.cfg.EnableMemory {
@ -221,11 +234,42 @@ func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration
func (l *StructLogger) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { 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) CaptureExit(output []byte, gasUsed uint64, err error) {
}
func (*StructLogger) CaptureTxStart(gasLimit uint64) {} 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()),
})
}
func (*StructLogger) CaptureTxEnd(restGas uint64) {} // 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. // StructLogs returns the captured log entries.
func (l *StructLogger) StructLogs() []StructLog { return l.logs } func (l *StructLogger) StructLogs() []StructLog { return l.logs }
@ -355,3 +399,66 @@ func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {}
func (*mdLogger) CaptureTxStart(gasLimit uint64) {} func (*mdLogger) CaptureTxStart(gasLimit uint64) {}
func (*mdLogger) CaptureTxEnd(restGas 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
}

View File

@ -1122,69 +1122,6 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args TransactionA
return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap())
} }
// 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 []logger.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
}
// RPCMarshalHeader converts the given header to the RPC output . // RPCMarshalHeader converts the given header to the RPC output .
func RPCMarshalHeader(head *types.Header) map[string]interface{} { func RPCMarshalHeader(head *types.Header) map[string]interface{} {
result := map[string]interface{}{ result := map[string]interface{}{