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 = (&quotient_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}