Guests
Guests contain functions for Jolt to prove. Making a function provable is as easy as ensuring it is inside the guest package and adding the jolt::provable macro above it.
Let's take a look at a simple guest program to better understand it.
#![allow(unused)] #![cfg_attr(feature = "guest", no_std)] fn main() { #[jolt::provable] fn add(x: u32, y: u32) -> u32 { x + y } }
As we can see, the guest looks like a normal no_std Rust library. The only major change is the addition of the jolt::provable macro, which lets Jolt know of the function's existence. The only requirement of these functions is that its inputs are serializable and outputs are deserializable with serde. Fortunately serde is prevalent throughout the Rust ecosystem, so most types will support it by default.
There is no requirement that just a single function lives within the guest, and we are free to add as many as we need. Additionally, we can import any no_std compatible library just as we normally would in Rust.
#![allow(unused)] #![cfg_attr(feature = "guest", no_std)] fn main() { use sha2::{Sha256, Digest}; use sha3::{Keccak256, Digest}; #[jolt::provable] fn sha2(input: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(input); let result = hasher.finalize(); Into::<[u8; 32]>::into(result) } #[jolt::provable] fn sha3(input: &[u8]) -> [u8; 32] { let mut hasher = Keccak256::new(); hasher.update(input); let result = hasher.finalize(); Into::<[u8; 32]>::into(result) } }
Prover and Verifier Views of Guest Program
A guest program consists of two main components: program code and inputs. Both the prover and verifier know the program code (and how it compiles to RISC-V instructions).
Jolt supports three types of inputs:
-
Public Input: These are inputs known to both the prover and verifier. The prover proves that given the bytecode and these inputs, the claimed output is correct.
-
Untrusted Advice (
jolt::UntrustedAdvice<T>): These inputs are known to the prover for generating the proof, but the verifier does not receive them. The prover proves that there exists some input that produces the claimed output. Note:UntrustedAdvicedoes not guarantee privacy — without thezkfeature, polynomial evaluations are revealed in the clear and the inputs may be extractable from the proof. -
Private Input (
jolt::PrivateInput<T>): Equivalent toUntrustedAdvice<T>but signals that the input should be cryptographically hidden from the verifier via the BlindFold protocol. Thezkfeature must be enabled onjolt-sdkin the host crate; the guest needs no feature flag. The#[jolt::provable]macro enforces this at compile time. -
Trusted Advice (
jolt::TrustedAdvice<T>): Similar to untrusted advice, but the verifier has a commitment to the input generated by an external party (not the prover). The prover proves that there exists an input matching the given commitment that, when executed with the program code, produces the claimed output.
A program can use any combination of these input types. To access the inner value, dereference the wrapper with *.
Private inputs (ZK)
When you need inputs that are cryptographically hidden from the verifier, use PrivateInput<T>. Enable the zk feature on jolt-sdk in the host crate — the guest needs no feature flag. You can scaffold a ZK-ready project with jolt new my-project --zk.
Guest Cargo.toml (no zk feature needed):
[dependencies]
jolt = { package = "jolt-sdk" }
Host Cargo.toml:
[dependencies]
jolt-sdk = { features = ["host", "zk"] }
If the guest uses PrivateInput<T> but the host doesn't enable zk, compilation fails with a clear error message.
Guest (guest/src/lib.rs):
#![allow(unused)] #![cfg_attr(feature = "guest", no_std)] fn main() { use jolt::PrivateInput; #[jolt::provable(heap_size = 32768, max_trace_length = 65536)] fn fib(n: PrivateInput<u32>) -> u128 { let mut a: u128 = 0; let mut b: u128 = 1; for _ in 1..*n { let sum = a + b; a = b; b = sum; } b } }
Host (src/main.rs):
#![allow(unused)] fn main() { use jolt_sdk::PrivateInput; let verifier_setup = prover_preprocessing.generators.to_verifier_setup(); let blindfold_setup = prover_preprocessing.blindfold_setup(); let verifier_preprocessing = guest::preprocess_verifier_fib(shared_preprocessing, verifier_setup, Some(blindfold_setup)); // Prover receives the private input let (output, proof, io_device) = prove_fib(PrivateInput::new(50)); // Verifier does not — it only sees the output and proof let is_valid = verify_fib(output, io_device.panic, proof); }
The generated verifier closure omits private and advice parameters — only public inputs appear in the verifier's signature.
For a complete example of advice inputs, see the merkle-tree example.
Zero knowledge
By default, the prover reveals polynomial evaluations in the clear. To produce zero-knowledge proofs where the verifier learns nothing beyond the validity of the output, enable the zk feature on jolt-sdk in the host crate. This activates the BlindFold protocol, which commits all sumcheck round polynomials via Pedersen and proves correctness via Nova folding + Spartan.
PrivateInput<T> is always available in the guest. If you use UntrustedAdvice<T> without zk, the verifier won't receive the inputs, but the proof itself does not hide them cryptographically.
The generated preprocess_verifier_* function always takes three arguments: shared, generators, and Option<BlindfoldSetup<Bn254Curve>>. Pass Some(blindfold_setup) when using zk mode, or None otherwise.
Advice inputs vs. runtime advice
Advice inputs described above (UntrustedAdvice<T>, PrivateInput<T>, TrustedAdvice<T>) are host-provided: the host supplies the data before execution, and it is written into a preallocated memory region that the guest reads from. The guest program runs once, and the values are fixed before execution begins.
Runtime advice is a separate mechanism where the guest itself computes advice values during a first-pass execution, and those values are fed back via an advice tape for the proving pass. This is useful when the advice depends on the guest's own execution (e.g. computing a modular inverse or finding factors) and checking the result is cheaper than computing it. Runtime advice uses #[jolt::advice] functions rather than wrapper-type parameters on #[jolt::provable].
Both mechanisms signal that the data is untrusted and must be verified by the guest, but they differ in where the data originates and how it is serialized (advice inputs use serde; runtime advice uses AdviceTapeIO).
Security model
Beyond the usual Rust rules around unsafe and undefined behavior, Jolt guest programs are subject to a few zkVM-specific rules:
-
Self-modifying programs are undefined behavior. Jolt commits to the program's bytecode at preprocessing time. A guest that writes to its own code region can still produce a valid proof, but the semantics of that proof are undefined — the proof attests to execution against the committed bytecode, not the modified version.
-
Direct invocation of Jolt's custom RISC-V instructions is unsafe. Jolt extends the RISC-V ISA with custom instructions used internally (e.g. for inline cryptographic primitives and runtime advice). Invoking these instructions directly from guest code, such as via inline assembly, bypasses the safety checks performed by the SDK and may produce incorrect proofs or cause proving to fail.
-
Prover-supplied advice must be validated by the guest.
UntrustedAdvice<T>andPrivateInput<T>are supplied by the prover and are not constrained by the proof system itself. The guest is responsible for verifying that any advice it consumes satisfies the properties the program relies on (e.g. that a claimed factor actually divides the input, or that a claimed Merkle path matches a known root).TrustedAdvice<T>carries an external commitment, but the data itself may still require validation. -
No source of randomness or wall clock. The guest has no entropy source and no clock. The platform exposes
sys_rand(seejolt-platform/src/random.rs), but it is a deterministic PRNG seeded with a fixed constant — its output is fully predictable, including by the verifier — and is not suitable for cryptographic use. Any value that must be unpredictable (nonces, keys, sampling weights) has to be supplied as a public input. Supplying it via advice does not solve the problem: advice is chosen by the prover, so a "key" derived from advice is a key the prover picked. -
Bytecode is public. The compiled guest ELF is committed at preprocessing time and is known to the verifier. The
zkfeature protects inputs, not the program. Do not embed secrets in guest code — API keys, hardcoded credentials, or proprietary algorithms you do not want disclosed. -
Outputs and the panic flag are public. The return value and
io_device.panicare revealed to the verifier and are what the proof attests to. Returning a value derived from a private input leaks that value; panicking conditionally on a private input leaks one bit per panic site. Guests handling secret data should be written so that the output and panic behavior depend only on public information. -
No side-channel resistance. The
zkfeature, via the BlindFold protocol, makes proofs zero-knowledge with respect to the verifier. However, Jolt's prover implementation is not constant-time and makes no claims of resistance to side-channel attacks. A party that observes prover execution (timing, memory access patterns, power consumption, etc.) may learn information about private inputs. Do not run the prover on secret data in adversarial environments without additional mitigations.
Standard Library
Jolt supports the Rust standard library. To enable support, simply add the guest-std feature to the Jolt import in the guest's Cargo.toml file and remove the #![cfg_attr(feature = "guest", no_std)] directive from the guest code.
Example
#![allow(unused)] fn main() { [package] name = "guest" version = "0.1.0" edition = "2021" [features] guest = [] [dependencies] jolt = { package = "jolt-sdk", git = "https://github.com/a16z/jolt", features = ["guest-std"] } }
#![allow(unused)] fn main() { #[jolt::provable] fn int_to_string(n: i32) -> String { n.to_string() } }
alloc
Jolt provides an allocator which supports most containers such as Vec and Box. This is useful for Jolt users who would like to write no_std code rather than using Jolt's standard library support. To use these containers, they must be explicitly imported from alloc. The alloc crate is automatically provided by rust and does not need to be added to the Cargo.toml file.
Example
#![allow(unused)] #![cfg_attr(feature = "guest", no_std)] fn main() { extern crate alloc; use alloc::vec::Vec; #[jolt::provable] fn alloc(n: u32) -> u32 { let mut v = Vec::<u32>::new(); for i in 0..100 { v.push(i); } v[n as usize] } }
Print statements
Jolt supports standard print! and println! macros in guest programs.
Example
#![allow(unused)] fn main() { #[jolt::provable(heap_size = 10240, max_trace_length = 65536)] fn int_to_string(n: i32) -> String { print!("Hello, "); println!("from int_to_string({n})!"); n.to_string() } }
The printed strings are written to stdout during RISC-V emulation of the guest.
Optimization level
By default, the guest program is compiled with optimization level 3 to attempt to minimize the number of RISC-V instructions executed during emulation.
To change this optimization level, set the JOLT_GUEST_OPT environment variable to one of the allowed values: 0, 1, 2, 3, s, or z.
A common choice is z which optimizes for code size, at the cost of a larger number of instructions executed.