commonware_cryptography/banderwagon.rs
1use crate::{
2 bls12381::primitives::group::{Scalar, ScalarReadCfg},
3 zk::circuit::{BoolVar, Context, Selector, Var},
4};
5#[cfg(not(feature = "std"))]
6use alloc::vec::Vec;
7use blst::blst_fr;
8use bytes::{Buf, BufMut};
9use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt, Write};
10use commonware_math::algebra::{
11 msm_naive, Additive, CryptoGroup, Field, HashToGroup, Multiplicative, Object, Random, Ring,
12 Space,
13};
14use commonware_parallel::Strategy;
15use core::{
16 array,
17 ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
18};
19
20/// A scalar exponent for the Banderwagon group [`G`]: an element of the scalar
21/// field `Z/r`, where `r` is the prime group order.
22///
23/// Stored as a canonical little-endian integer in `[0, r)`. The in-circuit
24/// secret is witnessed directly as its [`bits`](F::bits) (`F::BITS` of them,
25/// enough to cover all of `[0, r)`), with no recomposition or canonicity
26/// constraint (see `G::scalar_mul_bits`): the bits simply *are* the exponent.
27/// A witnessed bit pattern is not itself constrained to `[0, r)` and so may
28/// exceed `r`, but that is harmless: every use feeds the bits into a group
29/// scalar multiplication, which is well-defined modulo `r`, so any alias of the
30/// same residue yields the same result.
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
32pub struct F {
33 limbs: [u64; 4],
34}
35
36impl F {
37 /// The bit-width of the modulus `r`; enough bits to represent any element of
38 /// `[0, r)`. See [`F`].
39 const BITS: usize = 253;
40
41 /// The field modulus `r`, the prime order of the Banderwagon group, as
42 /// little-endian 64-bit limbs. Every [`F`] value is kept canonical in
43 /// `[0, r)`, so `PartialEq` (derived limb equality) is correct.
44 const R: [u64; 4] = [
45 0x74fd_06b5_2876_e7e1,
46 0xff8f_8700_7419_0471,
47 0x0cce_7602_0268_7600,
48 0x1cfb_69d4_ca67_5f52,
49 ];
50
51 /// The little-endian bits of the integer, `F::BITS` of them (enough to
52 /// represent any element of `[0, r)`).
53 pub fn bits(&self) -> Vec<bool> {
54 (0..Self::BITS)
55 .map(|i| (self.limbs[i / 64] >> (i % 64)) & 1 == 1)
56 .collect()
57 }
58}
59
60impl Random for F {
61 fn random(mut rng: impl rand_core::CryptoRng) -> Self {
62 // Rejection-sample a uniform element of `[0, r)`. Each candidate is
63 // `F::BITS` random bits (the width of `r`), so it lands in `[0, 2^BITS)`
64 // and is accepted with probability `r / 2^BITS > 0.9`: ~1.1 draws on
65 // average.
66 loop {
67 let mut limbs: [u64; 4] = array::from_fn(|_| rng.next_u64());
68 limbs[3] &= (1u64 << (Self::BITS - 192)) - 1;
69 // Accept iff `limbs < r`, i.e. computing `limbs - r` borrows.
70 let mut borrow = false;
71 for (&x, &r) in limbs.iter().zip(Self::R.iter()) {
72 let (d, b) = x.overflowing_sub(r);
73 let (_, c) = d.overflowing_sub(borrow as u64);
74 borrow = b | c;
75 }
76 if borrow {
77 return Self { limbs };
78 }
79 }
80 }
81}
82
83impl<'a> AddAssign<&'a Self> for F {
84 fn add_assign(&mut self, rhs: &'a Self) {
85 // Add the two canonical `< r` integers limb by limb with carry. The sum
86 // is `< 2r < 2^254`, so it never overflows the 256-bit `limbs`.
87 let mut sum = [0u64; 4];
88 let mut carry = false;
89 for (s, (&x, &y)) in sum.iter_mut().zip(self.limbs.iter().zip(rhs.limbs.iter())) {
90 let (a, b) = x.overflowing_add(y);
91 let (a, c) = a.overflowing_add(carry as u64);
92 *s = a;
93 carry = b | c;
94 }
95 // Reduce: since `sum < 2r`, subtracting `r` at most once is enough.
96 // Compute `sum - r`; a final borrow means `sum < r`, so we keep `sum`.
97 let mut diff = [0u64; 4];
98 let mut borrow = false;
99 for (d, (&s, &r)) in diff.iter_mut().zip(sum.iter().zip(Self::R.iter())) {
100 let (x, b) = s.overflowing_sub(r);
101 let (x, c) = x.overflowing_sub(borrow as u64);
102 *d = x;
103 borrow = b | c;
104 }
105 self.limbs = if borrow { sum } else { diff };
106 }
107}
108
109impl<'a> Add<&'a Self> for F {
110 type Output = Self;
111
112 fn add(mut self, rhs: &'a Self) -> Self::Output {
113 self += rhs;
114 self
115 }
116}
117
118impl Neg for F {
119 type Output = Self;
120
121 fn neg(self) -> Self::Output {
122 // `-0 = 0`; otherwise `r - self`, which lies in `[1, r)` and is canonical.
123 if self.limbs == [0u64; 4] {
124 return self;
125 }
126 let mut diff = [0u64; 4];
127 let mut borrow = false;
128 for (d, (&r, &x)) in diff.iter_mut().zip(Self::R.iter().zip(self.limbs.iter())) {
129 let (v, b) = r.overflowing_sub(x);
130 let (v, c) = v.overflowing_sub(borrow as u64);
131 *d = v;
132 borrow = b | c;
133 }
134 Self { limbs: diff }
135 }
136}
137
138impl<'a> SubAssign<&'a Self> for F {
139 fn sub_assign(&mut self, rhs: &'a Self) {
140 let rhs = -rhs.clone();
141 *self += &rhs;
142 }
143}
144
145impl<'a> Sub<&'a Self> for F {
146 type Output = Self;
147
148 fn sub(mut self, rhs: &'a Self) -> Self::Output {
149 self -= rhs;
150 self
151 }
152}
153
154impl<'a> MulAssign<&'a Self> for F {
155 fn mul_assign(&mut self, rhs: &'a Self) {
156 // `a * b mod r` is `a` added to itself `b` times in the additive group,
157 // which `Additive::scale` computes by double-and-add. Each step reduces
158 // mod `r`, so the result is canonical.
159 //
160 // This is deliberately slow (~380 modular additions per multiply, ~100x
161 // a schoolbook mul) and not constant-time (the loop branches on the bits
162 // of `rhs`). That is fine for our use: the only production caller is the
163 // eVRF signing response `s = e * x + k`, whose cost is dwarfed by the
164 // accompanying `G` scalar multiplication, and whose surrounding native
165 // code is variable-time regardless. To improve when needed: replace with
166 // schoolbook 4x4-limb multiplication into a 512-bit product followed by a
167 // Montgomery (or Barrett) reduction, and make it constant-time if a
168 // caller ever multiplies secret material on a timing-sensitive path.
169 *self = self.scale(&rhs.limbs);
170 }
171}
172
173impl<'a> Mul<&'a Self> for F {
174 type Output = Self;
175
176 fn mul(mut self, rhs: &'a Self) -> Self::Output {
177 self *= rhs;
178 self
179 }
180}
181
182impl Object for F {}
183
184impl Additive for F {
185 fn zero() -> Self {
186 Self { limbs: [0u64; 4] }
187 }
188}
189
190impl Multiplicative for F {}
191
192impl Ring for F {
193 fn one() -> Self {
194 Self {
195 limbs: [1, 0, 0, 0],
196 }
197 }
198}
199
200impl Field for F {
201 fn inv(&self) -> Self {
202 // Fermat: for prime `r`, `a^(r-2) = a^-1` for nonzero `a`, and the
203 // square-and-multiply leaves `0^(r-2) = 0`, matching the trait contract.
204 // `r - 2` only borrows from the lowest limb (`R[0]` ends in `...e1`).
205 self.exp(&[Self::R[0] - 2, Self::R[1], Self::R[2], Self::R[3]])
206 }
207}
208
209impl FixedSize for F {
210 const SIZE: usize = 32;
211}
212
213impl Write for F {
214 fn write(&self, buf: &mut impl BufMut) {
215 // Little-endian canonical integer, one limb at a time.
216 for limb in self.limbs {
217 buf.put_u64_le(limb);
218 }
219 }
220}
221
222impl Read for F {
223 type Cfg = ();
224
225 fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
226 let bytes = <[u8; 32]>::read(buf)?;
227 let limbs =
228 array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()));
229 // Reject non-canonical encodings (`>= r`) so the `[0, r)` invariant that
230 // equality and arithmetic rely on holds for values from untrusted input.
231 // Canonical iff computing `limbs - r` borrows.
232 let mut borrow = false;
233 for (&x, &r) in limbs.iter().zip(Self::R.iter()) {
234 let (d, b) = x.overflowing_sub(r);
235 let (_, c) = d.overflowing_sub(borrow as u64);
236 borrow = b | c;
237 }
238 if !borrow {
239 return Err(CodecError::Invalid(
240 "banderwagon::F",
241 "scalar not canonical",
242 ));
243 }
244 Ok(Self { limbs })
245 }
246}
247
248#[cfg(any(test, feature = "arbitrary"))]
249impl arbitrary::Arbitrary<'_> for F {
250 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
251 // Reduce an arbitrary 256-bit integer mod `r` via the additive
252 // double-and-add (`1` scaled by the integer), keeping it canonical.
253 let limbs: [u64; 4] = [
254 u.arbitrary()?,
255 u.arbitrary()?,
256 u.arbitrary()?,
257 u.arbitrary()?,
258 ];
259 Ok(Self::one().scale(&limbs))
260 }
261}
262
263/// The Bandersnatch twisted Edwards `a` coefficient, `-5`, in Montgomery form.
264#[allow(dead_code)]
265const A: Scalar = Scalar(blst_fr {
266 l: [
267 0xffff_fff4_0000_000c,
268 0xece3_b023_ffec_4ff3,
269 0x66b6_2060_7396_203f,
270 0x6f23_d7e5_f361_df62,
271 ],
272});
273
274/// The Bandersnatch twisted Edwards `d` coefficient in Montgomery form.
275#[allow(dead_code)]
276const D: Scalar = Scalar(blst_fr {
277 l: [
278 0xa8dc_ed1b_47a2_c730,
279 0x381c_065a_ad3c_ccc7,
280 0x53ff_52e1_1883_51f8,
281 0x362e_8d63_990f_e940,
282 ],
283});
284
285// Banderwagon group structure:
286//
287// `G` holds a point on the Bandersnatch twisted Edwards curve
288// (`a*x^2 + y^2 = 1 + d*x^2*y^2`, with `a = -5`). The full curve group is not
289// prime order; it is isomorphic to `Z/2 x Z/2 x Z/r` (cofactor 4, `r` the large
290// prime). The cofactor part is the "2-torsion" subgroup `{O, (0,-1), S, S'}`,
291// where `(0,-1)` has order 2 and `S`, `S'` are the two other order-2 points.
292//
293// Banderwagon turns this into a clean prime-order group of size `r` in two steps:
294//
295// 1. Restrict to the subgroup of order `2r`, i.e. the prime-order subgroup
296// together with its coset by `(0,-1)`. The order-2 points `S`, `S'` (and
297// anything built from them) are *not* in this subgroup. Membership is
298// enforced by a "subgroup check": `1 - a*x^2` must be a square in the base
299// field (equivalently, in projective form, `z^2 - a*x^2`, avoiding an
300// inversion). This is only needed when decoding an untrusted point; values
301// produced internally (the generator plus the group law) never leave this
302// subgroup, so we don't perform it here yet.
303// 2. Quotient that `2r` subgroup by the order-2 subgroup `{O, (0,-1)}`, which
304// collapses it to size `r`. Concretely this identifies each point `P` with
305// `P + (0,-1)`; since adding `(0,-1)` maps `(x, y)` to `(-x, -y)`, the two
306// representatives of every group element are `(x, y)` and `(-x, -y)`.
307//
308// So a single group element has many in-memory representations (any projective
309// scaling of either of its two affine representatives), and equality (below)
310// must see through all of them.
311
312/// Represents a point in the Banderwagon group.
313///
314/// This group is defined over the BLS12-381 [`Scalar`] field.
315/// Because of that, we can efficiently use it in ZK proofs using BLS.
316#[derive(Clone, Debug)]
317pub struct G {
318 // We use a projective representation where xy = tz.
319 x: Scalar,
320 y: Scalar,
321 t: Scalar,
322 z: Scalar,
323}
324
325impl PartialEq for G {
326 fn eq(&self, other: &Self) -> bool {
327 // See the group-structure notes above `struct G`. Two representations are
328 // the same group element iff their affine `x:y` ratios match, i.e.
329 // `x1/z1 * y2/z2 == x2/z2 * y1/z1`. Clearing the `z` factors (they cancel)
330 // gives `x1 * y2 == x2 * y1`. This single check absorbs both sources of
331 // redundancy:
332 //
333 // - projective scaling `(x,y,t,z)` vs `(λx,λy,λt,λz)`: the `λ`s cancel;
334 // - the `(0,-1)` quotient `(x,y)` vs `(-x,-y)`: the signs cancel
335 // (`x * -y == -x * y`).
336 //
337 // (Soundness relies on both points being valid subgroup elements; for
338 // points off the curve or outside the `2r` subgroup this is meaningless.)
339 self.x.clone() * &other.y == other.x.clone() * &self.y
340 }
341}
342
343impl Eq for G {}
344
345impl CryptoGroup for G {
346 type Scalar = F;
347
348 /// Returns the prime-order Bandersnatch generator in extended coordinates.
349 fn generator() -> Self {
350 Self {
351 // x = 0x29c132cc2c0b34c5743711777bbe42f32b79c022ad998465e1e71866a252ae18
352 x: Scalar(blst_fr {
353 l: [
354 0xec26_27e1_e7ab_47f5,
355 0x3e63_de48_4f01_aa9c,
356 0xfe0f_5c3b_5394_6dc4,
357 0x2d71_920b_aeb2_cfcd,
358 ],
359 }),
360 // y = 0x2a6c669eda123e0f157d8b50badcd586358cad81eee464605e3167b6cc974166
361 y: Scalar(blst_fr {
362 l: [
363 0x4e30_593e_1895_bd34,
364 0x156d_738f_32af_be4b,
365 0x45ef_0b1c_cdeb_75f4,
366 0x6a7c_ca00_37d2_e71f,
367 ],
368 }),
369 // t = x * y = 0x5e61c8a110562844571f0fdc470ac5ea53e51c121b538d00e2594f7a0d4781ab
370 t: Scalar(blst_fr {
371 l: [
372 0x5a92_e8f6_97ad_b6b9,
373 0xf138_8d46_06b1_4609,
374 0x101c_7836_40a6_4516,
375 0x1e9a_e707_3cc7_a9fc,
376 ],
377 }),
378 // z = 0x1
379 z: Scalar(blst_fr {
380 l: [
381 0x0000_0001_ffff_fffe,
382 0x5884_b7fa_0003_4802,
383 0x998c_4fef_ecbc_4ff5,
384 0x1824_b159_acc5_056f,
385 ],
386 }),
387 }
388 }
389}
390
391impl Object for G {}
392
393impl<'a> AddAssign<&'a Self> for G {
394 fn add_assign(&mut self, rhs: &'a Self) {
395 // A bit of a trick to take ownership of the fields.
396 let Self {
397 x: x1,
398 y: y1,
399 t: t1,
400 z: z1,
401 } = core::mem::replace(self, Self::zero());
402 // These are by reference.
403 let Self {
404 x: x2,
405 y: y2,
406 t: t2,
407 z: z2,
408 } = rhs;
409
410 let x1_y2 = x1.clone() * y2;
411 let y1_x2 = y1.clone() * x2;
412 let y1_y2 = y1 * y2;
413 let x1_x2 = x1 * x2;
414 let z1_z2 = z1 * z2;
415 let t1_t2 = t1 * t2;
416
417 let x1_y2_plus_y1_x2 = x1_y2 + &y1_x2;
418 let y1_y2_minus_a_x1_x2 = y1_y2 - &(x1_x2 * &A);
419 let d_t1_t2 = t1_t2 * &D;
420 let z_minus_d_t = z1_z2.clone() - &d_t1_t2;
421 let z_plus_d_t = z1_z2 + &d_t1_t2;
422
423 *self = Self {
424 x: x1_y2_plus_y1_x2.clone() * &z_minus_d_t,
425 y: y1_y2_minus_a_x1_x2.clone() * &z_plus_d_t,
426 t: x1_y2_plus_y1_x2 * &y1_y2_minus_a_x1_x2,
427 z: z_minus_d_t * &z_plus_d_t,
428 }
429 }
430}
431
432impl<'a> Add<&'a Self> for G {
433 type Output = Self;
434
435 fn add(mut self, rhs: &'a Self) -> Self::Output {
436 self += rhs;
437 self
438 }
439}
440
441impl<'a> SubAssign<&'a Self> for G {
442 fn sub_assign(&mut self, rhs: &'a Self) {
443 let rhs = -rhs.clone();
444 *self += &rhs;
445 }
446}
447
448impl<'a> Sub<&'a Self> for G {
449 type Output = Self;
450
451 fn sub(mut self, rhs: &'a Self) -> Self::Output {
452 self -= rhs;
453 self
454 }
455}
456
457impl Neg for G {
458 type Output = Self;
459
460 fn neg(self) -> Self::Output {
461 Self {
462 x: -self.x,
463 y: self.y,
464 t: -self.t,
465 z: self.z,
466 }
467 }
468}
469
470impl Additive for G {
471 fn zero() -> Self {
472 Self {
473 x: Scalar::zero(),
474 y: Scalar::one(),
475 t: Scalar::zero(),
476 z: Scalar::one(),
477 }
478 }
479
480 fn double(&mut self) {
481 let x_sq = {
482 let mut out = self.x.clone();
483 out.square();
484 out
485 };
486
487 let y_sq = {
488 let mut out = self.y.clone();
489 out.square();
490 out
491 };
492
493 let z_sq_twice = {
494 let mut out = self.z.clone();
495 out.square();
496 out.double();
497 out
498 };
499
500 let a_x_sq = x_sq.clone() * &A;
501
502 let x_plus_y_sq = {
503 let mut out = self.x.clone() + &self.y;
504 out.square();
505 out -= &x_sq;
506 out -= &y_sq;
507 out
508 };
509
510 let g = a_x_sq.clone() + &y_sq;
511 let f = g.clone() - &z_sq_twice;
512 let h = a_x_sq - &y_sq;
513
514 self.x = x_plus_y_sq.clone() * &f;
515 self.y = g.clone() * &h;
516 self.t = x_plus_y_sq * &h;
517 self.z = f * &g;
518 }
519}
520
521impl<'a> MulAssign<&'a F> for G {
522 fn mul_assign(&mut self, rhs: &'a F) {
523 *self = self.scale(&rhs.limbs);
524 }
525}
526
527impl<'a> Mul<&'a F> for G {
528 type Output = Self;
529
530 fn mul(mut self, rhs: &'a F) -> Self::Output {
531 self *= rhs;
532 self
533 }
534}
535
536impl Space<F> for G {
537 fn msm(points: &[Self], scalars: &[F], _strategy: &impl Strategy) -> Self {
538 msm_naive(points, scalars)
539 }
540}
541
542// Banderwagon (de)serialization. See the spec for full details:
543// <https://hackmd.io/@6iQDuIePQjyYBqDChYw_jg/BJBNcv9fq> and the implementation
544// notes <https://hackmd.io/wliPP_RMT4emsucVuCqfHA>.
545//
546// A group element is encoded as the single base-field element
547// `u = (X/Z) * Sign(Y/Z)`, written big-endian. Multiplying by `Sign(Y/Z)` makes
548// `P` and its quotient twin `P + (0,-1) = (-X,-Y)` (see notes above `struct G`)
549// serialize to the same bytes, since negating the point negates both `X` and
550// `Y`, leaving `u` unchanged. The sign, subgroup, and square-root helpers used
551// here live on [`Scalar`].
552
553impl Write for G {
554 fn write(&self, buf: &mut impl BufMut) {
555 // Convert to affine `(x, y) = (X/Z, Y/Z)`; `Z != 0` for all elements of
556 // the subgroup we represent.
557 let z_inv = self.z.inv();
558 let x = self.x.clone() * &z_inv;
559 let y = self.y.clone() * &z_inv;
560 // `u = x * Sign(y)`: keep `x` when `y` is the positive (largest)
561 // representative, otherwise negate it.
562 let u = if y.is_positive() { x } else { -x };
563 u.write(buf);
564 }
565}
566
567impl FixedSize for G {
568 const SIZE: usize = Scalar::SIZE;
569}
570
571impl G {
572 /// The affine coordinates `(x/z, y/z)` of this representative.
573 fn affine(&self) -> (Scalar, Scalar) {
574 let z_inv = self.z.inv();
575 (self.x.clone() * &z_inv, self.y.clone() * &z_inv)
576 }
577
578 /// Out-of-circuit squared affine x-coordinate of `[scalar] * self`.
579 ///
580 /// We expose the *squared* abscissa rather than the bare one because the
581 /// quotient identifies `(x, y)` with `(-x, -y)`: a plain x-coordinate flips
582 /// sign between the two representatives, whereas its square is a well-defined
583 /// function of the group element (no representative to agree on, no
584 /// `canonicalize` needed). The native counterpart of
585 /// the in-circuit [`scalar_mul_x_squared`](Self::scalar_mul_x_squared).
586 pub fn scalar_mul_x_squared_base(&self, scalar: &Scalar) -> Scalar {
587 let (mut x, _) = self.scale(&scalar_limbs(scalar)).affine();
588 x.square();
589 x
590 }
591
592 /// Out-of-circuit squared affine x-coordinate of `[x] * self` for an [`F`]
593 /// exponent — the native counterpart of
594 /// [`scalar_mul_x_squared_base`](Self::scalar_mul_x_squared_base), the
595 /// square is representative-independent.
596 pub fn scalar_mul_x_squared_f(&self, x: &F) -> Scalar {
597 let (mut a, _) = (self.clone() * x).affine();
598 a.square();
599 a
600 }
601
602 /// Return the canonical representative of this group element.
603 ///
604 /// A Banderwagon element has two affine representatives `(x, y)` and
605 /// `(-x, -y)`; this picks the one with positive `y` (the same choice
606 /// [`G::from_x`] makes when decoding), so that two in-memory values for the
607 /// same element become bit-identical. Used by [`GVar::constant`] so that a
608 /// point folded into a circuit yields representative-independent constants.
609 fn canonicalize(&self) -> Self {
610 let (x, y) = self.affine();
611 let u = if y.is_positive() { x } else { -x };
612 Self::from_x(u).expect("a valid subgroup element re-derives from its abscissa")
613 }
614
615 /// Recovers the group element whose serialization has abscissa `x`.
616 ///
617 /// Returns `None` if `x` is not the serialization of an element of the
618 /// subgroup we represent (i.e. it fails the subgroup check or lies off the
619 /// curve). This is the shared core of both [`Read`] and
620 /// [`hash_to_group`](HashToGroup::hash_to_group).
621 fn from_x(x: Scalar) -> Option<Self> {
622 let one = Scalar::one();
623 let x_sq = {
624 let mut out = x.clone();
625 out.square();
626 out
627 };
628
629 // `num = 1 - a*x^2`, `den = 1 - d*x^2`, and `y^2 = num / den`.
630 let num = one.clone() - &(x_sq.clone() * &A);
631 let den = one.clone() - &(x_sq * &D);
632
633 // Subgroup check: `1 - a*x^2` must be a square (see notes above `struct G`).
634 if !num.is_square() {
635 return None;
636 }
637
638 // Recover `y` and pick the positive (largest) representative. `sqrt`
639 // returning `None` means `x` is not a valid abscissa (point off-curve).
640 let ratio = num * &den.inv();
641 let mut y = ratio.sqrt()?;
642 if !y.is_positive() {
643 y = -y;
644 }
645
646 let t = x.clone() * &y;
647 Some(Self { x, y, t, z: one })
648 }
649}
650
651impl HashToGroup for G {
652 /// Hashes `(domain_separator, message)` to a Banderwagon point.
653 ///
654 /// Uses try-and-increment: for `counter = 0, 1, 2, ...` we derive a candidate
655 /// abscissa `x = H(domain_separator, message || counter)` and return the
656 /// first one that is a valid subgroup serialization. Because the
657 /// serialization is a bijection between group elements and valid abscissae,
658 /// and each candidate is an independent uniform field element, the result is
659 /// a uniformly random group element whose discrete log w.r.t. the generator
660 /// is unknown.
661 ///
662 /// We deliberately choose this over a constant-time map (e.g. Elligator-2 on
663 /// the Montgomery model of bandersnatch). Try-and-increment is *simpler* — it
664 /// reuses `from_x` and adds no new trusted constants or rational maps — at
665 /// the cost of *not being constant-time*: the number of attempts (~4 on
666 /// average, since roughly 1/4 of field elements are valid abscissae) depends
667 /// on the input.
668 ///
669 /// That tradeoff fits our use case. The intended caller is the Golden DKG
670 /// eVRF, where the hash inputs (public keys and messages) and outputs are
671 /// public and the secret scalar is only applied *afterwards* — so the
672 /// data-dependent timing reveals nothing secret. A future caller that hashes
673 /// secret material would instead need a constant-time map.
674 fn hash_to_group(domain_separator: &[u8], message: &[u8]) -> Self {
675 // `message || counter`, with an 8-byte big-endian counter we overwrite
676 // in place each attempt.
677 let mut data = Vec::with_capacity(message.len() + 8);
678 data.extend_from_slice(message);
679 data.extend_from_slice(&[0u8; 8]);
680 let counter_at = message.len();
681
682 // The loop is unbounded for totality, but is expected to terminate
683 // quickly: each attempt succeeds with probability ~1/4, so the number of
684 // iterations is geometric with mean ~4 and an exponentially small tail.
685 let mut counter: u64 = 0;
686 loop {
687 data[counter_at..].copy_from_slice(&counter.to_be_bytes());
688 // `Scalar::map` is RFC 9380 hash-to-field, giving a uniform abscissa.
689 if let Some(p) = Self::from_x(Scalar::map(domain_separator, &data)) {
690 return p;
691 }
692 counter += 1;
693 }
694 }
695}
696
697impl Read for G {
698 type Cfg = ();
699
700 fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
701 let bytes = <[u8; 32]>::read(buf)?;
702 let mut bytes = bytes.as_ref();
703 let x = Scalar::read_cfg(&mut bytes, &ScalarReadCfg::AllowZero)
704 .map_err(|_| CodecError::Invalid("Banderwagon", "x not a canonical field element"))?;
705 Self::from_x(x).ok_or(CodecError::Invalid("Banderwagon", "point not in subgroup"))
706 }
707}
708
709/// An in circuit representation of a banderwagon point.
710#[derive(Clone)]
711pub struct GVar<'ctx> {
712 // We use an affine representation in circuit. Inversions are cheap in circuit,
713 // but expensive out of circuit. This makes the projecive representation less
714 // interesting for us.
715 x: Var<'ctx, Scalar>,
716 y: Var<'ctx, Scalar>,
717}
718
719impl<'ctx> AddAssign<&Self> for GVar<'ctx> {
720 fn add_assign(&mut self, rhs: &Self) {
721 // Complete affine addition law for the twisted Edwards curve
722 // `a*x^2 + y^2 = 1 + d*x^2*y^2` (the same formula as the projective
723 // `AddAssign` for `G`, specialized to `z = 1`):
724 //
725 // x3 = (x1*y2 + y1*x2) / (1 + d*x1*x2*y1*y2)
726 // y3 = (y1*y2 - a*x1*x2) / (1 - d*x1*x2*y1*y2)
727 //
728 // We use the standard circuit-optimal arrangement, which costs six
729 // multiplications (the rest are additions and scalings by the fixed
730 // constants `a`, `d`, all linear and free in an R1CS-style backend):
731 //
732 // 1. `A = x1*x2`
733 // 2. `B = y1*y2`
734 // 3. `U = (x1 + y1)*(x2 + y2)`, so `x1*y2 + y1*x2 = U - A - B`
735 // 4. `AB = A*B`, so the shared term `d*x1*x2*y1*y2 = d*AB`
736 // 5,6. the two `/` operations, each a single constraint `q*den == num`
737 // (see [`Var`]'s `Div`).
738 //
739 // The curve is complete (`a` is a square and `d` is not), so both
740 // denominators are nonzero for every pair of subgroup points and the
741 // divisions never add an unsatisfiable or underconstrained quotient.
742 let Self { x: x1, y: y1 } = core::mem::replace(self, Self::identity());
743 let Self { x: x2, y: y2 } = rhs;
744
745 // Curve constants as native vars; they fold into the circuit as
746 // constants when combined with the (circuit-backed) coordinates.
747 let a = Var::native(A);
748 let d = Var::native(D);
749
750 let x1_x2 = x1.clone() * x2; // A
751 let y1_y2 = y1.clone() * y2; // B
752 let u = (x1 + &y1) * &(x2.clone() + y2); // U
753 let ab = x1_x2.clone() * &y1_y2; // A*B
754 let c = d * &ab; // d*x1*x2*y1*y2
755
756 let x_num = u - &x1_x2 - &y1_y2; // x1*y2 + y1*x2
757 let y_num = y1_y2 - &(a * &x1_x2); // y1*y2 - a*x1*x2
758 let one = Var::one();
759 let x_den = one.clone() + &c; // 1 + d*x1*x2*y1*y2
760 let y_den = one - &c; // 1 - d*x1*x2*y1*y2
761
762 *self = Self {
763 x: x_num / &x_den,
764 y: y_num / &y_den,
765 };
766 }
767}
768
769impl<'ctx> Add<&Self> for GVar<'ctx> {
770 type Output = Self;
771
772 fn add(mut self, rhs: &Self) -> Self::Output {
773 self += rhs;
774 self
775 }
776}
777
778/// Bit-length of the circuit field modulus `p` (`ceil(log2 p) = 255`).
779const SCALAR_BITS: usize = 255;
780
781impl<'ctx> GVar<'ctx> {
782 /// The circuit representation of the group identity, the twisted Edwards
783 /// neutral element `(0, 1)`. The complete addition law treats it as the
784 /// identity, so it is a valid starting accumulator.
785 fn identity() -> Self {
786 Self {
787 x: Var::zero(),
788 y: Var::one(),
789 }
790 }
791
792 /// The in-circuit representation of an out-of-circuit point, as native
793 /// constants (its canonical affine coordinates `(X/Z, Y/Z)`). Folding the
794 /// point in as a constant pins its representative, which is what makes
795 /// reading a coordinate of a derived point well-defined.
796 ///
797 /// The point is `canonicalize`d first, so the embedded
798 /// constants are a function of the *group element*, not of whichever of its
799 /// two affine representatives the caller happens to hold. Two parties folding
800 /// in the same element therefore build byte-identical circuits without having
801 /// to canonicalize their public inputs themselves.
802 pub fn constant(point: &G) -> Self {
803 let (x, y) = point.canonicalize().affine();
804 Self {
805 x: Var::native(x),
806 y: Var::native(y),
807 }
808 }
809
810 /// Select between two points based on `bit`: `on_true` when the bit is `1`,
811 /// `on_false` when it is `0`. Selecting each coordinate independently is
812 /// sound because `bit` is boolean, so the result is exactly one of the two
813 /// (already valid) input points.
814 fn select(bit: &BoolVar<'ctx, Scalar>, on_true: &Self, on_false: &Self) -> Self {
815 Self {
816 x: bit.select(&on_true.x, &on_false.x),
817 y: bit.select(&on_true.y, &on_false.y),
818 }
819 }
820
821 /// Assert (in circuit) that `self` and `other` are the same group element.
822 ///
823 /// Mirrors the out-of-circuit [`PartialEq`] for [`G`]: two representatives
824 /// denote the same quotient element iff `x1 * y2 == x2 * y1`. Constraining
825 /// the affine coordinates directly would be unsound here, because it would
826 /// reject the equally-valid twin representative `(-x, -y)`.
827 pub fn assert_eq(&self, other: &Self) {
828 (self.x.clone() * &other.y).assert_eq(&(other.x.clone() * &self.y));
829 }
830
831 /// Return the squared affine x-coordinate of this circuit point.
832 pub fn x_squared(&self) -> Var<'ctx, Scalar> {
833 self.x.clone() * &self.x
834 }
835
836 /// Multiply by a scalar given as its little-endian bits, via double-and-add.
837 ///
838 /// `cur` holds `[2^i] * self` and `acc` accumulates the conditionally-added
839 /// terms. The complete addition law makes the identity start and the
840 /// doublings (including over leading zero bits) need no special casing.
841 fn mul_bits(self, bits: &[BoolVar<'ctx, Scalar>]) -> Self {
842 let mut acc = Self::identity();
843 let mut cur = self;
844 for bit in bits {
845 let added = acc.clone() + &cur;
846 acc = Self::select(bit, &added, &acc);
847 cur = cur.clone() + &cur;
848 }
849 acc
850 }
851}
852
853/// The canonical little-endian 64-bit limbs of `x`'s integer value in `[0, r)`.
854///
855/// This is the integer [`Additive::scale`] multiplies a point by, and matches
856/// the canonical bit decomposition `to_canonical_bits_le` performs in circuit,
857/// so the out-of-circuit and in-circuit scalar muls agree.
858fn scalar_limbs(x: &Scalar) -> [u64; 4] {
859 let bytes = x.as_blst_scalar().b;
860 array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()))
861}
862
863/// Decompose `x` into its canonical little-endian bits (`SCALAR_BITS` of them).
864///
865/// The bits are fresh witnesses bound to `x` by two constraints:
866///
867/// 1. *recomposition*, `sum_i b_i * 2^i == x`, computed in the field;
868/// 2. *canonicity*, the integer `sum_i b_i * 2^i < p`.
869///
870/// Both are required. Recomposition alone holds only modulo the field modulus
871/// `p`, so without the range check a prover could supply the non-canonical
872/// alias `x + p`; because the Banderwagon group order does not divide `p`, that
873/// alias scales a point to a *different* result. The canonicity check pins the
874/// decomposition to the unique integer in `[0, p)`.
875pub fn scalar_bits_le<'ctx>(
876 ctx: Context<'ctx, Scalar>,
877 x: &Var<'ctx, Scalar>,
878) -> Vec<BoolVar<'ctx, Scalar>> {
879 // `as_blst_scalar` yields the canonical integer in `[0, p)` as little-endian
880 // bytes, so bit `i` is `(bytes[i / 8] >> (i % 8)) & 1`.
881 let bits: Vec<BoolVar<'ctx, Scalar>> = (0..SCALAR_BITS)
882 .map(|i| {
883 let x = x.clone();
884 BoolVar::witness(ctx, move |v| {
885 (x.value(v).as_blst_scalar().b[i / 8] >> (i % 8)) & 1 == 1
886 })
887 })
888 .collect();
889
890 // Constraint 1: recomposition `sum_i b_i * 2^i == x`. Each `b_i * 2^i` is a
891 // scaling by a constant, so this whole sum is linear (free in the backend).
892 let mut acc = Var::zero();
893 let mut pow = Scalar::one();
894 for bit in &bits {
895 acc += &(bit.var().clone() * &Var::native(pow.clone()));
896 pow.double();
897 }
898 acc.assert_eq(x);
899
900 // Constraint 2: canonicity, `value <= p - 1` (equivalently `value < p`).
901 //
902 // We walk the bits from most to least significant alongside the bits of the
903 // largest canonical value `c = p - 1` (its little-endian bytes, just like
904 // the witness bits above), maintaining `run = "every 1-bit of c so far was
905 // matched by a 1 in value"`. At a 1-bit of `c` the value bit is free, but it
906 // is folded into `run`; at a 0-bit of `c`, if `run` still holds the value
907 // bit must be 0 (otherwise value would exceed `c`). This is the standard
908 // run-folding field-membership check.
909 let c = (-Scalar::one()).as_blst_scalar().b;
910 let mut run = BoolVar::constant(true);
911 for i in (0..SCALAR_BITS).rev() {
912 if (c[i / 8] >> (i % 8)) & 1 == 1 {
913 run = run & bits[i].clone();
914 } else {
915 (run.clone() & bits[i].clone()).assert_eq(&BoolVar::constant(false));
916 }
917 }
918
919 bits
920}
921
922impl G {
923 /// In-circuit scalar multiplication, returning the point `[scalar] * self`.
924 ///
925 /// `self` is an out-of-circuit (public) point. Taking it by value here, and
926 /// folding its canonical affine coordinates in as constants, pins its
927 /// representative; that is what makes a coordinate of the result well-defined
928 /// (a Banderwagon element has two affine representatives `(x, y)` and
929 /// `(-x, -y)`, so a coordinate of a *witnessed* point would be ambiguous).
930 ///
931 /// `scalar` may be a witness; it is canonically decomposed (see
932 /// `to_canonical_bits_le`) and the bits drive a double-and-add. Soundness
933 /// additionally assumes `self` is a valid prime-order subgroup element, as
934 /// produced by this module's constructors.
935 ///
936 /// Use [`GVar::assert_eq`] to bind the result against a known point (e.g. a
937 /// public key); use [`scalar_mul_x_squared`](Self::scalar_mul_x_squared) when
938 /// only a (representative-independent) abscissa is needed.
939 pub(crate) fn scalar_mul<'ctx>(
940 &self,
941 ctx: Context<'ctx, Scalar>,
942 scalar: &Var<'ctx, Scalar>,
943 ) -> GVar<'ctx> {
944 let bits = scalar_bits_le(ctx, scalar);
945 GVar::constant(self).mul_bits(&bits)
946 }
947
948 /// In-circuit fixed-base scalar multiplication for several public bases
949 /// sharing one scalar bit decomposition.
950 ///
951 /// `WINDOW` controls how many bits are selected at a time. Selector
952 /// monomials are built once per window and reused across every base.
953 pub fn scalar_mul_many_in_circuit<'ctx, const WINDOW: usize>(
954 bases: &[Self],
955 bits: &[BoolVar<'ctx, Scalar>],
956 ) -> Vec<GVar<'ctx>> {
957 const {
958 assert!(WINDOW >= 1 && WINDOW <= 8, "window size must be in 1..=8");
959 }
960
961 let mut accs = vec![GVar::identity(); bases.len()];
962 let mut shifted = bases.to_vec();
963 for window in bits.chunks(WINDOW) {
964 let selector = Selector::new(window);
965 for (acc, base) in accs.iter_mut().zip(&shifted) {
966 let (xs, ys): (Vec<_>, Vec<_>) = (0..(1usize << window.len()))
967 .scan(Self::zero(), |p, _| {
968 let (x, y) = p.canonicalize().affine();
969 *p += base;
970 Some((x, y))
971 })
972 .unzip();
973 *acc += &GVar {
974 x: selector.select_constant(&xs),
975 y: selector.select_constant(&ys),
976 }
977 }
978 for base in &mut shifted {
979 for _ in 0..window.len() {
980 base.double();
981 }
982 }
983 }
984 accs
985 }
986
987 /// In-circuit squared affine x-coordinate of `[scalar] * self`.
988 ///
989 /// We expose the *squared* abscissa rather than the bare one because the
990 /// quotient identifies `(x, y)` with `(-x, -y)`: a plain x-coordinate flips
991 /// sign between the two representatives, whereas its square is a well-defined
992 /// function of the group element, so the read agrees regardless of which
993 /// representative a point happens to hold. The in-circuit counterpart of
994 /// [`scalar_mul_x_squared_base`](Self::scalar_mul_x_squared_base).
995 pub fn scalar_mul_x_squared<'ctx>(
996 &self,
997 ctx: Context<'ctx, Scalar>,
998 scalar: &Var<'ctx, Scalar>,
999 ) -> Var<'ctx, Scalar> {
1000 let x = self.scalar_mul(ctx, scalar).x;
1001 x.clone() * &x
1002 }
1003
1004 /// In-circuit `[scalar] * self` where the scalar is given *directly* as its
1005 /// little-endian bits, rather than as a [`Var`] to be decomposed.
1006 ///
1007 /// This is the path for an [`F`] exponent (e.g. the eVRF secret): the bits
1008 /// are the witness, so unlike `scalar_mul` there is no
1009 /// recomposition or canonicity constraint — see [`F`] for why that is sound
1010 /// at `F::BITS` width. The caller must allocate the bits once and reuse the
1011 /// same slice across operations, so a single exponent is bound everywhere.
1012 pub fn scalar_mul_bits<'ctx>(&self, bits: &[BoolVar<'ctx, Scalar>]) -> GVar<'ctx> {
1013 GVar::constant(self).mul_bits(bits)
1014 }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020 use arbitrary::Unstructured;
1021 use commonware_codec::{DecodeExt, Encode, EncodeFixed};
1022 use commonware_invariants::minifuzz;
1023
1024 fn arbitrary_point(u: &mut Unstructured<'_>) -> arbitrary::Result<G> {
1025 Ok(G::generator()
1026 * &F {
1027 limbs: [u.arbitrary()?, 0, 0, 0],
1028 })
1029 }
1030
1031 #[test]
1032 fn test_field_laws() {
1033 // Exercise the full algebraic suite (additive, multiplicative, ring, and
1034 // field/inverse laws) on `F`.
1035 minifuzz::test(commonware_math::algebra::test_suites::fuzz_field::<F>);
1036 }
1037
1038 #[test]
1039 fn test_random_canonical_and_full_range() {
1040 // `random()` must stay canonical (`< r`) and, unlike the old 252-bit
1041 // cap, cover the whole field: values in `[2^252, r)` (top bit set) must
1042 // occur.
1043 let mut rng = commonware_utils::test_rng();
1044 let mut saw_top_bit = false;
1045 for _ in 0..1000 {
1046 let f = F::random(&mut rng);
1047 // Canonical iff `f - r` borrows.
1048 let mut borrow = false;
1049 for (&x, &r) in f.limbs.iter().zip(F::R.iter()) {
1050 let (d, b) = x.overflowing_sub(r);
1051 let (_, c) = d.overflowing_sub(borrow as u64);
1052 borrow = b | c;
1053 }
1054 assert!(borrow, "random() produced a non-canonical value: {f:?}");
1055 saw_top_bit |= f.bits()[F::BITS - 1];
1056 }
1057 assert!(
1058 saw_top_bit,
1059 "random() never set the top bit; range is capped"
1060 );
1061 }
1062
1063 #[test]
1064 fn test_f_codec_round_trip() {
1065 minifuzz::test(|u| {
1066 let f: F = u.arbitrary()?;
1067 assert_eq!(F::decode(f.encode()).unwrap(), f);
1068 Ok(())
1069 });
1070 }
1071
1072 #[test]
1073 fn test_f_codec_rejects_non_canonical() {
1074 // `r` itself (the modulus) is the smallest non-canonical encoding.
1075 let mut bytes = [0u8; 32];
1076 for (i, limb) in F::R.iter().enumerate() {
1077 bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes());
1078 }
1079 assert!(F::decode(&bytes[..]).is_err());
1080 assert!(F::decode(&[0xffu8; 32][..]).is_err());
1081 }
1082
1083 #[test]
1084 fn test_field_modulus_matches_group_order() {
1085 // The field modulus `r` must be the order of the group `G`: scaling the
1086 // generator by the integer product `a * b` must agree with scaling it by
1087 // the field product `a * b mod r`. This ties `F::R` to the independently
1088 // verified group law (see the codec test vectors), and would fail for any
1089 // other modulus.
1090 minifuzz::test(|u| {
1091 let a: F = u.arbitrary()?;
1092 let b: F = u.arbitrary()?;
1093 let g = G::generator();
1094 assert_eq!((g.clone() * &a) * &b, g * &(a * &b));
1095 Ok(())
1096 });
1097 }
1098
1099 #[test]
1100 fn test_eq_identity() {
1101 assert_eq!(G::zero(), G::zero());
1102 }
1103
1104 #[test]
1105 fn test_eq_reflexive() {
1106 minifuzz::test(|u| {
1107 let p = arbitrary_point(u)?;
1108 assert_eq!(p, p.clone());
1109 Ok(())
1110 });
1111 }
1112
1113 #[test]
1114 fn test_eq_invariant_under_projective_scaling() {
1115 // Scaling every coordinate by a common nonzero factor yields the same point.
1116 minifuzz::test(|u| {
1117 let p = arbitrary_point(u)?;
1118 let mut lambda: Scalar = u.arbitrary()?;
1119 if lambda == Scalar::zero() {
1120 lambda = Scalar::one();
1121 }
1122 let scaled = G {
1123 x: p.x.clone() * &lambda,
1124 y: p.y.clone() * &lambda,
1125 t: p.t.clone() * &lambda,
1126 z: p.z.clone() * &lambda,
1127 };
1128 assert_eq!(p, scaled);
1129 Ok(())
1130 });
1131 }
1132
1133 #[test]
1134 fn test_eq_invariant_under_two_torsion() {
1135 // Adding the order-2 point (0, -1) maps (x, y) to (-x, -y); `t = xy` is
1136 // unchanged. Banderwagon must treat this as the same group element.
1137 minifuzz::test(|u| {
1138 let p = arbitrary_point(u)?;
1139 let twin = G {
1140 x: -p.x.clone(),
1141 y: -p.y.clone(),
1142 t: p.t.clone(),
1143 z: p.z.clone(),
1144 };
1145 assert_eq!(p, twin);
1146 Ok(())
1147 });
1148 }
1149
1150 #[test]
1151 fn test_neq_distinct_points() {
1152 // `P` and `P + generator` differ by a prime-order element, so they are
1153 // always distinct group elements.
1154 minifuzz::test(|u| {
1155 let p = arbitrary_point(u)?;
1156 assert_ne!(p, p.clone() + &G::generator());
1157 Ok(())
1158 });
1159 }
1160
1161 #[test]
1162 fn test_codec_fixed_size() {
1163 assert_eq!(G::SIZE, 32);
1164 }
1165
1166 #[test]
1167 fn test_codec_round_trip_identity() {
1168 // The identity serializes to all-zero bytes and decodes back to itself.
1169 let encoded = G::zero().encode();
1170 assert_eq!(encoded.as_ref(), &[0u8; 32]);
1171 let decoded = G::decode(encoded).unwrap();
1172 assert_eq!(decoded, G::zero());
1173 }
1174
1175 #[test]
1176 fn test_codec_round_trip_generator() {
1177 let g = G::generator();
1178 let decoded = G::decode(g.encode()).unwrap();
1179 assert_eq!(decoded, g);
1180 }
1181
1182 const TEST_DST: &[u8] = b"COMMONWARE_BANDERWAGON_HASH_TO_CURVE_TEST";
1183
1184 #[test]
1185 fn test_hash_to_group_deterministic() {
1186 // Same inputs always produce the same point; it round-trips through the
1187 // codec (so it really is a valid subgroup element).
1188 let p = G::hash_to_group(TEST_DST, b"hello");
1189 let q = G::hash_to_group(TEST_DST, b"hello");
1190 assert_eq!(p, q);
1191 assert_eq!(G::decode(p.encode()).unwrap(), p);
1192 }
1193
1194 #[test]
1195 fn test_hash_to_group_distinct_messages() {
1196 // Different messages map to different points.
1197 let p = G::hash_to_group(TEST_DST, b"message-a");
1198 let q = G::hash_to_group(TEST_DST, b"message-b");
1199 assert_ne!(p, q);
1200 }
1201
1202 #[test]
1203 fn test_hash_to_group_in_subgroup() {
1204 // Every output must pass the subgroup check, i.e. re-encode/decode.
1205 minifuzz::test(|u| {
1206 let msg: Vec<u8> = u.arbitrary()?;
1207 let p = G::hash_to_group(TEST_DST, &msg);
1208 assert_eq!(G::decode(p.encode()).unwrap(), p);
1209 Ok(())
1210 });
1211 }
1212
1213 #[test]
1214 fn test_hash_to_group_laws() {
1215 minifuzz::test(commonware_math::algebra::test_suites::fuzz_hash_to_group::<G>);
1216 }
1217
1218 #[test]
1219 fn test_codec_fixed_vectors() {
1220 // Official Banderwagon test vectors: the serialization of `G, 2G, 4G,
1221 // 8G, ...` (successive doublings of the generator). Matching these
1222 // confirms interoperability with go-ipa / Ethereum Verkle.
1223 //
1224 // Source: crate-crypto/rust-verkle `banderwagon::element::fixed_test_vectors`.
1225 let expected = [
1226 "4a2c7486fd924882bf02c6908de395122843e3e05264d7991e18e7985dad51e9",
1227 "43aa74ef706605705989e8fd38df46873b7eae5921fbed115ac9d937399ce4d5",
1228 "5e5f550494159f38aa54d2ed7f11a7e93e4968617990445cc93ac8e59808c126",
1229 "0e7e3748db7c5c999a7bcd93d71d671f1f40090423792266f94cb27ca43fce5c",
1230 "14ddaa48820cb6523b9ae5fe9fe257cbbd1f3d598a28e670a40da5d1159d864a",
1231 "6989d1c82b2d05c74b62fb0fbdf8843adae62ff720d370e209a7b84e14548a7d",
1232 "26b8df6fa414bf348a3dc780ea53b70303ce49f3369212dec6fbe4b349b832bf",
1233 "37e46072db18f038f2cc7d3d5b5d1374c0eb86ca46f869d6a95fc2fb092c0d35",
1234 "2c1ce64f26e1c772282a6633fac7ca73067ae820637ce348bb2c8477d228dc7d",
1235 "297ab0f5a8336a7a4e2657ad7a33a66e360fb6e50812d4be3326fab73d6cee07",
1236 "5b285811efa7a965bd6ef5632151ebf399115fcc8f5b9b8083415ce533cc39ce",
1237 "1f939fa2fd457b3effb82b25d3fe8ab965f54015f108f8c09d67e696294ab626",
1238 "3088dcb4d3f4bacd706487648b239e0be3072ed2059d981fe04ce6525af6f1b8",
1239 "35fbc386a16d0227ff8673bc3760ad6b11009f749bb82d4facaea67f58fc60ed",
1240 "00f29b4f3255e318438f0a31e058e4c081085426adb0479f14c64985d0b956e0",
1241 "3fa4384b2fa0ecc3c0582223602921daaa893a97b64bdf94dcaa504e8b7b9e5f",
1242 ];
1243
1244 let mut point = G::generator();
1245 for (i, want) in expected.into_iter().enumerate() {
1246 let encoded = point.encode();
1247 assert_eq!(
1248 commonware_formatting::hex(encoded.as_ref()),
1249 want,
1250 "encoding mismatch at index {i}"
1251 );
1252 // The vectors must also decode back to the same point.
1253 let decoded = G::decode(encoded).unwrap();
1254 assert_eq!(decoded, point, "decode mismatch at index {i}");
1255 point.double();
1256 }
1257 }
1258
1259 #[test]
1260 fn test_codec_round_trip() {
1261 minifuzz::test(|u| {
1262 let p = arbitrary_point(u)?;
1263 let decoded = G::decode(p.encode()).unwrap();
1264 assert_eq!(decoded, p);
1265 Ok(())
1266 });
1267 }
1268
1269 #[test]
1270 fn test_codec_canonical_for_twin() {
1271 // `P` and its quotient twin `(-x, -y)` are the same group element, so
1272 // they must serialize to identical bytes.
1273 minifuzz::test(|u| {
1274 let p = arbitrary_point(u)?;
1275 let twin = G {
1276 x: -p.x.clone(),
1277 y: -p.y.clone(),
1278 t: p.t.clone(),
1279 z: p.z.clone(),
1280 };
1281 assert_eq!(p.encode(), twin.encode());
1282 Ok(())
1283 });
1284 }
1285
1286 #[test]
1287 fn test_codec_canonical_under_projective_scaling() {
1288 // Projective scaling does not change the group element, so the encoding
1289 // must be invariant under it.
1290 minifuzz::test(|u| {
1291 let p = arbitrary_point(u)?;
1292 let mut lambda: Scalar = u.arbitrary()?;
1293 if lambda == Scalar::zero() {
1294 lambda = Scalar::one();
1295 }
1296 let scaled = G {
1297 x: p.x.clone() * &lambda,
1298 y: p.y.clone() * &lambda,
1299 t: p.t.clone() * &lambda,
1300 z: p.z.clone() * &lambda,
1301 };
1302 assert_eq!(p.encode(), scaled.encode());
1303 Ok(())
1304 });
1305 }
1306
1307 #[test]
1308 fn test_decode_rejects_non_canonical_field_element() {
1309 // `r - 1` is the largest valid field element; `r` (and above) must be
1310 // rejected as non-canonical. These are the big-endian bytes of `r`.
1311 let r_bytes: [u8; 32] = [
1312 0x73, 0xed, 0xa7, 0x53, 0x29, 0x9d, 0x7d, 0x48, 0x33, 0x39, 0xd8, 0x08, 0x09, 0xa1,
1313 0xd8, 0x05, 0x53, 0xbd, 0xa4, 0x02, 0xff, 0xfe, 0x5b, 0xfe, 0xff, 0xff, 0xff, 0xff,
1314 0x00, 0x00, 0x00, 0x01,
1315 ];
1316 assert!(G::decode(&r_bytes[..]).is_err());
1317 assert!(G::decode(&[0xffu8; 32][..]).is_err());
1318 }
1319
1320 #[test]
1321 fn test_decode_rejects_off_subgroup() {
1322 // Search for an `x` whose `1 - a*x^2` is a non-square: decoding must fail
1323 // the subgroup check. We expect to find one quickly (~half of all `x`).
1324 minifuzz::test(|u| {
1325 let x: Scalar = u.arbitrary()?;
1326 let x_sq = {
1327 let mut out = x.clone();
1328 out.square();
1329 out
1330 };
1331 let num = Scalar::one() - &(x_sq * &A);
1332 if num != Scalar::zero() && !num.is_square() {
1333 let bytes = x.encode_fixed::<32>();
1334 assert!(G::decode(&bytes[..]).is_err());
1335 }
1336 Ok(())
1337 });
1338 }
1339
1340 /// Build the circuit `[scalar] * base` and assert (in circuit) that it equals
1341 /// `expected`, returning whether the circuit is satisfied. `witness` chooses
1342 /// whether the scalar is a circuit witness (exercising `to_canonical_bits_le`)
1343 /// or a public constant.
1344 ///
1345 /// The check uses the quotient-aware [`GVar::assert_eq`], so `base` and
1346 /// `expected` need not be canonical: either affine representative is accepted.
1347 fn run_scalar_mul(base: &G, scalar: &Scalar, expected: &G, witness: bool) -> bool {
1348 use crate::zk::circuit::build_with_values;
1349
1350 let base = base.clone();
1351 let expected = expected.clone();
1352 let scalar = scalar.clone();
1353 let (valued, _) = build_with_values(move |ctx| {
1354 let s = if witness {
1355 Var::witness(ctx, move |_| scalar)
1356 } else {
1357 Var::constant(ctx, scalar)
1358 };
1359 base.scalar_mul(ctx, &s)
1360 .assert_eq(&GVar::constant(&expected));
1361 Vec::new()
1362 });
1363 valued.is_satisfied()
1364 }
1365
1366 #[test]
1367 fn test_scalar_mul_small() {
1368 let base = G::generator();
1369 for k in [0u64, 1, 2, 3, 4, 7, 8, 15, 16, 1_000_000, u64::MAX] {
1370 let scalar = Scalar::from_u64(k);
1371 let expected = base.scale(&[k, 0, 0, 0]);
1372 assert!(
1373 run_scalar_mul(&base, &scalar, &expected, false),
1374 "constant scalar k={k}"
1375 );
1376 assert!(
1377 run_scalar_mul(&base, &scalar, &expected, true),
1378 "witness scalar k={k}"
1379 );
1380 }
1381 }
1382
1383 #[test]
1384 fn test_scalar_mul_rejects_wrong_result() {
1385 // Sanity: the in-circuit equality must actually constrain the result.
1386 let base = G::generator();
1387 let scalar = Scalar::from_u64(5);
1388 let correct = base.scale(&[5, 0, 0, 0]);
1389 let wrong = correct.clone() + &G::generator();
1390 assert!(run_scalar_mul(&base, &scalar, &correct, true));
1391 assert!(!run_scalar_mul(&base, &scalar, &wrong, true));
1392 }
1393
1394 #[test]
1395 fn test_scalar_mul_random() {
1396 // Random base point and full-width random scalar, exercising the witness
1397 // decomposition (recomposition + canonicity) over the whole bit range.
1398 minifuzz::test(|u| {
1399 let base = arbitrary_point(u)?;
1400 let scalar: Scalar = u.arbitrary()?;
1401 // Scale `base` by the scalar's canonical integer (little-endian
1402 // limbs), matching the integer `scalar_mul` decomposes in circuit.
1403 let bytes = scalar.as_blst_scalar().b;
1404 let limbs: [u64; 4] =
1405 array::from_fn(|i| u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().unwrap()));
1406 let expected = base.scale(&limbs);
1407 assert!(run_scalar_mul(&base, &scalar, &expected, true));
1408 Ok(())
1409 });
1410 }
1411}