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.

Receipts and Verification

A receipt is the cryptographic proof that certifies correct execution of guest code in the zkVM. This page explores the structure of receipts, how they’re generated, and how verification works.

What is a Receipt?

A receipt serves as a zero-knowledge proof of computation. It proves:
  • Specific code (identified by an Image ID) was executed
  • The execution completed successfully (or with a specific exit code)
  • The journal contains the actual public outputs produced
  • All of this without revealing private inputs or execution details
Think of a receipt as a tamper-proof certificate that says: “This journal was produced by correctly running this specific program.”

Receipt Structure

From the source code (risc0/zkvm/src/receipt.rs):
pub struct Receipt {
    /// The polymorphic InnerReceipt
    pub inner: InnerReceipt,
    
    /// The public commitment written by the guest
    pub journal: Journal,
    
    /// Metadata providing context on the receipt
    pub metadata: ReceiptMetadata,
}

Components

InnerReceipt The InnerReceipt is an enum containing the actual cryptographic proof:
pub enum InnerReceipt {
    Segment(SegmentReceipt),      // Single segment proof
    Succinct(SuccinctReceipt),    // Recursively composed STARK
    Composite(CompositeReceipt),  // Multiple receipts with assumptions
    Groth16(Groth16Receipt),      // SNARK for on-chain verification
    Fake(FakeReceipt),            // Dev mode placeholder
}
Journal The journal contains public outputs from guest execution:
pub struct Journal {
    pub bytes: Vec<u8>,
}

impl Journal {
    /// Decode journal using risc0 deserializer
    pub fn decode<T: DeserializeOwned>(&self) -> Result<T> {
        from_slice(&self.bytes)
    }
}
ReceiptMetadata Metadata about the proving system and receipt version (not cryptographically bound):
pub struct ReceiptMetadata {
    pub verifier_parameters: String,
}
Metadata is not cryptographically authenticated. Don’t use it for security-critical decisions like version checking.

Receipt Types

Segment Receipt

Proof of a single execution segment:
  • Generated directly by the RISC-V prover
  • Contains STARK proof for one segment of execution
  • Proof size grows with execution length
  • Used as building blocks for other receipt types
pub struct SegmentReceipt {
    pub seal: Vec<u32>,
    pub claim: ReceiptClaim,
    pub verifier_parameters: SegmentReceiptVerifierParameters,
}

Succinct Receipt

A recursively composed proof with constant size:
  • Multiple segment receipts aggregated into one
  • Fixed size regardless of execution length
  • Hides information about execution length
  • Generated by the Recursion Prover
pub struct SuccinctReceipt {
    pub seal: Vec<u32>,
    pub claim: MaybePruned<ReceiptClaim>,
    pub verifier_parameters: SuccinctReceiptVerifierParameters,
}
Succinct receipts are ideal for most applications - they’re compact and reveal minimal information about execution.

Groth16 Receipt

A SNARK proof optimized for on-chain verification:
  • Compressed to ~200 bytes
  • Efficient verification on Ethereum and EVM chains
  • Generated by the STARK-to-SNARK prover
  • Uses BN254 elliptic curve pairing
pub struct Groth16Receipt {
    pub seal: Groth16Seal,
    pub claim: MaybePruned<ReceiptClaim>,
    pub verifier_parameters: Groth16ReceiptVerifierParameters,
}
Typical use case:
use risc0_zkvm::{ProverOpts, ReceiptKind};

let opts = ProverOpts {
    receipt_kind: ReceiptKind::Groth16,
};

let prove_info = prover.prove_with_opts(env, ELF, &opts)?;
let receipt = prove_info.receipt;

// Now verify on-chain

Composite Receipt

A collection of receipts with assumptions:
  • Used for proof composition
  • Contains multiple inner receipts
  • Tracks assumption dependencies
  • Enables modular proof construction
pub struct CompositeReceipt {
    pub segments: Vec<SegmentReceipt>,
    pub assumptions: Vec<AssumptionReceipt>,
    pub verifier_parameters: CompositeReceiptVerifierParameters,
}

Fake Receipt

Development mode placeholder:
  • No actual cryptographic proof
  • Generated instantly
  • Only valid when dev mode is enabled
  • Never use in production
// Enable dev mode
std::env::set_var("RISC0_DEV_MODE", "1");

// Receipts are generated instantly but provide no security
let receipt = prover.prove(env, ELF)?.receipt;

