risc0_zkvm/receipt/
composite.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use alloc::{vec, vec::Vec};
16use core::fmt::Debug;
17
18use anyhow::Result;
19use borsh::{BorshDeserialize, BorshSerialize};
20use risc0_binfmt::{tagged_struct, Digestible, ExitCode};
21use risc0_circuit_recursion::CircuitImpl;
22use risc0_zkp::{
23    adapter::{CircuitInfo, PROOF_SYSTEM_INFO},
24    core::{digest::Digest, hash::sha::Sha256},
25    verify::VerificationError,
26};
27use serde::{Deserialize, Serialize};
28
29// Make succinct receipt available through this `receipt` module.
30use super::{
31    Groth16ReceiptVerifierParameters, SegmentReceipt, SegmentReceiptVerifierParameters,
32    SuccinctReceiptVerifierParameters, VerifierContext,
33};
34use crate::{
35    sha, Assumption, InnerAssumptionReceipt, MaybePruned, Output, PrunedValueError, ReceiptClaim,
36};
37
38/// A receipt composed of one or more [SegmentReceipt] structs proving a single execution with
39/// continuations, and zero or more [InnerAssumptionReceipt](crate::InnerAssumptionReceipt) structs
40/// proving any assumptions.
41#[derive(Clone, Debug, Deserialize, Serialize, BorshSerialize, BorshDeserialize)]
42#[cfg_attr(test, derive(PartialEq))]
43#[non_exhaustive]
44pub struct CompositeReceipt {
45    /// Segment receipts forming the proof of an execution with continuations.
46    pub segments: Vec<SegmentReceipt>,
47
48    /// An ordered list of assumptions, either proven or unresolved, made within
49    /// the continuation represented by the segment receipts. If any
50    /// assumptions are unresolved, this receipt is only _conditionally_
51    /// valid.
52    // TODO(#982): Allow for unresolved assumptions in this list.
53    pub assumption_receipts: Vec<InnerAssumptionReceipt>,
54
55    /// A digest of the verifier parameters that can be used to verify this receipt.
56    ///
57    /// Acts as a fingerprint to identity differing proof system or circuit versions between a
58    /// prover and a verifier. Is not intended to contain the full verifier parameters, which must
59    /// be provided by a trusted source (e.g. packaged with the verifier code).
60    pub verifier_parameters: Digest,
61}
62
63impl CompositeReceipt {
64    /// Verify the integrity of this receipt, ensuring the claim is attested
65    /// to by the seal.
66    pub fn verify_integrity_with_context(
67        &self,
68        ctx: &VerifierContext,
69    ) -> Result<(), VerificationError> {
70        tracing::debug!("CompositeReceipt::verify_integrity_with_context");
71        // Verify the continuation, by verifying every segment receipt in order.
72        let (final_receipt, receipts) = self
73            .segments
74            .as_slice()
75            .split_last()
76            .ok_or(VerificationError::ReceiptFormatError)?;
77
78        // Verify each segment and its chaining to the next.
79        let mut expected_pre_state_digest = None;
80        for receipt in receipts {
81            receipt.verify_integrity_with_context(ctx)?;
82            tracing::debug!("claim: {:#?}", receipt.claim);
83            if let Some(id) = expected_pre_state_digest {
84                if id != receipt.claim.pre.digest::<sha::Impl>() {
85                    return Err(VerificationError::ImageVerificationError);
86                }
87            }
88            if receipt.claim.exit_code != ExitCode::SystemSplit {
89                return Err(VerificationError::UnexpectedExitCode);
90            }
91            if !receipt.claim.output.is_none() {
92                return Err(VerificationError::ReceiptFormatError);
93            }
94            expected_pre_state_digest = Some(
95                receipt
96                    .claim
97                    .post
98                    .as_value()
99                    .map_err(|_| VerificationError::ReceiptFormatError)?
100                    .digest::<sha::Impl>(),
101            );
102        }
103
104        // Verify the last receipt in the continuation.
105        final_receipt.verify_integrity_with_context(ctx)?;
106        tracing::debug!("final: {:#?}", final_receipt.claim);
107        if let Some(id) = expected_pre_state_digest {
108            if id != final_receipt.claim.pre.digest::<sha::Impl>() {
109                return Err(VerificationError::ImageVerificationError);
110            }
111        }
112
113        // Verify all assumptions on the receipt are resolved by attached receipts.
114        // Ensure that there is one receipt for every assumption. An explicit check is required
115        // because zip will terminate if either iterator terminates.
116        let assumptions = self.assumptions()?;
117        if assumptions.len() != self.assumption_receipts.len() {
118            tracing::debug!(
119                "only {} receipts provided for {} assumptions",
120                assumptions.len(),
121                self.assumption_receipts.len()
122            );
123            return Err(VerificationError::ReceiptFormatError);
124        }
125        for (assumption, receipt) in assumptions.into_iter().zip(self.assumption_receipts.iter()) {
126            let assumption_ctx = match assumption.control_root {
127                // If the control root is all zeroes, we should use the same verifier parameters.
128                Digest::ZERO => None,
129                // Otherwise, we should verify the assumption receipt using the guest-provided root.
130                control_root => Some(
131                    VerifierContext::empty()
132                        .with_suites(ctx.suites.clone())
133                        .with_succinct_verifier_parameters(SuccinctReceiptVerifierParameters {
134                            control_root,
135                            inner_control_root: None,
136                            proof_system_info: PROOF_SYSTEM_INFO,
137                            circuit_info: CircuitImpl::CIRCUIT_INFO,
138                        }),
139                ),
140            };
141            tracing::debug!("verifying assumption: {assumption:?}");
142            receipt.verify_integrity_with_context(assumption_ctx.as_ref().unwrap_or(ctx))?;
143            if receipt.claim_digest()? != assumption.claim {
144                tracing::debug!(
145                    "verifying assumption failed due to claim mismatch: assumption: {assumption:?}, receipt claim digest: {}",
146                    receipt.claim_digest()?
147                );
148                return Err(VerificationError::ClaimDigestMismatch {
149                    expected: assumption.claim,
150                    received: receipt.claim_digest()?,
151                });
152            }
153        }
154
155        Ok(())
156    }
157
158    /// Returns the [ReceiptClaim] for this [CompositeReceipt].
159    pub fn claim(&self) -> Result<ReceiptClaim, VerificationError> {
160        let first_claim = &self
161            .segments
162            .first()
163            .ok_or(VerificationError::ReceiptFormatError)?
164            .claim;
165        let last_claim = &self
166            .segments
167            .last()
168            .ok_or(VerificationError::ReceiptFormatError)?
169            .claim;
170
171        // Remove the assumptions from the last receipt claim, as the verify routine requires every
172        // assumption to have an associated verifiable receipt.
173        // TODO(#982) Support unresolved assumptions here by only removing the proven assumptions.
174        let output = last_claim
175            .output
176            .as_value()
177            .map_err(|_| VerificationError::ReceiptFormatError)?
178            .as_ref()
179            .map(|output| Output {
180                journal: output.journal.clone(),
181                assumptions: vec![].into(),
182            })
183            .into();
184
185        Ok(ReceiptClaim {
186            pre: first_claim.pre.clone(),
187            post: last_claim.post.clone(),
188            exit_code: last_claim.exit_code,
189            input: first_claim.input.clone(),
190            output,
191        })
192    }
193
194    fn assumptions(&self) -> Result<Vec<Assumption>, VerificationError> {
195        // Collect the assumptions from the output of the last segment, handling any pruned values
196        // encountered and returning and empty list if the output is None.
197        Ok(self
198            .segments
199            .last()
200            .ok_or(VerificationError::ReceiptFormatError)?
201            .claim
202            .output
203            .as_value()
204            .map_err(|_| VerificationError::ReceiptFormatError)?
205            .as_ref()
206            .map(|output| match output.assumptions.is_empty() {
207                true => Ok(Default::default()),
208                false => Ok(output
209                    .assumptions
210                    .as_value()?
211                    .iter()
212                    .map(|a| a.as_value().cloned())
213                    .collect::<Result<_, _>>()?),
214            })
215            .transpose()
216            .map_err(|_: PrunedValueError| VerificationError::ReceiptFormatError)?
217            .unwrap_or_default())
218    }
219
220    /// Total number of bytes used by the seals of this receipt.
221    pub fn seal_size(&self) -> usize {
222        // NOTE: This sum cannot overflow because all seals are in memory.
223        let mut result = 0;
224
225        for receipt in &self.segments {
226            result += receipt.seal_size();
227        }
228
229        // Take into account the assumption receipts since this is not a
230        // verifiable receipt without them.
231        for receipt in &self.assumption_receipts {
232            result += receipt.seal_size();
233        }
234
235        result
236    }
237}
238
239/// Verifier parameters for [CompositeReceipt][super::CompositeReceipt].
240///
241/// [CompositeReceipt][super::CompositeReceipt] is a collection of individual receipts that
242/// collectively  prove a claim. It can contain any of the individual receipt types, and so it's
243/// verifier is a combination of the verifiers for every other receipt type.
244#[derive(Clone, Debug, Deserialize, Serialize)]
245#[non_exhaustive]
246pub struct CompositeReceiptVerifierParameters {
247    /// Verifier parameters related to [SegmentReceipt].
248    pub segment: MaybePruned<SegmentReceiptVerifierParameters>,
249
250    /// Verifier parameters related to [SuccinctReceipt][crate::SuccinctReceipt].
251    pub succinct: MaybePruned<SuccinctReceiptVerifierParameters>,
252
253    /// Verifier parameters related to [Groth16Receipt][crate::Groth16Receipt].
254    pub groth16: MaybePruned<Groth16ReceiptVerifierParameters>,
255}
256
257impl CompositeReceiptVerifierParameters {
258    /// Construct verifier parameters that will accept receipts with control any of the default
259    /// control ID associated with cycle counts as powers of two (po2) up to the given max
260    /// inclusive.
261    #[stability::unstable]
262    pub fn from_max_po2(po2_max: usize) -> Self {
263        Self {
264            segment: MaybePruned::Value(SegmentReceiptVerifierParameters::default()),
265            succinct: MaybePruned::Value(SuccinctReceiptVerifierParameters::from_max_po2(po2_max)),
266            groth16: MaybePruned::Value(Groth16ReceiptVerifierParameters::from_max_po2(po2_max)),
267        }
268    }
269
270    /// Construct verifier parameters that will accept receipts with control any of the default
271    /// control ID associated with cycle counts of all supported powers of two (po2).
272    #[stability::unstable]
273    pub fn all_po2s() -> Self {
274        Self::from_max_po2(risc0_zkp::MAX_CYCLES_PO2)
275    }
276}
277
278impl Digestible for CompositeReceiptVerifierParameters {
279    /// Hash the [Groth16ReceiptVerifierParameters] to get a digest of the struct.
280    fn digest<S: Sha256>(&self) -> Digest {
281        tagged_struct::<S>(
282            "risc0.CompositeReceiptVerifierParameters",
283            &[
284                &self.segment.digest::<S>(),
285                &self.succinct.digest::<S>(),
286                &self.groth16.digest::<S>(),
287            ],
288            &[],
289        )
290    }
291}
292
293impl Default for CompositeReceiptVerifierParameters {
294    /// Default set of parameters used to verify a [CompositeReceipt].
295    fn default() -> Self {
296        Self {
297            segment: MaybePruned::Value(Default::default()),
298            succinct: MaybePruned::Value(Default::default()),
299            groth16: MaybePruned::Value(Default::default()),
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::CompositeReceiptVerifierParameters;
307    use crate::sha::Digestible;
308    use risc0_zkp::core::digest::digest;
309
310    // Check that the verifier parameters has a stable digest (and therefore a stable value). This struct
311    // encodes parameters used in verification, and so this value should be updated if and only if
312    // a change to the verifier parameters is expected. Updating the verifier parameters will result in
313    // incompatibility with previous versions.
314    #[test]
315    fn composite_receipt_verifier_parameters_is_stable() {
316        assert_eq!(
317            CompositeReceiptVerifierParameters::default().digest(),
318            digest!("3daead8f1ec08eb96b60ce6cad42f82eba80f6cf89ba5007ca317e57256b6038")
319        );
320    }
321}