kimchi/circuits/polynomials/varbasemul.rs
1//! This module implements short Weierstrass curve variable base scalar multiplication custom Plonk polynomials.
2//!
3//! ```ignore
4//! Acc := [2]T
5//! for i = n-1 ... 0:
6//! Q := (r_i == 1) ? T : -T
7//! Acc := Acc + (Q + Acc)
8//! ```
9//!
10//! See <https://github.com/zcash/zcash/issues/3924>
11//! and 3.1 of <https://arxiv.org/pdf/math/0208038.pdf> for details.
12use alloc::{string::String, vec, vec::Vec};
13
14use crate::circuits::{
15 argument::{Argument, ArgumentEnv, ArgumentType},
16 berkeley_columns::{BerkeleyChallengeTerm, Column},
17 expr::{constraints::ExprOps, Cache, Variable as VariableGen},
18 gate::{CircuitGate, CurrOrNext, GateType},
19 wires::{GateWires, COLUMNS},
20};
21use ark_ff::{FftField, PrimeField};
22use core::marker::PhantomData;
23use CurrOrNext::{Curr, Next};
24
25type Variable = VariableGen<Column>;
26
27//~ We implement custom Plonk constraints for short Weierstrass curve variable base scalar multiplication.
28//~
29//~ Given a finite field $\mathbb{F}_q$ of order $q$, if the order is not a multiple of 2 nor 3, then an
30//~ elliptic curve over $\mathbb{F}_q$ in short Weierstrass form is represented by the set of points $(x,y)$
31//~ that satisfy the following equation with $a,b\in\mathbb{F}_q$ and $4a^3+27b^2\neq_{\mathbb{F}_q} 0$:
32//~ $$E(\mathbb{F}_q): y^2 = x^3 + a x + b$$
33//~ If $P=(x_p, y_p)$ and $Q=(x_q, y_q)$ are two points in the curve $E(\mathbb{F}_q)$, the algorithm we
34//~ represent here computes the operation $2P+Q$ (point doubling and point addition) as $(P+Q)+Q$.
35//~
36//~ ```admonish info
37//~ Point $Q=(x_q, y_q)$ has nothing to do with the order $q$ of the field $\mathbb{F}_q$.
38//~ ```
39//~
40//~ The original algorithm that is being used can be found in the Section 3.1 of <https://arxiv.org/pdf/math/0208038.pdf>,
41//~ which can perform the above operation using 1 multiplication, 2 squarings and 2 divisions (one more squaring)
42//~ if $P=Q$), thanks to the fact that computing the $Y$-coordinate of the intermediate addition is not required.
43//~ This is more efficient to the standard algorithm that requires 1 more multiplication, 3 squarings in total and 2 divisions.
44//~
45//~ Moreover, this algorithm can be applied not only to the operation $2P+Q$, but any other scalar multiplication $kP$.
46//~ This can be done by expressing the scalar $k$ in biwise form and performing a double-and-add approach.
47//~ Nonetheless, this requires conditionals to differentiate $2P$ from $2P+Q$. For that reason, we will implement
48//~ the following pseudocode from <https://github.com/zcash/zcash/issues/3924> (where instead, they give a variant
49//~ of the above efficient algorithm for Montgomery curves $b\cdot y^2 = x^3 + a \cdot x^2 + x$).
50//~
51//~ ```ignore
52//~ Acc := [2]T
53//~ for i = n-1 ... 0:
54//~ Q := (k_{i + 1} == 1) ? T : -T
55//~ Acc := Acc + (Q + Acc)
56//~ return (k_0 == 0) ? Acc - P : Acc
57//~ ```
58//~
59//~ The layout of the witness requires 2 rows.
60//~ The i-th row will be a `VBSM` gate whereas the next row will be a `ZERO` gate.
61//~
62//~ | Row | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Type |
63//~ |-------|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|------|
64//~ | i | xT | yT | x0 | y0 | n | n' | | x1 | y1 | x2 | y2 | x3 | y3 | x4 | y4 | VBSM |
65//~ | i+1 | x5 | y5 | b0 | b1 | b2 | b3 | b4 | s0 | s1 | s2 | s3 | s4 | | | | ZERO |
66//~
67//~ The gate constraints take care of 5 bits of the scalar multiplication.
68//~ Each single bit consists of 4 constraints.
69//~ There is one additional constraint imposed on the final number.
70//~ Thus, the `VarBaseMul` gate argument requires 21 constraints.
71//~
72//~ For every bit, there will be one constraint meant to differentiate between addition and subtraction
73//~ for the operation $(P±T)+P$:
74//~
75//~ `S = (P + (b ? T : −T)) + P`
76//~
77//~ We follow these criteria:
78//~
79//~ * If the bit is positive, the sign should be a subtraction
80//~ * If the bit is negative, the sign should be an addition
81//~
82//~ Then, paraphrasing the above, we will represent this behavior as:
83//~
84//~ `S = (P - (2 * b - 1) * T ) + P`
85//~
86//~ Let us call `Input` the point with coordinates `(xI, yI)` and
87//~ `Target` is the point being added with coordinates `(xT, yT)`.
88//~ Then `Output` will be the point with coordinates `(xO, yO)` resulting from `O = ( I ± T ) + I`
89//~
90//~ ```admonish info
91//~ Do not confuse our `Output` point `(xO, yO)` with the point at infinity that is normally represented as $\mathcal{O}$.
92//~ ```
93//~
94//~ In each step of the algorithm, we consider the following elliptic curves affine arithmetic equations:
95//~
96//~ * $s_1 := \frac{y_i - (2\cdot b - 1) \cdot y_t}{x_i - x_t}$
97//~ * $s_2 := \frac{2 \cdot y_i}{2 * x_i + x_t - s_1^2} - s_1$
98//~ * $x_o := x_t + s_2^2 - s_1^2$
99//~ * $y_o := s_2 \cdot (x_i - x_o) - y_i$
100//~
101//~ For readability, we define the following 3 variables
102//~ in such a way that $s_2$ can be expressed as `u / t`:
103//~
104//~ * `rx` $:= s_1^2 - x_i - x_t$
105//~ * `t` $:= x_i - $ `rx` $ \iff 2 \cdot x_i - s_1^2 + x_t$
106//~ * `u` $:= 2 \cdot y_i - $ `t` $\cdot s_1 \iff 2 \cdot y_i - s_1 \cdot (2\cdot x_i - s^2_1 + x_t)$
107//~
108//~ Next, for each bit in the algorithm, we create the following 4 constraints that derive from the above:
109//~
110//~ * Booleanity check on the bit $b$:
111//~ `0 = b * b - b`
112//~ * Constrain $s_1$:
113//~ `(xI - xT) * s1 = yI – (2b - 1) * yT`
114//~ * Constrain `Output` $X$-coordinate $x_o$ and $s_2$:
115//~ `0 = u^2 - t^2 * (xO - xT + s1^2)`
116//~ * Constrain `Output` $Y$-coordinate $y_o$ and $s_2$:
117//~ `0 = (yO + yI) * t - (xI - xO) * u`
118//~
119//~ When applied to the 5 bits, the value of the `Target` point `(xT, yT)` is maintained,
120//~ whereas the values for the `Input` and `Output` points form the chain:
121//~
122//~ `[(x0, y0) -> (x1, y1) -> (x2, y2) -> (x3, y3) -> (x4, y4) -> (x5, y5)]`
123//~
124//~ Similarly, 5 different `s0..s4` are required, just like the 5 bits `b0..b4`.
125//~
126//~ Finally, the additional constraint makes sure that the scalar is being correctly expressed
127//~ into its binary form (using the double-and-add decomposition) as:
128//~ $$ n' = 2^5 \cdot n + 2^4 \cdot b_0 + 2^3 \cdot b_1 + 2^2 \cdot b_2 + 2^1 \cdot b_3 + b_4$$
129//~ This equation is translated as the constraint:
130//~
131//~ * Binary decomposition:
132//~ `0 = n' - (b4 + 2 * (b3 + 2 * (b2 + 2 * (b1 + 2 * (b0 + 2*n)))))`
133//~
134
135impl<F: PrimeField> CircuitGate<F> {
136 pub fn create_vbmul(wires: &[GateWires; 2]) -> Vec<Self> {
137 vec![
138 CircuitGate::new(GateType::VarBaseMul, wires[0], vec![]),
139 CircuitGate::new(GateType::Zero, wires[1], vec![]),
140 ]
141 }
142
143 /// Verify the `GateType::VarBaseMul`(TODO)
144 ///
145 /// # Errors
146 ///
147 /// TODO
148 pub fn verify_vbmul(&self, _row: usize, _witness: &[Vec<F>; COLUMNS]) -> Result<(), String> {
149 // TODO: implement
150 Ok(())
151 }
152
153 pub fn vbmul(&self) -> F {
154 if self.typ == GateType::VarBaseMul {
155 F::one()
156 } else {
157 F::zero()
158 }
159 }
160}
161
162#[derive(Copy, Clone)]
163struct Point<T> {
164 x: T,
165 y: T,
166}
167
168impl<T> Point<T> {
169 pub fn create(x: T, y: T) -> Self {
170 Point { x, y }
171 }
172}
173
174impl Point<Variable> {
175 pub fn new_from_env<F: PrimeField, T: ExprOps<F, BerkeleyChallengeTerm>>(
176 &self,
177 env: &ArgumentEnv<F, T>,
178 ) -> Point<T> {
179 Point::create(self.x.new_from_env(env), self.y.new_from_env(env))
180 }
181}
182
183fn set<F>(w: &mut [Vec<F>; COLUMNS], row0: usize, var: Variable, x: F) {
184 match var.col {
185 Column::Witness(i) => w[i][row0 + var.row.shift()] = x,
186 _ => panic!("Can only set witness columns"),
187 }
188}
189
190#[allow(clippy::too_many_arguments)]
191fn single_bit_witness<F: FftField>(
192 w: &mut [Vec<F>; COLUMNS],
193 row: usize,
194 b: Variable,
195 base: &Point<Variable>,
196 s1: Variable,
197 input: &Point<Variable>,
198 output: &Point<Variable>,
199 b_value: F,
200 base_value: (F, F),
201 input_value: (F, F),
202) -> (F, F) {
203 let mut set = |var, x| set(w, row, var, x);
204
205 set(b, b_value);
206 set(input.x, input_value.0);
207 set(input.y, input_value.1);
208
209 set(base.x, base_value.0);
210 set(base.y, base_value.1);
211
212 let s1_value = (input_value.1 - (base_value.1 * (b_value.double() - F::one())))
213 / (input_value.0 - base_value.0);
214
215 set(s1, s1_value);
216
217 let s1_squared = s1_value.square();
218
219 let s2 =
220 input_value.1.double() / (input_value.0.double() + base_value.0 - s1_squared) - s1_value;
221 let out_x = base_value.0 + s2.square() - s1_squared;
222 let out_y = (input_value.0 - out_x) * s2 - input_value.1;
223 set(output.x, out_x);
224 set(output.y, out_y);
225 (out_x, out_y)
226}
227
228fn single_bit<F: FftField, T: ExprOps<F, BerkeleyChallengeTerm>>(
229 cache: &mut Cache,
230 b: &T,
231 base: Point<T>,
232 s1: &T,
233 input: &Point<T>,
234 output: &Point<T>,
235) -> Vec<T> {
236 let b_sign = b.double() - T::one();
237
238 let s1_squared = cache.cache(s1.clone() * s1.clone());
239
240 // s1 = (input.y - (2b - 1) * base.y) / (input.x - base.x)
241 // s2 = 2*input.y / (2*input.x + base.x – s1^2) - s1
242 // output.x = base.x + s2^2 - s1^2
243 // output.y = (input.x – output.x) * s2 - input.y
244
245 let rx = s1_squared.clone() - input.x.clone() - base.x.clone();
246 let t = cache.cache(input.x.clone() - rx);
247 let u = cache.cache(input.y.double() - t.clone() * s1.clone());
248 // s2 = u / t
249
250 // output.x = base.x + s2^2 - s1^2
251 // <=>
252 // output.x = base.x + u^2 / t^2 - s1^2
253 // output.x - base.x + s1^2 = u^2 / t^2
254 // t^2 (output.x - base.x + s1^2) = u^2
255 //
256 // output.y = (input.x – output.x) * s2 - input.y
257 // <=>
258 // output.y = (input.x – output.x) * (u/t) - input.y
259 // output.y + input.y = (input.x – output.x) * (u/t)
260 // (output.y + input.y) * t = (input.x – output.x) * u
261
262 vec![
263 // boolean constrain the bit.
264 b.boolean(),
265 // constrain s1:
266 // (input.x - base.x) * s1 = input.y – (2b-1)*base.y
267 (input.x.clone() - base.x.clone()) * s1.clone() - (input.y.clone() - b_sign * base.y),
268 // constrain output.x
269 (u.clone() * u.clone())
270 - (t.clone() * t.clone()) * (output.x.clone() - base.x + s1_squared),
271 // constrain output.y
272 (output.y.clone() + input.y.clone()) * t - (input.x.clone() - output.x.clone()) * u,
273 ]
274}
275
276pub struct Layout<T> {
277 accs: [Point<T>; 6],
278 bits: [T; 5],
279 ss: [T; 5],
280 base: Point<T>,
281 n_prev: T,
282 n_next: T,
283}
284
285trait FromWitness<F, T>
286where
287 F: PrimeField,
288{
289 fn new_from_env(&self, env: &ArgumentEnv<F, T>) -> T;
290}
291
292impl<F, T> FromWitness<F, T> for Variable
293where
294 F: PrimeField,
295 T: ExprOps<F, BerkeleyChallengeTerm>,
296{
297 fn new_from_env(&self, env: &ArgumentEnv<F, T>) -> T {
298 let column_to_index = |_| match self.col {
299 Column::Witness(i) => i,
300 _ => panic!("Can't get index from witness columns"),
301 };
302
303 match self.row {
304 Curr => env.witness_curr(column_to_index(self.col)),
305 Next => env.witness_next(column_to_index(self.col)),
306 }
307 }
308}
309
310impl Layout<Variable> {
311 fn create() -> Self {
312 Layout {
313 accs: [
314 Point::create(v(Curr, 2), v(Curr, 3)), // (x0, y0)
315 Point::create(v(Curr, 7), v(Curr, 8)), // (x1, y1)
316 Point::create(v(Curr, 9), v(Curr, 10)), // (x2, y2)
317 Point::create(v(Curr, 11), v(Curr, 12)), // (x3, y3)
318 Point::create(v(Curr, 13), v(Curr, 14)), // (x4, y4)
319 Point::create(v(Next, 0), v(Next, 1)), // (x5, y5)
320 ],
321 // bits = [b0, b1, b2, b3, b4]
322 bits: [v(Next, 2), v(Next, 3), v(Next, 4), v(Next, 5), v(Next, 6)],
323
324 // ss = [ s0, s1, s2, s3, s4]
325 ss: [v(Next, 7), v(Next, 8), v(Next, 9), v(Next, 10), v(Next, 11)],
326
327 base: Point::create(v(Curr, 0), v(Curr, 1)), // (xT, yT)
328 n_prev: v(Curr, 4), // n
329 n_next: v(Curr, 5), // n'
330 }
331 }
332
333 fn new_from_env<F: PrimeField, T: ExprOps<F, BerkeleyChallengeTerm>>(
334 &self,
335 env: &ArgumentEnv<F, T>,
336 ) -> Layout<T> {
337 Layout {
338 accs: self.accs.map(|point| point.new_from_env(env)),
339 bits: self.bits.map(|var| var.new_from_env(env)),
340 ss: self.ss.map(|s| s.new_from_env(env)),
341 base: self.base.new_from_env(env),
342 n_prev: self.n_prev.new_from_env(env),
343 n_next: self.n_next.new_from_env(env),
344 }
345 }
346}
347
348// We lay things out like
349// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
350// xT yT x0 y0 n n' x1 y1 x2 y2 x3 y3 x4 y4
351// x5 y5 b0 b1 b2 b3 b4 s0 s1 s2 s3 s4
352const fn v(row: CurrOrNext, col: usize) -> Variable {
353 Variable {
354 row,
355 col: Column::Witness(col),
356 }
357}
358
359pub struct VarbaseMulResult<F> {
360 pub acc: (F, F),
361 pub n: F,
362}
363
364/// Apply the `witness` value.
365///
366/// # Panics
367///
368/// Will panic if `bits chunk` length validation fails.
369pub fn witness<F: FftField + core::fmt::Display>(
370 w: &mut [Vec<F>; COLUMNS],
371 row0: usize,
372 base: (F, F),
373 bits: &[bool],
374 acc0: (F, F),
375) -> VarbaseMulResult<F> {
376 let layout = Layout::create();
377 let bits: Vec<_> = bits.iter().map(|b| F::from(u64::from(*b))).collect();
378 let bits_per_chunk = 5;
379 assert_eq!(bits_per_chunk * (bits.len() / bits_per_chunk), bits.len());
380
381 let mut acc = acc0;
382 let mut n_acc = F::zero();
383 for (chunk, bs) in bits.chunks(bits_per_chunk).enumerate() {
384 let row = row0 + 2 * chunk;
385
386 set(w, row, layout.n_prev, n_acc);
387 for (i, bs) in bs.iter().enumerate().take(bits_per_chunk) {
388 n_acc.double_in_place();
389 n_acc += bs;
390 acc = single_bit_witness(
391 w,
392 row,
393 layout.bits[i],
394 &layout.base,
395 layout.ss[i],
396 &layout.accs[i],
397 &layout.accs[i + 1],
398 *bs,
399 base,
400 acc,
401 );
402 }
403 set(w, row, layout.n_next, n_acc);
404 }
405 VarbaseMulResult { acc, n: n_acc }
406}
407
408/// Implementation of the `VarbaseMul` gate
409#[derive(Default)]
410pub struct VarbaseMul<F>(PhantomData<F>);
411
412impl<F> Argument<F> for VarbaseMul<F>
413where
414 F: PrimeField,
415{
416 const ARGUMENT_TYPE: ArgumentType = ArgumentType::Gate(GateType::VarBaseMul);
417 const CONSTRAINTS: u32 = 21;
418
419 fn constraint_checks<T: ExprOps<F, BerkeleyChallengeTerm>>(
420 env: &ArgumentEnv<F, T>,
421 cache: &mut Cache,
422 ) -> Vec<T> {
423 let Layout {
424 base,
425 accs,
426 bits,
427 ss,
428 n_prev,
429 n_next,
430 } = Layout::create().new_from_env::<F, T>(env);
431
432 // n'
433 // = 2^5 * n + 2^4 b0 + 2^3 b1 + 2^2 b2 + 2^1 b3 + b4
434 // = b4 + 2 (b3 + 2 (b2 + 2 (b1 + 2(b0 + 2 n))))
435
436 let mut res = vec![n_next - bits.iter().fold(n_prev, |acc, b| b.clone() + acc.double())];
437
438 for i in 0..5 {
439 res.append(&mut single_bit(
440 cache,
441 &bits[i],
442 base.clone(),
443 &ss[i],
444 &accs[i],
445 &accs[i + 1],
446 ));
447 }
448
449 res
450 }
451}