Skip to main content
Jolt zkVM follows a guest/host architecture that separates the program being proven (guest) from the environment that generates and verifies proofs (host).

Guest Programs

Guest programs are RISC-V binaries that execute inside the zkVM. These programs:
  • Are compiled to RISC-V (RV64IMAC) instruction set
  • Run in a constrained environment with limited memory
  • Cannot directly perform I/O operations
  • Have all execution traced and proven

Compilation Targets

Guest code compiles to different targets depending on the mode:
#![cfg_attr(feature = "guest", no_std)]
Feature flags:
  • guest - Compiles for RISC-V target, runs in zkVM
  • guest-std - Enables std library support in guest (uses custom allocator)
  • Without guest features - Compiles for native execution

Memory Layout

Guests have a structured memory space defined by MemoryLayout:
  • Program code - Read-only executable instructions
  • Stack - Grows downward, configurable size
  • Heap - Dynamic allocation, configurable size
  • Input region - Public inputs from host
  • Output region - Results written back to host
  • Advice regions - Trusted and untrusted advice data
pub struct MemoryLayout {
    pub max_input_size: u64,
    pub max_output_size: u64,
    pub max_untrusted_advice_size: u64,
    pub max_trusted_advice_size: u64,
    pub stack_size: u64,
    pub heap_size: u64,
    pub program_size: u64,
    // ... derived addresses
}

Host Environment

The host is the native (x86/ARM) environment that:
  • Compiles guest programs to RISC-V
  • Executes the guest in the tracer/emulator
  • Generates zero-knowledge proofs
  • Verifies proofs
  • Manages memory and I/O

Host-Only Operations

These operations only exist in the host (feature-gated with #[cfg(not(feature = "guest"))]): Program management:
use jolt::host::Program;

let mut program = Program::new("guest-name");
program.set_func("my_function");
program.build(&target_dir);
Preprocessing:
let shared_preprocessing = JoltSharedPreprocessing::new(
    bytecode,
    memory_layout,
    memory_init,
    max_trace_length,
);

let prover_preprocessing = JoltProverPreprocessing::new(shared_preprocessing);
Proving:
let (output, proof, program_io) = prove_my_function(
    program,
    preprocessing,
    public_input,
);
Verification:
let is_valid = verify_my_function(
    public_input,
    output,
    panic_flag,
    proof,
);

Communication Between Guest and Host

Public Inputs and Outputs

Data flows through serialized memory regions: Host side:
// Inputs are serialized and placed in guest memory
io_device.inputs.append(&mut postcard::to_stdvec(&input).unwrap());

// Outputs are read after execution
let output = postcard::from_bytes::<OutputType>(&io_device.outputs).unwrap();
Guest side:
#[jolt::provable]
fn my_function(x: u32, y: u64) -> u128 {
    // x and y are deserialized from input region
    let result = compute(x, y);
    // result is serialized to output region
    result
}

Advice (Non-Deterministic Hints)

Guests can receive additional data via the advice mechanism (see Runtime Advice).

Dual-Mode Execution

Many functions in Jolt support both guest and host execution:
#[jolt::provable]
fn fibonacci(n: u32) -> u128 {
    // This code runs in BOTH environments:
    // - Native execution (testing, development)
    // - RISC-V guest (when proving)
    let mut a = 0u128;
    let mut b = 1u128;
    for _ in 1..n {
        let sum = a + b;
        a = b;
        b = sum;
    }
    b
}
In host mode: Compiles to native code, runs directly In guest mode: Compiles to RISC-V, executes in zkVM

Example: Complete Guest/Host Flow

Guest (guest/src/lib.rs):
#![cfg_attr(feature = "guest", no_std)]

#[jolt::provable(heap_size = 32768, max_trace_length = 65536)]
fn muldiv(a: u32, b: u32, c: u32) -> u32 {
    a * b / c
}
Host (src/main.rs):
use guest::*;

fn main() {
    // Compile guest to RISC-V
    let mut program = compile_muldiv("/tmp/jolt-guest");
    
    // Preprocess
    let shared = preprocess_shared_muldiv(&mut program);
    let preprocessing = preprocess_prover_muldiv(shared);
    
    // Build prover closure
    let prove = build_prover_muldiv(program, preprocessing);
    
    // Prove execution
    let (result, proof, io) = prove(10, 20, 5);
    
    println!("Result: {}", result); // 40
    println!("Proof size: {} bytes", proof.size().unwrap());
}

Key Takeaways

Guest = Proven

Guest code runs in the zkVM and every instruction is traced and proven

Host = Prover/Verifier

Host code manages compilation, proof generation, and verification

Memory Isolation

Guests have isolated memory with explicit I/O regions

Dual Compilation

Same Rust code can compile for both native (host) and RISC-V (guest)

Build docs developers (and LLMs) love