miden_lifted_stark/proof.rs
1//! STARK proof types and structured transcript.
2//!
3//! This module defines the proof artifact types shared by prover and verifier:
4//! - [`StarkProof`]: raw transcript data (field elements and commitments)
5//! - [`StarkDigest`]: binding digest committing to the entire interaction
6//! - [`StarkOutput`]: combined prover output (proof + digest)
7//! - [`StarkTranscript`]: structured parse-only view of the full protocol interaction
8//!
9//! [`StarkTranscript`] has a [`from_proof`](StarkTranscript::from_proof) constructor
10//! that parses it from proof data and a challenger, following the same pattern as
11//! [`PcsTranscript`] alongside the PCS verifier.
12
13extern crate alloc;
14
15use alloc::{vec, vec::Vec};
16
17use miden_lifted_air::LiftedAir;
18use miden_stark_transcript::{Channel, TranscriptData, VerifierChannel, VerifierTranscript};
19use p3_challenger::CanFinalizeDigest;
20use p3_field::{ExtensionField, Field, TwoAdicField};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 StarkConfig,
25 coset::LiftedCoset,
26 instance::{AirInstance, InstanceShapes, validate_air_order, validate_inputs},
27 lmcs::{Lmcs, utils::aligned_len},
28 pcs::proof::PcsTranscript,
29 verifier::VerifierError,
30};
31
32/// Commitment type alias for convenience.
33type Commitment<F, EF, SC> = <<SC as StarkConfig<F, EF>>::Lmcs as Lmcs>::Commitment;
34
35/// STARK proof: per-instance shape metadata plus raw transcript data.
36///
37/// Fields are opaque. The accessors below expose wire-format summaries
38/// (trace count, transcript sizes). Read per-instance log trace heights by
39/// parsing via [`StarkTranscript::from_proof`], which validates the shape
40/// metadata and binds it into the Fiat-Shamir challenger —
41/// [`verify_multi`](crate::verifier::verify_multi) runs the same validation.
42// Bounds target `Commitment` directly; `SC` itself isn't `Serialize`/`Debug`.
43#[derive(Clone, Serialize, Deserialize)]
44#[serde(bound(serialize = "TranscriptData<F, Commitment<F, EF, SC>>: Serialize"))]
45#[serde(bound(deserialize = "TranscriptData<F, Commitment<F, EF, SC>>: Deserialize<'de>"))]
46pub struct StarkProof<F: TwoAdicField, EF: ExtensionField<F>, SC: StarkConfig<F, EF>> {
47 pub(crate) instance_shapes: InstanceShapes,
48 pub(crate) transcript: TranscriptData<F, Commitment<F, EF, SC>>,
49}
50
51impl<F, EF, SC> core::fmt::Debug for StarkProof<F, EF, SC>
52where
53 F: TwoAdicField + core::fmt::Debug,
54 EF: ExtensionField<F>,
55 SC: StarkConfig<F, EF>,
56 Commitment<F, EF, SC>: core::fmt::Debug,
57{
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59 f.debug_struct("StarkProof")
60 .field("instance_shapes", &self.instance_shapes)
61 .field("transcript", &self.transcript)
62 .finish()
63 }
64}
65
66impl<F, EF, SC> StarkProof<F, EF, SC>
67where
68 F: TwoAdicField,
69 EF: ExtensionField<F>,
70 SC: StarkConfig<F, EF>,
71{
72 /// The AIR ordering used by the proof: `air_order()[j]` is the caller's
73 /// original index of the instance at position `j`.
74 ///
75 /// Read this before building the Fiat-Shamir challenger so you can bind
76 /// AIR configurations and the ordering — see the prover module-level docs.
77 pub fn air_order(&self) -> &[u32] {
78 self.instance_shapes.air_order()
79 }
80
81 /// Number of traces (instances) the proof was produced for.
82 pub fn num_traces(&self) -> usize {
83 self.instance_shapes.log_trace_heights.len()
84 }
85
86 /// Number of base-field elements in the transcript.
87 pub fn num_field_elements(&self) -> usize {
88 self.transcript.fields().len()
89 }
90
91 /// Number of commitments in the transcript.
92 pub fn num_commitments(&self) -> usize {
93 self.transcript.commitments().len()
94 }
95
96 /// Total byte size of the proof.
97 pub fn size_in_bytes(&self) -> usize {
98 self.instance_shapes.size_in_bytes() + self.transcript.size_in_bytes()
99 }
100}
101
102/// Transcript digest: the challenger's native binding digest that commits to
103/// the entire prover–verifier interaction. The prover and verifier must produce
104/// the same digest for the proof to be valid.
105pub type StarkDigest<F, EF, SC> =
106 <<SC as StarkConfig<F, EF>>::Challenger as CanFinalizeDigest>::Digest;
107
108/// Output of [`crate::prover::prove_single`] / [`crate::prover::prove_multi`]: the proof data and
109/// its transcript digest.
110pub struct StarkOutput<F: TwoAdicField, EF: ExtensionField<F>, SC: StarkConfig<F, EF>> {
111 /// Transcript digest committing to the entire prover–verifier interaction.
112 pub digest: StarkDigest<F, EF, SC>,
113 /// Proof data consumed by the verifier.
114 pub proof: StarkProof<F, EF, SC>,
115}
116
117impl<F, EF, SC> core::fmt::Debug for StarkOutput<F, EF, SC>
118where
119 F: TwoAdicField + core::fmt::Debug,
120 EF: ExtensionField<F>,
121 SC: StarkConfig<F, EF>,
122 StarkDigest<F, EF, SC>: core::fmt::Debug,
123 Commitment<F, EF, SC>: core::fmt::Debug,
124{
125 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126 f.debug_struct("StarkOutput")
127 .field("digest", &self.digest)
128 .field("proof", &self.proof)
129 .finish()
130 }
131}
132
133/// Structured transcript view for the full lifted STARK protocol.
134///
135/// Captures instance shape metadata, commitments, sampled challenges, the OOD
136/// evaluation point, and the PCS sub-transcript. Constructed via
137/// [`from_proof`](Self::from_proof), which mirrors steps 0–9 of
138/// [`verify_multi`](crate::verifier::verify_multi) but skips the constraint
139/// check.
140pub struct StarkTranscript<EF, L>
141where
142 L: Lmcs,
143 L::F: Field,
144 EF: ExtensionField<L::F>,
145{
146 /// Per-instance shape metadata. Validated and observed into the challenger
147 /// by [`from_proof`](Self::from_proof).
148 pub instance_shapes: InstanceShapes,
149 /// Throwaway challenge squeezed right after observing the instance shapes,
150 /// used to clear the challenger's absorb buffer so that later sampled
151 /// challenges depend on the full shape metadata regardless of sponge state.
152 pub instance_challenge: EF,
153 /// Main trace commitment.
154 pub main_commit: L::Commitment,
155 /// Randomness sampled for auxiliary traces.
156 pub randomness: Vec<EF>,
157 /// Auxiliary trace commitment.
158 pub aux_commit: L::Commitment,
159 /// Aux values per AIR instance, observed into the transcript after the aux commitment.
160 pub all_aux_values: Vec<Vec<EF>>,
161 /// Constraint folding challenge alpha.
162 pub alpha: EF,
163 /// AIR accumulation challenge beta.
164 pub beta: EF,
165 /// Quotient polynomial commitment.
166 pub quotient_commit: L::Commitment,
167 /// Out-of-domain evaluation point z.
168 pub z: EF,
169 /// PCS sub-transcript (DEEP evals, FRI rounds, query openings).
170 pub pcs_transcript: PcsTranscript<EF, L>,
171}
172
173impl<EF, L> StarkTranscript<EF, L>
174where
175 L: Lmcs,
176 L::F: TwoAdicField,
177 EF: ExtensionField<L::F>,
178{
179 /// Parse a STARK transcript from proof data and a challenger.
180 ///
181 /// Mirrors steps 0–9 of [`verify_multi`](crate::verifier::verify_multi):
182 /// 0. Validate instance shapes, then observe log trace heights into the challenger and squeeze
183 /// a throwaway `instance_challenge` to clear the absorb buffer
184 /// 1. Receive main trace commitment
185 /// 2. Sample randomness for auxiliary traces
186 /// 3. Receive auxiliary trace commitment
187 /// 4. Receive aux values (per AIR instance)
188 /// 5. Sample constraint folding alpha and accumulation beta
189 /// 6. Receive quotient commitment
190 /// 7. Sample OOD point z
191 /// 8. Build commitment widths for PCS
192 /// 9. Parse PCS sub-transcript via [`PcsTranscript::from_verifier_channel`]
193 ///
194 /// Does **not** verify constraints or check the quotient identity.
195 /// Finalizes the transcript and returns the digest alongside the parsed view.
196 #[allow(clippy::type_complexity)]
197 pub fn from_proof<A, SC>(
198 config: &SC,
199 instances: &[(&A, AirInstance<'_, L::F>)],
200 proof: &StarkProof<L::F, EF, SC>,
201 mut challenger: SC::Challenger,
202 ) -> Result<(Self, StarkDigest<L::F, EF, SC>), VerifierError>
203 where
204 A: LiftedAir<L::F, EF>,
205 SC: StarkConfig<L::F, EF, Lmcs = L>,
206 {
207 validate_air_order(proof.instance_shapes.air_order(), instances.len())?;
208 let instances = proof.instance_shapes.reorder(instances.to_vec())?;
209
210 let log_blowup = config.pcs().log_blowup();
211 let log_max_trace_height = validate_inputs(&instances, &proof.instance_shapes, log_blowup)?;
212 proof.instance_shapes.observe_heights::<L::F, _>(&mut challenger);
213
214 let mut channel = VerifierTranscript::from_data(challenger, &proof.transcript);
215
216 // Clear the challenger's absorb buffer after observing instance shapes.
217 // Mirrors `prove_multi` / `verify_multi`.
218 let instance_challenge: EF = channel.sample_algebra_element::<EF>();
219
220 let alignment = config.lmcs().alignment();
221
222 // Infer constraint degree from symbolic AIR analysis (max across all AIRs)
223 let constraint_degree =
224 instances.iter().map(|(air, _)| air.constraint_degree()).max().unwrap_or(2);
225 let log_lde_height = log_max_trace_height + log_blowup;
226
227 // Max LDE coset (for the largest trace, no lifting)
228 let max_lde_coset = LiftedCoset::unlifted(log_max_trace_height, log_blowup);
229
230 // 1. Receive main trace commitment
231 let main_commit = channel.receive_commitment()?.clone();
232
233 // 2. Sample randomness for aux traces
234 let max_num_randomness =
235 instances.iter().map(|(air, _)| air.num_randomness()).max().unwrap_or(0);
236
237 let randomness: Vec<EF> = (0..max_num_randomness)
238 .map(|_| channel.sample_algebra_element::<EF>())
239 .collect();
240
241 // 3. Receive aux trace commitment
242 let aux_commit = channel.receive_commitment()?.clone();
243
244 // 4. Receive aux values from the transcript (one EF element per aux value, per instance).
245 let all_aux_values: Vec<Vec<EF>> = instances
246 .iter()
247 .map(|(air, _)| {
248 let count = air.num_aux_values();
249 (0..count)
250 .map(|_| channel.receive_algebra_element::<EF>())
251 .collect::<Result<Vec<_>, _>>()
252 })
253 .collect::<Result<Vec<_>, _>>()?;
254
255 // 5. Sample constraint folding alpha and accumulation beta
256 let alpha: EF = channel.sample_algebra_element::<EF>();
257 let beta: EF = channel.sample_algebra_element::<EF>();
258
259 // 6. Receive quotient commitment
260 let quotient_commit = channel.receive_commitment()?.clone();
261
262 // 7. Sample OOD point (outside max trace domain H and max LDE coset gK)
263 let z: EF = max_lde_coset.sample_ood_point(&mut channel);
264 let h = L::F::two_adic_generator(log_max_trace_height.into());
265 let z_next = z * h;
266
267 // 8. Build commitment widths for PCS.
268 //
269 // The LMCS commits to rows padded to `alignment` boundary, so DEEP evals and
270 // batch openings are stored at aligned widths in the transcript. We must use
271 // aligned widths here to parse the transcript correctly.
272 // (The verifier's `verify_aligned` does the same alignment internally, then
273 // truncates the returned evals back to original widths for constraint checking.)
274 let main_widths: Vec<usize> =
275 instances.iter().map(|(air, _)| aligned_len(air.width(), alignment)).collect();
276 let quotient_width = aligned_len(constraint_degree * EF::DIMENSION, alignment);
277
278 let aux_widths: Vec<usize> = instances
279 .iter()
280 .map(|(air, _)| aligned_len(air.aux_width() * EF::DIMENSION, alignment))
281 .collect();
282
283 let commitments = vec![
284 (main_commit.clone(), main_widths),
285 (aux_commit.clone(), aux_widths),
286 (quotient_commit.clone(), vec![quotient_width]),
287 ];
288
289 // 9. Parse PCS sub-transcript
290 let pcs_transcript = PcsTranscript::from_verifier_channel::<_, 2>(
291 config.pcs(),
292 config.lmcs(),
293 &commitments,
294 log_lde_height,
295 [z, z_next],
296 &mut channel,
297 )?;
298
299 // 10. Finalize transcript and extract digest
300 let digest = channel.finalize()?;
301
302 Ok((
303 Self {
304 instance_shapes: proof.instance_shapes.clone(),
305 instance_challenge,
306 main_commit,
307 randomness,
308 aux_commit,
309 all_aux_values,
310 alpha,
311 beta,
312 quotient_commit,
313 z,
314 pcs_transcript,
315 },
316 digest,
317 ))
318 }
319}