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 claim::Unknown,
43 receipt::{
44 merkle::{MerkleGroup, MerkleProof},
45 VerifierContext,
46 },
47 sha, MaybePruned,
48};
49
50#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
58#[cfg_attr(test, derive(PartialEq))]
59#[non_exhaustive]
60pub struct SuccinctReceipt<Claim> {
61 #[debug("{} bytes", self.get_seal_bytes().len())]
64 pub seal: Vec<u32>,
65
66 pub control_id: Digest,
69
70 pub claim: MaybePruned<Claim>,
75
76 pub hashfn: String,
78
79 pub verifier_parameters: Digest,
85
86 pub control_inclusion_proof: MerkleProof,
88}
89
90impl<Claim> SuccinctReceipt<Claim> {
91 pub fn verify_integrity(&self) -> Result<(), VerificationError>
94 where
95 Claim: risc0_binfmt::Digestible + core::fmt::Debug,
96 {
97 self.verify_integrity_with_context(&VerifierContext::default())
98 }
99
100 pub fn verify_integrity_with_context(
103 &self,
104 ctx: &VerifierContext,
105 ) -> Result<(), VerificationError>
106 where
107 Claim: risc0_binfmt::Digestible + core::fmt::Debug,
108 {
109 let params = ctx
110 .succinct_verifier_parameters
111 .as_ref()
112 .ok_or(VerificationError::VerifierParametersMissing)?;
113
114 if params.digest::<sha::Impl>() != self.verifier_parameters {
115 return Err(VerificationError::VerifierParametersMismatch {
116 expected: params.digest::<sha::Impl>(),
117 received: self.verifier_parameters,
118 });
119 }
120
121 if params.proof_system_info != PROOF_SYSTEM_INFO {
125 return Err(VerificationError::ProofSystemInfoMismatch {
126 expected: PROOF_SYSTEM_INFO,
127 received: params.proof_system_info,
128 });
129 }
130 if params.circuit_info != CircuitImpl::CIRCUIT_INFO {
131 return Err(VerificationError::CircuitInfoMismatch {
132 expected: CircuitImpl::CIRCUIT_INFO,
133 received: params.circuit_info,
134 });
135 }
136
137 let suite = ctx
138 .suites
139 .get(&self.hashfn)
140 .ok_or(VerificationError::InvalidHashSuite)?;
141
142 let check_code = |_, control_id: &Digest| -> Result<(), VerificationError> {
143 self.control_inclusion_proof
144 .verify(control_id, ¶ms.control_root, suite.hashfn.as_ref())
145 .map_err(|_| {
146 tracing::debug!(
147 "failed to verify control inclusion proof for {control_id} against root {} with {}",
148 params.control_root,
149 suite.name,
150 );
151 VerificationError::ControlVerificationError {
152 control_id: *control_id,
153 }
154 })
155 };
156
157 risc0_zkp::verify::verify(&CIRCUIT, suite, &self.seal, check_code)?;
160
161 let output_elems: &[BabyBearElem] =
163 bytemuck::checked::cast_slice(&self.seal[..CircuitImpl::OUTPUT_SIZE]);
164 let mut seal_claim = VecDeque::new();
165 for elem in output_elems {
166 seal_claim.push_back(elem.as_u32())
167 }
168
169 let control_root: Digest = seal_claim
174 .drain(0..16)
175 .enumerate()
176 .filter_map(|(i, word)| (i & 1 == 0).then_some(word))
177 .collect::<Vec<_>>()
178 .try_into()
179 .map_err(|_| VerificationError::ReceiptFormatError)?;
180
181 if control_root != params.inner_control_root.unwrap_or(params.control_root) {
182 tracing::debug!(
183 "succinct receipt does not match the expected control root: decoded: {:#?}, expected: {:?}",
184 control_root,
185 params.inner_control_root.unwrap_or(params.control_root),
186 );
187 return Err(VerificationError::ControlVerificationError {
188 control_id: control_root,
189 });
190 }
191
192 let output_hash =
194 read_sha_halfs(&mut seal_claim).map_err(|_| VerificationError::ReceiptFormatError)?;
195 if output_hash != self.claim.digest::<sha::Impl>() {
196 tracing::debug!(
197 "succinct receipt claim does not match the output digest: claim: {:#?}, digest expected: {output_hash:?}",
198 self.claim,
199 );
200 return Err(VerificationError::JournalDigestMismatch);
201 }
202 Ok(())
204 }
205
206 pub fn get_seal_bytes(&self) -> Vec<u8> {
208 self.seal.iter().flat_map(|x| x.to_le_bytes()).collect()
209 }
210
211 pub fn seal_size(&self) -> usize {
213 core::mem::size_of_val(self.seal.as_slice())
214 }
215
216 #[cfg(feature = "prove")]
217 pub(crate) fn control_root(&self) -> anyhow::Result<Digest> {
218 let hash_suite = hash_suite_from_name(&self.hashfn)
219 .ok_or_else(|| anyhow::anyhow!("unsupported hash function: {}", self.hashfn))?;
220 Ok(self
221 .control_inclusion_proof
222 .root(&self.control_id, hash_suite.hashfn.as_ref()))
223 }
224
225 #[cfg(feature = "prove")]
226 pub(crate) fn to_assumption(
227 &self,
228 erase_control_root: bool,
229 ) -> anyhow::Result<crate::Assumption>
230 where
231 Claim: risc0_binfmt::Digestible,
232 {
233 Ok(crate::Assumption {
234 claim: self.claim.digest::<sha::Impl>(),
235 control_root: match erase_control_root {
236 false => self.control_root()?,
237 true => Digest::ZERO,
238 },
239 })
240 }
241
242 pub fn into_unknown(self) -> SuccinctReceipt<Unknown>
245 where
246 Claim: risc0_binfmt::Digestible,
247 {
248 SuccinctReceipt {
249 claim: MaybePruned::Pruned(self.claim.digest::<sha::Impl>()),
250 seal: self.seal,
251 control_id: self.control_id,
252 hashfn: self.hashfn,
253 verifier_parameters: self.verifier_parameters,
254 control_inclusion_proof: self.control_inclusion_proof,
255 }
256 }
257}
258
259pub(crate) fn allowed_control_ids(
261 hash_name: impl AsRef<str> + 'static,
262 po2_max: usize,
263) -> anyhow::Result<impl Iterator<Item = Digest>> {
264 let po2_range = MIN_LIFT_PO2..=usize::min(po2_max, risc0_zkp::MAX_CYCLES_PO2);
266 let allowed_zkr_names: BTreeSet<String> = [
267 "join.zkr",
268 "join_povw.zkr",
269 "join_unwrap_povw.zkr",
270 "resolve.zkr",
271 "resolve_povw.zkr",
272 "resolve_unwrap_povw.zkr",
273 "identity.zkr",
274 "unwrap_povw.zkr",
275 "union.zkr",
276 ]
277 .map(str::to_string)
278 .into_iter()
279 .chain(po2_range.clone().map(|i| format!("lift_rv32im_v2_{i}.zkr")))
280 .chain(po2_range.map(|i| format!("lift_rv32im_v2_povw_{i}.zkr")))
281 .collect();
282
283 let zkr_control_ids = match hash_name.as_ref() {
284 "sha-256" => SHA256_CONTROL_IDS,
285 "poseidon2" => POSEIDON2_CONTROL_IDS,
286 _ => bail!(
287 "unrecognized hash name for zkr control ids: {}",
288 hash_name.as_ref()
289 ),
290 };
291
292 Ok(allowed_zkr_names.into_iter().map(move |name| {
293 *zkr_control_ids
294 .iter()
295 .find_map(|(zkr_name, digest)| (zkr_name == &name).then_some(digest))
296 .expect("zkr name in allowed_zkr_names not found in zkr_control_ids")
297 }))
298}
299
300pub(crate) fn allowed_control_root(
302 hash_name: impl AsRef<str> + 'static,
303 po2_max: usize,
304) -> anyhow::Result<Digest> {
305 Ok(
306 MerkleGroup::new(allowed_control_ids(hash_name.as_ref().to_string(), po2_max)?.collect())?
307 .calc_root(
308 hash_suite_from_name(hash_name.as_ref())
309 .unwrap()
310 .hashfn
311 .as_ref(),
312 ),
313 )
314}
315
316#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
318pub struct SuccinctReceiptVerifierParameters {
319 pub control_root: Digest,
321 pub inner_control_root: Option<Digest>,
327 pub proof_system_info: ProtocolInfo,
329 pub circuit_info: ProtocolInfo,
331}
332
333impl SuccinctReceiptVerifierParameters {
334 #[stability::unstable]
338 pub fn from_max_po2(po2_max: usize) -> Self {
339 Self {
340 control_root: allowed_control_root("poseidon2", po2_max).unwrap(),
341 inner_control_root: None,
342 proof_system_info: PROOF_SYSTEM_INFO,
343 circuit_info: CircuitImpl::CIRCUIT_INFO,
344 }
345 }
346
347 #[stability::unstable]
350 pub fn all_po2s() -> Self {
351 Self::from_max_po2(risc0_zkp::MAX_CYCLES_PO2)
352 }
353}
354
355impl Digestible for SuccinctReceiptVerifierParameters {
356 fn digest<S: Sha256>(&self) -> Digest {
358 tagged_struct::<S>(
359 "risc0.SuccinctReceiptVerifierParameters",
360 &[
361 self.control_root,
362 self.inner_control_root.unwrap_or(self.control_root),
363 *S::hash_bytes(&self.proof_system_info.0),
364 *S::hash_bytes(&self.circuit_info.0),
365 ],
366 &[],
367 )
368 }
369}
370
371impl Default for SuccinctReceiptVerifierParameters {
372 fn default() -> Self {
374 Self {
375 control_root: ALLOWED_CONTROL_ROOT,
378 inner_control_root: None,
379 proof_system_info: PROOF_SYSTEM_INFO,
380 circuit_info: CircuitImpl::CIRCUIT_INFO,
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::{allowed_control_root, SuccinctReceiptVerifierParameters, ALLOWED_CONTROL_ROOT};
388 use crate::{receipt::DEFAULT_MAX_PO2, sha::Digestible};
389 use risc0_zkp::core::digest::digest;
390
391 #[test]
396 fn succinct_receipt_verifier_parameters_is_stable() {
397 assert_eq!(
398 SuccinctReceiptVerifierParameters::default().digest(),
399 digest!("ece5e9b8ae2cd6ea6b1827b464ff0348f9a7f4decd269c0087fdfd75098da013")
400 );
401 }
402
403 #[test]
404 fn allowed_control_root_fn_matches_bootstrap() {
405 assert_eq!(
406 allowed_control_root("poseidon2", DEFAULT_MAX_PO2).unwrap(),
407 ALLOWED_CONTROL_ROOT
408 )
409 }
410
411 #[test]
412 fn allowed_control_root_fn_doesnt_panic() {
413 for i in 0..=24 {
414 allowed_control_root("poseidon2", i)
415 .unwrap_or_else(|_| panic!("allowed_control_root panicked with i = {i}"));
416 }
417 assert_eq!(
419 allowed_control_root("poseidon2", 24).unwrap(),
420 allowed_control_root("poseidon2", 25).unwrap(),
421 );
422 }
423}