Skip to main content

p3_commit/pcs/
univariate.rs

1//! Traits for univariate polynomial commitment schemes.
2
3use alloc::vec::Vec;
4use core::fmt::Debug;
5
6use p3_field::ExtensionField;
7use p3_matrix::Matrix;
8use p3_matrix::dense::RowMajorMatrix;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use crate::{PeriodicLdeTable, PolynomialSpace};
13
14pub type Val<D> = <D as PolynomialSpace>::Val;
15
16/// Optional fast path for building the periodic LDE table (e.g. via coset LDE).
17/// Implement for PCS backends that can avoid evaluating at every quotient point.
18pub trait BuildPeriodicLdeTableFast {
19    type PeriodicDomain: PolynomialSpace;
20
21    fn maybe_build_periodic_lde_table_fast(
22        &self,
23        _periodic_cols: &[Vec<Val<Self::PeriodicDomain>>],
24        _trace_domain: Self::PeriodicDomain,
25        _quotient_domain: Self::PeriodicDomain,
26    ) -> Option<PeriodicLdeTable<Val<Self::PeriodicDomain>>>
27    where
28        Val<Self::PeriodicDomain>: Clone,
29    {
30        None
31    }
32}
33
34/// A polynomial commitment scheme, for committing to batches of polynomials defined by their evaluations
35/// over some domain.
36///
37/// In general this does not have to be a hiding commitment scheme but it might be for some implementations.
38// TODO: Should we have a super-trait for weakly-binding PCSs, like FRI outside unique decoding radius?
39pub trait Pcs<Challenge, Challenger>:
40    BuildPeriodicLdeTableFast<PeriodicDomain = Self::Domain>
41where
42    Challenge: ExtensionField<Val<Self::Domain>>,
43{
44    /// The class of evaluation domains that this commitment scheme works over.
45    type Domain: PolynomialSpace;
46
47    /// The commitment that's sent to the verifier.
48    type Commitment: Clone + Serialize + DeserializeOwned;
49
50    /// Data that the prover stores for committed polynomials, to help the prover with opening.
51    type ProverData;
52
53    /// Type of the output of `get_evaluations_on_domain`.
54    type EvaluationsOnDomain<'a>: Matrix<Val<Self::Domain>> + 'a;
55
56    /// The opening argument.
57    type Proof: Clone + Serialize + DeserializeOwned;
58
59    /// The type of a proof verification error.
60    type Error: Debug;
61
62    /// Set to true to activate randomization and achieve zero-knowledge.
63    const ZK: bool;
64
65    /// Index of the trace commitment in the computed opened values.
66    const TRACE_IDX: usize = Self::ZK as usize;
67
68    /// Index of the quotient commitments in the computed opened values.
69    const QUOTIENT_IDX: usize = Self::TRACE_IDX + 1;
70
71    /// Index of the preprocessed trace commitment in the computed opened values.
72    const PREPROCESSED_TRACE_IDX: usize = Self::QUOTIENT_IDX + 1; // Note: not always present
73
74    /// This should return a domain such that `Domain::next_point` returns `Some`.
75    fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain;
76
77    /// The base-2 logarithm of the largest evaluation domain this PCS can construct.
78    fn log_max_lde_height(&self) -> usize {
79        usize::BITS as usize - 1
80    }
81
82    /// Given a collection of evaluation matrices, produce a binding commitment to
83    /// the polynomials defined by those evaluations. If `zk` is enabled, the evaluations are
84    /// first randomized as explained in Section 3 of <https://eprint.iacr.org/2024/1037.pdf>.
85    ///
86    /// Returns both the commitment which should be sent to the verifier
87    /// and the prover data which can be used to produce opening proofs.
88    #[allow(clippy::type_complexity)]
89    fn commit(
90        &self,
91        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
92    ) -> (Self::Commitment, Self::ProverData);
93
94    /// Same as `commit` but without randomization. This is used for preprocessed columns
95    /// which do not have to be randomized even when ZK is enabled. Note that the preprocessed columns still
96    /// need to be padded to the extended domain height.
97    ///
98    /// Returns both the commitment which should be sent to the verifier
99    /// and the prover data which can be used to produce opening proofs.
100    fn commit_preprocessing(
101        &self,
102        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
103    ) -> (Self::Commitment, Self::ProverData) {
104        self.commit(evaluations)
105    }
106
107    /// Commit to the quotient polynomial. We first decompose the quotient polynomial into
108    /// `num_chunks` many smaller polynomials each of degree `degree / num_chunks`.
109    /// This can have minor performance benefits, but is not strictly necessary in the non `zk` case.
110    /// When `zk` is enabled, this commitment will additionally include some randomization process
111    /// to hide the inputs.
112    ///
113    /// ### Arguments
114    /// - `quotient_domain` the domain of the quotient polynomial.
115    /// - `quotient_evaluations` the evaluations of the quotient polynomial over the domain. This should be in
116    ///   standard (not bit-reversed) order.
117    /// - `num_chunks` the number of smaller polynomials to decompose the quotient polynomial into.
118    #[allow(clippy::type_complexity)]
119    fn commit_quotient(
120        &self,
121        quotient_domain: Self::Domain,
122        quotient_evaluations: RowMajorMatrix<Val<Self::Domain>>,
123        num_chunks: usize,
124    ) -> (Self::Commitment, Self::ProverData) {
125        // Given the evaluation vector of `Q_i(x)` over a domain, split it into evaluation vectors
126        // of `q_{i0}(x), ...` over subdomains and commit to these `q`'s.
127        // TODO: Currently, split_evals involves copying the data to a new matrix.
128        //       We may be able to avoid this copy making use of bit-reversals.
129        let quotient_sub_evaluations =
130            quotient_domain.split_evals(num_chunks, quotient_evaluations);
131        let quotient_sub_domains = quotient_domain.split_domains(num_chunks);
132
133        let ldes = self.get_quotient_ldes(
134            quotient_sub_domains
135                .into_iter()
136                .zip(quotient_sub_evaluations),
137            num_chunks,
138        );
139        self.commit_ldes(ldes)
140    }
141
142    /// When committing to quotient polynomials in batch-STARK,
143    /// it is simpler to first compute the LDE evaluations before batch-committing to them.
144    ///
145    /// This corresponds to the first step of `commit_quotient`. When `zk` is enabled,
146    /// this will additionally add randomization.
147    fn get_quotient_ldes(
148        &self,
149        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
150        num_chunks: usize,
151    ) -> Vec<RowMajorMatrix<Val<Self::Domain>>>;
152
153    /// Commits to a collection of LDE evaluation matrices.
154    fn commit_ldes(
155        &self,
156        ldes: Vec<RowMajorMatrix<Val<Self::Domain>>>,
157    ) -> (Self::Commitment, Self::ProverData);
158
159    /// Given prover data corresponding to a commitment to a collection of evaluation matrices,
160    /// return the evaluations of those matrices on the given domain.
161    ///
162    /// This is essentially a no-op when called with a `domain` which is a subset of the evaluation domain
163    /// on which the evaluation matrices are defined.
164    fn get_evaluations_on_domain<'a>(
165        &self,
166        prover_data: &'a Self::ProverData,
167        idx: usize,
168        domain: Self::Domain,
169    ) -> Self::EvaluationsOnDomain<'a>;
170
171    /// This is the same as `get_evaluations_on_domain` but without randomization.
172    /// This is used for preprocessed columns which do not have to be randomized even when ZK is enabled.
173    fn get_evaluations_on_domain_no_random<'a>(
174        &self,
175        prover_data: &'a Self::ProverData,
176        idx: usize,
177        domain: Self::Domain,
178    ) -> Self::EvaluationsOnDomain<'a> {
179        self.get_evaluations_on_domain(prover_data, idx, domain)
180    }
181
182    /// Open a collection of polynomial commitments at a set of points. Produce the values at those points along with a proof
183    /// of correctness.
184    ///
185    /// Arguments:
186    /// - `commitment_data_with_opening_points`: A vector whose elements are a pair:
187    ///     - `data`: The prover data corresponding to a multi-matrix commitment.
188    ///     - `opening_points`: A vector containing, for each matrix committed to, a vector of opening points.
189    /// - `fiat_shamir_challenger`: The challenger that will be used to generate the proof.
190    ///
191    /// Unwrapping the arguments further, each `data` contains a vector of the committed matrices (`matrices = Vec<M>`).
192    /// If the length of `matrices` is not equal to the length of `opening_points` the function will error. Otherwise, for
193    /// each index `i`, the matrix `M = matrices[i]` will be opened at the points `opening_points[i]`.
194    ///
195    /// This means that each column of `M` will be interpreted as the evaluation vector of some polynomial
196    /// and we will compute the value of all of those polynomials at `opening_points[i]`.
197    ///
198    /// The domains on which the evaluation vectors are defined is not part of the arguments here
199    /// but should be public information known to both the prover and verifier.
200    fn open(
201        &self,
202        // For each multi-matrix commitment,
203        commitment_data_with_opening_points: Vec<(
204            // The matrices and auxiliary prover data
205            &Self::ProverData,
206            // for each matrix,
207            Vec<
208                // the points to open
209                Vec<Challenge>,
210            >,
211        )>,
212        fiat_shamir_challenger: &mut Challenger,
213    ) -> (OpenedValues<Challenge>, Self::Proof);
214
215    /// Open a collection of polynomial commitments at a set of points, when there is preprocessing data.
216    /// It is the same as `open` when `ZK` is disabled.
217    /// Produce the values at those points along with a proof of correctness.
218    ///
219    /// Arguments:
220    /// - `commitment_data_with_opening_points`: A vector whose elements are a pair:
221    ///     - `data`: The prover data corresponding to a multi-matrix commitment.
222    ///     - `opening_points`: A vector containing, for each matrix committed to, a vector of opening points.
223    /// - `fiat_shamir_challenger`: The challenger that will be used to generate the proof.
224    /// - `is_preprocessing`: If one of the committed matrices corresponds to preprocessed columns, this is the index of that matrix.
225    ///
226    /// Unwrapping the arguments further, each `data` contains a vector of the committed matrices (`matrices = Vec<M>`).
227    /// If the length of `matrices` is not equal to the length of `opening_points` the function will error. Otherwise, for
228    /// each index `i`, the matrix `M = matrices[i]` will be opened at the points `opening_points[i]`.
229    ///
230    /// This means that each column of `M` will be interpreted as the evaluation vector of some polynomial
231    /// and we will compute the value of all of those polynomials at `opening_points[i]`.
232    ///
233    /// The domains on which the evaluation vectors are defined is not part of the arguments here
234    /// but should be public information known to both the prover and verifier.
235    fn open_with_preprocessing(
236        &self,
237        // For each multi-matrix commitment,
238        commitment_data_with_opening_points: Vec<(
239            // The matrices and auxiliary prover data
240            &Self::ProverData,
241            // for each matrix,
242            Vec<
243                // the points to open
244                Vec<Challenge>,
245            >,
246        )>,
247        fiat_shamir_challenger: &mut Challenger,
248        _is_preprocessing: bool,
249    ) -> (OpenedValues<Challenge>, Self::Proof) {
250        debug_assert!(
251            !Self::ZK,
252            "open_with_preprocessing should have a different implementation when ZK is enabled"
253        );
254        self.open(commitment_data_with_opening_points, fiat_shamir_challenger)
255    }
256
257    /// Verify that a collection of opened values is correct.
258    ///
259    /// Arguments:
260    /// - `commitments_with_opening_points`: A vector whose elements are a pair:
261    ///     - `commitment`: A multi matrix commitment.
262    ///     - `opening_points`: A vector containing, for each matrix committed to, a vector of opening points and claimed evaluations.
263    /// - `proof`: A claimed proof of correctness for the opened values.
264    /// - `fiat_shamir_challenger`: The challenger that will be used to generate the proof.
265    #[allow(clippy::type_complexity)]
266    fn verify(
267        &self,
268        // For each commitment:
269        commitments_with_opening_points: Vec<(
270            // The commitment
271            Self::Commitment,
272            // for each matrix in the commitment:
273            Vec<(
274                // its domain,
275                Self::Domain,
276                // A vector of (point, claimed_evaluation) pairs
277                Vec<(
278                    // the point the matrix was opened at,
279                    Challenge,
280                    // the claimed evaluations at that point
281                    Vec<Challenge>,
282                )>,
283            )>,
284        )>,
285        // The opening proof for all claimed evaluations.
286        proof: &Self::Proof,
287        fiat_shamir_challenger: &mut Challenger,
288    ) -> Result<(), Self::Error>;
289
290    fn get_opt_randomization_poly_commitment(
291        &self,
292        _domain: impl IntoIterator<Item = Self::Domain>,
293    ) -> Option<(Self::Commitment, Self::ProverData)> {
294        None
295    }
296
297    /// Build the compact periodic LDE table (height = max_period × blowup, width = num periodic columns).
298    ///
299    /// Default: try fast path (e.g. coset LDE), else evaluate each column at the first `extended_height` quotient points.
300    fn build_periodic_lde_table(
301        &self,
302        periodic_cols: &[Vec<Val<Self::Domain>>],
303        trace_domain: Self::Domain,
304        quotient_domain: Self::Domain,
305    ) -> PeriodicLdeTable<Val<Self::Domain>>
306    where
307        Self::Domain: Clone,
308        Val<Self::Domain>: Clone,
309    {
310        if let Some(table) =
311            self.maybe_build_periodic_lde_table_fast(periodic_cols, trace_domain, quotient_domain)
312        {
313            return table;
314        }
315        if periodic_cols.is_empty() {
316            return PeriodicLdeTable::empty();
317        }
318        let trace_size = trace_domain.size();
319        let quotient_size = quotient_domain.size();
320        assert!(
321            quotient_size >= trace_size,
322            "quotient domain size ({quotient_size}) must be >= trace domain size ({trace_size})",
323        );
324        assert!(
325            quotient_size.is_multiple_of(trace_size),
326            "quotient domain size ({quotient_size}) must be divisible by trace domain size ({trace_size})",
327        );
328        let blowup = quotient_size / trace_size;
329
330        for col in periodic_cols {
331            let period = col.len();
332            assert!(
333                period > 0 && period.is_power_of_two(),
334                "periodic column length must be a non-zero power of 2, got {period}",
335            );
336            assert!(
337                trace_size.is_multiple_of(period),
338                "trace domain size ({trace_size}) must be divisible by periodic column length ({period})",
339            );
340        }
341        let max_period = periodic_cols.iter().map(|c| c.len()).max().unwrap();
342        let extended_height = max_period
343            .checked_mul(blowup)
344            .expect("extended height overflow when computing max_period * blowup");
345        // Implied by the column checks above.
346        // Each period divides the trace size, so max_period <= trace_size.
347        // Therefore extended_height = max_period * blowup <= trace_size * blowup = quotient_size.
348        debug_assert!(extended_height <= quotient_size);
349        let num_cols = periodic_cols.len();
350        let row_major_capacity = extended_height
351            .checked_mul(num_cols)
352            .expect("row-major periodic table capacity overflow");
353
354        let mut quotient_pts = Vec::with_capacity(extended_height);
355        let mut pt = quotient_domain.first_point();
356        for _ in 0..extended_height {
357            quotient_pts.push(pt);
358            pt = quotient_domain
359                .next_point(pt)
360                .expect("quotient domain must support next_point");
361        }
362
363        let padded_cols: Vec<Vec<Val<Self::Domain>>> = periodic_cols
364            .iter()
365            .map(|col| (0..max_period).map(|i| col[i % col.len()]).collect())
366            .collect();
367
368        let mut row_major = Vec::with_capacity(row_major_capacity);
369        for point in quotient_pts.iter().take(extended_height) {
370            for padded in &padded_cols {
371                row_major.push(trace_domain.evaluate_periodic_column_at(padded, *point));
372            }
373        }
374        PeriodicLdeTable::new(RowMajorMatrix::new(row_major, num_cols))
375    }
376}
377
378pub type OpenedValues<F> = Vec<OpenedValuesForRound<F>>;
379pub type OpenedValuesForRound<F> = Vec<OpenedValuesForMatrix<F>>;
380pub type OpenedValuesForMatrix<F> = Vec<OpenedValuesForPoint<F>>;
381pub type OpenedValuesForPoint<F> = Vec<F>;