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}