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::{PeriodicColumns, PeriodicLdeTable, 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    /// The opening argument.
35    type Proof: Clone + Serialize + DeserializeOwned;
36
37    /// The type of a proof verification error.
38    type Error: Debug;
39
40    /// Configuration or budget failure during commitment or opening.
41    type ProverError: Debug;
42
43    /// This should return a domain such that `Domain::next_point` returns `Some`.
44    fn natural_domain_for_degree(&self, degree: usize) -> Self::Domain;
45
46    /// Given a collection of evaluation matrices, produce a binding commitment to
47    /// the polynomials defined by those evaluations. Hiding implementations may randomize
48    /// their encoding before committing.
49    ///
50    /// Returns both the commitment which should be sent to the verifier
51    /// and the prover data which can be used to produce opening proofs.
52    /// Configuration and budget failures are returned before consuming private randomness.
53    #[allow(clippy::type_complexity)]
54    fn commit(
55        &self,
56        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
57    ) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError>;
58
59    /// Open each requested commitment, matrix and point in caller order.
60    ///
61    /// Each request must supply one point vector per committed matrix. Columns are
62    /// interpreted as polynomials evaluated over the domain supplied to [`Self::commit`].
63    /// The returned values retain request, matrix, point and column order.
64    ///
65    /// Configuration and budget rejection leaves the challenger, private randomness,
66    /// and any single-use opening state unchanged. This does not undo earlier successful calls.
67    fn open(
68        &self,
69        // For each multi-matrix commitment,
70        commitment_data_with_opening_points: Vec<OpeningRequest<'_, Self::ProverData, Challenge>>,
71        fiat_shamir_challenger: &mut Challenger,
72    ) -> Result<(OpenedValues<Challenge>, Self::Proof), Self::ProverError>;
73
74    /// Verify the claimed column evaluations for each commitment, matrix and point.
75    ///
76    /// Claims supply the original evaluation domains and must retain the ordering used
77    /// to construct the opening proof. The proof and transcript formats are backend-specific.
78    fn verify(
79        &self,
80        // For each commitment:
81        commitments_with_opening_points: Vec<
82            CommitmentOpening<Challenge, Self::Commitment, Self::Domain>,
83        >,
84        // The opening proof for all claimed evaluations.
85        proof: &Self::Proof,
86        fiat_shamir_challenger: &mut Challenger,
87    ) -> Result<(), Self::Error>;
88}
89
90/// Capabilities used by univariate STARK provers and verifiers.
91///
92/// Generic commitment clients only need [`Pcs`]. Evaluation views remain backend-specific
93/// through the GAT, so implementations can borrow committed LDEs without copying.
94pub trait UnivariateStarkPcs<Challenge, Challenger>: Pcs<Challenge, Challenger>
95where
96    Challenge: ExtensionField<Val<Self::Domain>>,
97{
98    /// Type of the output of `get_evaluations_on_domain`.
99    type EvaluationsOnDomain<'a>: Matrix<Val<Self::Domain>> + 'a;
100
101    /// Whether to activate the STARK's randomized layout and masking protocol.
102    ///
103    /// Hiding implementations must enforce their trace-size and opening budgets.
104    /// The flag alone does not certify caller-supplied commitments, randomness,
105    /// or an arbitrary use of the underlying opening protocol.
106    const ZK: bool;
107
108    /// The base-2 logarithm of the largest trace domain a proof may claim.
109    ///
110    /// - A verifier rejects a proof-supplied height above this bound.
111    /// - The rejection happens before any domain is derived from that height.
112    /// - A backend whose evaluation domain must fit a two-adic subgroup subtracts its blowup.
113    ///
114    /// This is the upper end of the pair.
115    ///
116    /// The smallest claimable height is the lower end.
117    fn log_max_trace_height(&self) -> usize;
118
119    /// The base-2 logarithm of the smallest base trace domain a proof may claim.
120    ///
121    /// The base trace domain carries the selectors and the periodic columns.
122    /// Under zero knowledge it is one bit shorter than the committed domain.
123    ///
124    /// - A verifier builds it from a proof-supplied height before the opening argument runs.
125    /// - The height is therefore rejected here or not at all.
126    /// - A positive bound keeps a malformed proof out of domain arithmetic it would break.
127    /// - Backends defined down to a single row return zero.
128    ///
129    /// There is no default, so a backend with a minimum has to state it.
130    fn log_min_trace_height(&self) -> usize;
131
132    /// Same as `commit` but without randomization. This is used for preprocessed columns
133    /// which do not have to be randomized even when ZK is enabled. Note that the preprocessed columns still
134    /// need to be padded to the extended domain height.
135    ///
136    /// Returns both the commitment which should be sent to the verifier
137    /// and the prover data which can be used to produce opening proofs.
138    fn commit_preprocessing(
139        &self,
140        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
141    ) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError> {
142        self.commit(evaluations)
143    }
144
145    /// Commit to the quotient polynomial. We first decompose the quotient polynomial into
146    /// `num_chunks` many smaller polynomials each of degree `degree / num_chunks`.
147    /// This can have minor performance benefits, but is not strictly necessary in the non `zk` case.
148    /// When `zk` is enabled, this commitment will additionally include some randomization process
149    /// to hide the inputs.
150    ///
151    /// ### Arguments
152    /// - `quotient_domain` the domain of the quotient polynomial.
153    /// - `quotient_evaluations` the evaluations of the quotient polynomial over the domain. This should be in
154    ///   standard (not bit-reversed) order.
155    /// - `num_chunks` the number of smaller polynomials to decompose the quotient polynomial into.
156    #[allow(clippy::type_complexity)]
157    fn commit_quotient(
158        &self,
159        quotient_domain: Self::Domain,
160        quotient_evaluations: RowMajorMatrix<Val<Self::Domain>>,
161        num_chunks: usize,
162    ) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError> {
163        // Given the evaluation vector of `Q_i(x)` over a domain, split it into evaluation vectors
164        // of `q_{i0}(x), ...` over subdomains and commit to these `q`'s.
165        // TODO: Currently, split_evals involves copying the data to a new matrix.
166        //       We may be able to avoid this copy making use of bit-reversals.
167        let quotient_sub_evaluations =
168            quotient_domain.split_evals(num_chunks, quotient_evaluations);
169        let quotient_sub_domains = quotient_domain.split_domains(num_chunks);
170
171        let ldes = self.get_quotient_ldes(
172            quotient_sub_domains
173                .into_iter()
174                .zip(quotient_sub_evaluations),
175            num_chunks,
176        )?;
177        self.commit_ldes(ldes)
178    }
179
180    /// When committing to quotient polynomials in batch-STARK,
181    /// it is simpler to first compute the LDE evaluations before batch-committing to them.
182    ///
183    /// This corresponds to the first step of `commit_quotient`. When `zk` is enabled,
184    /// this will additionally add randomization.
185    #[allow(clippy::type_complexity)]
186    fn get_quotient_ldes(
187        &self,
188        evaluations: impl IntoIterator<Item = (Self::Domain, RowMajorMatrix<Val<Self::Domain>>)>,
189        num_chunks: usize,
190    ) -> Result<Vec<RowMajorMatrix<Val<Self::Domain>>>, Self::ProverError>;
191
192    /// Commits to a collection of LDE evaluation matrices.
193    fn commit_ldes(
194        &self,
195        ldes: Vec<RowMajorMatrix<Val<Self::Domain>>>,
196    ) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError>;
197
198    /// Given prover data corresponding to a commitment to a collection of evaluation matrices,
199    /// return the evaluations of those matrices on the given domain.
200    ///
201    /// This is essentially a no-op when called with a `domain` which is a subset of the evaluation domain
202    /// on which the evaluation matrices are defined.
203    fn get_evaluations_on_domain<'a>(
204        &self,
205        prover_data: &'a Self::ProverData,
206        idx: usize,
207        domain: Self::Domain,
208    ) -> Self::EvaluationsOnDomain<'a>;
209
210    /// This is the same as `get_evaluations_on_domain` but without randomization.
211    /// This is used for preprocessed columns which do not have to be randomized even when ZK is enabled.
212    fn get_evaluations_on_domain_no_random<'a>(
213        &self,
214        prover_data: &'a Self::ProverData,
215        idx: usize,
216        domain: Self::Domain,
217    ) -> Self::EvaluationsOnDomain<'a> {
218        self.get_evaluations_on_domain(prover_data, idx, domain)
219    }
220
221    /// Open commitments with an optional commitment to unrandomized preprocessing.
222    ///
223    /// `preprocessed_commitment` identifies a request in the batch, not a matrix within
224    /// a commitment. Hiding implementations omit random codewords for that request.
225    /// The caller owns the commitment ordering; PCS implementations impose no STARK layout.
226    /// Non-hiding implementations behave exactly like [`Pcs::open`].
227    fn open_with_preprocessing(
228        &self,
229        // For each multi-matrix commitment,
230        commitment_data_with_opening_points: Vec<OpeningRequest<'_, Self::ProverData, Challenge>>,
231        fiat_shamir_challenger: &mut Challenger,
232        _preprocessed_commitment: Option<usize>,
233    ) -> Result<(OpenedValues<Challenge>, Self::Proof), Self::ProverError> {
234        assert!(
235            !Self::ZK,
236            "open_with_preprocessing should have a different implementation when ZK is enabled"
237        );
238        self.open(commitment_data_with_opening_points, fiat_shamir_challenger)
239    }
240
241    /// Verify with trusted metadata identifying the unrandomized preprocessing commitment.
242    ///
243    /// The index identifies a commitment request, not a matrix. It must come from the
244    /// verifier's statement or key, never from the proof or its random-opening lengths.
245    /// `None` requires every commitment to use the ordinary PCS opening format.
246    /// Non-hiding implementations behave exactly like [`Pcs::verify`].
247    fn verify_with_preprocessing(
248        &self,
249        rounds: Vec<CommitmentOpening<Challenge, Self::Commitment, Self::Domain>>,
250        proof: &Self::Proof,
251        challenger: &mut Challenger,
252        _preprocessed_commitment: Option<usize>,
253    ) -> Result<(), Self::Error> {
254        self.verify(rounds, proof, challenger)
255    }
256
257    #[allow(clippy::type_complexity)]
258    fn get_opt_randomization_poly_commitment(
259        &self,
260        _domain: impl IntoIterator<Item = Self::Domain>,
261    ) -> Result<Option<(Self::Commitment, Self::ProverData)>, Self::ProverError> {
262        Ok(None)
263    }
264
265    /// Build the compact periodic LDE table (height = max_period × blowup, width = num periodic columns).
266    ///
267    /// Default: evaluate each column at the first `extended_height` quotient points. Backends that
268    /// can compute this faster (e.g. via coset LDE) should override this method.
269    fn build_periodic_lde_table(
270        &self,
271        periodic_cols: &[Vec<Val<Self::Domain>>],
272        trace_domain: Self::Domain,
273        quotient_domain: Self::Domain,
274    ) -> PeriodicLdeTable<Val<Self::Domain>>
275    where
276        Self::Domain: Clone,
277        Val<Self::Domain>: Clone,
278    {
279        let trace_size = trace_domain.size();
280        let quotient_size = quotient_domain.size();
281        assert!(
282            quotient_size >= trace_size,
283            "quotient domain size ({quotient_size}) must be >= trace domain size ({trace_size})",
284        );
285        assert!(
286            quotient_size.is_multiple_of(trace_size),
287            "quotient domain size ({quotient_size}) must be divisible by trace domain size ({trace_size})",
288        );
289        let blowup = quotient_size / trace_size;
290
291        // A malformed declaration is a bug in the AIR the prover was handed, not proof data.
292        let periodic_cols =
293            PeriodicColumns::new(periodic_cols, trace_size).unwrap_or_else(|err| panic!("{err}"));
294
295        // No declared column means no table, and no longest period to pad up to.
296        let Some(max_period) = periodic_cols.max_period() else {
297            return PeriodicLdeTable::empty();
298        };
299
300        let extended_height = max_period
301            .checked_mul(blowup)
302            .expect("extended height overflow when computing max_period * blowup");
303        // Every period divides the trace size, so the longest one is at most the trace size.
304        // Hence max_period * blowup <= trace_size * blowup = quotient_size.
305        debug_assert!(extended_height <= quotient_size);
306        let num_cols = periodic_cols.len();
307        let row_major_capacity = extended_height
308            .checked_mul(num_cols)
309            .expect("row-major periodic table capacity overflow");
310
311        let mut quotient_pts = Vec::with_capacity(extended_height);
312        let mut pt = quotient_domain.first_point();
313        for _ in 0..extended_height {
314            quotient_pts.push(pt);
315            pt = quotient_domain
316                .next_point(pt)
317                .expect("quotient domain must support next_point");
318        }
319
320        let padded_cols: Vec<Vec<Val<Self::Domain>>> = periodic_cols
321            .as_slice()
322            .iter()
323            .map(|col| (0..max_period).map(|i| col[i % col.len()]).collect())
324            .collect();
325
326        let mut row_major = Vec::with_capacity(row_major_capacity);
327        for point in quotient_pts.iter().take(extended_height) {
328            for padded in &padded_cols {
329                row_major.push(trace_domain.evaluate_periodic_column_at(padded, *point));
330            }
331        }
332        PeriodicLdeTable::new(RowMajorMatrix::new(row_major, num_cols))
333    }
334}
335
336/// A joint commitment to a collection of matrices and their opening at
337/// a collection of points.
338///
339/// This is the shape [`Pcs::verify`] checks an opening argument against, so it is also what
340/// any code building that argument produces.
341#[derive(Clone, Debug)]
342pub struct CommitmentOpening<Challenge, Commitment, Domain> {
343    /// Commitment whose matrices are opened, in commitment order.
344    pub commitment: Commitment,
345    /// Claims for each matrix, in the order supplied to `commit`.
346    pub matrices: Vec<MatrixOpening<Challenge, Domain>>,
347}
348
349/// Opening points and claimed column evaluations for one matrix.
350#[derive(Clone, Debug)]
351pub struct MatrixOpening<Challenge, Domain> {
352    pub domain: Domain,
353    pub points: Vec<PointOpening<Challenge>>,
354}
355
356/// Claimed evaluations of every column at one point, in column order.
357#[derive(Clone, Debug)]
358pub struct PointOpening<Challenge> {
359    pub point: Challenge,
360    pub values: Vec<Challenge>,
361}
362
363/// Points to open for each matrix in one commitment.
364///
365/// Requests, matrices and points retain caller order in [`OpenedValues`].
366/// The prover data is borrowed; point vectors are moved without copying.
367#[derive(Debug)]
368pub struct OpeningRequest<'a, ProverData, Challenge> {
369    pub prover_data: &'a ProverData,
370    pub points: Vec<Vec<Challenge>>,
371}
372
373impl<ProverData, Challenge: Clone> Clone for OpeningRequest<'_, ProverData, Challenge> {
374    fn clone(&self) -> Self {
375        Self {
376            prover_data: self.prover_data,
377            points: self.points.clone(),
378        }
379    }
380}
381
382impl<'a, ProverData, Challenge> From<(&'a ProverData, Vec<Vec<Challenge>>)>
383    for OpeningRequest<'a, ProverData, Challenge>
384{
385    fn from((prover_data, points): (&'a ProverData, Vec<Vec<Challenge>>)) -> Self {
386        Self {
387            prover_data,
388            points,
389        }
390    }
391}
392
393impl<Challenge> From<(Challenge, Vec<Challenge>)> for PointOpening<Challenge> {
394    fn from((point, values): (Challenge, Vec<Challenge>)) -> Self {
395        Self { point, values }
396    }
397}
398
399impl<Challenge, Domain> From<(Domain, Vec<(Challenge, Vec<Challenge>)>)>
400    for MatrixOpening<Challenge, Domain>
401{
402    fn from((domain, points): (Domain, Vec<(Challenge, Vec<Challenge>)>)) -> Self {
403        Self {
404            domain,
405            points: points.into_iter().map(Into::into).collect(),
406        }
407    }
408}
409
410impl<Challenge, Commitment, Domain>
411    From<(Commitment, Vec<(Domain, Vec<(Challenge, Vec<Challenge>)>)>)>
412    for CommitmentOpening<Challenge, Commitment, Domain>
413{
414    fn from(
415        (commitment, matrices): (Commitment, Vec<(Domain, Vec<(Challenge, Vec<Challenge>)>)>),
416    ) -> Self {
417        Self {
418            commitment,
419            matrices: matrices.into_iter().map(Into::into).collect(),
420        }
421    }
422}
423
424/// Compatibility name for the named verification claim.
425pub type CommitmentWithOpeningPoints<Challenge, Commitment, Domain> =
426    CommitmentOpening<Challenge, Commitment, Domain>;
427
428pub type OpenedValues<F> = Vec<OpenedValuesForRound<F>>;
429pub type OpenedValuesForRound<F> = Vec<OpenedValuesForMatrix<F>>;
430pub type OpenedValuesForMatrix<F> = Vec<OpenedValuesForPoint<F>>;
431pub type OpenedValuesForPoint<F> = Vec<F>;