1use std::vec::IntoIter;
2
3use falcon_profiler::profiling;
4use itertools::Itertools;
5use num::{BigInt, FromPrimitive, One, Zero};
6use num_complex::Complex64;
7use rand::Rng;
8
9use crate::{
10 falcon_field::{Felt, Q},
11 fast_fft::FastFft,
12 fixed_point::FixedPoint128,
13 multiword_int::MultiwordInt,
14 multiword_poly::MultiwordPoly,
15 packed::Packed,
16 polynomial::Polynomial,
17 rns::{
18 intt_inplace_cached, ntt_inplace_cached, NttPrimeList, NttPrimes24Bit2, NttPrimes24Bit4,
19 NttPrimes24Bit5, NttPrimes24Bit8, Rns,
20 },
21 rns_runtime::RuntimeNtt,
22 samplerz::SamplerZCtx,
23 U32Field,
24};
25
26#[rustfmt::skip]
35pub const NTRU_SOLVE_BABAI_COEFF_BITS: [(f64, f64, f64, f64); 11] = [
36 ( 4.00, 0.00, 19.61, 0.49), (10.99, 0.08, 39.82, 0.41), (24.07, 0.25, 78.20, 0.73), (50.37, 0.53, 153.65, 1.39), (101.62, 1.02, 303.49, 2.38), (202.22, 1.87, 599.81, 3.87), (400.67, 3.10,1188.68, 6.04), (794.17, 4.98,2361.84, 9.31), (1576.87,7.49,4703.30, 14.77), (3138.35,12.25,9403.29,27.55), (6307.52,24.48,6319.66,24.51), ];
48
49fn babai_shifts(capital_size: u64, size: u64, d: u64) -> (u32, u32) {
69 if d > 53 {
70 ((capital_size - 106) as u32, (d - 53) as u32)
71 } else {
72 ((size - 53) as u32, 0)
73 }
74}
75
76#[doc(hidden)]
88#[profiling]
89pub fn babai_reduce_bigint(
90 f: &Polynomial<BigInt>,
91 g: &Polynomial<BigInt>,
92 capital_f: &mut Polynomial<BigInt>,
93 capital_g: &mut Polynomial<BigInt>,
94) -> Result<(), String> {
95 let bitsize = |bi: &BigInt| bi.bits();
96 let n = f.coefficients.len();
97 let size = [
98 f.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
99 g.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
100 53,
101 ]
102 .into_iter()
103 .max()
104 .unwrap();
105 let shift = (size as i64) - 53;
106 let f_adjusted = f
107 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
108 .fft();
109 let g_adjusted = g
110 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
111 .fft();
112
113 let f_star_adjusted = f_adjusted.map(|c| c.conj());
114 let g_star_adjusted = g_adjusted.map(|c| c.conj());
115 let denominator_fft =
116 f_adjusted.hadamard_mul(&f_star_adjusted) + g_adjusted.hadamard_mul(&g_star_adjusted);
117
118 let mut prev_capital_size = u64::MAX;
119 loop {
120 let capital_size = [
121 capital_f.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
122 capital_g.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
123 53,
124 ]
125 .into_iter()
126 .max()
127 .unwrap();
128
129 if capital_size < size {
132 break;
133 }
134 if capital_size >= prev_capital_size {
135 break;
136 }
137 prev_capital_size = capital_size;
138
139 let d = capital_size - size;
142 let (capital_shift, back_shift) = babai_shifts(capital_size, size, d);
143 let (capital_shift, back_shift) = (capital_shift as i64, back_shift as i64);
144
145 let capital_f_adjusted = capital_f
146 .map(|bi| Complex64::new(i128::try_from(bi >> capital_shift).unwrap() as f64, 0.0))
147 .fft();
148 let capital_g_adjusted = capital_g
149 .map(|bi| Complex64::new(i128::try_from(bi >> capital_shift).unwrap() as f64, 0.0))
150 .fft();
151
152 let numerator = capital_f_adjusted.hadamard_mul(&f_star_adjusted)
153 + capital_g_adjusted.hadamard_mul(&g_star_adjusted);
154 let quotient = numerator.hadamard_div(&denominator_fft).ifft();
155
156 let k = quotient.map(|f| BigInt::from(f.re.round() as i128));
160
161 if k.is_zero() {
162 break;
163 }
164 let kf = (k.clone().karatsuba(f)).reduce_by_cyclotomic(n);
165 let kg = (k.clone().karatsuba(g)).reduce_by_cyclotomic(n);
166 let shifted_kf = kf.map(|bi| bi << back_shift);
167 let shifted_kg = kg.map(|bi| bi << back_shift);
168
169 if d > 53 {
174 let new_cs_f = capital_f
175 .coefficients
176 .iter()
177 .zip(shifted_kf.coefficients.iter())
178 .map(|(a, b)| (a - b).bits())
179 .max()
180 .unwrap_or(0);
181 let new_cs_g = capital_g
182 .coefficients
183 .iter()
184 .zip(shifted_kg.coefficients.iter())
185 .map(|(a, b)| (a - b).bits())
186 .max()
187 .unwrap_or(0);
188 if u64::max(new_cs_f, new_cs_g) >= capital_size {
189 let cs_old = (capital_size as i64) - 53;
191 let cf_old = capital_f
192 .map(|bi| Complex64::new(i64::try_from(bi >> cs_old).unwrap() as f64, 0.0))
193 .fft();
194 let cg_old = capital_g
195 .map(|bi| Complex64::new(i64::try_from(bi >> cs_old).unwrap() as f64, 0.0))
196 .fft();
197 let num_old =
198 cf_old.hadamard_mul(&f_star_adjusted) + cg_old.hadamard_mul(&g_star_adjusted);
199 let quot_old = num_old.hadamard_div(&denominator_fft).ifft();
200 let k_old = quot_old.map(|f| BigInt::from(f.re.round() as i64));
201 if k_old.is_zero() {
202 break;
203 }
204 let kf_old = (k_old.clone().karatsuba(f)).reduce_by_cyclotomic(n);
205 let kg_old = (k_old.karatsuba(g)).reduce_by_cyclotomic(n);
206 *capital_f -= kf_old.map(|bi| bi << d);
207 *capital_g -= kg_old.map(|bi| bi << d);
208 continue;
209 }
210 }
211
212 *capital_f -= shifted_kf;
213 *capital_g -= shifted_kg;
214 }
215 Ok(())
216}
217
218trait ReductionCapital: Clone {
225 fn from_bigint(x: &BigInt) -> Self;
226 fn to_bigint(&self) -> BigInt;
227 fn bit_length(&self) -> u64;
229 fn shr_window(&self, shift: u32) -> i128;
232 fn shl(&self, bits: u32) -> Self;
234 fn sub(&self, other: &Self) -> Self;
236 fn from_rns<const K: usize, P: NttPrimeList<K>>(r: &Rns<K, P>) -> Self;
238}
239
240impl ReductionCapital for BigInt {
241 fn from_bigint(x: &BigInt) -> Self {
242 x.clone()
243 }
244 fn to_bigint(&self) -> BigInt {
245 self.clone()
246 }
247 fn bit_length(&self) -> u64 {
248 self.bits()
249 }
250 fn shr_window(&self, shift: u32) -> i128 {
251 i128::try_from(self >> (shift as i64)).unwrap()
252 }
253 fn shl(&self, bits: u32) -> Self {
254 self << (bits as u64)
255 }
256 fn sub(&self, other: &Self) -> Self {
257 self - other
258 }
259 fn from_rns<const K: usize, P: NttPrimeList<K>>(r: &Rns<K, P>) -> Self {
260 r.to_bigint()
261 }
262}
263
264impl<const L: usize> ReductionCapital for Packed<L> {
265 fn from_bigint(x: &BigInt) -> Self {
266 Packed::from_bigint(x)
267 }
268 fn to_bigint(&self) -> BigInt {
269 Packed::to_bigint(*self)
270 }
271 fn bit_length(&self) -> u64 {
272 Packed::bit_length(self)
273 }
274 fn shr_window(&self, shift: u32) -> i128 {
275 self.shr_to_i128(shift)
276 }
277 fn shl(&self, bits: u32) -> Self {
278 Packed::shl(self, bits)
279 }
280 fn sub(&self, other: &Self) -> Self {
281 Packed::sub(self, other)
282 }
283 fn from_rns<const K: usize, P: NttPrimeList<K>>(r: &Rns<K, P>) -> Self {
284 Packed::from_rns(r)
285 }
286}
287
288fn babai_reduce_rns_generic<C, const K: usize, P>(
301 f: &Polynomial<BigInt>,
302 g: &Polynomial<BigInt>,
303 capital_f: &mut Polynomial<BigInt>,
304 capital_g: &mut Polynomial<BigInt>,
305) -> Result<(), String>
306where
307 C: ReductionCapital,
308 P: NttPrimeList<K>,
309{
310 let bitsize = |bi: &BigInt| bi.bits();
311 let n = f.coefficients.len();
312
313 let tables = P::ntt_tables(n);
316
317 let mut f_ntt: Vec<Rns<K, P>> = f
320 .coefficients
321 .iter()
322 .map(Rns::<K, P>::from_bigint)
323 .collect();
324 let mut g_ntt: Vec<Rns<K, P>> = g
325 .coefficients
326 .iter()
327 .map(Rns::<K, P>::from_bigint)
328 .collect();
329 ntt_inplace_cached::<K, P>(&mut f_ntt, &tables);
330 ntt_inplace_cached::<K, P>(&mut g_ntt, &tables);
331
332 let size = [
333 f.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
334 g.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
335 53,
336 ]
337 .into_iter()
338 .max()
339 .unwrap();
340
341 let modulus_signed_bits: f64 = P::PRIMES.iter().map(|&p| (p as f64).log2()).sum::<f64>() - 1.0;
347 debug_assert!(
348 modulus_signed_bits >= (size + 54) as f64,
349 "RNS prime list too small: modulus {modulus_signed_bits:.0} signed bits < \
350 product ~{} bits (max|f,g|={size})",
351 size + 54
352 );
353
354 let shift = (size as i64) - 53;
355 let f_adjusted = f
356 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
357 .fft();
358 let g_adjusted = g
359 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
360 .fft();
361
362 let f_star_adjusted = f_adjusted.map(|c| c.conj());
363 let g_star_adjusted = g_adjusted.map(|c| c.conj());
364 let denominator_fft =
365 f_adjusted.hadamard_mul(&f_star_adjusted) + g_adjusted.hadamard_mul(&g_star_adjusted);
366
367 let mut cf: Vec<C> = capital_f.coefficients.iter().map(C::from_bigint).collect();
370 let mut cg: Vec<C> = capital_g.coefficients.iter().map(C::from_bigint).collect();
371
372 let mul = |k_ntt: &[Rns<K, P>], h_ntt: &[Rns<K, P>]| -> Vec<C> {
375 let mut prod: Vec<Rns<K, P>> = k_ntt
376 .iter()
377 .zip(h_ntt.iter())
378 .map(|(&a, &b)| a * b)
379 .collect();
380 intt_inplace_cached::<K, P>(&mut prod, &tables);
381 prod.iter().map(C::from_rns).collect()
382 };
383 let window = |cs: &[C], sh: u32| -> Polynomial<Complex64> {
385 Polynomial::new(
386 cs.iter()
387 .map(|c| Complex64::new(c.shr_window(sh) as f64, 0.0))
388 .collect::<Vec<_>>(),
389 )
390 .fft()
391 };
392 let cap_size = |cf: &[C], cg: &[C]| -> u64 {
393 cf.iter()
394 .chain(cg.iter())
395 .map(C::bit_length)
396 .fold(53, u64::max)
397 };
398
399 let mut prev_capital_size = u64::MAX;
400 loop {
401 let capital_size = cap_size(&cf, &cg);
402 if capital_size < size || capital_size >= prev_capital_size {
403 break;
404 }
405 prev_capital_size = capital_size;
406
407 let d = capital_size - size;
408 let (capital_shift, back_shift) = babai_shifts(capital_size, size, d);
409
410 let numerator = window(&cf, capital_shift).hadamard_mul(&f_star_adjusted)
411 + window(&cg, capital_shift).hadamard_mul(&g_star_adjusted);
412 let quotient = numerator.hadamard_div(&denominator_fft).ifft();
413
414 let k: Vec<i128> = quotient
415 .coefficients
416 .iter()
417 .map(|c| c.re.round() as i128)
418 .collect();
419 if k.iter().all(|&x| x == 0) {
420 break;
421 }
422
423 let mut k_ntt: Vec<Rns<K, P>> = k.iter().map(|&v| Rns::from_i128(v)).collect();
425 ntt_inplace_cached::<K, P>(&mut k_ntt, &tables);
426 let kf = mul(&k_ntt, &f_ntt);
427 let kg = mul(&k_ntt, &g_ntt);
428 let shifted_kf: Vec<C> = kf.iter().map(|p| p.shl(back_shift)).collect();
429 let shifted_kg: Vec<C> = kg.iter().map(|p| p.shl(back_shift)).collect();
430
431 if d > 53 {
432 let new_cs_f = cf
433 .iter()
434 .zip(shifted_kf.iter())
435 .map(|(a, b)| a.sub(b).bit_length())
436 .fold(0, u64::max);
437 let new_cs_g = cg
438 .iter()
439 .zip(shifted_kg.iter())
440 .map(|(a, b)| a.sub(b).bit_length())
441 .fold(0, u64::max);
442 if u64::max(new_cs_f, new_cs_g) >= capital_size {
443 let cs_old = (capital_size - 53) as u32;
444 let num_old = window(&cf, cs_old).hadamard_mul(&f_star_adjusted)
445 + window(&cg, cs_old).hadamard_mul(&g_star_adjusted);
446 let quot_old = num_old.hadamard_div(&denominator_fft).ifft();
447 let k_old: Vec<i128> = quot_old
448 .coefficients
449 .iter()
450 .map(|c| c.re.round() as i128)
451 .collect();
452 if k_old.iter().all(|&x| x == 0) {
453 break;
454 }
455 let mut k_old_ntt: Vec<Rns<K, P>> =
456 k_old.iter().map(|&v| Rns::from_i128(v)).collect();
457 ntt_inplace_cached::<K, P>(&mut k_old_ntt, &tables);
458 let kf_old = mul(&k_old_ntt, &f_ntt);
459 let kg_old = mul(&k_old_ntt, &g_ntt);
460 for i in 0..n {
461 cf[i] = cf[i].sub(&kf_old[i].shl(d as u32));
462 cg[i] = cg[i].sub(&kg_old[i].shl(d as u32));
463 }
464 continue;
465 }
466 }
467
468 for i in 0..n {
469 cf[i] = cf[i].sub(&shifted_kf[i]);
470 cg[i] = cg[i].sub(&shifted_kg[i]);
471 }
472 }
473
474 for (c, p) in capital_f.coefficients.iter_mut().zip(cf.iter()) {
476 *c = p.to_bigint();
477 }
478 for (c, p) in capital_g.coefficients.iter_mut().zip(cg.iter()) {
479 *c = p.to_bigint();
480 }
481 Ok(())
482}
483
484#[profiling]
495pub(crate) fn babai_reduce_rns_runtime(
496 f: &Polynomial<BigInt>,
497 g: &Polynomial<BigInt>,
498 capital_f: &mut Polynomial<BigInt>,
499 capital_g: &mut Polynomial<BigInt>,
500 k_primes: usize,
501 cap_w: usize,
502) -> Result<(), String> {
503 use crate::multiword_int as mw;
504 use crate::multiword_poly::SCHOOLBOOK_MAX_N;
505 let n = f.coefficients.len();
506 let use_schoolbook = n <= SCHOOLBOOK_MAX_N;
511 let ntt = if use_schoolbook {
512 None
513 } else {
514 Some(RuntimeNtt::cached(n, k_primes))
515 };
516
517 let bitsize = |bi: &BigInt| bi.bits();
518 let size = [
519 f.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
520 g.map(bitsize).fold(0, |a, &b| u64::max(a, b)),
521 53,
522 ]
523 .into_iter()
524 .max()
525 .unwrap();
526
527 let shift = (size as i64) - 53;
529 let f_adjusted = f
530 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
531 .fft();
532 let g_adjusted = g
533 .map(|bi| Complex64::new(i64::try_from(bi >> shift).unwrap() as f64, 0.0))
534 .fft();
535 let f_star = f_adjusted.map(|c| c.conj());
536 let g_star = g_adjusted.map(|c| c.conj());
537 let denominator_fft = f_adjusted.hadamard_mul(&f_star) + g_adjusted.hadamard_mul(&g_star);
538
539 let fw = (size as usize / 64) + 2;
541 let f_mw = MultiwordPoly::from_bigint_poly(f, fw);
542 let g_mw = MultiwordPoly::from_bigint_poly(g, fw);
543 let (f_ntt, g_ntt) = match &ntt {
545 Some(ctx) => (Some(ctx.forward(&f_mw)), Some(ctx.forward(&g_mw))),
546 None => (None, None),
547 };
548 let mul_kf = |kp: &MultiwordPoly| -> MultiwordPoly {
551 match &ntt {
552 Some(ctx) => ctx.mul_transformed(kp, f_ntt.as_ref().unwrap(), cap_w),
553 None => kp.schoolbook_negacyclic_mul(&f_mw, cap_w),
554 }
555 };
556 let mul_kg = |kp: &MultiwordPoly| -> MultiwordPoly {
557 match &ntt {
558 Some(ctx) => ctx.mul_transformed(kp, g_ntt.as_ref().unwrap(), cap_w),
559 None => kp.schoolbook_negacyclic_mul(&g_mw, cap_w),
560 }
561 };
562 let mut cf = MultiwordPoly::from_bigint_poly(capital_f, cap_w);
563 let mut cg = MultiwordPoly::from_bigint_poly(capital_g, cap_w);
564
565 let window = |cs: &MultiwordPoly, sh: u32| -> Polynomial<Complex64> {
566 Polynomial::new(
567 (0..n)
568 .map(|i| Complex64::new(mw::shr_to_i128(cs.coeff(i), sh) as f64, 0.0))
569 .collect::<Vec<_>>(),
570 )
571 .fft()
572 };
573 let cap_size = |cf: &MultiwordPoly, cg: &MultiwordPoly| -> u64 {
574 (0..n)
575 .flat_map(|i| [mw::bit_length(cf.coeff(i)), mw::bit_length(cg.coeff(i))])
576 .fold(53, u64::max)
577 };
578 let k_poly = |k: &[i128]| -> MultiwordPoly {
580 let mut kp = MultiwordPoly::zeros(n, 2);
581 for (i, &v) in k.iter().enumerate() {
582 kp.coeff_mut(i)
583 .copy_from_slice(MultiwordInt::from_i128(v, 2).limbs());
584 }
585 kp
586 };
587 let estimate_k = |cf: &MultiwordPoly, cg: &MultiwordPoly, sh: u32| -> Vec<i128> {
588 let numerator = window(cf, sh).hadamard_mul(&f_star) + window(cg, sh).hadamard_mul(&g_star);
589 let quotient = numerator.hadamard_div(&denominator_fft).ifft();
590 quotient
591 .coefficients
592 .iter()
593 .map(|c| c.re.round() as i128)
594 .collect()
595 };
596
597 let capital_bits = cap_size(&cf, &cg);
608 if (cap_w as u64) * 64 < capital_bits + 2 {
609 return Err(format!(
610 "cap_w={cap_w} limbs ({} bits) too small for capital ~{capital_bits} bits \
611 plus the back-shifted k·f correction",
612 cap_w * 64
613 ));
614 }
615 if let Some(ctx) = &ntt {
616 let modulus_signed_bits: f64 =
620 ctx.primes().iter().map(|&p| (p as f64).log2()).sum::<f64>() - 1.0;
621 if modulus_signed_bits < (size + 54) as f64 {
622 return Err(format!(
623 "RNS modulus {modulus_signed_bits:.0} signed bits < k·f product ~{} bits \
624 (k_primes={k_primes}, max|f,g|={size})",
625 size + 54
626 ));
627 }
628 }
629
630 let mut prev_capital_size = u64::MAX;
631 loop {
632 let capital_size = cap_size(&cf, &cg);
633 if capital_size < size || capital_size >= prev_capital_size {
634 break;
635 }
636 prev_capital_size = capital_size;
637
638 let d = capital_size - size;
639 let (capital_shift, back_shift) = babai_shifts(capital_size, size, d);
640
641 let k = estimate_k(&cf, &cg, capital_shift);
642 if k.iter().all(|&x| x == 0) {
643 break;
644 }
645 let kp = k_poly(&k);
646 let shifted_kf = mul_kf(&kp).shl_coeffs(back_shift);
647 let shifted_kg = mul_kg(&kp).shl_coeffs(back_shift);
648
649 if d > 53 {
650 let new_cs = cap_size(&cf.sub(&shifted_kf), &cg.sub(&shifted_kg));
651 if new_cs >= capital_size {
652 let k_old = estimate_k(&cf, &cg, (capital_size - 53) as u32);
654 if k_old.iter().all(|&x| x == 0) {
655 break;
656 }
657 let kpo = k_poly(&k_old);
658 let kf_old = mul_kf(&kpo).shl_coeffs(d as u32);
659 let kg_old = mul_kg(&kpo).shl_coeffs(d as u32);
660 cf.sub_assign(&kf_old);
661 cg.sub_assign(&kg_old);
662 continue;
663 }
664 }
665
666 cf.sub_assign(&shifted_kf);
667 cg.sub_assign(&shifted_kg);
668 }
669
670 for (c, i) in capital_f.coefficients.iter_mut().zip(0..n) {
671 *c = mw::to_bigint(cf.coeff(i));
672 }
673 for (c, i) in capital_g.coefficients.iter_mut().zip(0..n) {
674 *c = mw::to_bigint(cg.coeff(i));
675 }
676 Ok(())
677}
678
679pub(crate) fn babai_reduce_rns_bigint<const K: usize, P: NttPrimeList<K>>(
683 f: &Polynomial<BigInt>,
684 g: &Polynomial<BigInt>,
685 capital_f: &mut Polynomial<BigInt>,
686 capital_g: &mut Polynomial<BigInt>,
687) -> Result<(), String> {
688 babai_reduce_rns_generic::<BigInt, K, P>(f, g, capital_f, capital_g)
689}
690
691#[doc(hidden)]
705#[profiling]
706pub fn babai_reduce_i32(
707 f: &Polynomial<i32>,
708 g: &Polynomial<i32>,
709 capital_f: &mut Polynomial<i32>,
710 capital_g: &mut Polynomial<i32>,
711) -> Result<(), String> {
712 let f_ntt: Polynomial<U32Field> = f.map(|&i| U32Field::from(i)).fft();
713 let g_ntt: Polynomial<U32Field> = g.map(|&i| U32Field::from(i)).fft();
714
715 let bitsize = |itr: IntoIter<i32>| {
716 match itr.map(|i| i.abs()).max().unwrap() {
718 0 => 0,
719 m => (m * 2).ilog2().next_multiple_of(8) as usize,
720 }
721 };
722 let size = usize::max(
723 bitsize(
724 f.coefficients
725 .iter()
726 .chain(g.coefficients.iter())
727 .cloned()
728 .collect_vec()
729 .into_iter(),
730 ),
731 53,
732 );
733
734 let shift = (size as i64) - 53;
735 let f_adjusted = f
736 .map(|i| Complex64::new(i64::from(*i >> shift) as f64, 0.0))
737 .fft();
738 let g_adjusted = g
739 .map(|i| Complex64::new(i64::from(*i >> shift) as f64, 0.0))
740 .fft();
741
742 let f_star_adjusted = f_adjusted.map(|c| c.conj());
743 let g_star_adjusted = g_adjusted.map(|c| c.conj());
744 let denominator_fft =
745 f_adjusted.hadamard_mul(&f_star_adjusted) + g_adjusted.hadamard_mul(&g_star_adjusted);
746
747 let mut prev_capital_size = usize::MAX;
748 loop {
749 let capital_size = [
750 bitsize(
751 capital_f
752 .coefficients
753 .iter()
754 .chain(capital_g.coefficients.iter())
755 .copied()
756 .collect_vec()
757 .into_iter(),
758 ),
759 53,
760 ]
761 .into_iter()
762 .max()
763 .unwrap();
764
765 if capital_size < size || capital_size >= prev_capital_size {
766 break;
767 }
768 prev_capital_size = capital_size;
769
770 let capital_shift = (capital_size as i64) - 53;
771 let capital_f_adjusted = capital_f
772 .map(|bi| Complex64::new(i64::from(*bi >> capital_shift) as f64, 0.0))
773 .fft();
774 let capital_g_adjusted = capital_g
775 .map(|bi| Complex64::new(i64::from(*bi >> capital_shift) as f64, 0.0))
776 .fft();
777
778 let numerator = capital_f_adjusted.hadamard_mul(&f_star_adjusted)
779 + capital_g_adjusted.hadamard_mul(&g_star_adjusted);
780 let quotient = numerator.hadamard_div(&denominator_fft).ifft();
781
782 let k_ntt = quotient.map(|f| U32Field::from(f.re.round() as i32)).fft();
783
784 if k_ntt.is_zero() {
785 break;
786 }
787
788 let kf_ntt = k_ntt.hadamard_mul(&f_ntt).ifft();
789 let kg_ntt = k_ntt.hadamard_mul(&g_ntt).ifft();
790
791 let kf = kf_ntt.map(|p| p.balanced_value() as i32);
792 let kg = kg_ntt.map(|p| p.balanced_value() as i32);
793
794 *capital_f -= kf;
795 *capital_g -= kg;
796 }
797 Ok(())
798}
799
800#[profiling]
812pub(crate) fn babai_reduce_rns<const K: usize, P: NttPrimeList<K>>(
813 f: &Polynomial<i32>,
814 g: &Polynomial<i32>,
815 capital_f: &mut [Rns<K, P>],
816 capital_g: &mut [Rns<K, P>],
817) -> Result<(), String> {
818 let n = f.coefficients.len();
819
820 let tables = P::ntt_tables(n);
824
825 let mut f_rns_ntt: Vec<Rns<K, P>> = f.coefficients.iter().map(|&i| Rns::from_i32(i)).collect();
827 let mut g_rns_ntt: Vec<Rns<K, P>> = g.coefficients.iter().map(|&i| Rns::from_i32(i)).collect();
828 ntt_inplace_cached::<K, P>(&mut f_rns_ntt, &tables);
829 ntt_inplace_cached::<K, P>(&mut g_rns_ntt, &tables);
830
831 let f_fft = f.map(|&i| Complex64::new(i as f64, 0.0)).fft();
834 let g_fft = g.map(|&i| Complex64::new(i as f64, 0.0)).fft();
835 let f_adj = f_fft.map(|c| c.conj());
836 let g_adj = g_fft.map(|c| c.conj());
837 let denom_fft = f_fft.hadamard_mul(&f_adj) + g_fft.hadamard_mul(&g_adj);
838
839 let max_fg = f
842 .coefficients
843 .iter()
844 .chain(g.coefficients.iter())
845 .map(|&i| i.unsigned_abs())
846 .max()
847 .unwrap_or(1);
848 let fg_bits = if max_fg == 0 {
849 0u32
850 } else {
851 (max_fg.saturating_mul(2)).ilog2().next_multiple_of(8)
852 };
853 let size = fg_bits.max(53);
854
855 let mut prev_capital_size = u32::MAX;
856 loop {
857 let cf_i128: Vec<i128> = capital_f.iter().map(|r| r.to_i128()).collect();
860 let cg_i128: Vec<i128> = capital_g.iter().map(|r| r.to_i128()).collect();
861
862 let max_cap: u128 = cf_i128
863 .iter()
864 .chain(cg_i128.iter())
865 .map(|&v| v.unsigned_abs())
866 .max()
867 .unwrap_or(0);
868
869 let cap_bits = if max_cap == 0 {
870 0u32
871 } else {
872 (max_cap.saturating_mul(2)).ilog2().next_multiple_of(8)
873 };
874 let capital_size = cap_bits.max(53);
875
876 if capital_size < size || capital_size >= prev_capital_size {
877 break;
878 }
879 prev_capital_size = capital_size;
880
881 let capital_shift = capital_size - 53;
883
884 let capital_f_fft = Polynomial::new(
885 cf_i128
886 .iter()
887 .map(|&v| Complex64::new((v >> capital_shift) as f64, 0.0))
888 .collect::<Vec<_>>(),
889 )
890 .fft();
891 let capital_g_fft = Polynomial::new(
892 cg_i128
893 .iter()
894 .map(|&v| Complex64::new((v >> capital_shift) as f64, 0.0))
895 .collect::<Vec<_>>(),
896 )
897 .fft();
898
899 let numerator = capital_f_fft.hadamard_mul(&f_adj) + capital_g_fft.hadamard_mul(&g_adj);
900 let quotient = numerator.hadamard_div(&denom_fft).ifft();
901
902 let k: Vec<i64> = quotient
904 .coefficients
905 .iter()
906 .map(|c| c.re.round() as i64)
907 .collect();
908
909 if k.iter().all(|&x| x == 0) {
910 break;
911 }
912
913 let mut k_rns_ntt: Vec<Rns<K, P>> = k
915 .iter()
916 .map(|&k_adj| Rns::<K, P>::from_i128((k_adj as i128) << capital_shift))
917 .collect();
918 ntt_inplace_cached::<K, P>(&mut k_rns_ntt, &tables);
919
920 let mut kf: Vec<Rns<K, P>> = k_rns_ntt
921 .iter()
922 .zip(f_rns_ntt.iter())
923 .map(|(&a, &b)| a * b)
924 .collect();
925 let mut kg: Vec<Rns<K, P>> = k_rns_ntt
926 .iter()
927 .zip(g_rns_ntt.iter())
928 .map(|(&a, &b)| a * b)
929 .collect();
930 intt_inplace_cached::<K, P>(&mut kf, &tables);
931 intt_inplace_cached::<K, P>(&mut kg, &tables);
932
933 for i in 0..n {
934 capital_f[i] -= kf[i];
935 capital_g[i] -= kg[i];
936 }
937 }
938 Ok(())
939}
940
941#[profiling]
950fn xgcd(a: &BigInt, b: &BigInt) -> (BigInt, BigInt, BigInt) {
951 let (mut old_r, mut r) = (a.clone(), b.clone());
952 let (mut old_s, mut s) = (BigInt::one(), BigInt::zero());
953 let (mut old_t, mut t) = (BigInt::zero(), BigInt::one());
954
955 while r != BigInt::zero() {
956 let quotient = old_r.clone() / r.clone();
957 (old_r, r) = (r.clone(), old_r.clone() - quotient.clone() * r);
958 (old_s, s) = (s.clone(), old_s.clone() - quotient.clone() * s);
959 (old_t, t) = (t.clone(), old_t.clone() - quotient * t);
960 }
961
962 (old_r, old_s, old_t)
963}
964
965fn try_bigint_poly_to_i32(p: &Polynomial<BigInt>) -> Option<Polynomial<i32>> {
966 let coeffs: Option<Vec<i32>> = p
967 .coefficients
968 .iter()
969 .map(|c| i32::try_from(c.clone()).ok())
970 .collect();
971 coeffs.map(Polynomial::new)
972}
973
974fn babai_rns_with_fallback<const K: usize, P: NttPrimeList<K>>(
975 f: &Polynomial<BigInt>,
976 g: &Polynomial<BigInt>,
977 capital_f: &mut Polynomial<BigInt>,
978 capital_g: &mut Polynomial<BigInt>,
979) -> Result<(), String> {
980 let to_rns_vec = |poly: &Polynomial<BigInt>| -> Option<Vec<Rns<K, P>>> {
981 poly.coefficients
982 .iter()
983 .map(|c| i128::try_from(c.clone()).ok().map(Rns::<K, P>::from_i128))
984 .collect()
985 };
986 if let (Some(f_i32), Some(g_i32)) = (try_bigint_poly_to_i32(f), try_bigint_poly_to_i32(g)) {
987 if let (Some(mut cf_rns), Some(mut cg_rns)) = (to_rns_vec(capital_f), to_rns_vec(capital_g))
988 {
989 if babai_reduce_rns::<K, P>(&f_i32, &g_i32, &mut cf_rns, &mut cg_rns).is_ok() {
990 for (c, r) in capital_f.coefficients.iter_mut().zip(cf_rns.iter()) {
991 *c = BigInt::from(r.to_i64());
992 }
993 for (c, r) in capital_g.coefficients.iter_mut().zip(cg_rns.iter()) {
994 *c = BigInt::from(r.to_i64());
995 }
996 return Ok(());
997 }
998 }
999 }
1000 babai_reduce_bigint(f, g, capital_f, capital_g)
1001}
1002
1003#[doc(hidden)]
1009pub fn babai_reduce_rns_depth1(
1010 f: &Polynomial<i32>,
1011 g: &Polynomial<i32>,
1012 capital_f: &mut [i128],
1013 capital_g: &mut [i128],
1014) -> Result<(), String> {
1015 let mut cf_rns: Vec<Rns<2, NttPrimes24Bit2>> =
1016 capital_f.iter().map(|&v| Rns::from_i128(v)).collect();
1017 let mut cg_rns: Vec<Rns<2, NttPrimes24Bit2>> =
1018 capital_g.iter().map(|&v| Rns::from_i128(v)).collect();
1019 babai_reduce_rns::<2, NttPrimes24Bit2>(f, g, &mut cf_rns, &mut cg_rns)?;
1020 for (out, r) in capital_f.iter_mut().zip(cf_rns.iter()) {
1021 *out = r.to_i128();
1022 }
1023 for (out, r) in capital_g.iter_mut().zip(cg_rns.iter()) {
1024 *out = r.to_i128();
1025 }
1026 Ok(())
1027}
1028
1029#[doc(hidden)]
1033pub fn babai_reduce_rns_depth2(
1034 f: &Polynomial<i32>,
1035 g: &Polynomial<i32>,
1036 capital_f: &mut [i128],
1037 capital_g: &mut [i128],
1038) -> Result<(), String> {
1039 let mut cf_rns: Vec<Rns<4, NttPrimes24Bit4>> =
1040 capital_f.iter().map(|&v| Rns::from_i128(v)).collect();
1041 let mut cg_rns: Vec<Rns<4, NttPrimes24Bit4>> =
1042 capital_g.iter().map(|&v| Rns::from_i128(v)).collect();
1043 babai_reduce_rns::<4, NttPrimes24Bit4>(f, g, &mut cf_rns, &mut cg_rns)?;
1044 for (out, r) in capital_f.iter_mut().zip(cf_rns.iter()) {
1045 *out = r.to_i128();
1046 }
1047 for (out, r) in capital_g.iter_mut().zip(cg_rns.iter()) {
1048 *out = r.to_i128();
1049 }
1050 Ok(())
1051}
1052
1053pub(crate) fn babai_reduce_rns_packed<const LP: usize, const K: usize, P: NttPrimeList<K>>(
1070 f: &Polynomial<BigInt>,
1071 g: &Polynomial<BigInt>,
1072 capital_f: &mut Polynomial<BigInt>,
1073 capital_g: &mut Polynomial<BigInt>,
1074) -> Result<(), String> {
1075 babai_reduce_rns_generic::<Packed<LP>, K, P>(f, g, capital_f, capital_g)
1076}
1077
1078#[doc(hidden)]
1085pub fn babai_reduce_rns_packed_depth3(
1086 f: &Polynomial<BigInt>,
1087 g: &Polynomial<BigInt>,
1088 capital_f: &mut Polynomial<BigInt>,
1089 capital_g: &mut Polynomial<BigInt>,
1090) -> Result<(), String> {
1091 babai_reduce_rns_packed::<4, 5, NttPrimes24Bit5>(f, g, capital_f, capital_g)
1092}
1093
1094#[doc(hidden)]
1111pub fn babai_reduce_rns_packed_depth4(
1112 f: &Polynomial<BigInt>,
1113 g: &Polynomial<BigInt>,
1114 capital_f: &mut Polynomial<BigInt>,
1115 capital_g: &mut Polynomial<BigInt>,
1116) -> Result<(), String> {
1117 babai_reduce_rns_packed::<5, 8, NttPrimes24Bit8>(f, g, capital_f, capital_g)
1118}
1119
1120#[doc(hidden)]
1126pub fn babai_reduce_rns_bigint_depth3(
1127 f: &Polynomial<BigInt>,
1128 g: &Polynomial<BigInt>,
1129 capital_f: &mut Polynomial<BigInt>,
1130 capital_g: &mut Polynomial<BigInt>,
1131) -> Result<(), String> {
1132 babai_reduce_rns_bigint::<5, NttPrimes24Bit5>(f, g, capital_f, capital_g)
1133}
1134
1135#[doc(hidden)]
1145pub fn babai_reduce_rns_bigint_depth4(
1146 f: &Polynomial<BigInt>,
1147 g: &Polynomial<BigInt>,
1148 capital_f: &mut Polynomial<BigInt>,
1149 capital_g: &mut Polynomial<BigInt>,
1150) -> Result<(), String> {
1151 babai_reduce_rns_bigint::<8, NttPrimes24Bit8>(f, g, capital_f, capital_g)
1152}
1153
1154const fn rns_runtime_babai_params(depth: usize) -> (usize, usize) {
1160 match depth {
1161 2 => (5, 4),
1162 3 => (6, 5),
1163 4 => (8, 7),
1164 5 => (13, 12),
1165 6 => (22, 22),
1166 7 => (41, 40),
1167 8 => (77, 77),
1168 9 => (149, 152),
1169 _ => (0, 0),
1170 }
1171}
1172
1173fn rns_runtime_max_depth() -> usize {
1184 use std::sync::LazyLock;
1185 static D: LazyLock<usize> = LazyLock::new(|| {
1186 std::env::var("RNS_RUNTIME_MAX_DEPTH")
1187 .ok()
1188 .and_then(|s| s.parse().ok())
1189 .unwrap_or(9)
1190 });
1191 *D
1192}
1193
1194#[profiling]
1201fn field_norm_flatword(p: &Polynomial<BigInt>) -> Polynomial<BigInt> {
1202 let max_bits = p.coefficients.iter().map(|c| c.bits()).max().unwrap_or(0);
1203 let w_in = (max_bits / 64 + 1) as usize;
1205 MultiwordPoly::from_bigint_poly(p, w_in)
1206 .field_norm()
1207 .to_bigint_poly()
1208}
1209
1210#[profiling]
1217fn construction_mul_flatword(a: &Polynomial<BigInt>, b: &Polynomial<BigInt>) -> Polynomial<BigInt> {
1218 let n = a.coefficients.len();
1219 let bits_a = a.coefficients.iter().map(|c| c.bits()).max().unwrap_or(0);
1220 let bits_b = b.coefficients.iter().map(|c| c.bits()).max().unwrap_or(0);
1221 let wa = (bits_a / 64 + 1) as usize;
1223 let wb = (bits_b / 64 + 1) as usize;
1224 let prod_bits = bits_a + bits_b + (n as u64).max(1).ilog2() as u64 + 2;
1226 let out_w = (prod_bits / 64 + 1) as usize;
1227
1228 let ma = MultiwordPoly::from_bigint_poly(a, wa);
1229 let mb = MultiwordPoly::from_bigint_poly(b, wb);
1230 ma.negacyclic_mul(&mb, out_w).to_bigint_poly()
1231}
1232
1233#[profiling]
1242fn ntru_solve(
1243 f: &Polynomial<BigInt>,
1244 g: &Polynomial<BigInt>,
1245 depth: usize,
1246 max_rns_depth: usize,
1247) -> Option<(Polynomial<BigInt>, Polynomial<BigInt>)> {
1248 let n = f.coefficients.len();
1249 if n == 1 {
1250 let (gcd, u, v) = xgcd(&f.coefficients[0], &g.coefficients[0]);
1251 if gcd != BigInt::one() {
1252 return None;
1253 }
1254 return Some((
1255 (Polynomial::new(vec![-v * BigInt::from_u32(Q as u32).unwrap()])),
1256 Polynomial::new(vec![u * BigInt::from_u32(Q as u32).unwrap()]),
1257 ));
1258 }
1259
1260 let f_prime = field_norm_flatword(f);
1261 let g_prime = field_norm_flatword(g);
1262 let (capital_f_prime, capital_g_prime) =
1263 ntru_solve(&f_prime, &g_prime, depth + 1, max_rns_depth)?;
1264
1265 let capital_f_prime_xsq = capital_f_prime.lift_next_cyclotomic();
1266 let capital_g_prime_xsq = capital_g_prime.lift_next_cyclotomic();
1267 let f_minx = f.galois_adjoint();
1268 let g_minx = g.galois_adjoint();
1269
1270 let mut capital_f = construction_mul_flatword(&capital_f_prime_xsq, &g_minx);
1271 let mut capital_g = construction_mul_flatword(&capital_g_prime_xsq, &f_minx);
1272
1273 let babai_result = match depth {
1274 1 if max_rns_depth >= 1 => {
1275 babai_rns_with_fallback::<2, NttPrimes24Bit2>(f, g, &mut capital_f, &mut capital_g)
1276 }
1277 2 if max_rns_depth >= 2 => {
1278 babai_rns_with_fallback::<4, NttPrimes24Bit4>(f, g, &mut capital_f, &mut capital_g)
1279 }
1280 d if (2..=rns_runtime_max_depth()).contains(&d) => {
1285 let (k_primes, cap_w) = rns_runtime_babai_params(d);
1286 babai_reduce_rns_runtime(f, g, &mut capital_f, &mut capital_g, k_primes, cap_w)
1287 }
1288 _ => babai_reduce_bigint(f, g, &mut capital_f, &mut capital_g),
1289 };
1290 match babai_result {
1291 Ok(_) => Some((capital_f, capital_g)),
1292 Err(_e) => {
1293 #[cfg(test)]
1294 {
1295 panic!("{}", _e);
1296 }
1297 #[cfg(not(test))]
1298 {
1299 None
1300 }
1301 }
1302 }
1303}
1304
1305#[profiling]
1306fn ntru_solve_entrypoint(
1307 f: Polynomial<i32>,
1308 g: Polynomial<i32>,
1309 max_rns_depth: usize,
1310) -> Option<(Polynomial<i32>, Polynomial<i32>)> {
1311 let g_prime = g.field_norm().map(|c| BigInt::from(*c));
1312 let f_prime = f.field_norm().map(|c| BigInt::from(*c));
1313 let (capital_f_prime_bi, capital_g_prime_bi) =
1314 ntru_solve(&f_prime, &g_prime, 1, max_rns_depth)?;
1315
1316 let capital_f_prime_coefficients = capital_f_prime_bi
1317 .coefficients
1318 .into_iter()
1319 .map(i32::try_from)
1320 .collect_vec();
1321 let capital_g_prime_coefficients = capital_g_prime_bi
1322 .coefficients
1323 .into_iter()
1324 .map(i32::try_from)
1325 .collect_vec();
1326
1327 if !capital_f_prime_coefficients
1328 .iter()
1329 .chain(capital_g_prime_coefficients.iter())
1330 .all(|c| c.is_ok())
1331 {
1332 return None;
1333 }
1334 let capital_f_prime = Polynomial::new(
1335 capital_f_prime_coefficients
1336 .into_iter()
1337 .map(|c| c.unwrap())
1338 .collect_vec(),
1339 );
1340 let capital_g_prime = Polynomial::new(
1341 capital_g_prime_coefficients
1342 .into_iter()
1343 .map(|c| c.unwrap())
1344 .collect_vec(),
1345 );
1346
1347 let capital_f_prime_xsq = capital_f_prime.lift_next_cyclotomic();
1348 let capital_g_prime_xsq = capital_g_prime.lift_next_cyclotomic();
1349 let f_minx = f.galois_adjoint();
1350 let g_minx = g.galois_adjoint();
1351
1352 let cfp_ntt = capital_f_prime_xsq.map(|c| U32Field::from(*c)).fft();
1355 let cgp_ntt = capital_g_prime_xsq.map(|c| U32Field::from(*c)).fft();
1356 let gm_ntt = g_minx.map(|c| U32Field::from(*c)).fft();
1357 let fm_ntt = f_minx.map(|c| U32Field::from(*c)).fft();
1358 let cf_ntt = cfp_ntt.hadamard_mul(&gm_ntt).ifft();
1359 let cg_ntt = cgp_ntt.hadamard_mul(&fm_ntt).ifft();
1360
1361 let mut capital_f = cf_ntt.map(|c| c.balanced_value() as i32);
1362 let mut capital_g = cg_ntt.map(|c| c.balanced_value() as i32);
1363
1364 match babai_reduce_i32(&f, &g, &mut capital_f, &mut capital_g) {
1365 Ok(_) => Some((capital_f, capital_g)),
1366 Err(_e) => {
1367 #[cfg(test)]
1368 {
1369 panic!("{}", _e);
1370 }
1371 #[cfg(not(test))]
1372 {
1373 None
1374 }
1375 }
1376 }
1377}
1378
1379fn ntru_equation_holds(
1386 f: &Polynomial<i16>,
1387 g: &Polynomial<i16>,
1388 capital_f: &Polynomial<i32>,
1389 capital_g: &Polynomial<i32>,
1390) -> bool {
1391 let n = f.coefficients.len();
1392 let f_i64 = f.map(|&i| i as i64);
1393 let g_i64 = g.map(|&i| i as i64);
1394 let cf_i64 = capital_f.map(|&i| i as i64);
1395 let cg_i64 = capital_g.map(|&i| i as i64);
1396 let ntru_eq =
1397 (f_i64 * cg_i64).reduce_by_cyclotomic(n) - (g_i64 * cf_i64).reduce_by_cyclotomic(n);
1398 ntru_eq == Polynomial::constant(Q as i64)
1399}
1400
1401#[doc(hidden)]
1409#[profiling]
1410pub fn ntru_gen<R: Rng + ?Sized>(
1411 n: usize,
1412 rng: &mut R,
1413) -> (
1414 Polynomial<i16>,
1415 Polynomial<i16>,
1416 Polynomial<i16>,
1417 Polynomial<i16>,
1418) {
1419 loop {
1422 let f = gen_poly(n, rng);
1423 let g = gen_poly(n, rng);
1424
1425 let f_ntt = f.map(|&i| Felt::from(i)).fft();
1426 if f_ntt.coefficients.iter().any(|e| e.is_zero()) {
1427 continue;
1428 }
1429 let gamma = gram_schmidt_norm_squared(&f, &g);
1430 if gamma > 1.3689f64 * (Q as f64) {
1431 continue;
1432 }
1433
1434 if let Some((capital_f, capital_g)) =
1439 ntru_solve_entrypoint(f.map(|&i| i as i32), g.map(|&i| i as i32), 1)
1440 {
1441 if !ntru_equation_holds(&f, &g, &capital_f, &capital_g) {
1446 continue;
1447 }
1448 return (
1449 f,
1450 g,
1451 capital_f.map(|&i| i as i16),
1452 capital_g.map(|&i| i as i16),
1453 );
1454 }
1455 }
1456}
1457
1458#[doc(hidden)]
1468#[profiling]
1469pub fn ntru_gen_with_rns_depth<R: Rng + ?Sized>(
1470 n: usize,
1471 rng: &mut R,
1472 max_rns_depth: usize,
1473) -> (
1474 Polynomial<i16>,
1475 Polynomial<i16>,
1476 Polynomial<i16>,
1477 Polynomial<i16>,
1478) {
1479 loop {
1480 let f = gen_poly(n, rng);
1481 let g = gen_poly(n, rng);
1482
1483 let f_ntt = f.map(|&i| Felt::from(i)).fft();
1484 if f_ntt.coefficients.iter().any(|e| e.is_zero()) {
1485 continue;
1486 }
1487 let gamma = gram_schmidt_norm_squared(&f, &g);
1488 if gamma > 1.3689f64 * (Q as f64) {
1489 continue;
1490 }
1491
1492 if let Some((capital_f, capital_g)) =
1493 ntru_solve_entrypoint(f.map(|&i| i as i32), g.map(|&i| i as i32), max_rns_depth)
1494 {
1495 if !ntru_equation_holds(&f, &g, &capital_f, &capital_g) {
1498 continue;
1499 }
1500 return (
1501 f,
1502 g,
1503 capital_f.map(|&i| i as i16),
1504 capital_g.map(|&i| i as i16),
1505 );
1506 }
1507 }
1508}
1509
1510#[profiling]
1515fn gen_poly<R: Rng + ?Sized>(n: usize, rng: &mut R) -> Polynomial<i16> {
1516 let mu = FixedPoint128::ZERO;
1517 let sigma_star = FixedPoint128::from(1.43300980528773f64);
1518 let ctx = SamplerZCtx::new(sigma_star, sigma_star - FixedPoint128::from(0.001f64));
1520 const NUM_COEFFICIENTS: usize = 4096;
1521 Polynomial {
1522 coefficients: (0..NUM_COEFFICIENTS)
1523 .map(|_| ctx.sample(mu, rng))
1524 .collect_vec()
1525 .chunks(NUM_COEFFICIENTS / n)
1526 .map(|ch| ch.iter().sum())
1527 .collect_vec(),
1528 }
1529}
1530
1531#[profiling]
1536fn gram_schmidt_norm_squared(f: &Polynomial<i16>, g: &Polynomial<i16>) -> f64 {
1537 let n = f.coefficients.len();
1538 let gamma1 = f64::from(f.l2_norm_squared() + g.l2_norm_squared());
1539
1540 let fp = |i: &i16| Complex64::new(*i as f64, 0.0);
1541 let q_fp = Q as f64;
1542 let n_fp = n as f64;
1543
1544 let f_fft = f.map(fp).fft();
1545 let g_fft = g.map(fp).fft();
1546 let f_adj_fft = f_fft.map(|c| c.conj());
1547 let g_adj_fft = g_fft.map(|c| c.conj());
1548 let ffgg_fft = f_fft.hadamard_mul(&f_adj_fft) + g_fft.hadamard_mul(&g_adj_fft);
1549 let ffgg_fft_inverse = ffgg_fft.hadamard_inv();
1550 let qf_over_ffgg_fft = f_adj_fft.map(|c| c * q_fp).hadamard_mul(&ffgg_fft_inverse);
1551 let qg_over_ffgg_fft = g_adj_fft.map(|c| c * q_fp).hadamard_mul(&ffgg_fft_inverse);
1552 let norm_f_over_ffgg_squared = qf_over_ffgg_fft
1553 .coefficients
1554 .iter()
1555 .map(|c| (c * c.conj()).re)
1556 .sum::<f64>()
1557 / n_fp;
1558 let norm_g_over_ffgg_squared = qg_over_ffgg_fft
1559 .coefficients
1560 .iter()
1561 .map(|c| (c * c.conj()).re)
1562 .sum::<f64>()
1563 / n_fp;
1564
1565 let gamma2 = norm_f_over_ffgg_squared + norm_g_over_ffgg_squared;
1566
1567 f64::max(gamma1, gamma2)
1568}
1569
1570#[cfg(test)]
1571mod test {
1572
1573 use std::str::FromStr;
1574
1575 use itertools::Itertools;
1576 use num::BigInt;
1577 use proptest::collection::vec;
1578 use proptest::prop_assert_eq;
1579 use proptest::strategy::Just;
1580 use rand::{rngs::StdRng, SeedableRng};
1581 use test_strategy::proptest as strategy_proptest;
1582
1583 use crate::math::{
1584 babai_reduce_i32, babai_reduce_rns, gram_schmidt_norm_squared, ntru_gen, ntru_solve,
1585 };
1586 use crate::{
1587 fp_field::FpField,
1588 polynomial::Polynomial,
1589 rns::{NttPrimeList, NttPrimes24Bit2, NttPrimes24Bit4, PrimeList, Rns},
1590 };
1591
1592 use super::babai_reduce_bigint;
1593
1594 struct NttPrimes3;
1596 impl PrimeList<3> for NttPrimes3 {
1597 const PRIMES: [u32; 3] = [786_433, 998_244_353, 1_073_754_113];
1598 }
1599 impl NttPrimeList<3> for NttPrimes3 {
1600 const ROOTS_OF_UNITY_2048: [u32; 3] = [
1601 FpField::<786_433>::primitive_nth_root_of_unity(2048).value(),
1602 FpField::<998_244_353>::primitive_nth_root_of_unity(2048).value(),
1603 FpField::<1_073_754_113>::primitive_nth_root_of_unity(2048).value(),
1604 ];
1605 }
1606 type Rns3 = Rns<3, NttPrimes3>;
1607
1608 #[test]
1609 fn ntt_primes3_covers_babai_depths_0_and_1() {
1610 let capacity: f64 = NttPrimes3::PRIMES
1612 .iter()
1613 .map(|&p| f64::log2(p as f64))
1614 .sum::<f64>()
1615 - 1.0;
1616
1617 let required = |d: usize| {
1620 let (_, _, avg_f, std_f) = super::NTRU_SOLVE_BABAI_COEFF_BITS[d];
1621 avg_f + 6.0 * std_f + 2.0
1622 };
1623
1624 assert!(
1625 capacity >= required(0),
1626 "depth 0: {capacity:.1} < {:.1}",
1627 required(0)
1628 );
1629 assert!(
1630 capacity >= required(1),
1631 "depth 1: {capacity:.1} < {:.1}",
1632 required(1)
1633 );
1634 assert!(
1637 capacity < required(2),
1638 "depth 2: {capacity:.1} >= {:.1}",
1639 required(2)
1640 );
1641 }
1642
1643 #[test]
1644 fn ntt_primes24_2_covers_depth1() {
1645 let capacity: f64 = NttPrimes24Bit2::PRIMES
1646 .iter()
1647 .map(|&p| f64::log2(p as f64))
1648 .sum::<f64>()
1649 - 1.0;
1650 let required = |d: usize| {
1651 let (_, _, avg_f, std_f) = super::NTRU_SOLVE_BABAI_COEFF_BITS[d];
1652 avg_f + 6.0 * std_f + 2.0
1653 };
1654 assert!(
1655 capacity >= required(1),
1656 "depth 1: {capacity:.1} < {:.1}",
1657 required(1)
1658 );
1659 assert!(
1660 capacity < required(2),
1661 "depth 2: {capacity:.1} >= {:.1}",
1662 required(2)
1663 );
1664 }
1665
1666 #[test]
1667 fn ntt_primes24_4_covers_depth2() {
1668 let capacity: f64 = NttPrimes24Bit4::PRIMES
1669 .iter()
1670 .map(|&p| f64::log2(p as f64))
1671 .sum::<f64>()
1672 - 1.0;
1673 let required = |d: usize| {
1674 let (_, _, avg_f, std_f) = super::NTRU_SOLVE_BABAI_COEFF_BITS[d];
1675 avg_f + 6.0 * std_f + 2.0
1676 };
1677 assert!(
1678 capacity >= required(2),
1679 "depth 2: {capacity:.1} < {:.1}",
1680 required(2)
1681 );
1682 assert!(
1683 capacity < required(3),
1684 "depth 3: {capacity:.1} >= {:.1}",
1685 required(3)
1686 );
1687 }
1688
1689 fn babai_infinite_loop_polynomials() -> (
1690 Polynomial<BigInt>,
1691 Polynomial<BigInt>,
1692 Polynomial<BigInt>,
1693 Polynomial<BigInt>,
1694 ) {
1695 let f = Polynomial::new(
1696 [
1697 BigInt::from_str("6426042728002").unwrap(),
1698 BigInt::from_str("-20675284604736").unwrap(),
1699 BigInt::from_str("-12121913318466").unwrap(),
1700 BigInt::from_str("-27836101162563").unwrap(),
1701 ]
1702 .to_vec(),
1703 );
1704
1705 let g = Polynomial::new(
1706 [
1707 BigInt::from_str("-1001246212").unwrap(),
1708 BigInt::from_str("-1347303037").unwrap(),
1709 BigInt::from_str("987026048").unwrap(),
1710 BigInt::from_str("-1001311747").unwrap(),
1711 ]
1712 .to_vec(),
1713 );
1714
1715 let capital_f = Polynomial::new(
1716 [
1717 BigInt::from_str(
1718 "563985131491945032326798334533872091781886676547754689048287010878681928",
1719 )
1720 .unwrap(),
1721 BigInt::from_str(
1722 "-348444005402208553421931883447687919671423051554816023996113866522386058",
1723 )
1724 .unwrap(),
1725 BigInt::from_str(
1726 "-85657170778585026649528684432821341936755757853602491207147473952485632",
1727 )
1728 .unwrap(),
1729 BigInt::from_str(
1730 "135623655239747178410899900677875843487151183900794566193191499131611018",
1731 )
1732 .unwrap(),
1733 ]
1734 .to_vec(),
1735 );
1736
1737 let capital_g = Polynomial::new(
1738 [
1739 BigInt::from_str(
1740 "49040356584788663746447138446729467702643846166576265941049418069366",
1741 )
1742 .unwrap(),
1743 BigInt::from_str(
1744 "-57075549200927059197269512430308877512934179841045274854176350745681",
1745 )
1746 .unwrap(),
1747 BigInt::from_str(
1748 "18442173959410247991253446345066800376513088376845717824090327663990",
1749 )
1750 .unwrap(),
1751 BigInt::from_str(
1752 "19528334302175388221061434098432127604592213845277598673231565264960",
1753 )
1754 .unwrap(),
1755 ]
1756 .to_vec(),
1757 );
1758
1759 (f, g, capital_f, capital_g)
1760 }
1761
1762 #[test]
1763 fn babai_oscillation_terminates() {
1764 let (f, g, mut capital_f, mut capital_g) = babai_infinite_loop_polynomials();
1765 let _ = babai_reduce_bigint(&f, &g, &mut capital_f, &mut capital_g);
1766 }
1767
1768 fn rand_signed_bigint(rng: &mut StdRng, bits: u32) -> BigInt {
1770 use rand::RngExt;
1771 let mut v = BigInt::from(0);
1772 for _ in 0..bits {
1773 v = (v << 1) | BigInt::from(rng.random::<bool>() as u8);
1774 }
1775 if rng.random::<bool>() {
1776 -v
1777 } else {
1778 v
1779 }
1780 }
1781
1782 fn depth2_inputs(
1788 rng: &mut StdRng,
1789 ) -> (
1790 Polynomial<BigInt>,
1791 Polynomial<BigInt>,
1792 Polynomial<BigInt>,
1793 Polynomial<BigInt>,
1794 ) {
1795 let n = 256;
1796 let f = Polynomial::new(
1797 (0..n)
1798 .map(|_| rand_signed_bigint(rng, 24))
1799 .collect::<Vec<_>>(),
1800 );
1801 let g = Polynomial::new(
1802 (0..n)
1803 .map(|_| rand_signed_bigint(rng, 24))
1804 .collect::<Vec<_>>(),
1805 );
1806 let cf = Polynomial::new(
1807 (0..n)
1808 .map(|_| rand_signed_bigint(rng, 78))
1809 .collect::<Vec<_>>(),
1810 );
1811 let cg = Polynomial::new(
1812 (0..n)
1813 .map(|_| rand_signed_bigint(rng, 78))
1814 .collect::<Vec<_>>(),
1815 );
1816 (f, g, cf, cg)
1817 }
1818
1819 fn depth3_inputs(
1825 rng: &mut StdRng,
1826 ) -> (
1827 Polynomial<BigInt>,
1828 Polynomial<BigInt>,
1829 Polynomial<BigInt>,
1830 Polynomial<BigInt>,
1831 ) {
1832 let n = 128;
1833 let f = Polynomial::new(
1834 (0..n)
1835 .map(|_| rand_signed_bigint(rng, 50))
1836 .collect::<Vec<_>>(),
1837 );
1838 let g = Polynomial::new(
1839 (0..n)
1840 .map(|_| rand_signed_bigint(rng, 50))
1841 .collect::<Vec<_>>(),
1842 );
1843 let cf = Polynomial::new(
1844 (0..n)
1845 .map(|_| rand_signed_bigint(rng, 154))
1846 .collect::<Vec<_>>(),
1847 );
1848 let cg = Polynomial::new(
1849 (0..n)
1850 .map(|_| rand_signed_bigint(rng, 154))
1851 .collect::<Vec<_>>(),
1852 );
1853 (f, g, cf, cg)
1854 }
1855
1856 fn depth4_inputs(
1860 rng: &mut StdRng,
1861 ) -> (
1862 Polynomial<BigInt>,
1863 Polynomial<BigInt>,
1864 Polynomial<BigInt>,
1865 Polynomial<BigInt>,
1866 ) {
1867 let n = 64;
1868 let f = Polynomial::new(
1869 (0..n)
1870 .map(|_| rand_signed_bigint(rng, 101))
1871 .collect::<Vec<_>>(),
1872 );
1873 let g = Polynomial::new(
1874 (0..n)
1875 .map(|_| rand_signed_bigint(rng, 101))
1876 .collect::<Vec<_>>(),
1877 );
1878 let cf = Polynomial::new(
1879 (0..n)
1880 .map(|_| rand_signed_bigint(rng, 303))
1881 .collect::<Vec<_>>(),
1882 );
1883 let cg = Polynomial::new(
1884 (0..n)
1885 .map(|_| rand_signed_bigint(rng, 303))
1886 .collect::<Vec<_>>(),
1887 );
1888 (f, g, cf, cg)
1889 }
1890
1891 #[test]
1908 fn product_size_model_matches_measurements() {
1909 for &(f_bits, measured) in &[(50.0_f64, 107.0_f64), (101.0, 155.0)] {
1911 let model = f_bits + 54.0;
1912 assert!(
1913 (model - measured).abs() <= 8.0,
1914 "product-size model {model:.0} far from measured {measured:.0} (bits(f)={f_bits})"
1915 );
1916 }
1917 }
1918
1919 #[test]
1924 fn babai_reduce_rns_bigint_depth3_matches_bigint() {
1925 use super::babai_reduce_rns_bigint_depth3;
1926
1927 let mut rng = StdRng::seed_from_u64(0xba_ba_13_d3);
1928 for _ in 0..32 {
1929 let (f, g, cap_f, cap_g) = depth3_inputs(&mut rng);
1930
1931 let (mut bf, mut bg) = (cap_f.clone(), cap_g.clone());
1932 babai_reduce_bigint(&f, &g, &mut bf, &mut bg).unwrap();
1933
1934 let (mut rf, mut rg) = (cap_f, cap_g);
1935 babai_reduce_rns_bigint_depth3(&f, &g, &mut rf, &mut rg).unwrap();
1936
1937 assert_eq!(bf, rf, "capital_F mismatch between bigint and rns-bigint");
1938 assert_eq!(bg, rg, "capital_G mismatch between bigint and rns-bigint");
1939 }
1940 }
1941
1942 #[test]
1945 fn babai_reduce_rns_packed_depth3_matches_bigint() {
1946 use super::babai_reduce_rns_packed_depth3;
1947
1948 let mut rng = StdRng::seed_from_u64(0x09ac_edd3);
1949 for _ in 0..32 {
1950 let (f, g, cap_f, cap_g) = depth3_inputs(&mut rng);
1951
1952 let (mut bf, mut bg) = (cap_f.clone(), cap_g.clone());
1953 babai_reduce_bigint(&f, &g, &mut bf, &mut bg).unwrap();
1954
1955 let (mut pf, mut pg) = (cap_f, cap_g);
1956 babai_reduce_rns_packed_depth3(&f, &g, &mut pf, &mut pg).unwrap();
1957
1958 assert_eq!(bf, pf, "capital_F mismatch between bigint and packed");
1959 assert_eq!(bg, pg, "capital_G mismatch between bigint and packed");
1960 }
1961 }
1962
1963 fn depth5_inputs(
1966 rng: &mut StdRng,
1967 ) -> (
1968 Polynomial<BigInt>,
1969 Polynomial<BigInt>,
1970 Polynomial<BigInt>,
1971 Polynomial<BigInt>,
1972 ) {
1973 let n = 32;
1974 let f = Polynomial::new(
1975 (0..n)
1976 .map(|_| rand_signed_bigint(rng, 202))
1977 .collect::<Vec<_>>(),
1978 );
1979 let g = Polynomial::new(
1980 (0..n)
1981 .map(|_| rand_signed_bigint(rng, 202))
1982 .collect::<Vec<_>>(),
1983 );
1984 let cf = Polynomial::new(
1985 (0..n)
1986 .map(|_| rand_signed_bigint(rng, 600))
1987 .collect::<Vec<_>>(),
1988 );
1989 let cg = Polynomial::new(
1990 (0..n)
1991 .map(|_| rand_signed_bigint(rng, 600))
1992 .collect::<Vec<_>>(),
1993 );
1994 (f, g, cf, cg)
1995 }
1996
1997 fn depth6_inputs(
2000 rng: &mut StdRng,
2001 ) -> (
2002 Polynomial<BigInt>,
2003 Polynomial<BigInt>,
2004 Polynomial<BigInt>,
2005 Polynomial<BigInt>,
2006 ) {
2007 let n = 16;
2008 let f = Polynomial::new(
2009 (0..n)
2010 .map(|_| rand_signed_bigint(rng, 401))
2011 .collect::<Vec<_>>(),
2012 );
2013 let g = Polynomial::new(
2014 (0..n)
2015 .map(|_| rand_signed_bigint(rng, 401))
2016 .collect::<Vec<_>>(),
2017 );
2018 let cf = Polynomial::new(
2019 (0..n)
2020 .map(|_| rand_signed_bigint(rng, 1189))
2021 .collect::<Vec<_>>(),
2022 );
2023 let cg = Polynomial::new(
2024 (0..n)
2025 .map(|_| rand_signed_bigint(rng, 1189))
2026 .collect::<Vec<_>>(),
2027 );
2028 (f, g, cf, cg)
2029 }
2030
2031 fn depth7_inputs(
2035 rng: &mut StdRng,
2036 ) -> (
2037 Polynomial<BigInt>,
2038 Polynomial<BigInt>,
2039 Polynomial<BigInt>,
2040 Polynomial<BigInt>,
2041 ) {
2042 let n = 8;
2043 let f = Polynomial::new(
2044 (0..n)
2045 .map(|_| rand_signed_bigint(rng, 850))
2046 .collect::<Vec<_>>(),
2047 );
2048 let g = Polynomial::new(
2049 (0..n)
2050 .map(|_| rand_signed_bigint(rng, 850))
2051 .collect::<Vec<_>>(),
2052 );
2053 let cf = Polynomial::new(
2054 (0..n)
2055 .map(|_| rand_signed_bigint(rng, 2000))
2056 .collect::<Vec<_>>(),
2057 );
2058 let cg = Polynomial::new(
2059 (0..n)
2060 .map(|_| rand_signed_bigint(rng, 2000))
2061 .collect::<Vec<_>>(),
2062 );
2063 (f, g, cf, cg)
2064 }
2065
2066 fn depth8_inputs(
2069 rng: &mut StdRng,
2070 ) -> (
2071 Polynomial<BigInt>,
2072 Polynomial<BigInt>,
2073 Polynomial<BigInt>,
2074 Polynomial<BigInt>,
2075 ) {
2076 let n = 4;
2077 let f = Polynomial::new(
2078 (0..n)
2079 .map(|_| rand_signed_bigint(rng, 1577))
2080 .collect::<Vec<_>>(),
2081 );
2082 let g = Polynomial::new(
2083 (0..n)
2084 .map(|_| rand_signed_bigint(rng, 1577))
2085 .collect::<Vec<_>>(),
2086 );
2087 let cf = Polynomial::new(
2088 (0..n)
2089 .map(|_| rand_signed_bigint(rng, 4703))
2090 .collect::<Vec<_>>(),
2091 );
2092 let cg = Polynomial::new(
2093 (0..n)
2094 .map(|_| rand_signed_bigint(rng, 4703))
2095 .collect::<Vec<_>>(),
2096 );
2097 (f, g, cf, cg)
2098 }
2099
2100 fn depth9_inputs(
2104 rng: &mut StdRng,
2105 ) -> (
2106 Polynomial<BigInt>,
2107 Polynomial<BigInt>,
2108 Polynomial<BigInt>,
2109 Polynomial<BigInt>,
2110 ) {
2111 let n = 2;
2112 let f = Polynomial::new(
2113 (0..n)
2114 .map(|_| rand_signed_bigint(rng, 3138))
2115 .collect::<Vec<_>>(),
2116 );
2117 let g = Polynomial::new(
2118 (0..n)
2119 .map(|_| rand_signed_bigint(rng, 3138))
2120 .collect::<Vec<_>>(),
2121 );
2122 let cf = Polynomial::new(
2123 (0..n)
2124 .map(|_| rand_signed_bigint(rng, 9403))
2125 .collect::<Vec<_>>(),
2126 );
2127 let cg = Polynomial::new(
2128 (0..n)
2129 .map(|_| rand_signed_bigint(rng, 9403))
2130 .collect::<Vec<_>>(),
2131 );
2132 (f, g, cf, cg)
2133 }
2134
2135 #[test]
2144 fn babai_reduce_rns_runtime_matches_bigint() {
2145 use super::{babai_reduce_rns_runtime, rns_runtime_babai_params};
2146 type Inputs = (
2147 Polynomial<BigInt>,
2148 Polynomial<BigInt>,
2149 Polynomial<BigInt>,
2150 Polynomial<BigInt>,
2151 );
2152 type Gen = fn(&mut StdRng) -> Inputs;
2153 let cases: &[(&str, Gen, usize, u64)] = &[
2155 ("depth2", depth2_inputs as Gen, 2, 0xd2),
2156 ("depth3", depth3_inputs as Gen, 3, 0xd3),
2157 ("depth4", depth4_inputs as Gen, 4, 0xd4),
2158 ("depth5", depth5_inputs as Gen, 5, 0xd5),
2159 ("depth6", depth6_inputs as Gen, 6, 0xd6),
2160 ("depth7", depth7_inputs as Gen, 7, 0xd7),
2161 ("depth8", depth8_inputs as Gen, 8, 0xd8),
2162 ("depth9", depth9_inputs as Gen, 9, 0xd9),
2163 ];
2164 for &(name, gen, depth, seed) in cases {
2165 let (k_primes, cap_w) = rns_runtime_babai_params(depth);
2166 let mut rng = StdRng::seed_from_u64(0x5117_0000 + seed);
2167 for _ in 0..16 {
2168 let (f, g, cap_f, cap_g) = gen(&mut rng);
2169
2170 let (mut bf, mut bg) = (cap_f.clone(), cap_g.clone());
2171 babai_reduce_bigint(&f, &g, &mut bf, &mut bg).unwrap();
2172
2173 let (mut rf, mut rg) = (cap_f, cap_g);
2174 babai_reduce_rns_runtime(&f, &g, &mut rf, &mut rg, k_primes, cap_w).unwrap();
2175
2176 assert_eq!(bf, rf, "{name}: capital_F mismatch (runtime vs bigint)");
2177 assert_eq!(bg, rg, "{name}: capital_G mismatch (runtime vs bigint)");
2178 }
2179 }
2180 }
2181
2182 #[test]
2191 fn ntt_primes24_5_covers_depth3_product() {
2192 use crate::rns::NttPrimes24Bit5;
2193 const MEASURED_PRODUCT_BITS: f64 = 107.0;
2194 let capacity: f64 = NttPrimes24Bit5::PRIMES
2195 .iter()
2196 .map(|&p| f64::log2(p as f64))
2197 .sum::<f64>()
2198 - 1.0; assert!(
2200 capacity >= MEASURED_PRODUCT_BITS,
2201 "NttPrimes24Bit5 capacity {capacity:.1} < depth-3 product {MEASURED_PRODUCT_BITS:.1}"
2202 );
2203 }
2204
2205 #[test]
2209 fn ntt_primes24_8_covers_depth4_product() {
2210 use crate::rns::NttPrimes24Bit8;
2211 let required = 101.62 + 6.0 * 1.02 + 54.0 + 2.0; let cap = |primes: &[u32]| -> f64 {
2214 primes.iter().map(|&p| f64::log2(p as f64)).sum::<f64>() - 1.0
2215 };
2216 let cap8 = cap(&NttPrimes24Bit8::PRIMES);
2217 let cap7 = cap(&NttPrimes24Bit8::PRIMES[..7]);
2218 assert!(
2219 cap8 >= required,
2220 "K=8 capacity {cap8:.1} < depth-4 product {required:.1}"
2221 );
2222 assert!(
2223 cap7 < required,
2224 "K=7 capacity {cap7:.1} unexpectedly covers {required:.1}"
2225 );
2226 }
2227
2228 #[test]
2233 #[should_panic(expected = "prime list too small")]
2234 #[cfg_attr(not(debug_assertions), ignore)]
2238 fn rns_reduction_guards_against_undersized_primes() {
2239 use crate::rns::NttPrimes24Bit2;
2240 let mut rng = StdRng::seed_from_u64(0xdead_beef);
2241 let (f, g, mut cf, mut cg) = depth3_inputs(&mut rng);
2242 let _ = super::babai_reduce_rns_bigint::<2, NttPrimes24Bit2>(&f, &g, &mut cf, &mut cg);
2243 }
2244
2245 #[test]
2248 fn babai_reduce_rns_bigint_depth4_matches_bigint() {
2249 use super::babai_reduce_rns_bigint_depth4;
2250
2251 let mut rng = StdRng::seed_from_u64(0xba_ba_14_d4);
2252 for _ in 0..16 {
2253 let (f, g, cap_f, cap_g) = depth4_inputs(&mut rng);
2254
2255 let (mut bf, mut bg) = (cap_f.clone(), cap_g.clone());
2256 babai_reduce_bigint(&f, &g, &mut bf, &mut bg).unwrap();
2257
2258 let (mut rf, mut rg) = (cap_f, cap_g);
2259 babai_reduce_rns_bigint_depth4(&f, &g, &mut rf, &mut rg).unwrap();
2260
2261 assert_eq!(bf, rf, "capital_F mismatch (depth 4, rns-bigint)");
2262 assert_eq!(bg, rg, "capital_G mismatch (depth 4, rns-bigint)");
2263 }
2264 }
2265
2266 #[test]
2270 fn babai_reduce_rns_packed_depth4_matches_bigint() {
2271 use super::babai_reduce_rns_packed_depth4;
2272
2273 let mut rng = StdRng::seed_from_u64(0x09ac_edd4);
2274 for _ in 0..16 {
2275 let (f, g, cap_f, cap_g) = depth4_inputs(&mut rng);
2276
2277 let (mut bf, mut bg) = (cap_f.clone(), cap_g.clone());
2278 babai_reduce_bigint(&f, &g, &mut bf, &mut bg).unwrap();
2279
2280 let (mut pf, mut pg) = (cap_f, cap_g);
2281 babai_reduce_rns_packed_depth4(&f, &g, &mut pf, &mut pg).unwrap();
2282
2283 assert_eq!(bf, pf, "capital_F mismatch (depth 4, packed)");
2284 assert_eq!(bg, pg, "capital_G mismatch (depth 4, packed)");
2285 }
2286 }
2287
2288 #[test]
2304 fn test_gs_norm() {
2305 let n = 512;
2306 let f = (0..n).map(|i| i % 5).collect_vec();
2307 let g = (0..n).map(|i| (i % 7) - 4).collect_vec();
2308 let norm_squared = gram_schmidt_norm_squared(&Polynomial::new(f), &Polynomial::new(g));
2309 let expected = 5992556.183229722f64;
2310 let difference = (norm_squared - expected).abs();
2311 assert!(
2312 difference < 1.0,
2313 "norm squared was {norm_squared} =/= {expected} (expected)",
2314 );
2315 }
2316
2317 #[test]
2318 fn test_ntru_solve() {
2319 let n = 64;
2320 let f_coefficients = (0..n).map(|i| ((i % 7) as i32) - 4).collect_vec();
2321 let f = Polynomial::new(f_coefficients).map(|&i| i.into());
2322 let g_coefficients = (0..n).map(|i| ((i % 5) as i32) - 3).collect_vec();
2323 let g = Polynomial::new(g_coefficients).map(|&i| i.into());
2324 let (capital_f, capital_g) = ntru_solve(&f, &g, 1, 2).unwrap();
2325
2326 let ntru = (f * capital_g - g * capital_f).reduce_by_cyclotomic(n);
2327 assert_eq!(Polynomial::constant(12289.into()), ntru);
2328 }
2329
2330 #[strategy_proptest]
2331 fn rns_and_i32_babai_reduce_agree(
2332 #[strategy(1usize..5)] _logn: usize,
2333 #[strategy(Just(1<<#_logn))] _n: usize,
2334 #[strategy(vec(-5i32..5, #_n))] f_coefficients: Vec<i32>,
2335 #[strategy(vec(-5i32..5, #_n))] g_coefficients: Vec<i32>,
2336 #[strategy(vec(-115i32..115, #_n))] capital_f_coefficients: Vec<i32>,
2337 #[strategy(vec(-115i32..115, #_n))] capital_g_coefficients: Vec<i32>,
2338 ) {
2339 let n = f_coefficients.len();
2340 let f = Polynomial::new(f_coefficients);
2341 let g = Polynomial::new(g_coefficients);
2342
2343 if g.coefficients.iter().all(|&x| x == 0) {
2344 return Ok(());
2345 }
2346
2347 let f_bi = f.map(|&x| BigInt::from(x));
2351 let g_bi = g.map(|&x| BigInt::from(x));
2352 let cap_f_bi_orig = Polynomial::new(
2353 capital_f_coefficients
2354 .iter()
2355 .map(|&x| BigInt::from(x))
2356 .collect::<Vec<_>>(),
2357 );
2358 let cap_g_bi_orig = Polynomial::new(
2359 capital_g_coefficients
2360 .iter()
2361 .map(|&x| BigInt::from(x))
2362 .collect::<Vec<_>>(),
2363 );
2364 let invariant =
2365 (f_bi.clone() * cap_g_bi_orig - g_bi.clone() * cap_f_bi_orig).reduce_by_cyclotomic(n);
2366
2367 let mut capital_f_i32 = Polynomial::new(capital_f_coefficients.clone());
2368 let mut capital_g_i32 = Polynomial::new(capital_g_coefficients.clone());
2369 let mut capital_f_rns: Vec<Rns3> = capital_f_coefficients
2370 .iter()
2371 .map(|&i| Rns3::from_i32(i))
2372 .collect();
2373 let mut capital_g_rns: Vec<Rns3> = capital_g_coefficients
2374 .iter()
2375 .map(|&i| Rns3::from_i32(i))
2376 .collect();
2377
2378 let _ = babai_reduce_i32(&f, &g, &mut capital_f_i32, &mut capital_g_i32);
2379 let _ = babai_reduce_rns::<3, NttPrimes3>(&f, &g, &mut capital_f_rns, &mut capital_g_rns);
2380
2381 let cap_f_rns_bi = Polynomial::new(
2383 capital_f_rns
2384 .iter()
2385 .map(|r| BigInt::from(r.to_i64()))
2386 .collect::<Vec<_>>(),
2387 );
2388 let cap_g_rns_bi = Polynomial::new(
2389 capital_g_rns
2390 .iter()
2391 .map(|r| BigInt::from(r.to_i64()))
2392 .collect::<Vec<_>>(),
2393 );
2394 let invariant_after = (f_bi * cap_g_rns_bi - g_bi * cap_f_rns_bi).reduce_by_cyclotomic(n);
2395 prop_assert_eq!(
2396 invariant,
2397 invariant_after,
2398 "rns babai did not preserve NTRU invariant f·G'−g·F'"
2399 );
2400 }
2401
2402 #[strategy_proptest]
2403 fn bigint_and_smallint_babai_reduce_agree(
2404 #[strategy(1usize..5)] _logn: usize,
2405 #[strategy(Just(1<<#_logn))] _n: usize,
2406 #[strategy(vec(-5..5, #_n))] f_coefficients: Vec<i32>,
2407 #[strategy(vec(-5..5, #_n))] g_coefficients: Vec<i32>,
2408 #[strategy(vec(-115..115, #_n))] capital_f_coefficients: Vec<i32>,
2409 #[strategy(vec(-115..115, #_n))] capital_g_coefficients: Vec<i32>,
2410 ) {
2411 let f_i32 = Polynomial::new(f_coefficients);
2412 let g_i32 = Polynomial::new(g_coefficients);
2413 let mut capital_f_i32 = Polynomial::new(capital_f_coefficients);
2414 let mut capital_g_i32 = Polynomial::new(capital_g_coefficients);
2415 let f_bigint = f_i32.map(|i| BigInt::from(*i));
2416 let g_bigint = g_i32.map(|i| BigInt::from(*i));
2417 let mut capital_f_bigint = capital_f_i32.map(|i| BigInt::from(*i));
2418 let mut capital_g_bigint = capital_g_i32.map(|i| BigInt::from(*i));
2419
2420 let _ = babai_reduce_i32(&f_i32, &g_i32, &mut capital_f_i32, &mut capital_g_i32);
2421 let _ = babai_reduce_bigint(
2422 &f_bigint,
2423 &g_bigint,
2424 &mut capital_f_bigint,
2425 &mut capital_g_bigint,
2426 );
2427
2428 prop_assert_eq!(capital_f_i32.map(|c| BigInt::from(*c)), capital_f_bigint);
2429 prop_assert_eq!(capital_g_i32.map(|c| BigInt::from(*c)), capital_g_bigint);
2430 }
2431
2432 #[test]
2433 fn test_ntru_gen() {
2434 let n = 512;
2435 let seed: [u8; 32] =
2436 hex::decode("deadbeef00000000deadbeef00000000deadbeef00000000deadbeef00000000")
2437 .unwrap()
2438 .try_into()
2439 .unwrap();
2440 let mut rng: StdRng = SeedableRng::from_seed(seed);
2441 let (f, g, capital_f, capital_g) = ntru_gen(n, &mut rng);
2442
2443 println!("f: {}", f);
2444 println!("g: {}", g);
2445 println!("capital f: {}", capital_f);
2446 println!("capital g: {}", capital_g);
2447 let f_times_capital_g = (f * capital_g).reduce_by_cyclotomic(n);
2448 let g_times_capital_f = (g * capital_f).reduce_by_cyclotomic(n);
2449 let difference = f_times_capital_g - g_times_capital_f;
2450 assert_eq!(Polynomial::constant(12289), difference);
2451 }
2452
2453 #[test]
2456 fn test_ntru_gen_1024() {
2457 let n = 1024;
2458 let seed: [u8; 32] = *b"\xc0ffee_multiword_alloc_free_2026!";
2459 let mut rng: StdRng = SeedableRng::from_seed(seed);
2460 let (f, g, capital_f, capital_g) = ntru_gen(n, &mut rng);
2461 let f_times_capital_g = (f * capital_g).reduce_by_cyclotomic(n);
2462 let g_times_capital_f = (g * capital_f).reduce_by_cyclotomic(n);
2463 assert_eq!(
2464 Polynomial::constant(12289),
2465 f_times_capital_g - g_times_capital_f
2466 );
2467 }
2468}