Skip to main content

dcrypt_algorithms/poly/ntt/
mod.rs

1//! Number Theoretic Transform Implementation
2//!
3//! Generic NTT/iNTT for polynomials over finite fields, with full
4//! FIPS-204 compliance for ML-DSA and a generic cyclic-transform fallback.
5//!
6//! ## ML-DSA (FIPS-204)
7//! - Forward NTT: Algorithm 41 (DIF with standard domain I/O)
8//! - Inverse NTT: Algorithm 42 (GS with standard domain I/O)
9//! - Twiddle factors: Precomputed in Montgomery form (ζ·R mod q)
10//! - Butterfly differences: Kept in [0, 2Q) range as per spec
11//! - Pointwise multiplication: Standard domain multiplication
12//!
13//! ## Generic fallback
14//! - Cooley-Tukey NTT with on-the-fly twiddle computation
15//! - Full Montgomery domain processing
16//! - Pointwise multiplication: Montgomery domain multiplication
17
18use super::params::{Modulus, NttModulus, PostInvNtt};
19use super::polynomial::Polynomial;
20use crate::error::{Error, Result};
21
22/// Modular exponentiation in standard domain
23#[inline(always)]
24fn pow_mod<M: Modulus>(mut base: u32, mut exp: u32) -> u32 {
25    let mut acc: u32 = 1;
26    while exp != 0 {
27        if (exp & 1) == 1 {
28            acc = ((acc as u64 * base as u64) % M::Q as u64) as u32;
29        }
30        base = ((base as u64 * base as u64) % M::Q as u64) as u32;
31        exp >>= 1;
32    }
33    acc
34}
35
36/// Forward Number Theoretic Transform
37pub trait NttOperator<M: NttModulus> {
38    /// Performs forward NTT on polynomial in-place
39    ///
40    /// # ML-DSA (FIPS-204)
41    /// - Implements Algorithm 41 (DIF)
42    /// - Input: coefficients in standard domain
43    /// - Output: coefficients in standard domain
44    ///
45    /// # Generic fallback
46    /// - Implements Cooley-Tukey NTT
47    /// - Converts to Montgomery domain internally
48    fn ntt(poly: &mut Polynomial<M>) -> Result<()>;
49}
50
51/// Inverse Number Theoretic Transform
52pub trait InverseNttOperator<M: NttModulus> {
53    /// Performs inverse NTT on polynomial in-place
54    ///
55    /// # ML-DSA (FIPS-204)
56    /// - Implements Algorithm 42 (GS)
57    /// - Input: coefficients in standard domain
58    /// - Output: standard or Montgomery domain based on POST_INVNTT_MODE
59    ///
60    /// # Generic fallback
61    /// - Implements Cooley-Tukey inverse NTT
62    /// - Scales by N^(-1) and converts back to standard domain
63    fn inv_ntt(poly: &mut Polynomial<M>) -> Result<()>;
64}
65
66/// Cooley-Tukey NTT implementation
67pub struct CooleyTukeyNtt;
68
69/// Montgomery reduction: computes a * R^-1 mod Q
70///
71/// For a ∈ [0, Q·R), returns a·R^(-1) mod Q in [0, Q)
72#[inline(always)]
73pub fn montgomery_reduce<M: NttModulus>(a: u64) -> u32 {
74    let q = M::Q as u64;
75    let neg_qinv = M::NEG_QINV as u64;
76
77    // Compute m = (a * NEG_QINV) mod 2^32
78    let m = ((a as u32) as u64).wrapping_mul(neg_qinv) & 0xFFFFFFFF;
79    // Compute t = (a + m * q) >> 32
80    let t = a.wrapping_add(m.wrapping_mul(q)) >> 32;
81
82    // Conditional reduction
83    let result = t as u32;
84    let mask = ((result >= M::Q) as u32).wrapping_neg();
85    result.wrapping_sub(M::Q & mask)
86}
87
88/// Reduce any u32 to [0, Q)
89/// Handles both normal range and wrapped values from underflow
90#[inline]
91fn reduce_to_q<M: Modulus>(x: u32) -> u32 {
92    // Fast path for common case (x < 4Q)
93    let mut y = x;
94    y -= M::Q & ((y >= M::Q) as u32).wrapping_neg();
95    y -= M::Q & ((y >= M::Q) as u32).wrapping_neg();
96
97    if y < M::Q {
98        return y;
99    }
100
101    // Barrett reduction for large/wrapped values
102    let (mu, k) = if M::BARRETT_MU != 0 {
103        (M::BARRETT_MU, M::BARRETT_K)
104    } else {
105        // Dynamic computation for moduli without precomputed constants
106        let log_q = 64 - (M::Q as u64).leading_zeros(); // FIXED: Removed unnecessary cast
107        let k = log_q + 32;
108        let mu = (1u128 << k) / M::Q as u128; // FIXED: Removed unnecessary cast
109        (mu, k)
110    };
111
112    let x_wide = y as u128;
113    let q = ((x_wide * mu) >> k) as u32;
114    let mut r = y.wrapping_sub(q.wrapping_mul(M::Q));
115
116    r = r.wrapping_sub(M::Q & ((r >= M::Q) as u32).wrapping_neg());
117    r
118}
119
120/// Montgomery multiplication: a * b * R^-1 mod Q
121/// Accepts extended range inputs (e.g., [0, 9Q)) to preserve sign encoding
122#[inline(always)]
123fn montgomery_mul<M: NttModulus>(a: u32, b: u32) -> u32 {
124    montgomery_reduce::<M>((a as u64) * (b as u64))
125}
126
127/// Modular addition with full reduction
128#[inline(always)]
129fn add_mod<M: Modulus>(a: u32, b: u32) -> u32 {
130    ((a as u64 + b as u64) % M::Q as u64) as u32
131}
132
133/// Fast modular addition for inputs < Q
134#[inline(always)]
135fn add_mod_fast<M: Modulus>(a: u32, b: u32) -> u32 {
136    let s = a + b;
137    let mask = ((s >= M::Q) as u32).wrapping_neg();
138    s - (M::Q & mask)
139}
140
141/// Fast modular subtraction for inputs < Q
142#[inline(always)]
143fn sub_mod_fast<M: Modulus>(a: u32, b: u32) -> u32 {
144    let t = a.wrapping_add(M::Q).wrapping_sub(b);
145    let mask = ((t >= M::Q) as u32).wrapping_neg();
146    t - (M::Q & mask)
147}
148
149/// Modular subtraction returning [0, 2Q)
150/// Used in FIPS-204 butterflies to preserve sign information
151#[inline(always)]
152fn sub_mod_upto_2q<M: Modulus>(a: u32, b: u32) -> u32 {
153    a.wrapping_add(M::Q).wrapping_sub(b)
154}
155
156/// Convert standard domain to Montgomery domain
157#[inline(always)]
158fn to_montgomery<M: NttModulus>(val: u32) -> u32 {
159    ((val as u64 * M::MONT_R as u64) % M::Q as u64) as u32
160}
161
162impl<M: NttModulus> NttOperator<M> for CooleyTukeyNtt {
163    fn ntt(poly: &mut Polynomial<M>) -> Result<()> {
164        let n = M::N;
165        if n & (n - 1) != 0 {
166            return Err(Error::Parameter {
167                name: "NTT".into(),
168                reason: "Polynomial degree must be a power of 2".into(),
169            });
170        }
171
172        let coeffs = poly.as_mut_coeffs_slice();
173        let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
174
175        if is_ml_dsa {
176            // FIPS-204 Algorithm 41: Forward NTT
177            // Decimation-in-Frequency (DIF) with row-major twiddle traversal
178            // Input: standard domain, Output: standard domain
179            let mut k = 0;
180            let mut len = n / 2; // Start at 128 for N=256
181
182            while len >= 1 {
183                // Row-major (block-first) iteration matches twiddle table order
184                for start in (0..n).step_by(2 * len) {
185                    let zeta = M::ZETAS[k]; // ζ·R mod q (Montgomery form)
186                    k += 1;
187
188                    for j in start..start + len {
189                        let a = coeffs[j];
190                        let b = coeffs[j + len];
191
192                        // FIPS-204 DIF butterfly:
193                        // t = ζ * b (Montgomery mul with ζ·R gives standard domain)
194                        let t = montgomery_mul::<M>(b, zeta);
195                        // a' = a + t mod q
196                        coeffs[j] = add_mod::<M>(a, t);
197                        // b' = a - t + Q (kept in [0, 2Q) per Algorithm 41)
198                        coeffs[j + len] = sub_mod_upto_2q::<M>(a, t);
199                    }
200                }
201
202                len >>= 1;
203            }
204
205            // Reduce all coefficients to [0, Q) for ML-DSA compatibility
206            for c in coeffs.iter_mut() {
207                *c = reduce_to_q::<M>(*c);
208            }
209        } else {
210            // Generic cyclic NTT.
211            for c in coeffs.iter_mut() {
212                *c = to_montgomery::<M>(*c);
213            }
214
215            let mut len = 1_usize;
216            while len < n {
217                let exp = n / (len << 1);
218                let root_std = pow_mod::<M>(M::ZETA, exp as u32);
219                let root_mont = to_montgomery::<M>(root_std);
220
221                for start in (0..n).step_by(len << 1) {
222                    let mut w_mont = M::MONT_R;
223
224                    for j in 0..len {
225                        let u = coeffs[start + j];
226                        let v = montgomery_mul::<M>(coeffs[start + j + len], w_mont);
227
228                        coeffs[start + j] = add_mod_fast::<M>(u, v);
229                        coeffs[start + j + len] = sub_mod_fast::<M>(u, v);
230
231                        w_mont = montgomery_mul::<M>(w_mont, root_mont);
232                    }
233                }
234                len <<= 1;
235            }
236            // Keep coefficients in Montgomery form for the inverse transform.
237        }
238
239        Ok(())
240    }
241}
242
243impl<M: NttModulus> InverseNttOperator<M> for CooleyTukeyNtt {
244    fn inv_ntt(poly: &mut Polynomial<M>) -> Result<()> {
245        let n = M::N;
246        if n & (n - 1) != 0 {
247            return Err(Error::Parameter {
248                name: "Inverse NTT".into(),
249                reason: "Polynomial degree must be a power of 2".into(),
250            });
251        }
252
253        let coeffs = poly.as_mut_coeffs_slice();
254        let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
255
256        if is_ml_dsa {
257            // FIPS-204 Algorithm 42: Inverse NTT
258            // Gentleman-Sande (GS) with row-major traversal
259
260            // Pre-condition: ensure coefficients < Q for GS butterflies
261            for c in coeffs.iter_mut() {
262                *c = reduce_to_q::<M>(*c);
263            }
264
265            let mut k = M::ZETAS.len(); // Start after last entry
266            let mut len = 1;
267
268            while len < n {
269                // Row-major iteration matching forward NTT structure
270                for start in (0..n).step_by(2 * len) {
271                    k -= 1; // Traverse ZETAS in reverse
272
273                    // Use negated forward twiddle for inverse
274                    let zeta_fwd = M::ZETAS[k];
275                    let zeta = if zeta_fwd == 0 { 0 } else { M::Q - zeta_fwd };
276
277                    for j in start..start + len {
278                        let t = coeffs[j];
279                        let u = coeffs[j + len];
280
281                        // FIPS-204 GS butterfly:
282                        // Line 13: w_j ← w_j + w_{j+len}
283                        coeffs[j] = add_mod::<M>(t, u);
284                        // Line 14: w_{j+len} ← ζ^(-1) * (w_j - w_{j+len})
285                        let diff = sub_mod_upto_2q::<M>(t, u);
286                        coeffs[j + len] = montgomery_mul::<M>(diff, zeta);
287                    }
288                }
289
290                len <<= 1;
291            }
292
293            // Final reduction before N^(-1) scaling
294            for c in coeffs.iter_mut() {
295                *c = reduce_to_q::<M>(*c);
296            }
297
298            // Scale by N^(-1) in standard domain
299            let n_inv_std = pow_mod::<M>(M::N as u32, M::Q - 2);
300            for c in coeffs.iter_mut() {
301                *c = ((*c as u64 * n_inv_std as u64) % M::Q as u64) as u32;
302            }
303
304            match M::POST_INVNTT_MODE {
305                PostInvNtt::Standard => {} // Already in standard domain
306                PostInvNtt::Montgomery => {
307                    // Convert to Montgomery if requested
308                    for c in coeffs.iter_mut() {
309                        *c = to_montgomery::<M>(*c);
310                    }
311                }
312            }
313        } else {
314            // Generic cyclic inverse NTT.
315            let root_inv_std = pow_mod::<M>(M::ZETA, M::Q - 2); // FIXED: Removed unnecessary cast
316
317            let mut len = n >> 1;
318            while len >= 1 {
319                let exp = n / (len << 1);
320                let root_std = pow_mod::<M>(root_inv_std, exp as u32);
321                let root_mont = to_montgomery::<M>(root_std);
322
323                for start in (0..n).step_by(len << 1) {
324                    let mut w_mont = M::MONT_R;
325
326                    for j in 0..len {
327                        let u = coeffs[start + j];
328                        let v = coeffs[start + j + len];
329
330                        coeffs[start + j] = add_mod_fast::<M>(u, v);
331                        coeffs[start + j + len] =
332                            montgomery_mul::<M>(sub_mod_fast::<M>(u, v), w_mont);
333
334                        w_mont = montgomery_mul::<M>(w_mont, root_mont);
335                    }
336                }
337                len >>= 1;
338            }
339
340            // Scale by N^(-1)
341            for c in coeffs.iter_mut() {
342                *c = montgomery_mul::<M>(*c, M::N_INV);
343            }
344
345            if M::POST_INVNTT_MODE == PostInvNtt::Standard {
346                for c in coeffs.iter_mut() {
347                    *c = montgomery_reduce::<M>(*c as u64);
348                }
349            }
350        }
351
352        Ok(())
353    }
354}
355
356/// Extension methods for Polynomial
357impl<M: NttModulus> Polynomial<M> {
358    /// Convert polynomial to NTT domain
359    pub fn ntt_inplace(&mut self) -> Result<()> {
360        CooleyTukeyNtt::ntt(self)
361    }
362
363    /// Convert polynomial from NTT domain
364    pub fn from_ntt_inplace(&mut self) -> Result<()> {
365        CooleyTukeyNtt::inv_ntt(self)
366    }
367
368    /// Pointwise multiplication in NTT domain
369    ///
370    /// Both polynomials must already be in NTT domain.
371    /// For ML-DSA: inputs/output in standard domain (post-NTT)
372    /// For the generic fallback: inputs/output in Montgomery domain
373    pub fn ntt_mul(&self, other: &Self) -> Self {
374        let mut result = Self::zero();
375        let n = M::N;
376        let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
377
378        if is_ml_dsa {
379            // ML-DSA: coefficients are in standard domain after NTT
380            // Use standard multiplication
381            for i in 0..n {
382                result.coeffs[i] =
383                    ((self.coeffs[i] as u64 * other.coeffs[i] as u64) % M::Q as u64) as u32;
384            }
385        } else {
386            // Generic coefficients are in Montgomery domain after NTT.
387            // Use Montgomery multiplication to keep result in Montgomery domain
388            for i in 0..n {
389                result.coeffs[i] = montgomery_mul::<M>(self.coeffs[i], other.coeffs[i]);
390            }
391        }
392
393        result
394    }
395}
396
397#[cfg(test)]
398mod tests;