git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/a16z/jolt/jolt" ~/.claude/skills/neversight-learn-skills-dev-jolt && rm -rf "$T"
data/skills-md/a16z/jolt/jolt/SKILL.mdInvoke 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 |
|---|---|
param in no_std | — or switch to std mode |
where N > 32 in no_std | Split across multiple params (serde_core array size limit) |
| (guest is 32-bit) |
/ | Fixed-point integer (e.g. ) — RV64IMAC has no FPU |
, | 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
here with the proof scaffold and import your library as a path dependency. Sound good?"<library-name>-jolt/
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
)
guest/src/lib.rsno_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 |
|---|---|---|---|
| 4096 | | Start at 8388608 (8 MB, matches Linux default); reduce in the optimization pass. |
| 2^22 | | Run to get actual cycle count, round up to next power of 2. Proving time and memory scale with this — tighten in Step 9. |
| 32 MB | | Estimate peak live allocations; halve until it fails, then double back. |
Prover-only inputs — two options depending on whether you need cryptographic privacy:
— prover-only; excluded from the verifier API but values may be recoverable from the proofjolt::UntrustedAdvice<T>
— same underlying type, signals that values should be cryptographically hidden via BlindFold (requiresjolt::PrivateInput<T>
on the host, not the guest)zk
#[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
)
src/main.rsuse 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
first to get the actual cycle count — if it's well belowanalyze_<fn>we can lower it and reduce memory significantly, or (b) proceed directly. Which do you prefer?"max_trace_length
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 |
|---|---|
| Add (tight power of 2 — proving time scales with this) |
| Add |
| Increase ; start at 8388608 (8 MB) if not already set |
| Rewrite floats as fixed-point |
| Find no_std alternative or switch to std mode |
| Add |
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
to reduce memory and proving time, profile which sections dominate cycle count, and offload expensive witness computation."max_trace_length
Step 9 — Optimize (offer after Step 8 succeeds)
Work through these in order:
1. Tighten
— run max_trace_length
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.
2. 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.
3. 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 }
4. 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" }
5. Trim
and stack_size
— over-allocation doesn't cost cycles but does increase peak prover memory. Lower to actual usage once heap_size
max_trace_length is tight.