risc0_zkvm/receipt/
succinct.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::{
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/// A succinct receipt, produced via recursion, proving the execution of the zkVM with a [STARK].
51///
52/// Using recursion, a [CompositeReceipt][crate::CompositeReceipt] can be compressed to form a
53/// [SuccinctReceipt]. In this way, a constant sized proof can be generated for arbitrarily long
54/// computations, and with an arbitrary number of segments linked via composition.
55///
56/// [STARK]: https://dev.risczero.com/terminology#stark
57#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
58#[cfg_attr(test, derive(PartialEq))]
59#[non_exhaustive]
60pub struct SuccinctReceipt<Claim> {
61    /// The cryptographic seal of this receipt. This seal is a STARK proving an execution of the
62    /// recursion circuit.
63    #[debug("{} bytes", self.get_seal_bytes().len())]
64    pub seal: Vec<u32>,
65
66    /// The control ID of this receipt, identifying the recursion program that was run (e.g. lift,
67    /// join, or resolve).
68    pub control_id: Digest,
69
70    /// Claim containing information about the computation that this receipt proves.
71    ///
72    /// The standard claim type is [ReceiptClaim][crate::ReceiptClaim], which represents a RISC-V
73    /// zkVM execution.
74    pub claim: MaybePruned<Claim>,
75
76    /// Name of the hash function used to create this receipt.
77    pub hashfn: String,
78
79    /// A digest of the verifier parameters that can be used to verify this receipt.
80    ///
81    /// Acts as a fingerprint to identify differing proof system or circuit versions between a
82    /// prover and a verifier. It is not intended to contain the full verifier parameters, which must
83    /// be provided by a trusted source (e.g. packaged with the verifier code).
84    pub verifier_parameters: Digest,
85
86    /// Merkle inclusion proof for control_id against the control root for this receipt.
87    pub control_inclusion_proof: MerkleProof,
88}
89
90impl<Claim> SuccinctReceipt<Claim> {
91    /// Verify the integrity of this receipt, ensuring the claim is attested
92    /// to by the seal.
93    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    /// Verify the integrity of this receipt, ensuring the claim is attested
101    /// to by the seal.
102    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        // Check that the proof system and circuit info strings match what is implemented by this
122        // function. Info strings are used a version identifiers, and this verify implementation
123        // supports exactly one proof systema and circuit version at a time.
124        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, &params.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        // Verify the receipt itself is correct, and therefore the encoded globals are
158        // reliable.
159        risc0_zkp::verify::verify(&CIRCUIT, suite, &self.seal, check_code)?;
160
161        // Extract the globals from the seal
162        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        // Read the Poseidon2 control root digest from the first 16 words of the output.
170        // NOTE: Implemented recursion programs have two output slots, each of size 16 elems.
171        // A SHA2 digest is encoded as 16 half words. Poseidon digests are encoded in 8 elems,
172        // but are interspersed with padding to fill out the whole 16 elems.
173        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        // Verify the output hash matches that data
193        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        // Everything passed
203        Ok(())
204    }
205
206    /// Return the seal for this receipt, as a vector of bytes.
207    pub fn get_seal_bytes(&self) -> Vec<u8> {
208        self.seal.iter().flat_map(|x| x.to_le_bytes()).collect()
209    }
210
211    /// Number of bytes used by the seal for this receipt.
212    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    /// Prunes the claim, retaining its digest, and converts into a [SuccinctReceipt] with an unknown
243    /// claim type. Can be used to get receipts of a uniform type across heterogeneous claims.
244    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
259/// Constructs the set of allowed control IDs, given a maximum cycle count as a po2.
260pub(crate) fn allowed_control_ids(
261    hash_name: impl AsRef<str> + 'static,
262    po2_max: usize,
263) -> anyhow::Result<impl Iterator<Item = Digest>> {
264    // Recursion programs (ZKRs) that are to be included in the allowed set.
265    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
300/// Constructs the root for the set of allowed control IDs, given a maximum cycle count as a po2.
301pub(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/// Verifier parameters used to verify a [SuccinctReceipt].
317#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
318pub struct SuccinctReceiptVerifierParameters {
319    /// Control root used to verify the control ID binding the executed recursion program.
320    pub control_root: Digest,
321    /// Control root used to verify the recursive control root in the output of the receipt.
322    ///
323    /// Usually, this should be set to None, which means it is equal to control_root. It may be set
324    /// to a different value than control root when switching hash functions (e.g. recursively
325    /// verifying a receipt produced with "poseidon2", producing a new receipt using "sha-256").
326    pub inner_control_root: Option<Digest>,
327    /// Protocol info string distinguishing the proof system under which the receipt should verify.
328    pub proof_system_info: ProtocolInfo,
329    /// Protocol info string distinguishing circuit with which the receipt should verify.
330    pub circuit_info: ProtocolInfo,
331}
332
333impl SuccinctReceiptVerifierParameters {
334    /// Construct verifier parameters that will accept receipts with control any of the default
335    /// control ID associated with cycle counts as powers of two (po2) up to the given max
336    /// inclusive.
337    #[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    /// Construct verifier parameters that will accept receipts with control any of the default
348    /// control ID associated with cycle counts of all supported powers of two (po2).
349    #[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    /// Hash the [SuccinctReceiptVerifierParameters] to get a digest of the struct.
357    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    /// Default set of parameters used to verify a [SuccinctReceipt].
373    fn default() -> Self {
374        Self {
375            // ALLOWED_CONTROL_ROOT is a precalculated version of the control root, as calculated
376            // by the allowed_control_root function above.
377            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    // Check that the verifier parameters has a stable digest (and therefore a stable value). This
392    // struct encodes parameters used in verification, and so this value should be updated if and
393    // only if a change to the verifier parameters is expected. Updating the verifier parameters
394    // will result in incompatibility with previous versions.
395    #[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        // When po2_max is greater than 24, this simply returns the same result as 24.
418        assert_eq!(
419            allowed_control_root("poseidon2", 24).unwrap(),
420            allowed_control_root("poseidon2", 25).unwrap(),
421        );
422    }
423}