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: These inputs are known to the prover for generating the proof, but the verifier does not know them for the purpose of verification. Here, the prover proves that there exists some input that produces the claimed output.
-
Trusted Advice: 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.
Important: While the verifier does not see advice inputs, advice inputs (both trusted and untrusted) do not currently support zero-knowledge properties and are not designed for that purpose. Their main purpose is to allow the verifier to verify an execution proof without receiving the advice inputs themselves.
A program can use any combination of these three input types. In guest code, you can define trusted inputs using jolt::TrustedAdvice<input_type> and untrusted inputs using jolt::UntrustedAdvice<input_type>. To access the data inside these wrappers, simply use deref(). These wrappers serve solely to indicate to the macro expander how to handle each input type and generate the appropriate functions for both prover and verifier.
For a complete example of advice inputs, see the merkle-tree example.
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 provides utilities emulating print! and println! in a guest program.
Example
#![allow(unused)] fn main() { use jolt::{jolt_print, jolt_println}; #[jolt::provable(memory_size = 10240, max_trace_length = 65536)] fn int_to_string(n: i32) -> String { jolt_print!("Hello, ") jolt_println!("from int_to_string({n})!"); n.to_string() } }
Note that jolt_print and jolt_println support format strings. The printed strings are written to stdout during RISC-V emulation of the guest.