p3_uni_stark/prover.rs
1use alloc::vec;
2use alloc::vec::Vec;
3
4use itertools::Itertools;
5use p3_air::Air;
6use p3_challenger::{CanObserve, FieldChallenger};
7use p3_commit::{Pcs, PolynomialSpace};
8use p3_field::{BasedVectorSpace, PackedValue, PrimeCharacteristicRing};
9use p3_matrix::Matrix;
10use p3_matrix::dense::RowMajorMatrix;
11use p3_maybe_rayon::prelude::*;
12use p3_util::{log2_ceil_usize, log2_strict_usize};
13use tracing::{debug_span, info_span, instrument};
14
15use crate::{
16 Commitments, Domain, OpenedValues, PackedChallenge, PackedVal, Proof, ProverConstraintFolder,
17 StarkGenericConfig, SymbolicAirBuilder, SymbolicExpression, Val, get_symbolic_constraints,
18};
19
20#[instrument(skip_all)]
21#[allow(clippy::multiple_bound_locations)] // cfg not supported in where clauses?
22pub fn prove<
23 SC,
24 #[cfg(debug_assertions)] A: for<'a> Air<crate::check_constraints::DebugConstraintBuilder<'a, Val<SC>>>,
25 #[cfg(not(debug_assertions))] A,
26>(
27 config: &SC,
28 air: &A,
29 trace: RowMajorMatrix<Val<SC>>,
30 public_values: &Vec<Val<SC>>,
31) -> Proof<SC>
32where
33 SC: StarkGenericConfig,
34 A: Air<SymbolicAirBuilder<Val<SC>>> + for<'a> Air<ProverConstraintFolder<'a, SC>>,
35{
36 #[cfg(debug_assertions)]
37 crate::check_constraints::check_constraints(air, &trace, public_values);
38
39 // Compute the height `N = 2^n` and `log_2(height)`, `n`, of the trace.
40 let degree = trace.height();
41 let log_degree = log2_strict_usize(degree);
42 let log_ext_degree = log_degree + config.is_zk();
43
44 // Compute the constraint polynomials as vectors of symbolic expressions.
45 let symbolic_constraints = get_symbolic_constraints(air, 0, public_values.len());
46
47 // Count the number of constraints that we have.
48 let constraint_count = symbolic_constraints.len();
49
50 // Each constraint polynomial looks like `C_j(X_1, ..., X_w, Y_1, ..., Y_w, Z_1, ..., Z_j)`.
51 // When evaluated on a given row, the X_i's will be the `i`'th element of the that row, the
52 // Y_i's will be the `i`'th element of the next row and the Z_i's will be evaluations of
53 // selector polynomials on the given row index.
54 //
55 // When we convert to working with polynomials, the `X_i`'s and `Y_i`'s will be replaced by the
56 // degree `N - 1` polynomials `T_i(x)` and `T_i(hx)` respectively. The selector polynomials are
57 // a little more complicated however.
58 //
59 // In our our case, the selector polynomials are `S_1(x) = is_first_row`, `S_2(x) = is_last_row`
60 // and `S_3(x) = is_transition`. Both `S_1(x)` and `S_2(x)` are polynomials of degree `N - 1`
61 // as they must be non zero only at a single location in the initial domain. However, `is_transition`
62 // is a polynomial of degree `1` as it simply need to be `0` on the last row.
63 //
64 // The constraint degree (`deg(C)`) is the linear factor of `N` in the constraint polynomial. In other
65 // words, it is roughly the total degree of `C` however, we treat `Z_3` as a constant term which does
66 // not contribute to the degree.
67 //
68 // E.g. `C_j = Z_1 * (X_1^3 - X_2 * X_3 * X_4)` would have degree `4`.
69 // `C_j = Z_3 * (X_1^3 - X_2 * X_3 * X_4)` would have degree `3`.
70 //
71 // The point of all this is that, defining:
72 // C(x) = C(T_1(x), ..., T_w(x), T_1(hx), ... T_w(hx), S_1(x), S_2(x), S_3(x))
73 // We get the constraint bound:
74 // deg(C(x)) <= deg(C) * (N - 1) + 1
75 // The `+1` is due to the `is_transition` selector which is not accounted for in `deg(C)`. Note
76 // that S_i^2 should never appear in a constraint as it should just be replaced by `S_i`.
77 //
78 // For now in comments we assume that `deg(C) = 3` meaning `deg(C(x)) <= 3N - 2`
79 let constraint_degree = symbolic_constraints
80 .iter()
81 .map(SymbolicExpression::degree_multiple)
82 .max()
83 .unwrap_or(0);
84
85 // From the degree of the constraint polynomial, compute the number
86 // of quotient polynomials we will split Q(x) into. This is chosen to
87 // always be a power of 2.
88 let log_quotient_degree = log2_ceil_usize(constraint_degree - 1 + config.is_zk());
89 let quotient_degree = 1 << (log_quotient_degree + config.is_zk());
90
91 // Initialize the PCS and the Challenger.
92 let pcs = config.pcs();
93 let mut challenger = config.initialise_challenger();
94
95 // Get the subgroup `H` of size `N`. We treat each column `T_i` of
96 // the trace as an evaluation vector of polynomials `T_i(x)` over `H`.
97 // (In the Circle STARK case `H` is instead a standard position twin coset of size `N`)
98 let trace_domain = pcs.natural_domain_for_degree(degree);
99
100 // When ZK is enabled, we need to use an extended domain of size `2N` as we will
101 // add random values to the trace.
102 let ext_trace_domain = pcs.natural_domain_for_degree(degree * (config.is_zk() + 1));
103
104 // Let `g` denote a generator of the multiplicative group of `F` and `H'` the unique
105 // subgroup of `F` of size `N << (pcs.config.log_blowup + config.is_zk())`.
106 // If `zk` is enabled, we double the trace length by adding random values.
107 //
108 // For each trace column `T_i`, we compute the evaluation vector of `T_i(x)` over `H'`. This
109 // new extended trace `ET` is hashed into a Merkle tree with its rows bit-reversed.
110 // trace_commit contains the root of the tree
111 // trace_data contains the entire tree.
112 // - trace_data.leaves is the matrix containing `ET`.
113 let (trace_commit, trace_data) =
114 info_span!("commit to trace data").in_scope(|| pcs.commit([(ext_trace_domain, trace)]));
115
116 // Observe the instance.
117 // degree < 2^255 so we can safely cast log_degree to a u8.
118 challenger.observe(Val::<SC>::from_u8(log_ext_degree as u8));
119 challenger.observe(Val::<SC>::from_u8(log_degree as u8));
120 // TODO: Might be best practice to include other instance data here; see verifier comment.
121
122 // Observe the Merkle root of the trace commitment.
123 challenger.observe(trace_commit.clone());
124
125 // Observe the public input values.
126 challenger.observe_slice(public_values);
127
128 // Get the first Fiat Shamir challenge which will be used to combine all constraint polynomials
129 // into a single polynomial.
130 //
131 // Soundness Error:
132 // If a prover is malicious, we can find a row `i` such that some of the constraints
133 // C_0, ..., C_n are non 0 on this row. The malicious prover "wins" if the random challenge
134 // alpha is such that:
135 // (1): C_0(i) + alpha * C_1(i) + ... + alpha^n * C_n(i) = 0
136 // This is a polynomial of degree n, so it has at most n roots. Thus the probability of this
137 // occurring for a given trace and set of constraints is n/|EF|.
138 //
139 // Currently, we do not observe data about the constraint polynomials directly. In particular
140 // a prover could take a trace and fiddle around with the AIR it claims to satisfy without
141 // changing this sample alpha.
142 //
143 // In particular this means that a malicious prover could create a custom AIR for a given trace
144 // such that equation (1) holds. However, such AIRs would need to be very specific and
145 // so such tampering should be obvious to spot. The verifier needs to check the AIR anyway to
146 // confirm that satisfying it indeed proves what the prover claims. Hence this should not be
147 // a soundness issue.
148 let alpha: SC::Challenge = challenger.sample_algebra_element();
149
150 // A domain large enough to uniquely identify the quotient polynomial.
151 // This domain must be contained in the domain over which `trace_data` is defined.
152 // Explicitly it should be equal to `gK` for some subgroup `K` contained in `H'`.
153 let quotient_domain =
154 ext_trace_domain.create_disjoint_domain(1 << (log_ext_degree + log_quotient_degree));
155
156 // Return a the subset of the extended trace `ET` corresponding to the rows giving evaluations
157 // over the quotient domain.
158 //
159 // This only works if the trace domain is `gH'` and the quotient domain is `gK` for some subgroup `K` contained in `H'`.
160 // TODO: Make this explicit in `get_evaluations_on_domain` or otherwise fix this.
161 let trace_on_quotient_domain = pcs.get_evaluations_on_domain(&trace_data, 0, quotient_domain);
162
163 // Compute the quotient polynomial `Q(x)` by evaluating
164 // `C(T_1(x), ..., T_w(x), T_1(hx), ..., T_w(hx), selectors(x)) / Z_H(x)`
165 // at every point in the quotient domain. The degree of `Q(x)` is `<= deg(C(x)) - N = 2N - 2` in the case
166 // where `deg(C) = 3`. (See the discussion above constraint_degree for more details.)
167 let quotient_values = quotient_values(
168 air,
169 public_values,
170 trace_domain,
171 quotient_domain,
172 trace_on_quotient_domain,
173 alpha,
174 constraint_count,
175 );
176
177 // Due to `alpha`, evaluations of `Q` all lie in the extension field `E`.
178 // We flatten this into a matrix of `F` values by treating `E` as an `F`
179 // vector space and so separating each element of `E` into `e + 1 = [E: F]` elements of `F`.
180 //
181 // This is valid to do because our domain lies in the base field `F`. Hence we can split
182 // `Q(x)` into `e + 1` polynomials `Q_0(x), ... , Q_e(x)` each contained in `F`.
183 // such that `Q(x) = [Q_0(x), ... ,Q_e(x)]` holds for all `x` in `F`.
184 let quotient_flat = RowMajorMatrix::new_col(quotient_values).flatten_to_base();
185
186 // Currently each polynomial `Q_i(x)` is of degree `<= 2(N - 1)` and
187 // we have it's evaluations over a the coset `gK of size `2N`. Let `k` be the chosen
188 // generator of `K` which satisfies `k^2 = h`.
189 //
190 // We can split this coset into the sub-cosets `gH` and `gkH` each of size `N`.
191 // Define: L_g(x) = (x^N - (gk)^N)/(g^N - (gk)^N) = (x^N + g^N)/2g^N
192 // L_{gk}(x) = (x^N - g^N)/(g^N - (gk)^N) = -(x^N - g^N)/2g^N.
193 // Then `L_g` is equal to `1` on `gH` and `0` on `gkH` and `L_{gk}` is equal to `1` on `gkH` and `0` on `gH`.
194 //
195 // Thus we can decompose `Q_i(x) = L_{g}(x)q_{i0}(x) + L_{gk}(x)q_{i1}(x)` (Or an randomized version of this in the zk case)
196 // where `q_{i0}(x)` and `q_{i1}(x)` are polynomials of degree `<= N - 1`.
197 // Moreover the evaluations of `q_{i0}(x), q_{i1}(x)` on `gH` and `gkH` respectively are
198 // exactly the evaluations of `Q_i(x)` on `gH` and `gkH`.
199 // For each polynomial `q_{ij}`, compute the evaluation vector of `q_{ij}(x)` over `gH'`. We bit
200 // reverse the rows and hash the resulting matrix into a merkle tree.
201 // quotient_commit contains the root of the tree
202 // quotient_data contains the entire tree.
203 // - quotient_data.leaves is a pair of matrices containing the `q_i0(x)` and `q_i1(x)`.
204 let (quotient_commit, quotient_data) = info_span!("commit to quotient poly chunks")
205 .in_scope(|| pcs.commit_quotient(quotient_domain, quotient_flat, quotient_degree));
206 challenger.observe(quotient_commit.clone());
207
208 // If zk is enabled, we generate random extension field values of the size of the randomized trace. If `n` is the degree of the initial trace,
209 // then the randomized trace has degree `2n`. To randomize the FRI batch polynomial, we then need an extension field random polynomial of degree `2n -1`.
210 // So we can generate a random polynomial of degree `2n`, and provide it to `open` as is.
211 // Then the method will add `(R(X) - R(z)) / (X - z)` (which is of the desired degree `2n - 1`), to the batch of polynomials.
212 // Since we need a random polynomial defined over the extension field, and the `commit` method is over the base field,
213 // we actually need to commit to `SC::CHallenge::D` base field random polynomials.
214 // This is similar to what is done for the quotient polynomials.
215 // TODO: This approach is only statistically zk. To make it perfectly zk, `R` would have to truly be an extension field polynomial.
216 let (opt_r_commit, opt_r_data) = if SC::Pcs::ZK {
217 let (r_commit, r_data) = pcs
218 .get_opt_randomization_poly_commitment(ext_trace_domain)
219 .expect("ZK is enabled, so we should have randomization commitments");
220 (Some(r_commit), Some(r_data))
221 } else {
222 (None, None)
223 };
224
225 // Combine our commitments to the trace and quotient polynomials into a single object which
226 // will be passed to the verifier.
227 let commitments = Commitments {
228 trace: trace_commit,
229 quotient_chunks: quotient_commit,
230 random: opt_r_commit.clone(),
231 };
232
233 if let Some(r_commit) = opt_r_commit {
234 challenger.observe(r_commit);
235 }
236
237 // Get an out-of-domain point to open our values at.
238 //
239 // Soundness Error:
240 // This sample will be used to check the equality: `C(X) = ZH(X)Q(X)`. If a prover is malicious
241 // and this equality is false, the probability that it is true at the point `zeta` will be
242 // deg(C(X))/|EF| = dN/|EF| where `N` is the trace length and our constraints have degree `d`.
243 //
244 // Completeness Error:
245 // If zeta happens to lie in the domain `gK`, then when opening at zeta we will run into division
246 // by zero errors. This doesn't lead to a soundness issue as the verifier will just reject in those
247 // cases but it is a completeness issue and contributes a completeness error of |gK| = 2N/|EF|.
248 let zeta: SC::Challenge = challenger.sample_algebra_element();
249 let zeta_next = trace_domain.next_point(zeta).unwrap();
250
251 let is_random = opt_r_data.is_some();
252 let (opened_values, opening_proof) = info_span!("open").in_scope(|| {
253 let round0 = opt_r_data.as_ref().map(|r_data| (r_data, vec![vec![zeta]]));
254 let round1 = (&trace_data, vec![vec![zeta, zeta_next]]);
255 let round2 = ("ient_data, vec![vec![zeta]; quotient_degree]); // open every chunk at zeta
256
257 let rounds = round0.into_iter().chain([round1, round2]).collect();
258
259 pcs.open(rounds, &mut challenger)
260 });
261 let trace_idx = SC::Pcs::TRACE_IDX;
262 let quotient_idx = SC::Pcs::QUOTIENT_IDX;
263 let trace_local = opened_values[trace_idx][0][0].clone();
264 let trace_next = opened_values[trace_idx][0][1].clone();
265 let quotient_chunks = opened_values[quotient_idx]
266 .iter()
267 .map(|v| v[0].clone())
268 .collect_vec();
269 let random = if is_random {
270 Some(opened_values[0][0][0].clone())
271 } else {
272 None
273 };
274 let opened_values = OpenedValues {
275 trace_local,
276 trace_next,
277 quotient_chunks,
278 random,
279 };
280 Proof {
281 commitments,
282 opened_values,
283 opening_proof,
284 degree_bits: log_ext_degree,
285 }
286}
287
288#[instrument(name = "compute quotient polynomial", skip_all)]
289// TODO: Group some arguments to remove the `allow`?
290#[allow(clippy::too_many_arguments)]
291fn quotient_values<SC, A, Mat>(
292 air: &A,
293 public_values: &Vec<Val<SC>>,
294 trace_domain: Domain<SC>,
295 quotient_domain: Domain<SC>,
296 trace_on_quotient_domain: Mat,
297 alpha: SC::Challenge,
298 constraint_count: usize,
299) -> Vec<SC::Challenge>
300where
301 SC: StarkGenericConfig,
302 A: for<'a> Air<ProverConstraintFolder<'a, SC>>,
303 Mat: Matrix<Val<SC>> + Sync,
304{
305 let quotient_size = quotient_domain.size();
306 let width = trace_on_quotient_domain.width();
307 let mut sels = debug_span!("Compute Selectors")
308 .in_scope(|| trace_domain.selectors_on_coset(quotient_domain));
309
310 let qdb = log2_strict_usize(quotient_domain.size()) - log2_strict_usize(trace_domain.size());
311 let next_step = 1 << qdb;
312
313 // We take PackedVal::<SC>::WIDTH worth of values at a time from a quotient_size slice, so we need to
314 // pad with default values in the case where quotient_size is smaller than PackedVal::<SC>::WIDTH.
315 for _ in quotient_size..PackedVal::<SC>::WIDTH {
316 sels.is_first_row.push(Val::<SC>::default());
317 sels.is_last_row.push(Val::<SC>::default());
318 sels.is_transition.push(Val::<SC>::default());
319 sels.inv_vanishing.push(Val::<SC>::default());
320 }
321
322 let mut alpha_powers = alpha.powers().take(constraint_count).collect_vec();
323 alpha_powers.reverse();
324 // alpha powers looks like Vec<EF> ~ Vec<[F; D]>
325 // It's useful to also have access to the transpose of this of form [Vec<F>; D].
326 let decomposed_alpha_powers: Vec<_> = (0..SC::Challenge::DIMENSION)
327 .map(|i| {
328 alpha_powers
329 .iter()
330 .map(|x| x.as_basis_coefficients_slice()[i])
331 .collect()
332 })
333 .collect();
334 (0..quotient_size)
335 .into_par_iter()
336 .step_by(PackedVal::<SC>::WIDTH)
337 .flat_map_iter(|i_start| {
338 let i_range = i_start..i_start + PackedVal::<SC>::WIDTH;
339
340 let is_first_row = *PackedVal::<SC>::from_slice(&sels.is_first_row[i_range.clone()]);
341 let is_last_row = *PackedVal::<SC>::from_slice(&sels.is_last_row[i_range.clone()]);
342 let is_transition = *PackedVal::<SC>::from_slice(&sels.is_transition[i_range.clone()]);
343 let inv_vanishing = *PackedVal::<SC>::from_slice(&sels.inv_vanishing[i_range]);
344
345 let main = RowMajorMatrix::new(
346 trace_on_quotient_domain.vertically_packed_row_pair(i_start, next_step),
347 width,
348 );
349
350 let accumulator = PackedChallenge::<SC>::ZERO;
351 let mut folder = ProverConstraintFolder {
352 main: main.as_view(),
353 public_values,
354 is_first_row,
355 is_last_row,
356 is_transition,
357 alpha_powers: &alpha_powers,
358 decomposed_alpha_powers: &decomposed_alpha_powers,
359 accumulator,
360 constraint_index: 0,
361 };
362 air.eval(&mut folder);
363
364 // quotient(x) = constraints(x) / Z_H(x)
365 let quotient = folder.accumulator * inv_vanishing;
366
367 // "Transpose" D packed base coefficients into WIDTH scalar extension coefficients.
368 (0..core::cmp::min(quotient_size, PackedVal::<SC>::WIDTH)).map(move |idx_in_packing| {
369 SC::Challenge::from_basis_coefficients_fn(|coeff_idx| {
370 quotient.as_basis_coefficients_slice()[coeff_idx].as_slice()[idx_in_packing]
371 })
372 })
373 })
374 .collect()
375}