plat_core/params.rs
1//! Parameter sets for lattice-based FHE.
2//!
3//! We use NTT-friendly primes: q ≡ 1 (mod 2N) so that primitive 2N-th roots
4//! of unity exist in Z_q.
5
6use crate::modular::mod_pow;
7
8/// FHE parameter set.
9#[derive(Debug, Clone, Copy)]
10pub struct Params {
11 /// Ring dimension (must be a power of 2).
12 pub n: usize,
13 /// Ciphertext modulus (must be prime, q ≡ 1 mod 2N).
14 pub q: u64,
15 /// Gaussian error standard deviation (as fixed-point × 1000).
16 pub sigma_x1000: u64,
17 /// Plaintext modulus for encoding.
18 pub t: u64,
19}
20
21impl Params {
22 /// Research-grade parameters: N=2048, ~100-bit security.
23 /// q is an NTT-friendly prime: q ≡ 1 (mod 2*2048 = 4096).
24 pub fn research_2048() -> Self {
25 // q = 2^40 - 2^14 + 1 = 1099511611393 — but let's use a known good one.
26 // q = 132120577 (prime, ≡ 1 mod 4096, fits in u64 comfortably for research)
27 // Actually for research quality let's use a larger prime for noise headroom.
28 // q = 1152921504606830593 (2^60 - 2^14 + 1) — too large for comfortable NTT.
29 //
30 // Practical choice: q = 0xFFFFFFFF00000001 = 2^64 - 2^32 + 1 (Goldilocks prime)
31 // This is NTT-friendly with N up to 2^31.
32 // But it's close to u64::MAX, risking overflow in u128 multiplications — fine.
33 //
34 // Simpler: q = 132120577 = 2^27 - 2^13 + 1, prime, ≡ 1 mod 4096.
35 // This gives ~27 bits of modulus, enough for depth-1 circuits with research params.
36 Self {
37 n: 2048,
38 q: 132120577,
39 sigma_x1000: 3200, // σ = 3.2
40 t: 65537, // plaintext modulus, prime
41 }
42 }
43
44 /// Small parameters for fast testing: N=256.
45 /// q/t ratio ≈ 48, giving about 5 bits of noise budget per coefficient.
46 pub fn test_small() -> Self {
47 // q = 12289 is a classic lattice prime, 12289 ≡ 1 mod 512.
48 // t = 256: q/t ≈ 48, enough for small noise and a few additions.
49 // σ = 1.0 to keep noise small for testing.
50 Self {
51 n: 256,
52 q: 12289,
53 sigma_x1000: 1000, // σ = 1.0
54 t: 17, // small plaintext modulus for adequate q/t ratio (~723)
55 }
56 }
57
58 /// Tiny parameters for unit tests only: N=64.
59 pub fn test_tiny() -> Self {
60 // q = 769, t = 5, q/t ≈ 153. σ = 1.0.
61 // 769 - 1 = 768 = 2^8 * 3. 2N = 128. 768 / 128 = 6 ✓
62 Self {
63 n: 64,
64 q: 769,
65 sigma_x1000: 1000, // σ = 1.0
66 t: 5,
67 }
68 }
69
70 /// Find a primitive 2N-th root of unity modulo q.
71 pub fn ntt_root(&self) -> u64 {
72 let two_n = (2 * self.n) as u64;
73 // q - 1 must be divisible by 2N
74 assert!(
75 (self.q - 1) % two_n == 0,
76 "q - 1 = {} is not divisible by 2N = {two_n}",
77 self.q - 1
78 );
79 // Find generator: try small values
80 let exp = (self.q - 1) / two_n;
81 for g in 2..self.q {
82 let w = mod_pow(g, exp, self.q);
83 if w != 1 && mod_pow(w, self.n as u64, self.q) != 1 {
84 // w is a primitive 2N-th root if w^N ≡ -1 mod q
85 let wn = mod_pow(w, self.n as u64, self.q);
86 if wn == self.q - 1 {
87 return w;
88 }
89 }
90 }
91 panic!("no primitive 2N-th root of unity found");
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_tiny_root() {
101 let p = Params::test_tiny();
102 let w = p.ntt_root();
103 let wn = mod_pow(w, p.n as u64, p.q);
104 assert_eq!(wn, p.q - 1, "w^N should equal -1 mod q");
105 let w2n = mod_pow(w, (2 * p.n) as u64, p.q);
106 assert_eq!(w2n, 1, "w^(2N) should equal 1 mod q");
107 }
108
109 #[test]
110 fn test_small_root() {
111 let p = Params::test_small();
112 let w = p.ntt_root();
113 let wn = mod_pow(w, p.n as u64, p.q);
114 assert_eq!(wn, p.q - 1);
115 }
116}