Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/risc0/risc0/llms.txt

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.

Why Proof Composition?

Proof composition unlocks powerful capabilities:
  • Modularity: Break complex computations into smaller, reusable components
  • Privacy Preservation: Combine proofs from different parties without revealing private inputs
  • Efficient Verification: Verify multiple proofs in a single, constant-size receipt
  • Conditional Execution: Build programs that depend on verified external computations
To learn more about the power of proof composition, check out the RISC Zero blog post.

How It Works

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 approach leverages RISC Zero’s hyper-efficient recursion circuit to achieve proof composition with minimal overhead.

Basic Example

Here’s a simple example demonstrating proof composition:

Guest Program (Composer)

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 verified

fn 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
}

Host Program

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();
}

Resolving Assumptions

Assumptions are automatically resolved when you generate succinct or Groth16 receipts:
use risc0_zkvm::ProverOpts;

// Build environment with assumptions
let env = ExecutorEnv::builder()
    .add_assumption(receipt_a)
    .unwrap()
    .add_assumption(receipt_b)
    .unwrap()
    .build()
    .unwrap();

// Generate succinct receipt - assumptions are resolved automatically
let opts = ProverOpts::succinct();
let receipt = default_prover()
    .prove_with_opts(env, METHOD_ELF, &opts)
    .unwrap()
    .receipt;

// This receipt is now fully resolved (no assumptions)
receipt.verify(METHOD_ID).unwrap();

Complete Composition Example

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)
}

Multi-Level Composition

You can compose proofs recursively to arbitrary depth:
// Proof A
let receipt_a = prove_computation_a();

// Proof B depends on A
let 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 tree
let opts = ProverOpts::succinct();
let final_receipt = default_prover()
    .prove_with_opts(env_c, METHOD_C_ELF, &opts)
    .unwrap()
    .receipt;

Guest API Reference

env::verify()

Verify a receipt inside the guest program:
use risc0_zkvm::guest::env;

// Verify a receipt with a specific image ID
env::verify(image_id, &journal_bytes)?;

// The journal_bytes should be serialized output from the verified program
let 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

Host API Reference

add_assumption()

Provide a receipt that will be verified in the guest:
use risc0_zkvm::ExecutorEnv;

let env = ExecutorEnv::builder()
    .add_assumption(receipt)  // Receipt to verify in guest
    .unwrap()
    .build()
    .unwrap();
Parameter: receipt: Receipt - A valid RISC Zero receipt Returns: Result<ExecutorEnvBuilder> - Builder for chaining

Receipt Types and Assumptions

Receipt TypeContains Assumptions?When to Use
CompositeYesFast proving, will resolve later
SuccinctNo (automatically resolved)Constant-size proof needed
Groth16No (automatically resolved)Blockchain verification

Best Practices

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.

Advanced Topics

Conditional Receipts

You can create receipts that are valid only under certain conditions:
if should_verify_external_proof {
    env::verify(EXTERNAL_ID, &external_journal)?;
}

Proof Aggregation

Combine multiple independent proofs:
let env = ExecutorEnv::builder()
    .add_assumption(receipt_1)
    .unwrap()
    .add_assumption(receipt_2)
    .unwrap()
    .add_assumption(receipt_3)
    .unwrap()
    .build()
    .unwrap();

// Succinct receipt proves all three computations
let opts = ProverOpts::succinct();
let aggregated = default_prover()
    .prove_with_opts(env, AGGREGATOR_ELF, &opts)
    .unwrap()
    .receipt;

Examples and Resources

Composition Example

Full working example on GitHub

Study Club Recording

Deep dive into proof composition

Recursion Overview

Learn about RISC Zero’s recursion circuit

Blog: Proof Composition

Understand the power of modular proving

Next Steps

Local Proving

Set up local proving for development

Groth16 Proofs

Generate blockchain-ready proofs