Skip to main content

TraceResult

Complete execution trace result returned by debug_traceTransaction and debug_traceCall. Contains either opcode-level logs or call tree depending on tracer configuration.

Overview

TraceResult is the top-level response from Geth’s debug tracing methods. It provides execution outcome (gas used, success/failure, return data) plus optional detailed trace data.

Type Definition

type TraceResultType = {
  readonly gas: Uint256Type;                  // Total gas used
  readonly failed: boolean;                   // Whether execution failed
  readonly returnValue: Uint8Array;           // Return data or revert data
  readonly structLogs?: readonly StructLogType[]; // Opcode trace (default tracer)
  readonly callTrace?: CallTraceType;         // Call tree (callTracer)
};

Usage

Creating TraceResults

import * as TraceResult from '@tevm/primitives/TraceResult';

// Successful execution with structLogs
const successResult = TraceResult.from({
  gas: 50000n,
  failed: false,
  returnValue: new Uint8Array([0x00, 0x00, 0x00, 0x01]),
  structLogs: [/* ... opcode logs ... */],
});

// Failed execution
const failedResult = TraceResult.from({
  gas: 100000n,
  failed: true,
  returnValue: new Uint8Array(), // Revert data
});

// With call trace (callTracer)
const callTraceResult = TraceResult.from({
  gas: 75000n,
  failed: false,
  returnValue: new Uint8Array([0x01]),
  callTrace: /* ... CallTrace ... */,
});

Accessing Trace Data

import * as TraceResult from '@tevm/primitives/TraceResult';

// Get structured logs (default tracer)
const logs = TraceResult.getStructLogs(result);
if (logs.length > 0) {
  console.log(`${logs.length} opcode executions`);
}

// Get call trace (callTracer)
const trace = TraceResult.getCallTrace(result);
if (trace) {
  console.log(`Root call: ${trace.type}`);
  const allCalls = CallTrace.flatten(trace);
  console.log(`Total calls: ${allCalls.length}`);
}

RPC Usage

debug_traceTransaction

import * as TraceConfig from '@tevm/primitives/TraceConfig';
import * as TraceResult from '@tevm/primitives/TraceResult';

// Default tracer (opcode logs)
const config = TraceConfig.from({
  disableStorage: true,
  disableMemory: true,
});

const response = await rpc.debug_traceTransaction(txHash, config);
const result = TraceResult.from({
  gas: BigInt(response.gas),
  failed: response.failed,
  returnValue: hexToBytes(response.returnValue),
  structLogs: response.structLogs?.map(log => StructLog.from(log)),
});

// Check execution status
if (result.failed) {
  console.error("Transaction failed");
  // Analyze structLogs to find failure point
  const logs = TraceResult.getStructLogs(result);
  const revert = logs.find(log => log.op === "REVERT");
  if (revert) {
    console.error(`Reverted at PC ${revert.pc}`);
  }
}

debug_traceCall

// Trace a call without mining
const config = TraceConfig.withTracer({}, "callTracer");

const response = await rpc.debug_traceCall(
  {
    from: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    data: "0xa9059cbb...", // transfer(address,uint256)
    value: "0x0",
  },
  "latest",
  config
);

const result = TraceResult.from({
  gas: BigInt(response.gas),
  failed: response.failed,
  returnValue: hexToBytes(response.returnValue),
  callTrace: response as CallTraceType,
});

Common Patterns

Analyzing Failed Transactions

function analyzeFailed(result: TraceResultType): {
  revertLocation?: number;
  revertReason?: string;
  gasUsed: bigint;
} {
  if (!result.failed) {
    return { gasUsed: result.gas };
  }

  // Try to find revert in opcode trace
  const logs = TraceResult.getStructLogs(result);
  const revert = logs.find(log => log.op === "REVERT" || log.error);

  // Try to find revert in call trace
  const trace = TraceResult.getCallTrace(result);
  const failedCall = trace && CallTrace.flatten(trace).find(CallTrace.hasError);

  return {
    revertLocation: revert?.pc,
    revertReason: failedCall?.revertReason,
    gasUsed: result.gas,
  };
}

Gas Profiling

