Skip to main content
The check_advice! and check_advice_eq! macros enforce constraints on untrusted advice values. They generate efficient virtual assertions that are proved within the zkVM.

Overview

Advice values are provided by the prover and must be verified for correctness. These macros generate VirtualAssertEQ RISC-V custom instructions that the Jolt prover includes in the proof. On RISC-V targets (guest code): Generates custom instruction for ZK proof On non-RISC-V targets (native execution): Falls back to standard assert! / assert_eq!

check_advice!

Asserts that a boolean condition holds.

Syntax

jolt::check_advice!(condition)
jolt::check_advice!(condition, "error message")
condition
bool
required
Boolean expression that must evaluate to true.
error message
&str
Optional error message. Only used in native mode; not included in guest binary.

Examples

Basic Validation

let (a, b) = *factor(n);
jolt::check_advice!(a * b == n);
jolt::check_advice!(a > 1 && b > 1);

With Error Message

jolt::check_advice!(
    indices.len() == values.len(),
    "indices and values must have same length"
);

Range Checks

jolt::check_advice!(index < array.len());
jolt::check_advice!(1 < a && a <= b && b < n, "factors out of range");

Capacity Checks

let len = usize::new_from_advice_tape();
let capacity = usize::new_from_advice_tape();
jolt::check_advice!(capacity >= len);

check_advice_eq!

Asserts that two register-sized values are equal. More efficient than check_advice! for equality checks.

Syntax

jolt::check_advice_eq!(left, right)
jolt::check_advice_eq!(left, right, "error message")
left
register-sized
required
First value. Must fit in a RISC-V register (u8, u16, u32, u64, usize, or signed equivalents).
right
register-sized
required
Second value. Must fit in a RISC-V register.
error message
&str
Optional error message. Only used in native mode.

Examples

Verify Multiplication

let (a, b) = *factor_u8(n);
jolt::check_advice_eq!((a as u16) * (b as u16), n as u16);

Verify Array Length

let indices = &*subset_index(a, b);
jolt::check_advice_eq!(indices.len() as u64, a.len() as u64);

Verify Struct Fields

let frobnitz = &*frobnitz_advice();
jolt::check_advice_eq!(frobnitz.x as u64, 42u64);
jolt::check_advice_eq!(frobnitz.y, 9999u64);
jolt::check_advice_eq!(frobnitz.z.len() as u64, 5u64);

With Custom Message

jolt::check_advice_eq!(
    computed_hash,
    expected_hash,
    "hash mismatch"
);

Differences

Featurecheck_advice!check_advice_eq!
EvaluatesBoolean expressionTwo values
InstructionVirtualAssertEQ(cond as u64, 1)VirtualAssertEQ(left, right)
EfficiencyEvaluates condition firstDirect register comparison
Type requirementAny boolRegister-sized integers
Use caseComplex conditionsEquality checks

When to Use

Use check_advice! for:

  • Complex boolean conditions
  • Multiple conditions with && / ||
  • Range checks and comparisons
  • Conditions involving non-register types (e.g., u128)
jolt::check_advice!(1 < a && a <= b && b < n);
jolt::check_advice!((a as u128) * (b as u128) == (n as u128));
jolt::check_advice!(index < array.len() && array[index] == value);

Use check_advice_eq! for:

  • Simple equality checks
  • Verifying computed values against expected values
  • Register-sized comparisons (u8, u16, u32, u64, usize)
jolt::check_advice_eq!(a * b, n);
jolt::check_advice_eq!(len as u64, 10u64);
jolt::check_advice_eq!(computed, expected);

Implementation Details

RISC-V Custom Instruction

On riscv32 or riscv64 targets, both macros generate a VirtualAssertEQ instruction:
core::arch::asm!(
    ".insn b {opcode}, {funct3}, {rs1}, {rs2}, 0",
    opcode = const CUSTOM_OPCODE,     // 0x5B
    funct3 = const FUNCT3_VIRTUAL_ASSERT_EQ,  // 0b001
    rs1 = in(reg) left_value,
    rs2 = in(reg) right_value,
    options(nostack)
);
The instruction asserts rs1 == rs2. If the assertion fails, the proof generation fails.

