The #[jolt::advice] macro enables functions to compute values outside the zkVM and provide them as advice to the guest program. This allows expensive computations to run in native code while the guest only verifies the results.
Overview
Advice functions operate in two modes:
- Compute mode (
compute_advice feature enabled): Executes the function body and writes the result to the advice tape
- Consume mode (default): Reads the pre-computed result from the advice tape
This dual compilation enables a two-pass proving strategy where expensive computations happen outside the proof.
Basic Usage
#[jolt::advice]
fn factor(n: u64) -> jolt::UntrustedAdvice<(u64, u64)> {
// This expensive computation runs outside the zkVM
for i in 2..n {
if n % i == 0 {
return (i, n / i);
}
}
(1, n)
}
fn verify_composite(n: u64) -> bool {
let (a, b) = *factor(n);
// CRITICAL: Always verify advice!
jolt::check_advice_eq!(a * b, n);
a > 1 && b > 1
}
Requirements
Must be jolt::UntrustedAdvice<T> where T implements AdviceTapeIO.
Must be immutable. No mut bindings or &mut references allowed.
The function body computes the advice value. Should return UntrustedAdvice::new(value) or just value (automatic wrapping).
Supported Types
Types that implement AdviceTapeIO can be used as advice:
Built-in Types
- Primitive integers:
u8, u16, u32, u64, usize, i8, i16, i32, i64
- Arrays:
[T; N] where T: Pod
- Tuples:
(A, B), (A, B, C), … up to 7 elements (where each implements AdviceTapeIO)
- Vectors:
Vec<T> where T: Pod (requires std or guest-std feature)
Custom Types
Implement AdviceTapeIO manually:
struct Witness {
value: u64,
proof: Vec<u8>,
}
impl jolt::AdviceTapeIO for Witness {
fn write_to_advice_tape(&self) {
self.value.write_to_advice_tape();
self.proof.write_to_advice_tape();
}
fn new_from_advice_tape() -> Self {
Witness {
value: u64::new_from_advice_tape(),
proof: Vec::<u8>::new_from_advice_tape(),
}
}
}
Or use bytemuck for POD types:
use bytemuck_derive::{Pod, Zeroable};
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct Point {
x: u32,
y: u32,
}
impl jolt::JoltPod for Point {}
// Now Point automatically implements AdviceTapeIO
Examples
Integer Factorization
#[jolt::advice]
fn factor_u8(n: u8) -> jolt::UntrustedAdvice<(u8, u8)> {
let mut a = 1u8;
let mut b = n;
for i in 2..=n {
if n % i == 0 {
a = i;
b = n / i;
break;
}
}
(a, b)
}
fn verify_composite_u8(n: u8) {
let adv = factor_u8(n);
let (a, b) = *adv;
jolt::check_advice_eq!((a as u16) * (b as u16), n as u16);
jolt::check_advice!(1 < a && a <= b && b < n);
}
Subset Index Finding
#[jolt::advice]
fn subset_index(a: &[usize], b: &[usize]) -> jolt::UntrustedAdvice<Vec<usize>> {
let mut indices = Vec::new();
for &item in a.iter() {
for (i, &b_item) in b.iter().enumerate() {
if item == b_item {
indices.push(i);
break;
}
}
}
indices
}
fn verify_subset(a: &[usize], b: &[usize]) {
let adv = subset_index(a, b);
let indices = &*adv;
jolt::check_advice!(indices.len() == a.len());
for (i, &item) in a.iter().enumerate() {
let index = indices[i];
jolt::check_advice!(index < b.len() && b[index] == item);
}
}
Custom Struct with Nested Types
use bytemuck_derive::{Pod, Zeroable};
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct Triangle {
x1: u32, y1: u32,
x2: u32, y2: u32,
x3: u32, y3: u32,
}
impl jolt::JoltPod for Triangle {}
#[jolt::advice]
fn triangle_from_area(area: u32) -> jolt::UntrustedAdvice<Triangle> {
let target = 2 * area;
let mut x = 1;
let mut y = target;
for i in 1..=target {
if target % i == 0 {
x = i;
y = target / i;
break;
}
}
Triangle {
x1: 0, y1: 0,
x2: x, y2: 0,
x3: 0, y3: y,
}
}
fn verify_triangle(area: u32) {
let adv = triangle_from_area(area);
// Shoelace formula: Area = |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)| / 2
let double = (adv.x1 as i64 * (adv.y2 as i64 - adv.y3 as i64)
+ adv.x2 as i64 * (adv.y3 as i64 - adv.y1 as i64)
+ adv.x3 as i64 * (adv.y1 as i64 - adv.y2 as i64)).abs();
jolt::check_advice_eq!(double as u32, 2 * area);
}
How It Works
Two-Pass Compilation
The #[jolt::provable] macro compiles the guest program twice:
- First pass (
compute_advice feature): Advice functions execute their bodies and populate the advice tape
- Second pass (normal): Advice functions read pre-computed values from the tape
This is handled automatically by the generated prove_* functions.
Macro Expansion
The #[jolt::advice] macro expands to:
// With compute_advice feature
#[cfg(feature = "compute_advice")]
fn factor(n: u64) -> jolt::UntrustedAdvice<(u64, u64)> {
let result: (u64, u64) = { /* original body */ };
<(u64, u64) as AdviceTapeIO>::write_to_advice_tape(&result);
jolt::UntrustedAdvice::new(result)
}
// Without compute_advice feature
#[cfg(not(feature = "compute_advice"))]
#[allow(unused_variables)]
fn factor(n: u64) -> jolt::UntrustedAdvice<(u64, u64)> {
let result: (u64, u64) = <(u64, u64) as AdviceTapeIO>::new_from_advice_tape();
jolt::UntrustedAdvice::new(result)
}
Best Practices
Always Verify Advice
Advice is untrusted input from the prover. Always verify it satisfies the required constraints:
let (a, b) = *factor(n);
// REQUIRED: Verify the advice is correct
jolt::check_advice_eq!(a * b, n);
jolt::check_advice!(a > 1 && b > 1);
Advice functions can take references to avoid copying:
#[jolt::advice]
fn process_large_input(data: &[u8]) -> jolt::UntrustedAdvice<u64> {
// Process data without copying
data.iter().map(|&x| x as u64).sum()
}
Size Considerations
Set max_untrusted_advice_size in #[jolt::provable] large enough for all advice values:
#[jolt::provable(max_untrusted_advice_size = 8192)]
fn my_function(input: u32, witness: jolt::UntrustedAdvice<Vec<u8>>) {
// ...
}
Limitations
- Parameters must be immutable (no
mut or &mut)
- Return type must be
UntrustedAdvice<T> where T: AdviceTapeIO
- The function body is only executed during the first pass (with
compute_advice feature)
- Reading beyond the advice tape length causes a runtime error during proof generation