Skip to main content
Jolt’s advice system allows guest programs to receive non-deterministic hints (witnesses) from the prover. This enables efficient zero-knowledge proofs for computations that require witness data.

Why Advice?

Some computations are easy to verify but hard to compute:
  • Factoring: Given n = a * b, computing (a, b) is hard, but verifying is trivial
  • Path finding: Finding a path is hard, verifying a path is easy
  • Merkle proofs: Computing the proof path is non-trivial, verification is simple
  • Preimages: Finding x such that hash(x) = y is hard, checking is easy
Advice lets the prover provide the “hard part” (witness) while the guest verifies correctness inside the zkVM.

Types of Advice

TrustedAdvice

TrustedAdvice<T> is committed before the prover sees any challenges:
pub struct TrustedAdvice<T> {
    value: T,
}
Use trusted advice for:
  • Secret keys
  • Private witness data
  • Data that must be hidden from the verifier
Example:
#[jolt::provable]
fn prove_knowledge(
    public_key: [u8; 32],
    secret: jolt::TrustedAdvice<[u8; 64]>,
) -> bool {
    let derived_pubkey = derive_public_key(&secret);
    derived_pubkey == public_key
}
The verifier only sees the commitment to secret, never the value itself.

UntrustedAdvice

UntrustedAdvice<T> is provided by the prover but must be verified:
pub struct UntrustedAdvice<T> {
    value: T,
}
Use untrusted advice for:
  • Witness data
  • Intermediate computation results
  • Optimization hints
Example:
#[jolt::provable]
fn verify_factors(
    n: u64,
    factors: jolt::UntrustedAdvice<(u64, u64)>,
) -> bool {
    let (a, b) = *factors;
    // CRITICAL: Must verify untrusted advice!
    a * b == n && a > 1 && b > 1
}
Always verify untrusted advice! Untrusted data comes from the prover and could be malicious. Use check_advice! macros.

Runtime Advice Functions

The #[jolt::advice] macro creates dual-mode advice functions:

Basic Advice Function

#[jolt::advice]
fn factor_u32(n: u32) -> jolt::UntrustedAdvice<(u32, u32)> {
    // Expensive computation runs OUTSIDE the proof
    for i in 2..=n {
        if n % i == 0 {
            return (i, n / i);
        }
    }
    (1, n)
}
How it works:
  1. During advice computation phase (feature compute_advice):
    • Function body executes
    • Result is written to advice tape via AdviceTapeIO
  2. During proof generation (without compute_advice):
    • Function body is skipped
    • Result is read from advice tape

Using Advice in Guests

#[jolt::provable]
fn verify_composite(n: u32) -> bool {
    // Get factors from advice
    let factors_advice = factor_u32(n);
    let (a, b) = *factors_advice; // Deref UntrustedAdvice
    
    // MUST verify the advice!
    jolt::check_advice_eq!(a * b, n);
    jolt::check_advice!(a > 1 && b > 1 && a <= b);
    
    true
}

Verification Macros

check_advice!

Verifies a boolean condition:
jolt::check_advice!(condition);
jolt::check_advice!(condition, "error message");
Implementation:
  • On RISC-V: Emits VirtualAssertEQ custom instruction
  • On native: Regular assert!
Example:
let advice = get_merkle_proof(leaf);
jolt::check_advice!(advice.len() == expected_depth);
jolt::check_advice!(verify_proof(&advice), "Invalid Merkle proof");

check_advice_eq!

Verifies equality directly (more efficient):
jolt::check_advice_eq!(left, right);
jolt::check_advice_eq!(left, right, "error message");
Example:
let factors = factor_u64(n);
let (a, b) = *factors;
jolt::check_advice_eq!((a as u128) * (b as u128), n as u128);
check_advice_eq! requires values that fit in registers. For u128, use check_advice! instead.

AdviceTapeIO Trait

Custom types need to implement AdviceTapeIO to work with advice:

Automatic Implementation (Pod Types)

use bytemuck_derive::{Pod, Zeroable};
use jolt::JoltPod;

#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct Point {
    x: u32,
    y: u32,
}

impl JoltPod for Point {}

// Point now works with advice automatically!
#[jolt::advice]
fn get_point() -> jolt::UntrustedAdvice<Point> {
    Point { x: 10, y: 20 }
}

Manual Implementation

struct CustomData {
    a: u8,
    b: Vec<u16>,
    c: u64,
}

impl jolt::AdviceTapeIO for CustomData {
    fn write_to_advice_tape(&self) {
        self.a.write_to_advice_tape();
        self.b.write_to_advice_tape();
        self.c.write_to_advice_tape();
    }
    
