Invoke when the user says: "make this Jolt provable", "wrap this in Jolt", "prove this with Jolt", "add ZK proofs to this", "make this zero-knowledge", "make this provable", "jolt-ify this".
Step 1 — Identify the computation to prove
Look for a pure, deterministic Rust function — inputs in, result out, no I/O or side effects. If not obvious, ask:
"What function should I make provable? It needs to be a pure Rust function with no I/O or side effects."
Before writing any guest code, verify the target function and its entire module path are pub . If not, make it pub in the library source (preferred — we're proving the library) and confirm with the user, noting that inlining is an alternative if they'd rather not modify the library.
Step 2 — Analyze and adapt the signature
The guest has a real heap — Vec , String , alloc types work freely inside the body. The constraint is at the parameter boundary: std mode uses full serde (Vec/String as params fine); no_std uses serde_core (no Vec params, arrays capped at size 32). Only adapt what's necessary:
Issue Resolution
Vec<T> param in no_std [T; N], len: u32 — or switch to std mode
[T; N] where N > 32 in no_std Split across multiple params (serde_core array size limit)
usize
u32 (guest is 32-bit)
f32 / f64
Fixed-point integer (e.g. i64 * 1_000_000 ) — RV64IMAC has no FPU
std::io , std::net
Cannot run in guest — explain and stop
Non-determinism Pass seed/timestamp as explicit input
Build mode: read the library's Cargo.toml . Use std mode if the library requires std, or if it makes the example simpler (e.g. Vec/String as params). No_std is a choice, not the default.
Step 3 — Install Jolt
jolt --version # check if installed cargo install --git https://github.com/a16z/jolt --force jolt # if not
Step 4 — Scaffold
If inside an existing Rust library repo, propose:
"I'll create <library-name>-jolt/ here with the proof scaffold and import your library as a path dependency. Sound good?"
jolt new <project-name> # standard mode jolt new <project-name> --zk # with PrivateInput + BlindFold support
This generates a workspace with a fib example — replace it by renaming fib → <fn> throughout src/main.rs and guest/src/lib.rs . Preserve the [patch.crates-io] block in the root Cargo.toml (required arkworks patches).
Step 5 — Write the guest (guest/src/lib.rs )
no_std mode (default):
#![cfg_attr(feature = "guest", no_std)] extern crate alloc; // heap always available
#[jolt::provable] fn <fn>(<params>) -> <ret> { ... }
std mode — in guest/Cargo.toml :
jolt = { package = "jolt-sdk", git = "https://github.com/a16z/jolt", features = ["guest-std", "thread", "stdout"] }
Include "thread" for rayon/parallel, "stdout" for println! . No cfg_attr needed in the lib file.
Macro parameters — use #[jolt::provable] bare; only add parameters when you have a reason:
Parameter Default When to change How to pick a value
stack_size
4096 stack overflow
Start at 8388608 (8 MB, matches Linux default); reduce in the optimization pass.
max_trace_length
2^22 max_trace_length exceeded
Run analyze_<fn> to get actual cycle count, round up to next power of 2. Proving time and memory scale with this — tighten in Step 9.
heap_size
32 MB heap allocation failed
Estimate peak live allocations; halve until it fails, then double back.
Prover-only inputs — two options depending on whether you need cryptographic privacy:
-
jolt::UntrustedAdvice<T> — prover-only; excluded from the verifier API but values may be recoverable from the proof
-
jolt::PrivateInput<T> — same underlying type, signals that values should be cryptographically hidden via BlindFold (requires zk on the host, not the guest)
#[jolt::provable] fn my_fn(public: u64, secret: jolt::UntrustedAdvice<[u8; 32]>) -> bool { let secret = *secret; // ... }
Host prove call: prove(..., UntrustedAdvice::new(val)) . The generated verifier signature omits the advice entirely. Add use jolt_sdk::UntrustedAdvice; to the host.
For PrivateInput<T> , enable zk on the host only (see Step 7). The macro enforces this at compile time.
TrustedAdvice<T> is the alternative for data committed by a third party — it requires a commit_trusted_advice_<fn>(...) host call and the commitment is passed to the verifier.
Dependencies — add to guest/Cargo.toml . When wrapping an existing repo, add <library> = { path = "../.." } . Avoid default-features = false unless you know the library supports it — disabled default features can expose conditionally-compiled modules that still reference missing optional deps. For crypto, prefer jolt-inlines-sha2 , jolt-inlines-keccak256 , jolt-inlines-secp256k1 .
Multiple functions — each #[jolt::provable] generates independent compile_* , preprocess_* , build_prover_* , build_verifier_* APIs.
Advice functions — for expensive witness computation that should run outside the proof, use #[jolt::advice] in the guest. The function runs on the host/prover; the guest verifies the result cheaply with jolt::check_advice_eq!(computed, expected) .
Cycle tracking — instrument sections of the guest to measure per-section cycle counts (visible in the prover log):
use jolt::{start_cycle_tracking, end_cycle_tracking};
start_cycle_tracking("my section"); // ... code to measure ... end_cycle_tracking("my section");
Step 6 — Write the host (src/main.rs )
use std::time::Instant; use tracing::info;
pub fn main() { tracing_subscriber::fmt().with_env_filter( tracing_subscriber::EnvFilter::from_default_env() ).init();
let target_dir = "/tmp/jolt-guest-targets";
let mut program = guest::compile_<fn>(target_dir);
let shared = guest::preprocess_shared_<fn>(&mut program);
let prover_prep = guest::preprocess_prover_<fn>(shared.clone());
let verifier_setup = prover_prep.generators.to_verifier_setup();
let verifier_prep = guest::preprocess_verifier_<fn>(shared, verifier_setup, None);
let prove = guest::build_prover_<fn>(program, prover_prep);
let verify = guest::build_verifier_<fn>(verifier_prep);
let t = Instant::now();
let (output, proof, io) = prove(<inputs>);
info!("Prover runtime: {} s", t.elapsed().as_secs_f64());
// io.panic is true if the guest panicked; the verifier checks it matches the proof
let is_valid = verify(<inputs>, output, io.panic, proof);
info!("output: {:?}", output);
info!("valid: {is_valid}");
assert!(is_valid);
}
For multiple functions, replicate the block per function. To measure cycles before proving: guest::analyze_<fn>(<inputs>).write_to_file("summary.txt".into()).unwrap() .
Step 7 — Run
Before running, estimate peak memory from max_trace_length (conservative worst-case):
max_trace_length Peak memory
≤ 2^23 < 10 GB
2^24 ~15 GB
2^25 ~32 GB
2^26 ~42 GB
2^27 ~81 GB
2^28 ~99 GB
If max_trace_length is 2^24 or above, warn the user and ask how to proceed:
"This may require ~X GB of RAM. I can: (a) run analyze_<fn> first to get the actual cycle count — if it's well below max_trace_length we can lower it and reduce memory significantly, or (b) proceed directly. Which do you prefer?"
RUST_LOG=info cargo run --release
For full zero-knowledge (hides witness via BlindFold protocol), enable zk in both crates. Use jolt new --zk to scaffold a ZK project, or add manually:
Host Cargo.toml :
jolt-sdk = { git = "https://github.com/a16z/jolt", features = ["host", "zk"] }
Guest Cargo.toml :
jolt = { package = "jolt-sdk", git = "https://github.com/a16z/jolt", features = ["zk"] }
In the host, pass BlindfoldSetup to verifier preprocessing:
let blindfold_setup = prover_prep.blindfold_setup(); let verifier_prep = guest::preprocess_verifier_<fn>(shared, verifier_setup, Some(blindfold_setup));
Preprocessing runs once on first invocation and is not included in "Prover runtime". Diagnose failures:
Error Fix
max_trace_length exceeded
Add max_trace_length = N (tight power of 2 — proving time scales with this)
heap allocation failed
Add heap_size = N
stack overflow
Increase stack_size ; start at 8388608 (8 MB) if not already set
Illegal instruction
Rewrite floats as fixed-point
could not find crate
Find no_std alternative or switch to std mode
does not implement Serialize
Add #[derive(serde::Serialize, serde::Deserialize)]
Step 8 — Summarize
Tell the user: what function was made provable, what type adaptations were applied and why, std or no_std mode, and how to run it.
Once the proof runs end-to-end, always offer a performance optimization pass:
"The proof works! Want me to optimize it? I can tighten max_trace_length to reduce memory and proving time, profile which sections dominate cycle count, and offload expensive witness computation."
Step 9 — Optimize (offer after Step 8 succeeds)
Work through these in order:
-
Tighten max_trace_length — run guest::analyze_<fn>(<inputs>) , find the actual cycle count, set max_trace_length to the smallest power of 2 above it. Proving time and peak memory are both proportional — a 2× reduction is a 2× speedup.
-
Find the bottleneck — add start_cycle_tracking / end_cycle_tracking (see Step 5) around major sections and run analyze_<fn> again. Focus on whichever section consumes >50% of cycles.
-
Offload expensive witness computation — if a section is expensive to compute but cheap to verify (sorting, hashing, witness generation), convert it to #[jolt::advice] . The advice function runs on the host outside the proof; the guest only verifies the result:
#[jolt::advice] fn sort_array(input: &[u64]) -> jolt::UntrustedAdvice<Vec<u64>> { let mut v = input.to_vec(); v.sort_unstable(); v }
#[jolt::provable] fn my_fn(input: &[u64]) -> bool { let adv = sort_array(input); let sorted = &*adv; // O(n) verification: sorted order + length jolt::check_advice!(sorted.windows(2).all(|w| w[0] <= w[1])); jolt::check_advice!(sorted.len() == input.len()); true }
- Use crypto inlines — for SHA-2, Keccak, secp256k1, replace standard crate calls with jolt-inlines-* (constraint-native, fraction of the cycle cost):
jolt-inlines-sha2 = { git = "https://github.com/a16z/jolt" }
- Trim stack_size and heap_size — over-allocation doesn't cost cycles but does increase peak prover memory. Lower to actual usage once max_trace_length is tight.