Use this file to discover all available pages before exploring further.
RISC Zero’s zkVM provides a powerful API for verifying proofs inside the guest program. This enables proof composition - the ability to build modular applications where one zkVM program can verify the output of another, all while maintaining the cryptographic guarantees of zero-knowledge proofs.
Unlike the “obvious” approach of running a verifier inside the guest (which would be inefficient), RISC Zero uses a sophisticated system of assumptions and resolution.
1
Adding Assumptions
2
When you call env::verify() in the guest program, an assumption is added to the receipt claim. This creates a “conditional receipt” that says: “This computation is valid, assuming the provided receipt is valid.”
3
Resolving Assumptions
4
When you generate a succinct or Groth16 receipt, assumptions are automatically resolved using RISC Zero’s efficient recursion circuit. The final receipt proves both computations without requiring the verifier to check multiple receipts.
This program verifies another receipt and uses its output:
methods/guest/src/main.rs
use risc0_zkvm::{guest::env, serde};use methods::MULTIPLY_ID; // Image ID of the program being verifiedfn main() { // Read inputs: n (to verify), e (exponent), x (secret) let (n, e, x): (u64, u64, u64) = env::read(); // Verify that n has a known factorization // This adds an assumption to the receipt claim env::verify(MULTIPLY_ID, &serde::to_vec(&n).unwrap()).unwrap(); // Compute using the verified value let result = pow_mod(x, e, n); // Commit the result env::commit(&(n, e, result));}fn pow_mod(x: u64, mut e: u64, n: u64) -> u64 { let mut x = x as u128; let n = n as u128; let mut z = 1u128; while e > 0 { if e % 2 == 1 { z = (z * x) % n } e >>= 1; x = (x * x) % n; } z as u64}
The host provides the assumption receipt when building the execution environment:
src/main.rs
use risc0_zkvm::{ExecutorEnv, default_prover};use methods::{EXPONENTIATE_ELF, EXPONENTIATE_ID};fn main() { // Generate a receipt proving multiplication (factorization) let (multiply_receipt, n) = some_function_that_multiplies(17, 23); // Build environment with the assumption receipt let env = ExecutorEnv::builder() .add_assumption(multiply_receipt) // Provide the receipt to verify .unwrap() .write(&(n, 9u64, 100u64)) // Write inputs .unwrap() .build() .unwrap(); // Generate proof - this will be a composite receipt with assumptions let receipt = default_prover() .prove(env, EXPONENTIATE_ELF) .unwrap() .receipt; // At this point, the receipt contains an assumption // To resolve it, we need to create a succinct or Groth16 receipt receipt.verify(EXPONENTIATE_ID).unwrap();}
Here’s a real-world example inspired by RSA encryption:
Complete Example
use risc0_zkvm::{ExecutorEnv, default_prover, ProverOpts};use methods::*;fn main() { // Step 1: Alice proves knowledge of factorization // (Similar to RSA key generation) let (multiply_receipt, n) = prove_multiplication(17, 23); // Step 2: Bob uses Alice's public key (n) to encrypt a message // The zkVM proves the encryption is valid AND that n has known factors let env = ExecutorEnv::builder() .add_assumption(multiply_receipt) // Verify n's factorization .unwrap() .write(&(n, 9u64, 100u64)) // (modulus, exponent, secret) .unwrap() .build() .unwrap(); // Generate composite receipt (fast, but contains assumptions) let composite_receipt = default_prover() .prove(env.clone(), EXPONENTIATE_ELF) .unwrap() .receipt; // Verify the composite receipt (assumptions not yet resolved) composite_receipt.verify(EXPONENTIATE_ID).unwrap(); // Step 3: Resolve assumptions to create a single, verifiable proof let opts = ProverOpts::succinct(); let succinct_receipt = default_prover() .prove_with_opts(env, EXPONENTIATE_ELF, &opts) .unwrap() .receipt; // The succinct receipt proves BOTH: // 1. The modulus n has a known factorization // 2. The ciphertext is correctly computed succinct_receipt.verify(EXPONENTIATE_ID).unwrap(); let (n, e, ciphertext): (u64, u64, u64) = succinct_receipt.journal.decode().unwrap(); println!( "Ciphertext {} computed under modulus {} with known factors", ciphertext, n );}fn prove_multiplication(a: u64, b: u64) -> (Receipt, u64) { let env = ExecutorEnv::builder() .write(&(a, b)).unwrap() .build().unwrap(); let receipt = default_prover() .prove(env, MULTIPLY_ELF) .unwrap() .receipt; let product: u64 = receipt.journal.decode().unwrap(); (receipt, product)}
You can compose proofs recursively to arbitrary depth:
// Proof Alet receipt_a = prove_computation_a();// Proof B depends on Alet env_b = ExecutorEnv::builder() .add_assumption(receipt_a) .unwrap() .build().unwrap();let receipt_b = default_prover().prove(env_b, METHOD_B_ELF).unwrap().receipt;// Proof C depends on both A and B let env_c = ExecutorEnv::builder() .add_assumption(receipt_a) .unwrap() .add_assumption(receipt_b) .unwrap() .build().unwrap();// Final succinct receipt resolves all assumptions in the entire treelet opts = ProverOpts::succinct();let final_receipt = default_prover() .prove_with_opts(env_c, METHOD_C_ELF, &opts) .unwrap() .receipt;
use risc0_zkvm::guest::env;// Verify a receipt with a specific image IDenv::verify(image_id, &journal_bytes)?;// The journal_bytes should be serialized output from the verified programlet expected_output = risc0_zkvm::serde::to_vec(&value).unwrap();env::verify(IMAGE_ID, &expected_output)?;
Parameters:
image_id: [u32; 8] - The image ID of the program that generated the receipt
journal: &[u8] - The expected journal contents
Effect: Adds an assumption to the current receipt claim
Verify Image IDs: Always verify receipts against the correct image ID. Using the wrong ID will result in verification failures or security vulnerabilities.
Journal Matching: The journal bytes passed to env::verify() must exactly match the journal in the provided assumption receipt.
Performance: Composite receipts with assumptions are fast to generate. Defer resolution to succinct/Groth16 proving until you need a fully resolved proof.