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