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#[cfg(feature = "prover")]
207impl<const FULL_ROUNDS: usize, F, G, Srs> ProverIndex<FULL_ROUNDS, G, Srs>
208where
209 F: PrimeField,
210 G: KimchiCurve<FULL_ROUNDS, ScalarField = F>,
211 Srs: poly_commitment::SRS<G>,
212{
213 /// permutation quotient poly contribution computation
214 ///
215 /// # Errors
216 ///
217 /// Will give error if `polynomial division` fails.
218 ///
219 /// # Panics
220 ///
221 /// Will panic if `power of alpha` is missing.
222 #[allow(clippy::type_complexity)]
223 pub fn perm_quot(
224 &self,
225 lagrange: &WitnessOverDomains<F>,
226 beta: F,
227 gamma: F,
228 z: &DensePolynomial<F>,
229 mut alphas: impl Iterator<Item = F>,
230 ) -> Result<(Evaluations<F, D<F>>, DensePolynomial<F>), ProverError> {
231 let alpha0 = alphas.next().expect("missing power of alpha");
232 let alpha1 = alphas.next().expect("missing power of alpha");
233 let alpha2 = alphas.next().expect("missing power of alpha");
234
235 let zk_rows = self.cs.zk_rows as usize;
236
237 // constant gamma in evaluation form (in domain d8)
238 let gamma = &self.cs.precomputations().constant_1_d8.scale(gamma);
239
240 //~ The quotient contribution of the permutation is split into two parts $perm$ and $bnd$.
241 //~ They will be used by the prover.
242 //~
243 //~ $$
244 //~ \begin{align}
245 //~ perm(x) =
246 //~ & \; a^{PERM0} \cdot zkpl(x) \cdot [ \\
247 //~ & \;\; z(x) \cdot \\
248 //~ & \;\; (w_0(x) + \gamma + x \cdot \beta \cdot \text{shift}_0) \cdot \\
249 //~ & \;\; (w_1(x) + \gamma + x \cdot \beta \cdot \text{shift}_1) \cdot \\
250 //~ & \;\; (w_2(x) + \gamma + x \cdot \beta \cdot \text{shift}_2) \cdot \\
251 //~ & \;\; (w_3(x) + \gamma + x \cdot \beta \cdot \text{shift}_3) \cdot \\
252 //~ & \;\; (w_4(x) + \gamma + x \cdot \beta \cdot \text{shift}_4) \cdot \\
253 //~ & \;\; (w_5(x) + \gamma + x \cdot \beta \cdot \text{shift}_5) \cdot \\
254 //~ & \;\; (w_6(x) + \gamma + x \cdot \beta \cdot \text{shift}_6) \cdot \\
255 //~ & \; - \\
256 //~ & \;\; z(x \cdot w) \cdot \\
257 //~ & \;\; (w_0(x) + \gamma + \sigma_0 \cdot \beta) \cdot \\
258 //~ & \;\; (w_1(x) + \gamma + \sigma_1 \cdot \beta) \cdot \\
259 //~ & \;\; (w_2(x) + \gamma + \sigma_2 \cdot \beta) \cdot \\
260 //~ & \;\; (w_3(x) + \gamma + \sigma_3 \cdot \beta) \cdot \\
261 //~ & \;\; (w_4(x) + \gamma + \sigma_4 \cdot \beta) \cdot \\
262 //~ & \;\; (w_5(x) + \gamma + \sigma_5 \cdot \beta) \cdot \\
263 //~ & \;\; (w_6(x) + \gamma + \sigma_6 \cdot \beta) \cdot \\
264 //~ &]
265 //~ \end{align}
266 //~ $$
267 //~
268 let perm = {
269 // shifts = z(x) *
270 // (w[0](x) + gamma + x * beta * shift[0]) *
271 // (w[1](x) + gamma + x * beta * shift[1]) * ...
272 // (w[6](x) + gamma + x * beta * shift[6])
273 // in evaluation form in d8
274 let shifts: Evaluations<F, D<F>> = &lagrange
275 .d8
276 .this
277 .w
278 .par_iter()
279 .zip(self.cs.shift.par_iter())
280 .map(|(witness, shift)| {
281 &(witness + gamma) + &self.cs.precomputations().poly_x_d1.scale(beta * shift)
282 })
283 .reduce_with(|mut l, r| {
284 l *= &r;
285 l
286 })
287 .unwrap()
288 * &lagrange.d8.this.z.clone();
289
290 // sigmas = z(x * w) *
291 // (w8[0] + gamma + sigma[0] * beta) *
292 // (w8[1] + gamma + sigma[1] * beta) * ...
293 // (w8[6] + gamma + sigma[6] * beta)
294 // in evaluation form in d8
295 let sigmas = &lagrange
296 .d8
297 .this
298 .w
299 .par_iter()
300 .zip(
301 self.column_evaluations
302 .get()
303 .permutation_coefficients8
304 .par_iter(),
305 )
306 .map(|(witness, sigma)| witness + &(gamma + &sigma.scale(beta)))
307 .reduce_with(|mut l, r| {
308 l *= &r;
309 l
310 })
311 .unwrap()
312 * &lagrange.d8.next.z.clone();
313
314 &(&shifts - &sigmas).scale(alpha0)
315 * &self.cs.precomputations().permutation_vanishing_polynomial_l
316 };
317
318 //~ and `bnd`:
319 //~
320 //~ $$bnd(x) =
321 //~ a^{PERM1} \cdot \frac{z(x) - 1}{x - 1}
322 //~ +
323 //~ a^{PERM2} \cdot \frac{z(x) - 1}{x - sid[n-k]}
324 //~ $$
325 let bnd = {
326 let one_poly = DensePolynomial::from_coefficients_slice(&[F::one()]);
327 let z_minus_1 = z - &one_poly;
328
329 // TODO(mimoo): use self.sid[0] instead of 1
330 // accumulator init := (z(x) - 1) / (x - 1)
331 let x_minus_1 = DensePolynomial::from_coefficients_slice(&[-F::one(), F::one()]);
332 let (bnd1, res) = DenseOrSparsePolynomial::divide_with_q_and_r(
333 &z_minus_1.clone().into(),
334 &x_minus_1.into(),
335 )
336 .ok_or(ProverError::Permutation("first division"))?;
337 if !res.is_zero() {
338 return Err(ProverError::Permutation("first division rest"));
339 }
340
341 // accumulator end := (z(x) - 1) / (x - sid[n-zk_rows])
342 let denominator = DensePolynomial::from_coefficients_slice(&[
343 -self.cs.sid[self.cs.domain.d1.size() - zk_rows],
344 F::one(),
345 ]);
346 let (bnd2, res) = DenseOrSparsePolynomial::divide_with_q_and_r(
347 &z_minus_1.into(),
348 &denominator.into(),
349 )
350 .ok_or(ProverError::Permutation("second division"))?;
351 if !res.is_zero() {
352 return Err(ProverError::Permutation("second division rest"));
353 }
354
355 &bnd1.scale(alpha1) + &bnd2.scale(alpha2)
356 };
357 Ok((perm, bnd))
358 }
359
360 /// permutation linearization poly contribution computation
361 pub fn perm_lnrz(
362 &self,
363 e: &ProofEvaluations<PointEvaluations<F>>,
364 zeta: F,
365 beta: F,
366 gamma: F,
367 alphas: impl Iterator<Item = F>,
368 ) -> Evaluations<F, D<F>> {
369 //~
370 //~ The linearization:
371 //~
372 //~ $\text{scalar} \cdot \sigma_6(x)$
373 //~
374 let zkpm_zeta = self
375 .cs
376 .precomputations()
377 .permutation_vanishing_polynomial_m
378 .evaluate(&zeta);
379 let scalar = ConstraintSystem::<F>::perm_scalars(e, beta, gamma, alphas, zkpm_zeta);
380 let evals8 = &self.column_evaluations.get().permutation_coefficients8[PERMUTS - 1].evals;
381 const STRIDE: usize = 8;
382 let n = evals8.len() / STRIDE;
383 let evals = (0..n)
384 .into_par_iter()
385 .map(|i| scalar * evals8[STRIDE * i])
386 .collect();
387 Evaluations::from_vec_and_domain(evals, D::new(n).unwrap())
388 }
389}
390
391impl<F: PrimeField> ConstraintSystem<F> {
392 pub fn perm_scalars(
393 e: &ProofEvaluations<PointEvaluations<F>>,
394 beta: F,
395 gamma: F,
396 mut alphas: impl Iterator<Item = F>,
397 zkp_zeta: F,
398 ) -> F {
399 let alpha0 = alphas
400 .next()
401 .expect("not enough powers of alpha for permutation");
402 let _alpha1 = alphas
403 .next()
404 .expect("not enough powers of alpha for permutation");
405 let _alpha2 = alphas
406 .next()
407 .expect("not enough powers of alpha for permutation");
408
409 //~ where $\text{scalar}$ is computed as:
410 //~
411 //~ $$
412 //~ \begin{align}
413 //~ z(\zeta \omega) \beta \alpha^{PERM0} zkpl(\zeta) \cdot \\
414 //~ (\gamma + \beta \sigma_0(\zeta) + w_0(\zeta)) \cdot \\
415 //~ (\gamma + \beta \sigma_1(\zeta) + w_1(\zeta)) \cdot \\
416 //~ (\gamma + \beta \sigma_2(\zeta) + w_2(\zeta)) \cdot \\
417 //~ (\gamma + \beta \sigma_3(\zeta) + w_3(\zeta)) \cdot \\
418 //~ (\gamma + \beta \sigma_4(\zeta) + w_4(\zeta)) \cdot \\
419 //~ (\gamma + \beta \sigma_5(\zeta) + w_5(\zeta)) \cdot \\
420 //~ \end{align}
421 //~$$
422 //~
423 let init = e.z.zeta_omega * beta * alpha0 * zkp_zeta;
424 let res =
425 e.w.iter()
426 .zip(e.s.iter())
427 .map(|(w, s)| gamma + (beta * s.zeta) + w.zeta)
428 .fold(init, |x, y| x * y);
429 -res
430 }
431}
432
433#[cfg(feature = "prover")]
434impl<const FULL_ROUNDS: usize, F, G, Srs> ProverIndex<FULL_ROUNDS, G, Srs>
435where
436 F: PrimeField,
437 G: KimchiCurve<FULL_ROUNDS, ScalarField = F>,
438 Srs: poly_commitment::SRS<G>,
439{
440 /// permutation aggregation polynomial computation
441 ///
442 /// # Errors
443 ///
444 /// Will give error if permutation result is not correct.
445 ///
446 /// # Panics
447 ///
448 /// Will panic if `first element` is not 1.
449 pub fn perm_aggreg(
450 &self,
451 witness: &[Vec<F>; COLUMNS],
452 beta: &F,
453 gamma: &F,
454 rng: &mut (impl RngCore + CryptoRng),
455 ) -> Result<DensePolynomial<F>, ProverError> {
456 let n = self.cs.domain.d1.size();
457
458 let zk_rows = self.cs.zk_rows as usize;
459
460 // only works if first element is 1
461 assert_eq!(self.cs.domain.d1.elements().next(), Some(F::one()));
462
463 //~ To compute the permutation aggregation polynomial,
464 //~ the prover interpolates the polynomial that has the following evaluations.
465
466 //~ The first evaluation represents the initial value of the accumulator:
467 //~ $$z(g^0) = 1$$
468
469 //~ For $i = 0, \cdot, n - 4$, where $n$ is the size of the domain,
470 //~ evaluations are computed as:
471 //~
472 //~ $$z(g^{i+1}) = z_1 / z_2$$
473 //~
474 //~ with
475 //~
476 //~ $$
477 //~ \begin{align}
478 //~ z_1 = &\ (w_0(g^i + sid(g^i) \cdot beta \cdot shift_0 + \gamma) \cdot \\
479 //~ &\ (w_1(g^i) + sid(g^i) \cdot beta \cdot shift_1 + \gamma) \cdot \\
480 //~ &\ (w_2(g^i) + sid(g^i) \cdot beta \cdot shift_2 + \gamma) \cdot \\
481 //~ &\ (w_3(g^i) + sid(g^i) \cdot beta \cdot shift_3 + \gamma) \cdot \\
482 //~ &\ (w_4(g^i) + sid(g^i) \cdot beta \cdot shift_4 + \gamma) \cdot \\
483 //~ &\ (w_5(g^i) + sid(g^i) \cdot beta \cdot shift_5 + \gamma) \cdot \\
484 //~ &\ (w_6(g^i) + sid(g^i) \cdot beta \cdot shift_6 + \gamma)
485 //~ \end{align}
486 //~ $$
487 //~
488 //~ and
489 //~
490 //~ $$
491 //~ \begin{align}
492 //~ z_2 = &\ (w_0(g^i) + \sigma_0 \cdot beta + \gamma) \cdot \\
493 //~ &\ (w_1(g^i) + \sigma_1 \cdot beta + \gamma) \cdot \\
494 //~ &\ (w_2(g^i) + \sigma_2 \cdot beta + \gamma) \cdot \\
495 //~ &\ (w_3(g^i) + \sigma_3 \cdot beta + \gamma) \cdot \\
496 //~ &\ (w_4(g^i) + \sigma_4 \cdot beta + \gamma) \cdot \\
497 //~ &\ (w_5(g^i) + \sigma_5 \cdot beta + \gamma) \cdot \\
498 //~ &\ (w_6(g^i) + \sigma_6 \cdot beta + \gamma)
499 //~ \end{align}
500 //~ $$
501 //~
502
503 // We compute z such that:
504 // z[0] = 1
505 // z[j+1] = \Prod_{i=0}^{PERMUTS}(wit[i][j] + (s[i][8*j] * beta) + gamma) for j ā 0..n-1
506 //
507 // We compute every product batch separately first (one batch
508 // per iā[COLUMNS]), and then multiply all batches together.
509 //
510 // Note that we zip array of COLUMNS with array of PERMUTS;
511 // Since PERMUTS < COLUMNS, that's what's actually used.
512 let mut z: Vec<F> = witness
513 .par_iter()
514 .zip(
515 self.column_evaluations
516 .get()
517 .permutation_coefficients8
518 .par_iter(),
519 )
520 .map(|(w_i, perm_coeffs8_i)| {
521 let mut output_vec: Vec<_> = vec![F::one(); 1];
522 for (j, w_i_j) in w_i.iter().enumerate().take(n - 1) {
523 output_vec.push(*w_i_j + (perm_coeffs8_i[8 * j] * beta) + gamma);
524 }
525 output_vec
526 })
527 .reduce_with(|mut l, r| {
528 for i in 0..n {
529 l[i] *= &r[i];
530 }
531 l
532 })
533 .unwrap();
534
535 ark_ff::fields::batch_inversion::<F>(&mut z[1..n]);
536
537 let z_prefolded: Vec<F> = witness
538 .par_iter()
539 .zip(self.cs.shift.par_iter())
540 .map(|(w_i, shift_i)| {
541 let mut output_vec: Vec<_> = vec![F::one(); 1];
542 for (j, w_i_j) in w_i.iter().enumerate().take(n - 1) {
543 output_vec.push(*w_i_j + (self.cs.sid[j] * beta * shift_i) + gamma);
544 }
545 output_vec
546 })
547 .reduce_with(|mut l, r| {
548 for i in 0..n {
549 l[i] *= &r[i];
550 }
551 l
552 })
553 .unwrap();
554
555 //~ We randomize the evaluations at `n - zk_rows + 1` and `n - zk_rows + 2` in order to add
556 //~ zero-knowledge to the protocol.
557 //~
558 for j in 0..n - 1 {
559 if j != n - zk_rows && j != n - zk_rows + 1 {
560 let x = z[j];
561 z[j + 1] *= z_prefolded[j + 1] * x;
562 } else {
563 z[j + 1] = F::rand(rng);
564 }
565 }
566
567 //~ For a valid witness, we then have have $z(g^{n-zk_rows}) = 1$.
568 //~
569 if z[n - zk_rows] != F::one() {
570 return Err(ProverError::Permutation("final value"));
571 };
572
573 let res = Evaluations::<F, D<F>>::from_vec_and_domain(z, self.cs.domain.d1).interpolate();
574
575 Ok(res)
576 }
577}