Cross-Contract Call Tracing
Source: crates/vm-runtime/src/context.rs, crates/vm-runtime/src/trace_*.rs
Overview
Section titled “Overview”The VM captures hierarchical call traces during contract execution. Each trace
is a tree of TraceFrame nodes --- one per cross-contract call --- with storage
operations, events, gas usage, and timing attached to the originating frame.
Tracing has zero overhead when disabled.
Data Structures
Section titled “Data Structures”TraceFrame
Section titled “TraceFrame”The core node type representing a single call in the trace tree:
| Field | Type | Description |
|---|---|---|
call_type | TraceCallType | Kind of call |
from | Vec<u8> | Caller address |
to | Vec<u8> | Callee address |
value | u128 | Value transferred |
gas_limit | Gas | Gas allocated to this call |
gas_used | Gas | Gas consumed |
input | Vec<u8> | Calldata (selector + args) |
output | Vec<u8> | Return data |
storage_ops | Vec<TraceStorageOp> | Storage reads/writes/deletes |
events | Vec<TraceLogEntry> | Emitted events |
children | Vec<TraceFrame> | Nested calls (recursive tree) |
error | Option<TraceError> | Error if call reverted |
duration_ns | u64 | Wall-clock execution time |
TraceCallType
Section titled “TraceCallType”Call | StaticCall | DelegateCall | Create | Create2TraceStorageOp
Section titled “TraceStorageOp”| Field | Type | Description |
|---|---|---|
op_type | TraceStorageOpType | Read, Write, or Delete |
key | Vec<u8> | Storage key |
value_before | Option<Vec<u8>> | Value before the operation |
value_after | Option<Vec<u8>> | Value after the operation |
gas_cost | Gas | Gas cost for this operation |
TraceLogEntry
Section titled “TraceLogEntry”| Field | Type | Description |
|---|---|---|
contract | Vec<u8> | Event emitter address |
topics | Vec<[u8; 32]> | Indexed topics |
data | Vec<u8> | Event data |
TracingConfig
Section titled “TracingConfig”| Field | Default | Description |
|---|---|---|
max_depth | 64 | Maximum call depth to trace |
Frames beyond max_depth are suppressed automatically to prevent DoS from
deeply nested calls.
Capture Mechanism
Section titled “Capture Mechanism”Tracing is driven by the TxContext during execution:
- Enable:
ctx.enable_tracing(TracingConfig { max_depth: 64 }) - Enter frame:
ctx.enter_call_frame(call_type, from, to, value, gas, input) - During execution:
ctx.trace_storage_op(op)andctx.trace_event(event) - Exit frame:
ctx.exit_call_frame(output, gas_used, error) - Extract:
ctx.take_root_trace()returns the complete call tree
The internal frame stack (TraceFrameState) maintains the current nesting. On
exit, each frame is attached to its parent’s children vector or saved as the
root frame.
Output Formats
Section titled “Output Formats”trace_format::to_json(&frame) // Compact JSONtrace_format::to_json_pretty(&frame) // Pretty-printedtrace_format::to_json_with_metadata( // With tx hash and block number &frame, Some(&tx_hash), Some(block_number))Addresses are hex-encoded with 0x prefix. Byte arrays are hex-encoded.
Chrome Trace (Visualization)
Section titled “Chrome Trace (Visualization)”trace_format::to_chrome_trace(&frame)Compatible with chrome://tracing and Perfetto. Gas used is mapped to duration
for visual sizing. Call depth maps to thread ID for visual stacking.
Human-Readable Tree
Section titled “Human-Readable Tree”trace_format::to_tree(&frame)trace_format::to_tree_with_config(&frame, &config)Example output:
|-- CALL 0xabc123...(0xdeadbeef) [1,000,000 gas]| |-- SLOAD 0x0102... [2,100 gas]| |-- SSTORE 0x0304... [5,000 gas]| |-- STATICCALL 0xdef456...(0x12345678) [50,000 gas]| | '-- RETURN: 32 bytes| |-- EVENT 0xdddd...| '-- RETURN: 4 bytes [45,230/1,000,000 gas, 4.5%]TreeConfig presets:
| Preset | Shows |
|---|---|
minimal() | Call structure only |
default() | Balanced (gas, values, errors) |
verbose() | All details (storage, events, timing) |
Trace Compression
Section titled “Trace Compression”Source: crates/vm-runtime/src/trace_compress.rs
Batch operations (airdrops, bulk transfers) produce massive traces with repeated
subtrees. The TraceCompressor performs exact subtree deduplication:
| Setting | Default | Description |
|---|---|---|
min_repetitions | 3 | Minimum repeats to trigger compression |
sample_count | 3 | Number of samples preserved in detail |
Repeated subtrees are collapsed into a Repeated node with a template frame,
repetition count, and sampled instances.
Typical compression ratios:
| Workload | Ratio |
|---|---|
| 1000-address airdrop | ~1000x |
| 100 identical swaps | ~100x |
| Mixed batch (50% unique) | ~2x |
The compressed trace can be expand()-ed back to the full tree.
Trace Filtering
Section titled “Trace Filtering”Source: crates/vm-runtime/src/trace_filter.rs
For production use, TraceFilter reduces overhead by selectively tracing:
| Filter | Description |
|---|---|
contracts(addresses) | Only trace calls to/from specific contracts |
failures_only() | Only trace reverted calls |
high_gas(threshold) | Only trace calls above a gas threshold |
max_depth(depth) | Cap trace depth |
depth_range(min, max) | Trace only a range of call depths |
| Method selectors | Filter by function selector |
min_value(amount) | Only trace calls with value transfer |
Filters apply at two points:
should_trace(ctx)--- pre-execution (skip tracing entirely)should_keep(frame)--- post-execution (prune from output)
Trace Diffing
Section titled “Trace Diffing”Source: crates/vm-runtime/src/trace_diff.rs
diff_traces(old, new) compares two traces for regression testing:
pub struct TraceDiff { pub added_frames: Vec<FramePath>, pub removed_frames: Vec<FramePath>, pub changed_frames: Vec<FrameChange>, pub gas_delta: i64,}Changes detected: callee, gas used, call type, outcome, value, input/output length, storage ops count, events count, and child count.
Transaction Summary
Section titled “Transaction Summary”Source: crates/vm-runtime/src/trace_summary.rs
TraceSummary::from_trace(&root) collapses a trace into net effects:
| Field | Description |
|---|---|
storage_deltas | Net storage changes per (contract, key) |
contracts_touched | All contracts involved |
total_gas_used | Cumulative gas |
events | All emitted events (flattened) |
storage_reads/writes/deletes | Operation counts |
max_depth | Deepest call nesting |
total_frames | Total call frames |
success | Whether the root call succeeded |
Integration
Section titled “Integration”Tracing is invoked via execute_entrypoint_with_trace() in the execution layer.
The resulting TraceFrame is attached to ExecMetadata::vm_traces during block
execution and can be retrieved per-transaction.
Related
Section titled “Related”- Gas Schedule --- storage op costs visible in traces
- Precompiles & Syscalls --- syscalls appear as trace events