function profileGas(result: TraceResultType): {
  total: bigint;
  byOpcode?: Map<string, bigint>;
  byCall?: Map<string, bigint>;
} {
  const analysis = { total: result.gas } as any;

  // Opcode-level profiling
  const logs = TraceResult.getStructLogs(result);
  if (logs.length > 0) {
    const byOpcode = new Map<string, bigint>();
    for (const log of logs) {
      const current = byOpcode.get(log.op) ?? 0n;
      byOpcode.set(log.op, current + log.gasCost);
    }
    analysis.byOpcode = byOpcode;
  }

  // Call-level profiling
  const trace = TraceResult.getCallTrace(result);
  if (trace) {
    const byCall = new Map<string, bigint>();
    for (const call of CallTrace.flatten(trace)) {
      const key = `${call.type} ${call.to ? call.to.toHex() : "CREATE"}`;
      const current = byCall.get(key) ?? 0n;
      byCall.set(key, current + call.gasUsed);
    }
    analysis.byCall = byCall;
  }

  return analysis;
}

Finding Expensive Operations

function findExpensiveOps(result: TraceResultType, threshold: bigint): StructLogType[] {
  const logs = TraceResult.getStructLogs(result);
  return logs.filter(log => log.gasCost >= threshold);
}

Extracting Storage Changes

function extractStorageChanges(result: TraceResultType): Record<string, string> {
  const logs = TraceResult.getStructLogs(result);
  const changes: Record<string, string> = {};

  for (const log of logs) {
    if (log.storage) {
      Object.assign(changes, log.storage);
    }
  }

  return changes;
}

Call Flow Visualization

function visualizeCallFlow(result: TraceResultType): string {
  const trace = TraceResult.getCallTrace(result);
  if (!trace) return "No call trace available";

  function formatCall(call: CallTraceType, depth: number): string[] {
    const indent = "  ".repeat(depth);
    const status = CallTrace.hasError(call) ? "❌" : "✅";
    const to = call.to ? call.to.toHex().slice(0, 10) : "CREATE";

    const lines = [
      `${indent}${status} ${call.type} ${to} (${call.gasUsed} gas)`
    ];

    if (call.error) {
      lines.push(`${indent}   Error: ${call.error}`);
    }

    for (const subcall of CallTrace.getCalls(call)) {
      lines.push(...formatCall(subcall, depth + 1));
    }

    return lines;
  }

  return formatCall(trace, 0).join("\n");
}

Tracer-Specific Results

Default Tracer (structLogs)

const config = TraceConfig.from({});
const result = await rpc.debug_traceTransaction(txHash, config);

// Result contains structLogs
const logs = TraceResult.getStructLogs(result);
for (const log of logs) {
  console.log(`${log.pc}: ${log.op} (${log.gasCost} gas)`);
}

callTracer

const config = TraceConfig.withTracer({}, "callTracer");
const result = await rpc.debug_traceTransaction(txHash, config);

// Result contains callTrace
const trace = TraceResult.getCallTrace(result);
if (trace) {
  console.log(`${trace.type} from ${trace.from.toHex()}`);
}

prestateTracer

const config = TraceConfig.withTracer({}, "prestateTracer");
const result = await rpc.debug_traceTransaction(txHash, config);

// Result is account state map (not in TraceResult schema)
// Custom parsing needed for prestateTracer

Performance Considerations

Memory Usage

  • structLogs can be very large (10k+ entries for complex transactions)
  • Each log contains stack/memory/storage snapshots
  • Use TraceConfig.disableAll() for minimal memory

Processing Time

  • Opcode-level tracing is slow (10-100x slower than normal execution)
  • callTracer is faster (only tracks calls, not every opcode)
  • Use timeouts in TraceConfig for long-running traces

Best Practices

// Fast: Just need to know if it failed
const minimalConfig = TraceConfig.disableAll();

// Medium: Need call tree
const callConfig = TraceConfig.withTracer(
  TraceConfig.disableAll(),
  "callTracer"
);

// Slow: Need every opcode
const fullConfig = TraceConfig.from({
  enableMemory: true,
  enableReturnData: true,
});

Security Analysis

Reentrancy Detection

function detectReentrancy(result: TraceResultType): boolean {
  const trace = TraceResult.getCallTrace(result);
  if (!trace) return false;

  const visited = new Set<string>();

  function check(call: CallTraceType): boolean {
    if (!call.to) return false;

    const addr = call.to.toHex();
    if (visited.has(addr)) return true;

    visited.add(addr);
    for (const subcall of CallTrace.getCalls(call)) {
      if (check(subcall)) return true;
    }
    visited.delete(addr);

    return false;
  }

  return check(trace);
}

SELFDESTRUCT Detection

function findSelfDestructs(result: TraceResultType): AddressType[] {
  const trace = TraceResult.getCallTrace(result);
  if (!trace) return [];

  return CallTrace.flatten(trace)
    .filter(call => call.type === "SELFDESTRUCT")
    .map(call => call.from);
}

See Also