arnaucube
arnaucube/fhe-studyPublic Self-hosted repository
fhe-study/arithmetic/src/ntt.rs
arithmetic/src/ntt.rsRaw
  1//! Implementation of the NTT & iNTT, following the CT & GS algorighms, more details in
  2//! https://eprint.iacr.org/2017/727.pdf, some notes at
  3//! https://github.com/arnaucube/math/blob/master/notes_ntt.pdf .
  4use crate::zq::Zq;
  5
  6#[derive(Debug)]
  7pub struct NTT<const Q: u64, const N: usize> {}
  8
  9impl<const Q: u64, const N: usize> NTT<Q, N> {
 10    const N_INV: Zq<Q> = Zq(const_inv_mod::<Q>(N as u64));
 11    // since we work over Zq[X]/(X^N+1) (negacyclic), get the 2*N-th root of unity
 12    pub(crate) const ROOT_OF_UNITY: u64 = primitive_root_of_unity::<Q>(2 * N);
 13    pub(crate) const ROOTS_OF_UNITY: [Zq<Q>; N] = roots_of_unity(Self::ROOT_OF_UNITY);
 14    const ROOTS_OF_UNITY_INV: [Zq<Q>; N] = roots_of_unity_inv(Self::ROOTS_OF_UNITY);
 15}
 16
 17impl<const Q: u64, const N: usize> NTT<Q, N> {
 18    /// implements the Cooley-Tukey (CT) algorithm. Details at
 19    /// https://eprint.iacr.org/2017/727.pdf, also some notes at section 3.1 of
 20    /// https://github.com/arnaucube/math/blob/master/notes_ntt.pdf
 21    pub fn ntt(a: [Zq<Q>; N]) -> [Zq<Q>; N] {
 22        let mut t = N / 2;
 23        let mut m = 1;
 24        let mut r: [Zq<Q>; N] = a.clone();
 25        while m < N {
 26            let mut k = 0;
 27            for i in 0..m {
 28                let S: Zq<Q> = Self::ROOTS_OF_UNITY[m + i];
 29                for j in k..k + t {
 30                    let U: Zq<Q> = r[j];
 31                    let V: Zq<Q> = r[j + t] * S;
 32                    r[j] = U + V;
 33                    r[j + t] = U - V;
 34                }
 35                k = k + 2 * t;
 36            }
 37            t /= 2;
 38            m *= 2;
 39        }
 40        r
 41    }
 42
 43    /// implements the Cooley-Tukey (CT) algorithm. Details at
 44    /// https://eprint.iacr.org/2017/727.pdf, also some notes at section 3.2 of
 45    /// https://github.com/arnaucube/math/blob/master/notes_ntt.pdf
 46    pub fn intt(a: [Zq<Q>; N]) -> [Zq<Q>; N] {
 47        let mut t = 1;
 48        let mut m = N / 2;
 49        let mut r: [Zq<Q>; N] = a.clone();
 50        while m > 0 {
 51            let mut k = 0;
 52            for i in 0..m {
 53                let S: Zq<Q> = Self::ROOTS_OF_UNITY_INV[m + i];
 54                for j in k..k + t {
 55                    let U: Zq<Q> = r[j];
 56                    let V: Zq<Q> = r[j + t];
 57                    r[j] = U + V;
 58                    r[j + t] = (U - V) * S;
 59                }
 60                k += 2 * t;
 61            }
 62            t *= 2;
 63            m /= 2;
 64        }
 65        for i in 0..N {
 66            r[i] = r[i] * Self::N_INV;
 67        }
 68        r
 69    }
 70}
 71
 72/// computes a primitive N-th root of unity using the method described by Thomas
 73/// Pornin in https://crypto.stackexchange.com/a/63616
 74const fn primitive_root_of_unity<const Q: u64>(N: usize) -> u64 {
 75    assert!(N.is_power_of_two());
 76    assert!((Q - 1) % N as u64 == 0);
 77
 78    let n: u64 = N as u64;
 79    let mut k = 1;
 80    while k < Q {
 81        // alternatively could get a random k at each iteration, if so, add the following if:
 82        // `if k == 0 { continue; }`
 83        let w = const_exp_mod::<Q>(k, (Q - 1) / n);
 84        if const_exp_mod::<Q>(w, n / 2) != 1 {
 85            return w; // w is a primitive N-th root of unity
 86        }
 87        k += 1;
 88    }
 89    panic!("No primitive root of unity");
 90}
 91
 92const fn roots_of_unity<const Q: u64, const N: usize>(w: u64) -> [Zq<Q>; N] {
 93    let mut r: [Zq<Q>; N] = [Zq(0u64); N];
 94    let mut i = 0;
 95    let log_n = N.ilog2();
 96    while i < N {
 97        // (return the roots in bit-reverset order)
 98        let j = ((i as u64).reverse_bits() >> (64 - log_n)) as usize;
 99        r[i] = Zq(const_exp_mod::<Q>(w, j as u64));
100        i += 1;
101    }
102    r
103}
104
105const fn roots_of_unity_inv<const Q: u64, const N: usize>(v: [Zq<Q>; N]) -> [Zq<Q>; N] {
106    // assumes that the inputted roots are already in bit-reverset order
107    let mut r: [Zq<Q>; N] = [Zq(0u64); N];
108    let mut i = 0;
109    while i < N {
110        r[i] = Zq(const_inv_mod::<Q>(v[i].0));
111        i += 1;
112    }
113    r
114}
115
116/// returns x^k mod Q
117const fn const_exp_mod<const Q: u64>(x: u64, k: u64) -> u64 {
118    // work on u128 to avoid overflow
119    let mut r = 1u128;
120    let mut x = x as u128;
121    let mut k = k as u128;
122    x = x % Q as u128;
123    // exponentiation by square strategy
124    while k > 0 {
125        if k % 2 == 1 {
126            r = (r * x) % Q as u128;
127        }
128        x = (x * x) % Q as u128;
129        k /= 2;
130    }
131    r as u64
132}
133
134/// returns x^-1 mod Q
135const fn const_inv_mod<const Q: u64>(x: u64) -> u64 {
136    // by Fermat's Little Theorem, x^-1 mod q \equiv  x^{q-2} mod q
137    const_exp_mod::<Q>(x, Q - 2)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    use anyhow::Result;
145    use std::array;
146
147    #[test]
148    fn test_ntt() -> Result<()> {
149        const Q: u64 = 2u64.pow(16) + 1;
150        const N: usize = 4;
151
152        let a: [u64; N] = [1u64, 2, 3, 4];
153        let a: [Zq<Q>; N] = array::from_fn(|i| Zq::from_u64(a[i]));
154
155        let a_ntt = NTT::<Q, N>::ntt(a);
156
157        let a_intt = NTT::<Q, N>::intt(a_ntt);
158
159        dbg!(&a);
160        dbg!(&a_ntt);
161        dbg!(&a_intt);
162        dbg!(NTT::<Q, N>::ROOT_OF_UNITY);
163        dbg!(NTT::<Q, N>::ROOTS_OF_UNITY);
164
165        assert_eq!(a, a_intt);
166        Ok(())
167    }
168
169    #[test]
170    fn test_ntt_loop() -> Result<()> {
171        const Q: u64 = 2u64.pow(16) + 1;
172        const N: usize = 512;
173
174        use rand::distributions::Distribution;
175        use rand::distributions::Uniform;
176        let mut rng = rand::thread_rng();
177        let dist = Uniform::new(0_f64, Q as f64);
178
179        for _ in 0..100 {
180            let a: [Zq<Q>; N] = array::from_fn(|_| Zq::from_f64(dist.sample(&mut rng)));
181            let a_ntt = NTT::<Q, N>::ntt(a);
182            let a_intt = NTT::<Q, N>::intt(a_ntt);
183            assert_eq!(a, a_intt);
184        }
185        Ok(())
186    }
187}