kimchi/circuits/polynomials/endosclmul.rs
1//! This module implements short Weierstrass curve
2//! endomorphism optimised variable base
3//! scalar multiplication custom Plonk polynomials.
4
5use crate::{
6 circuits::{
7 argument::{Argument, ArgumentEnv, ArgumentType},
8 berkeley_columns::{BerkeleyChallengeTerm, BerkeleyChallenges},
9 constraints::ConstraintSystem,
10 expr::{
11 self,
12 constraints::{boolean, ExprOps},
13 Cache,
14 },
15 gate::{CircuitGate, GateType},
16 wires::{GateWires, COLUMNS},
17 },
18 curve::KimchiCurve,
19 proof::{PointEvaluations, ProofEvaluations},
20};
21use ark_ff::{Field, PrimeField};
22use core::marker::PhantomData;
23
24//~ We implement custom gate constraints for short Weierstrass curve
25//~ endomorphism optimised variable base scalar multiplication.
26//~
27//~ Given a finite field $\mathbb{F}_{q}$ of order $q$, if the order is not a multiple of 2 nor 3, then an
28//~ elliptic curve over $\mathbb{F}_{q}$ in short Weierstrass form is represented by the set of points $(x,y)$
29//~ that satisfy the following equation with
30//~ $a,b\in\mathbb{F}_{q}$
31//~ and
32//~ $4a^3+27b^2\neq_{\mathbb{F}_q} 0 $:
33//~ $$E(\mathbb{F}_q): y^2 = x^3 + a x + b$$
34//~ If $P=(x_p, y_p)$ and $T=(x_t, y_t)$ are two points in the curve $E(\mathbb{F}_q)$, the goal of this
35//~ operation is to perform the operation $2P±T$ efficiently as $(P±T)+P$.
36//~
37//~ `S = (P + (b ? T : −T)) + P`
38//~
39//~ The same algorithm can be used to perform other scalar multiplications, meaning it is
40//~ not restricted to the case $2\cdot P$, but it can be used for any arbitrary $k\cdot P$. This is done
41//~ by decomposing the scalar $k$ into its binary representation.
42//~ Moreover, for every step, there will be a one-bit constraint meant to differentiate between addition and subtraction
43//~ for the operation $(P±T)+P$:
44//~
45//~ In particular, the constraints of this gate take care of 4 bits of the scalar within a single EVBSM row.
46//~ When the scalar is longer (which will usually be the case), multiple EVBSM rows will be concatenated.
47//~
48//~ | Row | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Type |
49//~ |-------|----|----|----|----|----|----|----|-----|-----|-----|-----|-----|-----|-----|-----|-------|
50//~ | i | xT | yT | Ø | Ø | xP | yP | n | xR | yR | s1 | s3 | b1 | b2 | b3 | b4 | EVBSM |
51//~ | i+1 | = | = | | | xS | yS | n' | xR' | yR' | s1' | s3' | b1' | b2' | b3' | b4' | EVBSM |
52//~
53//~ The layout of this gate (and the next row) allows for this chained behavior where the output point
54//~ of the current row $S$ gets accumulated as one of the inputs of the following row, becoming $P$ in
55//~ the next constraints. Similarly, the scalar is decomposed into binary form and $n$ ($n'$ respectively)
56//~ will store the current accumulated value and the next one for the check.
57//~
58//~ For readability, we define the following variables for the constraints:
59//~
60//~ * `endo` $:=$ `EndoCoefficient`
61//~ * `xq1` $:= (1 + ($`endo`$ - 1)\cdot b_1) \cdot x_t$
62//~ * `xq2` $:= (1 + ($`endo`$ - 1)\cdot b_3) \cdot x_t$
63//~ * `yq1` $:= (2\cdot b_2 - 1) \cdot y_t$
64//~ * `yq2` $:= (2\cdot b_4 - 1) \cdot y_t$
65//~
66//~ These are the 11 constraints that correspond to each EVBSM gate,
67//~ which take care of 4 bits of the scalar within a single EVBSM row:
68//~
69//~ * First block:
70//~ * `(xq1 - xp) * s1 = yq1 - yp`
71//~ * `(2 * xp – s1^2 + xq1) * ((xp – xr) * s1 + yr + yp) = (xp – xr) * 2 * yp`
72//~ * `(yr + yp)^2 = (xp – xr)^2 * (s1^2 – xq1 + xr)`
73//~ * Second block:
74//~ * `(xq2 - xr) * s3 = yq2 - yr`
75//~ * `(2*xr – s3^2 + xq2) * ((xr – xs) * s3 + ys + yr) = (xr – xs) * 2 * yr`
76//~ * `(ys + yr)^2 = (xr – xs)^2 * (s3^2 – xq2 + xs)`
77//~ * Booleanity checks:
78//~ * Bit flag $b_1$: `0 = b1 * (b1 - 1)`
79//~ * Bit flag $b_2$: `0 = b2 * (b2 - 1)`
80//~ * Bit flag $b_3$: `0 = b3 * (b3 - 1)`
81//~ * Bit flag $b_4$: `0 = b4 * (b4 - 1)`
82//~ * Binary decomposition:
83//~ * Accumulated scalar: `n_next = 16 * n + 8 * b1 + 4 * b2 + 2 * b3 + b4`
84//~
85//~ The constraints above are derived from the following EC Affine arithmetic equations:
86//~
87//~ * (1) => $(x_{q_1} - x_p) \cdot s_1 = y_{q_1} - y_p$
88//~ * (2&3) => $(x_p – x_r) \cdot s_2 = y_r + y_p$
89//~ * (2) => $(2 \cdot x_p + x_{q_1} – s_1^2) \cdot (s_1 + s_2) = 2 \cdot y_p$
90//~ * <=> $(2 \cdot x_p – s_1^2 + x_{q_1}) \cdot ((x_p – x_r) \cdot s_1 + y_r + y_p) = (x_p – x_r) \cdot 2 \cdot y_p$
91//~ * (3) => $s_1^2 - s_2^2 = x_{q_1} - x_r$
92//~ * <=> $(y_r + y_p)^2 = (x_p – x_r)^2 \cdot (s_1^2 – x_{q_1} + x_r)$
93//~ *
94//~ * (4) => $(x_{q_2} - x_r) \cdot s_3 = y_{q_2} - y_r$
95//~ * (5&6) => $(x_r – x_s) \cdot s_4 = y_s + y_r$
96//~ * (5) => $(2 \cdot x_r + x_{q_2} – s_3^2) \cdot (s_3 + s_4) = 2 \cdot y_r$
97//~ * <=> $(2 \cdot x_r – s_3^2 + x_{q_2}) \cdot ((x_r – x_s) \cdot s_3 + y_s + y_r) = (x_r – x_s) \cdot 2 \cdot y_r$
98//~ * (6) => $s_3^2 – s_4^2 = x_{q_2} - x_s$
99//~ * <=> $(y_s + y_r)^2 = (x_r – x_s)^2 \cdot (s_3^2 – x_{q_2} + x_s)$
100//~
101//~ Defining $s_2$ and $s_4$ as
102//~
103//~ * $s_2 := \frac{2 \cdot y_P}{2 * x_P + x_T - s_1^2} - s_1$
104//~ * $s_4 := \frac{2 \cdot y_R}{2 * x_R + x_T - s_3^2} - s_3$
105//~
106//~ Gives the following equations when substituting the values of $s_2$ and $s_4$:
107//~
108//~ 1. `(xq1 - xp) * s1 = (2 * b1 - 1) * yt - yp`
109//~ 2. `(2 * xp – s1^2 + xq1) * ((xp – xr) * s1 + yr + yp) = (xp – xr) * 2 * yp`
110//~ 3. `(yr + yp)^2 = (xp – xr)^2 * (s1^2 – xq1 + xr)`
111//~
112//~ 4. `(xq2 - xr) * s3 = (2 * b2 - 1) * yt - yr`
113//~ 5. `(2 * xr – s3^2 + xq2) * ((xr – xs) * s3 + ys + yr) = (xr – xs) * 2 * yr`
114//~ 6. `(ys + yr)^2 = (xr – xs)^2 * (s3^2 – xq2 + xs)`
115//~
116
117/// Implementation of group endomorphism optimised
118/// variable base scalar multiplication custom Plonk constraints.
119impl<F: PrimeField> CircuitGate<F> {
120 pub fn create_endomul(wires: GateWires) -> Self {
121 CircuitGate::new(GateType::EndoMul, wires, vec![])
122 }
123
124 /// Verify the `EndoMul` gate.
125 ///
126 /// # Errors
127 ///
128 /// Will give error if `self.typ` is not `GateType::EndoMul`, or `constraint evaluation` fails.
129 pub fn verify_endomul<
130 const FULL_ROUNDS: usize,
131 G: KimchiCurve<FULL_ROUNDS, ScalarField = F>,
132 >(
133 &self,
134 row: usize,
135 witness: &[Vec<F>; COLUMNS],
136 cs: &ConstraintSystem<F>,
137 ) -> Result<(), String> {
138 ensure_eq!(self.typ, GateType::EndoMul, "incorrect gate type");
139
140 let this: [F; COLUMNS] = core::array::from_fn(|i| witness[i][row]);
141 let next: [F; COLUMNS] = core::array::from_fn(|i| witness[i][row + 1]);
142
143 let pt = F::from(123456u64);
144
145 let constants = expr::Constants {
146 mds: &G::sponge_params().mds,
147 endo_coefficient: cs.endo,
148 zk_rows: cs.zk_rows,
149 };
150 let challenges = BerkeleyChallenges {
151 alpha: F::zero(),
152 beta: F::zero(),
153 gamma: F::zero(),
154 joint_combiner: F::zero(),
155 };
156
157 let evals: ProofEvaluations<PointEvaluations<G::ScalarField>> =
158 ProofEvaluations::dummy_with_witness_evaluations(this, next);
159
160 let constraints = EndosclMul::constraints(&mut Cache::default());
161 for (i, c) in constraints.iter().enumerate() {
162 match c.evaluate_(cs.domain.d1, pt, &evals, &constants, &challenges) {
163 Ok(x) => {
164 if x != F::zero() {
165 return Err(format!("Bad endo equation {i}"));
166 }
167 }
168 Err(e) => return Err(format!("evaluation failed: {e}")),
169 }
170 }
171
172 Ok(())
173 }
174
175 pub fn endomul(&self) -> F {
176 if self.typ == GateType::EndoMul {
177 F::one()
178 } else {
179 F::zero()
180 }
181 }
182}
183
184/// Implementation of the `EndosclMul` gate.
185#[derive(Default)]
186pub struct EndosclMul<F>(PhantomData<F>);
187
188impl<F> Argument<F> for EndosclMul<F>
189where
190 F: PrimeField,
191{
192 const ARGUMENT_TYPE: ArgumentType = ArgumentType::Gate(GateType::EndoMul);
193 const CONSTRAINTS: u32 = 11;
194
195 fn constraint_checks<T: ExprOps<F, BerkeleyChallengeTerm>>(
196 env: &ArgumentEnv<F, T>,
197 cache: &mut Cache,
198 ) -> Vec<T> {
199 let b1 = env.witness_curr(11);
200 let b2 = env.witness_curr(12);
201 let b3 = env.witness_curr(13);
202 let b4 = env.witness_curr(14);
203
204 let xt = env.witness_curr(0);
205 let yt = env.witness_curr(1);
206
207 let xs = env.witness_next(4);
208 let ys = env.witness_next(5);
209
210 let xp = env.witness_curr(4);
211 let yp = env.witness_curr(5);
212
213 let xr = env.witness_curr(7);
214 let yr = env.witness_curr(8);
215
216 let s1 = env.witness_curr(9);
217 let s3 = env.witness_curr(10);
218
219 let endo_minus_1 = env.endo_coefficient() - T::one();
220 let xq1 = cache.cache((T::one() + b1.clone() * endo_minus_1.clone()) * xt.clone());
221 let xq2 = cache.cache((T::one() + b3.clone() * endo_minus_1) * xt);
222
223 let yq1 = (b2.double() - T::one()) * yt.clone();
224 let yq2 = (b4.double() - T::one()) * yt;
225
226 let s1_squared = cache.cache(s1.square());
227 let s3_squared = cache.cache(s3.square());
228
229 // n_next = 16*n + 8*b1 + 4*b2 + 2*b3 + b4
230 let n = env.witness_curr(6);
231 let n_next = env.witness_next(6);
232 let n_constraint =
233 (((n.double() + b1.clone()).double() + b2.clone()).double() + b3.clone()).double()
234 + b4.clone()
235 - n_next;
236
237 let xp_xr = cache.cache(xp.clone() - xr.clone());
238 let xr_xs = cache.cache(xr.clone() - xs.clone());
239
240 let ys_yr = cache.cache(ys + yr.clone());
241 let yr_yp = cache.cache(yr.clone() + yp.clone());
242
243 vec![
244 // verify booleanity of the scalar bits
245 boolean(&b1),
246 boolean(&b2),
247 boolean(&b3),
248 boolean(&b4),
249 // (xq1 - xp) * s1 = yq1 - yp
250 ((xq1.clone() - xp.clone()) * s1.clone()) - (yq1 - yp.clone()),
251 // (2*xp – s1^2 + xq1) * ((xp - xr) * s1 + yr + yp) = (xp - xr) * 2*yp
252 (((xp.double() - s1_squared.clone()) + xq1.clone())
253 * ((xp_xr.clone() * s1) + yr_yp.clone()))
254 - (yp.double() * xp_xr.clone()),
255 // (yr + yp)^2 = (xp – xr)^2 * (s1^2 – xq1 + xr)
256 yr_yp.square() - (xp_xr.square() * ((s1_squared - xq1) + xr.clone())),
257 // (xq2 - xr) * s3 = yq2 - yr
258 ((xq2.clone() - xr.clone()) * s3.clone()) - (yq2 - yr.clone()),
259 // (2*xr – s3^2 + xq2) * ((xr – xs) * s3 + ys + yr) = (xr - xs) * 2*yr
260 (((xr.double() - s3_squared.clone()) + xq2.clone())
261 * ((xr_xs.clone() * s3) + ys_yr.clone()))
262 - (yr.double() * xr_xs.clone()),
263 // (ys + yr)^2 = (xr – xs)^2 * (s3^2 – xq2 + xs)
264 ys_yr.square() - (xr_xs.square() * ((s3_squared - xq2) + xs)),
265 n_constraint,
266 ]
267 }
268}
269
270/// The result of performing an endoscaling: the accumulated curve point
271/// and scalar.
272pub struct EndoMulResult<F> {
273 pub acc: (F, F),
274 pub n: F,
275}
276
277/// Generates the `witness_curr` values for a series of endoscaling constraints.
278///
279/// # Panics
280///
281/// Will panic if `bits` length does not match the requirement.
282pub fn gen_witness<F: Field + core::fmt::Display>(
283 w: &mut [Vec<F>; COLUMNS],
284 row0: usize,
285 endo: F,
286 base: (F, F),
287 bits: &[bool],
288 acc0: (F, F),
289) -> EndoMulResult<F> {
290 let bits_per_row = 4;
291 let rows = bits.len() / 4;
292 assert_eq!(0, bits.len() % 4);
293
294 let bits: Vec<_> = bits.iter().map(|x| F::from(u64::from(*x))).collect();
295 let one = F::one();
296
297 let mut acc = acc0;
298 let mut n_acc = F::zero();
299
300 // TODO: Could be more efficient
301 for i in 0..rows {
302 let b1 = bits[i * bits_per_row];
303 let b2 = bits[i * bits_per_row + 1];
304 let b3 = bits[i * bits_per_row + 2];
305 let b4 = bits[i * bits_per_row + 3];
306
307 let (xt, yt) = base;
308 let (xp, yp) = acc;
309
310 let xq1 = (one + (endo - one) * b1) * xt;
311 let yq1 = (b2.double() - one) * yt;
312
313 let s1 = (yq1 - yp) / (xq1 - xp);
314 let s1_squared = s1.square();
315 // (2*xp – s1^2 + xq) * ((xp – xr) * s1 + yr + yp) = (xp – xr) * 2*yp
316 // => 2 yp / (2*xp – s1^2 + xq) = s1 + (yr + yp) / (xp – xr)
317 // => 2 yp / (2*xp – s1^2 + xq) - s1 = (yr + yp) / (xp – xr)
318 //
319 // s2 := 2 yp / (2*xp – s1^2 + xq) - s1
320 //
321 // (yr + yp)^2 = (xp – xr)^2 * (s1^2 – xq1 + xr)
322 // => (s1^2 – xq1 + xr) = (yr + yp)^2 / (xp – xr)^2
323 //
324 // => xr = s2^2 - s1^2 + xq
325 // => yr = s2 * (xp - xr) - yp
326 let s2 = yp.double() / (xp.double() + xq1 - s1_squared) - s1;
327
328 // (xr, yr)
329 let xr = xq1 + s2.square() - s1_squared;
330 let yr = (xp - xr) * s2 - yp;
331
332 let xq2 = (one + (endo - one) * b3) * xt;
333 let yq2 = (b4.double() - one) * yt;
334 let s3 = (yq2 - yr) / (xq2 - xr);
335 let s3_squared = s3.square();
336 let s4 = yr.double() / (xr.double() + xq2 - s3_squared) - s3;
337
338 let xs = xq2 + s4.square() - s3_squared;
339 let ys = (xr - xs) * s4 - yr;
340
341 let row = i + row0;
342
343 w[0][row] = base.0;
344 w[1][row] = base.1;
345 w[4][row] = xp;
346 w[5][row] = yp;
347 w[6][row] = n_acc;
348 w[7][row] = xr;
349 w[8][row] = yr;
350 w[9][row] = s1;
351 w[10][row] = s3;
352 w[11][row] = b1;
353 w[12][row] = b2;
354 w[13][row] = b3;
355 w[14][row] = b4;
356
357 acc = (xs, ys);
358
359 n_acc.double_in_place();
360 n_acc += b1;
361 n_acc.double_in_place();
362 n_acc += b2;
363 n_acc.double_in_place();
364 n_acc += b3;
365 n_acc.double_in_place();
366 n_acc += b4;
367 }
368 w[4][row0 + rows] = acc.0;
369 w[5][row0 + rows] = acc.1;
370 w[6][row0 + rows] = n_acc;
371
372 EndoMulResult { acc, n: n_acc }
373}