Receipt Claims

Every receipt proves a ReceiptClaim:
pub struct ReceiptClaim {
    /// Pre-state digest
    pub pre: MaybePruned<SystemState>,
    
    /// Post-state digest  
    pub post: MaybePruned<SystemState>,
    
    /// Exit code
    pub exit_code: ExitCode,
    
    /// Input digest (optional)
    pub input: MaybePruned<Input>,
    
    /// Output (journal + assumptions)
    pub output: MaybePruned<Output>,
}

SystemState

Represents zkVM state at a point in time:
pub struct SystemState {
    pub pc: u32,              // Program counter
    pub merkle_root: Digest,  // Memory merkle root
}

ExitCode

How the guest program terminated:
pub enum ExitCode {
    Halted(u8),      // Normal exit with code
    Paused(u8),      // Execution paused
    Fault,           // Invalid execution
    // ... other variants
}

Output

Public outputs from execution:
pub struct Output {
    pub journal: MaybePruned<Digest>,        // Journal digest
    pub assumptions: MaybePruned<Assumptions>, // Verified receipts
}
MaybePruned<T> can be either the full value or just a cryptographic digest, allowing flexibility in receipt size vs. information available.

Verification Process

Basic Verification

The simplest verification checks that:
  1. The cryptographic seal is valid
  2. The code (Image ID) matches expectations
  3. The guest exited successfully
  4. The journal hasn’t been tampered with
use risc0_zkvm::Receipt;

// Verify receipt
receipt.verify(IMAGE_ID)?;

// Now safe to use journal
let output: OutputType = receipt.journal.decode()?;
Internal verification logic (from risc0/zkvm/src/receipt.rs:152-194):
pub fn verify(&self, image_id: impl Into<Digest>) -> Result<(), VerificationError> {
    self.verify_with_context(&VerifierContext::default(), image_id)
}

pub fn verify_with_context(
    &self,
    ctx: &VerifierContext,
    image_id: impl Into<Digest>,
) -> Result<(), VerificationError> {
    // Verify cryptographic integrity
    self.inner.verify_integrity_with_context(ctx)?;
    
    // Check claim matches expectations
    let expected_claim = ReceiptClaim::ok(
        image_id, 
        MaybePruned::Pruned(self.journal.digest())
    );
    
    if expected_claim.digest() != self.inner.claim()?.digest() {
        return Err(VerificationError::ClaimDigestMismatch {
            expected: expected_claim.digest(),
            received: self.claim()?.digest(),
        });
    }
    
    Ok(())
}

Integrity Verification

For more advanced use cases, verify integrity without checking Image ID or exit code:
receipt.verify_integrity_with_context(&ctx)?;

// Now manually check the claim
let claim = receipt.claim()?.as_value()?;

match claim.exit_code {
    ExitCode::Halted(0) => println!("Success"),
    ExitCode::Halted(n) => println!("Error: {}", n),
    _ => println!("Unexpected exit"),
}
This is useful when:
  • You need to handle multiple possible Image IDs
  • Non-zero exit codes are acceptable
  • You’re implementing custom verification logic

Verifier Context

The VerifierContext controls verification parameters:
pub struct VerifierContext {
    // Allowed control IDs for recursion programs
    // Verified against ALLOWED_CONTROL_ROOT
}

impl Default for VerifierContext {
    fn default() -> Self {
        // Uses standard RISC Zero control root
    }
}
Custom context (advanced):
let ctx = VerifierContext {
    // Custom verification parameters
};

receipt.verify_with_context(&ctx, IMAGE_ID)?;

Verification Errors

Possible verification failures:
pub enum VerificationError {
    ClaimDigestMismatch { expected: Digest, received: Digest },
    JournalDigestMismatch,
    ReceiptFormatError,
    InvalidProof,
    // ... other variants
}
Handling errors:
match receipt.verify(IMAGE_ID) {
    Ok(()) => {
        println!("Receipt verified!");
        let output: Output = receipt.journal.decode()?;
    }
    Err(VerificationError::ClaimDigestMismatch { expected, received }) => {
        eprintln!("Wrong code was executed!");
        eprintln!("Expected: {:?}", expected);
        eprintln!("Received: {:?}", received);
    }
    Err(VerificationError::InvalidProof) => {
        eprintln!("Cryptographic proof is invalid!");
    }
    Err(e) => {
        eprintln!("Verification failed: {:?}", e);
    }
}
Always handle verification errors. A failed verification means the receipt cannot be trusted.

