Skip to main content

lib_q_stark_commit/
pcs.rs

1//! Traits for polynomial commitment schemes.
2
3use alloc::vec::Vec;
4use core::fmt::Debug;
5
6use lib_q_stark_field::ExtensionField;
7use lib_q_stark_matrix::Matrix;
8use lib_q_stark_matrix::dense::RowMajorMatrix;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use crate::PolynomialSpace;
13
14pub type Val<D> = <D as PolynomialSpace>::Val;
15
16/// A polynomial commitment scheme, for committing to batches of polynomials defined by their evaluations
17/// over some domain.
18///
19/// In general this does not have to be a hiding commitment scheme but it might be for some implementations.
20// TODO: Should we have a super-trait for weakly-binding PCSs, like FRI outside unique decoding radius?
21pub trait Pcs<Challenge, Challenger>
22where
23    Challenge: ExtensionField<Val<Self::Domain>>,
24{
25    /// The class of evaluation domains that this commitment scheme works over.
26    type Domain: PolynomialSpace;
27
28    /// The commitment that's sent to the verifier.
29    type Commitment: Clone + Serialize + DeserializeOwned;
30
31    /// Data that the prover stores for committed polynomials, to help the prover with opening.
32    type ProverData;
33
34    /// Type of the output of `get_evaluations_on_domain`.
35    type EvaluationsOnDomain<'a>: Matrix<Val<Self::Domain>> + 'a;
36
37    /// The opening argument.
38    type Proof: Clone + Serialize + DeserializeOwned;
39
40    /// The type of a proof verification error.
41    type Error: Debug;
42
43    /// Set to true to activate randomization and achieve zero-knowledge.
44    const ZK: bool;
45
46    /// Index of the trace commitment in the computed opened values.
47    const TRACE_IDX: usize = Self::ZK as usize;
48
49    /// Index of the quotient commitments in the computed opened values.
50    const QUOTIENT_IDX: usize = Self::TRACE_IDX + 1;
51
52    /// Index of the preprocessed trace commitment in the computed opened values.
53    const PREPROCESSED_TRACE_IDX: usize = Self::QUOTIENT_IDX + 1; // Note: not always present
54
55    /// This should return a domain such that `Domain::next_point` returns `Some`.
56    ///
57    /// # Panics
58    /// Implementations backed by a two-adic domain (the only kind in this workspace today) will
59    /// panic if `degree` is not a power of two, or exceeds the field's two-adicity. Callers that
60    /// process untrusted/adversarial `degree` values (e.g. a proof verifier) MUST validate `degree`
61    /// themselves before calling this, or use [`try_natural_domain_for_degree`](Self::try_natural_domain_for_degree) instead.
62    fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain;
63
64    /// Fallible sibling of [`natural_domain_for_degree`](Self::natural_domain_for_degree), for
65    /// callers (such as proof verifiers) that must reject an out-of-range `degree` instead of
66    /// panicking. Returns `None` exactly under the conditions documented on
67    /// `natural_domain_for_degree`'s `# Panics` section.
68    ///
69    /// The default implementation simply delegates to the infallible method, so it is only
70    /// non-panicking for implementations that override it; `TwoAdicFriPcs` and `HidingFriPcs`
71    /// (the only implementations in this workspace, in `lib-q-stark-fri`) both override it with a
72    /// genuinely non-panicking check.
73    fn try_natural_domain_for_degree(&self, degree: usize) -> Option<Self::Domain> {
74        Some(self.natural_domain_for_degree(degree))
75    }
76
77    /// Given a collection of evaluation matrices, produce a binding commitment to
78    /// the polynomials defined by those evaluations. If `zk` is enabled, the evaluations are
79    /// first randomized as explained in Section 3 of <https://eprint.iacr.org/2024/1037.pdf>.
80    ///
81    /// Returns both the commitment which should be sent to the verifier
82    /// and the prover data which can be used to produce opening proofs.
83    #[allow(clippy::type_complexity)]
84    fn commit(
85        &self,
86        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
87    ) -> (Self::Commitment, Self::ProverData);
88
89    /// Commit to the quotient polynomial. We first decompose the quotient polynomial into
90    /// `num_chunks` many smaller polynomials each of degree `degree / num_chunks`.
91    /// This can have minor performance benefits, but is not strictly necessary in the non `zk` case.
92    /// When `zk` is enabled, this commitment will additionally include some randomization process
93    /// to hide the inputs.
94    ///
95    /// ### Arguments
96    /// - `quotient_domain` the domain of the quotient polynomial.
97    /// - `quotient_evaluations` the evaluations of the quotient polynomial over the domain. This should be in
98    ///   standard (not bit-reversed) order.
99    /// - `num_chunks` the number of smaller polynomials to decompose the quotient polynomial into.
100    #[allow(clippy::type_complexity)]
101    fn commit_quotient(
102        &self,
103        quotient_domain: Self::Domain,
104        quotient_evaluations: RowMajorMatrix<Val<Self::Domain>>,
105        num_chunks: usize,
106    ) -> (Self::Commitment, Self::ProverData) {
107        let quotient_sub_evaluations =
108            quotient_domain.split_evals(num_chunks, quotient_evaluations);
109        let quotient_sub_domains = quotient_domain.split_domains(num_chunks);
110        let ldes = self.get_quotient_ldes(
111            quotient_sub_domains
112                .into_iter()
113                .zip(quotient_sub_evaluations),
114            num_chunks,
115        );
116        self.commit_ldes(ldes)
117    }
118
119    /// When committing to quotient polynomials in batch-STARK, it is simpler to first compute
120    /// the LDE evaluations before batch-committing. When `zk` is enabled, this may add randomization.
121    fn get_quotient_ldes(
122        &self,
123        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
124        num_chunks: usize,
125    ) -> Vec<RowMajorMatrix<Val<Self::Domain>>>;
126
127    /// Commits to a collection of LDE evaluation matrices.
128    fn commit_ldes(
129        &self,
130        ldes: Vec<RowMajorMatrix<Val<Self::Domain>>>,
131    ) -> (Self::Commitment, Self::ProverData);
132
133    /// Same as `commit`; used when the committed data is preprocessing (e.g. fixed trace).
134    fn commit_preprocessing(
135        &self,
136        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
137    ) -> (Self::Commitment, Self::ProverData) {
138        self.commit(evaluations)
139    }
140
141    /// Given prover data corresponding to a commitment to a collection of evaluation matrices,
142    /// return the evaluations of those matrices on the given domain.
143    ///
144    /// This is essentially a no-op when called with a `domain` which is a subset of the evaluation domain
145    /// on which the evaluation matrices are defined.
146    fn get_evaluations_on_domain<'a>(
147        &self,
148        prover_data: &'a Self::ProverData,
149        idx: usize,
150        domain: Self::Domain,
151    ) -> Self::EvaluationsOnDomain<'a>;
152
153    /// Like `get_evaluations_on_domain` but without applying ZK randomization (e.g. for quotient domain).
154    fn get_evaluations_on_domain_no_random<'a>(
155        &self,
156        prover_data: &'a Self::ProverData,
157        idx: usize,
158        domain: Self::Domain,
159    ) -> Self::EvaluationsOnDomain<'a> {
160        self.get_evaluations_on_domain(prover_data, idx, domain)
161    }
162
163    /// Open a collection of polynomial commitments at a set of points. Produce the values at those points along with a proof
164    /// of correctness.
165    ///
166    /// Arguments:
167    /// - `commitment_data_with_opening_points`: A vector whose elements are a pair:
168    ///     - `data`: The prover data corresponding to a multi-matrix commitment.
169    ///     - `opening_points`: A vector containing, for each matrix committed to, a vector of opening points.
170    /// - `fiat_shamir_challenger`: The challenger that will be used to generate the proof.
171    ///
172    /// Unwrapping the arguments further, each `data` contains a vector of the committed matrices (`matrices = Vec<M>`).
173    /// If the length of `matrices` is not equal to the length of `opening_points` the function will error. Otherwise, for
174    /// each index `i`, the matrix `M = matrices[i]` will be opened at the points `opening_points[i]`.
175    ///
176    /// This means that each column of `M` will be interpreted as the evaluation vector of some polynomial
177    /// and we will compute the value of all of those polynomials at `opening_points[i]`.
178    ///
179    /// The domains on which the evaluation vectors are defined is not part of the arguments here
180    /// but should be public information known to both the prover and verifier.
181    fn open(
182        &self,
183        // For each multi-matrix commitment,
184        commitment_data_with_opening_points: Vec<(
185            // The matrices and auxiliary prover data
186            &Self::ProverData,
187            // for each matrix,
188            Vec<
189                // the points to open
190                Vec<Challenge>,
191            >,
192        )>,
193        fiat_shamir_challenger: &mut Challenger,
194    ) -> (OpenedValues<Challenge>, Self::Proof);
195
196    /// Like `open` but allows the implementation to treat some rounds as preprocessing (e.g. for ZK).
197    #[allow(clippy::type_complexity)]
198    fn open_with_preprocessing(
199        &self,
200        rounds: Vec<(&Self::ProverData, Vec<Vec<Challenge>>)>,
201        challenger: &mut Challenger,
202        _is_preprocessing: bool,
203    ) -> (OpenedValues<Challenge>, Self::Proof) {
204        self.open(rounds, challenger)
205    }
206
207    /// Verify that a collection of opened values is correct.
208    ///
209    /// Arguments:
210    /// - `commitments_with_opening_points`: A vector whose elements are a pair:
211    ///     - `commitment`: A multi matrix commitment.
212    ///     - `opening_points`: A vector containing, for each matrix committed to, a vector of opening points and claimed evaluations.
213    /// - `proof`: A claimed proof of correctness for the opened values.
214    /// - `fiat_shamir_challenger`: The challenger that will be used to generate the proof.
215    #[allow(clippy::type_complexity)]
216    fn verify(
217        &self,
218        // For each commitment:
219        commitments_with_opening_points: Vec<(
220            // The commitment
221            Self::Commitment,
222            // for each matrix in the commitment:
223            Vec<(
224                // its domain,
225                Self::Domain,
226                // A vector of (point, claimed_evaluation) pairs
227                Vec<(
228                    // the point the matrix was opened at,
229                    Challenge,
230                    // the claimed evaluations at that point
231                    Vec<Challenge>,
232                )>,
233            )>,
234        )>,
235        // The opening proof for all claimed evaluations.
236        proof: &Self::Proof,
237        fiat_shamir_challenger: &mut Challenger,
238    ) -> Result<(), Self::Error>;
239
240    fn get_opt_randomization_poly_commitment(
241        &self,
242        _domains: impl IntoIterator<Item = Self::Domain>,
243    ) -> Option<(Self::Commitment, Self::ProverData)> {
244        None
245    }
246}
247
248pub type OpenedValues<F> = Vec<OpenedValuesForRound<F>>;
249pub type OpenedValuesForRound<F> = Vec<OpenedValuesForMatrix<F>>;
250pub type OpenedValuesForMatrix<F> = Vec<OpenedValuesForPoint<F>>;
251pub type OpenedValuesForPoint<F> = Vec<F>;
252
253#[cfg(test)]
254mod tests {
255    use alloc::vec;
256    use core::marker::PhantomData;
257
258    use lib_q_stark_baby_bear::BabyBear;
259    use lib_q_stark_challenger::{
260        CanSample,
261        Shake128Challenger32,
262    };
263    use lib_q_stark_dft::{
264        NaiveDft,
265        TwoAdicSubgroupDft,
266    };
267    use lib_q_stark_field::coset::TwoAdicMultiplicativeCoset;
268    use lib_q_stark_field::{
269        PrimeCharacteristicRing,
270        TwoAdicField,
271    };
272    use lib_q_stark_shake128::Shake128Hash;
273
274    use super::*;
275    use crate::testing::{
276        TrivialPcs,
277        eval_coeffs_at_pt,
278    };
279
280    type F = BabyBear;
281    type Challenge = F;
282    type Challenger = Shake128Challenger32<F>;
283
284    fn pcs(log_n: usize) -> TrivialPcs<F, NaiveDft> {
285        TrivialPcs {
286            dft: NaiveDft,
287            log_n,
288            _phantom: PhantomData,
289        }
290    }
291
292    fn challenger() -> Challenger {
293        Challenger::from_hasher(Vec::new(), Shake128Hash)
294    }
295
296    /// Coefficients (one polynomial per column, low-degree-first down each column) for two
297    /// distinct small polynomials over an 8-element domain.
298    fn coeffs() -> RowMajorMatrix<F> {
299        RowMajorMatrix::new(
300            [1u32, 10, 2, 0, 3, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0]
301                .into_iter()
302                .map(F::new)
303                .collect(),
304            2,
305        )
306    }
307
308    /// `TrivialPcs::commit` is documented as "only commit on larger domain than natural": it takes
309    /// evaluations over a domain and recovers coefficients via an inverse DFT weighted by the
310    /// domain's shift. On the *natural* domain (shift = `ONE`, so the weighting is a no-op), this
311    /// must exactly invert the forward DFT used to build the evaluations in the first place.
312    #[test]
313    fn commit_recovers_the_original_coefficients_on_the_natural_domain() {
314        let p = pcs(3);
315        let domain =
316            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
317                &p, 8,
318            );
319        let original = coeffs();
320        let evals = NaiveDft.dft_batch(original.clone());
321
322        let (_commitment, prover_data) =
323            Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals)]);
324        assert_eq!(prover_data[0], original);
325    }
326
327    /// End-to-end `commit` -> `open` -> `verify` round trip: the value produced by `open` for an
328    /// arbitrary (off-domain) evaluation point must be the true evaluation of the committed
329    /// polynomial there, and `verify` must accept that (correct) claim.
330    #[test]
331    fn open_and_verify_accept_an_honest_evaluation_claim() {
332        let p = pcs(3);
333        let domain =
334            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
335                &p, 8,
336            );
337        let original = coeffs();
338        let evals = NaiveDft.dft_batch(original.clone());
339        let (commitment, prover_data) = Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals)]);
340
341        let z = F::new(12345);
342        let expected = eval_coeffs_at_pt(&original, z);
343
344        let (opened, proof) = Pcs::<Challenge, Challenger>::open(
345            &p,
346            vec![(&prover_data, vec![vec![z]])],
347            &mut challenger(),
348        );
349        assert_eq!(opened[0][0][0], expected);
350
351        Pcs::<Challenge, Challenger>::verify(
352            &p,
353            vec![(
354                commitment,
355                vec![(domain, vec![(z, opened[0][0][0].clone())])],
356            )],
357            &proof,
358            &mut challenger(),
359        )
360        .expect("verify must accept the value `open` itself produced for an honest commitment");
361    }
362
363    /// Negative control for the round trip above: assert a claimed evaluation known to be wrong
364    /// and confirm `verify` does NOT accept it. `TrivialPcs` is a testing-only PCS that signals a
365    /// bad claim by panicking (`assert_eq!` internally, documented on `Pcs::verify`'s impl), so the
366    /// failure mode here is a panic rather than an `Err`.
367    #[test]
368    #[should_panic]
369    fn verify_rejects_a_tampered_evaluation_claim() {
370        let p = pcs(3);
371        let domain =
372            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
373                &p, 8,
374            );
375        let original = coeffs();
376        let evals = NaiveDft.dft_batch(original.clone());
377        let (commitment, prover_data) = Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals)]);
378
379        let z = F::new(12345);
380        let (_opened, proof) = Pcs::<Challenge, Challenger>::open(
381            &p,
382            vec![(&prover_data, vec![vec![z]])],
383            &mut challenger(),
384        );
385
386        let tampered_value = eval_coeffs_at_pt(&original, z)
387            .into_iter()
388            .map(|v| v + F::ONE)
389            .collect();
390
391        Pcs::<Challenge, Challenger>::verify(
392            &p,
393            vec![(commitment, vec![(domain, vec![(z, tampered_value)])])],
394            &proof,
395            &mut challenger(),
396        )
397        .ok();
398    }
399
400    // ---- Coverage of `Pcs`'s DEFAULT method bodies ----
401    //
402    // `TrivialPcs` (the only `Pcs` impl in this crate) overrides every required method plus
403    // `commit_quotient`, but deliberately leaves `try_natural_domain_for_degree`,
404    // `commit_preprocessing`, `open_with_preprocessing` and `get_opt_randomization_poly_commitment`
405    // on the trait's own default bodies (see `testing.rs`'s `impl Pcs for TrivialPcs`: those four
406    // methods are simply absent). So calling them through `TrivialPcs` exercises the DEFAULT body
407    // defined right here in `pcs.rs`, not an override elsewhere -- closing exactly the gap the
408    // 0/20-covered `pcs.rs` report pointed at.
409
410    /// `try_natural_domain_for_degree`'s default just wraps `natural_domain_for_degree` in `Some`.
411    #[test]
412    fn try_natural_domain_for_degree_default_wraps_the_infallible_version() {
413        let p = pcs(3);
414        let degree = 8;
415        let direct =
416            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
417                &p, degree,
418            );
419        let via_default = Pcs::<Challenge, Challenger>::try_natural_domain_for_degree(&p, degree);
420        // `TwoAdicMultiplicativeCoset` does not implement `PartialEq`; compare the two
421        // domain-identifying fields instead (shift and size uniquely determine a coset).
422        let via_default = via_default.expect("default must wrap in `Some`");
423        assert_eq!(via_default.shift(), direct.shift());
424        assert_eq!(via_default.log_size(), direct.log_size());
425    }
426
427    /// `commit_preprocessing`'s default is a pure passthrough to `commit`: same input must produce
428    /// the identical (commitment, prover_data) pair.
429    #[test]
430    fn commit_preprocessing_default_delegates_to_commit() {
431        let p = pcs(3);
432        let domain =
433            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
434                &p, 8,
435            );
436        let evals = NaiveDft.dft_batch(coeffs());
437
438        let (direct_commitment, direct_prover_data) =
439            Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals.clone())]);
440        let (default_commitment, default_prover_data) =
441            Pcs::<Challenge, Challenger>::commit_preprocessing(&p, [(domain, evals)]);
442
443        assert_eq!(default_commitment, direct_commitment);
444        assert_eq!(default_prover_data, direct_prover_data);
445    }
446
447    /// Negative control for the delegation test above: prove the two calls are not being compared
448    /// via some vacuously-equal placeholder by feeding `commit` a genuinely different polynomial
449    /// and confirming the two commitments then differ.
450    #[test]
451    fn commit_preprocessing_negative_control_distinct_input_gives_distinct_commitment() {
452        let p = pcs(3);
453        let domain =
454            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
455                &p, 8,
456            );
457        let evals_a = NaiveDft.dft_batch(coeffs());
458        let mut other = coeffs();
459        // Perturb one coefficient so the two polynomials are genuinely different.
460        other.values[0] += F::ONE;
461        let evals_b = NaiveDft.dft_batch(other);
462
463        let (commitment_a, _) =
464            Pcs::<Challenge, Challenger>::commit_preprocessing(&p, [(domain, evals_a)]);
465        let (commitment_b, _) =
466            Pcs::<Challenge, Challenger>::commit_preprocessing(&p, [(domain, evals_b)]);
467        assert_ne!(commitment_a, commitment_b);
468    }
469
470    /// `open_with_preprocessing`'s default ignores the `is_preprocessing` flag entirely and
471    /// delegates straight to `open`; check both flag values produce `open`'s own result.
472    #[test]
473    fn open_with_preprocessing_default_delegates_to_open_regardless_of_flag() {
474        let p = pcs(3);
475        let domain =
476            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
477                &p, 8,
478            );
479        let evals = NaiveDft.dft_batch(coeffs());
480        let (_commitment, prover_data) =
481            Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals)]);
482        let z = F::new(999);
483
484        let (direct_opened, _) = Pcs::<Challenge, Challenger>::open(
485            &p,
486            vec![(&prover_data, vec![vec![z]])],
487            &mut challenger(),
488        );
489        for flag in [false, true] {
490            let (via_default, _) = Pcs::<Challenge, Challenger>::open_with_preprocessing(
491                &p,
492                vec![(&prover_data, vec![vec![z]])],
493                &mut challenger(),
494                flag,
495            );
496            assert_eq!(via_default, direct_opened);
497        }
498    }
499
500    /// `get_evaluations_on_domain_no_random`'s default is a pure passthrough to
501    /// `get_evaluations_on_domain`: same inputs must produce identical evaluations.
502    #[test]
503    fn get_evaluations_on_domain_no_random_default_delegates() {
504        let p = pcs(3);
505        let domain =
506            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
507                &p, 8,
508            );
509        let evals = NaiveDft.dft_batch(coeffs());
510        let (_commitment, prover_data) =
511            Pcs::<Challenge, Challenger>::commit(&p, [(domain, evals)]);
512
513        let direct =
514            Pcs::<Challenge, Challenger>::get_evaluations_on_domain(&p, &prover_data, 0, domain);
515        let via_default = Pcs::<Challenge, Challenger>::get_evaluations_on_domain_no_random(
516            &p,
517            &prover_data,
518            0,
519            domain,
520        );
521        assert_eq!(via_default, direct);
522    }
523
524    /// `get_opt_randomization_poly_commitment`'s default always returns `None`; nothing about a
525    /// non-hiding PCS like `TrivialPcs` should make it produce a randomization commitment.
526    #[test]
527    fn get_opt_randomization_poly_commitment_default_is_none() {
528        let p = pcs(3);
529        let domain =
530            <TrivialPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
531                &p, 8,
532            );
533        assert!(
534            Pcs::<Challenge, Challenger>::get_opt_randomization_poly_commitment(&p, [domain])
535                .is_none()
536        );
537    }
538
539    /// Minimal wrapper around `TrivialPcs` that supplies real (if non-LDE) bodies for
540    /// `get_quotient_ldes`/`commit_ldes` -- `TrivialPcs`'s own versions are `unimplemented!()` --
541    /// and, crucially, does NOT re-override `commit_quotient`. That leaves `commit_quotient` on
542    /// the trait's own default body in `pcs.rs`, so calling it here runs that default end-to-end
543    /// instead of `TrivialPcs`'s override (see `testing.rs`, which DOES override `commit_quotient`).
544    struct QuotientDefaultPcs<Val: TwoAdicField, Dft: TwoAdicSubgroupDft<Val>>(
545        TrivialPcs<Val, Dft>,
546    );
547
548    impl<Val, Dft, Challenge, Challenger> Pcs<Challenge, Challenger> for QuotientDefaultPcs<Val, Dft>
549    where
550        Val: TwoAdicField,
551        Challenge: ExtensionField<Val>,
552        Challenger: CanSample<Challenge>,
553        Dft: TwoAdicSubgroupDft<Val>,
554        Vec<Vec<Val>>: Serialize + DeserializeOwned,
555    {
556        type Domain = TwoAdicMultiplicativeCoset<Val>;
557        type Commitment = Vec<Vec<Val>>;
558        type ProverData = Vec<RowMajorMatrix<Val>>;
559        type EvaluationsOnDomain<'a> = Dft::Evaluations;
560        type Proof = ();
561        type Error = ();
562        const ZK: bool = false;
563
564        fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain {
565            Pcs::<Challenge, Challenger>::natural_domain_for_degree(&self.0, degree)
566        }
567
568        fn commit(
569            &self,
570            evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
571        ) -> (Self::Commitment, Self::ProverData) {
572            Pcs::<Challenge, Challenger>::commit(&self.0, evaluations)
573        }
574
575        // Deliberately NOT overriding `commit_quotient`: that is the point of this type.
576
577        fn get_quotient_ldes(
578            &self,
579            evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val>)>,
580            _num_chunks: usize,
581        ) -> Vec<RowMajorMatrix<Val>> {
582            // Identity passthrough: this type exists only to observe `commit_quotient`'s default
583            // plumbing, not to compute an actual low-degree extension.
584            evaluations
585                .into_iter()
586                .map(|(_domain, evals)| evals)
587                .collect()
588        }
589
590        fn commit_ldes(
591            &self,
592            ldes: Vec<RowMajorMatrix<Val>>,
593        ) -> (Self::Commitment, Self::ProverData) {
594            (ldes.iter().map(|m| m.values.clone()).collect(), ldes)
595        }
596
597        fn get_evaluations_on_domain<'a>(
598            &self,
599            prover_data: &'a Self::ProverData,
600            idx: usize,
601            domain: Self::Domain,
602        ) -> Self::EvaluationsOnDomain<'a> {
603            Pcs::<Challenge, Challenger>::get_evaluations_on_domain(
604                &self.0,
605                prover_data,
606                idx,
607                domain,
608            )
609        }
610
611        fn open(
612            &self,
613            rounds: Vec<(&Self::ProverData, Vec<Vec<Challenge>>)>,
614            challenger: &mut Challenger,
615        ) -> (OpenedValues<Challenge>, Self::Proof) {
616            Pcs::<Challenge, Challenger>::open(&self.0, rounds, challenger)
617        }
618
619        #[allow(clippy::type_complexity)]
620        fn verify(
621            &self,
622            rounds: Vec<(
623                Self::Commitment,
624                Vec<(Self::Domain, Vec<(Challenge, Vec<Challenge>)>)>,
625            )>,
626            proof: &Self::Proof,
627            challenger: &mut Challenger,
628        ) -> Result<(), Self::Error> {
629            Pcs::<Challenge, Challenger>::verify(&self.0, rounds, proof, challenger)
630        }
631    }
632
633    /// `commit_quotient`'s default: split the quotient domain/evaluations into `num_chunks`
634    /// pieces, hand them to `get_quotient_ldes`, then commit the result via `commit_ldes`. Check
635    /// the whole default end-to-end against an independently-assembled expectation built from the
636    /// same `PolynomialSpace::split_evals` call the default itself must be using.
637    #[test]
638    fn commit_quotient_default_splits_then_delegates_to_get_quotient_ldes_and_commit_ldes() {
639        let p = QuotientDefaultPcs(pcs(2));
640        let quotient_domain =
641            <QuotientDefaultPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
642                &p, 8,
643            );
644        let evals = NaiveDft.dft_batch(coeffs());
645        let num_chunks = 2;
646
647        let (commitment, prover_data) = Pcs::<Challenge, Challenger>::commit_quotient(
648            &p,
649            quotient_domain,
650            evals.clone(),
651            num_chunks,
652        );
653
654        // Independently reconstruct what the default *should* produce: `get_quotient_ldes` here is
655        // an identity passthrough of the split evaluation chunks, and `commit_ldes` copies each
656        // chunk's raw values as the commitment -- so the expected prover data is exactly
657        // `split_evals`'s own output, in order.
658        let expected_ldes = PolynomialSpace::split_evals(&quotient_domain, num_chunks, evals);
659        let expected_commitment: Vec<Vec<F>> =
660            expected_ldes.iter().map(|m| m.values.clone()).collect();
661
662        assert_eq!(prover_data, expected_ldes);
663        assert_eq!(commitment, expected_commitment);
664    }
665
666    /// Negative control for the `commit_quotient` default test: corrupting `num_chunks` (splitting
667    /// into a different number of pieces than the default actually used) must produce a different
668    /// prover-data shape/content, proving the equality check above is not vacuous.
669    #[test]
670    fn commit_quotient_negative_control_wrong_num_chunks_disagrees() {
671        let p = QuotientDefaultPcs(pcs(2));
672        let quotient_domain =
673            <QuotientDefaultPcs<F, NaiveDft> as Pcs<Challenge, Challenger>>::natural_domain_for_degree(
674                &p, 8,
675            );
676        let evals = NaiveDft.dft_batch(coeffs());
677
678        let (_commitment, prover_data) =
679            Pcs::<Challenge, Challenger>::commit_quotient(&p, quotient_domain, evals.clone(), 2);
680        // Split into 4 chunks instead of the 2 actually used above: different shape entirely.
681        let wrong_split = PolynomialSpace::split_evals(&quotient_domain, 4, evals);
682        assert_ne!(prover_data.len(), wrong_split.len());
683    }
684}