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