Skip to main content

openvm_stark_backend/verifier/
proof_shape.rs

1use std::cmp::{max, Reverse};
2
3use itertools::{izip, Itertools};
4use thiserror::Error;
5
6use crate::{
7    calculate_n_logup, keygen::types::MultiStarkVerifyingKey0, proof::Proof,
8    prover::stacked_pcs::StackedLayout, StarkProtocolConfig,
9};
10
11#[derive(Debug, Error, PartialEq, Eq)]
12pub enum ProofShapeError {
13    #[error("Invalid VData: {0}")]
14    InvalidVData(ProofShapeVDataError),
15    #[error("Invalid GkrProof shape: {0}")]
16    InvalidGkrProofShape(GkrProofShapeError),
17    #[error("Invalid BatchConstraintProof shape: {0}")]
18    InvalidBatchConstraintProofShape(BatchProofShapeError),
19    #[error("Invalid StackingProof shape: {0}")]
20    InvalidStackingProofShape(StackingProofShapeError),
21    #[error("Invalid WhirProof shape: {0}")]
22    InvalidWhirProofShape(WhirProofShapeError),
23}
24
25impl ProofShapeError {
26    fn invalid_vdata<T>(err: ProofShapeVDataError) -> Result<T, Self> {
27        Err(Self::InvalidVData(err))
28    }
29
30    fn invalid_gkr<T>(err: GkrProofShapeError) -> Result<T, Self> {
31        Err(Self::InvalidGkrProofShape(err))
32    }
33
34    fn invalid_batch_constraint<T>(err: BatchProofShapeError) -> Result<T, Self> {
35        Err(Self::InvalidBatchConstraintProofShape(err))
36    }
37
38    fn invalid_stacking<T>(err: StackingProofShapeError) -> Result<T, Self> {
39        Err(Self::InvalidStackingProofShape(err))
40    }
41
42    fn invalid_whir<T>(err: WhirProofShapeError) -> Result<T, Self> {
43        Err(Self::InvalidWhirProofShape(err))
44    }
45}
46
47#[derive(Debug, Error, PartialEq, Eq)]
48pub enum ProofShapeVDataError {
49    #[error("Proof trace_vdata length ({len}) does not match number of AIRs ({num_airs})")]
50    InvalidVDataLength { len: usize, num_airs: usize },
51    #[error("Proof public_values length ({len}) does not match number of AIRs ({num_airs})")]
52    InvalidPublicValuesLength { len: usize, num_airs: usize },
53    #[error("AIR {air_idx} is required, but trace_vdata[{air_idx}] is None")]
54    RequiredAirNoVData { air_idx: usize },
55    #[error("AIR {air_idx} has no TraceVData, but a non-zero amount of public values")]
56    PublicValuesNoVData { air_idx: usize },
57    #[error(
58        "TraceVata for AIR {air_idx} should have {expected} cached commitments, but has {actual}"
59    )]
60    InvalidCachedCommitments {
61        air_idx: usize,
62        expected: usize,
63        actual: usize,
64    },
65    #[error("AIR {air_idx} should have log_height <= {}, but has {actual} (l_skip = {l_skip}, n_stack = {n_stack}", l_skip + n_stack)]
66    LogHeightOutOfBounds {
67        air_idx: usize,
68        l_skip: usize,
69        n_stack: usize,
70        actual: usize,
71    },
72    #[error("AIR {air_idx} should have {expected} public values, but has {actual}")]
73    InvalidPublicValues {
74        air_idx: usize,
75        expected: usize,
76        actual: usize,
77    },
78}
79
80#[derive(Debug, Error, PartialEq, Eq)]
81pub enum GkrProofShapeError {
82    #[error(
83        "claims_per_layer should have num_gkr_rounds = {expected} claims, but it has {actual}"
84    )]
85    InvalidClaimsPerLayer { expected: usize, actual: usize },
86    #[error(
87        "sumcheck_polys should have num_gkr_rounds.saturating_sub(1) = {expected} polynomials, but it has {actual}"
88    )]
89    InvalidSumcheckPolys { expected: usize, actual: usize },
90    #[error(
91        "Sumcheck polynomial for round {round} should have {expected} evaluations, but it has {actual}"
92    )]
93    InvalidSumcheckPolyEvals {
94        round: usize,
95        expected: usize,
96        actual: usize,
97    },
98}
99
100#[derive(Debug, Error, PartialEq, Eq)]
101pub enum BatchProofShapeError {
102    #[error("numerator_term_per_air should have num_airs = {expected} terms, but it has {actual}")]
103    InvalidNumeratorTerms { expected: usize, actual: usize },
104    #[error(
105        "denominator_term_per_air should have num_airs = {expected} terms, but it has {actual}"
106    )]
107    InvalidDenominatorTerms { expected: usize, actual: usize },
108    #[error(
109        "univariate_round_coeffs should have (max_constraint_degree + 1) * (2^l_skip - 1) + 1 = {expected} coefficients, but it has {actual}"
110    )]
111    InvalidUnivariateRoundCoeffs { expected: usize, actual: usize },
112    #[error(
113        "sumcheck_round_polys should have n_global = {expected} polynomials, but it has {actual}"
114    )]
115    InvalidSumcheckRoundPolys { expected: usize, actual: usize },
116    #[error(
117        "column_openings should have num_airs = {expected} sets of openings, but it has {actual}"
118    )]
119    InvalidColumnOpeningsAirs { expected: usize, actual: usize },
120    #[error(
121        "sumcheck_round_polys[{round}] should have degree = {expected} evaluations, but it has {actual}"
122    )]
123    InvalidSumcheckRoundPolyEvals {
124        round: usize,
125        expected: usize,
126        actual: usize,
127    },
128    #[error(
129        "AIR {air_idx} has {expected} parts, but there are {actual} sets of per-part column openings"
130    )]
131    InvalidColumnOpeningsPerAir {
132        air_idx: usize,
133        expected: usize,
134        actual: usize,
135    },
136    #[error(
137        "There should be {expected} column opening pairs for AIR {air_idx}'s main trace, but instead there are {actual}"
138    )]
139    InvalidColumnOpeningsPerAirMain {
140        air_idx: usize,
141        expected: usize,
142        actual: usize,
143    },
144    #[error(
145        "There should be {expected} column opening pairs for AIR {air_idx}'s preprocessed trace, but instead there are {actual}"
146    )]
147    InvalidColumnOpeningsPerAirPreprocessed {
148        air_idx: usize,
149        expected: usize,
150        actual: usize,
151    },
152    #[error(
153        "There should be {expected} column opening pairs for AIR {air_idx}'s cached trace {cached_idx}, but instead there are {actual}"
154    )]
155    InvalidColumnOpeningsPerAirCached {
156        air_idx: usize,
157        cached_idx: usize,
158        expected: usize,
159        actual: usize,
160    },
161}
162
163#[derive(Debug, Error, PartialEq, Eq)]
164pub enum StackingProofShapeError {
165    #[error(
166        "univariate_round_coeffs should have 2 * ((1 << mvk.params.l_skip) - 1) + 1 = {expected} coefficients, but it has {actual}"
167    )]
168    InvalidUnivariateRoundCoeffs { expected: usize, actual: usize },
169    #[error(
170        "sumcheck_round_polys should have n_stack = {expected} polynomials, but it has {actual}"
171    )]
172    InvalidSumcheckRoundPolys { expected: usize, actual: usize },
173    #[error(
174        "There should be {expected} sets of per-commit stacking openings, but instead there are {actual}"
175    )]
176    InvalidStackOpenings { expected: usize, actual: usize },
177    #[error(
178        "Stacked matrix {commit_idx} should have {expected} stacking openings, but instead there are {actual}"
179    )]
180    InvalidStackOpeningsPerMatrix {
181        commit_idx: usize,
182        expected: usize,
183        actual: usize,
184    },
185    #[error("Total stacked width across commits ({actual}) exceeds w_stack ({w_stack})")]
186    TotalStackedWidthOutOfBounds { w_stack: usize, actual: usize },
187}
188
189#[derive(Debug, Error, PartialEq, Eq)]
190pub enum WhirProofShapeError {
191    #[error(
192        "whir_sumcheck_polys should have num_whir_sumcheck_rounds = {expected} polynomials, but it has {actual}"
193    )]
194    InvalidSumcheckPolys { expected: usize, actual: usize },
195    #[error("final_poly should have len = {expected}, but it has {actual}")]
196    InvalidFinalPolyLen { expected: usize, actual: usize },
197    #[error(
198        "There should be num_whir_rounds = {expected} codeword commits, but there are {actual}"
199    )]
200    InvalidCodewordCommits { expected: usize, actual: usize },
201    #[error(
202        "There should be num_whir_rounds = {expected} out-of-domain values, but there are {actual}"
203    )]
204    InvalidOodValues { expected: usize, actual: usize },
205    #[error(
206        "There should be num_whir_sumcheck_rounds = {expected} folding PoW witnesses, but there are {actual}"
207    )]
208    InvalidFoldingPowWitnesses { expected: usize, actual: usize },
209    #[error(
210        "There should be num_whir_rounds = {expected} query phase PoW witnesses, but there are {actual}"
211    )]
212    InvalidQueryPhasePowWitnesses { expected: usize, actual: usize },
213    #[error(
214        "There should be num_commits = {expected} sets of initial round opened rows, but there are {actual}"
215    )]
216    InvalidInitialRoundOpenedRows { expected: usize, actual: usize },
217    #[error(
218        "There should be num_commits = {expected} sets of initial round merkle proofs, but there are {actual}"
219    )]
220    InvalidInitialRoundMerkleProofs { expected: usize, actual: usize },
221    #[error(
222        "There should be num_whir_rounds = {expected} sets of non-initial round opened rows, but there are {actual}"
223    )]
224    InvalidCodewordOpenedRows { expected: usize, actual: usize },
225    #[error(
226        "There should be num_whir_rounds = {expected} sets of non-initial round merkle proofs, but there are {actual}"
227    )]
228    InvalidCodewordMerkleProofs { expected: usize, actual: usize },
229    #[error(
230        "There should be num_whir_queries = {expected} initial round opened rows for commit {commit_idx}, but there are {actual}"
231    )]
232    InvalidInitialRoundOpenedRowsQueries {
233        commit_idx: usize,
234        expected: usize,
235        actual: usize,
236    },
237    #[error(
238        "There should be num_whir_queries = {expected} initial round merkle proofs for commit {commit_idx}, but there are {actual}"
239    )]
240    InvalidInitialRoundMerkleProofsQueries {
241        commit_idx: usize,
242        expected: usize,
243        actual: usize,
244    },
245    #[error(
246        "Initial round opened row {opened_idx} for commit {commit_idx} should have length {expected}, but it has length {actual}"
247    )]
248    InvalidInitialRoundOpenedRowK {
249        opened_idx: usize,
250        commit_idx: usize,
251        expected: usize,
252        actual: usize,
253    },
254    #[error(
255        "Initial round opened row {row_idx} for commit {commit_idx} should have width {expected}, but it has width {actual}"
256    )]
257    InvalidInitialRoundOpenedRowWidth {
258        row_idx: usize,
259        commit_idx: usize,
260        expected: usize,
261        actual: usize,
262    },
263    #[error(
264        "Initial round merkle proof {opened_idx} for commit {commit_idx} should have depth {expected}, but it has depth {actual}"
265    )]
266    InvalidInitialRoundMerkleProofDepth {
267        opened_idx: usize,
268        commit_idx: usize,
269        expected: usize,
270        actual: usize,
271    },
272    #[error(
273        "There should be num_whir_queries = {expected} round {round} opened rows, but there are {actual}"
274    )]
275    InvalidCodewordOpenedRowsQueries {
276        round: usize,
277        expected: usize,
278        actual: usize,
279    },
280    #[error(
281        "There should be num_whir_queries = {expected} round {round} merkle proofs, but there are {actual}"
282    )]
283    InvalidCodewordMerkleProofsQueries {
284        round: usize,
285        expected: usize,
286        actual: usize,
287    },
288    #[error(
289        "Round {round} opened row {opened_idx} should have length {expected}, but it has length {actual}"
290    )]
291    InvalidCodewordOpenedValues {
292        round: usize,
293        opened_idx: usize,
294        expected: usize,
295        actual: usize,
296    },
297    #[error(
298        "Round {round} merkle proof {opened_idx} should have depth {expected}, but it has depth {actual}"
299    )]
300    InvalidCodewordMerkleProofDepth {
301        round: usize,
302        opened_idx: usize,
303        expected: usize,
304        actual: usize,
305    },
306}
307
308pub fn verify_proof_shape<SC: StarkProtocolConfig>(
309    mvk: &MultiStarkVerifyingKey0<SC>,
310    proof: &Proof<SC>,
311) -> Result<Vec<StackedLayout>, ProofShapeError> {
312    // TRACE HEIGHTS AND PUBLIC VALUES
313    let num_airs = mvk.per_air.len();
314    let l_skip = mvk.params.l_skip;
315
316    if proof.trace_vdata.len() != num_airs {
317        return ProofShapeError::invalid_vdata(ProofShapeVDataError::InvalidVDataLength {
318            len: proof.trace_vdata.len(),
319            num_airs,
320        });
321    } else if proof.public_values.len() != num_airs {
322        return ProofShapeError::invalid_vdata(ProofShapeVDataError::InvalidPublicValuesLength {
323            len: proof.public_values.len(),
324            num_airs,
325        });
326    }
327
328    for (air_idx, (vk, vdata, pvs)) in
329        izip!(&mvk.per_air, &proof.trace_vdata, &proof.public_values).enumerate()
330    {
331        if vdata.is_none() {
332            if vk.is_required {
333                return ProofShapeError::invalid_vdata(ProofShapeVDataError::RequiredAirNoVData {
334                    air_idx,
335                });
336            } else if !pvs.is_empty() {
337                return ProofShapeError::invalid_vdata(ProofShapeVDataError::PublicValuesNoVData {
338                    air_idx,
339                });
340            }
341        } else {
342            let vdata = vdata.as_ref().unwrap();
343            if vdata.cached_commitments.len() != vk.num_cached_mains() {
344                return ProofShapeError::invalid_vdata(
345                    ProofShapeVDataError::InvalidCachedCommitments {
346                        air_idx,
347                        expected: vk.num_cached_mains(),
348                        actual: vdata.cached_commitments.len(),
349                    },
350                );
351            } else if vdata.log_height > l_skip + mvk.params.n_stack {
352                return ProofShapeError::invalid_vdata(
353                    ProofShapeVDataError::LogHeightOutOfBounds {
354                        air_idx,
355                        l_skip,
356                        n_stack: mvk.params.n_stack,
357                        actual: vdata.log_height,
358                    },
359                );
360            } else if vk.params.num_public_values != pvs.len() {
361                return ProofShapeError::invalid_vdata(ProofShapeVDataError::InvalidPublicValues {
362                    air_idx,
363                    expected: vk.params.num_public_values,
364                    actual: pvs.len(),
365                });
366            }
367        }
368    }
369
370    let per_trace = mvk
371        .per_air
372        .iter()
373        .zip(&proof.trace_vdata)
374        .enumerate()
375        .filter_map(|(air_idx, (vk, vdata))| vdata.as_ref().map(|vdata| (air_idx, vk, vdata)))
376        .sorted_by_key(|(_, _, vdata)| Reverse(vdata.log_height))
377        .collect_vec();
378    let num_airs_present = per_trace.len();
379
380    // GKR PROOF SHAPE
381    let total_interactions = per_trace.iter().fold(0u64, |acc, (_, vk, vdata)| {
382        acc + ((vk.num_interactions() as u64) << max(vdata.log_height, l_skip))
383    });
384    let n_logup = calculate_n_logup(l_skip, total_interactions);
385    let num_gkr_rounds = if total_interactions == 0 {
386        0
387    } else {
388        l_skip + n_logup
389    };
390
391    if proof.gkr_proof.claims_per_layer.len() != num_gkr_rounds {
392        return ProofShapeError::invalid_gkr(GkrProofShapeError::InvalidClaimsPerLayer {
393            expected: num_gkr_rounds,
394            actual: proof.gkr_proof.claims_per_layer.len(),
395        });
396    } else if proof.gkr_proof.sumcheck_polys.len() != num_gkr_rounds.saturating_sub(1) {
397        return ProofShapeError::invalid_gkr(GkrProofShapeError::InvalidSumcheckPolys {
398            expected: num_gkr_rounds.saturating_sub(1),
399            actual: proof.gkr_proof.sumcheck_polys.len(),
400        });
401    }
402
403    for (i, poly) in proof.gkr_proof.sumcheck_polys.iter().enumerate() {
404        if poly.len() != i + 1 {
405            return ProofShapeError::invalid_gkr(GkrProofShapeError::InvalidSumcheckPolyEvals {
406                round: i + 1,
407                expected: i + 1,
408                actual: poly.len(),
409            });
410        }
411    }
412
413    // BATCH CONSTRAINTS PROOF SHAPE
414    let batch_proof = &proof.batch_constraint_proof;
415
416    let n_max = per_trace[0].2.log_height.saturating_sub(l_skip);
417
418    let s_0_deg = (mvk.max_constraint_degree() + 1) * ((1 << l_skip) - 1);
419    if batch_proof.numerator_term_per_air.len() != num_airs_present {
420        return ProofShapeError::invalid_batch_constraint(
421            BatchProofShapeError::InvalidNumeratorTerms {
422                expected: num_airs_present,
423                actual: batch_proof.numerator_term_per_air.len(),
424            },
425        );
426    } else if batch_proof.denominator_term_per_air.len() != num_airs_present {
427        return ProofShapeError::invalid_batch_constraint(
428            BatchProofShapeError::InvalidDenominatorTerms {
429                expected: num_airs_present,
430                actual: batch_proof.denominator_term_per_air.len(),
431            },
432        );
433    } else if batch_proof.univariate_round_coeffs.len() != s_0_deg + 1 {
434        return ProofShapeError::invalid_batch_constraint(
435            BatchProofShapeError::InvalidUnivariateRoundCoeffs {
436                expected: s_0_deg + 1,
437                actual: batch_proof.univariate_round_coeffs.len(),
438            },
439        );
440    } else if batch_proof.sumcheck_round_polys.len() != n_max {
441        return ProofShapeError::invalid_batch_constraint(
442            BatchProofShapeError::InvalidSumcheckRoundPolys {
443                expected: n_max,
444                actual: batch_proof.sumcheck_round_polys.len(),
445            },
446        );
447    } else if batch_proof.column_openings.len() != num_airs_present {
448        return ProofShapeError::invalid_batch_constraint(
449            BatchProofShapeError::InvalidColumnOpeningsAirs {
450                expected: num_airs_present,
451                actual: batch_proof.column_openings.len(),
452            },
453        );
454    }
455
456    for (i, evals) in batch_proof.sumcheck_round_polys.iter().enumerate() {
457        if evals.len() != mvk.max_constraint_degree() + 1 {
458            return ProofShapeError::invalid_batch_constraint(
459                BatchProofShapeError::InvalidSumcheckRoundPolyEvals {
460                    round: i,
461                    expected: mvk.max_constraint_degree() + 1,
462                    actual: evals.len(),
463                },
464            );
465        }
466    }
467
468    for (part_openings, &(air_idx, vk, _)) in batch_proof.column_openings.iter().zip(&per_trace) {
469        let need_rot = mvk.per_air[air_idx].params.need_rot;
470        let openings_per_col = if need_rot { 2 } else { 1 };
471        if part_openings.len() != vk.num_parts() {
472            return ProofShapeError::invalid_batch_constraint(
473                BatchProofShapeError::InvalidColumnOpeningsPerAir {
474                    air_idx,
475                    expected: vk.num_parts(),
476                    actual: part_openings.len(),
477                },
478            );
479        } else if part_openings[0].len() != vk.params.width.common_main * openings_per_col {
480            return ProofShapeError::invalid_batch_constraint(
481                BatchProofShapeError::InvalidColumnOpeningsPerAirMain {
482                    air_idx,
483                    expected: vk.params.width.common_main,
484                    actual: part_openings[0].len(),
485                },
486            );
487        } else if let Some(preprocessed_width) = &vk.params.width.preprocessed {
488            if part_openings[1].len() != *preprocessed_width * openings_per_col {
489                return ProofShapeError::invalid_batch_constraint(
490                    BatchProofShapeError::InvalidColumnOpeningsPerAirPreprocessed {
491                        air_idx,
492                        expected: *preprocessed_width,
493                        actual: part_openings[1].len(),
494                    },
495                );
496            }
497        }
498
499        let cached_openings = &part_openings[1 + (vk.preprocessed_data.is_some() as usize)..];
500        for (cached_idx, (col_opening, &width)) in cached_openings
501            .iter()
502            .zip(&vk.params.width.cached_mains)
503            .enumerate()
504        {
505            if col_opening.len() != width * openings_per_col {
506                return ProofShapeError::invalid_batch_constraint(
507                    BatchProofShapeError::InvalidColumnOpeningsPerAirCached {
508                        air_idx,
509                        cached_idx,
510                        expected: width,
511                        actual: col_opening.len(),
512                    },
513                );
514            }
515        }
516    }
517
518    // STACKING PROOF SHAPE
519    let stacking_proof = &proof.stacking_proof;
520
521    let s_0_deg = 2 * ((1 << l_skip) - 1);
522    if stacking_proof.univariate_round_coeffs.len() != s_0_deg + 1 {
523        return ProofShapeError::invalid_stacking(
524            StackingProofShapeError::InvalidUnivariateRoundCoeffs {
525                expected: s_0_deg + 1,
526                actual: stacking_proof.univariate_round_coeffs.len(),
527            },
528        );
529    } else if stacking_proof.sumcheck_round_polys.len() != mvk.params.n_stack {
530        return ProofShapeError::invalid_stacking(
531            StackingProofShapeError::InvalidSumcheckRoundPolys {
532                expected: mvk.params.n_stack,
533                actual: stacking_proof.sumcheck_round_polys.len(),
534            },
535        );
536    }
537
538    let common_main_layout = StackedLayout::new(
539        l_skip,
540        mvk.params.n_stack + l_skip,
541        per_trace
542            .iter()
543            .map(|(_, vk, vdata)| (vk.params.width.common_main, vdata.log_height))
544            .collect_vec(),
545    )
546    .unwrap();
547
548    let other_layouts = per_trace
549        .iter()
550        .flat_map(|(_, vk, vdata)| {
551            vk.params
552                .width
553                .preprocessed
554                .iter()
555                .chain(&vk.params.width.cached_mains)
556                .copied()
557                .map(|width| (width, vdata.log_height))
558                .collect_vec()
559        })
560        .map(|sorted| {
561            StackedLayout::new(l_skip, mvk.params.n_stack + l_skip, vec![sorted]).unwrap()
562        })
563        .collect_vec();
564
565    // layouts.len() is the total number of commitments, which is always >= 1. The total number is
566    // NOT determined from the vkey because it depends on which traces are present.
567    let layouts = [common_main_layout]
568        .into_iter()
569        .chain(other_layouts)
570        .collect_vec();
571
572    let total_stacked_width: usize = layouts.iter().map(StackedLayout::width).sum();
573    if total_stacked_width > mvk.params.w_stack {
574        return ProofShapeError::invalid_stacking(
575            StackingProofShapeError::TotalStackedWidthOutOfBounds {
576                w_stack: mvk.params.w_stack,
577                actual: total_stacked_width,
578            },
579        );
580    }
581
582    if stacking_proof.stacking_openings.len() != layouts.len() {
583        return ProofShapeError::invalid_stacking(StackingProofShapeError::InvalidStackOpenings {
584            expected: layouts.len(),
585            actual: stacking_proof.stacking_openings.len(),
586        });
587    }
588
589    for (commit_idx, (openings, layout)) in stacking_proof
590        .stacking_openings
591        .iter()
592        .zip(&layouts)
593        .enumerate()
594    {
595        let stacked_matrix_width = layout.width();
596        if openings.len() != stacked_matrix_width {
597            return ProofShapeError::invalid_stacking(
598                StackingProofShapeError::InvalidStackOpeningsPerMatrix {
599                    commit_idx,
600                    expected: stacked_matrix_width,
601                    actual: openings.len(),
602                },
603            );
604        }
605    }
606
607    // WHIR PROOF SHAPE
608    let whir_proof = &proof.whir_proof;
609
610    let log_stacked_height = mvk.params.log_stacked_height();
611    let num_whir_rounds = mvk.params.num_whir_rounds();
612    let num_whir_sumcheck_rounds = mvk.params.num_whir_sumcheck_rounds();
613    let k_whir = mvk.params.k_whir();
614    debug_assert_ne!(num_whir_rounds, 0);
615
616    if whir_proof.whir_sumcheck_polys.len() != num_whir_sumcheck_rounds {
617        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidSumcheckPolys {
618            expected: num_whir_sumcheck_rounds,
619            actual: whir_proof.whir_sumcheck_polys.len(),
620        });
621    } else if whir_proof.codeword_commits.len() != num_whir_rounds - 1 {
622        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidCodewordCommits {
623            expected: num_whir_rounds - 1,
624            actual: whir_proof.codeword_commits.len(),
625        });
626    } else if whir_proof.ood_values.len() != num_whir_rounds - 1 {
627        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidOodValues {
628            expected: num_whir_rounds - 1,
629            actual: whir_proof.ood_values.len(),
630        });
631    } else if whir_proof.folding_pow_witnesses.len() != num_whir_sumcheck_rounds {
632        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidFoldingPowWitnesses {
633            expected: num_whir_sumcheck_rounds,
634            actual: whir_proof.folding_pow_witnesses.len(),
635        });
636    } else if whir_proof.query_phase_pow_witnesses.len() != num_whir_rounds {
637        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidQueryPhasePowWitnesses {
638            expected: num_whir_rounds,
639            actual: whir_proof.query_phase_pow_witnesses.len(),
640        });
641    } else if whir_proof.initial_round_opened_rows.len() != layouts.len() {
642        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidInitialRoundOpenedRows {
643            expected: layouts.len(),
644            actual: whir_proof.initial_round_opened_rows.len(),
645        });
646    } else if whir_proof.initial_round_merkle_proofs.len() != layouts.len() {
647        return ProofShapeError::invalid_whir(
648            WhirProofShapeError::InvalidInitialRoundMerkleProofs {
649                expected: layouts.len(),
650                actual: whir_proof.initial_round_merkle_proofs.len(),
651            },
652        );
653    } else if whir_proof.codeword_opened_values.len() != num_whir_rounds - 1 {
654        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidCodewordOpenedRows {
655            expected: num_whir_rounds - 1,
656            actual: whir_proof.codeword_opened_values.len(),
657        });
658    } else if whir_proof.codeword_merkle_proofs.len() != num_whir_rounds - 1 {
659        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidCodewordMerkleProofs {
660            expected: num_whir_rounds - 1,
661            actual: whir_proof.codeword_merkle_proofs.len(),
662        });
663    } else if whir_proof.final_poly.len() != 1 << mvk.params.log_final_poly_len() {
664        return ProofShapeError::invalid_whir(WhirProofShapeError::InvalidFinalPolyLen {
665            expected: 1 << mvk.params.log_final_poly_len(),
666            actual: whir_proof.final_poly.len(),
667        });
668    }
669
670    let initial_whir_round_num_queries = mvk.params.whir.rounds[0].num_queries;
671    for (commit_idx, (opened_rows, merkle_proofs)) in whir_proof
672        .initial_round_opened_rows
673        .iter()
674        .zip(&whir_proof.initial_round_merkle_proofs)
675        .enumerate()
676    {
677        if opened_rows.len() != initial_whir_round_num_queries {
678            return ProofShapeError::invalid_whir(
679                WhirProofShapeError::InvalidInitialRoundOpenedRowsQueries {
680                    commit_idx,
681                    expected: initial_whir_round_num_queries,
682                    actual: opened_rows.len(),
683                },
684            );
685        } else if merkle_proofs.len() != initial_whir_round_num_queries {
686            return ProofShapeError::invalid_whir(
687                WhirProofShapeError::InvalidInitialRoundMerkleProofsQueries {
688                    commit_idx,
689                    expected: initial_whir_round_num_queries,
690                    actual: merkle_proofs.len(),
691                },
692            );
693        }
694        let width = stacking_proof.stacking_openings[commit_idx].len();
695        for (opened_idx, rows) in opened_rows.iter().enumerate() {
696            if rows.len() != 1 << k_whir {
697                return ProofShapeError::invalid_whir(
698                    WhirProofShapeError::InvalidInitialRoundOpenedRowK {
699                        opened_idx,
700                        commit_idx,
701                        expected: 1 << k_whir,
702                        actual: rows.len(),
703                    },
704                );
705            }
706            for (row_idx, row) in rows.iter().enumerate() {
707                if row.len() != width {
708                    return ProofShapeError::invalid_whir(
709                        WhirProofShapeError::InvalidInitialRoundOpenedRowWidth {
710                            row_idx,
711                            commit_idx,
712                            expected: width,
713                            actual: row.len(),
714                        },
715                    );
716                }
717            }
718        }
719
720        let merkle_depth = (log_stacked_height + mvk.params.log_blowup).saturating_sub(k_whir);
721        for (opened_idx, proof) in merkle_proofs.iter().enumerate() {
722            if proof.len() != merkle_depth {
723                return ProofShapeError::invalid_whir(
724                    WhirProofShapeError::InvalidInitialRoundMerkleProofDepth {
725                        opened_idx,
726                        commit_idx,
727                        expected: merkle_depth,
728                        actual: proof.len(),
729                    },
730                );
731            }
732        }
733    }
734
735    for (round_minus_one, (opened_values_per_query, merkle_proofs)) in whir_proof
736        .codeword_opened_values
737        .iter()
738        .zip(&whir_proof.codeword_merkle_proofs)
739        .take(num_whir_rounds - 1)
740        .enumerate()
741    {
742        let round = round_minus_one + 1;
743        let num_queries = mvk.params.whir.rounds[round].num_queries;
744        if opened_values_per_query.len() != num_queries {
745            return ProofShapeError::invalid_whir(
746                WhirProofShapeError::InvalidCodewordOpenedRowsQueries {
747                    round,
748                    expected: num_queries,
749                    actual: opened_values_per_query.len(),
750                },
751            );
752        } else if merkle_proofs.len() != num_queries {
753            return ProofShapeError::invalid_whir(
754                WhirProofShapeError::InvalidCodewordMerkleProofsQueries {
755                    round,
756                    expected: num_queries,
757                    actual: merkle_proofs.len(),
758                },
759            );
760        }
761
762        for (opened_idx, opened_values) in opened_values_per_query.iter().enumerate() {
763            if opened_values.len() != 1 << mvk.params.k_whir() {
764                return ProofShapeError::invalid_whir(
765                    WhirProofShapeError::InvalidCodewordOpenedValues {
766                        round,
767                        opened_idx,
768                        expected: 1 << mvk.params.k_whir(),
769                        actual: opened_values.len(),
770                    },
771                );
772            }
773        }
774
775        let merkle_depth = log_stacked_height + mvk.params.log_blowup - k_whir - round;
776        for (opened_idx, proof) in merkle_proofs.iter().enumerate() {
777            if proof.len() != merkle_depth {
778                return ProofShapeError::invalid_whir(
779                    WhirProofShapeError::InvalidCodewordMerkleProofDepth {
780                        round,
781                        opened_idx,
782                        expected: merkle_depth,
783                        actual: proof.len(),
784                    },
785                );
786            }
787        }
788    }
789
790    Ok(layouts)
791}