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, PROOF_REQUEST_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/// Domain tag for the proof-request key: the registered selector
46/// `(PROOF_REQUEST_DOMAIN_ID << 8) | 1` (see the [`domain`](super::domain) module).
47pub const REQUEST_DOMAIN_TAG: Felt = domain_selector(PROOF_REQUEST_DOMAIN_ID, 1);
48
49// EXECUTION CLAIM
50// ================================================================================================
51
52/// The external statement a Miden VM proof attests: the program root and kernel identify the
53/// executed code and its syscall authorization set; the stack inputs and outputs are the
54/// execution's public I/O.
55///
56/// Stack inputs and stack outputs are both stored top-of-stack first: the first value in each
57/// slice is the top of the operand stack. The claim stores both in their canonical zero-padded
58/// 16-element form.
59///
60/// The deferred root is deliberately absent: verification returns it as an obligation.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ExecutionClaim {
63 program_root: Word,
64 kernel: KernelDescriptor,
65 stack_inputs: StackInputs,
66 stack_outputs: StackOutputs,
67}
68
69impl ExecutionClaim {
70 /// Creates a new execution claim from the program root, kernel, and stack I/O.
71 pub const fn new(
72 program_root: Word,
73 kernel: KernelDescriptor,
74 stack_inputs: StackInputs,
75 stack_outputs: StackOutputs,
76 ) -> Self {
77 Self {
78 program_root,
79 kernel,
80 stack_inputs,
81 stack_outputs,
82 }
83 }
84
85 /// Creates a new execution claim from the program info and the stack I/O.
86 pub fn from_program_info(
87 program_info: ProgramInfo,
88 stack_inputs: StackInputs,
89 stack_outputs: StackOutputs,
90 ) -> Self {
91 let (program_root, kernel) = program_info.into_parts();
92 Self::new(program_root, kernel, stack_inputs, stack_outputs)
93 }
94
95 /// Returns the MAST root of the executed program.
96 pub const fn program_root(&self) -> Word {
97 self.program_root
98 }
99
100 /// Returns the kernel descriptor of this claim.
101 pub const fn kernel(&self) -> &KernelDescriptor {
102 &self.kernel
103 }
104
105 /// Returns the program info (program root + kernel) of this claim.
106 ///
107 /// This constructs a new [`ProgramInfo`], cloning the kernel descriptor.
108 pub fn to_program_info(&self) -> ProgramInfo {
109 ProgramInfo::new(self.program_root, self.kernel.clone())
110 }
111
112 /// Returns the stack inputs of this claim.
113 pub const fn stack_inputs(&self) -> &StackInputs {
114 &self.stack_inputs
115 }
116
117 /// Returns the stack outputs of this claim.
118 pub const fn stack_outputs(&self) -> &StackOutputs {
119 &self.stack_outputs
120 }
121
122 /// Splits this claim into its program root, kernel, and stack I/O.
123 pub fn into_parts(self) -> (Word, KernelDescriptor, StackInputs, StackOutputs) {
124 (self.program_root, self.kernel, self.stack_inputs, self.stack_outputs)
125 }
126
127 /// Returns the canonical 40-element encoding `P ‖ K ‖ I ‖ O` of this claim.
128 pub fn to_elements(&self) -> [Felt; NUM_CLAIM_ELEMENTS] {
129 let mut elements = [ZERO; NUM_CLAIM_ELEMENTS];
130 elements[0..4].copy_from_slice(self.program_root.as_elements());
131 elements[4..8].copy_from_slice(self.kernel.commitment().as_elements());
132 elements[8..24].copy_from_slice(&self.stack_inputs[..]);
133 elements[24..40].copy_from_slice(&self.stack_outputs[..]);
134 elements
135 }
136
137 /// Returns the canonical commitment to this claim (`CLAIM_HASH`).
138 ///
139 /// This is the verifier-independent name of the claim: the value used to request proof
140 /// packages and to bind verified claims into a consumer's own statement.
141 pub fn commitment(&self) -> Word {
142 claim_commitment(&self.to_elements())
143 }
144}
145
146/// Returns the canonical claim commitment over an already-encoded claim.
147///
148/// This is the single implementation of `CLAIM_HASH`; every native computation of the claim
149/// commitment (including the transcript observation in `miden-air`) must go through it.
150pub fn claim_commitment(elements: &[Felt; NUM_CLAIM_ELEMENTS]) -> Word {
151 hasher::hash_elements_in_domain(elements, CLAIM_DOMAIN_TAG)
152}
153
154/// Returns the advice-map key addressing a proof package for `claim_commitment` under the
155/// verifier identified by `verifier_root`.
156///
157/// The key is `H_tag(claim_commitment ‖ verifier_root)` (one rate block, domain-separated). It
158/// is a lookup address, not a trust anchor: the verifier re-checks the retrieved package against
159/// the claim, so a wrong package fails verification. Both inputs are values the requester owns
160/// (the verifier's MAST root; the claim commitment it computed or holds from its own inputs) —
161/// neither is taken from advice.
162pub fn request_key(verifier_root: Word, claim_commitment: Word) -> Word {
163 // Absorb claim_commitment first so the MASM mirror needs a single word-swap to place the
164 // rate; the order is otherwise arbitrary (a domain-separated hash of the two words).
165 let mut preimage = [ZERO; 2 * 4];
166 preimage[0..4].copy_from_slice(claim_commitment.as_elements());
167 preimage[4..8].copy_from_slice(verifier_root.as_elements());
168 hasher::hash_elements_in_domain(&preimage, REQUEST_DOMAIN_TAG)
169}
170
171// TESTS
172// ================================================================================================
173
174#[cfg(test)]
175mod tests {
176 use super::{
177 super::{KERNEL_DOMAIN_TAG, KernelDescriptor},
178 *,
179 };
180
181 fn test_claim() -> ExecutionClaim {
182 let word = |a: u64| -> Word {
183 [
184 Felt::new_unchecked(a),
185 Felt::new_unchecked(a + 1),
186 Felt::new_unchecked(a + 2),
187 Felt::new_unchecked(a + 3),
188 ]
189 .into()
190 };
191 let kernel = KernelDescriptor::from_hashes(vec![word(100)]).unwrap();
192 let program_info = ProgramInfo::new(word(1), kernel);
193 let inputs = StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
194 let outputs = StackOutputs::new(&[Felt::new_unchecked(7)]).unwrap();
195 ExecutionClaim::from_program_info(program_info, inputs, outputs)
196 }
197
198 /// The commitment must bind every field and the I/O order, be domain-separated, and use
199 /// the registered selector.
200 #[test]
201 fn commitment_binds_fields_order_and_domain() {
202 let base = test_claim();
203 let base_commitment = base.commitment();
204 let base_elements = base.to_elements();
205
206 // mutate P
207 let mut mutated = base.clone();
208 mutated.program_root = [Felt::new_unchecked(999), ZERO, ZERO, ZERO].into();
209 assert_ne!(mutated.commitment(), base_commitment, "P not bound");
210
211 // mutate K (different kernel)
212 let mut mutated = base.clone();
213 mutated.kernel = KernelDescriptor::from_hashes(vec![
214 [Felt::new_unchecked(200), ZERO, ZERO, ZERO].into(),
215 ])
216 .unwrap();
217 assert_ne!(mutated.commitment(), base_commitment, "K not bound");
218
219 // mutate one element of I
220 let mut mutated = base.clone();
221 mutated.stack_inputs =
222 StackInputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(60)]).unwrap();
223 assert_ne!(mutated.commitment(), base_commitment, "I not bound");
224
225 // mutate one element of O
226 let mut mutated = base.clone();
227 mutated.stack_outputs = StackOutputs::new(&[Felt::new_unchecked(70)]).unwrap();
228 assert_ne!(mutated.commitment(), base_commitment, "O not bound");
229
230 // swap I and O (order binding): same multiset of felts, different positions
231 let mut mutated = base;
232 mutated.stack_inputs = StackInputs::new(&[Felt::new_unchecked(7)]).unwrap();
233 mutated.stack_outputs =
234 StackOutputs::new(&[Felt::new_unchecked(5), Felt::new_unchecked(6)]).unwrap();
235 assert_ne!(mutated.commitment(), base_commitment, "I/O order not bound");
236
237 // domain separation: differs from the untagged hash and from another registered tag
238 let elements = base_elements;
239 assert_ne!(
240 base_commitment,
241 hasher::hash_elements(&elements),
242 "claim commitment must differ from the untagged hash"
243 );
244 assert_ne!(
245 base_commitment,
246 hasher::hash_elements_in_domain(&elements, KERNEL_DOMAIN_TAG),
247 "claim commitment must differ from a kernel-tagged hash of the same data"
248 );
249
250 // the tag is the registered selector
251 assert_eq!(
252 CLAIM_DOMAIN_TAG.as_canonical_u64(),
253 (u64::from(EXECUTION_CLAIM_DOMAIN_ID) << 8) | 1
254 );
255 }
256}