Skip to main content

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}