Native Fallback

On non-RISC-V targets (e.g., during native testing or host execution):
assert!(condition, "error message");
assert_eq!(left, right, "error message");

Error Messages

Error messages are only active in native mode. When compiling for RISC-V:
  • The error message string is not included in the guest binary
  • Only the assertion logic is preserved
  • This keeps the guest code size minimal
For debugging, run your guest function natively first:
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_verify_factors() {
        // This will show error messages if assertions fail
        verify_composite(15);
    }
}

Common Patterns

Verify Factorization

#[jolt::advice]
fn factor(n: u64) -> jolt::UntrustedAdvice<(u64, u64)> {
    // ... compute factors ...
}

fn verify_composite(n: u64) {
    let (a, b) = *factor(n);
    jolt::check_advice_eq!(a * b, n);
    jolt::check_advice!(1 < a && a <= b && b < n);
}

Verify Subset Membership

#[jolt::advice]
fn subset_index(a: &[usize], b: &[usize]) -> jolt::UntrustedAdvice<Vec<usize>> {
    // ... find indices ...
}

fn verify_subset(a: &[usize], b: &[usize]) {
    let indices = &*subset_index(a, b);
    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);
    }
}

Verify Sorted Indices

fn verify_sorted(x: &[u32], indices: jolt::UntrustedAdvice<Vec<usize>>) {
    let idx = &*indices;
    jolt::check_advice_eq!(idx.len() as u64, x.len() as u64);
    for i in 0..idx.len() {
        jolt::check_advice!(idx[i] < x.len());
    }
    for i in 0..idx.len() - 1 {
        jolt::check_advice!(x[idx[i]] <= x[idx[i + 1]]);
    }
}

Verify Geometric Properties

fn verify_triangle(area: u32) {
    let adv = triangle_from_area(area);
    // Shoelace formula
    let double = (adv.p1.x as i64 * (adv.p2.y as i64 - adv.p3.y as i64)
        + adv.p2.x as i64 * (adv.p3.y as i64 - adv.p1.y as i64)
        + adv.p3.x as i64 * (adv.p1.y as i64 - adv.p2.y as i64)).abs();
    jolt::check_advice_eq!(double as u32, 2 * area);
}

Best Practices

1. Always Verify Advice

Every piece of advice must be constrained:
// BAD: Trusting advice without verification
let (a, b) = *factor(n);
return a > 1 && b > 1;  // Prover could cheat!

// GOOD: Verify the advice
let (a, b) = *factor(n);
jolt::check_advice_eq!(a * b, n);
jolt::check_advice!(a > 1 && b > 1);

2. Widen Types to Avoid Overflow

When multiplying advice values, widen to prevent overflow:
// For u8 factors
jolt::check_advice_eq!((a as u16) * (b as u16), n as u16);

// For u32 factors
jolt::check_advice_eq!((a as u64) * (b as u64), n as u64);

// For u64 factors (check_advice_eq! doesn't support u128)
jolt::check_advice!((a as u128) * (b as u128) == (n as u128));

3. Check Bounds Before Indexing

for i in 0..indices.len() {
    let idx = indices[i];
    jolt::check_advice!(idx < array.len());
    let value = array[idx];
    // ... use value ...
}

4. Verify Lengths Match

When advice provides parallel arrays:
let indices = &*subset_index(a, b);
jolt::check_advice_eq!(indices.len() as u64, a.len() as u64);
for (i, &item) in a.iter().enumerate() {
    // Safe: lengths match
    let index = indices[i];
    // ...
}

Limitations

Register Size Constraint

check_advice_eq! requires both values fit in a RISC-V register:
// OK: u64 fits in register
jolt::check_advice_eq!(a as u64, b as u64);

// ERROR: u128 doesn't fit in register
// jolt::check_advice_eq!(a as u128, b as u128);  // Won't compile

// Use check_advice! instead
jolt::check_advice!((a as u128) == (b as u128));

Compile-Time vs Runtime

The macros check conditions at runtime during proof generation, not at compile time:
// This compiles fine but will fail during proving if n != a*b
jolt::check_advice_eq!(a * b, n);

Build docs developers (and LLMs) love