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:
- The cryptographic seal is valid
- The code (Image ID) matches expectations
- The guest exited successfully
- 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.
Receipt Size
| Receipt Type | Typical Size | Verification Time |
|---|
| Segment | ~200KB per segment | ~100ms per segment |
| Succinct | ~200KB (constant) | ~100ms |
| Groth16 | ~200 bytes | ~5ms (on-chain) |
| Composite | Varies | Varies |
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