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")
Boolean expression that must evaluate to true.
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")
First value. Must fit in a RISC-V register (u8, u16, u32, u64, usize, or signed equivalents).
Second value. Must fit in a RISC-V register.
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
| Feature | check_advice! | check_advice_eq! |
|---|
| Evaluates | Boolean expression | Two values |
| Instruction | VirtualAssertEQ(cond as u64, 1) | VirtualAssertEQ(left, right) |
| Efficiency | Evaluates condition first | Direct register comparison |
| Type requirement | Any bool | Register-sized integers |
| Use case | Complex conditions | Equality 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);