Skip to main content

openvm_stark_backend/prover/
sumcheck.rs

1use std::array::from_fn;
2
3use cfg_if::cfg_if;
4use itertools::Itertools;
5use p3_dft::TwoAdicSubgroupDft;
6use p3_field::{
7    batch_multiplicative_inverse, ExtensionField, Field, PrimeCharacteristicRing, TwoAdicField,
8};
9use p3_interpolation::interpolate_coset_with_precomputation;
10use p3_matrix::dense::RowMajorMatrix;
11use p3_maybe_rayon::prelude::*;
12use p3_util::log2_strict_usize;
13use tracing::{debug, instrument, trace};
14
15use crate::{
16    dft::Radix2BowersSerial,
17    poly_common::UnivariatePoly,
18    prover::{
19        error::SumcheckError, ColMajorMatrix, ColMajorMatrixView, MatrixDimensions, MatrixView,
20        StridedColMajorMatrixView,
21    },
22    FiatShamirTranscript, StarkProtocolConfig,
23};
24
25/// The univariate skip round 0: we want to compute the univariate polynomial `s(Z) = sum_{x \in
26/// H_n} \hat{f}(Z, x)`. For this function, assume that `\hat{f}(\vec z) = \hat\eps(\vec z)
27/// W(\hat{T}_0(\vec z), .., \hat{T}_{m-1}(\vec z))` for a sequence of `\hat{T}_i` where each
28/// `\hat{T}_i` consists of a collection of prismalinear polynomials in `n + 1` variables, with
29/// degree `< 2^{l_skip}` in the first variable.
30///
31/// The `mats` consists of the evaluations of `\hat{T}_i` on the hyperprism `D_n`, where evaluations
32/// of each `\hat{T}_i` are in column-major order.
33/// For round 0, we also provide a boolean `is_rotation` indicating whether the matrix should be
34/// accessed at a cyclic offset of 1 (aka rotation).
35///
36/// `eps` is a single column vector of evaluations on `D_n`, except valued in extension field.
37///
38/// Let `W` be degree `d` in each variable. Then `s` is degree `<= d * (2^{l_skip} - 1)`, so it can
39/// be interpolated using `d * (2^{l_skip} - 1) + 1` points.
40///
41/// This function returns `s` in **coefficient** form.
42///
43/// If `n > 0`, then all `mats` should have the same height equal to `2^{l_skip + n}`.
44/// If `n = 0`, then all `mats` should have height `<= 2^{l_skip}` and they will be univariate
45/// lifted to height `2^l_skip`.
46#[instrument(level = "trace", skip_all)]
47pub fn sumcheck_uni_round0_poly<F, EF, FN, const WD: usize>(
48    l_skip: usize,
49    n: usize,
50    d: usize,
51    mats: &[(StridedColMajorMatrixView<F>, bool)],
52    w: FN,
53) -> [UnivariatePoly<EF>; WD]
54where
55    F: TwoAdicField,
56    EF: ExtensionField<F> + TwoAdicField,
57    FN: Fn(
58            F,         /* Z */
59            usize,     /* x_int */
60            &[Vec<F>], /* mats eval at (Z, bin(x_int)) */
61        ) -> [EF; WD]
62        + Sync,
63{
64    if d == 0 {
65        return from_fn(|_| UnivariatePoly(vec![]));
66    }
67    #[cfg(debug_assertions)]
68    if n > 0 {
69        for (m, _) in mats.iter() {
70            assert_eq!(m.height(), 1 << (l_skip + n));
71        }
72    } else {
73        for (m, _) in mats.iter() {
74            assert!(
75                m.height() <= 1 << l_skip,
76                "mat height {} > 2^{l_skip}",
77                m.height()
78            );
79        }
80    }
81    let g = F::GENERATOR;
82    let omega_skip = F::two_adic_generator(l_skip);
83    // skip 1 to avoid divide by zero in zerocheck
84    let coset_shifts = g.powers().skip(1).take(d).collect_vec();
85
86    // Map-Reduce
87    // Map: for each x in H_n, compute
88    // ```
89    // [W(\hat{T}_0(z, x), ..., \hat{T}_{m-1}(z, x)) for z in `g_i D` for `d` cosets of `D`.
90    // ```
91    // We choose to iterate over x first to avoid multiple memory accesses to `\hat{T}`s
92    let evals = (0..1 << n).into_par_iter().map(|x| {
93        let dft = Radix2BowersSerial;
94        // For fixed `x`, `Z -> \hat{T}_i(Z, x)` is a polynomial of degree `<2^l_skip` and we
95        // have evaluations on the univariate skip domain `D = <ω_skip>` and we want to get
96        // evaluations on the larger domain `L`.
97        //
98        // For now, we apply iDFT on D and then DFT on L.
99        // PERF[jpw]: the most efficient algorithm would be to use Chirp-Z transform on L.
100        let mats_at_zs = mats
101            .iter()
102            .map(|(mat, is_rot)| {
103                let height = mat.height();
104                let offset = usize::from(*is_rot);
105                (0..mat.width())
106                    .map(|col_idx| {
107                        // SAFETY: col_idx < width
108                        // Note that the % height is necessary even when `offset = 0` because we may
109                        // have `height < 2^{l_skip + n}` in the case where we are taking the lifts
110                        // of `mats`
111                        let col_x = ((x << l_skip)..(x + 1) << l_skip)
112                            .map(|i| unsafe { *mat.get_unchecked((i + offset) % height, col_idx) })
113                            .collect_vec();
114                        let coeffs = dft.idft(col_x);
115                        coset_shifts
116                            .iter()
117                            .flat_map(|&shift| dft.coset_dft(coeffs.clone(), shift))
118                            .collect_vec()
119                    })
120                    .collect_vec()
121            })
122            .collect_vec();
123        // Apply W(..) to `{\hat{T}_i(z, x)}` for each z in g_i D
124        omega_skip
125            .powers()
126            .take(1 << l_skip)
127            .enumerate()
128            .flat_map(|(z_idx, z)| {
129                coset_shifts
130                    .iter()
131                    .enumerate()
132                    .map(|(coset_idx, &shift)| {
133                        let z_int = (coset_idx << l_skip) + z_idx;
134                        let row_z_x = mats_at_zs
135                            .iter()
136                            .map(|mat_at_zs| {
137                                mat_at_zs
138                                    .iter()
139                                    .map(|col_at_zs| col_at_zs[z_int])
140                                    .collect_vec()
141                            })
142                            .collect_vec();
143                        w(shift * z, x, &row_z_x)
144                    })
145                    .collect_vec()
146            })
147            .collect_vec()
148    });
149    // Reduce: sum over H_n
150    let hypercube_sum = |mut acc: Vec<[EF; WD]>, x| {
151        for (acc, x) in acc.iter_mut().zip(x) {
152            for (acc_i, x_i) in acc.iter_mut().zip(x) {
153                *acc_i += x_i;
154            }
155        }
156        acc
157    };
158    cfg_if! {
159        if #[cfg(feature = "parallel")] {
160            let evals = evals.reduce(
161                || vec![[EF::ZERO; WD]; d << l_skip],
162                hypercube_sum
163            );
164        } else {
165            let evals = evals.collect_vec();
166            let evals = evals.into_iter().fold(
167                vec![[EF::ZERO; WD]; d << l_skip],
168                hypercube_sum
169            );
170        }
171    }
172    from_fn(|i| {
173        let values = evals.iter().map(|x| x[i]).collect_vec();
174        UnivariatePoly::from_geometric_cosets_evals_idft(RowMajorMatrix::new(values, d), g, g)
175    })
176}
177
178pub const fn sumcheck_round0_deg(l_skip: usize, d: usize) -> usize {
179    d * ((1 << l_skip) - 1)
180}
181
182/// `mat` is a matrix of the evaluations on hyperprism D_n of a prismalinear extensions of the
183/// columns. We "fold" it by evaluating the prismalinear polynomials at `r` in the univariate
184/// variable `Z`.
185///
186/// If `n < 0`, then we evaluate `mat` at `r^{-n}`, which is equivalent to folding the lift of
187/// `mat`.
188#[instrument(level = "trace", skip_all)]
189pub fn fold_ple_evals<F, EF>(
190    l_skip: usize,
191    mat: StridedColMajorMatrixView<F>,
192    is_rot: bool,
193    r: EF,
194) -> ColMajorMatrix<EF>
195where
196    F: TwoAdicField,
197    EF: ExtensionField<F> + TwoAdicField,
198{
199    let height = mat.height();
200    let lifted_height = height.max(1 << l_skip);
201    let width = mat.width();
202
203    let omega = F::two_adic_generator(l_skip);
204    let omega_pows = omega.powers().take(1 << l_skip).collect_vec();
205    let denoms = omega_pows
206        .iter()
207        .map(|&x_i| r - EF::from(x_i))
208        .collect_vec();
209    let inv_denoms = batch_multiplicative_inverse(&denoms);
210
211    let offset = usize::from(is_rot);
212    let new_height = lifted_height >> l_skip;
213    let values = (0..width * new_height)
214        .into_par_iter()
215        .map(|idx| {
216            // `values` needs to be column-major
217            let x = idx % new_height;
218            let j = idx / new_height;
219            // SAFETY: j < width and we mod by height so row_idx < height
220            // Note that the `% height` is also necessary to handle lifting of `mats`
221            let uni_evals = (0..1 << l_skip)
222                .map(|z| unsafe { *mat.get_unchecked(((x << l_skip) + z + offset) % height, j) })
223                .collect_vec();
224            interpolate_coset_with_precomputation(
225                &RowMajorMatrix::new_col(uni_evals),
226                F::ONE,
227                r,
228                &omega_pows,
229                &inv_denoms,
230            )[0]
231        })
232        .collect::<Vec<_>>();
233    ColMajorMatrix::new(values, width)
234}
235
236pub fn batch_fold_ple_evals<F, EF>(
237    l_skip: usize,
238    mats: Vec<ColMajorMatrix<F>>,
239    is_rot: bool,
240    r: EF,
241) -> Vec<ColMajorMatrix<EF>>
242where
243    F: TwoAdicField,
244    EF: ExtensionField<F> + TwoAdicField,
245{
246    mats.into_par_iter()
247        .map(|mat| fold_ple_evals(l_skip, mat.as_view().into(), is_rot, r))
248        .collect()
249}
250
251/// For a sumcheck round, we want to compute the univariate polynomial `s(X) = sum_{y \in H_{n-1}}
252/// \hat{f}(X, y)`. For this function, assume that `\hat{f}(\vec x) = W(\hat{T}_0(\vec x), ..,
253/// \hat{T}_{m-1}(\vec x))` for a sequence of `\hat{T}_i` where each `\hat{T}_i` consists of a
254/// collection of MLE polynomials in `n` variables.
255///
256/// The `mats` consists of the evaluations of `\hat{T}_i` on the hypercube `H_n`, where evaluations
257/// of each `\hat{T}_i` are in column-major order.
258///
259/// Let `W` be degree `d` in each variable. Then `s` is degree `d`, so it can be interpolated using
260/// `d + 1` points. This function returns the evaluations of `s` at `{1, ..., d}`. The evaluation at
261/// `0` is omitted because our use of sumcheck always leaves the verifier to infer the evaluation at
262/// `0` from the previous round's claim.
263///
264/// The generic `WF` is a closure `{\hat{T}_i(X, y)}_i -> W(\hat{T}_0(X, y), .., \hat{T}_{m-1}(X,
265/// y))`.
266///
267/// This function should **not** be used for the univariate skip round.
268#[instrument(level = "trace", skip_all)]
269pub fn sumcheck_round_poly_evals<F, FN, const WD: usize>(
270    n: usize,
271    d: usize,
272    mats: &[ColMajorMatrixView<F>],
273    w: FN,
274) -> [Vec<F>; WD]
275where
276    F: Field,
277    FN: Fn(
278            F,         /* X */
279            usize,     /* y_int */
280            &[Vec<F>], /* mats eval at (X, bin(y_int)) */
281        ) -> [F; WD]
282        + Sync,
283{
284    debug_assert!(mats.iter().all(|mat| mat.height() == 1 << n));
285    if n == 0 {
286        // Sum is trivial, s(X) is constant
287        let evals = mats.iter().map(|row| row.values.to_vec()).collect_vec();
288        return w(F::ONE, 0, &evals).map(|x| vec![x; d]);
289    }
290    let hypercube_dim = n - 1;
291    // \hat{f}(x, \vec y) where \vec y is point on hypercube H_{n-1}
292    let f_hat = |x: usize, y: usize| {
293        let x = F::from_usize(x);
294        let row_x_y = mats
295            .iter()
296            .map(|mat| {
297                mat.columns()
298                    .map(|col| {
299                        let t_0 = col[y << 1];
300                        let t_1 = col[(y << 1) | 1];
301                        // Evaluate \hat{t}(x, \vec y) by linear interpolation since
302                        // \hat{t} is MLE
303                        t_0 + (t_1 - t_0) * x
304                    })
305                    .collect_vec()
306            })
307            .collect_vec();
308        w(x, y, &row_x_y)
309    };
310    trace!(sum_claim = ?{(0..1 << n)
311        .map(|x| f_hat(x & 1, x >> 1))
312        .fold([F::ZERO; WD], |mut acc, x| {
313            for (acc_i, x_i) in acc.iter_mut().zip(x) {
314                *acc_i += x_i;
315            }
316            acc
317        })
318    }, "sumcheck_round");
319    // Map-Reduce
320    // Map: for each y in H_{n-1}, compute
321    // ```
322    // [W(\hat{T}_0(x, y), ..., \hat{T}_{m-1}(x, y)) for x in {1,...,d}]
323    // ```
324    // We choose to iterate over y first to avoid multiple memory accesses to `\hat{T}`s
325    let evals = (0..1 << hypercube_dim)
326        .into_par_iter()
327        .map(|y| (1..=d).map(|x| f_hat(x, y)).collect_vec());
328    // Reduce: sum over H_{n-1}
329    let hypercube_sum = |mut acc: Vec<[F; WD]>, x| {
330        for (acc, x) in acc.iter_mut().zip(x) {
331            for (acc_i, x_i) in acc.iter_mut().zip(x) {
332                *acc_i += x_i;
333            }
334        }
335        acc
336    };
337    cfg_if! {
338        if #[cfg(feature = "parallel")] {
339            let evals = evals.reduce(
340                || vec![[F::ZERO; WD]; d],
341                hypercube_sum
342            );
343        } else {
344            let evals = evals.collect_vec();
345            let evals = evals.into_iter().fold(
346                vec![[F::ZERO; WD]; d],
347                hypercube_sum
348            );
349        }
350    }
351    from_fn(|i| evals.iter().map(|eval| eval[i]).collect_vec())
352}
353
354#[instrument(level = "trace", skip_all)]
355pub fn fold_mle_evals<EF: Field>(mat: ColMajorMatrix<EF>, r: EF) -> ColMajorMatrix<EF> {
356    let height = mat.height();
357    if height <= 1 {
358        return mat;
359    }
360    let width = mat.width();
361    let values = mat
362        .values
363        .par_chunks_exact(height)
364        .flat_map(|t| {
365            t.par_chunks_exact(2).map(|t_01| {
366                let t_0 = t_01[0];
367                let t_1 = t_01[1];
368                t_0 + (t_1 - t_0) * r
369            })
370        })
371        .collect::<Vec<_>>();
372    ColMajorMatrix::new(values, width)
373}
374
375pub fn batch_fold_mle_evals<EF: Field>(
376    mats: Vec<ColMajorMatrix<EF>>,
377    r: EF,
378) -> Vec<ColMajorMatrix<EF>> {
379    mats.into_par_iter()
380        .map(|mat| fold_mle_evals(mat, r))
381        .collect()
382}
383
384/// `mat` is column major evaluations on H_n
385pub fn fold_mle_evals_inplace<EF: Field>(mat: &mut ColMajorMatrix<EF>, r: EF) {
386    let height = mat.height();
387    if height <= 1 {
388        return;
389    }
390    mat.values.par_chunks_exact_mut(height).for_each(|t| {
391        for y in 0..height / 2 {
392            let t_0 = t[y << 1];
393            let t_1 = t[(y << 1) + 1];
394            t[y] = t_0 + (t_1 - t_0) * r;
395        }
396    });
397}
398
399pub struct SumcheckCubeProof<EF> {
400    /// Note: the sum claim is always observed as an element of the extension field.
401    pub sum_claim: EF,
402    /// For each `round`, we have univariate polynomial `s_round`. We store evaluations at `{1,
403    /// ..., deg(s_round)}` where evaluation at `0` is left for the verifier to infer from the
404    /// previous round claim.
405    pub round_polys_eval: Vec<Vec<EF>>,
406    /// Final evaluation claim of the polynomial at the random vector `r`
407    pub eval_claim: EF,
408}
409
410pub struct SumcheckPrismProof<EF> {
411    pub sum_claim: EF,
412    /// The univariate polynomial `s_0` in coefficient form.
413    pub s_0: UnivariatePoly<EF>,
414    /// for each hypercube `round`, the evaluations of univariate polynomial `s_round` at `{1, ...,
415    /// deg(s_round)}`. See [SumcheckCubeProof] for details.
416    pub round_polys_eval: Vec<Vec<EF>>,
417    /// Final evaluation claim of the polynomial at the random vector `r`
418    pub eval_claim: EF,
419}
420
421/// "Plain" sumcheck on a multilinear polynomial
422///
423/// The slice `evals` contains the evaluations of a multilinear polynomial on boolean hypercube.
424/// The length of `evals` should equal `2^n` where `n` is hypercube dimension.
425///
426/// Returns the sumcheck proof containing all prover messages and the random evaluation point.
427//
428// NOTE[jpw]: we currently fix EF for the transcript, but the evaluations in F can be either base
429// field or extension field
430#[allow(clippy::type_complexity)]
431pub fn sumcheck_multilinear<SC: StarkProtocolConfig, F: Field, TS: FiatShamirTranscript<SC>>(
432    transcript: &mut TS,
433    evals: &[F],
434) -> Result<(SumcheckCubeProof<SC::EF>, Vec<SC::EF>), SumcheckError>
435where
436    SC::EF: ExtensionField<F>,
437{
438    let n = log2_strict_usize(evals.len());
439    let mut round_polys_eval = Vec::with_capacity(n);
440    let mut r = Vec::with_capacity(n);
441
442    // Working copy of evaluations that gets folded after each round
443    // PERF[jpw]: the first round should be treated specially in the case F is the base field
444    let mut current_evals =
445        ColMajorMatrix::new(evals.iter().map(|&x| SC::EF::from(x)).collect(), 1);
446    let sum_claim: SC::EF = evals.iter().fold(F::ZERO, |acc, &x| acc + x).into();
447    transcript.observe_ext(sum_claim);
448
449    // Sumcheck rounds:
450    // - each round the prover needs to compute univariate polynomial `s_round`. This poly is linear
451    //   since we are taking MLE of `evals`.
452    // - at end of each round, sample random `r_round` in `EF`
453    for round in 0..n {
454        let [s] =
455            sumcheck_round_poly_evals(n - round, 1, &[current_evals.as_view()], |_x, _y, evals| {
456                [evals[0][0]]
457            });
458
459        if s.len() != 1 {
460            return Err(SumcheckError::MultilinearRoundPolyLen { len: s.len() });
461        }
462        transcript.observe_ext(s[0]);
463        round_polys_eval.push(s);
464
465        let r_round = transcript.sample_ext();
466        debug!(%round, %r_round);
467        r.push(r_round);
468
469        current_evals = fold_mle_evals(current_evals, r_round);
470    }
471
472    // After all rounds, current_evals should have exactly one element
473    if current_evals.values.len() != 1 {
474        return Err(SumcheckError::MultilinearFinalEvalLen {
475            len: current_evals.values.len(),
476        });
477    }
478    let eval_claim = current_evals.values[0];
479
480    // Add final evaluation to transcript
481    transcript.observe_ext(eval_claim);
482
483    Ok((
484        SumcheckCubeProof {
485            sum_claim,
486            round_polys_eval,
487            eval_claim,
488        },
489        r,
490    ))
491}
492
493/// "Plain" sumcheck on a prismalinear polynomial with Gruen's univariate skip.
494///
495/// The slice `evals` contains the evaluations of a prismalinear polynomial on the hyperprism.
496/// The length of `evals` should equal `2^{l_skip + n}` where `l_skip` is the univariate skip
497/// parameter and `n` is hypercube dimension.
498/// Indexing is such that `evals[x * 2^{l_skip} + i]` is the evaluation of `f(omega_D^i, x)` where
499/// `omega_D` is a fixed generator of the univariate skip domain `D` (which is a subgroup of
500/// `F^*`).
501///
502/// Returns the sumcheck proof containing all prover messages and the random evaluation point.
503//
504// NOTE[jpw]:
505// - we currently fix EF for the transcript, but the evaluations in F can be either base
506// field or extension field.
507// - for simplicity, the transcript observes `sum_claim` and `s_0` as valued in `EF`. More
508//   fine-grained approaches may observe in `F`.
509#[allow(clippy::type_complexity)]
510pub fn sumcheck_prismalinear<SC: StarkProtocolConfig, F, TS: FiatShamirTranscript<SC>>(
511    transcript: &mut TS,
512    l_skip: usize,
513    evals: &[F],
514) -> Result<(SumcheckPrismProof<SC::EF>, Vec<SC::EF>), SumcheckError>
515where
516    F: TwoAdicField,
517    SC::EF: ExtensionField<F> + TwoAdicField,
518{
519    let prism_dim = log2_strict_usize(evals.len());
520    if prism_dim < l_skip {
521        return Err(SumcheckError::PrismalinearDimTooSmall { prism_dim, l_skip });
522    }
523    let n = prism_dim - l_skip;
524
525    let mut round_polys_eval = Vec::with_capacity(n);
526    let mut r = Vec::with_capacity(n + 1);
527
528    let sum_claim: SC::EF = evals.iter().copied().sum::<F>().into();
529    transcript.observe_ext(sum_claim);
530    let current_evals = ColMajorMatrix::new(evals.to_vec(), 1);
531    let [s_0] = sumcheck_uni_round0_poly(
532        l_skip,
533        n,
534        1,
535        &[(current_evals.as_view().into(), false)],
536        |_z, _x, evals| [evals[0][0]],
537    );
538    let s_0_ext = UnivariatePoly::new(
539        s_0.0
540            .into_iter()
541            .map(|x| {
542                let ext = SC::EF::from(x);
543                transcript.observe_ext(ext);
544                ext
545            })
546            .collect(),
547    );
548
549    let r_0 = transcript.sample_ext();
550    debug!(round = 0, r_round = %r_0);
551    r.push(r_0);
552
553    // After sampling r_0, we need to evaluate the prismalinear polynomial at (r_0, x) for each x in
554    // hypercube. For each x in the hypercube, we have evaluations f(z, x) for z in the
555    // univariate skip domain D. We interpolate these to get a univariate polynomial and evaluate
556    // at r_0.
557    let mut current_evals = fold_ple_evals(l_skip, current_evals.as_view().into(), false, r_0);
558    debug_assert_eq!(current_evals.height(), 1 << n);
559
560    // Sumcheck rounds:
561    // - each round the prover needs to compute univariate polynomial `s_round`. This poly is linear
562    //   since we are taking MLE of `evals`.
563    // - at end of each round, sample random `r_round` in `EF`
564    for round in 1..=n {
565        debug!(
566            cur_sum = %current_evals
567                .values
568                .iter()
569                .fold(SC::EF::ZERO, |acc, x| acc + *x)
570        );
571        let [s] = sumcheck_round_poly_evals(
572            n + 1 - round,
573            1,
574            &[current_evals.as_view()],
575            |_x, _y, evals| [evals[0][0]],
576        );
577        if s.len() != 1 {
578            return Err(SumcheckError::PrismalinearRoundPolyLen { len: s.len() });
579        }
580        transcript.observe_ext(s[0]);
581        round_polys_eval.push(s);
582
583        let r_round = transcript.sample_ext();
584        debug!(%round, %r_round);
585        r.push(r_round);
586
587        current_evals = fold_mle_evals(current_evals, r_round);
588    }
589
590    if r.len() != n + 1 {
591        return Err(SumcheckError::PrismalinearRLen {
592            r_len: r.len(),
593            expected: n + 1,
594        });
595    }
596    // After all rounds, current_evals should have exactly one element
597    if current_evals.values.len() != 1 {
598        return Err(SumcheckError::PrismalinearFinalEvalLen {
599            len: current_evals.values.len(),
600        });
601    }
602    let eval_claim = current_evals.values[0];
603
604    // Add final evaluation to transcript
605    transcript.observe_ext(eval_claim);
606
607    Ok((
608        SumcheckPrismProof {
609            sum_claim,
610            s_0: s_0_ext,
611            round_polys_eval,
612            eval_claim,
613        },
614        r,
615    ))
616}