evm-trace

repository·main·Indexed 43 days ago

https://github.com/apeworx/evm-trace

An Ethereum Virtual Machine (EVM) transaction tracing tool used to parse and reconstruct call trees and opcode traces from node RPC outputs, including Geth, Parity, and Reth. It provides utilities to convert struct logs into TraceFrame objects, build hierarchical CallTreeNode structures, generate gas reports, and visualize execution flows via get_tree_display().

Tokens
6.9K
Snippets
26
Records
37
Agent score
55%

What's inside evm-trace

  1. Understand Events and Call Order in Traces

    main

    In struct-log call trees, events (LOG0–LOG4) are included in each node's events list.

    • Ordering: event.position indicates the number of child calls preceding that event in its parent. This allows for interleaving events and calls in execution order.
    • Log Inclusion: To include logs in a callTracer request, use {"tracer": "callTracer", "tracerConfig": {"withLog": true}}.
    • Reverted Logs: Struct logs include attempted emissions even if the execution reverts. However, some clients might discard reverted logs in their callTracer output.
    • LOG0 Specifics: LOG0 events have no topics and their selector is None.
  2. Understand the VMTrace data model

    main

    The VMTrace model represents the complete execution state of an EVM transaction. It is composed of:

    • VMTrace: The root container containing the contract code (HexBytes) and a list of ops (VMOperation).
    • VMOperation: An individual instruction at a specific pc (program counter) with an associated cost (gas). It may contain:
      • ex (VMExecutedOperation): Details about what the operation did (gas used, stack changes, memory/storage changes).
      • sub (VMTrace): A subordinate trace for sub-calls (e.g., CALL, CREATE).
      • op (str): The opcode name.
    • VMExecutedOperation: Contains used (remaining gas), push (items added to stack), mem (MemoryDiff), and store (StorageDiff).
    • MemoryDiff: Represents a change at a specific off (offset) with data (HexBytes).
    • StorageDiff: Represents a change to a key (uint256) with a new val (uint256).
  3. Understand the VMTraceFrame data model

    main

    A VMTraceFrame is a synthetic snapshot of the EVM state at a specific instruction step. It is used for replaying traces in a format compatible with Geth-style debuggers.

    Fields:

    • address (str): The contract address being executed.
    • pc (int): The program counter.
    • op (str): The opcode name.
    • depth (int): The call depth.
    • stack (list[int]): The current stack values.
    • memory (bytes | memoryview): The current memory state.
    • storage (dict[int, int]): The current storage state (mapping of key to value).
  4. How TreeRepresentation formats node titles

    main

    The title property of a TreeRepresentation object dynamically formats information based on the type of node being displayed:

    1. EventNodes: Displays the CallType and the hex-encoded selector (e.g., LOG: 0xabcdef12).
    2. CallTreeNodes:
      • Address: Displays the checksummed Ethereum address.
      • Method ID: If the call is not a CREATE or SELFDESTRUCT type, it extracts the first 4 bytes of calldata and displays it as <0x...>.
      • Gas Cost: If available, appends the gas used in brackets, e.g., [500 gas].
      • Reversion: If the root call failed, it prefixes the title with [reverted].

    Example title format: CALL: 0x71C...<0x12345678> [1200 gas]

  5. Process Parity Style Traces

    main

    If your node supports the trace_transaction RPC, you can retrieve trace objects and validate them using ParityTraceList.model_validate(raw_trace_list). To build a call-tree from these traces, use get_calltree_from_parity_trace.

    from evm_trace import CallType, ParityTraceList, get_calltree_from_parity_trace
    
    # Get raw trace list from node
    raw_trace_list = web3.manager.request_blocking("trace_transaction", [txn_hash])
    # Validate as ParityTraceList
    trace_list = ParityTraceList.model_validate(raw_trace_list)
    # Create call-tree
    tree = get_calltree_from_parity_trace(trace_list)
  6. Process Geth Style Traces

    main

    If your node supports the debug_traceTransaction RPC, you can use web3.py to retrieve struct logs and convert them into TraceFrame objects using TraceFrame.model_validate(item).

    from web3 import HTTPProvider, Web3
    from evm_trace import TraceFrame
    
    web3 = Web3(HTTPProvider("https://path.to.my.node"))
    txn_hash = "0x..."
    struct_logs = web3.manager.request_blocking("debug_traceTransaction", [txn_hash]).structLogs
    for item in struct_logs:
        frame = TraceFrame.model_validate(item)
  7. Build a call-tree from Geth Style Traces

    main

    To reconstruct a call-tree from Geth struct logs, use get_calltree_from_geth_trace. You must provide a TraceFrame (the trace) and a dictionary of root node arguments including gas_cost, gas_limit, address, calldata, value, and call_type.

    from evm_trace import CallType, get_calltree_from_geth_trace
    
    root_node_kwargs = {
        "gas_cost": 10000000,
        "gas_limit": 10000000000,
        "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
        "calldata": "0x00",
        "value": 1000,
        "call_type": CallType.CALL,
    }
    
    # Assuming 'trace' is a TraceFrame obtained from Geth logs
    calltree = get_calltree_from_geth_trace(trace, **root_node_kwargs)
  8. Generate Gas Reports

    main

    You can generate a gas report from a call-tree using get_gas_report(calltree).

    To combine multiple reports into one, use merge_reports. This method accepts a list of Dict[Any, Dict[Any, List[int]]] where the inner List[int] represents the gas used. You can customize the keys (the Any types) to represent addresses, token names, or method selectors.

    from evm_trace.gas import get_gas_report, merge_reports
    
    # Assuming 'calltree' was created previously
    gas_report = get_gas_report(calltree)
    
    # To combine multiple reports:
    # combined_report = merge_reports([report1, report2])
  9. Convert Compressed VM Traces

    main

    For clients supporting trace_replayTransaction with ["vmTrace"], you can process the opcode trace (containing stack and memory deltas) using evm_trace.vmtrace.

    Use from_rpc_response(response_bytes) to parse the raw JSON-RPC response and to_trace_frames(vm_trace, address=...) to iterate through frames.

    Note: These synthetic frames are not complete TraceFrame substitutes; they do not expose gas, refund, or error fields, and storage only contains deltas reported in that specific call.

    from evm_trace.vmtrace import from_rpc_response, to_trace_frames
    
    # response_bytes is the complete JSON-RPC response, encoded as bytes.
    vm_trace = from_rpc_response(response_bytes)
    for frame in to_trace_frames(vm_trace, address=transaction_to):
        print(frame.pc, frame.op, frame.stack)
  10. Handle incomplete traces with IncompleteTraceError

    main
    If a VMTrace is missing critical information required to reconstruct the execution state (such as missing bytecode when an opcode name cannot be recovered, or missing results for specific opcodes like CALL), the library raises an IncompleteTraceError. This error inherits from ValueError.