Skip to main content

falcon_rust/
math.rs

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/// Coefficient sizes (log₂ of max absolute value, in bits) at each recursion
27/// depth in [`ntru_solve`], from the Falcon specification
28/// (<https://falcon-sign.info/falcon.pdf>, Table on p. 59), gathered
29/// experimentally over many Falcon-1024 key-generation runs.
30///
31/// Depth 0 = outermost call (n = 1024); depth 10 = base case (n = 1, xgcd
32/// only, no Babai step).  Columns: `(avg log₂ max|f,g|, σ, avg log₂
33/// max|F,G|, σ)`, where F, G are measured before `babai_reduce` runs.
34#[rustfmt::skip]
35pub const NTRU_SOLVE_BABAI_COEFF_BITS: [(f64, f64, f64, f64); 11] = [
36    ( 4.00,  0.00,  19.61,  0.49), // depth  0  n = 1024 (babai_reduce_i32)
37    (10.99,  0.08,  39.82,  0.41), // depth  1  n =  512
38    (24.07,  0.25,  78.20,  0.73), // depth  2  n =  256
39    (50.37,  0.53, 153.65,  1.39), // depth  3  n =  128
40    (101.62, 1.02, 303.49,  2.38), // depth  4  n =   64
41    (202.22, 1.87, 599.81,  3.87), // depth  5  n =   32
42    (400.67, 3.10,1188.68,  6.04), // depth  6  n =   16
43    (794.17, 4.98,2361.84,  9.31), // depth  7  n =    8
44    (1576.87,7.49,4703.30, 14.77), // depth  8  n =    4
45    (3138.35,12.25,9403.29,27.55), // depth  9  n =    2
46    (6307.52,24.48,6319.66,24.51), // depth 10  n =    1 (xgcd, no Babai)
47];
48
49/// Window / back-shift schedule for one Babai reduction pass, shared by all
50/// three estimator loops ([`babai_reduce_bigint`], [`babai_reduce_rns_generic`],
51/// [`babai_reduce_rns_runtime`]) so they stay bit-identical (the differential
52/// tests depend on it).
53///
54/// The per-coefficient quotient `k_true = C / f` has `d = capital_size − size`
55/// bits, but the f64 `round()` resolves only ~53 bits at once:
56/// - `d > 53`: window the top 106 bits (`capital_size − 106`) so the quotient is
57///   ≈ 2^53 — k_true's top 53 bits — and `back_shift = d − 53` re-aligns them.
58/// - `d ≤ 53`: k_true already fits in 53 bits, so window at the *denominator's*
59///   scale (`size − 53`), making the quotient equal k_true exactly and reducing
60///   all `d` bits in ONE pass, with `back_shift = 0`.
61///
62/// The earlier `d ≤ 53` form `(capital_size − 53, d)` scaled the quotient down to
63/// ≈ 2^(53 − size), i.e. only a few bits whenever `size` sat near its 53-bit
64/// floor (e.g. depth 3, where `max|f,g| ≈ 50`), so it crawled ~1 bit per pass —
65/// 24 wasted passes of an expensive n=128 multiply. Windowing at `size − 53`
66/// removes the crawl; f64 precision was never the limit (k_true is only ~28 bits
67/// there).
68fn 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/// Reduce the vector (F,G) relative to (f,g). This method follows the python
77/// implementation [1].
78///
79/// Algorithm 7 in the spec [2, p.35]
80///
81/// [1]: https://github.com/tprest/falcon.py
82///
83/// [2]: https://falcon-sign.info/falcon.pdf
84///
85/// This function is marked pub for the purpose of benchmarking; it is not
86/// considered part of the public API.
87#[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        // Stop when we've reached the target size, or when capital_size stopped
130        // strictly decreasing (floating-point precision limit reached).
131        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        // See `babai_shifts` for the windowing schedule (two-step for d > 53,
140        // full-k single pass for d <= 53).
141        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        // Use i128 to avoid i64 saturation when the FFT quotient is large
157        // (can happen for small n when |f_fft[i]|^2 + |g_fft[i]|^2 is small at
158        // some frequency).
159        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        // Tentative check: if applying shifted_kf would make capital_F grow,
170        // the step overshot (common when |f_fft|^2+|g_fft|^2 is very small at
171        // one frequency for small n).  In that case fall back to the old
172        // single-bit formula for this iteration.
173        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                // Recompute with old formula (capital_shift = capital_size - 53)
190                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
218/// Capital-coefficient representation for the generic RNS Babai reduction
219/// [`babai_reduce_rns_generic`].  Implemented for `BigInt` (uncapped — the
220/// `babai_reduce_rns_bigint` path) and [`Packed<L>`] (fixed-width limbs — the
221/// faster `babai_reduce_rns_packed` path).  The reference `babai_reduce_bigint`
222/// (which multiplies via karatsuba, not the NTT) is intentionally kept separate
223/// as the correctness oracle.
224trait ReductionCapital: Clone {
225    fn from_bigint(x: &BigInt) -> Self;
226    fn to_bigint(&self) -> BigInt;
227    /// Bit-length of the absolute value (matches `BigInt::bits`).
228    fn bit_length(&self) -> u64;
229    /// Arithmetic right shift by `shift`, low 128 bits as `i128` — the windowed
230    /// top-word input to the `f64` k-estimation FFT.
231    fn shr_window(&self, shift: u32) -> i128;
232    /// Left shift by `bits` (multiply by `2^bits`).
233    fn shl(&self, bits: u32) -> Self;
234    /// `self - other`.
235    fn sub(&self, other: &Self) -> Self;
236    /// Reconstruct one `k·h` product coefficient from its RNS residues.
237    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
288/// Babai reduction where the capital coefficients live in `C` (a positional
289/// multi-word representation) and the `k·f`/`k·g` products are evaluated in RNS
290/// via the NTT instead of `BigInt` karatsuba.
291///
292/// This is the fn-dsa-style split, generic over the capital representation:
293/// the capital stays positional so the reduction coefficient `k` can be
294/// estimated from its top words (the `f64` FFT windowing, shared verbatim with
295/// [`babai_reduce_bigint`]) and is not capped at 127 bits, while the one
296/// expensive operation — the polynomial product — runs in the RNS/NTT domain
297/// (`f`, `g`, `k` only).  `k` is transformed once per iteration and reused for
298/// both products.  Unlike [`babai_reduce_rns`] (capital in RNS, i128-capped to
299/// depths 1–2), this works at any depth for which `P` covers the `k·f` product.
300fn 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    // Precompute the per-prime twiddle tables once; reused by every transform.
314    // Production prime lists return compile-time tables; see `NttPrimeList::ntt_tables`.
315    let tables = P::ntt_tables(n);
316
317    // Forward NTT of f and g once (reused every iteration).  `from_bigint`
318    // works at any depth, including where the coefficients exceed i128.
319    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    // Guard: the modulus must cover the `k·f` product.  With `k` windowed to
342    // ~53 bits the product is `≈ max|f,g| + 54` bits (the measured model — see
343    // `product_size_model_matches_measurements`); a too-small prime list `P`
344    // for this depth would silently wrap the centered CRT, so catch it here in
345    // debug builds instead of corrupting the reduction in release.
346    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    // Capital coefficients move into the positional representation `C` for the
368    // duration of the loop; `BigInt` is touched only at entry and exit.
369    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    // Pointwise-multiply the (already forward-transformed) `k_ntt` by `h_ntt`,
373    // inverse-transform, and reconstruct each coefficient into `C`.
374    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    // Windowed top-word read of a capital vector, transformed for k-estimation.
384    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        // Transform `k` once, reuse for both the k·f and k·g products.
424        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    // Move the reduced capital coefficients back out to BigInt.
475    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/// Runtime-`K` Babai reduction: capital lives in [`MultiwordPoly`] limbs and the
485/// `k·f`/`k·g` corrections are formed by the runtime-prime-count RNS multiply
486/// ([`RuntimeNtt`]), so it works at any recursion depth without a compile-time
487/// prime list.  Algorithmically identical to [`babai_reduce_rns_generic`] (same
488/// windowed k-estimation, same `d > 53` two-step), differing only in the
489/// representation: capital and operands are flat words, no `BigInt` in the loop.
490///
491/// `k_primes` must cover the `k·f` product (`≈ max|f,g| + 54` bits); `cap_w` is
492/// the capital limb width (must hold the capital plus the mid-reduction shifts).
493/// `BigInt` is touched only at entry (operands, capital) and exit (capital).
494#[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    // At deep levels (tiny `n`, huge coefficients) the flat-word schoolbook
507    // multiply beats the RNS/NTT one, and building the `RuntimeNtt` — `k_primes`
508    // root-finds plus an O(k²) Garner table, with `k_primes` reaching ~149 at
509    // depth 9 — is pure waste.  Only build it in the large-`n` (RNS) regime.
510    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    // f64 FFT denominator from windowed f,g (identical to the BigInt path).
528    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    // Operands as flat-word polynomials (multiply inputs); capital likewise.
540    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    // RNS regime only: transform f, g once (only k changes per reduction pass).
544    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    // `k·f` / `k·g` per pass: pre-transformed RNS multiply for large `n`, flat-word
549    // schoolbook for tiny `n` (where the RNS overhead dominates).
550    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    // k (i128 per coefficient) -> width-2 flat-word polynomial.
579    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    // Capacity guard (active in release).  The per-depth `(k_primes, cap_w)`
598    // provisioning ([`rns_runtime_babai_params`]) covers a +6σ tail of the
599    // coefficient distribution; a rarer seed can exceed it, and then the RNS
600    // branch would silently wrap the centered CRT (modulus < product) or the
601    // schoolbook branch would truncate the product to `cap_w` limbs — either way
602    // corrupting (F, G).  Detect it from this seed's actual magnitudes and
603    // return `Err` so `ntru_solve` retries, the same recovery the depth-0
604    // `fG − gF = q` check provides.  (The sibling [`babai_reduce_rns_generic`]
605    // only `debug_assert!`s the analogous bound, which is compiled out in
606    // release; here it must hold on the shipped path.)
607    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        // Signed reconstruction capacity of the runtime prime list, vs the
617        // `≈ size + 54`-bit k·f product (the measured model — see
618        // `product_size_model_matches_measurements`).
619        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                // Overshoot: fall back to the single-bit (d) formula this pass.
653                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
679/// Babai reduction with `BigInt` (uncapped) capital and an RNS/NTT `k·f`
680/// product.  Thin wrapper over [`babai_reduce_rns_generic`]; see it for the
681/// algorithm.
682pub(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/// Reduce the vector (F,G) relative to (f,g). This method follows the python
692/// implementation [1] but uses multimodular arithmetic for fast operations on
693/// big integer polynomials in a cyclotomic ring.
694///
695/// Algorithm 7 in the spec [2, p.35]
696///
697/// [1]: https://github.com/tprest/falcon.py
698///
699/// [2]: https://falcon-sign.info/falcon.pdf
700///
701///
702/// This function is marked pub for the purpose of benchmarking; it is not
703/// considered part of the public API.
704#[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        // All-zero input has no meaningful bit size; `ilog2(0)` would panic.
717        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/// Reduce the vector (F, G) relative to (f, g) using f64 FFT for the quotient
801/// step and RNS NTT for polynomial multiplication.
802///
803/// This mirrors `babai_reduce_i32`: Complex64 FFT computes the rounded quotient
804/// k, which is then applied via RNS NTT multiplication.  capital_F,G live in
805/// RNS throughout; they are decoded to i128 (via Garner) once per iteration
806/// only for the FFT input and convergence check.
807///
808/// Algorithm 7 in the spec [1, p.35].
809///
810/// [1]: https://falcon-sign.info/falcon.pdf
811#[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    // Precompute the per-prime twiddle tables once; reused by every transform
821    // (the one-off f/g forward NTTs and the per-iteration k transform).
822    // Production prime lists return compile-time tables; see `NttPrimeList::ntt_tables`.
823    let tables = P::ntt_tables(n);
824
825    // Precompute RNS NTT of f,g for polynomial multiplication.
826    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    // Precompute Complex64 FFT of f,g. f,g are i32, so values always fit in
832    // f64 with no shifting required.
833    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    // "size" for f,g: bit-width rounded to multiple of 8, at least 53.
840    // Since f,g are i32, size == 53 always.
841    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        // Decode capital_F,G from RNS to i128. Needed both for the convergence
858        // check and for the f64 FFT input.
859        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        // Shift capital into f64 range: values become ~2^53 at most.
882        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        // k_adj = round(k_true / 2^capital_shift).
903        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        // k_true = k_adj << capital_shift. Encode in RNS and apply via NTT.
914        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/// Extended Euclidean algorithm for computing the greatest common divisor (g) and
942/// Bézout coefficients (u, v) for the relation
943///
944///  u a + v b = g .
945///
946/// Implementation adapted from Wikipedia [1].
947///
948/// [1]: https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode
949#[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/// Run `babai_reduce_rns` at recursion depth 1 (n = 512, K = 2 primes).
1004///
1005/// `capital_f` and `capital_g` are passed and returned as `i128` slices.
1006/// The conversion to/from `Rns` is included in the timed region because it
1007/// is part of the hot path in `babai_rns_with_fallback`.
1008#[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/// Run `babai_reduce_rns` at recursion depth 2 (n = 256, K = 4 primes).
1030///
1031/// See [`babai_reduce_rns_depth1`] for calling convention.
1032#[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
1053/// Babai reduction with **packed** fixed-width (`L`-limb) capital coefficients.
1054///
1055/// [`babai_reduce_rns_generic`] specialized to [`Packed<L>`]: the capital lives
1056/// in `64·L`-bit two's-complement limbs instead of `BigInt`, so the
1057/// per-iteration hot path is allocation-free and word-level:
1058///
1059/// * `capital_size` is a limb scan ([`Packed::bit_length`]);
1060/// * the FFT input is a top-word read ([`Packed::shr_to_i128`]);
1061/// * the reduction coefficient `k` stays `i128` (it never exceeds ~2^53), so
1062///   feeding it into RNS uses the cheap `Rns::from_i128`, not a `BigInt` CRT;
1063/// * the `k·f` product is reconstructed straight into limbs with word-level
1064///   CRT ([`Packed::from_rns`]), then shifted and subtracted in place.
1065///
1066/// `BigInt` is touched only once at entry and once at exit.  This is the
1067/// representation fn-dsa uses (base 2^31 there, base 2^64 here); it lets the
1068/// NTT multiply beat `BigInt` karatsuba at depth 3 (see the per-depth bench).
1069pub(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/// Run [`babai_reduce_rns_packed`] at recursion depth 3 (n = 128, K = 5 primes,
1079/// L = 4 limbs / 256-bit capital).
1080///
1081/// Only 5 primes are needed: the path pushes the `k·f` product (a hard ≈107-bit
1082/// quantity, see `ntt_primes24_5_covers_depth3_product`) — not the ≈154-bit
1083/// capital — through RNS.  The ≈154-bit capital fits in 4 limbs.
1084#[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/// Run [`babai_reduce_rns_packed`] at recursion depth 4 (n = 64, K = 8 primes,
1095/// L = 5 limbs / 320-bit capital).
1096///
1097/// At depth 4 the `k·f` product is ≈155 bits (`bits(f)≈101` plus the ≈53-bit
1098/// reduction coefficient `k`; see `product_size_model_matches_measurements`),
1099/// so the 8-prime ≈183-bit list is required — the 5-prime list (≈114 bits)
1100/// would wrap.  The ≈303-bit capital needs 5 limbs.
1101///
1102/// `#[doc(hidden)]` and NOT wired into [`ntru_solve`]: at depth 4 this is
1103/// **slower** than `BigInt` karatsuba (n=64 bench: ≈1.6 ms vs ≈1.0 ms).  As `n`
1104/// halves with depth, karatsuba on a small polynomial gets cheap while the RNS
1105/// per-iteration overhead (k→RNS conversion, the now 8-prime NTT, word-level
1106/// CRT reconstruction) and the extra reduction iterations dominate — the RNS
1107/// crossover sits at ~depth 3.  This entry point exists only so the per-depth
1108/// benchmark and equivalence test can measure that, and to keep the const-`L`
1109/// machinery exercised; it is not part of the production reduction path.
1110#[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/// Run [`babai_reduce_rns_bigint`] at recursion depth 3 (n = 128, K = 5 primes).
1121///
1122/// Capital coefficients at this depth exceed 127 bits, so they are carried as
1123/// `BigInt` rather than `i128`; the `k·f` product still runs in RNS via the
1124/// NTT.  Provided for the per-depth benchmark and the depth-3 correctness test.
1125#[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/// Run [`babai_reduce_rns_bigint`] at recursion depth 4 (n = 64, K = 8 primes).
1136/// Capital carried as `BigInt`; the ≈155-bit `k·f` product needs the 8-prime
1137/// list.
1138///
1139/// `#[doc(hidden)]` and NOT wired into [`ntru_solve`]: like its packed sibling
1140/// [`babai_reduce_rns_packed_depth4`], this loses to `BigInt` karatsuba at
1141/// depth 4 — see that function's note for why the RNS crossover ends at
1142/// ~depth 3.  Provided only for the per-depth benchmark and the depth-4
1143/// equivalence test.
1144#[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
1154/// Per-depth parameters for the runtime-`K` flat-word babai reduction
1155/// ([`babai_reduce_rns_runtime`]): `(k_primes, cap_w)`.  `k_primes` covers the
1156/// `≈ bits(f)+54`-bit `k·f` product with a +6σ tail; `cap_w` (64-bit limbs)
1157/// holds the depth's capital (`max|F,G|`, +6σ) plus the mid-reduction shifts.
1158/// Derived from [`NTRU_SOLVE_BABAI_COEFF_BITS`].
1159const 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
1173/// Deepest recursion depth at which the allocation-free flat-word babai reduction
1174/// ([`babai_reduce_rns_runtime`]) is used instead of `babai_reduce_bigint`.
1175/// Depths 3..=this use the flat-word path; deeper levels fall back to BigInt.
1176/// Overridable at runtime via the `RNS_RUNTIME_MAX_DEPTH` env var.
1177///
1178/// Default 9 (all depths): the flat-word babai now dispatches its `k·f` multiply
1179/// internally — RNS/NTT at large `n` (depth 3, n=128) and flat-word schoolbook at
1180/// tiny `n` (depths 4–9, n≤64), the latter skipping the expensive `RuntimeNtt`
1181/// entirely.  This replaces the old RNS-only crossover (which had to fall back to
1182/// BigInt at depth 5 because the RNS multiply lost at small `n`).
1183fn 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/// Field norm via the allocation-free flat-word [`MultiwordPoly`] path instead of
1195/// `Polynomial<BigInt>::field_norm`.  The two squarings inside dominate the deep
1196/// NTRU-solve recursion; routing them through the size-dispatched negacyclic
1197/// multiply (RNS/NTT at shallow large `n`, flat-word schoolbook at deep tiny `n`)
1198/// removes the per-`BigInt`-operation heap allocation.  Bit-exact with the
1199/// `BigInt` field norm (see `field_norm_matches_bigint`).
1200#[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    // ceil(max_bits/64) limbs for the magnitude, +1 for the sign bit / safety.
1204    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/// Negacyclic product `a * b mod (X^n + 1)` via the allocation-free flat-word
1211/// multiply, replacing `a.karatsuba(&b).reduce_by_cyclotomic(n)` on
1212/// `Polynomial<BigInt>`.  Used for the two F,G construction multiplies in
1213/// `ntru_solve` (`capital_*_prime_xsq × *_minx`), which are asymmetric — a large
1214/// lifted capital times a small galois-adjoint operand.  Bit-exact with
1215/// karatsuba-then-reduce (`negacyclic_mul_dispatch_matches_bigint`).
1216#[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    // ceil(bits/64) magnitude limbs + 1 sign/safety limb per operand.
1222    let wa = (bits_a / 64 + 1) as usize;
1223    let wb = (bits_b / 64 + 1) as usize;
1224    // |product coeff| < n · 2^(bits_a+bits_b); +1 sign limb.
1225    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/// Solve the NTRU equation. Given f, g in ZZ[X], find F, G in ZZ[X].
1234/// such that
1235///
1236///    f G - g F = q  mod (X^n + 1)
1237///
1238/// Algorithm 6 of the specification [1, p.35].
1239///
1240/// [1]: https://falcon-sign.info/falcon.pdf
1241#[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        // Depth 2 (production, where max_rns_depth=1 so the i128 arm above is
1281        // skipped) and the deep levels: allocation-free flat-word reduction. At
1282        // depth 2 (n=256 > schoolbook cutoff) this is the transform-once RNS
1283        // multiply; deeper levels dispatch to the flat-word schoolbook internally.
1284        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    // Multiply via the single-prime NTT, reusing the compile-time twiddle
1353    // tables through `FastFft` (no per-call `bitreversed_powers` recompute).
1354    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
1379/// Verify the NTRU equation `fG − gF = q  mod (X^n + 1)` for a candidate
1380/// solution `(F, G)`.  The depth-0 reduction NTT uses a single 24-bit prime
1381/// whose ~2^22 reconstruction range only narrowly covers the construction
1382/// products, so a rare +Nσ seed can overflow it and corrupt F/G; this check
1383/// turns that into a keygen retry instead of a silently invalid key.  Evaluated
1384/// in i64 so the unreduced products cannot overflow.
1385fn 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/// Sample 4 small polynomials f, g, F, G such that f * G - g * F = q mod (X^n + 1).
1402/// Algorithm 5 (NTRUgen) of the documentation [1, p.34].
1403///
1404/// This function is marked pub for benchmarking purposes only. Not considered part
1405/// of the public API.
1406///
1407/// [1]: https://falcon-sign.info/falcon.pdf
1408#[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    // let mut rng: StdRng = SeedableRng::from_seed(seed);
1420
1421    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        // max_rns_depth=1: use the i128 RNS babai only at depth 1 (n=512, K=2),
1435        // where it beats BigInt.  Depth 2 (K=4) is a measured net loss, so it
1436        // stays on BigInt.  The deeper allocation-free runtime-K RNS path is
1437        // gated separately by `rns_runtime_max_depth()`, not by this argument.
1438        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            // Verify the NTRU equation fG − gF = q.  The depth-0 reduction NTT
1442            // uses a single 24-bit prime whose ~2^22 reconstruction range only
1443            // narrowly covers the products; this check turns any rare overflow
1444            // into a retry instead of a silently invalid key.
1445            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/// Like [`ntru_gen`] but with an explicit RNS depth threshold for benchmarking.
1459///
1460/// `max_rns_depth` controls how deep into the NTRU recursion `babai_reduce_rns`
1461/// is used instead of `babai_reduce_bigint`:
1462/// - 0: always use BigInt (baseline)
1463/// - 1: RNS at depth 1 only (n=512, K=2 primes)
1464/// - 2: RNS at depths 1–2 (n=512 and n=256, K=2 and K=4 primes)
1465///
1466/// This function is marked pub for benchmarking purposes only.
1467#[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            // Same depth-0 single-prime overflow guard as `ntru_gen`: reject a
1496            // silently-corrupted (F, G) and retry rather than emit an invalid key.
1497            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/// Generate a polynomial of degree at most n-1 whose coefficients are
1511/// distributed according to a discrete Gaussian with mu = 0 and
1512/// sigma = 1.17 * sqrt(Q / (2n)).
1513// fn gen_poly(n: usize, rng: &mut dyn Rng) -> Polynomial<i16> {
1514#[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    // Build the sigma-dependent sampler constants once, not per sample.
1519    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/// Compute the Gram-Schmidt norm of B = [[g, -f], [G, -F]] from f and g.
1532/// Corresponds to line 9 in algorithm 5 of the spec [1, p.34]
1533///
1534/// [1]: https://falcon-sign.info/falcon.pdf
1535#[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    // Prime list for babai_reduce_rns tests: three primes ≡ 1 (mod 2048).
1595    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        // Signed bit-capacity of the NttPrimes3 configuration.
1611        let capacity: f64 = NttPrimes3::PRIMES
1612            .iter()
1613            .map(|&p| f64::log2(p as f64))
1614            .sum::<f64>()
1615            - 1.0;
1616
1617        // Required bits at depth d: avg + 6·σ + 2 (sign bit + one factor-of-2
1618        // slack because F − k·f can transiently reach 2·|F| before shrinking).
1619        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        // Depth 2 must exceed our capacity — if this assertion ever fails, the
1635        // prime list is large enough to extend babai_reduce_rns to depth 2.
1636        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    /// Generate a random signed `BigInt` with up to `bits` magnitude bits.
1769    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    /// Realistic depth-2 inputs: n = 256, max|f,g| ≈ 24 bits, max|F,G| ≈ 78
1783    /// bits (from `NTRU_SOLVE_BABAI_COEFF_BITS`).  Exercises the ≈80-bit `k·f`
1784    /// product through the runtime path (K = 5 primes, 4-limb capital) — the
1785    /// CRT-wrap detector for the depth-2 params now that production routes
1786    /// depth 2 through `babai_reduce_rns_runtime`.
1787    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    /// Realistic depth-3 inputs: n = 128, max|f,g| ≈ 50 bits, max|F,G| ≈ 154
1820    /// bits (the actual Falcon-1024 depth-3 magnitudes, from
1821    /// `NTRU_SOLVE_BABAI_COEFF_BITS`).  These sizes exercise the full ≈107-bit
1822    /// `k·f` product, so the equivalence tests double as a CRT-wrap detector
1823    /// for the right-sized (K = 5) prime list.
1824    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    /// Realistic depth-4 inputs: n = 64, max|f,g| ≈ 101 bits, max|F,G| ≈ 303
1857    /// bits.  Exercises the full ≈155-bit `k·f` product (CRT-wrap detector for
1858    /// the K = 8 prime list) and the 5-limb (320-bit) capital.
1859    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    /// Sizing model for the multiword RNS reduction paths, established by
1892    /// measuring the max `k·f` product bit-width on realistic inputs (with the
1893    /// 183-bit `NttPrimes24Bit8` so the product never wraps during measurement):
1894    ///
1895    ///   depth 3 (n=128, bits(f)≈50):  product ≈ 107 bits,  k ≈ 54–55 bits
1896    ///   depth 4 (n=64,  bits(f)≈101): product ≈ 155 bits,  k ≈ 51–52 bits
1897    ///
1898    /// The product does NOT self-normalize: the reduction coefficient `k` stays
1899    /// ≈53 bits at every depth (it is estimated from windowed top words), but
1900    /// the product is `k · f_full` with the *full* `f`, so it grows with depth
1901    /// as `bits(f) + ~54`.  Crucially this is still far below the capital
1902    /// (`bits(F) ≈ 3·bits(f)`): the modulus need only cover the product, so the
1903    /// prime count is roughly halved versus sizing for the capital.  Sizing for
1904    /// the +6σ tail, `ceil((avg_f + 6·σ_f + 54 + 2) / 23)` primes: 5 at depth 3,
1905    /// 8 at depth 4 (the +6σ tail pushes the product to ≈162 bits, past K=7's
1906    /// ≈160-bit capacity).
1907    #[test]
1908    fn product_size_model_matches_measurements() {
1909        // (bits(f), measured max product) data points.
1910        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    /// The RNS-NTT multiply backend must produce exactly the same reduction as
1920    /// the BigInt-karatsuba backend, on realistic depth-3-sized inputs (capital
1921    /// coefficients > 127 bits, exercising the `BigInt` capital path that the
1922    /// i128-bound `babai_reduce_rns` cannot represent).
1923    #[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    /// The packed-limb backend must produce exactly the same reduction as the
1943    /// BigInt backend (same realistic depth-3-sized inputs).
1944    #[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    /// Realistic depth-5 inputs: n = 32, max|f,g| ≈ 202 bits, max|F,G| ≈ 600
1964    /// bits.  Exercises a ≈256-bit `k·f` product that needs ~12 runtime primes.
1965    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    /// Realistic depth-6 inputs: n = 16, max|f,g| ≈ 401 bits, max|F,G| ≈ 1189
1998    /// bits (from `NTRU_SOLVE_BABAI_COEFF_BITS[6]`).  Schoolbook regime (n ≤ 64).
1999    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    /// Realistic depth-7 inputs: n = 8, max|f,g| ≈ 850 bits, max|F,G| ≈ 2000
2032    /// bits.  Deep in the schoolbook regime (n ≤ 64), where the flat-word babai
2033    /// uses `schoolbook_negacyclic_mul` and skips the `RuntimeNtt` entirely.
2034    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    /// Realistic depth-8 inputs: n = 4, max|f,g| ≈ 1577 bits, max|F,G| ≈ 4703
2067    /// bits (from `NTRU_SOLVE_BABAI_COEFF_BITS[8]`).  Schoolbook regime (n ≤ 64).
2068    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    /// Realistic depth-9 inputs: n = 2, max|f,g| ≈ 3138 bits, max|F,G| ≈ 9403
2101    /// bits (from `NTRU_SOLVE_BABAI_COEFF_BITS[9]`).  Schoolbook regime (n ≤ 64);
2102    /// the deepest level the runtime path serves.
2103    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    /// The runtime-`K` flat-word backend ([`babai_reduce_rns_runtime`]) must
2136    /// produce exactly the same reduction as the BigInt-karatsuba oracle, across
2137    /// every depth the production dispatch serves (2–9; n = 256 … 2): depths 2–3
2138    /// (n>64) take the RNS multiply, depths 4–9 the flat-word schoolbook one.
2139    ///
2140    /// The `(k_primes, cap_w)` come straight from the production
2141    /// [`rns_runtime_babai_params`] — this test is the wrap-detector for the
2142    /// *shipped* parameters, so it must not hardcode its own.
2143    #[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        // (name, inputs, depth, seed); params are looked up from production.
2154        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    /// The multiword `babai_reduce_rns_{packed,bigint}` paths only push the
2183    /// `k·f` *product* through RNS, not the capital.  At a fixed depth the
2184    /// product does not grow with the capital's magnitude (the `d>53` windowing
2185    /// keeps `k ≈ 53` bits), so it is `bits(f) + ~54` — measured to peak at a
2186    /// hard 107 bits at depth 3.  So the prime list need only cover ~107 bits,
2187    /// not the ≈154-bit capital — hence `NttPrimes24Bit5` (≈114-bit capacity)
2188    /// rather than `NttPrimes24Bit8`.  (See `product_size_model_matches_…` for
2189    /// why this grows with depth — at depth 4 the product is ≈155 bits.)
2190    #[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; // sign bit for centered reconstruction
2199        assert!(
2200            capacity >= MEASURED_PRODUCT_BITS,
2201            "NttPrimes24Bit5 capacity {capacity:.1} < depth-3 product {MEASURED_PRODUCT_BITS:.1}"
2202        );
2203    }
2204
2205    /// Depth 4: the ≈155-bit `k·f` product (`bits(f)≈101` + ≈54) needs the
2206    /// 8-prime ≈183-bit list; the 5-prime ≈114-bit list would wrap.  Assert K=8
2207    /// covers the +6σ tail (≈162 bits) and that K=7 would not.
2208    #[test]
2209    fn ntt_primes24_8_covers_depth4_product() {
2210        use crate::rns::NttPrimes24Bit8;
2211        // avg_f + 6σ_f + 54 + 2, from NTRU_SOLVE_BABAI_COEFF_BITS[4] = (101.62, 1.02, …).
2212        let required = 101.62 + 6.0 * 1.02 + 54.0 + 2.0; // ≈ 163.8 bits
2213        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    /// The capacity guard in babai_reduce_rns_generic must fire (debug builds)
2229    /// when the prime list is too small for the product, rather than silently
2230    /// wrapping the CRT.  Depth-3 inputs need ~107 product bits; the 2-prime
2231    /// list has only ~45-bit capacity.
2232    #[test]
2233    #[should_panic(expected = "prime list too small")]
2234    // The guard is a `debug_assert!`, compiled out under `--release`; only run this
2235    // in debug builds so `cargo test --release` isn't red. (Production keygen never
2236    // hits this path — it uses the release-guarded runtime babai; see afea26d.)
2237    #[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    /// rns-bigint backend must match BigInt karatsuba on realistic depth-4
2246    /// inputs (n=64, capital ≈303 bits). Wrap-detector for the K=8 prime list.
2247    #[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    /// packed backend (L=5 / 320-bit capital) must match BigInt karatsuba on
2267    /// realistic depth-4 inputs.  Exercises both the K=8 product sizing and the
2268    /// widened limb count.
2269    #[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]
2289    // fn test_gen_poly() {
2290    //     let mut rng = rng();
2291    //     let n = 1024;
2292    //     let mut sum_norms = 0.0;
2293    //     let num_iterations = 100;
2294    //     for _ in 0..num_iterations {
2295    //         let f = gen_poly(n, &mut rng);
2296    //         sum_norms += f.l2_norm();
2297    //     }
2298    //     let average = sum_norms / (num_iterations as f64);
2299    //     assert!(90.0 < average);
2300    //     assert!(average < 94.0);
2301    // }
2302
2303    #[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        // Compute NTRU invariant f·G − g·F on the original inputs.
2348        // Babai reduction preserves this exactly (each step subtracts k·f from F
2349        // and k·g from G, so f·(G−k·g) − g·(F−k·f) = f·G − g·F).
2350        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        // Verify the invariant is preserved by babai_reduce_rns.
2382        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    /// Full n=1024 keygen exercises the runtime-K flat-word babai reduction down
2454    /// to depth 9 (n=2, ~149 primes).  Validates `fG − gF = q` end-to-end.
2455    #[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}