kimchi/circuits/polynomials/
permutation.rs

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