    fn new_from_advice_tape() -> Self {
        CustomData {
            a: u8::new_from_advice_tape(),
            b: Vec::<u16>::new_from_advice_tape(),
            c: u64::new_from_advice_tape(),
        }
    }
}

Built-in Implementations

  • Primitives: u8, u16, u32, u64, i8, i16, i32, i64, usize
  • Arrays: [T; N] where T: Pod
  • Tuples: Up to 7 elements where each implements AdviceTapeIO
  • Vectors: Vec<T> where T: Pod (requires std or guest-std)

Advanced Example: Subset Verification

/// Provide indices showing that `a` is a subset of `b`
#[jolt::advice]
fn subset_index(a: &[usize], b: &[usize]) -> jolt::UntrustedAdvice<Vec<usize>> {
    let mut indices = Vec::new();
    for &item in a {
        for (i, &b_item) in b.iter().enumerate() {
            if item == b_item {
                indices.push(i);
                break;
            }
        }
    }
    indices
}

/// Verify that all elements of `a` exist in `b`
fn verify_subset(a: &[usize], b: &[usize]) {
    let advice = subset_index(a, b);
    let indices = &*advice;
    
    // Verify advice correctness
    jolt::check_advice_eq!(indices.len(), a.len());
    
    for (i, &item) in a.iter().enumerate() {
        let idx = indices[i];
        jolt::check_advice!(idx < b.len());
        jolt::check_advice_eq!(b[idx], item);
    }
}

#[jolt::provable]
fn prove_subset(a: Vec<usize>, b: Vec<usize>) {
    verify_subset(&a, &b);
}

Low-Level Advice API

For advanced use cases, use the raw advice tape API:

AdviceWriter (Compute Advice Phase)

let mut writer = jolt::AdviceWriter::get();
writer.write_u8(42);
writer.write_u32(0xdeadbeef);
writer.write_u64(123456789);

AdviceReader (Proof Generation Phase)

let mut reader = jolt::AdviceReader::get();
let value = reader.read_u8();
let count = reader.read_u32();
let data = reader.read_u64();

// Check remaining bytes
let remaining = reader.bytes_remaining();
Raw advice API is error-prone. Prefer #[jolt::advice] functions which handle serialization automatically.

Two-Pass Execution

Jolt uses a two-pass strategy for advice:

Pass 1: Compute Advice

Guest builds with compute_advice feature:
  • Advice functions execute their bodies
  • Results written to advice tape
  • Tape is saved

Pass 2: Generate Proof

Guest builds without compute_advice:
  • Advice functions read from tape
  • Bodies are not executed
  • Proof is generated
This is handled automatically by the #[jolt::provable] macro.

Best Practices

Always Verify

Use check_advice! for all UntrustedAdvice. Never trust prover-provided data.

Use Trusted for Secrets

Secrets should be TrustedAdvice, never public inputs or UntrustedAdvice.

Optimize Advice Computation

Advice functions run outside the proof. Use efficient native algorithms.

Minimize Advice Size

Large advice increases memory usage. Provide minimal witness data.

Common Patterns

Pattern: Factorization Witness

#[jolt::advice]
fn factor(n: u64) -> jolt::UntrustedAdvice<(u64, u64)> {
    /* expensive factorization */
}

#[jolt::provable]
fn prove_composite(n: u64) -> bool {
    let (a, b) = *factor(n);
    jolt::check_advice_eq!((a as u128) * (b as u128), n as u128);
    jolt::check_advice!(1 < a && a <= b && b < n);
    true
}

Pattern: Merkle Proof Verification

#[jolt::advice]
fn get_merkle_proof(
    tree: &MerkleTree,
    leaf: [u8; 32]
) -> jolt::UntrustedAdvice<Vec<[u8; 32]>> {
    tree.proof_for_leaf(leaf)
}

#[jolt::provable]
fn verify_membership(
    root: [u8; 32],
    leaf: [u8; 32],
    tree: jolt::UntrustedAdvice<MerkleTree>,
) -> bool {
    let proof = get_merkle_proof(&tree, leaf);
    verify_merkle_proof(root, leaf, &proof)
}

Pattern: Search Result

#[jolt::advice]
fn find_index(haystack: &[u8], needle: u8) -> jolt::UntrustedAdvice<usize> {
    haystack.iter().position(|&x| x == needle).unwrap_or(haystack.len())
}

fn search(haystack: &[u8], needle: u8) -> Option<usize> {
    let idx = *find_index(haystack, needle);
    jolt::check_advice!(idx <= haystack.len());
    if idx < haystack.len() {
        jolt::check_advice_eq!(haystack[idx], needle);
        Some(idx)
    } else {
        None
    }
}

Build docs developers (and LLMs) love