Skip to main content

openvm_stark_backend/prover/logup_zerocheck/
mod.rs

1//! Batch sumcheck for ZeroCheck constraints and sumcheck for LogUp input layer MLEs
2
3use std::{cmp::max, iter::zip};
4
5use itertools::Itertools;
6use p3_dft::TwoAdicSubgroupDft;
7use p3_field::{ExtensionField, Field, PrimeCharacteristicRing, TwoAdicField};
8use p3_matrix::dense::RowMajorMatrix;
9use p3_maybe_rayon::prelude::*;
10use p3_util::log2_strict_usize;
11use tracing::{debug, info_span, instrument};
12
13use crate::{
14    calculate_n_logup,
15    dft::Radix2BowersSerial,
16    poly_common::{eq_uni_poly, UnivariatePoly},
17    proof::{column_openings_by_rot, BatchConstraintProof, GkrProof},
18    prover::{
19        error::LogupZerocheckError,
20        fractional_sumcheck_gkr::{fractional_sumcheck, Frac},
21        poly::eq_sharp_uni_poly,
22        stacked_pcs::StackedLayout,
23        sumcheck::sumcheck_round0_deg,
24        ColMajorMatrix, CpuColMajorBackend, DeviceMultiStarkProvingKey, MatrixDimensions,
25        MatrixView, ProverBackend, ProvingContext,
26    },
27    FiatShamirTranscript, StarkProtocolConfig,
28};
29
30mod cpu;
31mod evaluator;
32pub mod fractional_sumcheck_gkr;
33mod single;
34
35pub use cpu::LogupZerocheckCpu;
36pub use single::*;
37
38#[instrument(level = "info", skip_all)]
39#[allow(clippy::type_complexity)]
40pub fn prove_zerocheck_and_logup<SC: StarkProtocolConfig, TS>(
41    transcript: &mut TS,
42    mpk: &DeviceMultiStarkProvingKey<CpuColMajorBackend<SC>>,
43    ctx: &ProvingContext<CpuColMajorBackend<SC>>,
44) -> Result<(GkrProof<SC>, BatchConstraintProof<SC>, Vec<SC::EF>), LogupZerocheckError>
45where
46    TS: FiatShamirTranscript<SC>,
47    SC::F: TwoAdicField,
48    SC::EF: TwoAdicField + ExtensionField<SC::F>,
49    CpuColMajorBackend<SC>: ProverBackend<Val = SC::F, Matrix = ColMajorMatrix<SC::F>>,
50{
51    let l_skip = mpk.params.l_skip;
52    let constraint_degree = mpk.max_constraint_degree;
53    let num_traces = ctx.per_trace.len();
54
55    // Traces are sorted
56    let n_max = log2_strict_usize(ctx.per_trace[0].1.common_main.height()).saturating_sub(l_skip);
57    // Gather interactions metadata, including interactions stacked layout which depends on trace
58    // heights
59    let mut total_interactions = 0u64;
60    let interactions_meta: Vec<_> = ctx
61        .per_trace
62        .iter()
63        .map(|(air_idx, trace_ctx)| {
64            let pk = &mpk.per_air[*air_idx];
65
66            let num_interactions = pk.vk.symbolic_constraints.interactions.len();
67            let height = trace_ctx.common_main.height();
68            let log_height = log2_strict_usize(height);
69            let log_lifted_height = log_height.max(l_skip);
70            total_interactions += (num_interactions as u64) << log_lifted_height;
71            (num_interactions, log_lifted_height)
72        })
73        .collect();
74    // Implicitly, the width of this stacking should be 1
75    let n_logup = calculate_n_logup(l_skip, total_interactions);
76    debug!(%n_logup);
77    // There's no stride threshold for `interactions_layout` because there's no univariate skip for
78    // GKR
79    let interactions_layout = StackedLayout::new(0, l_skip + n_logup, interactions_meta)?;
80
81    // Grind to increase soundness of random sampling for LogUp
82    let logup_pow_witness = transcript.grind(mpk.params.logup.pow_bits);
83    let alpha_logup = transcript.sample_ext();
84    let beta_logup = transcript.sample_ext();
85    debug!(%alpha_logup, %beta_logup);
86
87    let mut prover = LogupZerocheckCpu::new(
88        mpk,
89        ctx,
90        n_logup,
91        interactions_layout,
92        alpha_logup,
93        beta_logup,
94    )?;
95    // GKR
96    // Compute logup input layer: these are the evaluations of \hat{p}, \hat{q} on the hypercube
97    // `H_{l_skip + n_logup}`
98    let has_interactions = !prover.interactions_layout.sorted_cols.is_empty();
99    let gkr_input_evals = if !has_interactions {
100        vec![]
101    } else {
102        // Per trace, a row major matrix of interaction evaluations
103        // NOTE: these are the evaluations _without_ lifting
104        // PERF[jpw]: we should write directly to the stacked `evals` in memory below
105        let unstacked_interaction_evals = prover
106            .eval_helpers
107            .par_iter()
108            .enumerate()
109            .map(|(trace_idx, helper)| {
110                let trace_ctx = &ctx.per_trace[trace_idx].1;
111                let mats = helper.view_mats(trace_ctx);
112                let height = trace_ctx.common_main.height();
113                (0..height)
114                    .into_par_iter()
115                    .map(|i| {
116                        let mut row_parts = Vec::with_capacity(mats.len() + 1);
117                        let is_first = SC::F::from_bool(i == 0);
118                        let is_transition = SC::F::from_bool(i != height - 1);
119                        let is_last = SC::F::from_bool(i == height - 1);
120                        let sels = vec![is_first, is_transition, is_last];
121                        row_parts.push(sels);
122                        for (mat, is_rot) in &mats {
123                            let offset = usize::from(*is_rot);
124                            row_parts.push(
125                                // SAFETY: %height ensures we never go out of bounds
126                                (0..mat.width())
127                                    .map(|j| unsafe {
128                                        *mat.get_unchecked((i + offset) % height, j)
129                                    })
130                                    .collect_vec(),
131                            );
132                        }
133                        helper.eval_interactions(&row_parts, &prover.beta_pows)
134                    })
135                    .collect::<Vec<_>>()
136            })
137            .collect::<Vec<_>>();
138        let mut evals = vec![Frac::default(); 1 << (l_skip + n_logup)];
139        for (trace_idx, interaction_idx, s) in
140            prover.interactions_layout.sorted_cols.iter().copied()
141        {
142            let pq_evals = &unstacked_interaction_evals[trace_idx];
143            let height = pq_evals.len();
144            debug_assert_eq!(s.col_idx, 0);
145            // the interactions layout has internal striding threshold=0
146            debug_assert_eq!(1 << s.log_height(), s.len(0));
147            debug_assert_eq!(s.len(0) % height, 0);
148            let norm_factor_denom = s.len(0) / height;
149            let norm_factor = SC::F::from_usize(norm_factor_denom).inverse();
150            // We need to fill `evals` with the logup evaluations on the lifted trace, which is
151            // the same as cyclic repeating of the unlifted evaluations
152            evals[s.row_idx..s.row_idx + s.len(0)]
153                .chunks_exact_mut(height)
154                .for_each(|evals| {
155                    evals
156                        .par_iter_mut()
157                        .zip(pq_evals)
158                        .for_each(|(pq_eval, evals_at_z)| {
159                            let (mut numer, denom) = evals_at_z[interaction_idx];
160                            numer *= norm_factor;
161                            *pq_eval = Frac::new(numer.into(), denom);
162                        });
163                });
164        }
165        // Prevent division by zero:
166        evals.par_iter_mut().for_each(|frac| frac.q += alpha_logup);
167        evals
168    };
169
170    let (frac_sum_proof, mut xi) =
171        fractional_sumcheck::<SC, _>(transcript, &gkr_input_evals, true)?;
172
173    // Sample more for `\xi` in the edge case that some AIRs don't have interactions
174    let n_global = max(n_max, n_logup);
175    debug!(%n_global);
176    while xi.len() != l_skip + n_global {
177        xi.push(transcript.sample_ext());
178    }
179    debug!(?xi);
180    prover.xi = xi;
181    // we now have full \xi vector
182
183    // begin batch sumcheck
184    let mut sumcheck_round_polys = Vec::with_capacity(n_max);
185    let mut r = Vec::with_capacity(n_max + 1);
186    // batching randomness
187    let lambda = transcript.sample_ext();
188    debug!(%lambda);
189
190    let sp_0_polys = prover.sumcheck_uni_round0_polys(ctx, lambda)?;
191    let sp_0_deg = sumcheck_round0_deg(l_skip, constraint_degree);
192    let s_deg = constraint_degree + 1;
193    let s_0_deg = sumcheck_round0_deg(l_skip, s_deg);
194    let large_uni_domain = (s_0_deg + 1).next_power_of_two();
195    let dft = Radix2BowersSerial;
196    let s_0_logup_polys = {
197        let eq_sharp_uni = eq_sharp_uni_poly(&prover.xi[..l_skip]);
198        let mut eq_coeffs = eq_sharp_uni.into_coeffs();
199        eq_coeffs.resize(large_uni_domain, SC::EF::ZERO);
200        let eq_evals = dft.dft(eq_coeffs);
201
202        let width = 2 * num_traces;
203        let mut sp_coeffs_mat = SC::EF::zero_vec(width * large_uni_domain);
204        for (i, coeffs) in sp_0_polys[..2 * num_traces].iter().enumerate() {
205            // NOTE: coeffs could have length longer than `sp_0_deg + 1` due to coset evaluation,
206            // but trailing coefficients should be zero.
207            for (j, &c_j) in coeffs.coeffs().iter().enumerate().take(sp_0_deg + 1) {
208                // SAFETY:
209                // - coeffs length is <= sp_0_deg + 1 <= s_0_deg < large_uni_domain
210                // - sp_coeffs_mat allocated for width
211                unsafe {
212                    *sp_coeffs_mat.get_unchecked_mut(j * width + i) = c_j;
213                }
214            }
215        }
216        let mut s_evals = dft.dft_batch(RowMajorMatrix::new(sp_coeffs_mat, width));
217        for (eq, row) in zip(eq_evals, s_evals.values.chunks_mut(width)) {
218            for x in row {
219                *x *= eq;
220            }
221        }
222        dft.idft_batch(s_evals)
223    };
224
225    let skip_domain_size = SC::F::from_usize(1 << l_skip);
226    // logup sum claims (sum_{\hat p}, sum_{\hat q}) per present AIR
227    let (numerator_term_per_air, denominator_term_per_air): (Vec<_>, Vec<_>) = (0..num_traces)
228        .map(|trace_idx| {
229            let [sum_claim_p, sum_claim_q] = [0, 1].map(|is_denom| {
230                // Compute sum over D of s_0(Z) to get the sum claim
231                (0..=s_0_deg)
232                    .step_by(1 << l_skip)
233                    .map(|j| unsafe {
234                        // SAFETY: matrix is 2 * num_trace x large_uni_domain, s_0_deg <
235                        // large_uni_domain
236                        *s_0_logup_polys
237                            .values
238                            .get_unchecked(j * 2 * num_traces + 2 * trace_idx + is_denom)
239                    })
240                    .sum::<SC::EF>()
241                    * skip_domain_size
242            });
243            transcript.observe_ext(sum_claim_p);
244            transcript.observe_ext(sum_claim_q);
245
246            (sum_claim_p, sum_claim_q)
247        })
248        .unzip();
249
250    let mu = transcript.sample_ext();
251    debug!(%mu);
252    let mu_pows = mu.powers().take(3 * num_traces).collect_vec();
253
254    let s_0_zc_poly = {
255        let eq_uni = eq_uni_poly::<SC::F, _>(l_skip, prover.xi[0]);
256        let mut eq_coeffs = eq_uni.into_coeffs();
257        eq_coeffs.resize(large_uni_domain, SC::EF::ZERO);
258        let eq_evals = dft.dft(eq_coeffs);
259
260        let mut sp_coeffs = SC::EF::zero_vec(large_uni_domain);
261        let mus = &mu_pows[2 * num_traces..];
262        let polys = &sp_0_polys[2 * num_traces..];
263        for (j, batch_coeff) in sp_coeffs.iter_mut().enumerate().take(sp_0_deg + 1) {
264            for (&mu, poly) in zip(mus, polys) {
265                *batch_coeff += mu * *poly.coeffs().get(j).unwrap_or(&SC::EF::ZERO);
266            }
267        }
268        let mut s_evals = dft.dft(sp_coeffs);
269        for (eq, x) in zip(eq_evals, &mut s_evals) {
270            *x *= eq;
271        }
272        dft.idft(s_evals)
273    };
274
275    // Algebraically batch
276    let s_0_poly = UnivariatePoly::new(
277        zip(
278            s_0_logup_polys.values.chunks_exact(2 * num_traces),
279            s_0_zc_poly,
280        )
281        .take(s_0_deg + 1)
282        .map(|(logup_row, batched_zc)| {
283            let coeff = batched_zc
284                + zip(&mu_pows, logup_row)
285                    .map(|(&mu_j, &x)| mu_j * x)
286                    .sum::<SC::EF>();
287            transcript.observe_ext(coeff);
288            coeff
289        })
290        .collect(),
291    );
292
293    let r_0 = transcript.sample_ext();
294    r.push(r_0);
295    debug!(round = 0, r_round = %r_0);
296    prover.prev_s_eval = s_0_poly.eval_at_point(r_0);
297    debug!("s_0(r_0) = {}", prover.prev_s_eval);
298
299    prover.fold_ple_evals(ctx, r_0);
300
301    // Sumcheck rounds:
302    // - each round the prover needs to compute univariate polynomial `s_round`. This poly is linear
303    //   since we are taking MLE of `evals`.
304    // - at end of each round, sample random `r_round` in `EF`
305    //
306    // `s_round` is degree `s_deg` so we evaluate it at `0, ..., =s_deg`. The prover skips
307    // evaluation at `0` because the verifier can infer it from the previous round's
308    // `s_{round-1}(r)` claim. The degree is constraint_degree + 1, where + 1 is from eq term
309    let _mle_rounds_span =
310        info_span!("prover.batch_constraints.mle_rounds", phase = "prover").entered();
311    debug!(%s_deg);
312    for round in 1..=n_max {
313        let sp_round_evals = prover.sumcheck_polys_eval(round, r[round - 1])?;
314        // From s'_T above, we can form s'_head(X) and s'_tail where s'_tail is constant
315        // The desired polynomial s(X) for this round `j` is
316        // s(X) = eq(\vec xi, \vec r_{j-1}) eq(xi_{}, X) s'_head(X) + s'_tail * X
317        //
318        // The head vs tail corresponds to the cutoff in front loaded batching where the coordinates
319        // have been exhausted.
320        //
321        // In fact, we further need to split s'_head into s'_{head,zc} and s'_{head,logup} due to
322        // different eq versus eq_sharp round 0 contributions.
323        let tail_start = prover
324            .n_per_trace
325            .iter()
326            .find_position(|&&n| round as isize > n)
327            .map(|(i, _)| i)
328            .unwrap_or(num_traces);
329        let mut sp_head_zc = vec![SC::EF::ZERO; constraint_degree];
330        let mut sp_head_logup = vec![SC::EF::ZERO; constraint_degree];
331        let mut sp_tail = SC::EF::ZERO;
332        for trace_idx in 0..num_traces {
333            let zc_idx = 2 * num_traces + trace_idx;
334            let numer_idx = 2 * trace_idx;
335            let denom_idx = numer_idx + 1;
336            if trace_idx < tail_start {
337                for i in 0..constraint_degree {
338                    sp_head_zc[i] += mu_pows[zc_idx] * sp_round_evals[zc_idx][i];
339                    sp_head_logup[i] += mu_pows[numer_idx] * sp_round_evals[numer_idx][i]
340                        + mu_pows[denom_idx] * sp_round_evals[denom_idx][i];
341                }
342            } else {
343                sp_tail += mu_pows[zc_idx] * sp_round_evals[zc_idx][0]
344                    + mu_pows[numer_idx] * sp_round_evals[numer_idx][0]
345                    + mu_pows[denom_idx] * sp_round_evals[denom_idx][0];
346            }
347        }
348        // With eq(xi,r) contributions
349        let mut sp_head_evals = vec![SC::EF::ZERO; s_deg];
350        for i in 0..constraint_degree {
351            sp_head_evals[i + 1] = prover.eq_ns[round - 1] * sp_head_zc[i]
352                + prover.eq_sharp_ns[round - 1] * sp_head_logup[i];
353        }
354        // We need to derive s'(0).
355        // We use that s_j(0) + s_j(1) = s_{j-1}(r_{j-1})
356        let xi_cur = prover.xi[l_skip + round - 1];
357        {
358            let eq_xi_0 = SC::EF::ONE - xi_cur;
359            let eq_xi_1 = xi_cur;
360            sp_head_evals[0] =
361                (prover.prev_s_eval - eq_xi_1 * sp_head_evals[1] - sp_tail) * eq_xi_0.inverse();
362        }
363        // s' has degree s_deg - 1
364        let sp_head = UnivariatePoly::lagrange_interpolate(
365            &(0..s_deg).map(SC::F::from_usize).collect_vec(),
366            &sp_head_evals,
367        );
368        // eq(xi, X) = (2 * xi - 1) * X + (1 - xi)
369        // Compute s(X) = eq(xi, X) * s'_head(X) + s'_tail * X (s'_head now contains eq(..,r))
370        // s(X) has degree s_deg
371        let batch_s = {
372            let mut coeffs = sp_head.into_coeffs();
373            coeffs.push(SC::EF::ZERO);
374            let b = SC::EF::ONE - xi_cur;
375            let a = xi_cur - b;
376            for i in (0..s_deg).rev() {
377                coeffs[i + 1] = a * coeffs[i] + b * coeffs[i + 1];
378            }
379            coeffs[0] *= b;
380            coeffs[1] += sp_tail;
381            UnivariatePoly::new(coeffs)
382        };
383        let batch_s_evals = (1..=s_deg)
384            .map(|i| batch_s.eval_at_point(SC::EF::from_usize(i)))
385            .collect_vec();
386        for &eval in &batch_s_evals {
387            transcript.observe_ext(eval);
388        }
389        sumcheck_round_polys.push(batch_s_evals);
390
391        let r_round = transcript.sample_ext();
392        debug!(%round, %r_round);
393        r.push(r_round);
394        prover.prev_s_eval = batch_s.eval_at_point(r_round);
395
396        prover.fold_mle_evals(round, r_round);
397    }
398    drop(_mle_rounds_span);
399    if r.len() != n_max + 1 {
400        return Err(LogupZerocheckError::RLenMismatch {
401            r_len: r.len(),
402            expected: n_max + 1,
403        });
404    }
405
406    let column_openings = prover.into_column_openings()?;
407
408    // Observe common main openings first, and then preprocessed/cached
409    for (helper, openings) in prover.eval_helpers.iter().zip(column_openings.iter()) {
410        for (claim, claim_rot) in column_openings_by_rot(&openings[0], helper.needs_next) {
411            transcript.observe_ext(claim);
412            transcript.observe_ext(claim_rot);
413        }
414    }
415    for (helper, openings) in prover.eval_helpers.iter().zip(column_openings.iter()) {
416        for part in openings.iter().skip(1) {
417            for (claim, claim_rot) in column_openings_by_rot(part, helper.needs_next) {
418                transcript.observe_ext(claim);
419                transcript.observe_ext(claim_rot);
420            }
421        }
422    }
423
424    let batch_constraint_proof = BatchConstraintProof::<SC> {
425        numerator_term_per_air,
426        denominator_term_per_air,
427        univariate_round_coeffs: s_0_poly.into_coeffs(),
428        sumcheck_round_polys,
429        column_openings,
430    };
431    let gkr_proof = GkrProof::<SC> {
432        logup_pow_witness,
433        q0_claim: frac_sum_proof.fractional_sum.1,
434        claims_per_layer: frac_sum_proof.claims_per_layer,
435        sumcheck_polys: frac_sum_proof.sumcheck_polys,
436    };
437    Ok((gkr_proof, batch_constraint_proof, r))
438}