risc0_zkvm/claim/
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
15//! [ReceiptClaim] and associated types and functions.
16//!
17//! A [ReceiptClaim] struct contains the public claims (i.e. public outputs) of a zkVM guest
18//! execution, such as the journal committed to by the guest. It also includes important
19//! information such as the exit code and the starting and ending system state (i.e. the state of
20//! memory).
21
22use alloc::{collections::VecDeque, vec::Vec};
23use core::{fmt, ops::Deref};
24
25#[cfg(feature = "std")]
26use anyhow::Context;
27use anyhow::{anyhow, bail, ensure};
28use borsh::{BorshDeserialize, BorshSerialize};
29use derive_more::Debug;
30use risc0_binfmt::{
31    read_sha_halfs, tagged_list, tagged_list_cons, tagged_struct, write_sha_halfs, Digestible,
32    ExitCode, InvalidExitCodeError,
33};
34use risc0_circuit_rv32im::{HighLowU16, Rv32imV2Claim, TerminateState};
35use risc0_zkp::core::digest::Digest;
36use risc0_zkvm_platform::syscall::halt;
37use serde::{Deserialize, Serialize};
38
39use super::{
40    maybe_pruned::{MaybePruned, PrunedValueError},
41    work::WorkClaimError,
42    Unknown,
43};
44use crate::{
45    sha::{self, Sha256},
46    SystemState, WorkClaim,
47};
48
49/// Public claims about a zkVM guest execution, such as the journal committed to by the guest.
50///
51/// Also includes important information such as the exit code and the starting and ending system
52/// state (i.e. the state of memory). [ReceiptClaim] is a "Merkle-ized struct" supporting
53/// partial openings of the underlying fields from a hash commitment to the full structure. Also
54/// see [MaybePruned].
55#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
56#[cfg_attr(test, derive(PartialEq))]
57pub struct ReceiptClaim {
58    /// The [SystemState] just before execution has begun.
59    pub pre: MaybePruned<SystemState>,
60
61    /// The [SystemState] just after execution has completed.
62    pub post: MaybePruned<SystemState>,
63
64    /// The exit code for the execution.
65    pub exit_code: ExitCode,
66
67    /// Input to the guest.
68    pub input: MaybePruned<Option<Input>>,
69
70    /// [Output] of the guest, including the journal and assumptions set during execution.
71    pub output: MaybePruned<Option<Output>>,
72}
73
74impl ReceiptClaim {
75    /// Construct a [ReceiptClaim] representing a zkVM execution that ended normally (i.e.
76    /// Halted(0)) with the given image ID and journal.
77    pub fn ok(
78        image_id: impl Into<Digest>,
79        journal: impl Into<MaybePruned<Vec<u8>>>,
80    ) -> ReceiptClaim {
81        Self {
82            pre: MaybePruned::Pruned(image_id.into()),
83            post: MaybePruned::Value(SystemState {
84                pc: 0,
85                merkle_root: Digest::ZERO,
86            }),
87            exit_code: ExitCode::Halted(0),
88            input: None.into(),
89            output: Some(Output {
90                journal: journal.into(),
91                assumptions: MaybePruned::Pruned(Digest::ZERO),
92            })
93            .into(),
94        }
95    }
96
97    /// Construct a [ReceiptClaim] representing a zkVM execution that ended in a normal paused
98    /// state (i.e. Paused(0)) with the given image ID and journal.
99    pub fn paused(
100        image_id: impl Into<Digest>,
101        journal: impl Into<MaybePruned<Vec<u8>>>,
102    ) -> ReceiptClaim {
103        Self {
104            pre: MaybePruned::Pruned(image_id.into()),
105            post: MaybePruned::Value(SystemState {
106                pc: 0,
107                merkle_root: Digest::ZERO,
108            }),
109            exit_code: ExitCode::Paused(0),
110            input: None.into(),
111            output: Some(Output {
112                journal: journal.into(),
113                assumptions: MaybePruned::Pruned(Digest::ZERO),
114            })
115            .into(),
116        }
117    }
118
119    /// Decode a [ReceiptClaim] from a list of [u32]'s
120    pub fn decode(flat: &mut VecDeque<u32>) -> Result<Self, DecodeError> {
121        let input = read_sha_halfs(flat)?;
122        let pre = SystemState::decode(flat)?;
123        let post = SystemState::decode(flat)?;
124        let sys_exit = flat
125            .pop_front()
126            .ok_or(risc0_binfmt::DecodeError::EndOfStream)?;
127        let user_exit = flat
128            .pop_front()
129            .ok_or(risc0_binfmt::DecodeError::EndOfStream)?;
130        let exit_code = ExitCode::from_pair(sys_exit, user_exit)?;
131        let output = read_sha_halfs(flat)?;
132
133        Ok(Self {
134            input: MaybePruned::Pruned(input),
135            pre: pre.into(),
136            post: post.into(),
137            exit_code,
138            output: MaybePruned::Pruned(output),
139        })
140    }
141
142    /// Encode a [ReceiptClaim] to a list of [u32]'s
143    pub fn encode(&self, flat: &mut Vec<u32>) -> Result<(), PrunedValueError> {
144        write_sha_halfs(flat, &self.input.digest::<sha::Impl>());
145        self.pre.as_value()?.encode(flat);
146        self.post.as_value()?.encode(flat);
147        let (sys_exit, user_exit) = self.exit_code.into_pair();
148        flat.push(sys_exit);
149        flat.push(user_exit);
150        write_sha_halfs(flat, &self.output.digest::<sha::Impl>());
151        Ok(())
152    }
153
154    pub(crate) fn decode_from_seal_v2(
155        seal: &[u32],
156        _po2: Option<u32>,
157    ) -> anyhow::Result<ReceiptClaim> {
158        let claim = Rv32imV2Claim::decode(seal)?;
159        tracing::debug!("claim: {claim:#?}");
160
161        // TODO(flaub): implement this once shutdownCycle is supported in rv32im-v2 circuit
162        // if let Some(po2) = po2 {
163        //     let segment_threshold = (1 << po2) - MAX_INSN_CYCLES;
164        //     ensure!(claim.shutdown_cycle.unwrap() == segment_threshold as u32);
165        // }
166
167        let exit_code = exit_code_from_terminate_state(&claim.terminate_state)?;
168        let post_state = match exit_code {
169            ExitCode::Halted(_) => Digest::ZERO,
170            _ => claim.post_state,
171        };
172
173        Ok(ReceiptClaim {
174            pre: MaybePruned::Value(SystemState {
175                pc: 0,
176                merkle_root: claim.pre_state,
177            }),
178            post: MaybePruned::Value(SystemState {
179                pc: 0,
180                merkle_root: post_state,
181            }),
182            exit_code,
183            input: MaybePruned::Pruned(claim.input),
184            output: MaybePruned::Pruned(claim.output.unwrap_or_default()),
185        })
186    }
187
188    /// Produce the claim for joining two claims of execution in a continuation, asserting the
189    /// reachability of the post state of other from the pre state of self.
190    pub fn join(&self, other: &Self) -> Self {
191        ReceiptClaim {
192            pre: self.pre.clone(),
193            post: other.post.clone(),
194            exit_code: other.exit_code,
195            input: self.input.clone(),
196            output: other.output.clone(),
197        }
198    }
199
200    /// Produce the claim for resolving an assumption from the conditional claim (self). The
201    /// conditional claim must have a full (unpruned) assumptions list and the given claim must be
202    /// the head of the list.
203    #[cfg(feature = "std")]
204    pub fn resolve<Claim: risc0_binfmt::Digestible + ?Sized>(
205        &self,
206        assumption: &Claim,
207    ) -> anyhow::Result<Self> {
208        let mut resolved_claim = self.clone();
209
210        // Open the assumptions on the output of the claim.
211        let assumptions: &mut Assumptions = resolved_claim
212            .output
213            .as_value_mut()
214            .context("conditional receipt output is pruned")?
215            .as_mut()
216            .ok_or_else(|| anyhow!("conditional receipt has empty output and no assumptions"))?
217            .assumptions
218            .as_value_mut()
219            .context("conditional receipt has pruned assumptions")?;
220
221        // Use the control root from the head of the assumptions list to form an Assumption from
222        // the given claim. This is a simplifying assumption but connot guarantee that the claim
223        // actually resolves the assumption if it was produced with an incompatible control root.
224        let head_control_root = assumptions
225            .first()
226            .context("assumptions list is empty")?
227            .as_value()?
228            .control_root;
229
230        // Remove the head assumption.
231        assumptions.resolve(
232            &Assumption {
233                control_root: head_control_root,
234                claim: assumption.digest::<sha::Impl>(),
235            }
236            .digest::<sha::Impl>(),
237        )?;
238
239        Ok(resolved_claim)
240    }
241}
242
243impl MaybePruned<ReceiptClaim> {
244    /// Produce the claim for joining two claims of execution in a continuation, asserting the
245    /// reachability of the post state of other from the pre state of self.
246    pub fn join(&self, other: &Self) -> Result<Self, PrunedValueError> {
247        Ok(self.as_value()?.join(other.as_value()?).into())
248    }
249
250    /// Produce the claim for resolving an assumption from the conditional claim (self). The
251    /// conditional claim must have a full (unpruned) assumptions list and the given claim must be
252    /// the head of the list.
253    #[cfg(feature = "std")]
254    pub fn resolve<Claim: risc0_binfmt::Digestible + ?Sized>(
255        &self,
256        assumption: &Claim,
257    ) -> anyhow::Result<Self> {
258        Ok(self
259            .as_value()
260            .context("conditional claim is pruned")?
261            .resolve(assumption)?
262            .into())
263    }
264}
265
266impl WorkClaim<ReceiptClaim> {
267    /// Joins two work claims by combining their receipt claims and work values while ensuring
268    /// the consumed nonce ranges are disjoint.
269    pub fn join(&self, other: &Self) -> Result<Self, WorkClaimError> {
270        Ok(Self {
271            claim: self.claim.join(&other.claim)?,
272            work: self.work.join(&other.work)?,
273        })
274    }
275
276    /// Resolves assumptions in the receipt claim while preserving the work value.
277    #[cfg(feature = "std")]
278    pub fn resolve<Claim: risc0_binfmt::Digestible + ?Sized>(
279        &self,
280        assumption: &Claim,
281    ) -> anyhow::Result<Self> {
282        Ok(Self {
283            claim: self.claim.resolve(assumption)?,
284            work: self.work.clone(),
285        })
286    }
287}
288
289impl MaybePruned<WorkClaim<ReceiptClaim>> {
290    /// Joins two possibly pruned work claims by combining their receipt claims and work values
291    /// while ensuring the consumed nonce ranges are disjoint.
292    pub fn join(&self, other: &Self) -> Result<Self, WorkClaimError> {
293        Ok(self.as_value()?.join(other.as_value()?)?.into())
294    }
295
296    /// Resolves assumptions in a possibly pruned work claim while preserving the work value.
297    #[cfg(feature = "std")]
298    pub fn resolve<Claim: risc0_binfmt::Digestible + ?Sized>(
299        &self,
300        assumption: &Claim,
301    ) -> anyhow::Result<Self> {
302        Ok(self
303            .as_value()
304            .context("conditional povw claim is pruned")?
305            .resolve(assumption)?
306            .into())
307    }
308}
309
310pub(crate) fn exit_code_from_terminate_state(
311    terminate_state: &Option<TerminateState>,
312) -> anyhow::Result<ExitCode> {
313    let exit_code = if let Some(term) = terminate_state {
314        let HighLowU16(user_exit, halt_type) = term.a0;
315        match halt_type as u32 {
316            halt::TERMINATE => ExitCode::Halted(user_exit as u32),
317            halt::PAUSE => ExitCode::Paused(user_exit as u32),
318            _ => bail!("Illegal halt type: {halt_type}"),
319        }
320    } else {
321        ExitCode::SystemSplit
322    };
323    Ok(exit_code)
324}
325
326impl Digestible for ReceiptClaim {
327    /// Hash the [ReceiptClaim] to get a digest of the struct.
328    fn digest<S: Sha256>(&self) -> Digest {
329        let (sys_exit, user_exit) = self.exit_code.into_pair();
330        tagged_struct::<S>(
331            "risc0.ReceiptClaim",
332            &[
333                self.input.digest::<S>(),
334                self.pre.digest::<S>(),
335                self.post.digest::<S>(),
336                self.output.digest::<S>(),
337            ],
338            &[sys_exit, user_exit],
339        )
340    }
341}
342
343/// Error returned when decoding [ReceiptClaim] fails.
344#[derive(Debug, Copy, Clone)]
345pub enum DecodeError {
346    /// Decoding failure due to an invalid exit code.
347    InvalidExitCode(InvalidExitCodeError),
348    /// Decoding failure due to an inner decoding failure.
349    Decode(risc0_binfmt::DecodeError),
350}
351
352impl fmt::Display for DecodeError {
353    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
354        match self {
355            Self::InvalidExitCode(e) => write!(f, "failed to decode receipt claim: {e}"),
356            Self::Decode(e) => write!(f, "failed to decode receipt claim: {e}"),
357        }
358    }
359}
360
361impl From<risc0_binfmt::DecodeError> for DecodeError {
362    fn from(e: risc0_binfmt::DecodeError) -> Self {
363        Self::Decode(e)
364    }
365}
366
367impl From<InvalidExitCodeError> for DecodeError {
368    fn from(e: InvalidExitCodeError) -> Self {
369        Self::InvalidExitCode(e)
370    }
371}
372
373#[cfg(feature = "std")]
374impl std::error::Error for DecodeError {}
375
376/// Each UnionClaim can be used as an inner node in a Merkle mountain
377/// accumulator, the root of which commits to a set of claims.
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct UnionClaim {
380    /// Digest of the "left" Assumption struct.
381    ///
382    /// The left should always be lesser of the two when interpreting the digest as a big-endian number.
383    pub left: Digest,
384    /// Digest of the "right" Assumption struct.
385    pub right: Digest,
386}
387
388impl Digestible for UnionClaim {
389    fn digest<S: Sha256>(&self) -> Digest {
390        tagged_struct::<S>("risc0.UnionClaim", &[self.left, self.right], &[])
391    }
392}
393
394/// Input field in the [ReceiptClaim], committing to a public value accessible to the guest.
395///
396/// NOTE: This type is currently uninhabited (i.e. it cannot be constructed), and only its digest
397/// is accessible. It may become inhabited in a future release.
398#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
399#[cfg_attr(test, derive(PartialEq))]
400pub struct Input {
401    // Private field to ensure this type cannot be constructed.
402    // By making this type uninhabited, it can be populated later without breaking backwards
403    // compatibility.
404    pub(crate) x: Unknown,
405}
406
407impl Digestible for Input {
408    /// Hash the [Input] to get a digest of the struct.
409    fn digest<S: Sha256>(&self) -> Digest {
410        match self.x { /* unreachable  */ }
411    }
412}
413
414/// Output field in the [ReceiptClaim], committing to a claimed journal and assumptions list.
415#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
416#[cfg_attr(test, derive(PartialEq))]
417pub struct Output {
418    /// The journal committed to by the guest execution.
419    #[debug("{}", fmt_debug_journal(journal))]
420    pub journal: MaybePruned<Vec<u8>>,
421
422    /// An ordered list of [ReceiptClaim] digests corresponding to the
423    /// calls to `env::verify` and `env::verify_integrity`.
424    ///
425    /// Verifying the integrity of a [crate::Receipt] corresponding to a [ReceiptClaim] with a
426    /// non-empty assumptions list does not guarantee unconditionally any of the claims over the
427    /// guest execution (i.e. if the assumptions list is non-empty, then the journal digest cannot
428    /// be trusted to correspond to a genuine execution). The claims can be checked by additional
429    /// verifying a [crate::Receipt] for every digest in the assumptions list.
430    pub assumptions: MaybePruned<Assumptions>,
431}
432
433#[allow(dead_code)]
434fn fmt_debug_journal(journal: &MaybePruned<Vec<u8>>) -> alloc::string::String {
435    match journal {
436        MaybePruned::Value(bytes) => alloc::format!("{} bytes", bytes.len()),
437        MaybePruned::Pruned(_) => alloc::format!("{journal:?}"),
438    }
439}
440
441impl Digestible for Output {
442    /// Hash the [Output] to get a digest of the struct.
443    fn digest<S: Sha256>(&self) -> Digest {
444        tagged_struct::<S>(
445            "risc0.Output",
446            &[self.journal.digest::<S>(), self.assumptions.digest::<S>()],
447            &[],
448        )
449    }
450}
451
452/// An [assumption] made in the course of proving program execution.
453///
454/// Assumptions are generated when the guest makes a recursive verification call. Each assumption
455/// commits the statement, such that only a receipt proving that statement can be used to resolve
456/// and remove the assumption.
457///
458/// [assumption]: https://dev.risczero.com/terminology#assumption
459#[derive(
460    Clone, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, BorshSerialize, BorshDeserialize,
461)]
462pub struct Assumption {
463    /// Commitment to the assumption claim. It may be the digest of a [ReceiptClaim], or it could
464    /// be the digest of the claim for a different circuit such as an accelerator.
465    pub claim: Digest,
466
467    /// Commitment to the set of [recursion programs] that can be used to resolve this assumption.
468    ///
469    /// Binding the set of recursion programs also binds the circuits, and creates an assumption
470    /// resolved by independent set of circuits (e.g. keccak or Groth16 verify). Proofs of these
471    /// external claims are verified by a "lift" program implemented for the recursion VM which
472    /// brings the claim into the recursion system. This lift program is committed to in the
473    /// control root.
474    ///
475    /// A special value of all zeroes indicates "self-composition", where the control root used to
476    /// verify this claim is also used to verify the assumption.
477    ///
478    /// [recursion programs]: https://dev.risczero.com/terminology#recursion-program
479    pub control_root: Digest,
480}
481
482impl Digestible for Assumption {
483    /// Hash the [Assumption] to get a digest of the struct.
484    fn digest<S: Sha256>(&self) -> Digest {
485        tagged_struct::<S>("risc0.Assumption", &[self.claim, self.control_root], &[])
486    }
487}
488
489/// A list of assumptions, each a [Digest] or populated value of an [Assumption].
490#[derive(Clone, Default, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
491#[cfg_attr(test, derive(PartialEq))]
492pub struct Assumptions(pub Vec<MaybePruned<Assumption>>);
493
494impl Assumptions {
495    /// Add an assumption to the head of the assumptions list.
496    pub fn add(&mut self, assumption: MaybePruned<Assumption>) {
497        self.0.insert(0, assumption);
498    }
499
500    /// Mark an assumption as resolved and remove it from the list.
501    ///
502    /// Assumptions can only be removed from the head of the list.
503    pub fn resolve(&mut self, resolved: &Digest) -> anyhow::Result<()> {
504        let head = self
505            .0
506            .first()
507            .ok_or_else(|| anyhow!("cannot resolve assumption from empty list"))?;
508
509        ensure!(
510            &head.digest::<sha::Impl>() == resolved,
511            "resolved assumption is not equal to the head of the list: {} != {}",
512            resolved,
513            head.digest::<sha::Impl>()
514        );
515
516        // Drop the head of the assumptions list.
517        self.0 = self.0.split_off(1);
518        Ok(())
519    }
520}
521
522impl Deref for Assumptions {
523    type Target = [MaybePruned<Assumption>];
524
525    fn deref(&self) -> &Self::Target {
526        &self.0
527    }
528}
529
530impl Digestible for Assumptions {
531    /// Hash the [Assumptions] to get a digest of the struct.
532    fn digest<S: Sha256>(&self) -> Digest {
533        tagged_list::<S>(
534            "risc0.Assumptions",
535            &self.0.iter().map(|a| a.digest::<S>()).collect::<Vec<_>>(),
536        )
537    }
538}
539
540impl MaybePruned<Assumptions> {
541    /// Check if the (possibly pruned) assumptions list is empty.
542    pub fn is_empty(&self) -> bool {
543        match self {
544            MaybePruned::Value(list) => list.is_empty(),
545            MaybePruned::Pruned(digest) => digest == &Digest::ZERO,
546        }
547    }
548
549    /// Add an assumption to the head of the assumptions list.
550    ///
551    /// If this value is pruned, then the result will also be a pruned value.
552    pub fn add(&mut self, assumption: MaybePruned<Assumption>) {
553        match self {
554            MaybePruned::Value(list) => list.add(assumption),
555            MaybePruned::Pruned(list_digest) => {
556                *list_digest = tagged_list_cons::<sha::Impl>(
557                    "risc0.Assumptions",
558                    &assumption.digest::<sha::Impl>(),
559                    &*list_digest,
560                );
561            }
562        }
563    }
564
565    /// Mark an assumption as resolved and remove it from the list.
566    ///
567    /// Assumptions can only be removed from the head of the list. If this value
568    /// is pruned, then the result will also be a pruned value. The `tail`
569    /// parameter should be equal to the digest of the list after the
570    /// resolved assumption is removed.
571    pub fn resolve(&mut self, resolved: &Digest, tail: &Digest) -> anyhow::Result<()> {
572        match self {
573            MaybePruned::Value(list) => list.resolve(resolved),
574            MaybePruned::Pruned(list_digest) => {
575                let reconstructed =
576                    tagged_list_cons::<sha::Impl>("risc0.Assumptions", resolved, tail);
577                ensure!(
578                    &reconstructed == list_digest,
579                    "reconstructed list digest does not match; expected {}, reconstructed {}",
580                    list_digest,
581                    reconstructed
582                );
583
584                // Set the pruned digest value to be equal to the rest parameter.
585                *list_digest = *tail;
586                Ok(())
587            }
588        }
589    }
590}
591
592impl From<Vec<MaybePruned<Assumption>>> for Assumptions {
593    fn from(value: Vec<MaybePruned<Assumption>>) -> Self {
594        Self(value)
595    }
596}
597
598impl From<Vec<Assumption>> for Assumptions {
599    fn from(value: Vec<Assumption>) -> Self {
600        Self(value.into_iter().map(Into::into).collect())
601    }
602}
603
604impl From<Vec<Assumption>> for MaybePruned<Assumptions> {
605    fn from(value: Vec<Assumption>) -> Self {
606        Self::Value(value.into())
607    }
608}