risc0_aggregation/
receipt.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;
16
17use alloy_primitives::B256;
18use alloy_sol_types::SolValue;
19use risc0_binfmt::{tagged_struct, Digestible};
20use risc0_zkvm::{
21    sha,
22    sha::{Digest, Sha256, DIGEST_BYTES},
23    InnerReceipt, MaybePruned, Receipt, ReceiptClaim, VerifierContext,
24};
25use serde::{Deserialize, Serialize};
26
27use crate::{merkle_path_root, GuestState, MerkleMountainRange, Seal};
28
29// TODO(#353)
30//pub use guest_set_builder::{SET_BUILDER_ELF, SET_BUILDER_ID, SET_BUILDER_PATH};
31
32/// A receipt for a claim that is part of a set of verified claims (i.e. an aggregation).
33#[derive(Clone, Debug, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct SetInclusionReceipt<Claim>
36where
37    Claim: Digestible + Clone + Serialize,
38{
39    /// Claim containing information about the computation that this receipt proves.
40    ///
41    /// The standard claim type is [ReceiptClaim], which represents a RISC-V
42    /// zkVM execution.
43    pub claim: MaybePruned<Claim>,
44
45    /// Root receipt attesting to the validity of all claims included in the set committed to by
46    /// the Merkle root in the journal of this receipt. It is required that this receipt was
47    /// produced by running the aggregation set builder, which verifies each receipt before adding
48    /// it to the set represented by a Merkle tree.
49    ///
50    /// In certain contexts, the root claim can be omitted. In particular, the zkVM guest can
51    /// verify the root by making an assumption (i.e. by calling `env::verify`), and verifies in an
52    /// EVM context may reference previously proven claims via a verification cache in storage.
53    pub root: Option<Receipt>,
54
55    /// Merkle proof for inclusion in the set of claims attested to by the root receipt.
56    pub merkle_path: Vec<Digest>,
57
58    /// A digest of the verifier parameters that can be used to verify this receipt.
59    ///
60    /// Acts as a fingerprint to identify differing proof system or circuit versions between a
61    /// prover and a verifier. Is not intended to contain the full verifier parameters, which must
62    /// be provided by a trusted source (e.g. packaged with the verifier code).
63    pub verifier_parameters: Digest,
64}
65
66#[derive(thiserror::Error, Debug)]
67pub enum VerificationError {
68    #[error("{0}")]
69    Base(risc0_zkp::verify::VerificationError),
70    #[error("root receipt claim does not match expected set builder claim: {claim_digest} != {expected}")]
71    ClaimDigestDoesNotMatch {
72        claim_digest: Digest,
73        expected: Digest,
74    },
75    #[error("failed to confirm the validity of the set root: {path_root}")]
76    RootNotVerified { path_root: Digest },
77}
78
79impl From<core::convert::Infallible> for VerificationError {
80    fn from(_: core::convert::Infallible) -> Self {
81        unreachable!()
82    }
83}
84
85impl From<risc0_zkp::verify::VerificationError> for VerificationError {
86    fn from(err: risc0_zkp::verify::VerificationError) -> Self {
87        VerificationError::Base(err)
88    }
89}
90
91#[derive(thiserror::Error, Debug)]
92pub enum SetInclusionEncodingError {
93    #[error("unsupported receipt type")]
94    UnsupportedReceipt,
95}
96
97#[derive(thiserror::Error, Debug)]
98pub enum SetInclusionDecodingError {
99    #[error("unsupported receipt type")]
100    UnsupportedReceipt,
101    #[error("Digest decoding error")]
102    Digest,
103    #[error("failed to decode aggregation seal from bytes")]
104    SolType(#[from] alloy_sol_types::Error),
105}
106
107impl<Claim> SetInclusionReceipt<Claim>
108where
109    Claim: Digestible + Clone + Serialize,
110{
111    /* TODO(#353)
112    /// Construct a [SetInclusionReceipt] with the given Merkle inclusion path and claim.
113    ///
114    /// Path should contain all sibling nodes in the tree from the leaf to the root. Note that the
115    /// path does not include the leaf or the root itself. Resulting receipt will have default
116    /// verifier parameters and no root receipt.
117    pub fn from_path(claim: impl Into<MaybePruned<Claim>>, merkle_path: Vec<Digest>);
118    }
119    */
120
121    /// Construct a [SetInclusionReceipt] with the given Merkle inclusion path and claim.
122    ///
123    /// Path should contain all sibling nodes in the tree from the leaf to the root. Note that the
124    /// path does not include the leaf or the root itself. Resulting receipt will have the given
125    /// verifier parameter digest and no root receipt.
126    pub fn from_path_with_verifier_params(
127        claim: impl Into<MaybePruned<Claim>>,
128        merkle_path: Vec<Digest>,
129        verifier_parameters: impl Into<Digest>,
130    ) -> Self {
131        Self {
132            claim: claim.into(),
133            root: None,
134            merkle_path,
135            verifier_parameters: verifier_parameters.into(),
136        }
137    }
138
139    /// Add the given root receipt to this set inclusion receipt.
140    ///
141    /// See [SetInclusionReceipt::root] for more information about the root receipt.
142    pub fn with_root(self, root_receipt: Receipt) -> Self {
143        Self {
144            root: Some(root_receipt),
145            ..self
146        }
147    }
148
149    /// Drops the root receipt from this [SetInclusionReceipt].
150    ///
151    /// This is useful when the verifier has a cache of verified roots, as is the case for smart
152    /// contract verifiers. Use this method when submitting this receipt as part of a batch of
153    /// receipts to be verified, to reduce the encoded size of this receipt.
154    pub fn without_root(self) -> Self {
155        Self { root: None, ..self }
156    }
157
158    /* TODO(#353)
159    /// Verify the integrity of this receipt, ensuring the claim is attested to by the seal.
160    pub fn verify_integrity(&self) -> Result<(), VerificationError> {
161        self.verify_integrity_with_context(
162            &VerifierContext::default(),
163            SetInclusionReceiptVerifierParameters::default(),
164            Some(RecursionVerifierParameters::default()),
165        )
166    }
167    */
168
169    /// Verify the integrity of this receipt, ensuring the claim is attested to by the seal.
170    // TODO: Use a different error type (e.g. the one from risc0-zkvm).
171    pub fn verify_integrity_with_context(
172        &self,
173        ctx: &VerifierContext,
174        set_verifier_params: SetInclusionReceiptVerifierParameters,
175        // used when target_os = zkvm
176        _recursion_verifier_params: Option<RecursionVerifierParameters>,
177    ) -> Result<(), VerificationError> {
178        let path_root = merkle_path_root(&self.claim.digest::<sha::Impl>(), &self.merkle_path);
179
180        // Calculate the expected value of the journal generated by the aggregation set builder.
181        let expected_root_claim = ReceiptClaim::ok(
182            set_verifier_params.image_id,
183            GuestState {
184                self_image_id: set_verifier_params.image_id,
185                mmr: MerkleMountainRange::new_finalized(path_root),
186            }
187            .encode(),
188        );
189
190        // If provided, directly verify the provided root receipt and check its consistency against
191        // the calculated root of the provided Merkle path.
192        if let Some(ref root_receipt) = self.root {
193            root_receipt.verify_integrity_with_context(ctx)?;
194            if root_receipt.claim()?.digest::<sha::Impl>()
195                != expected_root_claim.digest::<sha::Impl>()
196            {
197                return Err(VerificationError::ClaimDigestDoesNotMatch {
198                    claim_digest: root_receipt.claim()?.digest::<sha::Impl>(),
199                    expected: expected_root_claim.digest::<sha::Impl>(),
200                });
201            }
202            return Ok(());
203        }
204
205        #[cfg(target_os = "zkvm")]
206        if let Some(params) = _recursion_verifier_params {
207            risc0_zkvm::guest::env::verify_assumption(
208                expected_root_claim.digest::<sha::Impl>(),
209                params.control_root.unwrap_or(Digest::ZERO),
210            )?;
211            return Ok(());
212        }
213
214        Err(VerificationError::RootNotVerified { path_root })
215    }
216
217    /// Encode the seal of the given receipt for use with EVM smart contract verifiers.
218    ///
219    /// Appends the verifier selector, determined from the first 4 bytes of the verifier
220    /// parameters digest, which contains the aggregation set builder image ID. If non-empty, the
221    /// root receipt will be appended.
222    pub fn abi_encode_seal(&self) -> Result<Vec<u8>, SetInclusionEncodingError> {
223        let selector = &self.verifier_parameters.as_bytes()[..4];
224        let merkle_path: Vec<B256> = self
225            .merkle_path
226            .iter()
227            .map(|x| <[u8; DIGEST_BYTES]>::from(*x).into())
228            .collect();
229        let root_seal: Vec<u8> = self.root.as_ref().map(encode_seal).unwrap_or(Ok(vec![]))?;
230        let seal = Seal {
231            path: merkle_path,
232            root_seal: root_seal.into(),
233        };
234        let mut encoded_seal = Vec::<u8>::with_capacity(selector.len() + seal.abi_encoded_size());
235        encoded_seal.extend_from_slice(selector);
236        encoded_seal.extend_from_slice(&seal.abi_encode());
237        Ok(encoded_seal)
238    }
239}
240
241fn extract_path(seal: &[u8]) -> Result<Vec<Digest>, SetInclusionDecodingError> {
242    // Early return if seal is too short to contain a path
243    if seal.len() <= 4 {
244        return Ok(Vec::new());
245    }
246
247    // Skip the first 4 bytes (selector) and decode the seal
248    let aggregation_seal = <Seal>::abi_decode(&seal[4..], true)?;
249
250    // Convert each path element to a Digest
251    aggregation_seal
252        .path
253        .iter()
254        .map(|x| Digest::try_from(x.as_slice()).map_err(|_| SetInclusionDecodingError::Digest))
255        .collect()
256}
257
258pub fn decode_set_inclusion_seal(
259    seal: &[u8],
260    claim: ReceiptClaim,
261    verifier_parameters: Digest,
262) -> Result<SetInclusionReceipt<ReceiptClaim>, SetInclusionDecodingError> {
263    let receipt = SetInclusionReceipt::from_path_with_verifier_params(
264        claim.clone(),
265        extract_path(seal)?,
266        verifier_parameters,
267    );
268
269    Ok(receipt)
270}
271
272// TODO: Extract this method to a core crate to dedup with the one in risc0-ethereum-contracts
273fn encode_seal(receipt: &risc0_zkvm::Receipt) -> Result<Vec<u8>, SetInclusionEncodingError> {
274    match receipt.inner.clone() {
275        InnerReceipt::Fake(receipt) => {
276            let seal = receipt.claim.digest::<sha::Impl>().as_bytes().to_vec();
277            let selector = &[0u8; 4];
278            // Create a new vector with the capacity to hold both selector and seal
279            let mut selector_seal = Vec::with_capacity(selector.len() + seal.len());
280            selector_seal.extend_from_slice(selector);
281            selector_seal.extend_from_slice(&seal);
282            Ok(selector_seal)
283        }
284        InnerReceipt::Groth16(receipt) => {
285            let selector = &receipt.verifier_parameters.as_bytes()[..4];
286            // Create a new vector with the capacity to hold both selector and seal
287            let mut selector_seal = Vec::with_capacity(selector.len() + receipt.seal.len());
288            selector_seal.extend_from_slice(selector);
289            selector_seal.extend_from_slice(receipt.seal.as_ref());
290            Ok(selector_seal)
291        }
292        _ => Err(SetInclusionEncodingError::UnsupportedReceipt),
293    }
294}
295
296/// Verifier parameters used to verify a [SetInclusionReceipt].
297#[derive(Clone, Debug, Deserialize, Serialize)]
298pub struct SetInclusionReceiptVerifierParameters {
299    /// Image ID for the aggregation set builder guest.
300    pub image_id: Digest,
301}
302
303impl Digestible for SetInclusionReceiptVerifierParameters {
304    /// Hash the [SetInclusionReceiptVerifierParameters] to get a digest of the struct.
305    fn digest<S: Sha256>(&self) -> Digest {
306        tagged_struct::<S>(
307            "risc0.SetInclusionReceiptVerifierParameters",
308            &[self.image_id],
309            &[],
310        )
311    }
312}
313
314/* TODO(#353)
315impl Default for SetInclusionReceiptVerifierParameters {
316    /// Default set of parameters used to verify a
317    /// [SetInclusionReceipt][super::SetInclusionReceipt].
318    fn default() -> Self {
319        Self {
320            image_id: SET_BUILDER_ID.into(),
321        }
322    }
323}
324*/
325
326// TODO(victor): Move this into risc0-zkvm?
327/// Verifier parameters used for recursive verification (e.g. via env::verify) of receipts.
328#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
329pub struct RecursionVerifierParameters {
330    /// Control root to use for verifying claims via env::verify_assumption. If not provided, the
331    /// zero digest will be used, which means the same (zkVM) control root used to verify the guest
332    /// execution will be used to verify this claim.
333    pub control_root: Option<Digest>,
334}