Skip to main content

miden_core/program/
claim.rs

1//! The execution claim: the statement a Miden VM proof attests, and its canonical commitment.
2//!
3//! An execution claim binds four fields: the program digest `P`, the kernel commitment `K`, the
4//! stack inputs `I`, and the stack outputs `O`. The deferred root `D` produced by execution is
5//! *not* part of the claim: it is the obligation a verified claim hands back, bound separately
6//! into the transcript seed.
7//!
8//! # Canonical encoding
9//!
10//! The claim encodes as exactly [`NUM_CLAIM_ELEMENTS`] = 40 field elements:
11//!
12//! ```text
13//! offset  0..8    P ‖ K                       (program digest, kernel commitment)
14//! offset  8..24   I  StackInputs, 16 felts    (canonical zero-padded, native order)
15//! offset 24..40   O  StackOutputs, 16 felts   (canonical zero-padded, native order)
16//! ```
17//!
18//! The code context comes first so that callsites that pin `(P, K)` can resume the claim hash
19//! from a precomputed sponge state; 40 elements is exactly five Poseidon2 rate blocks, so no
20//! padding block is absorbed and both read points (the `(P, K)` prefix state and the claim
21//! commitment) fall on permutation boundaries.
22//!
23//! # Claim commitment
24//!
25//! `CLAIM_HASH = Poseidon2::hash_elements_in_domain(P ‖ K ‖ I ‖ O, CLAIM_DOMAIN_TAG)`, i.e. the
26//! domain tag rides in the second capacity element while the first carries the Sponge2 padding
27//! rule of <https://eprint.iacr.org/2024/911> (here `40 % 8 = 0`).
28
29use super::{
30    KernelDescriptor, ProgramInfo, StackInputs, StackOutputs,
31    domain::{EXECUTION_CLAIM_DOMAIN_ID, domain_selector},
32};
33use crate::{Felt, Word, ZERO, chiplets::hasher};
34
35// CONSTANTS
36// ================================================================================================
37
38/// Number of field elements in the canonical claim encoding: `P ‖ K ‖ I ‖ O`.
39pub const NUM_CLAIM_ELEMENTS: usize = 40;
40
41/// Domain tag for the claim commitment: the registered selector
42/// `(EXECUTION_CLAIM_DOMAIN_ID << 8) | 1` (see the [`domain`](super::domain) module).
43pub const CLAIM_DOMAIN_TAG: Felt = domain_selector(EXECUTION_CLAIM_DOMAIN_ID, 1);
44
45// EXECUTION CLAIM
46// ================================================================================================
47
48/// The external statement a Miden VM proof attests: the program root and kernel identify the
49/// executed code and its syscall authorization set; the stack inputs and outputs are the
50/// execution's public I/O.
51///
52/// Stack inputs and stack outputs are both stored top-of-stack first: the first value in each
53/// slice is the top of the operand stack. The claim stores both in their canonical zero-padded
54/// 16-element form.
55///
56/// The deferred root is deliberately absent: verification returns it as an obligation.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ExecutionClaim {
59    program_root: Word,
60    kernel: KernelDescriptor,
61    stack_inputs: StackInputs,
62    stack_outputs: StackOutputs,
63}
64
65impl ExecutionClaim {
66    /// Creates a new execution claim from the program root, kernel, and stack I/O.
67    pub const fn new(
68        program_root: Word,
69        kernel: KernelDescriptor,
70        stack_inputs: StackInputs,
71        stack_outputs: StackOutputs,
72    ) -> Self {
73        Self {
74            program_root,
75            kernel,
76            stack_inputs,
77            stack_outputs,
78        }
79    }
80
81    /// Creates a new execution claim from the program info and the stack I/O.
82    pub fn from_program_info(
83        program_info: ProgramInfo,
84        stack_inputs: StackInputs,
85        stack_outputs: StackOutputs,
86    ) -> Self {
87        let (program_root, kernel) = program_info.into_parts();
88        Self::new(program_root, kernel, stack_inputs, stack_outputs)
89    }
90
91    /// Returns the MAST root of the executed program.
92    pub const fn program_root(&self) -> Word {
93        self.program_root
94    }
95
96    /// Returns the kernel descriptor of this claim.
97    pub const fn kernel(&self) -> &KernelDescriptor {
98        &self.kernel
99    }
100
101    /// Returns the program info (program root + kernel) of this claim.
102    ///
103    /// This constructs a new [`ProgramInfo`], cloning the kernel descriptor.
104    pub fn to_program_info(&self) -> ProgramInfo {
105        ProgramInfo::new(self.program_root, self.kernel.clone())
106    }
107
108    /// Returns the stack inputs of this claim.
109    pub const fn stack_inputs(&self) -> &StackInputs {
110        &self.stack_inputs
111    }
112
113    /// Returns the stack outputs of this claim.
114    pub const fn stack_outputs(&self) -> &StackOutputs {
115        &self.stack_outputs
116    }
117
118    /// Splits this claim into its program root, kernel, and stack I/O.
119    pub fn into_parts(self) -> (Word, KernelDescriptor, StackInputs, StackOutputs) {
120        (self.program_root, self.kernel, self.stack_inputs, self.stack_outputs)
121    }
122
123    /// Returns the canonical 40-element encoding `P ‖ K ‖ I ‖ O` of this claim.
124    pub fn to_elements(&self) -> [Felt; NUM_CLAIM_ELEMENTS] {
125        let mut elements = [ZERO; NUM_CLAIM_ELEMENTS];
126        elements[0..4].copy_from_slice(self.program_root.as_elements());
127        elements[4..8].copy_from_slice(self.kernel.commitment().as_elements());
128        elements[8..24].copy_from_slice(&self.stack_inputs[..]);
129        elements[24..40].copy_from_slice(&self.stack_outputs[..]);
130        elements
131    }
132
133    /// Returns the canonical commitment to this claim (`CLAIM_HASH`).
134    ///
135    /// This is the verifier-independent name of the claim: the value used to request proof
136    /// packages and to bind verified claims into a consumer's own statement.
137    pub fn commitment(&self) -> Word {
138        claim_commitment(&self.to_elements())
139    }
140}
141
142/// Returns the canonical claim commitment over an already-encoded claim.
143///
144/// This is the single implementation of `CLAIM_HASH`; every native computation of the claim
145/// commitment (including the transcript observation in `miden-air`) must go through it.
146pub fn claim_commitment(elements: &[Felt; NUM_CLAIM_ELEMENTS]) -> Word {
147    hasher::hash_elements_in_domain(elements, CLAIM_DOMAIN_TAG)
148}
149
150// TESTS
151// ================================================================================================
152
153#[cfg(test)]
154mod tests {
155    use super::{
156        super::{KERNEL_DOMAIN_TAG, KernelDescriptor},
157        *,
158    };
159
160    fn test_claim() -> ExecutionClaim {
161        let word = |a: u64| -> Word {
162            [
163                Felt::new_unchecked(a),
164                Felt::new_unchecked(a + 1),
165                Felt::new_unchecked(a + 2),
166                Felt::new_unchecked(a + 3),
167            ]
168            .into()
169        };
170        let kernel = KernelDescriptor::from_hashes(vec![word(100)]).unwrap();
171        let program_info = ProgramInfo::new(word(1), kernel);
172        let inputs = StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
173        let outputs = StackOutputs::new(&[Felt::new_unchecked(7)]).unwrap();
174        ExecutionClaim::from_program_info(program_info, inputs, outputs)
175    }
176
177    /// The commitment must bind every field and the I/O order, be domain-separated, and use
178    /// the registered selector.
179    #[test]
180    fn commitment_binds_fields_order_and_domain() {
181        let base = test_claim();
182        let base_commitment = base.commitment();
183        let base_elements = base.to_elements();
184
185        // mutate P
186        let mut mutated = base.clone();
187        mutated.program_root = [Felt::new_unchecked(999), ZERO, ZERO, ZERO].into();
188        assert_ne!(mutated.commitment(), base_commitment, "P not bound");
189
190        // mutate K (different kernel)
191        let mut mutated = base.clone();
192        mutated.kernel = KernelDescriptor::from_hashes(vec![
193            [Felt::new_unchecked(200), ZERO, ZERO, ZERO].into(),
194        ])
195        .unwrap();
196        assert_ne!(mutated.commitment(), base_commitment, "K not bound");
197
198        // mutate one element of I
199        let mut mutated = base.clone();
200        mutated.stack_inputs =
201            StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(60)]).unwrap();
202        assert_ne!(mutated.commitment(), base_commitment, "I not bound");
203
204        // mutate one element of O
205        let mut mutated = base.clone();
206        mutated.stack_outputs = StackOutputs::new(&[Felt::new_unchecked(70)]).unwrap();
207        assert_ne!(mutated.commitment(), base_commitment, "O not bound");
208
209        // swap I and O (order binding): same multiset of felts, different positions
210        let mut mutated = base;
211        mutated.stack_inputs = StackInputs::new(&[Felt::new_unchecked(7)]).unwrap();
212        mutated.stack_outputs =
213            StackOutputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
214        assert_ne!(mutated.commitment(), base_commitment, "I/O order not bound");
215
216        // domain separation: differs from the untagged hash and from another registered tag
217        let elements = base_elements;
218        assert_ne!(
219            base_commitment,
220            hasher::hash_elements(&elements),
221            "claim commitment must differ from the untagged hash"
222        );
223        assert_ne!(
224            base_commitment,
225            hasher::hash_elements_in_domain(&elements, KERNEL_DOMAIN_TAG),
226            "claim commitment must differ from a kernel-tagged hash of the same data"
227        );
228
229        // the tag is the registered selector
230        assert_eq!(
231            CLAIM_DOMAIN_TAG.as_canonical_u64(),
232            (u64::from(EXECUTION_CLAIM_DOMAIN_ID) << 8) | 1
233        );
234    }
235}