1use alloc::{
16 collections::{BTreeSet, VecDeque},
17 format,
18 string::{String, ToString},
19 vec::Vec,
20};
21
22use anyhow::bail;
23use borsh::{BorshDeserialize, BorshSerialize};
24use derive_more::Debug;
25use risc0_binfmt::{read_sha_halfs, tagged_struct, Digestible};
26use risc0_circuit_recursion::{
27 control_id::{ALLOWED_CONTROL_ROOT, MIN_LIFT_PO2, POSEIDON2_CONTROL_IDS, SHA256_CONTROL_IDS},
28 CircuitImpl, CIRCUIT,
29};
30use risc0_core::field::baby_bear::BabyBearElem;
31use risc0_zkp::{
32 adapter::{CircuitInfo, ProtocolInfo, PROOF_SYSTEM_INFO},
33 core::{
34 digest::Digest,
35 hash::{hash_suite_from_name, sha::Sha256},
36 },
37 verify::VerificationError,
38};
39use serde::{Deserialize, Serialize};
40
41use crate::{
42 receipt::{
43 merkle::{MerkleGroup, MerkleProof},
44 VerifierContext,
45 },
46 receipt_claim::{MaybePruned, Unknown},
47 sha,
48};
49
50#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
58#[cfg_attr(test, derive(PartialEq))]
59#[non_exhaustive]
60pub struct SuccinctReceipt<Claim>
61where
62 Claim: Digestible + core::fmt::Debug + Clone + Serialize,
63{
64 #[debug("{} bytes", self.get_seal_bytes().len())]
67 pub seal: Vec<u32>,
68
69 pub control_id: Digest,
72
73 pub claim: MaybePruned<Claim>,
78
79 pub hashfn: String,
81
82 pub verifier_parameters: Digest,
88
89 pub control_inclusion_proof: MerkleProof,
91}
92
93impl<Claim> SuccinctReceipt<Claim>
94where
95 Claim: Digestible + core::fmt::Debug + Clone + Serialize,
96{
97 pub fn verify_integrity(&self) -> Result<(), VerificationError> {
100 self.verify_integrity_with_context(&VerifierContext::default())
101 }
102
103 pub fn verify_integrity_with_context(
106 &self,
107 ctx: &VerifierContext,
108 ) -> Result<(), VerificationError> {
109 let params = ctx
110 .succinct_verifier_parameters
111 .as_ref()
112 .ok_or(VerificationError::VerifierParametersMissing)?;
113
114 if params.proof_system_info != PROOF_SYSTEM_INFO {
118 return Err(VerificationError::ProofSystemInfoMismatch {
119 expected: PROOF_SYSTEM_INFO,
120 received: params.proof_system_info,
121 });
122 }
123 if params.circuit_info != CircuitImpl::CIRCUIT_INFO {
124 return Err(VerificationError::CircuitInfoMismatch {
125 expected: CircuitImpl::CIRCUIT_INFO,
126 received: params.circuit_info,
127 });
128 }
129
130 let suite = ctx
131 .suites
132 .get(&self.hashfn)
133 .ok_or(VerificationError::InvalidHashSuite)?;
134
135 let check_code = |_, control_id: &Digest| -> Result<(), VerificationError> {
136 self.control_inclusion_proof
137 .verify(control_id, ¶ms.control_root, suite.hashfn.as_ref())
138 .map_err(|_| {
139 tracing::debug!(
140 "failed to verify control inclusion proof for {control_id} against root {} with {}",
141 params.control_root,
142 suite.name,
143 );
144 VerificationError::ControlVerificationError {
145 control_id: *control_id,
146 }
147 })
148 };
149
150 risc0_zkp::verify::verify(&CIRCUIT, suite, &self.seal, check_code)?;
153
154 let output_elems: &[BabyBearElem] =
156 bytemuck::checked::cast_slice(&self.seal[..CircuitImpl::OUTPUT_SIZE]);
157 let mut seal_claim = VecDeque::new();
158 for elem in output_elems {
159 seal_claim.push_back(elem.as_u32())
160 }
161
162 let control_root: Digest = seal_claim
167 .drain(0..16)
168 .enumerate()
169 .filter_map(|(i, word)| (i & 1 == 0).then_some(word))
170 .collect::<Vec<_>>()
171 .try_into()
172 .map_err(|_| VerificationError::ReceiptFormatError)?;
173
174 if control_root != params.inner_control_root.unwrap_or(params.control_root) {
175 tracing::debug!(
176 "succinct receipt does not match the expected control root: decoded: {:#?}, expected: {:?}",
177 control_root,
178 params.inner_control_root.unwrap_or(params.control_root),
179 );
180 return Err(VerificationError::ControlVerificationError {
181 control_id: control_root,
182 });
183 }
184
185 let output_hash =
187 read_sha_halfs(&mut seal_claim).map_err(|_| VerificationError::ReceiptFormatError)?;
188 if output_hash != self.claim.digest::<sha::Impl>() {
189 tracing::debug!(
190 "succinct receipt claim does not match the output digest: claim: {:#?}, digest expected: {output_hash:?}",
191 self.claim,
192 );
193 return Err(VerificationError::JournalDigestMismatch);
194 }
195 Ok(())
197 }
198
199 pub fn get_seal_bytes(&self) -> Vec<u8> {
201 self.seal.iter().flat_map(|x| x.to_le_bytes()).collect()
202 }
203
204 pub fn seal_size(&self) -> usize {
206 core::mem::size_of_val(self.seal.as_slice())
207 }
208
209 #[cfg(feature = "prove")]
210 pub(crate) fn control_root(&self) -> anyhow::Result<Digest> {
211 let hash_suite = hash_suite_from_name(&self.hashfn)
212 .ok_or_else(|| anyhow::anyhow!("unsupported hash function: {}", self.hashfn))?;
213 Ok(self
214 .control_inclusion_proof
215 .root(&self.control_id, hash_suite.hashfn.as_ref()))
216 }
217
218 pub fn into_unknown(self) -> SuccinctReceipt<Unknown> {
221 SuccinctReceipt {
222 claim: MaybePruned::Pruned(self.claim.digest::<sha::Impl>()),
223 seal: self.seal,
224 control_id: self.control_id,
225 hashfn: self.hashfn,
226 verifier_parameters: self.verifier_parameters,
227 control_inclusion_proof: self.control_inclusion_proof,
228 }
229 }
230}
231
232pub(crate) fn allowed_control_ids(
234 hash_name: impl AsRef<str> + 'static,
235 po2_max: usize,
236) -> anyhow::Result<impl Iterator<Item = Digest>> {
237 let allowed_zkr_names: BTreeSet<String> =
241 ["join.zkr", "resolve.zkr", "identity.zkr", "union.zkr"]
242 .map(str::to_string)
243 .into_iter()
244 .chain((MIN_LIFT_PO2..=po2_max).map(|i| format!("lift_rv32im_v2_{i}.zkr")))
245 .collect();
246
247 let zkr_control_ids = match hash_name.as_ref() {
248 "sha-256" => SHA256_CONTROL_IDS,
249 "poseidon2" => POSEIDON2_CONTROL_IDS,
250 _ => bail!(
251 "unrecognized hash name for zkr control ids: {}",
252 hash_name.as_ref()
253 ),
254 };
255
256 Ok(zkr_control_ids
257 .into_iter()
258 .filter_map(move |(name, digest)| allowed_zkr_names.contains(name).then_some(digest)))
259}
260
261pub(crate) fn allowed_control_root(
263 hash_name: impl AsRef<str> + 'static,
264 po2_max: usize,
265) -> anyhow::Result<Digest> {
266 Ok(
267 MerkleGroup::new(allowed_control_ids(hash_name.as_ref().to_string(), po2_max)?.collect())?
268 .calc_root(
269 hash_suite_from_name(hash_name.as_ref())
270 .unwrap()
271 .hashfn
272 .as_ref(),
273 ),
274 )
275}
276
277#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
279pub struct SuccinctReceiptVerifierParameters {
280 pub control_root: Digest,
282 pub inner_control_root: Option<Digest>,
288 pub proof_system_info: ProtocolInfo,
290 pub circuit_info: ProtocolInfo,
292}
293
294impl SuccinctReceiptVerifierParameters {
295 #[stability::unstable]
299 pub fn from_max_po2(po2_max: usize) -> Self {
300 Self {
301 control_root: allowed_control_root("poseidon2", po2_max).unwrap(),
302 inner_control_root: None,
303 proof_system_info: PROOF_SYSTEM_INFO,
304 circuit_info: CircuitImpl::CIRCUIT_INFO,
305 }
306 }
307
308 #[stability::unstable]
311 pub fn all_po2s() -> Self {
312 Self::from_max_po2(risc0_zkp::MAX_CYCLES_PO2)
313 }
314}
315
316impl Digestible for SuccinctReceiptVerifierParameters {
317 fn digest<S: Sha256>(&self) -> Digest {
319 tagged_struct::<S>(
320 "risc0.SuccinctReceiptVerifierParameters",
321 &[
322 self.control_root,
323 self.inner_control_root.unwrap_or(self.control_root),
324 *S::hash_bytes(&self.proof_system_info.0),
325 *S::hash_bytes(&self.circuit_info.0),
326 ],
327 &[],
328 )
329 }
330}
331
332impl Default for SuccinctReceiptVerifierParameters {
333 fn default() -> Self {
335 Self {
336 control_root: ALLOWED_CONTROL_ROOT,
339 inner_control_root: None,
340 proof_system_info: PROOF_SYSTEM_INFO,
341 circuit_info: CircuitImpl::CIRCUIT_INFO,
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::{allowed_control_root, SuccinctReceiptVerifierParameters, ALLOWED_CONTROL_ROOT};
349 use crate::{receipt::DEFAULT_MAX_PO2, sha::Digestible};
350 use risc0_zkp::core::digest::digest;
351
352 #[test]
357 fn succinct_receipt_verifier_parameters_is_stable() {
358 assert_eq!(
359 SuccinctReceiptVerifierParameters::default().digest(),
360 digest!("68ecff4bad7b3348ca3ac642e852b8d66b7158307f7d2a001c13887698fe6019")
361 );
362 }
363
364 #[test]
365 fn allowed_control_root_fn_matches_bootstrap() {
366 assert_eq!(
367 allowed_control_root("poseidon2", DEFAULT_MAX_PO2).unwrap(),
368 ALLOWED_CONTROL_ROOT
369 )
370 }
371
372 #[test]
373 fn allowed_control_root_fn_doesnt_panic() {
374 for i in 0..=24 {
375 allowed_control_root("poseidon2", i)
376 .unwrap_or_else(|_| panic!("allowed_control_root panicked with i = {}", i));
377 }
378 assert_eq!(
380 allowed_control_root("poseidon2", 24).unwrap(),
381 allowed_control_root("poseidon2", 25).unwrap(),
382 );
383 }
384}