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>;