Working with Journals

Writing to the Journal

In guest code:
use risc0_zkvm::guest::env;

// Commit any serializable type
env::commit(&my_struct);

// Commit multiple values
env::commit(&value1);
env::commit(&value2);
env::commit(&value3);

// Commit raw bytes (more efficient)
env::commit_slice(&bytes);

Reading from the Journal

In host code:
// Decode single value
let output: MyStruct = receipt.journal.decode()?;

// Decode multiple values (same order as committed)
let bytes = &receipt.journal.bytes;
let value1: Type1 = from_slice(&bytes[..])?;
let value2: Type2 = from_slice(&bytes[size1..])?;

// Or use a wrapper struct
#[derive(Deserialize)]
struct JournalOutput {
    value1: Type1,
    value2: Type2,
    value3: Type3,
}

let output: JournalOutput = receipt.journal.decode()?;

Journal Digest

The journal is committed via its digest:
use risc0_binfmt::Digestible;

let digest = receipt.journal.digest::<Sha256>();
This digest is proven in the ReceiptClaim, ensuring journal integrity.

Proof Composition

Verifying Receipts in the Guest

Guest code can verify other receipts:
// Guest code
use risc0_zkvm::guest::env;

// Read receipt data
let receipt_bytes: Vec<u8> = env::read();

// Verify it
env::verify(receipt_bytes, EXPECTED_IMAGE_ID)
    .expect("Receipt verification failed");

// The verification is now proven as part of this receipt

Assumptions

When a guest verifies receipts, they become “assumptions”:
pub struct Assumptions(pub Vec<Assumption>);

pub struct Assumption {
    pub claim: ReceiptClaim,
    pub control_root: Digest,
}
The final receipt tracks all assumptions, creating a proof dependency tree.

Building Modular Proofs

Example: Prove a computation pipeline
// Step 1: Prove data processing
let receipt1 = prove_step1(input)?;

// Step 2: Prove validation (verifies receipt1)
let env2 = ExecutorEnv::builder()
    .add_assumption(receipt1.clone())
    .build()?;
let receipt2 = prove_step2_with_assumption(env2)?;

// receipt2 now proves the entire pipeline

Serialization

Receipts are serializable for storage and transmission:
use borsh::{BorshSerialize, BorshDeserialize};
use serde::{Serialize, Deserialize};

// Borsh (more compact)
let bytes = receipt.try_to_vec()?;
let receipt: Receipt = BorshDeserialize::try_from_slice(&bytes)?;

// Serde (more flexible)
let json = serde_json::to_string(&receipt)?;
let receipt: Receipt = serde_json::from_str(&json)?;

// Bincode (efficient binary)
let bytes = bincode::serialize(&receipt)?;
let receipt: Receipt = bincode::deserialize(&bytes)?;

On-Chain Verification

For blockchain applications, use Groth16 receipts:
use risc0_zkvm::{ProverOpts, ReceiptKind};

// Generate Groth16 receipt
let opts = ProverOpts {
    receipt_kind: ReceiptKind::Groth16,
};
let receipt = prover.prove_with_opts(env, ELF, &opts)?.receipt;

// Extract Groth16-specific seal
if let InnerReceipt::Groth16(groth16) = receipt.inner {
    let seal_bytes = groth16.seal.as_bytes();
    // Submit to smart contract
    verify_on_chain(seal_bytes, journal_bytes)?;
}
See the Blockchain Integration docs for contract examples.

Performance Considerations

Receipt Size

Receipt TypeTypical SizeVerification Time
Segment~200KB per segment~100ms per segment
Succinct~200KB (constant)~100ms
Groth16~200 bytes~5ms (on-chain)
CompositeVariesVaries

Optimization Tips

Minimize journal size
// Less efficient - large journal
env::commit(&all_intermediate_data);

// More efficient - only commit final result
let final_result = process_all(data);
env::commit(&final_result);
Use appropriate receipt type
  • Development: Fake receipts (instant)
  • Testing: Succinct receipts (fast, compact)
  • Production: Succinct or Groth16 (depending on use case)
Cache verification results
// Verify once, cache result
let verified = receipt.verify(IMAGE_ID).is_ok();
if verified {
    // Use journal safely
}

Next Steps