1use core::ops::{Add, AddAssign, Mul, Neg};
46use std::sync::OnceLock;
47
48use super::scalar::Scalar;
49use crate::signing::field::{Fp, Fp5};
50
51const A: Fp5 = Fp5([
53 Fp::from_u64_reduce(2),
54 Fp::ZERO,
55 Fp::ZERO,
56 Fp::ZERO,
57 Fp::ZERO,
58]);
59
60const B1: u64 = 263;
64
65const B: Fp5 = Fp5([
67 Fp::ZERO,
68 Fp::from_u64_reduce(B1),
69 Fp::ZERO,
70 Fp::ZERO,
71 Fp::ZERO,
72]);
73
74const B_MUL2: Fp5 = Fp5([
76 Fp::ZERO,
77 Fp::from_u64_reduce(2 * B1),
78 Fp::ZERO,
79 Fp::ZERO,
80 Fp::ZERO,
81]);
82
83const B_MUL4: Fp5 = Fp5([
85 Fp::ZERO,
86 Fp::from_u64_reduce(4 * B1),
87 Fp::ZERO,
88 Fp::ZERO,
89 Fp::ZERO,
90]);
91
92const B_MUL16: Fp5 = Fp5([
94 Fp::ZERO,
95 Fp::from_u64_reduce(16 * B1),
96 Fp::ZERO,
97 Fp::ZERO,
98 Fp::ZERO,
99]);
100
101const FOUR_FP5: Fp5 = Fp5([
103 Fp::from_u64_reduce(4),
104 Fp::ZERO,
105 Fp::ZERO,
106 Fp::ZERO,
107 Fp::ZERO,
108]);
109
110const WINDOW: u32 = 5;
112
113const WIN_SIZE: usize = 1 << (WINDOW - 1);
115
116const NUM_DIGITS: usize = (319 + WINDOW as usize) / WINDOW as usize;
119
120#[derive(Clone, Copy, Debug)]
128pub struct Point {
129 pub x: Fp5,
131 pub z: Fp5,
133 pub u: Fp5,
135 pub t: Fp5,
137}
138
139#[derive(Clone, Copy, Debug)]
142pub struct AffinePoint {
143 pub x: Fp5,
145 pub u: Fp5,
147}
148
149impl AffinePoint {
150 pub const NEUTRAL: Self = Self {
152 x: Fp5::ZERO,
153 u: Fp5::ZERO,
154 };
155
156 #[must_use]
158 pub fn to_point(self) -> Point {
159 Point {
160 x: self.x,
161 z: Fp5::ONE,
162 u: self.u,
163 t: Fp5::ONE,
164 }
165 }
166
167 pub fn set_neg(&mut self) {
169 self.u = -self.u;
170 }
171}
172
173impl Neg for AffinePoint {
174 type Output = Self;
175
176 fn neg(self) -> Self {
177 Self {
178 x: self.x,
179 u: -self.u,
180 }
181 }
182}
183
184impl Point {
185 pub const NEUTRAL: Self = Self {
187 x: Fp5::ZERO,
188 z: Fp5::ONE,
189 u: Fp5::ZERO,
190 t: Fp5::ONE,
191 };
192
193 pub const GENERATOR: Self = Self {
195 x: Fp5([
196 Fp::from_u64_reduce(12_883_135_586_176_881_569),
197 Fp::from_u64_reduce(4_356_519_642_755_055_268),
198 Fp::from_u64_reduce(5_248_930_565_894_896_907),
199 Fp::from_u64_reduce(2_165_973_894_480_315_022),
200 Fp::from_u64_reduce(2_448_410_071_095_648_785),
201 ]),
202 z: Fp5::ONE,
203 u: Fp5::ONE,
204 t: Fp5([
205 Fp::from_u64_reduce(4),
206 Fp::ZERO,
207 Fp::ZERO,
208 Fp::ZERO,
209 Fp::ZERO,
210 ]),
211 };
212
213 #[must_use]
216 pub fn eq_point(self, rhs: Self) -> bool {
217 self.u * rhs.t == rhs.u * self.t
218 }
219
220 #[inline]
223 #[must_use]
224 pub fn is_neutral(self) -> bool {
225 self.u.is_zero()
226 }
227
228 #[must_use]
231 pub fn encode(self) -> Fp5 {
232 self.t * self.u.invert()
233 }
234
235 #[must_use]
242 pub fn decode(w: Fp5) -> Option<Self> {
243 let e = w.square() - A;
244 let delta = e.square() - B_MUL4;
245 let r_opt = delta.canonical_sqrt();
246 let success = r_opt.is_some();
247 let r = r_opt.unwrap_or(Fp5::ZERO);
248
249 if !success {
250 return if w.is_zero() {
253 Some(Self::NEUTRAL)
254 } else {
255 None
256 };
257 }
258
259 let two_inv = Fp5::from_u64s_reduce([2, 0, 0, 0, 0]).invert();
260 let x1 = (e + r) * two_inv;
261 let x2 = (e - r) * two_inv;
262 let x = if x1.legendre() == Fp::ONE { x2 } else { x1 };
265
266 Some(Self {
267 x,
268 z: Fp5::ONE,
269 u: Fp5::ONE,
270 t: w,
271 })
272 }
273
274 #[must_use]
277 pub fn add_point(self, rhs: Self) -> Self {
278 let (x1, z1, u1, t1_) = (self.x, self.z, self.u, self.t);
279 let (x2, z2, u2, t2_) = (rhs.x, rhs.z, rhs.u, rhs.t);
280
281 let t1 = x1 * x2;
282 let t2 = z1 * z2;
283 let t3 = u1 * u2;
284 let t4 = t1_ * t2_;
285 let t5 = (x1 + z1) * (x2 + z2) - t1 - t2;
286 let t6 = (u1 + t1_) * (u2 + t2_) - t3 - t4;
287 let t7 = t1 + t2 * B;
288 let t8 = t4 * t7;
289 let t9 = t3 * (t5 * B_MUL2 + t7.double());
290 let t10 = (t4 + t3.double()) * (t5 + t7);
291
292 let x_new = (t10 - t8) * B;
293 let z_new = t8 - t9;
294 let u_new = t6 * (t2 * B - t1);
295 let t_new = t8 + t9;
296
297 Self {
298 x: x_new,
299 z: z_new,
300 u: u_new,
301 t: t_new,
302 }
303 }
304
305 #[must_use]
308 pub fn add_affine(self, rhs: AffinePoint) -> Self {
309 let (x1, z1, u1, t1_) = (self.x, self.z, self.u, self.t);
310 let (x2, u2) = (rhs.x, rhs.u);
311
312 let t1 = x1 * x2;
313 let t2 = z1;
314 let t3 = u1 * u2;
315 let t4 = t1_;
316 let t5 = x1 + x2 * z1;
317 let t6 = u1 + u2 * t1_;
318 let t7 = t1 + t2 * B;
319 let t8 = t4 * t7;
320 let t9 = t3 * (t5 * B_MUL2 + t7.double());
321 let t10 = (t4 + t3.double()) * (t5 + t7);
322
323 Self {
324 x: (t10 - t8) * B,
325 u: t6 * (t2 * B - t1),
326 z: t8 - t9,
327 t: t8 + t9,
328 }
329 }
330
331 #[must_use]
333 pub fn double(self) -> Self {
334 let mut p = self;
335 p.set_double();
336 p
337 }
338
339 pub fn set_double(&mut self) {
342 let x = self.x;
343 let z = self.z;
344 let u = self.u;
345 let t = self.t;
346
347 let t1 = z * t;
348 let t2 = t1 * t;
349 let x1 = t2.square();
350 let z1 = t1 * u;
351 let t3 = u.square();
352 let w1 = t2 - t3 * (x + z).double();
353 let t4 = z1.square();
354
355 let x_new = t4 * B_MUL4;
356 let z_new = w1.square();
357 let u_new = (w1 + z1).square() - t4 - z_new;
358 let t_new = x1.double() - (t4 * FOUR_FP5 + z_new);
359
360 self.x = x_new;
361 self.z = z_new;
362 self.u = u_new;
363 self.t = t_new;
364 }
365
366 #[must_use]
370 pub fn mdouble(self, n: u32) -> Self {
371 let mut p = self;
372 p.set_mdouble(n);
373 p
374 }
375
376 pub fn set_mdouble(&mut self, n: u32) {
378 if n == 0 {
379 return;
380 }
381
382 if n == 1 {
383 self.set_double();
384 return;
385 }
386
387 let x0 = self.x;
388 let z0 = self.z;
389 let u0 = self.u;
390 let t0 = self.t;
391
392 let t1 = z0 * t0;
393 let t2 = t1 * t0;
394 let x1 = t2.square();
395 let z1 = t1 * u0;
396 let t3 = u0.square();
397 let w1 = t2 - (x0 + z0).double() * t3;
398 let t4 = w1.square();
399 let t5 = z1.square();
400
401 let mut x_state = t5.square() * B_MUL16;
402 let mut w_state = x1.double() - (t5 * FOUR_FP5 + t4);
403 let mut z_state = (w1 + z1).square() - t4 - t5;
404
405 for _ in 2..n {
406 mdouble_inner_round(&mut x_state, &mut w_state, &mut z_state);
407 }
408
409 let t1f = w_state.square();
410 let t2f = z_state.square();
411 let t3f = (w_state + z_state).square() - t1f - t2f;
412 let w1f = t1f - (x_state + t2f).double();
413
414 let z_out = w1f.square();
415 self.x = t3f.square() * B;
416 self.z = z_out;
417 self.u = t3f * w1f;
418 self.t = t1f.double() * (t1f - t2f.double()) - z_out;
419 }
420
421 #[must_use]
424 pub fn make_window_affine(self) -> Vec<AffinePoint> {
425 let mut tmp = Vec::with_capacity(WIN_SIZE);
426 tmp.push(self);
427
428 for i in 1..WIN_SIZE {
429 if (i & 1) == 0 {
430 let last = tmp[i - 1];
431 tmp.push(last.add_point(self));
432 } else {
433 let half = tmp[i >> 1];
434 tmp.push(half.double());
435 }
436 }
437 batch_to_affine(&tmp)
438 }
439
440 #[must_use]
449 pub fn scalar_mul(self, s: Scalar) -> Self {
450 let win = self.make_window_affine();
451 let mut digits = [0i32; NUM_DIGITS];
452 s.recode_signed(&mut digits, WINDOW);
453 scalar_mul_with_window_var_time(&win, &digits)
454 }
455
456 #[must_use]
466 pub fn scalar_mul_ct(self, s: Scalar) -> Self {
467 let win = self.make_window_affine();
468 let mut digits = [0i32; NUM_DIGITS];
469 s.recode_signed(&mut digits, WINDOW);
470 scalar_mul_with_window_ct(&win, &digits)
471 }
472
473 #[must_use]
481 pub fn mulgen_ct(s: Scalar) -> Self {
482 let win = generator_window();
483 let mut digits = [0i32; NUM_DIGITS];
484 s.recode_signed(&mut digits, WINDOW);
485 scalar_mul_with_window_ct(win, &digits)
486 }
487
488 #[must_use]
493 pub fn mulgen(s: Scalar) -> Self {
494 let win = generator_window();
495 let mut digits = [0i32; NUM_DIGITS];
496 s.recode_signed(&mut digits, WINDOW);
497 scalar_mul_with_window_var_time(win, &digits)
498 }
499}
500
501fn scalar_mul_with_window_var_time(win: &[AffinePoint], digits: &[i32; NUM_DIGITS]) -> Point {
504 let mut p = lookup_var_time(win, digits[NUM_DIGITS - 1]).to_point();
505 for i in (0..NUM_DIGITS - 1).rev() {
506 p.set_mdouble(WINDOW);
507 let entry = lookup(win, digits[i]);
508 p = p.add_affine(entry);
509 }
510 p
511}
512
513fn scalar_mul_with_window_ct(win: &[AffinePoint], digits: &[i32; NUM_DIGITS]) -> Point {
516 let mut p = lookup_ct(win, digits[NUM_DIGITS - 1]).to_point();
517 for i in (0..NUM_DIGITS - 1).rev() {
518 p.set_mdouble(WINDOW);
519 let entry = lookup_ct(win, digits[i]);
520 p = p.add_affine(entry);
521 }
522 p
523}
524
525fn generator_window() -> &'static [AffinePoint; WIN_SIZE] {
528 static WINDOW_CACHE: OnceLock<[AffinePoint; WIN_SIZE]> = OnceLock::new();
529 WINDOW_CACHE.get_or_init(|| {
530 let v = Point::GENERATOR.make_window_affine();
531 let mut out = [AffinePoint::NEUTRAL; WIN_SIZE];
532 for (slot, entry) in out.iter_mut().zip(v.iter()) {
533 *slot = *entry;
534 }
535 out
536 })
537}
538
539fn mdouble_inner_round(x: &mut Fp5, w: &mut Fp5, z: &mut Fp5) {
542 let t1 = z.square();
543 let t2 = t1.square();
544 let t3 = w.square();
545 let t4 = t3.square();
546 let t5 = (*w + *z).square() - t1 - t3;
547
548 *z = t5 * ((*x + t1).double() - t3);
549 *x = t2 * t4 * B_MUL16;
550 *w = -(t4 + t2 * (B_MUL4 - FOUR_FP5));
551}
552
553#[must_use]
556pub fn batch_to_affine(src: &[Point]) -> Vec<AffinePoint> {
557 let n = src.len();
558 if n == 0 {
559 return Vec::new();
560 }
561
562 if n == 1 {
563 let p = src[0];
564 let m1 = (p.z * p.t).invert();
565 return vec![AffinePoint {
566 x: p.x * p.t * m1,
567 u: p.u * p.z * m1,
568 }];
569 }
570
571 let mut res = vec![
572 AffinePoint {
573 x: Fp5::ZERO,
574 u: Fp5::ZERO,
575 };
576 n
577 ];
578 let mut m = src[0].z * src[0].t;
579
580 for i in 1..n {
581 let x_partial = m;
582 m *= src[i].z;
583 let u_partial = m;
584 m *= src[i].t;
585
586 res[i] = AffinePoint {
587 x: x_partial,
588 u: u_partial,
589 };
590 }
591
592 m = m.invert();
593
594 for i in (1..n).rev() {
595 res[i].u = src[i].u * res[i].u * m;
596 m *= src[i].t;
597 res[i].x = src[i].x * res[i].x * m;
598 m *= src[i].z;
599 }
600 res[0].u = src[0].u * src[0].z * m;
601 m *= src[0].t;
602 res[0].x = src[0].x * m;
603
604 res
605}
606
607#[must_use]
617pub fn lookup(win: &[AffinePoint], k: i32) -> AffinePoint {
618 let sign = (k >> 31) as u32;
619 let ka = ((k as u32) ^ sign).wrapping_sub(sign);
620 let km1 = ka.wrapping_sub(1);
621
622 let mut x = Fp5::ZERO;
623 let mut u = Fp5::ZERO;
624
625 for (i, entry) in win.iter().enumerate() {
626 let m = km1.wrapping_sub(i as u32);
627 let c1 = (m | (!m).wrapping_add(1)) >> 31;
628 let c = (u64::from(c1)).wrapping_sub(1);
629 if c != 0 {
630 x = entry.x;
631 u = entry.u;
632 }
633 }
634
635 let neg_mask = u64::from(sign) | (u64::from(sign) << 32);
636 if neg_mask != 0 {
637 u = -u;
638 }
639 AffinePoint { x, u }
640}
641
642#[must_use]
647pub fn lookup_ct(win: &[AffinePoint], k: i32) -> AffinePoint {
648 let sign = (k >> 31) as u32;
649 let ka = ((k as u32) ^ sign).wrapping_sub(sign);
650 let km1 = ka.wrapping_sub(1);
651
652 let mut x = Fp5::ZERO;
653 let mut u = Fp5::ZERO;
654
655 for (i, entry) in win.iter().enumerate() {
656 let m = km1.wrapping_sub(i as u32);
657 let c1 = (m | (!m).wrapping_add(1)) >> 31;
658 let mask = u64::from(c1).wrapping_sub(1);
659 x = Fp5::ct_select(mask, x, entry.x);
660 u = Fp5::ct_select(mask, u, entry.u);
661 }
662
663 let neg_mask = sign as i32 as i64 as u64;
664 let neg_u = -u;
665 u = Fp5::ct_select(neg_mask, u, neg_u);
666 AffinePoint { x, u }
667}
668
669#[must_use]
672pub fn lookup_var_time(win: &[AffinePoint], k: i32) -> AffinePoint {
673 if k == 0 {
674 AffinePoint::NEUTRAL
675 } else if k > 0 {
676 win[(k - 1) as usize]
677 } else {
678 let mut res = win[(-k - 1) as usize];
679 res.set_neg();
680 res
681 }
682}
683
684impl Add for Point {
685 type Output = Self;
686
687 fn add(self, rhs: Self) -> Self {
688 self.add_point(rhs)
689 }
690}
691
692impl AddAssign for Point {
693 fn add_assign(&mut self, rhs: Self) {
694 *self = self.add_point(rhs);
695 }
696}
697
698impl Mul<Scalar> for Point {
699 type Output = Self;
700
701 fn mul(self, rhs: Scalar) -> Self {
702 self.scalar_mul(rhs)
703 }
704}
705
706impl Mul<Point> for Scalar {
707 type Output = Point;
708
709 fn mul(self, rhs: Point) -> Point {
710 rhs.scalar_mul(self)
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use proptest::prelude::*;
717 use rstest::rstest;
718 use serde::Deserialize;
719
720 use super::*;
721 use crate::signing::fixtures::{
722 arb_point, arb_scalar, bytes_to_hex, decode_fp5_bytes, hex_to_bytes,
723 };
724
725 const VECTORS_JSON: &str = include_str!(concat!(
726 env!("CARGO_MANIFEST_DIR"),
727 "/test_data/signing_curve_ecgfp5_vectors.json",
728 ));
729
730 #[derive(Debug, Deserialize)]
731 struct VectorsFile {
732 vectors: Vectors,
733 }
734
735 #[derive(Debug, Deserialize)]
736 struct Vectors {
737 decode: Vec<DecodeVector>,
738 add: Vec<AddVector>,
739 scalar_mul: Vec<ScalarMulVector>,
740 scalar_ops: Vec<ScalarOpVector>,
741 }
742
743 #[derive(Debug, Deserialize)]
744 struct DecodeVector {
745 w: String,
746 decodes: bool,
747 is_neutral: bool,
748 encoded_back: String,
749 }
750
751 #[derive(Debug, Deserialize)]
752 struct AddVector {
753 a_w: String,
754 b_w: String,
755 sum_w: String,
756 a_double_w: String,
757 }
758
759 #[derive(Debug, Deserialize)]
760 struct ScalarMulVector {
761 base_w: String,
762 scalar_le: String,
763 out_w: String,
764 }
765
766 #[derive(Debug, Deserialize)]
767 struct ScalarOpVector {
768 a: String,
769 b: String,
770 add: String,
771 sub: String,
772 mul: String,
773 neg_a: String,
774 a_is_zero: bool,
775 }
776
777 fn decode_scalar(hex: &str) -> Scalar {
778 let bytes = hex_to_bytes(hex);
779 let mut buf = [0u8; super::super::scalar::SCALAR_BYTES];
780 buf.copy_from_slice(&bytes);
781 Scalar::from_le_bytes_reduce(buf)
782 }
783
784 fn fp5(c: [u64; 5]) -> Fp5 {
785 Fp5::from_u64s_reduce(c)
786 }
787
788 #[rstest]
789 fn neutral_decodes_from_zero() {
790 let p = Point::decode(Fp5::ZERO).expect("zero decodes to neutral");
791 assert!(p.is_neutral());
792 }
793
794 #[rstest]
795 fn generator_is_canonical() {
796 let g = Point::GENERATOR;
797 assert_eq!(g.encode(), fp5([4, 0, 0, 0, 0]));
798 }
799
800 #[rstest]
801 fn neutral_encodes_to_zero() {
802 assert_eq!(Point::NEUTRAL.encode(), Fp5::ZERO);
804 }
805
806 #[rstest]
807 fn add_with_neutral_is_identity() {
808 let g = Point::GENERATOR;
809 let g_plus_n = g.add_point(Point::NEUTRAL);
810 assert!(g_plus_n.eq_point(g));
811
812 let n_plus_g = Point::NEUTRAL.add_point(g);
813 assert!(n_plus_g.eq_point(g));
814 }
815
816 #[rstest]
817 fn double_matches_self_addition() {
818 let g = Point::GENERATOR;
819 let g2 = g.double();
820 let g_plus_g = g.add_point(g);
821 assert!(g2.eq_point(g_plus_g));
822 }
823
824 #[rstest]
825 fn mdouble_matches_iterated_double() {
826 let g = Point::GENERATOR;
827
828 for n in 0..10u32 {
829 let mut iter = g;
830 for _ in 0..n {
831 iter = iter.double();
832 }
833 let bulk = g.mdouble(n);
834 assert!(
835 iter.eq_point(bulk),
836 "mdouble({n}) diverged from iterated double"
837 );
838 }
839 }
840
841 #[rstest]
842 fn add_affine_matches_general_add() {
843 let g = Point::GENERATOR;
844 let g2 = g.double();
845 let g2_affine = AffinePoint {
846 x: g2.x * g2.z.invert(),
847 u: g2.u * g2.t.invert(),
848 };
849 let sum_affine = g.add_affine(g2_affine);
850 let sum_point = g.add_point(g2);
851 assert!(sum_affine.eq_point(sum_point));
852 }
853
854 #[rstest]
855 fn scalar_one_is_identity_under_mul() {
856 let g = Point::GENERATOR;
857 let g1 = g * Scalar::ONE;
858 assert!(g1.eq_point(g));
859 }
860
861 #[rstest]
862 fn scalar_mul_ct_matches_scalar_mul() {
863 let g = Point::GENERATOR;
864 let mut order_minus_one = super::super::scalar::ORDER.to_limbs();
867 order_minus_one[0] -= 1;
868
869 let scalars = [
870 Scalar::ZERO,
871 Scalar::ONE,
872 Scalar::from_limbs([2, 0, 0, 0, 0]),
873 Scalar::from_limbs([0xDEAD_BEEF, 0, 0, 0, 0]),
874 Scalar::from_limbs([
875 0x0123_4567_89AB_CDEF,
876 0xFEDC_BA98_7654_3210,
877 0x1111_2222_3333_4444,
878 0x5555_6666_7777_8888,
879 0x0000_0001_0000_0001,
880 ]),
881 Scalar::from_limbs(order_minus_one),
882 ];
883
884 for (i, s) in scalars.iter().enumerate() {
885 let var = g.scalar_mul(*s);
886 let ct = g.scalar_mul_ct(*s);
887 assert!(var.eq_point(ct), "scalar {i}: var-time vs CT diverged");
888 assert_eq!(
889 var.encode().to_le_bytes(),
890 ct.encode().to_le_bytes(),
891 "scalar {i}: encoded outputs diverged",
892 );
893 }
894 }
895
896 #[rstest]
897 #[case(0)]
898 #[case(1)]
899 #[case(-1)]
900 #[case(2)]
901 #[case(-2)]
902 #[case(7)]
903 #[case(-7)]
904 #[case(8)]
905 #[case(-8)]
906 #[case(15)]
907 #[case(-15)]
908 #[case(16)]
909 #[case(-16)]
910 fn lookup_ct_matches_lookup(#[case] k: i32) {
911 let win = Point::GENERATOR.make_window_affine();
912 let var = lookup(&win, k);
913 let ct = lookup_ct(&win, k);
914 assert_eq!(var.x, ct.x, "k={k}: x coordinate diverged");
915 assert_eq!(var.u, ct.u, "k={k}: u coordinate diverged");
916 }
917
918 #[rstest]
919 fn batch_to_affine_handles_empty() {
920 let out = batch_to_affine(&[]);
921 assert!(out.is_empty(), "empty input must produce empty output");
922 }
923
924 #[rstest]
925 fn batch_to_affine_handles_single() {
926 let g2 = Point::GENERATOR.double();
927 let out = batch_to_affine(&[g2]);
928 assert_eq!(out.len(), 1);
929 let expected = AffinePoint {
931 x: g2.x * g2.z.invert(),
932 u: g2.u * g2.t.invert(),
933 };
934 assert_eq!(out[0].x, expected.x, "single-batch x diverged");
935 assert_eq!(out[0].u, expected.u, "single-batch u diverged");
936 }
937
938 #[rstest]
942 fn order_times_generator_is_neutral() {
943 let mut order_minus_one_limbs = super::super::scalar::ORDER.to_limbs();
944 order_minus_one_limbs[0] -= 1;
945 let order_minus_one = Scalar::from_limbs(order_minus_one_limbs);
946
947 let neg_g = Point::GENERATOR * order_minus_one;
948 let total = neg_g.add_point(Point::GENERATOR);
949 assert!(total.is_neutral(), "(ORDER - 1) * G + G must equal NEUTRAL",);
950 }
951
952 #[rstest]
954 fn order_minus_one_times_generator_is_neg_generator() {
955 let mut order_minus_one_limbs = super::super::scalar::ORDER.to_limbs();
956 order_minus_one_limbs[0] -= 1;
957 let order_minus_one = Scalar::from_limbs(order_minus_one_limbs);
958
959 let neg_g = Point::GENERATOR * order_minus_one;
960 let identity = neg_g.add_point(Point::GENERATOR);
962 assert!(identity.is_neutral());
963 }
964
965 proptest! {
966 #[rstest]
970 fn prop_scalar_mul_ct_matches_scalar_mul(
971 base in arb_point(),
972 s in arb_scalar(),
973 ) {
974 let var = base.scalar_mul(s);
975 let ct = base.scalar_mul_ct(s);
976 prop_assert!(var.eq_point(ct));
977 prop_assert_eq!(var.encode().to_le_bytes(), ct.encode().to_le_bytes());
978 }
979
980 #[rstest]
983 fn prop_lookup_ct_matches_lookup(k in -16i32..=16) {
984 let win = Point::GENERATOR.make_window_affine();
985 let var = lookup(&win, k);
986 let ct = lookup_ct(&win, k);
987 prop_assert_eq!(var.x, ct.x);
988 prop_assert_eq!(var.u, ct.u);
989 }
990
991 #[rstest]
995 fn prop_mulgen_ct_matches_scalar_mul_ct(s in arb_scalar()) {
996 let baseline = Point::GENERATOR.scalar_mul_ct(s);
997 let cached = Point::mulgen_ct(s);
998 prop_assert!(baseline.eq_point(cached));
999 prop_assert_eq!(
1000 baseline.encode().to_le_bytes(),
1001 cached.encode().to_le_bytes(),
1002 );
1003 }
1004
1005 #[rstest]
1008 fn prop_mulgen_matches_scalar_mul(s in arb_scalar()) {
1009 let baseline = Point::GENERATOR.scalar_mul(s);
1010 let cached = Point::mulgen(s);
1011 prop_assert!(baseline.eq_point(cached));
1012 prop_assert_eq!(
1013 baseline.encode().to_le_bytes(),
1014 cached.encode().to_le_bytes(),
1015 );
1016 }
1017 }
1018
1019 proptest! {
1020 #[rstest]
1025 fn prop_add_commutative(a in arb_point(), b in arb_point()) {
1026 prop_assert!(a.add_point(b).eq_point(b.add_point(a)));
1027 }
1028
1029 #[rstest]
1031 fn prop_neutral_is_identity(p in arb_point()) {
1032 prop_assert!(p.add_point(Point::NEUTRAL).eq_point(p));
1033 prop_assert!(Point::NEUTRAL.add_point(p).eq_point(p));
1034 }
1035
1036 #[rstest]
1038 fn prop_double_via_add(p in arb_point()) {
1039 prop_assert!(p.add_point(p).eq_point(p.double()));
1040 }
1041
1042 #[rstest]
1045 fn prop_add_affine_matches_general_add(p in arb_point(), q in arb_point()) {
1046 prop_assume!(!q.is_neutral());
1049 let q_affine = AffinePoint {
1050 x: q.x * q.z.invert(),
1051 u: q.u * q.t.invert(),
1052 };
1053 prop_assert!(p.add_affine(q_affine).eq_point(p.add_point(q)));
1054 }
1055
1056 #[rstest]
1058 fn prop_encode_decode_round_trip(p in arb_point()) {
1059 let w = p.encode();
1060 let p2 = Point::decode(w).expect("encoded group point must decode");
1061 prop_assert!(p2.eq_point(p));
1062 }
1063
1064 #[rstest]
1066 fn prop_scalar_one_is_identity(p in arb_point()) {
1067 prop_assert!((p * Scalar::ONE).eq_point(p));
1068 }
1069
1070 #[rstest]
1072 fn prop_scalar_zero_kills_point(p in arb_point()) {
1073 prop_assert!((p * Scalar::ZERO).is_neutral());
1074 }
1075 }
1076
1077 proptest! {
1078 #![proptest_config(ProptestConfig {
1082 cases: 64,
1083 ..ProptestConfig::default()
1084 })]
1085
1086 #[rstest]
1088 fn prop_add_associative(a in arb_point(), b in arb_point(), c in arb_point()) {
1089 let lhs = a.add_point(b).add_point(c);
1090 let rhs = a.add_point(b.add_point(c));
1091 prop_assert!(lhs.eq_point(rhs));
1092 }
1093
1094 #[rstest]
1097 fn prop_scalar_mul_distributive(
1098 a in arb_scalar(),
1099 b in arb_scalar(),
1100 p in arb_point(),
1101 ) {
1102 let lhs = p * (a + b);
1103 let rhs = (p * a).add_point(p * b);
1104 prop_assert!(lhs.eq_point(rhs));
1105 }
1106
1107 #[rstest]
1110 fn prop_scalar_mul_associative(
1111 a in arb_scalar(),
1112 b in arb_scalar(),
1113 p in arb_point(),
1114 ) {
1115 let lhs = (p * b) * a;
1116 let rhs = p * (a * b);
1117 prop_assert!(lhs.eq_point(rhs));
1118 }
1119
1120 #[rstest]
1122 fn prop_mdouble_via_double(p in arb_point(), n in 0u32..6) {
1123 let mut iter = p;
1124 for _ in 0..n {
1125 iter = iter.double();
1126 }
1127 prop_assert!(p.mdouble(n).eq_point(iter));
1128 }
1129
1130 #[rstest]
1133 fn prop_batch_to_affine_matches_naive(
1134 seeds in proptest::collection::vec(arb_scalar(), 1..8),
1135 ) {
1136 let pts: Vec<Point> = seeds
1137 .iter()
1138 .map(|s| Point::GENERATOR.scalar_mul(*s))
1139 .filter(|p| !p.is_neutral())
1140 .collect();
1141 prop_assume!(!pts.is_empty());
1142
1143 let batched = batch_to_affine(&pts);
1144 prop_assert_eq!(batched.len(), pts.len());
1145
1146 for (i, p) in pts.iter().enumerate() {
1147 let expected = AffinePoint {
1148 x: p.x * p.z.invert(),
1149 u: p.u * p.t.invert(),
1150 };
1151 prop_assert_eq!(batched[i].x, expected.x, "x at {} diverged", i);
1152 prop_assert_eq!(batched[i].u, expected.u, "u at {} diverged", i);
1153 }
1154 }
1155 }
1156
1157 #[rstest]
1158 fn scalar_two_matches_double() {
1159 let g = Point::GENERATOR;
1160 let g2 = g * Scalar::from_limbs([2, 0, 0, 0, 0]);
1161 assert!(g2.eq_point(g.double()));
1162 }
1163
1164 #[rstest]
1165 fn add_is_commutative() {
1166 let g = Point::GENERATOR;
1167 let g3 = g.double().add_point(g);
1168 let g3_alt = g.add_point(g.double());
1169 assert!(g3.eq_point(g3_alt));
1170 }
1171
1172 #[rstest]
1173 fn decode_matches_go_reference_vectors() {
1174 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1175 assert!(!suite.vectors.decode.is_empty(), "decode vectors empty");
1176
1177 for (i, v) in suite.vectors.decode.iter().enumerate() {
1178 let w = decode_fp5_bytes(&v.w);
1179 let decoded = Point::decode(w);
1180
1181 assert_eq!(decoded.is_some(), v.decodes, "vector {i}: decode success");
1182
1183 if let Some(p) = decoded {
1184 assert_eq!(p.is_neutral(), v.is_neutral, "vector {i}: is_neutral");
1185
1186 let encoded = p.encode();
1187 assert_eq!(
1188 bytes_to_hex(&encoded.to_le_bytes()),
1189 v.encoded_back,
1190 "vector {i}: re-encode round trip",
1191 );
1192 }
1193 }
1194 }
1195
1196 #[rstest]
1197 fn add_and_double_match_go_reference_vectors() {
1198 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1199 assert!(!suite.vectors.add.is_empty(), "add vectors empty");
1200
1201 for (i, v) in suite.vectors.add.iter().enumerate() {
1202 let a_w = decode_fp5_bytes(&v.a_w);
1203 let b_w = decode_fp5_bytes(&v.b_w);
1204 let a = Point::decode(a_w).unwrap_or_else(|| panic!("vector {i}: decode a"));
1205 let b = Point::decode(b_w).unwrap_or_else(|| panic!("vector {i}: decode b"));
1206
1207 let sum = a.add_point(b);
1208 assert_eq!(
1209 bytes_to_hex(&sum.encode().to_le_bytes()),
1210 v.sum_w,
1211 "vector {i}: a + b",
1212 );
1213
1214 let double_a = a.double();
1215 assert_eq!(
1216 bytes_to_hex(&double_a.encode().to_le_bytes()),
1217 v.a_double_w,
1218 "vector {i}: 2 * a",
1219 );
1220 }
1221 }
1222
1223 #[rstest]
1224 fn scalar_mul_matches_go_reference_vectors() {
1225 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1226 assert!(
1227 !suite.vectors.scalar_mul.is_empty(),
1228 "scalar_mul vectors empty"
1229 );
1230
1231 for (i, v) in suite.vectors.scalar_mul.iter().enumerate() {
1232 let base_w = decode_fp5_bytes(&v.base_w);
1233 let base = Point::decode(base_w).unwrap_or_else(|| panic!("vector {i}: decode base"));
1234 let s = decode_scalar(&v.scalar_le);
1235
1236 let out = base * s;
1237 assert_eq!(
1238 bytes_to_hex(&out.encode().to_le_bytes()),
1239 v.out_w,
1240 "vector {i}: s * base",
1241 );
1242 }
1243 }
1244
1245 #[rstest]
1246 fn scalar_ops_match_go_reference_vectors() {
1247 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
1248 assert!(
1249 !suite.vectors.scalar_ops.is_empty(),
1250 "scalar_ops vectors empty"
1251 );
1252
1253 for (i, v) in suite.vectors.scalar_ops.iter().enumerate() {
1254 let a = decode_scalar(&v.a);
1255 let b = decode_scalar(&v.b);
1256
1257 assert_eq!(
1258 bytes_to_hex(&(a + b).to_le_bytes()),
1259 v.add,
1260 "vector {i}: a + b",
1261 );
1262 assert_eq!(
1263 bytes_to_hex(&(a - b).to_le_bytes()),
1264 v.sub,
1265 "vector {i}: a - b",
1266 );
1267 assert_eq!(
1268 bytes_to_hex(&(a * b).to_le_bytes()),
1269 v.mul,
1270 "vector {i}: a * b",
1271 );
1272 assert_eq!(bytes_to_hex(&(-a).to_le_bytes()), v.neg_a, "vector {i}: -a",);
1273 assert_eq!(a.is_zero(), v.a_is_zero, "vector {i}: is_zero");
1274 }
1275 }
1276}