Skip to main content
The #[jolt::provable] macro is the primary way to create provable functions in Jolt. It transforms a regular Rust function into a full zkVM program with generated host-side proving and verification infrastructure.

Basic Usage

#![cfg_attr(feature = "guest", no_std)]

#[jolt::provable]
fn fibonacci(n: u32) -> u128 {
    let mut a = 0u128;
    let mut b = 1u128;
    for _ in 1..n {
        let sum = a + b;
        a = b;
        b = sum;
    }
    b
}
This generates several host-side functions:
  • compile_fibonacci(target_dir) - Compiles guest to RISC-V
  • preprocess_shared_fibonacci(program) - Shared preprocessing
  • preprocess_prover_fibonacci(shared) - Prover-specific preprocessing
  • preprocess_verifier_fibonacci(shared, generators) - Verifier-specific preprocessing
  • build_prover_fibonacci(program, preprocessing) - Returns proving closure
  • build_verifier_fibonacci(preprocessing) - Returns verification closure
  • prove_fibonacci(...) - Direct proof generation
  • analyze_fibonacci(...) - Program analysis without proving

Parameters

The macro accepts configuration parameters:

Memory Configuration

#[jolt::provable(
    heap_size = 1048576,              // 1 MB heap (default: 4096)
    stack_size = 4096,                // 4 KB stack (default: 4096)
    max_input_size = 4096,            // Max input bytes (default: 4096)
    max_output_size = 4096,           // Max output bytes (default: 4096)
    max_untrusted_advice_size = 0,    // Untrusted advice (default: 0)
    max_trusted_advice_size = 0,      // Trusted advice (default: 0)
)]
fn my_function(input: Vec<u8>) -> Vec<u8> {
    // Function body
}
Memory sizes must be powers of 2 or the linker will fail. These values determine the guest’s memory layout.

Trace Configuration

#[jolt::provable(
    max_trace_length = 1048576  // Max execution steps (default: 1048576)
)]
fn compute(x: u64) -> u64 {
    // Complex computation
}
The trace length affects:
  • Maximum program complexity
  • Preprocessing time
  • Proof generation time

Development Features

#[jolt::provable(
    backtrace = "full",  // Enable backtraces: "full" or "simple"
    profile = "release"  // Build profile: "release" or "dev"
)]
fn debug_function(x: u32) -> u32 {
    x * 2
}
Backtraces and debug profiles increase guest binary size and execution time. Use only during development.

Function Signatures

Public Inputs

Regular parameters become public inputs to the proof:
#[jolt::provable]
fn verify_hash(data: Vec<u8>, expected_hash: [u8; 32]) -> bool {
    let computed = sha256(&data);
    computed == expected_hash
}
Both data and expected_hash are public inputs that must be provided by both prover and verifier.

Trusted Advice

Wrap parameters in TrustedAdvice<T> to mark them as committed advice:
#[jolt::provable]
fn prove_knowledge(
    public_key: [u8; 32],
    secret_key: jolt::TrustedAdvice<[u8; 32]>
) -> bool {
    derive_public_key(&secret_key) == public_key
}
The prover commits to trusted advice before seeing the challenge. The verifier receives only the commitment.

Untrusted Advice

Wrap parameters in UntrustedAdvice<T> for witness data:
#[jolt::provable]
fn verify_membership(
    root: [u8; 32],
    leaf: [u8; 32],
    proof: jolt::UntrustedAdvice<Vec<[u8; 32]>>
) -> bool {
    // Verify Merkle proof
    verify_merkle_proof(root, leaf, &proof)
}
Untrusted advice is provided by the prover but must be verified by the guest.
See Runtime Advice for the difference between trusted and untrusted advice.

Return Values

Return values become public outputs:
#[jolt::provable]
fn compute_hash(data: Vec<u8>) -> [u8; 32] {
    sha256(&data)
}
Both prover and verifier know the output value. For private outputs, write to trusted advice instead.

Generated Functions

The macro generates a complete proving/verification workflow:

Compilation

let mut program = compile_my_function("/tmp/jolt-guest");
Compiles the guest function to RISC-V ELF binary. The binary is cached in the target directory.

Preprocessing

Preprocessing happens in phases:
// Phase 1: Shared preprocessing (used by both prover and verifier)
let shared = preprocess_shared_my_function(&mut program);

// Phase 2: Prover preprocessing
let prover_preprocessing = preprocess_prover_my_function(shared.clone());

// Phase 2: Verifier preprocessing
let verifier_generators = /* commitment scheme setup */;
let verifier_preprocessing = preprocess_verifier_my_function(
    shared,
    verifier_generators
);
See Preprocessing for details.

Building Closures

Create reusable proving/verification closures:
// Build prover
let prove = build_prover_my_function(program, prover_preprocessing);

// Build verifier
let verify = build_verifier_my_function(verifier_preprocessing);

// Use multiple times
let (output1, proof1, _io1) = prove(input1);
let (output2, proof2, _io2) = prove(input2);

let valid1 = verify(input1, output1, false, proof1);
let valid2 = verify(input2, output2, false, proof2);

Direct Proving

For one-off proofs:
let (output, proof, program_io) = prove_my_function(
    program,
    preprocessing,
    public_input,
    untrusted_advice,
);

Analysis

Analyze program execution without generating a proof:
let summary = analyze_my_function(input);
println!("Cycles: {}", summary.total_cycles);
println!("Memory used: {}", summary.memory_used);

Complete Example

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

#[jolt::provable(
    heap_size = 32768,
    max_trace_length = 65536,
    max_input_size = 8192,
    max_output_size = 32
)]
fn hash_chain(data: Vec<u8>, iterations: u32) -> [u8; 32] {
    let mut hash = sha256(&data);
    for _ in 0..iterations {
        hash = sha256(&hash);
    }
    hash
}
Host (src/main.rs):
use guest::*;

fn main() {
    // Compile
    let mut program = compile_hash_chain("/tmp/jolt");
    
    // Preprocess (once per program)
    let shared = preprocess_shared_hash_chain(&mut program);
    let preprocessing = preprocess_prover_hash_chain(shared);
    
    // Build prover
    let prove = build_prover_hash_chain(program, preprocessing);
    
    // Prove
    let data = b"Hello, Jolt!".to_vec();
    let iterations = 100;
    let (hash, proof, _) = prove(data.clone(), iterations);
    
    // Verify
    let verifier_preprocessing = /* ... */;
    let verify = build_verifier_hash_chain(verifier_preprocessing);
    assert!(verify(data, iterations, hash, false, proof));
}

Best Practices

Overallocation wastes proving time. Measure actual usage with analyze_* before finalizing limits.
Large public inputs increase verification time. Use advice for large witness data.
Don’t pass secrets as public inputs. Use TrustedAdvice<T> instead.
Preprocessing is expensive. Reuse the same preprocessing for multiple proofs of the same program.

Limitations

  • Function must have a determinate signature (no generics in macro itself)
  • All types must implement Serialize + Deserialize (via postcard)
  • Advice types must implement AdviceTapeIO
  • Function cannot be async or have special ABIs

Build docs developers (and LLMs) love