Skip to main content

Overview

The tracer is Jolt’s RISC-V emulator that executes guest programs and produces detailed execution traces. It serves as the first stage in the proving pipeline, converting a compiled RISC-V ELF binary into a sequence of Cycle structs that capture every instruction executed.

Role in the Proving System

The tracer is the foundation of Jolt’s zkVM:
  1. Input: Takes a RISC-V ELF binary (RV64IMAC instruction set) and program inputs
  2. Execution: Emulates the program instruction-by-instruction, tracking all state changes
  3. Output: Produces a Vec<Cycle> execution trace and final memory state
  4. Handoff: The execution trace becomes input to witness generation in the next stage

Architecture

Core Components

Emulator (tracer/src/emulator/)
  • Full RISC-V CPU emulation with registers, memory management unit (MMU), and program counter
  • Supports both 32-bit and 64-bit XLEN modes
  • Handles compressed (16-bit) and standard (32-bit) instruction formats
Instruction Set (tracer/src/instruction/)
  • Complete RV64IMAC instruction implementations
  • Virtual instruction extensions for optimized cryptographic operations (via jolt-inlines)
  • Instruction normalization and decoding
Memory Management
  • Memory layout configuration for inputs, outputs, bytecode, and advice regions
  • Address remapping for efficient constraint checking
  • I/O device (JoltDevice) tracking program inputs, outputs, panic state

Execution Trace Format

Each Cycle in the trace represents a single instruction execution and contains:
  • Program counter (PC)
  • Instruction opcode and operands
  • Register reads and writes
  • Memory accesses (address, value)
  • Instruction-specific metadata (e.g., virtual sequence remaining for inlined instructions)

Lazy Tracing

Jolt uses lazy trace generation to reduce memory overhead:
pub struct LazyTraceIterator {
    lazy_tracer: CheckpointingTracer,
}
  • Traces are generated on-demand via iterator pattern
  • Checkpointing support for parallel proving
  • Avoids materializing the entire trace in memory upfront

Checkpointing

For large programs, the tracer can save periodic checkpoints:
  • Each checkpoint captures emulator state at a specific cycle count
  • Enables parallel proof generation by splitting trace into chunks
  • Checkpoints can independently regenerate their trace segment

Bytecode Preprocessing

The tracer preprocesses bytecode for efficient constraint checking: PC Mapping (bytecode/mod.rs)
  • Maps physical instruction addresses to virtual PC values
  • Handles inline instruction sequences (multi-cycle operations)
  • Prepends a no-op instruction at PC=0 for padding
Alignment
  • Instructions must be aligned to ALIGNMENT_FACTOR_BYTECODE boundaries
  • Pads bytecode to next power of 2 for efficient polynomial encoding

Integration with Proving System

The execution trace flows into witness generation:
  1. Trace → Witness Polynomials:
    • PC values → bytecode lookup polynomials
    • Register reads/writes → register checking polynomials
    • Memory accesses → RAM checking polynomials
    • Instruction flags → instruction lookup polynomials
  2. Memory State:
    • Initial memory: bytecode + inputs + advice
    • Final memory: outputs + termination/panic bits
    • Used to derive RAM checking constraints

Key Files

  • tracer/src/lib.rs: Main trace generation functions (trace, trace_lazy, trace_checkpoints)
  • tracer/src/emulator/: RISC-V CPU emulator implementation
  • tracer/src/instruction/: Instruction set implementations
  • jolt-core/src/zkvm/bytecode/: Bytecode preprocessing and PC mapping

Performance Considerations

  • Lazy evaluation: Traces generated on-demand reduce peak memory
  • Checkpointing overhead: Tracking memory state adds ~10-20% overhead
  • Parallel proving: Checkpoints enable splitting large traces across workers
  • ELF decoding: Bytecode extraction happens once during setup

Next Steps

Build docs developers (and LLMs) love