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    receipt::{
43        merkle::{MerkleGroup, MerkleProof},
44        VerifierContext,
45    },
46    receipt_claim::{MaybePruned, Unknown},
47    sha,
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>
61where
62    Claim: Digestible + core::fmt::Debug + Clone + Serialize,
63{
64    /// The cryptographic seal of this receipt. This seal is a STARK proving an execution of the
65    /// recursion circuit.
66    #[debug("{} bytes", self.get_seal_bytes().len())]
67    pub seal: Vec<u32>,
68
69    /// The control ID of this receipt, identifying the recursion program that was run (e.g. lift,
70    /// join, or resolve).
71    pub control_id: Digest,
72
73    /// Claim containing information about the computation that this receipt proves.
74    ///
75    /// The standard claim type is [ReceiptClaim][crate::ReceiptClaim], which represents a RISC-V
76    /// zkVM execution.
77    pub claim: MaybePruned<Claim>,
78
79    /// Name of the hash function used to create this receipt.
80    pub hashfn: String,
81
82    /// A digest of the verifier parameters that can be used to verify this receipt.
83    ///
84    /// Acts as a fingerprint to identify differing proof system or circuit versions between a
85    /// prover and a verifier. It is not intended to contain the full verifier parameters, which must
86    /// be provided by a trusted source (e.g. packaged with the verifier code).
87    pub verifier_parameters: Digest,
88
89    /// Merkle inclusion proof for control_id against the control root for this receipt.
90    pub control_inclusion_proof: MerkleProof,
91}
92
93impl<Claim> SuccinctReceipt<Claim>
94where
95    Claim: Digestible + core::fmt::Debug + Clone + Serialize,
96{
97    /// Verify the integrity of this receipt, ensuring the claim is attested
98    /// to by the seal.
99    pub fn verify_integrity(&self) -> Result<(), VerificationError> {
100        self.verify_integrity_with_context(&VerifierContext::default())
101    }
102
103    /// Verify the integrity of this receipt, ensuring the claim is attested
104    /// to by the seal.
105    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        // Check that the proof system and circuit info strings match what is implemented by this
115        // function. Info strings are used a version identifiers, and this verify implementation
116        // supports exactly one proof systema and circuit version at a time.
117        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, &params.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        // Verify the receipt itself is correct, and therefore the encoded globals are
151        // reliable.
152        risc0_zkp::verify::verify(&CIRCUIT, suite, &self.seal, check_code)?;
153
154        // Extract the globals from the seal
155        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        // Read the Poseidon2 control root digest from the first 16 words of the output.
163        // NOTE: Implemented recursion programs have two output slots, each of size 16 elems.
164        // A SHA2 digest is encoded as 16 half words. Poseidon digests are encoded in 8 elems,
165        // but are interspersed with padding to fill out the whole 16 elems.
166        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        // Verify the output hash matches that data
186        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        // Everything passed
196        Ok(())
197    }
198
199    /// Return the seal for this receipt, as a vector of bytes.
200    pub fn get_seal_bytes(&self) -> Vec<u8> {
201        self.seal.iter().flat_map(|x| x.to_le_bytes()).collect()
202    }
203
204    /// Number of bytes used by the seal for this receipt.
205    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    /// Prunes the claim, retaining its digest, and converts into a [SuccinctReceipt] with an unknown
219    /// claim type. Can be used to get receipts of a uniform type across heterogeneous claims.
220    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
232/// Constructs the set of allowed control IDs, given a maximum cycle count as a po2.
233pub(crate) fn allowed_control_ids(
234    hash_name: impl AsRef<str> + 'static,
235    po2_max: usize,
236) -> anyhow::Result<impl Iterator<Item = Digest>> {
237    // Recursion programs (ZKRs) that are to be included in the allowed set.
238    // NOTE: Although the rv32im circuit has control IDs down to po2 13, lift predicates are only
239    // generated for po2 14 and above, hence the magic 14 below.
240    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
261/// Constructs the root for the set of allowed control IDs, given a maximum cycle count as a po2.
262pub(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/// Verifier parameters used to verify a [SuccinctReceipt].
278#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
279pub struct SuccinctReceiptVerifierParameters {
280    /// Control root used to verify the control ID binding the executed recursion program.
281    pub control_root: Digest,
282    /// Control root used to verify the recursive control root in the output of the receipt.
283    ///
284    /// Usually, this should be set to None, which means it is equal to control_root. It may be set
285    /// to a different value than control root when switching hash functions (e.g. recursively
286    /// verifying a receipt produced with "poseidon2", producing a new receipt using "sha-256").
287    pub inner_control_root: Option<Digest>,
288    /// Protocol info string distinguishing the proof system under which the receipt should verify.
289    pub proof_system_info: ProtocolInfo,
290    /// Protocol info string distinguishing circuit with which the receipt should verify.
291    pub circuit_info: ProtocolInfo,
292}
293
294impl SuccinctReceiptVerifierParameters {
295    /// Construct verifier parameters that will accept receipts with control any of the default
296    /// control ID associated with cycle counts as powers of two (po2) up to the given max
297    /// inclusive.
298    #[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    /// Construct verifier parameters that will accept receipts with control any of the default
309    /// control ID associated with cycle counts of all supported powers of two (po2).
310    #[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    /// Hash the [SuccinctReceiptVerifierParameters] to get a digest of the struct.
318    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    /// Default set of parameters used to verify a [SuccinctReceipt].
334    fn default() -> Self {
335        Self {
336            // ALLOWED_CONTROL_ROOT is a precalculated version of the control root, as calculated
337            // by the allowed_control_root function above.
338            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    // Check that the verifier parameters has a stable digest (and therefore a stable value). This
353    // struct encodes parameters used in verification, and so this value should be updated if and
354    // only if a change to the verifier parameters is expected. Updating the verifier parameters
355    // will result in incompatibility with previous versions.
356    #[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        // When po2_max is greater than 24, this simply returns the same result as 24.
379        assert_eq!(
380            allowed_control_root("poseidon2", 24).unwrap(),
381            allowed_control_root("poseidon2", 25).unwrap(),
382        );
383    }
384}