use super::constants::P;
#[inline(always)]
pub fn freeze_ntt(a: i32) -> i32 {
const Q: i32 = 4_591;
let b = a - Q * ((228 * a) >> 20);
let c = b - Q * ((58_470 * b + 134_217_728) >> 28);
c
}
#[inline(always)]
pub fn mod_mul_ntt(a: i32, b: i32) -> i32 {
freeze_ntt(a * b)
}
pub fn mod_pow(mut base: i32, mut exp: i32, modulus: i32) -> i32 {
let mut result = 1;
base = ((base % modulus) + modulus) % modulus;
while exp > 0 {
if exp & 1 == 1 {
result = mod_mul_ntt(result, base) % modulus;
}
exp >>= 1;
base = mod_mul_ntt(base, base) % modulus;
}
result
}
#[inline(always)]
pub fn mod_inv(a: i32) -> i32 {
const Q: i32 = 4_591;
mod_pow(a, Q - 2, Q)
}
#[inline(always)]
fn mod_add_ntt(a: i16, b: i16) -> i16 {
freeze_ntt(a as i32 + b as i32) as i16
}
pub fn karatsuba_mul(a: &[i16], b: &[i16]) -> Vec<i16> {
const THRESHOLD: usize = 64;
let n = a.len();
debug_assert_eq!(n, b.len(), "Karatsuba requires equal-length inputs");
if n == 0 {
return vec![];
}
let full_len = 2 * n - 1;
if n <= THRESHOLD {
let mut result = vec![0i16; full_len];
for i in 0..full_len {
let mut acc = 0i32;
let lo = if i >= n { i - n + 1 } else { 0 };
let hi = i.min(n - 1);
for j in lo..=hi {
acc += (a[j] as i32) * (b[i - j] as i32);
}
result[i] = freeze_ntt(acc) as i16;
}
return result;
}
let half = n / 2;
let n_high = n - half;
let a_low = &a[..half];
let a_high = &a[half..];
let b_low = &b[..half];
let b_high = &b[half..];
let mid = half.max(n_high);
let mut a_sum = vec![0i16; mid];
let mut b_sum = vec![0i16; mid];
for i in 0..half {
a_sum[i] = mod_add_ntt(a_low[i], if i < n_high { a_high[i] } else { 0 });
b_sum[i] = mod_add_ntt(b_low[i], if i < n_high { b_high[i] } else { 0 });
}
for i in half..n_high {
a_sum[i] = a_high[i];
b_sum[i] = b_high[i];
}
let z0 = karatsuba_mul(a_low, b_low); let z2 = karatsuba_mul(a_high, b_high); let z1_temp = karatsuba_mul(&a_sum, &b_sum);
let z1_len = z1_temp.len().max(z0.len()).max(z2.len());
let mut z1 = vec![0i16; z1_len];
for i in 0..z1_len {
let mut v = 0i32;
if i < z1_temp.len() { v += z1_temp[i] as i32; }
if i < z0.len() { v -= z0[i] as i32; }
if i < z2.len() { v -= z2[i] as i32; }
z1[i] = freeze_ntt(v) as i16;
}
let mut result = vec![0i16; full_len];
for i in 0..z0.len() {
result[i] = z0[i];
}
for i in 0..z1.len() {
let idx = half + i;
if idx < full_len {
result[idx] = mod_add_ntt(result[idx], z1[i]);
}
}
for i in 0..z2.len() {
let idx = 2 * half + i;
if idx < full_len {
result[idx] = mod_add_ntt(result[idx], z2[i]);
}
}
result
}
pub fn ntru_poly_mul_opt(f: &[i16], g: &[i16]) -> Vec<i16> {
let mut fg = vec![0i32; 2 * P - 1];
let result = karatsuba_mul(f, g);
for (i, &val) in result.iter().enumerate() {
fg[i] = val as i32;
}
let mut result = vec![0i16; P];
for i in (P..(2 * P - 1)).rev() {
let coeff = freeze_ntt(fg[i]);
let target1 = i - P;
let target2 = i - P + 1;
fg[target1] += coeff as i32;
if target2 < P {
fg[target2] += coeff as i32;
}
}
for i in 0..P {
result[i] = freeze_ntt(fg[i]) as i16;
}
result
}
pub fn ntru_poly_mul_karatsuba(f: &[i16], g: &[i16]) -> Vec<i16> {
ntru_poly_mul_opt(f, g)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freeze_ntt() {
assert_eq!(freeze_ntt(0), 0);
assert_eq!(freeze_ntt(4591), 0);
assert_eq!(freeze_ntt(4591 * 2), 0);
assert_eq!(freeze_ntt(2295), 2295);
}
#[test]
fn test_mod_pow() {
assert_eq!(mod_pow(2, 0, 4591), 1);
assert_eq!(mod_pow(2, 1, 4591), 2);
assert_eq!(mod_pow(2, 10, 4591), 1024);
}
#[test]
fn test_karatsuba_small() {
let f = vec![1i16, 2i16, 3i16, 4i16, 5i16, 6i16, 7i16, 8i16];
let g = vec![2i16, 3i16, 4i16, 5i16, 6i16, 7i16, 8i16, 9i16];
let result = karatsuba_mul(&f, &g);
assert_eq!(result[0], 2);
assert_eq!(result[1], 7);
}
#[test]
fn test_karatsuba_mul() {
let f: Vec<i16> = (0..P).map(|i| (i % 3) as i16 - 1).collect();
let g: Vec<i16> = (0..P).map(|i| (i % 5) as i16 - 2).collect();
let result = karatsuba_mul(&f, &g);
assert_eq!(result.len(), 2 * P - 1);
for &val in &result {
assert!(val >= -2295 && val < 2296);
}
}
#[test]
fn test_ntru_poly_mul_opt() {
let f: Vec<i16> = (0..P).map(|i| (i % 3) as i16 - 1).collect();
let g: Vec<i16> = (0..P).map(|i| (i % 5) as i16 - 2).collect();
let result = ntru_poly_mul_opt(&f, &g);
assert_eq!(result.len(), P);
for &val in &result {
assert!(val >= -2295 && val < 2296);
}
}
#[test]
fn test_ntru_poly_mul_karatsuba_roundtrip_agrees_with_scalar() {
let f: Vec<i16> = (0..P).map(|i| (i % 7) as i16 - 3).collect();
let g: Vec<i16> = (0..P).map(|i| (i % 11) as i16 - 5).collect();
let karatsuba_result = ntru_poly_mul_karatsuba(&f, &g);
let mut naive = vec![0i16; P];
for i in 0..P {
let mut r = 0i32;
for j in 0..=i {
r += (f[j] as i32) * (g[i - j] as i32);
}
naive[i] = freeze_ntt(r) as i16;
}
let mut fg_naive = vec![0i16; 2 * P - 1];
fg_naive[..P].copy_from_slice(&naive);
for i in P..(2 * P - 1) {
let mut r = 0i32;
for j in (i - P + 1)..P {
r += (f[j] as i32) * (g[i - j] as i32);
}
fg_naive[i] = freeze_ntt(r) as i16;
}
let mut reduced = vec![0i16; P];
for i in (P..(2 * P - 1)).rev() {
let coeff = fg_naive[i] as i32;
let target1 = i - P;
let target2 = i - P + 1;
fg_naive[target1] = freeze_ntt(fg_naive[target1] as i32 + coeff) as i16;
if target2 < P {
fg_naive[target2] = freeze_ntt(fg_naive[target2] as i32 + coeff) as i16;
}
}
for i in 0..P {
reduced[i] = fg_naive[i];
}
for i in 0..P {
assert_eq!(
karatsuba_result[i], reduced[i],
"Coefficient {} differs: Karatsuba = {}, naive = {}",
i, karatsuba_result[i], reduced[i]
);
}
}
}