Skip to main content

kimchi/circuits/polynomials/
permutation.rs

1//! This module implements permutation constraint polynomials.
2use alloc::vec::Vec;
3
4//~ The permutation constraints are the following 4 constraints:
5//~
6//~ The two sides of the coin (with $\text{shift}_0 = 1$):
7//~
8//~ $$\begin{align}
9//~     & z(x) \cdot zkpm(x) \cdot \alpha^{PERM0} \cdot \\
10//~     & (w_0(x) + \beta \cdot \text{shift}_0 x + \gamma) \cdot \\
11//~     & (w_1(x) + \beta \cdot \text{shift}_1 x + \gamma) \cdot \\
12//~     & (w_2(x) + \beta \cdot \text{shift}_2 x + \gamma) \cdot \\
13//~     & (w_3(x) + \beta \cdot \text{shift}_3 x + \gamma) \cdot \\
14//~     & (w_4(x) + \beta \cdot \text{shift}_4 x + \gamma) \cdot \\
15//~     & (w_5(x) + \beta \cdot \text{shift}_5 x + \gamma) \cdot \\
16//~     & (w_6(x) + \beta \cdot \text{shift}_6 x + \gamma)
17//~ \end{align}$$
18//~
19//~ and
20//~
21//~ $$\begin{align}
22//~ & -1 \cdot z(x \omega) \cdot zkpm(x) \cdot \alpha^{PERM0} \cdot \\
23//~ & (w_0(x) + \beta \cdot \sigma_0(x) + \gamma) \cdot \\
24//~ & (w_1(x) + \beta \cdot \sigma_1(x) + \gamma) \cdot \\
25//~ & (w_2(x) + \beta \cdot \sigma_2(x) + \gamma) \cdot \\
26//~ & (w_3(x) + \beta \cdot \sigma_3(x) + \gamma) \cdot \\
27//~ & (w_4(x) + \beta \cdot \sigma_4(x) + \gamma) \cdot \\
28//~ & (w_5(x) + \beta \cdot \sigma_5(x) + \gamma) \cdot \\
29//~ & (w_6(x) + \beta \cdot \sigma_6(x) + \gamma) \cdot
30//~ \end{align}$$
31//~
32//~ the initialization of the accumulator:
33//~
34//~ $$(z(x) - 1) L_1(x) \alpha^{PERM1}$$
35//~
36//~ and the accumulator's final value:
37//~
38//~ $$(z(x) - 1) L_{n-k}(x) \alpha^{PERM2}$$
39//~
40//~ You can read more about why it looks like that in [this post](https://minaprotocol.com/blog/a-more-efficient-approach-to-zero-knowledge-for-plonk).
41//~
42use crate::{
43    circuits::{constraints::ConstraintSystem, wires::PERMUTS},
44    proof::{PointEvaluations, ProofEvaluations},
45};
46use ark_ff::{FftField, PrimeField};
47use ark_poly::{
48    univariate::DensePolynomial, DenseUVPolynomial, EvaluationDomain, Radix2EvaluationDomain as D,
49};
50use blake2::{Blake2b512, Digest};
51use core::array;
52
53#[cfg(feature = "prover")]
54use {
55    crate::{
56        circuits::{
57            polynomial::WitnessOverDomains,
58            wires::{Wire, COLUMNS},
59        },
60        curve::KimchiCurve,
61        error::ProverError,
62        prover_index::ProverIndex,
63    },
64    ark_ff::Zero,
65    ark_poly::{univariate::DenseOrSparsePolynomial, Evaluations, Polynomial},
66    o1_utils::{ExtendedDensePolynomial, ExtendedEvaluations},
67    rand::{CryptoRng, RngCore},
68};
69
70#[cfg(feature = "parallel")]
71use rayon::prelude::*;
72
73/// Number of constraints produced by the argument.
74pub const CONSTRAINTS: u32 = 3;
75
76/// Evaluates the polynomial
77/// (x - w^{n - i}) * (x - w^{n - i + 1}) * ... * (x - w^{n - 1})
78pub fn eval_vanishes_on_last_n_rows<F: FftField>(domain: D<F>, i: u64, x: F) -> F {
79    if i == 0 {
80        return F::one();
81    }
82    let mut term = domain.group_gen.pow([domain.size - i]);
83    let mut acc = x - term;
84    for _ in 0..i - 1 {
85        term *= domain.group_gen;
86        acc *= x - term;
87    }
88    acc
89}
90
91/// The polynomial
92/// (x - w^{n - i}) * (x - w^{n - i + 1}) * ... * (x - w^{n - 1})
93pub fn vanishes_on_last_n_rows<F: FftField>(domain: D<F>, i: u64) -> DensePolynomial<F> {
94    let constant = |a: F| DensePolynomial::from_coefficients_slice(&[a]);
95    if i == 0 {
96        return constant(F::one());
97    }
98    let x = DensePolynomial::from_coefficients_slice(&[F::zero(), F::one()]);
99    let mut term = domain.group_gen.pow([domain.size - i]);
100    let mut acc = &x - &constant(term);
101    for _ in 0..i - 1 {
102        term *= domain.group_gen;
103        acc = &acc * &(&x - &constant(term));
104    }
105    acc
106}
107
108/// Returns the end of the circuit, which is used for introducing zero-knowledge in the permutation polynomial
109pub fn zk_w<F: FftField>(domain: D<F>, zk_rows: u64) -> F {
110    domain.group_gen.pow([domain.size - zk_rows])
111}
112
113/// Evaluates the polynomial
114/// (x - w^{n - zk_rows}) * (x - w^{n - zk_rows + 1}) * (x - w^{n - 1})
115pub fn eval_permutation_vanishing_polynomial<F: FftField>(domain: D<F>, zk_rows: u64, x: F) -> F {
116    let term = domain.group_gen.pow([domain.size - zk_rows]);
117    (x - term) * (x - term * domain.group_gen) * (x - domain.group_gen.pow([domain.size - 1]))
118}
119
120/// The polynomial
121/// (x - w^{n - zk_rows}) * (x - w^{n - zk_rows + 1}) * (x - w^{n - 1})
122pub fn permutation_vanishing_polynomial<F: FftField>(
123    domain: D<F>,
124    zk_rows: u64,
125) -> DensePolynomial<F> {
126    let constant = |a: F| DensePolynomial::from_coefficients_slice(&[a]);
127    let x = DensePolynomial::from_coefficients_slice(&[F::zero(), F::one()]);
128    let term = domain.group_gen.pow([domain.size - zk_rows]);
129    &(&(&x - &constant(term)) * &(&x - &constant(term * domain.group_gen)))
130        * &(&x - &constant(domain.group_gen.pow([domain.size - 1])))
131}
132
133/// Shifts represent the shifts required in the permutation argument of PLONK.
134/// It also caches the shifted powers of omega for optimization purposes.
135pub struct Shifts<F> {
136    /// The coefficients `k` (in the Plonk paper) that create a coset when multiplied with the generator of our domain.
137    pub(crate) shifts: [F; PERMUTS],
138    /// A matrix that maps all cells coordinates `{col, row}` to their shifted field element.
139    /// For example the cell `{col:2, row:1}` will map to `omega * k2`,
140    /// which lives in `map[2][1]`
141    pub(crate) map: [Vec<F>; PERMUTS],
142}
143
144impl<F> Shifts<F>
145where
146    F: FftField,
147{
148    /// Generates the shifts for a given domain
149    pub fn new(domain: &D<F>) -> Self {
150        let mut shifts = [F::zero(); PERMUTS];
151
152        // first shift is the identity
153        shifts[0] = F::one();
154
155        // sample the other shifts
156        let mut i: u32 = 7;
157        for idx in 1..(PERMUTS) {
158            let mut shift = Self::sample(domain, &mut i);
159            // they have to be distincts
160            while shifts.contains(&shift) {
161                shift = Self::sample(domain, &mut i);
162            }
163            shifts[idx] = shift;
164        }
165
166        // create a map of cells to their shifted value
167        let map: [Vec<F>; PERMUTS] =
168            array::from_fn(|i| domain.elements().map(|elm| shifts[i] * elm).collect());
169
170        //
171        Self { shifts, map }
172    }
173
174    /// retrieve the shifts
175    pub fn shifts(&self) -> &[F; PERMUTS] {
176        &self.shifts
177    }
178
179    /// sample coordinate shifts deterministically
180    fn sample(domain: &D<F>, input: &mut u32) -> F {
181        let mut h = Blake2b512::new();
182
183        *input += 1;
184        h.update(input.to_be_bytes());
185
186        let mut shift = F::from_random_bytes(&h.finalize()[..31])
187            .expect("our field elements fit in more than 31 bytes");
188
189        while !shift.legendre().is_qnr() || domain.evaluate_vanishing_polynomial(shift).is_zero() {
190            let mut h = Blake2b512::new();
191            *input += 1;
192            h.update(input.to_be_bytes());
193            shift = F::from_random_bytes(&h.finalize()[..31])
194                .expect("our field elements fit in more than 31 bytes");
195        }
196        shift
197    }
198
199    /// Returns the field element that represents a position
200    #[cfg(feature = "prover")]
201    pub(crate) fn cell_to_field(&self, &Wire { row, col }: &Wire) -> F {
202        self.map[col][row]
203    }
204}
205
206/// Multiply `l` by `r` element-wise over d8, skipping the d1 rows -- the indices
207/// that are multiples of 8, since d8 = 8 * d1. The permutation contribution
208/// vanishes on all of d1, so those rows are left at the zero that building the
209/// terms produced; only the non-d1 products need computing.
210#[cfg(feature = "prover")]
211fn mul_assign_skipping_d1<F: FftField>(l: &mut Evaluations<F, D<F>>, r: &Evaluations<F, D<F>>) {
212    l.evals
213        .par_iter_mut()
214        .enumerate()
215        .zip(r.evals.par_iter())
216        .for_each(|((i, a), b)| {
217            if i & 7 != 0 {
218                *a *= b;
219            }
220        });
221}
222
223#[cfg(feature = "prover")]
224impl<const FULL_ROUNDS: usize, F, G, Srs> ProverIndex<FULL_ROUNDS, G, Srs>
225where
226    F: PrimeField,
227    G: KimchiCurve<FULL_ROUNDS, ScalarField = F>,
228    Srs: poly_commitment::SRS<G>,
229{
230    /// permutation quotient poly contribution computation
231    ///
232    /// # Errors
233    ///
234    /// Will give error if `polynomial division` fails.
235    ///
236    /// # Panics
237    ///
238    /// Will panic if `power of alpha` is missing.
239    #[allow(clippy::type_complexity)]
240    pub fn perm_quot(
241        &self,
242        lagrange: &WitnessOverDomains<F>,
243        beta: F,
244        gamma: F,
245        z: &DensePolynomial<F>,
246        mut alphas: impl Iterator<Item = F>,
247    ) -> Result<(Evaluations<F, D<F>>, DensePolynomial<F>), ProverError> {
248        let alpha0 = alphas.next().expect("missing power of alpha");
249        let alpha1 = alphas.next().expect("missing power of alpha");
250        let alpha2 = alphas.next().expect("missing power of alpha");
251
252        let zk_rows = self.cs.zk_rows as usize;
253
254        //~ The quotient contribution of the permutation is split into two parts $perm$ and $bnd$.
255        //~ They will be used by the prover.
256        //~
257        //~ $$
258        //~ \begin{align}
259        //~ perm(x) =
260        //~     & \; a^{PERM0} \cdot zkpl(x) \cdot [ \\
261        //~     & \;\;   z(x) \cdot \\
262        //~     & \;\;   (w_0(x) + \gamma + x \cdot \beta \cdot \text{shift}_0) \cdot \\
263        //~     & \;\;   (w_1(x) + \gamma + x \cdot \beta \cdot \text{shift}_1) \cdot \\
264        //~     & \;\;   (w_2(x) + \gamma + x \cdot \beta \cdot \text{shift}_2) \cdot \\
265        //~     & \;\;   (w_3(x) + \gamma + x \cdot \beta \cdot \text{shift}_3) \cdot \\
266        //~     & \;\;   (w_4(x) + \gamma + x \cdot \beta \cdot \text{shift}_4) \cdot \\
267        //~     & \;\;   (w_5(x) + \gamma + x \cdot \beta \cdot \text{shift}_5) \cdot \\
268        //~     & \;\;   (w_6(x) + \gamma + x \cdot \beta \cdot \text{shift}_6) \cdot \\
269        //~     & \;   - \\
270        //~     & \;\;   z(x \cdot w) \cdot \\
271        //~     & \;\;   (w_0(x) + \gamma + \sigma_0 \cdot \beta) \cdot \\
272        //~     & \;\;   (w_1(x) + \gamma + \sigma_1 \cdot \beta) \cdot \\
273        //~     & \;\;   (w_2(x) + \gamma + \sigma_2 \cdot \beta) \cdot \\
274        //~     & \;\;   (w_3(x) + \gamma + \sigma_3 \cdot \beta) \cdot \\
275        //~     & \;\;   (w_4(x) + \gamma + \sigma_4 \cdot \beta) \cdot \\
276        //~     & \;\;   (w_5(x) + \gamma + \sigma_5 \cdot \beta) \cdot \\
277        //~     & \;\;   (w_6(x) + \gamma + \sigma_6 \cdot \beta) \cdot \\
278        //~     &]
279        //~ \end{align}
280        //~ $$
281        //~
282        let perm = {
283            // shifts = z(x) *
284            // (w[0](x) + gamma + x * beta * shift[0]) *
285            // (w[1](x) + gamma + x * beta * shift[1]) * ...
286            // (w[6](x) + gamma + x * beta * shift[6])
287            // in evaluation form in d8, computed in a single pass per element:
288            // gamma is a constant, so it needs no all-ones broadcast vector,
289            // and x is read straight off the cached d8 domain points.
290            let shifts: Evaluations<F, D<F>> = &lagrange
291                .this
292                .w
293                .par_iter()
294                .zip(self.cs.shift.par_iter())
295                .map(|(witness, shift)| {
296                    let beta_shift = beta * shift;
297                    let evals: Vec<F> = witness
298                        .evals
299                        .par_iter()
300                        .zip(self.cs.precomputations().poly_x_d1.evals.par_iter())
301                        .enumerate()
302                        .map(|(i, (w, x))| {
303                            // Skip the d1 rows (multiples of 8): the permutation
304                            // vanishes there, so the products land at zero.
305                            if i & 7 == 0 {
306                                F::zero()
307                            } else {
308                                *w + gamma + beta_shift * x
309                            }
310                        })
311                        .collect();
312                    Evaluations::<F, D<F>>::from_vec_and_domain(evals, self.cs.domain.d8)
313                })
314                .reduce_with(|mut l, r| {
315                    mul_assign_skipping_d1(&mut l, &r);
316                    l
317                })
318                .unwrap()
319                * &lagrange.this.z.clone();
320
321            // sigmas = z(x * w) *
322            // (w8[0] + gamma + sigma[0] * beta) *
323            // (w8[1] + gamma + sigma[1] * beta) * ...
324            // (w8[6] + gamma + sigma[6] * beta)
325            // in evaluation form in d8, computed in a single pass per element
326            let sigmas = &lagrange
327                .this
328                .w
329                .par_iter()
330                .zip(
331                    self.column_evaluations
332                        .get()
333                        .permutation_coefficients8
334                        .par_iter(),
335                )
336                .map(|(witness, sigma)| {
337                    let evals: Vec<F> = witness
338                        .evals
339                        .par_iter()
340                        .zip(sigma.evals.par_iter())
341                        .enumerate()
342                        .map(|(i, (w, s))| {
343                            // Skip the d1 rows (multiples of 8): the permutation
344                            // vanishes there, so the products land at zero.
345                            if i & 7 == 0 {
346                                F::zero()
347                            } else {
348                                *w + gamma + beta * s
349                            }
350                        })
351                        .collect();
352                    Evaluations::<F, D<F>>::from_vec_and_domain(evals, self.cs.domain.d8)
353                })
354                .reduce_with(|mut l, r| {
355                    mul_assign_skipping_d1(&mut l, &r);
356                    l
357                })
358                .unwrap()
359                * &lagrange.z_next.clone();
360
361            &(&shifts - &sigmas).scale(alpha0)
362                * &self.cs.precomputations().permutation_vanishing_polynomial_l
363        };
364
365        //~ and `bnd`:
366        //~
367        //~ $$bnd(x) =
368        //~     a^{PERM1} \cdot \frac{z(x) - 1}{x - 1}
369        //~     +
370        //~     a^{PERM2} \cdot \frac{z(x) - 1}{x - sid[n-k]}
371        //~ $$
372        let bnd = {
373            let one_poly = DensePolynomial::from_coefficients_slice(&[F::one()]);
374            let z_minus_1 = z - &one_poly;
375
376            // TODO(mimoo): use self.sid[0] instead of 1
377            // accumulator init := (z(x) - 1) / (x - 1)
378            let x_minus_1 = DensePolynomial::from_coefficients_slice(&[-F::one(), F::one()]);
379            let (bnd1, res) = DenseOrSparsePolynomial::divide_with_q_and_r(
380                &z_minus_1.clone().into(),
381                &x_minus_1.into(),
382            )
383            .ok_or(ProverError::Permutation("first division"))?;
384            if !res.is_zero() {
385                return Err(ProverError::Permutation("first division rest"));
386            }
387
388            // accumulator end := (z(x) - 1) / (x - sid[n-zk_rows])
389            let denominator = DensePolynomial::from_coefficients_slice(&[
390                -self.cs.sid[self.cs.domain.d1.size() - zk_rows],
391                F::one(),
392            ]);
393            let (bnd2, res) = DenseOrSparsePolynomial::divide_with_q_and_r(
394                &z_minus_1.into(),
395                &denominator.into(),
396            )
397            .ok_or(ProverError::Permutation("second division"))?;
398            if !res.is_zero() {
399                return Err(ProverError::Permutation("second division rest"));
400            }
401
402            &bnd1.scale(alpha1) + &bnd2.scale(alpha2)
403        };
404        Ok((perm, bnd))
405    }
406
407    /// permutation linearization poly contribution computation
408    pub fn perm_lnrz(
409        &self,
410        e: &ProofEvaluations<PointEvaluations<F>>,
411        zeta: F,
412        beta: F,
413        gamma: F,
414        alphas: impl Iterator<Item = F>,
415    ) -> Evaluations<F, D<F>> {
416        //~
417        //~ The linearization:
418        //~
419        //~ $\text{scalar} \cdot \sigma_6(x)$
420        //~
421        let zkpm_zeta = self
422            .cs
423            .precomputations()
424            .permutation_vanishing_polynomial_m
425            .evaluate(&zeta);
426        let scalar = ConstraintSystem::<F>::perm_scalars(e, beta, gamma, alphas, zkpm_zeta);
427        let evals8 = &self.column_evaluations.get().permutation_coefficients8[PERMUTS - 1].evals;
428        const STRIDE: usize = 8;
429        let n = evals8.len() / STRIDE;
430        let evals = (0..n)
431            .into_par_iter()
432            .map(|i| scalar * evals8[STRIDE * i])
433            .collect();
434        Evaluations::from_vec_and_domain(evals, D::new(n).unwrap())
435    }
436}
437
438impl<F: PrimeField> ConstraintSystem<F> {
439    pub fn perm_scalars(
440        e: &ProofEvaluations<PointEvaluations<F>>,
441        beta: F,
442        gamma: F,
443        mut alphas: impl Iterator<Item = F>,
444        zkp_zeta: F,
445    ) -> F {
446        let alpha0 = alphas
447            .next()
448            .expect("not enough powers of alpha for permutation");
449        let _alpha1 = alphas
450            .next()
451            .expect("not enough powers of alpha for permutation");
452        let _alpha2 = alphas
453            .next()
454            .expect("not enough powers of alpha for permutation");
455
456        //~ where $\text{scalar}$ is computed as:
457        //~
458        //~ $$
459        //~ \begin{align}
460        //~ z(\zeta \omega) \beta \alpha^{PERM0} zkpl(\zeta) \cdot \\
461        //~ (\gamma + \beta \sigma_0(\zeta) + w_0(\zeta)) \cdot \\
462        //~ (\gamma + \beta \sigma_1(\zeta) + w_1(\zeta)) \cdot \\
463        //~ (\gamma + \beta \sigma_2(\zeta) + w_2(\zeta)) \cdot \\
464        //~ (\gamma + \beta \sigma_3(\zeta) + w_3(\zeta)) \cdot \\
465        //~ (\gamma + \beta \sigma_4(\zeta) + w_4(\zeta)) \cdot \\
466        //~ (\gamma + \beta \sigma_5(\zeta) + w_5(\zeta)) \cdot \\
467        //~ \end{align}
468        //~$$
469        //~
470        let init = e.z.zeta_omega * beta * alpha0 * zkp_zeta;
471        let res =
472            e.w.iter()
473                .zip(e.s.iter())
474                .map(|(w, s)| gamma + (beta * s.zeta) + w.zeta)
475                .fold(init, |x, y| x * y);
476        -res
477    }
478}
479
480#[cfg(feature = "prover")]
481impl<const FULL_ROUNDS: usize, F, G, Srs> ProverIndex<FULL_ROUNDS, G, Srs>
482where
483    F: PrimeField,
484    G: KimchiCurve<FULL_ROUNDS, ScalarField = F>,
485    Srs: poly_commitment::SRS<G>,
486{
487    /// permutation aggregation polynomial computation
488    ///
489    /// # Errors
490    ///
491    /// Will give error if permutation result is not correct.
492    ///
493    /// # Panics
494    ///
495    /// Will panic if `first element` is not 1.
496    pub fn perm_aggreg(
497        &self,
498        witness: &[Vec<F>; COLUMNS],
499        beta: &F,
500        gamma: &F,
501        rng: &mut (impl RngCore + CryptoRng),
502    ) -> Result<DensePolynomial<F>, ProverError> {
503        let n = self.cs.domain.d1.size();
504
505        let zk_rows = self.cs.zk_rows as usize;
506
507        // only works if first element is 1
508        assert_eq!(self.cs.domain.d1.elements().next(), Some(F::one()));
509
510        //~ To compute the permutation aggregation polynomial,
511        //~ the prover interpolates the polynomial that has the following evaluations.
512
513        //~ The first evaluation represents the initial value of the accumulator:
514        //~ $$z(g^0) = 1$$
515
516        //~ For $i = 0, \cdot, n - 4$, where $n$ is the size of the domain,
517        //~ evaluations are computed as:
518        //~
519        //~ $$z(g^{i+1}) = z_1 / z_2$$
520        //~
521        //~ with
522        //~
523        //~ $$
524        //~ \begin{align}
525        //~ z_1 = &\ (w_0(g^i + sid(g^i) \cdot beta \cdot shift_0 + \gamma) \cdot \\
526        //~ &\ (w_1(g^i) + sid(g^i) \cdot beta \cdot shift_1 + \gamma) \cdot \\
527        //~ &\ (w_2(g^i) + sid(g^i) \cdot beta \cdot shift_2 + \gamma) \cdot \\
528        //~ &\ (w_3(g^i) + sid(g^i) \cdot beta \cdot shift_3 + \gamma) \cdot \\
529        //~ &\ (w_4(g^i) + sid(g^i) \cdot beta \cdot shift_4 + \gamma) \cdot \\
530        //~ &\ (w_5(g^i) + sid(g^i) \cdot beta \cdot shift_5 + \gamma) \cdot \\
531        //~ &\ (w_6(g^i) + sid(g^i) \cdot beta \cdot shift_6 + \gamma)
532        //~ \end{align}
533        //~ $$
534        //~
535        //~ and
536        //~
537        //~ $$
538        //~ \begin{align}
539        //~ z_2 = &\ (w_0(g^i) + \sigma_0 \cdot beta + \gamma) \cdot \\
540        //~ &\ (w_1(g^i) + \sigma_1 \cdot beta + \gamma) \cdot \\
541        //~ &\ (w_2(g^i) + \sigma_2 \cdot beta + \gamma) \cdot \\
542        //~ &\ (w_3(g^i) + \sigma_3 \cdot beta + \gamma) \cdot \\
543        //~ &\ (w_4(g^i) + \sigma_4 \cdot beta + \gamma) \cdot \\
544        //~ &\ (w_5(g^i) + \sigma_5 \cdot beta + \gamma) \cdot \\
545        //~ &\ (w_6(g^i) + \sigma_6 \cdot beta + \gamma)
546        //~ \end{align}
547        //~ $$
548        //~
549
550        // We compute z such that:
551        // z[0] = 1
552        // z[j+1] = \Prod_{i=0}^{PERMUTS}(wit[i][j] + (s[i][8*j] * beta) + gamma)     for j ∈ 0..n-1
553        //
554        // We compute every product batch separately first (one batch
555        // per i∈[COLUMNS]), and then multiply all batches together.
556        //
557        // Note that we zip array of COLUMNS with array of PERMUTS;
558        // Since PERMUTS < COLUMNS, that's what's actually used.
559        let mut z: Vec<F> = witness
560            .par_iter()
561            .zip(
562                self.column_evaluations
563                    .get()
564                    .permutation_coefficients8
565                    .par_iter(),
566            )
567            .map(|(w_i, perm_coeffs8_i)| {
568                let mut output_vec: Vec<_> = vec![F::one(); 1];
569                for (j, w_i_j) in w_i.iter().enumerate().take(n - 1) {
570                    output_vec.push(*w_i_j + (perm_coeffs8_i[8 * j] * beta) + gamma);
571                }
572                output_vec
573            })
574            .reduce_with(|mut l, r| {
575                for i in 0..n {
576                    l[i] *= &r[i];
577                }
578                l
579            })
580            .unwrap();
581
582        ark_ff::fields::batch_inversion::<F>(&mut z[1..n]);
583
584        let z_prefolded: Vec<F> = witness
585            .par_iter()
586            .zip(self.cs.shift.par_iter())
587            .map(|(w_i, shift_i)| {
588                let mut output_vec: Vec<_> = vec![F::one(); 1];
589                for (j, w_i_j) in w_i.iter().enumerate().take(n - 1) {
590                    output_vec.push(*w_i_j + (self.cs.sid[j] * beta * shift_i) + gamma);
591                }
592                output_vec
593            })
594            .reduce_with(|mut l, r| {
595                for i in 0..n {
596                    l[i] *= &r[i];
597                }
598                l
599            })
600            .unwrap();
601
602        //~ We randomize the evaluations at `n - zk_rows + 1` and `n - zk_rows + 2` in order to add
603        //~ zero-knowledge to the protocol.
604        //~
605        for j in 0..n - 1 {
606            if j != n - zk_rows && j != n - zk_rows + 1 {
607                let x = z[j];
608                z[j + 1] *= z_prefolded[j + 1] * x;
609            } else {
610                z[j + 1] = F::rand(rng);
611            }
612        }
613
614        //~ For a valid witness, we then have have $z(g^{n-zk_rows}) = 1$.
615        //~
616        if z[n - zk_rows] != F::one() {
617            return Err(ProverError::Permutation("final value"));
618        };
619
620        let res = Evaluations::<F, D<F>>::from_vec_and_domain(z, self.cs.domain.d1).interpolate();
621
622        Ok(res)
623    }
624}