1const Q: i32 = 3329;
12const QINV: i32 = -3327; const ZETAS: [i16; 128] = [
16 -1044, -758, -359, -1517, 1493, 1422, 287, 202, -171, 622, 1577, 182, 962, -1202, -1474, 1468,
17 573, -1325, 264, 383, -829, 1458, -1602, -130, -681, 1017, 732, 608, -1542, 411, -205, -1571,
18 1223, 652, -552, 1015, -1293, 1491, -282, -1544, 516, -8, -320, -666, -1618, -1162, 126, 1469,
19 -853, -90, -271, 830, 107, -1421, -247, -951, -398, 961, -1508, -725, 448, -1065, 677, -1275,
20 -1103, 430, 555, 843, -1251, 871, 1550, 105, 422, 587, 177, -235, -291, -460, 1574, 1653, -246,
21 778, 1159, -147, -777, 1483, -602, 1119, -1590, 644, -872, 349, 418, 329, -156, -75, 817, 1097,
22 603, 610, 1322, -1285, -1465, 384, -1215, -136, 1218, -1335, -874, 220, -1187, -1659, -1185,
23 -1530, -1278, 794, -1510, -854, -870, 478, -108, -308, 996, 991, 958, -1460, 1522, 1628,
24];
25
26#[inline]
27fn montgomery_reduce(a: i32) -> i16 {
28 let t = (a as i16).wrapping_mul(QINV as i16);
29 ((a - (t as i32) * Q) >> 16) as i16
30}
31
32#[inline]
36fn cadd_q(x: i16) -> u16 {
37 (x + (Q as i16 & (x >> 15))) as u16
38}
39#[inline]
40fn fqmul(a: i16, b: i16) -> i16 {
41 montgomery_reduce(a as i32 * b as i32)
42}
43
44fn ntt_scalar(r: &mut [i16; 256]) {
46 let mut k = 1usize;
47 let mut len = 128usize;
48 while len >= 2 {
49 let mut start = 0usize;
50 while start < 256 {
51 let zeta = ZETAS[k];
52 k += 1;
53 for j in start..start + len {
54 let t = fqmul(zeta, r[j + len]);
55 r[j + len] = r[j].wrapping_sub(t);
56 r[j] = r[j].wrapping_add(t);
57 }
58 start += 2 * len;
59 }
60 len >>= 1;
61 }
62}
63
64#[cfg(target_arch = "x86_64")]
66#[target_feature(enable = "avx2")]
67unsafe fn ntt_avx2(r: &mut [i16; 256]) {
68 use std::arch::x86_64::*;
69 let qv = _mm256_set1_epi16(Q as i16);
70 let qinvv = _mm256_set1_epi16(QINV as i16);
71 let mut k = 1usize;
72 let mut len = 128usize;
73 while len >= 16 {
75 let mut start = 0usize;
76 while start < 256 {
77 let zeta = _mm256_set1_epi16(ZETAS[k]);
78 k += 1;
79 let mut j = start;
80 while j < start + len {
81 let aj = _mm256_loadu_si256(r.as_ptr().add(j) as *const __m256i);
82 let ajl = _mm256_loadu_si256(r.as_ptr().add(j + len) as *const __m256i);
83 let rlo = _mm256_mullo_epi16(zeta, ajl);
84 let rhi = _mm256_mulhi_epi16(zeta, ajl);
85 let tt = _mm256_mullo_epi16(rlo, qinvv);
86 let tt = _mm256_mulhi_epi16(tt, qv);
87 let t = _mm256_sub_epi16(rhi, tt);
88 _mm256_storeu_si256(r.as_mut_ptr().add(j) as *mut __m256i, _mm256_add_epi16(aj, t));
89 _mm256_storeu_si256(r.as_mut_ptr().add(j + len) as *mut __m256i, _mm256_sub_epi16(aj, t));
90 j += 16;
91 }
92 start += 2 * len;
93 }
94 len >>= 1;
95 }
96 {
98 let lo_mask = _mm256_set_epi64x(0, 0, -1, -1);
99 let mut start = 0usize;
100 while start < 256 {
101 let zeta = _mm256_set1_epi16(ZETAS[k]);
102 k += 1;
103 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
104 let hi = _mm256_permute2x128_si256(v, v, 0x01);
105 let rlo = _mm256_mullo_epi16(zeta, hi);
106 let rhi = _mm256_mulhi_epi16(zeta, hi);
107 let tt = _mm256_mullo_epi16(rlo, qinvv);
108 let tt = _mm256_mulhi_epi16(tt, qv);
109 let t = _mm256_sub_epi16(rhi, tt);
110 let add = _mm256_add_epi16(v, t);
111 let sub = _mm256_sub_epi16(v, t);
112 let sub_hi = _mm256_permute2x128_si256(sub, sub, 0x01);
113 let out = _mm256_blendv_epi8(sub_hi, add, lo_mask);
114 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
115 start += 16;
116 }
117 len >>= 1;
118 }
119 {
121 let mask4 = _mm256_set_epi64x(0, -1, 0, -1);
122 let mut start = 0usize;
123 while start < 256 {
124 let z0 = ZETAS[k];
125 let z1 = ZETAS[k + 1];
126 k += 2;
127 let zv = _mm256_set_epi16(z1, z1, z1, z1, z1, z1, z1, z1, z0, z0, z0, z0, z0, z0, z0, z0);
128 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
129 let vhi = _mm256_shuffle_epi32(v, 0x4E);
130 let rlo = _mm256_mullo_epi16(zv, vhi);
131 let rhi = _mm256_mulhi_epi16(zv, vhi);
132 let tt = _mm256_mullo_epi16(rlo, qinvv);
133 let tt = _mm256_mulhi_epi16(tt, qv);
134 let t = _mm256_sub_epi16(rhi, tt);
135 let add = _mm256_add_epi16(v, t);
136 let sub = _mm256_sub_epi16(v, t);
137 let sub_s = _mm256_shuffle_epi32(sub, 0x4E);
138 let out = _mm256_blendv_epi8(sub_s, add, mask4);
139 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
140 start += 16;
141 }
142 len >>= 1;
143 }
144 {
146 let mask2 = _mm256_set_epi32(0, -1, 0, -1, 0, -1, 0, -1);
147 let mut start = 0usize;
148 while start < 256 {
149 let z0 = ZETAS[k];
150 let z1 = ZETAS[k + 1];
151 let z2 = ZETAS[k + 2];
152 let z3 = ZETAS[k + 3];
153 k += 4;
154 let zv = _mm256_set_epi16(z3, z3, z3, z3, z2, z2, z2, z2, z1, z1, z1, z1, z0, z0, z0, z0);
155 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
156 let vhi = _mm256_shuffle_epi32(v, 0xB1);
157 let rlo = _mm256_mullo_epi16(zv, vhi);
158 let rhi = _mm256_mulhi_epi16(zv, vhi);
159 let tt = _mm256_mullo_epi16(rlo, qinvv);
160 let tt = _mm256_mulhi_epi16(tt, qv);
161 let t = _mm256_sub_epi16(rhi, tt);
162 let add = _mm256_add_epi16(v, t);
163 let sub = _mm256_sub_epi16(v, t);
164 let sub_s = _mm256_shuffle_epi32(sub, 0xB1);
165 let out = _mm256_blendv_epi8(sub_s, add, mask2);
166 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
167 start += 16;
168 }
169 }
170}
171
172#[cfg(target_arch = "x86_64")]
175#[target_feature(enable = "avx2")]
176#[inline]
177unsafe fn mont_mul_x16(
178 a: std::arch::x86_64::__m256i,
179 b: std::arch::x86_64::__m256i,
180) -> std::arch::x86_64::__m256i {
181 use std::arch::x86_64::*;
182 let q = _mm256_set1_epi16(Q as i16);
183 let qinv = _mm256_set1_epi16(QINV as i16);
184 let rlo = _mm256_mullo_epi16(b, a);
185 let rhi = _mm256_mulhi_epi16(b, a);
186 let tt = _mm256_mullo_epi16(rlo, qinv);
187 let tt = _mm256_mulhi_epi16(tt, q);
188 _mm256_sub_epi16(rhi, tt)
189}
190
191#[cfg(target_arch = "x86_64")]
196#[target_feature(enable = "avx2")]
197#[inline]
198unsafe fn barrett_x16(a: std::arch::x86_64::__m256i) -> std::arch::x86_64::__m256i {
199 use std::arch::x86_64::*;
200 const V: i16 = (((1 << 26) + Q / 2) / Q) as i16;
201 let q = _mm256_set1_epi16(Q as i16);
202 let hi = _mm256_mulhi_epi16(a, _mm256_set1_epi16(V));
203 let t = _mm256_srai_epi16(_mm256_add_epi16(hi, _mm256_set1_epi16(512)), 10);
204 _mm256_sub_epi16(a, _mm256_mullo_epi16(t, q))
205}
206
207#[cfg(target_arch = "x86_64")]
212#[target_feature(enable = "avx2")]
213unsafe fn invntt_avx2(r: &mut [i16; 256]) {
214 use std::arch::x86_64::*;
215 const F: i16 = 1441;
216 let mut k = 127usize;
217 {
219 let mask2 = _mm256_set_epi32(0, -1, 0, -1, 0, -1, 0, -1);
220 let mut start = 0usize;
221 while start < 256 {
222 let z0 = ZETAS[k];
223 let z1 = ZETAS[k - 1];
224 let z2 = ZETAS[k - 2];
225 let z3 = ZETAS[k - 3];
226 k -= 4;
227 let zv = _mm256_set_epi16(z3, z3, z3, z3, z2, z2, z2, z2, z1, z1, z1, z1, z0, z0, z0, z0);
228 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
229 let vsh = _mm256_shuffle_epi32(v, 0xB1);
230 let lopart = barrett_x16(_mm256_add_epi16(v, vsh));
231 let hipart = mont_mul_x16(zv, _mm256_sub_epi16(v, vsh));
232 let out = _mm256_blendv_epi8(hipart, lopart, mask2);
233 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
234 start += 16;
235 }
236 }
237 {
239 let mask4 = _mm256_set_epi64x(0, -1, 0, -1);
240 let mut start = 0usize;
241 while start < 256 {
242 let z0 = ZETAS[k];
243 let z1 = ZETAS[k - 1];
244 k -= 2;
245 let zv = _mm256_set_epi16(z1, z1, z1, z1, z1, z1, z1, z1, z0, z0, z0, z0, z0, z0, z0, z0);
246 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
247 let vsh = _mm256_shuffle_epi32(v, 0x4E);
248 let lopart = barrett_x16(_mm256_add_epi16(v, vsh));
249 let hipart = mont_mul_x16(zv, _mm256_sub_epi16(v, vsh));
250 let out = _mm256_blendv_epi8(hipart, lopart, mask4);
251 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
252 start += 16;
253 }
254 }
255 {
257 let lo_mask = _mm256_set_epi64x(0, 0, -1, -1);
258 let mut start = 0usize;
259 while start < 256 {
260 let zeta = _mm256_set1_epi16(ZETAS[k]);
261 k -= 1;
262 let v = _mm256_loadu_si256(r.as_ptr().add(start) as *const __m256i);
263 let vsh = _mm256_permute2x128_si256(v, v, 0x01);
264 let lopart = barrett_x16(_mm256_add_epi16(v, vsh));
265 let hipart = mont_mul_x16(zeta, _mm256_sub_epi16(v, vsh));
266 let out = _mm256_blendv_epi8(hipart, lopart, lo_mask);
267 _mm256_storeu_si256(r.as_mut_ptr().add(start) as *mut __m256i, out);
268 start += 16;
269 }
270 }
271 let mut len = 16usize;
273 while len <= 128 {
274 let mut start = 0usize;
275 while start < 256 {
276 let zeta = _mm256_set1_epi16(ZETAS[k]);
277 k = k.wrapping_sub(1);
278 let mut j = start;
279 while j < start + len {
280 let lo = _mm256_loadu_si256(r.as_ptr().add(j) as *const __m256i);
281 let hi = _mm256_loadu_si256(r.as_ptr().add(j + len) as *const __m256i);
282 _mm256_storeu_si256(
283 r.as_mut_ptr().add(j) as *mut __m256i,
284 barrett_x16(_mm256_add_epi16(lo, hi)),
285 );
286 _mm256_storeu_si256(
287 r.as_mut_ptr().add(j + len) as *mut __m256i,
288 mont_mul_x16(zeta, _mm256_sub_epi16(hi, lo)),
289 );
290 j += 16;
291 }
292 start += 2 * len;
293 }
294 len <<= 1;
295 }
296 let fv = _mm256_set1_epi16(F);
298 let mut i = 0usize;
299 while i < 256 {
300 let v = _mm256_loadu_si256(r.as_ptr().add(i) as *const __m256i);
301 _mm256_storeu_si256(r.as_mut_ptr().add(i) as *mut __m256i, mont_mul_x16(fv, v));
302 i += 16;
303 }
304}
305
306fn mlkem_ntt_raw(input: &[i64]) -> Vec<i64> {
309 assert_eq!(input.len(), 256, "mlkemNtt expects exactly 256 coefficients");
310 let mut r = [0i16; 256];
314 for (slot, &x) in r.iter_mut().zip(input) {
315 *slot = x.rem_euclid(Q as i64) as i16;
316 }
317 #[cfg(target_arch = "x86_64")]
318 {
319 if std::is_x86_feature_detected!("avx2") {
320 unsafe { ntt_avx2(&mut r) };
321 } else {
322 ntt_scalar(&mut r);
323 }
324 }
325 #[cfg(not(target_arch = "x86_64"))]
326 ntt_scalar(&mut r);
327 r.iter().map(|&x| (x as i32).rem_euclid(Q) as i64).collect()
328}
329
330pub fn mlkem_ntt(input: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
333 let out = mlkem_ntt_raw(input);
334 logicaffeine_data::LogosSeq::from_vec(out)
335}
336
337pub fn mlkem_ntt_w16(input: &[logicaffeine_base::Word16]) -> Vec<logicaffeine_base::Word16> {
341 use logicaffeine_base::Word16;
342 assert_eq!(input.len(), 256);
343 let mut r = [0i16; 256];
344 for (slot, w) in r.iter_mut().zip(input) {
345 *slot = w.0 as i16;
346 }
347 #[cfg(target_arch = "x86_64")]
348 {
349 if std::is_x86_feature_detected!("avx2") {
350 unsafe { ntt_avx2(&mut r) };
351 } else {
352 ntt_scalar(&mut r);
353 }
354 }
355 #[cfg(not(target_arch = "x86_64"))]
356 ntt_scalar(&mut r);
357 r.iter().map(|&x| Word16((x as i32).rem_euclid(Q) as u16)).collect()
358}
359
360pub fn mlkem_inv_ntt_w16(input: &[logicaffeine_base::Word16]) -> Vec<logicaffeine_base::Word16> {
362 use logicaffeine_base::Word16;
363 let mut r = [0i16; 256];
364 for (slot, w) in r.iter_mut().zip(input) {
365 *slot = w.0 as i16;
366 }
367 inv_ntt_inplace(&mut r);
368 r.iter().map(|&x| Word16((x as i32).rem_euclid(Q) as u16)).collect()
369}
370
371#[inline]
373fn inv_ntt_inplace(r: &mut [i16; 256]) {
374 #[cfg(target_arch = "x86_64")]
375 {
376 if std::is_x86_feature_detected!("avx2") {
377 unsafe { invntt_avx2(r) };
378 return;
379 }
380 }
381 invntt_scalar(r);
382}
383pub fn mlkem_base_mul_w16(
384 a: &[logicaffeine_base::Word16],
385 b: &[logicaffeine_base::Word16],
386) -> Vec<logicaffeine_base::Word16> {
387 use logicaffeine_base::Word16;
388 let to16 = |v: &[Word16]| -> [i16; 256] {
389 let mut r = [0i16; 256];
390 for (slot, w) in r.iter_mut().zip(v) {
391 *slot = w.0 as i16;
392 }
393 r
394 };
395 let r = basemul_scalar(&to16(a), &to16(b));
396 r.iter().map(|&x| Word16(cadd_q(barrett_reduce(x)))).collect()
399}
400pub fn mlkem_to_mont_w16(coeffs: &[logicaffeine_base::Word16]) -> Vec<logicaffeine_base::Word16> {
401 use logicaffeine_base::Word16;
402 const F: i32 = 1353;
403 coeffs.iter().map(|w| Word16(cadd_q(montgomery_reduce(w.0 as i16 as i32 * F)))).collect()
405}
406
407pub fn mlkem_ntt_w16_seq(
409 input: &[logicaffeine_base::Word16],
410) -> logicaffeine_data::LogosSeq<logicaffeine_base::Word16> {
411 logicaffeine_data::LogosSeq::from_vec(mlkem_ntt_w16(input))
412}
413
414use logicaffeine_base::{Word16, Word8};
416
417pub fn mlkem_cbd2_w16(buf: &[u8]) -> Vec<Word16> {
419 cbd_eta2(buf).iter().map(|&c| Word16((c as i32).rem_euclid(Q) as u16)).collect()
420}
421pub fn mlkem_cbd3_w16(buf: &[u8]) -> Vec<Word16> {
422 cbd_eta3(buf).iter().map(|&c| Word16((c as i32).rem_euclid(Q) as u16)).collect()
423}
424pub fn mlkem_compress_w16(coeffs: &[Word16], d: usize) -> Vec<Word16> {
425 let mask = (1u64 << d) - 1;
426 coeffs
427 .iter()
428 .map(|w| Word16(((((w.0 as u64) << d) + (Q as u64) / 2) / (Q as u64) & mask) as u16))
429 .collect()
430}
431pub fn mlkem_decompress_w16(coeffs: &[Word16], d: usize) -> Vec<Word16> {
432 let denom = 1u64 << d;
433 coeffs
434 .iter()
435 .map(|w| Word16((((w.0 as u64) * (Q as u64) + denom / 2) >> d) as u16))
436 .collect()
437}
438pub fn mlkem_byte_encode_w16(coeffs: &[Word16], d: usize) -> Vec<u8> {
439 let mask = (1u64 << d) - 1;
440 let mut out = Vec::with_capacity((coeffs.len() * d).div_ceil(8));
441 let (mut acc, mut nbits) = (0u64, 0u32);
442 for w in coeffs {
443 acc |= (w.0 as u64 & mask) << nbits;
444 nbits += d as u32;
445 while nbits >= 8 {
446 out.push((acc & 0xff) as u8);
447 acc >>= 8;
448 nbits -= 8;
449 }
450 }
451 if nbits > 0 {
452 out.push((acc & 0xff) as u8);
453 }
454 out
455}
456pub fn mlkem_byte_decode_w16(bytes: &[u8], d: usize) -> Vec<Word16> {
457 let n = (bytes.len() * 8) / d;
458 let mask = (1u64 << d) - 1;
459 let mut out = Vec::with_capacity(n);
460 let (mut acc, mut nbits, mut bi) = (0u64, 0u32, 0usize);
461 for _ in 0..n {
462 while nbits < d as u32 {
463 acc |= (bytes[bi] as u64) << nbits;
464 nbits += 8;
465 bi += 1;
466 }
467 let val = (acc & mask) as u16;
468 acc >>= d;
469 nbits -= d as u32;
470 out.push(Word16(if d == 12 { (val as i32 % Q) as u16 } else { val }));
471 }
472 out
473}
474#[inline]
477fn reject_sample_block(buf: &[u8; 168], out: &mut Vec<Word16>) {
478 let q = Q as u32;
479 let mut k = 0;
480 while k + 3 <= 168 && out.len() < 256 {
481 let (b0, b1, b2) = (buf[k] as u32, buf[k + 1] as u32, buf[k + 2] as u32);
482 let d1 = b0 + 256 * (b1 % 16);
483 let d2 = (b1 / 16) + 16 * b2;
484 if d1 < q {
485 out.push(Word16(d1 as u16));
486 }
487 if d2 < q && out.len() < 256 {
488 out.push(Word16(d2 as u16));
489 }
490 k += 3;
491 }
492}
493
494#[cfg(target_arch = "x86_64")]
498fn rej_compact_table() -> &'static [[u8; 16]; 256] {
499 use std::sync::OnceLock;
500 static TABLE: OnceLock<[[u8; 16]; 256]> = OnceLock::new();
501 TABLE.get_or_init(|| {
502 let mut t = [[0xFFu8; 16]; 256];
503 for (m, entry) in t.iter_mut().enumerate() {
504 let mut pos = 0usize;
505 for lane in 0..8u8 {
506 if (m >> lane) & 1 == 1 {
507 entry[pos * 2] = lane * 2;
508 entry[pos * 2 + 1] = lane * 2 + 1;
509 pos += 1;
510 }
511 }
512 }
513 t
514 })
515}
516
517#[cfg(target_arch = "x86_64")]
523#[target_feature(enable = "avx2")]
524unsafe fn reject_sample_block_avx2(buf: &[u8; 168], out: &mut Vec<Word16>) {
525 use std::arch::x86_64::*;
526 let table = rej_compact_table();
527 let qv = _mm256_set1_epi16(Q as i16);
528 let maskv = _mm256_set1_epi16(0x0fff);
529 let idx8 = _mm256_set_epi8(
532 15, 14, 14, 13, 12, 11, 11, 10, 9, 8, 8, 7, 6, 5, 5, 4, 11, 10, 10, 9, 8, 7, 7, 6, 5, 4, 4, 3, 2, 1, 1, 0,
534 );
535 let mut padded = [0u8; 192];
537 padded[..168].copy_from_slice(buf);
538
539 let mut off = 0usize;
540 while off + 24 <= 168 && out.len() < 256 {
541 let raw = _mm256_loadu_si256(padded.as_ptr().add(off) as *const __m256i);
542 let perm = _mm256_permute4x64_epi64(raw, 0x94); let mut f = _mm256_shuffle_epi8(perm, idx8);
544 let g = _mm256_srli_epi16(f, 4);
545 f = _mm256_blend_epi16(f, g, 0xAA); f = _mm256_and_si256(f, maskv);
547 let good = _mm256_cmpgt_epi16(qv, f); for half in 0..2 {
550 if out.len() >= 256 {
551 break;
552 }
553 let cand = if half == 0 { _mm256_castsi256_si128(f) } else { _mm256_extracti128_si256(f, 1) };
554 let gd = if half == 0 { _mm256_castsi256_si128(good) } else { _mm256_extracti128_si256(good, 1) };
555 let m = (_mm_movemask_epi8(_mm_packs_epi16(gd, gd)) & 0xff) as usize;
556 let sh = _mm_loadu_si128(table[m].as_ptr() as *const __m128i);
557 let packed = _mm_shuffle_epi8(cand, sh);
558 let mut tmp = [0i16; 8];
559 _mm_storeu_si128(tmp.as_mut_ptr() as *mut __m128i, packed);
560 let n = (m.count_ones() as usize).min(256 - out.len());
561 for &c in tmp.iter().take(n) {
562 out.push(Word16(c as u16));
563 }
564 }
565 off += 24;
566 }
567}
568
569#[inline]
572fn sample_a_xof_block(seed: &[u8], r: u8, c: u8) -> [u8; 168] {
573 let mut blk = [0u8; 168];
574 blk[..32].copy_from_slice(&seed[..32]);
575 blk[32] = r;
576 blk[33] = c;
577 blk[34] = 0x1f;
578 blk[167] |= 0x80;
579 blk
580}
581
582pub fn mlkem_sample_a_w16(seed: &[u8], idx_i: i64, idx_j: i64) -> Vec<Word16> {
584 let mut xof_in = [0u8; 34];
585 xof_in[..32].copy_from_slice(&seed[..32]);
586 xof_in[32] = idx_i.rem_euclid(256) as u8;
587 xof_in[33] = idx_j.rem_euclid(256) as u8;
588 let mut st = crate::keccak::shake128_absorb(&xof_in);
589 let mut out: Vec<Word16> = Vec::with_capacity(256);
590 let mut buf = [0u8; 168];
591 loop {
592 for i in 0..21 {
593 buf[i * 8..i * 8 + 8].copy_from_slice(&st[i].to_le_bytes());
594 }
595 #[cfg(target_arch = "x86_64")]
596 {
597 if std::is_x86_feature_detected!("avx2") {
598 unsafe { reject_sample_block_avx2(&buf, &mut out) };
599 } else {
600 reject_sample_block(&buf, &mut out);
601 }
602 }
603 #[cfg(not(target_arch = "x86_64"))]
604 reject_sample_block(&buf, &mut out);
605 if out.len() >= 256 {
606 out.truncate(256);
607 return out;
608 }
609 crate::keccak::keccak_f1600(&mut st);
610 }
611}
612
613pub fn mlkem_sample_matrix_w16(seed: &[u8]) -> Vec<Word16> {
618 const ENTRIES: [(u8, u8); 9] =
619 [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)];
620 let mut out = vec![Word16(0); 9 * 256];
621 let mut place = |e: usize, poly: &[Word16]| {
622 out[e * 256..e * 256 + 256].copy_from_slice(&poly[..256]);
623 };
624
625 #[cfg(target_arch = "x86_64")]
626 {
627 if std::is_x86_feature_detected!("avx2") {
628 let mut e = 0;
629 while e + 4 <= ENTRIES.len() {
630 let blocks: [[u8; 168]; 4] = std::array::from_fn(|l| {
631 sample_a_xof_block(seed, ENTRIES[e + l].0, ENTRIES[e + l].1)
632 });
633 let mut accs: [Vec<Word16>; 4] =
634 std::array::from_fn(|_| Vec::with_capacity(256));
635 let mut st = unsafe { crate::keccak::shake128_x4_absorb_once(&blocks) };
636 loop {
637 let outb = unsafe { crate::keccak::shake128_x4_squeeze_block(&st) };
638 let mut done = true;
639 for (l, acc) in accs.iter_mut().enumerate() {
640 if acc.len() < 256 {
641 unsafe { reject_sample_block_avx2(&outb[l], acc) };
642 }
643 if acc.len() < 256 {
644 done = false;
645 }
646 }
647 if done {
648 break;
649 }
650 unsafe { crate::keccak::keccak_f1600_x4(&mut st) };
651 }
652 for (l, acc) in accs.iter().enumerate() {
653 place(e + l, acc);
654 }
655 e += 4;
656 }
657 while e < ENTRIES.len() {
658 let v = mlkem_sample_a_w16(seed, ENTRIES[e].0 as i64, ENTRIES[e].1 as i64);
659 place(e, &v);
660 e += 1;
661 }
662 return out;
663 }
664 }
665
666 for (e, &(r, c)) in ENTRIES.iter().enumerate() {
667 let v = mlkem_sample_a_w16(seed, r as i64, c as i64);
668 place(e, &v);
669 }
670 out
671}
672
673#[inline]
674fn barrett_reduce(a: i16) -> i16 {
675 const V: i32 = ((1 << 26) + Q / 2) / Q;
676 let t = (((V * a as i32) + (1 << 25)) >> 26) as i16;
677 a.wrapping_sub(t.wrapping_mul(Q as i16))
678}
679
680fn invntt_scalar(r: &mut [i16; 256]) {
683 const F: i16 = 1441;
684 let mut k = 127usize;
685 let mut len = 2usize;
686 while len <= 128 {
687 let mut start = 0usize;
688 while start < 256 {
689 let zeta = ZETAS[k];
690 k = k.wrapping_sub(1);
691 for j in start..start + len {
692 let t = r[j];
693 r[j] = barrett_reduce(t.wrapping_add(r[j + len]));
694 r[j + len] = r[j + len].wrapping_sub(t);
695 r[j + len] = fqmul(zeta, r[j + len]);
696 }
697 start += 2 * len;
698 }
699 len <<= 1;
700 }
701 for x in r.iter_mut() {
702 *x = fqmul(*x, F);
703 }
704}
705
706fn mlkem_inv_ntt_raw(input: &[i64]) -> Vec<i64> {
707 assert_eq!(input.len(), 256, "mlkemInvNtt expects exactly 256 coefficients");
708 let mut r = [0i16; 256];
710 for (slot, &x) in r.iter_mut().zip(input) {
711 debug_assert!((0..Q as i64).contains(&x), "inv_ntt input out of [0,q)");
712 *slot = x as i16;
713 }
714 inv_ntt_inplace(&mut r);
715 r.iter().map(|&x| (x as i32).rem_euclid(Q) as i64).collect()
716}
717
718pub fn mlkem_inv_ntt(input: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
722 let out = mlkem_inv_ntt_raw(input);
723 logicaffeine_data::LogosSeq::from_vec(out)
724}
725
726#[inline]
729fn bmul(a: &[i16], b: &[i16], zeta: i16) -> (i16, i16) {
730 let r0 = fqmul(fqmul(a[1], b[1]), zeta).wrapping_add(fqmul(a[0], b[0]));
731 let r1 = fqmul(a[0], b[1]).wrapping_add(fqmul(a[1], b[0]));
732 (r0, r1)
733}
734
735fn basemul_scalar(a: &[i16; 256], b: &[i16; 256]) -> [i16; 256] {
738 let mut r = [0i16; 256];
739 for i in 0..64 {
740 let zeta = ZETAS[64 + i];
741 let (c0, c1) = bmul(&a[4 * i..4 * i + 2], &b[4 * i..4 * i + 2], zeta);
742 r[4 * i] = c0;
743 r[4 * i + 1] = c1;
744 let (d0, d1) = bmul(&a[4 * i + 2..4 * i + 4], &b[4 * i + 2..4 * i + 4], zeta.wrapping_neg());
745 r[4 * i + 2] = d0;
746 r[4 * i + 3] = d1;
747 }
748 r
749}
750
751fn mlkem_base_mul_raw(a: &[i64], b: &[i64]) -> Vec<i64> {
752 assert_eq!(a.len(), 256);
753 assert_eq!(b.len(), 256);
754 let to16 = |v: &[i64]| -> [i16; 256] {
757 let mut r = [0i16; 256];
758 for (slot, &x) in r.iter_mut().zip(v) {
759 debug_assert!((0..Q as i64).contains(&x), "base_mul input out of [0,q)");
760 *slot = x as i16;
761 }
762 r
763 };
764 let r = basemul_scalar(&to16(a), &to16(b));
765 r.iter().map(|&x| (x as i32).rem_euclid(Q) as i64).collect()
766}
767
768pub fn mlkem_base_mul(
772 a: &[i64],
773 b: &[i64],
774) -> logicaffeine_data::LogosSeq<i64> {
775 let out = mlkem_base_mul_raw(a, b);
776 logicaffeine_data::LogosSeq::from_vec(out)
777}
778
779fn mlkem_to_mont_raw(coeffs: &[i64]) -> Vec<i64> {
783 const F: i32 = 1353; coeffs
785 .iter()
786 .map(|&x| {
787 debug_assert!((0..Q as i64).contains(&x), "to_mont input out of [0,q)");
788 montgomery_reduce(x as i32 * F) as i32 % Q
789 })
790 .map(|x| x.rem_euclid(Q) as i64)
791 .collect()
792}
793
794pub fn mlkem_to_mont(coeffs: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
796 logicaffeine_data::LogosSeq::from_vec(mlkem_to_mont_raw(coeffs))
797}
798
799fn cbd_eta2(buf: &[u8]) -> [i16; 256] {
804 assert_eq!(buf.len(), 128, "CBD_2 needs 64·η = 128 bytes");
805 let mut r = [0i16; 256];
806 for i in 0..32 {
807 let t = u32::from_le_bytes(buf[4 * i..4 * i + 4].try_into().unwrap());
808 let mut d = t & 0x5555_5555;
809 d += (t >> 1) & 0x5555_5555;
810 for j in 0..8 {
811 let a = ((d >> (4 * j)) & 0x3) as i16;
812 let b = ((d >> (4 * j + 2)) & 0x3) as i16;
813 r[8 * i + j] = a - b;
814 }
815 }
816 r
817}
818
819fn cbd_eta3(buf: &[u8]) -> [i16; 256] {
821 assert_eq!(buf.len(), 192, "CBD_3 needs 64·η = 192 bytes");
822 let mut r = [0i16; 256];
823 for i in 0..64 {
824 let t =
825 buf[3 * i] as u32 | (buf[3 * i + 1] as u32) << 8 | (buf[3 * i + 2] as u32) << 16;
826 let mut d = t & 0x0024_9249;
827 d += (t >> 1) & 0x0024_9249;
828 d += (t >> 2) & 0x0024_9249;
829 for j in 0..4 {
830 let a = ((d >> (6 * j)) & 0x7) as i16;
831 let b = ((d >> (6 * j + 3)) & 0x7) as i16;
832 r[4 * i + j] = a - b;
833 }
834 }
835 r
836}
837
838fn mlkem_cbd_raw(buf: &[i64], eta: usize) -> Vec<i64> {
839 let bytes: Vec<u8> = buf.iter().map(|&x| x.rem_euclid(256) as u8).collect();
840 let r = match eta {
841 2 => cbd_eta2(&bytes),
842 3 => cbd_eta3(&bytes),
843 _ => panic!("ML-KEM CBD supports η ∈ {{2, 3}}"),
844 };
845 r.iter().map(|&x| x as i64).collect()
846}
847
848pub fn mlkem_cbd2(buf: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
851 logicaffeine_data::LogosSeq::from_vec(mlkem_cbd_raw(buf, 2))
852}
853pub fn mlkem_cbd3(buf: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
855 logicaffeine_data::LogosSeq::from_vec(mlkem_cbd_raw(buf, 3))
856}
857
858fn mlkem_compress_raw(coeffs: &[i64], d: usize) -> Vec<i64> {
863 let mask = (1u64 << d) - 1;
864 coeffs
865 .iter()
866 .map(|&x| {
867 let x = x.rem_euclid(Q as i64) as u64;
868 (((x << d) + (Q as u64) / 2) / (Q as u64) & mask) as i64
869 })
870 .collect()
871}
872
873fn mlkem_decompress_raw(coeffs: &[i64], d: usize) -> Vec<i64> {
875 let denom = 1u64 << d;
876 coeffs
877 .iter()
878 .map(|&y| (((y as u64) * (Q as u64) + denom / 2) >> d) as i64)
879 .collect()
880}
881
882fn mlkem_byte_encode_raw(coeffs: &[i64], d: usize) -> Vec<i64> {
886 let mask = (1u64 << d) - 1;
887 let mut out = Vec::with_capacity((coeffs.len() * d).div_ceil(8));
888 let mut acc: u64 = 0;
889 let mut nbits = 0u32;
890 for &c in coeffs {
891 acc |= (c.rem_euclid(1i64 << d) as u64 & mask) << nbits;
892 nbits += d as u32;
893 while nbits >= 8 {
894 out.push((acc & 0xff) as i64);
895 acc >>= 8;
896 nbits -= 8;
897 }
898 }
899 if nbits > 0 {
900 out.push((acc & 0xff) as i64);
901 }
902 out
903}
904
905fn mlkem_byte_decode_raw(bytes: &[i64], d: usize) -> Vec<i64> {
909 let n = (bytes.len() * 8) / d;
910 let mask = (1u64 << d) - 1;
911 let mut out = Vec::with_capacity(n);
912 let mut acc: u64 = 0;
913 let mut nbits = 0u32;
914 let mut bi = 0usize;
915 for _ in 0..n {
916 while nbits < d as u32 {
917 acc |= (bytes[bi].rem_euclid(256) as u64) << nbits;
918 nbits += 8;
919 bi += 1;
920 }
921 let val = (acc & mask) as i64;
922 acc >>= d;
923 nbits -= d as u32;
924 out.push(if d == 12 { val.rem_euclid(Q as i64) } else { val });
925 }
926 out
927}
928
929pub fn mlkem_compress(
931 coeffs: &[i64],
932 d: i64,
933) -> logicaffeine_data::LogosSeq<i64> {
934 logicaffeine_data::LogosSeq::from_vec(mlkem_compress_raw(coeffs, d as usize))
935}
936pub fn mlkem_decompress(
938 coeffs: &[i64],
939 d: i64,
940) -> logicaffeine_data::LogosSeq<i64> {
941 logicaffeine_data::LogosSeq::from_vec(mlkem_decompress_raw(coeffs, d as usize))
942}
943pub fn mlkem_byte_encode(
945 coeffs: &[i64],
946 d: i64,
947) -> logicaffeine_data::LogosSeq<i64> {
948 logicaffeine_data::LogosSeq::from_vec(mlkem_byte_encode_raw(coeffs, d as usize))
949}
950pub fn mlkem_byte_decode(
952 bytes: &[i64],
953 d: i64,
954) -> logicaffeine_data::LogosSeq<i64> {
955 logicaffeine_data::LogosSeq::from_vec(mlkem_byte_decode_raw(bytes, d as usize))
956}
957
958fn try_sample_ntt(stream: &[u8], want: usize) -> Option<Vec<i64>> {
964 let q = Q as u32;
965 let mut a = Vec::with_capacity(want);
966 let mut i = 0usize;
967 while a.len() < want {
968 if i + 3 > stream.len() {
969 return None;
970 }
971 let (b0, b1, b2) = (stream[i] as u32, stream[i + 1] as u32, stream[i + 2] as u32);
972 let d1 = b0 + 256 * (b1 % 16);
973 let d2 = (b1 / 16) + 16 * b2;
974 if d1 < q {
975 a.push(d1 as i64);
976 }
977 if d2 < q && a.len() < want {
978 a.push(d2 as i64);
979 }
980 i += 3;
981 }
982 Some(a)
983}
984
985fn mlkem_sample_ntt_raw(stream: &[i64]) -> Vec<i64> {
986 let bytes: Vec<u8> = stream.iter().map(|&x| x.rem_euclid(256) as u8).collect();
987 try_sample_ntt(&bytes, 256).expect("SampleNTT: XOF stream exhausted before 256 coefficients")
988}
989
990fn mlkem_sample_a_raw(seed: &[u8], idx_i: i64, idx_j: i64) -> Vec<i64> {
994 let mut xof_in = [0u8; 34];
999 xof_in[..32].copy_from_slice(&seed[..32]);
1000 xof_in[32] = idx_i.rem_euclid(256) as u8;
1001 xof_in[33] = idx_j.rem_euclid(256) as u8;
1002 let mut st = crate::keccak::shake128_absorb(&xof_in);
1003
1004 let q = Q as u32;
1005 let mut out = Vec::with_capacity(256);
1006 let mut buf = [0u8; 168];
1007 loop {
1008 for i in 0..21 {
1009 buf[i * 8..i * 8 + 8].copy_from_slice(&st[i].to_le_bytes());
1010 }
1011 let mut k = 0;
1012 while k + 3 <= 168 && out.len() < 256 {
1013 let (b0, b1, b2) = (buf[k] as u32, buf[k + 1] as u32, buf[k + 2] as u32);
1014 let d1 = b0 + 256 * (b1 % 16);
1015 let d2 = (b1 / 16) + 16 * b2;
1016 if d1 < q {
1017 out.push(d1 as i64);
1018 }
1019 if d2 < q && out.len() < 256 {
1020 out.push(d2 as i64);
1021 }
1022 k += 3;
1023 }
1024 if out.len() >= 256 {
1025 return out;
1026 }
1027 crate::keccak::keccak_f1600(&mut st);
1028 }
1029}
1030
1031pub fn mlkem_sample_ntt(stream: &[i64]) -> logicaffeine_data::LogosSeq<i64> {
1034 logicaffeine_data::LogosSeq::from_vec(mlkem_sample_ntt_raw(stream))
1035}
1036
1037pub fn mlkem_sample_a(
1040 seed: &[i64],
1041 idx_i: i64,
1042 idx_j: i64,
1043) -> logicaffeine_data::LogosSeq<i64> {
1044 let seed_bytes: Vec<u8> = seed.iter().map(|&x| x.rem_euclid(256) as u8).collect();
1045 logicaffeine_data::LogosSeq::from_vec(mlkem_sample_a_raw(&seed_bytes, idx_i, idx_j))
1046}
1047
1048type Seq16 = logicaffeine_data::LogosSeq<Word16>;
1053type Seq8 = logicaffeine_data::LogosSeq<Word8>;
1054#[inline]
1055fn w8_to_u8(s: &[Word8]) -> Vec<u8> {
1056 s.iter().map(|w| w.0).collect()
1057}
1058#[inline]
1059fn u8_to_seq8(v: Vec<u8>) -> Seq8 {
1060 logicaffeine_data::LogosSeq::from_vec(v.into_iter().map(Word8).collect())
1061}
1062
1063pub fn mlkem_inv_ntt_w16_seq(input: &[Word16]) -> Seq16 {
1064 logicaffeine_data::LogosSeq::from_vec(mlkem_inv_ntt_w16(input))
1065}
1066pub fn mlkem_base_mul_w16_seq(a: &[Word16], b: &[Word16]) -> Seq16 {
1067 logicaffeine_data::LogosSeq::from_vec(mlkem_base_mul_w16(a, b))
1068}
1069pub fn mlkem_to_mont_w16_seq(c: &[Word16]) -> Seq16 {
1070 logicaffeine_data::LogosSeq::from_vec(mlkem_to_mont_w16(c))
1071}
1072pub fn mlkem_compress_w16_seq(c: &[Word16], d: i64) -> Seq16 {
1073 logicaffeine_data::LogosSeq::from_vec(mlkem_compress_w16(c, d as usize))
1074}
1075pub fn mlkem_decompress_w16_seq(c: &[Word16], d: i64) -> Seq16 {
1076 logicaffeine_data::LogosSeq::from_vec(mlkem_decompress_w16(c, d as usize))
1077}
1078pub fn mlkem_byte_encode_w16_seq(c: &[Word16], d: i64) -> Seq8 {
1079 u8_to_seq8(mlkem_byte_encode_w16(c, d as usize))
1080}
1081pub fn mlkem_byte_decode_w16_seq(b: &[Word8], d: i64) -> Seq16 {
1082 logicaffeine_data::LogosSeq::from_vec(mlkem_byte_decode_w16(&w8_to_u8(b), d as usize))
1083}
1084pub fn mlkem_cbd2_w16_seq(buf: &[Word8]) -> Seq16 {
1085 logicaffeine_data::LogosSeq::from_vec(mlkem_cbd2_w16(&w8_to_u8(buf)))
1086}
1087pub fn mlkem_cbd3_w16_seq(buf: &[Word8]) -> Seq16 {
1088 logicaffeine_data::LogosSeq::from_vec(mlkem_cbd3_w16(&w8_to_u8(buf)))
1089}
1090pub fn mlkem_sample_a_w16_seq(seed: &[Word8], i: i64, j: i64) -> Seq16 {
1091 logicaffeine_data::LogosSeq::from_vec(mlkem_sample_a_w16(&w8_to_u8(seed), i, j))
1092}
1093pub fn sha3_256_w8_seq(input: &[Word8]) -> Seq8 {
1094 u8_to_seq8(crate::keccak::sha3_256_bytes(&w8_to_u8(input)).to_vec())
1095}
1096pub fn sha3_512_w8_seq(input: &[Word8]) -> Seq8 {
1097 u8_to_seq8(crate::keccak::sha3_512_bytes(&w8_to_u8(input)).to_vec())
1098}
1099pub fn shake128_w8_seq(input: &[Word8], outlen: i64) -> Seq8 {
1100 u8_to_seq8(crate::keccak::shake128_bytes(&w8_to_u8(input), outlen.max(0) as usize))
1101}
1102pub fn shake256_w8_seq(input: &[Word8], outlen: i64) -> Seq8 {
1103 u8_to_seq8(crate::keccak::shake256_bytes(&w8_to_u8(input), outlen.max(0) as usize))
1104}
1105
1106pub fn mlkem_add_mod_q_w16(a: &[Word16], b: &[Word16]) -> Seq16 {
1109 let q = Q as u16;
1112 logicaffeine_data::LogosSeq::from_vec(
1113 a.iter()
1114 .zip(b)
1115 .map(|(x, y)| {
1116 let s = x.0 + y.0;
1117 Word16(if s >= q { s - q } else { s })
1118 })
1119 .collect(),
1120 )
1121}
1122pub fn mlkem_sub_mod_q_w16(a: &[Word16], b: &[Word16]) -> Seq16 {
1124 let q = Q as i32;
1126 logicaffeine_data::LogosSeq::from_vec(
1127 a.iter()
1128 .zip(b)
1129 .map(|(x, y)| {
1130 let d = x.0 as i32 - y.0 as i32;
1131 Word16((if d < 0 { d + q } else { d }) as u16)
1132 })
1133 .collect(),
1134 )
1135}
1136pub fn mlkem_zeros_w16(n: i64) -> Seq16 {
1138 logicaffeine_data::LogosSeq::from_vec(vec![Word16(0); n.max(0) as usize])
1139}
1140
1141pub fn mlkem_cbd2_w16_from_int(buf: &[i64]) -> Seq16 {
1144 let bytes: Vec<u8> = buf.iter().map(|&x| x.rem_euclid(256) as u8).collect();
1145 logicaffeine_data::LogosSeq::from_vec(mlkem_cbd2_w16(&bytes))
1146}
1147pub fn mlkem_byte_encode_w16_to_int(c: &[Word16], d: i64) -> logicaffeine_data::LogosSeq<i64> {
1148 logicaffeine_data::LogosSeq::from_vec(
1149 mlkem_byte_encode_w16(c, d as usize).into_iter().map(|b| b as i64).collect(),
1150 )
1151}
1152pub fn mlkem_byte_decode_w16_from_int(b: &[i64], d: i64) -> Seq16 {
1153 let bytes: Vec<u8> = b.iter().map(|&x| x.rem_euclid(256) as u8).collect();
1154 logicaffeine_data::LogosSeq::from_vec(mlkem_byte_decode_w16(&bytes, d as usize))
1155}
1156pub fn mlkem_sample_a_w16_from_int(seed: &[i64], i: i64, j: i64) -> Seq16 {
1157 let bytes: Vec<u8> = seed.iter().map(|&x| x.rem_euclid(256) as u8).collect();
1158 logicaffeine_data::LogosSeq::from_vec(mlkem_sample_a_w16(&bytes, i, j))
1159}
1160pub fn mlkem_sample_matrix_w16_from_int(seed: &[i64]) -> Seq16 {
1163 let bytes: Vec<u8> = seed.iter().map(|&x| x.rem_euclid(256) as u8).collect();
1164 logicaffeine_data::LogosSeq::from_vec(mlkem_sample_matrix_w16(&bytes))
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 #[test]
1172 #[cfg(target_arch = "x86_64")]
1173 fn reject_sample_avx2_bit_exact_vs_scalar() {
1174 if !std::is_x86_feature_detected!("avx2") {
1175 return;
1176 }
1177 let mut s = 0x2545_F491_4F6C_DD1D_u64;
1180 let mut next = || {
1181 s ^= s << 13;
1182 s ^= s >> 7;
1183 s ^= s << 17;
1184 s
1185 };
1186 for iter in 0..2000 {
1187 let mut buf = [0u8; 168];
1188 match iter % 4 {
1189 0 => buf.iter_mut().for_each(|b| *b = (next() & 0xff) as u8), 1 => buf.fill(0), 2 => buf.fill(0xff), _ => buf.iter_mut().enumerate().for_each(|(i, b)| *b = if i % 3 == 1 { 0x0d } else { (next() & 0xff) as u8 }),
1193 }
1194 let mut want: Vec<Word16> = Vec::with_capacity(256);
1195 reject_sample_block(&buf, &mut want);
1196 let mut got: Vec<Word16> = Vec::with_capacity(256);
1197 unsafe { reject_sample_block_avx2(&buf, &mut got) };
1198 assert_eq!(got, want, "AVX2 rejection sampler must be bit-identical to scalar (iter {iter})");
1199
1200 for pre in [0usize, 100, 250, 255] {
1202 let mut w = vec![Word16(1); pre];
1203 let mut g = vec![Word16(1); pre];
1204 reject_sample_block(&buf, &mut w);
1205 unsafe { reject_sample_block_avx2(&buf, &mut g) };
1206 assert_eq!(g, w, "AVX2 sampler must match scalar with {pre} pre-filled (iter {iter})");
1207 }
1208 }
1209 }
1210
1211 #[test]
1212 fn word16_native_layer_is_correct() {
1213 let q = Q as u32;
1214 let a: Vec<Word16> = (0..256).map(|i| Word16((i * 13 % 3329) as u16)).collect();
1215 let b: Vec<Word16> = (0..256).map(|i| Word16((i * 29 % 3329) as u16)).collect();
1216
1217 let add = mlkem_add_mod_q_w16(&a, &b);
1219 let sub = mlkem_sub_mod_q_w16(&a, &b);
1220 for i in 0..256 {
1221 assert_eq!(add.borrow()[i].0 as u32, (a[i].0 as u32 + b[i].0 as u32) % q);
1222 assert_eq!(sub.borrow()[i].0 as i32, (a[i].0 as i32 - b[i].0 as i32).rem_euclid(Q));
1223 }
1224 assert_eq!(mlkem_zeros_w16(256).borrow().len(), 256);
1225 assert!(mlkem_zeros_w16(256).borrow().iter().all(|w| w.0 == 0));
1226
1227 let enc = mlkem_byte_encode_w16_seq(&a, 12);
1229 let dec = mlkem_byte_decode_w16_seq(&enc.borrow(), 12);
1230 assert_eq!(dec.borrow().as_slice(), a.as_slice(), "byteDecode∘byteEncode = id (Word16)");
1231
1232 let buf: Vec<Word8> = (0..128).map(|i| Word8((i * 7) as u8)).collect();
1234 let cbd_i64: Vec<i64> = mlkem_cbd2(&buf.iter().map(|w| w.0 as i64).collect::<Vec<_>>()).to_vec();
1235 let cbd_w16: Vec<i64> =
1236 mlkem_cbd2_w16_seq(&buf).borrow().iter().map(|w| (w.0 as i64).rem_euclid(Q as i64)).collect();
1237 let cbd_i64_red: Vec<i64> = cbd_i64.iter().map(|&c| c.rem_euclid(Q as i64)).collect();
1238 assert_eq!(cbd_w16, cbd_i64_red, "cbd2 Word16 == reduced i64 cbd2");
1239
1240 let seed: Vec<Word8> = (0..32).map(|i| Word8(i as u8)).collect();
1241 let sa_i64: Vec<i64> = mlkem_sample_a(&seed.iter().map(|w| w.0 as i64).collect::<Vec<_>>(), 1, 2).to_vec();
1242 let sa_w16: Vec<i64> = mlkem_sample_a_w16_seq(&seed, 1, 2).borrow().iter().map(|w| w.0 as i64).collect();
1243 assert_eq!(sa_w16, sa_i64, "sampleA Word16 == i64 sampleA");
1244 }
1245
1246 #[test]
1247 fn scalar_round_trip_and_avx2_matches_scalar() {
1248 let mut s = 0x1234_5678u64;
1249 let f: [i16; 256] = std::array::from_fn(|_| {
1250 s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1251 ((s >> 16) % Q as u64) as i16
1252 });
1253 const MONT: i32 = 2285;
1255 let mut r = f;
1256 ntt_scalar(&mut r);
1257 invntt_scalar(&mut r);
1258 let norm = |x: i16| ((x as i32) % Q + Q) % Q;
1259 assert!(
1260 (0..256).all(|i| norm(r[i]) == (f[i] as i32 * MONT).rem_euclid(Q)),
1261 "scalar Kyber NTT round-trip must hold"
1262 );
1263
1264 #[cfg(target_arch = "x86_64")]
1265 if std::is_x86_feature_detected!("avx2") {
1266 let mut a = f;
1267 let mut b = f;
1268 ntt_scalar(&mut a);
1269 unsafe { ntt_avx2(&mut b) };
1270 assert_eq!(a, b, "AVX2 NTT must be bit-identical to scalar");
1271 }
1272 }
1273
1274 #[test]
1275 #[cfg(target_arch = "x86_64")]
1276 fn invntt_avx2_matches_scalar() {
1277 if !std::is_x86_feature_detected!("avx2") {
1278 return;
1279 }
1280 let mut s = 0xC0FFEE_1234_5678u64;
1283 for _ in 0..2000 {
1284 let r0: [i16; 256] = std::array::from_fn(|_| {
1285 s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1286 ((s >> 33) % Q as u64) as i16
1287 });
1288 let mut a = r0;
1289 let mut b = r0;
1290 invntt_scalar(&mut a);
1291 unsafe { invntt_avx2(&mut b) };
1292 assert_eq!(a, b, "AVX2 inverse NTT must be bit-identical to scalar");
1293 }
1294 }
1295
1296 fn negacyclic_conv(a: &[i64], b: &[i64]) -> Vec<i64> {
1297 let q = Q as i64;
1298 let n = 256;
1299 let mut c = vec![0i64; n];
1300 for i in 0..n {
1301 for j in 0..n {
1302 let prod = (a[i] * b[j]).rem_euclid(q);
1303 let k = i + j;
1304 if k < n {
1305 c[k] = (c[k] + prod).rem_euclid(q);
1306 } else {
1307 c[k - n] = (c[k - n] - prod).rem_euclid(q);
1308 }
1309 }
1310 }
1311 c
1312 }
1313
1314 fn inv_mod_q(x: i64) -> i64 {
1315 let q = Q as i64;
1316 let (mut r, mut base, mut e) = (1i64, x.rem_euclid(q), q - 2);
1317 while e > 0 {
1318 if e & 1 == 1 {
1319 r = r * base % q;
1320 }
1321 base = base * base % q;
1322 e >>= 1;
1323 }
1324 r
1325 }
1326
1327 fn cbd_ref(buf: &[u8], eta: usize) -> [i16; 256] {
1329 let bit = |idx: usize| -> i16 { ((buf[idx / 8] >> (idx % 8)) & 1) as i16 };
1330 let mut r = [0i16; 256];
1331 for k in 0..256 {
1332 let base = 2 * eta * k;
1333 let (mut a, mut b) = (0i16, 0i16);
1334 for i in 0..eta {
1335 a += bit(base + i);
1336 b += bit(base + eta + i);
1337 }
1338 r[k] = a - b;
1339 }
1340 r
1341 }
1342
1343 #[test]
1344 fn sample_matrix_x4_matches_nine_scalar_entries() {
1345 for seed_byte in [0u8, 0x11, 0x5a, 0xff] {
1348 let seed: Vec<u8> = (0..32).map(|i| seed_byte ^ (i as u8 * 7)).collect();
1349 let matrix = mlkem_sample_matrix_w16(&seed);
1350 assert_eq!(matrix.len(), 9 * 256);
1351 for r in 0..3u8 {
1352 for c in 0..3u8 {
1353 let want = mlkem_sample_a_w16(&seed, r as i64, c as i64);
1354 let e = (r as usize * 3 + c as usize) * 256;
1355 assert_eq!(
1356 &matrix[e..e + 256],
1357 &want[..],
1358 "matrix entry ({r},{c}) must match the scalar sampleA"
1359 );
1360 }
1361 }
1362 }
1363 }
1364
1365 #[test]
1366 fn cbd_bit_trick_matches_bit_by_bit_reference() {
1367 let mut s = 0xDEAD_BEEFu64;
1368 let mut rb = || {
1369 s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1370 (s >> 56) as u8
1371 };
1372 let buf2: Vec<u8> = (0..128).map(|_| rb()).collect();
1373 assert_eq!(cbd_eta2(&buf2), cbd_ref(&buf2, 2), "fast CBD_2 must equal the bit-by-bit definition");
1374 assert!(cbd_eta2(&buf2).iter().all(|&c| (-2..=2).contains(&c)), "CBD_2 ∈ [−2, 2]");
1375
1376 let buf3: Vec<u8> = (0..192).map(|_| rb()).collect();
1377 assert_eq!(cbd_eta3(&buf3), cbd_ref(&buf3, 3), "fast CBD_3 must equal the bit-by-bit definition");
1378 assert!(cbd_eta3(&buf3).iter().all(|&c| (-3..=3).contains(&c)), "CBD_3 ∈ [−3, 3]");
1379 }
1380
1381 fn sample_ntt_reference(stream: &[u8]) -> Vec<i64> {
1384 let q = Q as u32;
1385 let mut a = Vec::new();
1386 let mut i = 0;
1387 while a.len() < 256 {
1388 let (b0, b1, b2) = (stream[i] as u32, stream[i + 1] as u32, stream[i + 2] as u32);
1389 let d1 = b0 | ((b1 & 0x0F) << 8);
1390 let d2 = (b1 >> 4) | (b2 << 4);
1391 if d1 < q && a.len() < 256 {
1392 a.push(d1 as i64);
1393 }
1394 if d2 < q && a.len() < 256 {
1395 a.push(d2 as i64);
1396 }
1397 i += 3;
1398 }
1399 a
1400 }
1401
1402 #[test]
1403 fn sample_ntt_matches_reference_and_stays_in_field() {
1404 let mut s = 0xCAFE_F00Du64;
1405 let mut rb = || {
1406 s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1407 (s >> 56) as u8
1408 };
1409 let stream: Vec<u8> = (0..2048).map(|_| rb()).collect();
1410 let stream_i: Vec<i64> = stream.iter().map(|&b| b as i64).collect();
1411
1412 let got = mlkem_sample_ntt_raw(&stream_i);
1413 assert_eq!(got.len(), 256, "SampleNTT yields exactly 256 coefficients");
1414 assert!(got.iter().all(|&c| (0..Q as i64).contains(&c)), "every coefficient is in [0, q)");
1415 assert_eq!(got, sample_ntt_reference(&stream), "kernel SampleNTT must match the reference");
1416
1417 let hand = [0x01i64, 0x20, 0x03].iter().chain(std::iter::repeat(&0)).take(2048).copied().collect::<Vec<_>>();
1419 let hg = mlkem_sample_ntt_raw(&hand);
1420 assert_eq!(hg[0], 1, "first sampled coefficient");
1421 assert_eq!(hg[1], 50, "second sampled coefficient");
1422 }
1423
1424 #[test]
1425 fn sample_a_is_deterministic_and_consumes_its_own_xof() {
1426 let seed: Vec<u8> = (0..32u8).collect();
1427 let a1 = mlkem_sample_a_raw(&seed, 1, 2);
1428 let a2 = mlkem_sample_a_raw(&seed, 1, 2);
1429 assert_eq!(a1, a2, "SampleA is a deterministic function of (ρ, i, j)");
1430 assert_ne!(a1, mlkem_sample_a_raw(&seed, 2, 1), "index order matters (Â is not symmetric)");
1431 assert_eq!(a1.len(), 256);
1432 assert!(a1.iter().all(|&c| (0..Q as i64).contains(&c)));
1433
1434 let mut xof_in = seed.clone();
1436 xof_in.push(1);
1437 xof_in.push(2);
1438 let stream = crate::keccak::shake128_bytes(&xof_in, 168 * 6);
1439 assert_eq!(a1, try_sample_ntt(&stream, 256).unwrap(), "SampleA = SampleNTT∘XOF");
1440 }
1441
1442 #[test]
1443 fn byte_encode_decode_round_trips_losslessly() {
1444 let mut s = 0x1234_5678u64;
1447 let mut rc = |bound: i64| -> i64 {
1448 s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1449 (s >> 40) as i64 % bound
1450 };
1451 for &d in &[1usize, 4, 5, 10, 11, 12] {
1452 let modulus = if d == 12 { Q as i64 } else { 1i64 << d };
1453 let coeffs: Vec<i64> = (0..256).map(|_| rc(modulus)).collect();
1454 let bytes = mlkem_byte_encode_raw(&coeffs, d);
1455 assert_eq!(bytes.len(), 32 * d, "ByteEncode_{d} length must be 32·d");
1456 assert!(bytes.iter().all(|&b| (0..256).contains(&b)), "encoded values are bytes");
1457 let back = mlkem_byte_decode_raw(&bytes, d);
1458 assert_eq!(back, coeffs, "ByteDecode_{d}∘ByteEncode_{d} must be the identity");
1459 }
1460 }
1461
1462 #[test]
1463 fn compress_decompress_meets_fips203_error_bound() {
1464 let q = Q as i64;
1466 let mod_pm_abs = |a: i64| -> i64 {
1467 let mut r = a.rem_euclid(q);
1468 if r > q / 2 {
1469 r -= q;
1470 }
1471 r.abs()
1472 };
1473 for &d in &[1usize, 4, 5, 10, 11] {
1474 let bound = (q + (1i64 << d)) / (1i64 << (d + 1)); for x in 0..q {
1476 let c = mlkem_compress_raw(&[x], d)[0];
1477 assert!((0..(1i64 << d)).contains(&c), "Compress_{d}({x}) must be a d-bit value");
1478 let back = mlkem_decompress_raw(&[c], d)[0];
1479 assert!(
1480 mod_pm_abs(back - x) <= bound,
1481 "d={d} x={x}: error {} exceeds bound {bound}",
1482 mod_pm_abs(back - x)
1483 );
1484 }
1485 }
1486 }
1487
1488 #[test]
1489 fn poly_multiply_via_ntt_matches_schoolbook_convolution() {
1490 let mut s = 0xCAFE_1234u64;
1491 let mut rand = || {
1492 s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1493 ((s >> 16) % Q as u64) as i64
1494 };
1495 let a: Vec<i64> = (0..256).map(|_| rand()).collect();
1496 let b: Vec<i64> = (0..256).map(|_| rand()).collect();
1497 let got = mlkem_inv_ntt_raw(&mlkem_base_mul_raw(&mlkem_ntt_raw(&a), &mlkem_ntt_raw(&b)));
1499 let conv = negacyclic_conv(&a, &b);
1500 let k0 = (0..256).find(|&k| conv[k] != 0).expect("nonzero convolution");
1502 let factor = (got[k0] * inv_mod_q(conv[k0])).rem_euclid(Q as i64);
1503 assert!(
1504 (0..256).all(|k| got[k] == (conv[k] * factor).rem_euclid(Q as i64)),
1505 "invntt(basemul(ntt(a),ntt(b))) must equal the negacyclic convolution × a uniform factor (got factor {factor})"
1506 );
1507 }
1508
1509 #[test]
1510 fn mlkem_ntt_reduces_and_matches_scalar() {
1511 let input: Vec<i64> = (0..256).map(|i| (i * 37 % 5000) as i64 - 1000).collect();
1512 let got = mlkem_ntt_raw(&input);
1513 assert_eq!(got.len(), 256);
1514 assert!(got.iter().all(|&x| (0..Q as i64).contains(&x)), "output reduced into [0, q)");
1515 let mut r = [0i16; 256];
1517 for (slot, &x) in r.iter_mut().zip(&input) {
1518 *slot = x.rem_euclid(Q as i64) as i16;
1519 }
1520 ntt_scalar(&mut r);
1521 let want: Vec<i64> = r.iter().map(|&x| (x as i32).rem_euclid(Q) as i64).collect();
1522 assert_eq!(got, want, "mlkem_ntt must equal the scalar reference (mod q)");
1523 }
1524
1525 #[test]
1536 #[cfg(target_arch = "x86_64")]
1537 fn lanes16_montgomery_butterfly_eq_scalar_and_ab() {
1538 use logicaffeine_base::{Lanes16Word16, Word16};
1539 if !std::is_x86_feature_detected!("avx2") {
1540 return; }
1542 let coeffs: [i16; 16] = std::array::from_fn(|i| ((i as i32 * 211 - 1500) % Q) as i16);
1543 let tw: [i16; 16] = std::array::from_fn(|i| ZETAS[i]);
1544
1545 let qinv_v = Lanes16Word16::splat((QINV as i16) as u16);
1546 let q_v = Lanes16Word16::splat(Q as u16);
1547 let lane_fqmul = |a: Lanes16Word16, b: Lanes16Word16| -> Lanes16Word16 {
1548 let lo = a.mullo(b);
1549 let hi = a.mulhi(b);
1550 let m = lo.mullo(qinv_v);
1551 let th = m.mulhi(q_v);
1552 hi.sub(th)
1553 };
1554 let a = Lanes16Word16::from_words(&coeffs.map(|c| Word16(c as u16)));
1555 let b = Lanes16Word16::from_words(&tw.map(|c| Word16(c as u16)));
1556 let res = lane_fqmul(a, b);
1557
1558 for i in 0..16 {
1559 let want = fqmul(coeffs[i], tw[i]);
1560 assert_eq!(res.lane(i).0 as i16, want, "lane fqmul lane {i} must equal scalar fqmul");
1561 }
1562
1563 const K: usize = 200_000;
1564 let mut v = a;
1565 let t0 = std::time::Instant::now();
1566 for _ in 0..K {
1567 v = lane_fqmul(v, b);
1568 }
1569 let lane_ns = t0.elapsed().as_nanos() as f64 / K as f64;
1570 let lane_sink = v.lane(0).0;
1571
1572 let t1 = std::time::Instant::now();
1573 let raw_sink = unsafe { raw_fqmul_chain(coeffs, tw, K) };
1574 let raw_ns = t1.elapsed().as_nanos() as f64 / K as f64;
1575
1576 assert_eq!(lane_sink, raw_sink, "lane and raw fqmul chains must be bit-identical after {K} iters");
1577
1578 println!("\n=== Lanes16Word16 Montgomery butterfly (fqmul ×16) — 200k dependent chain, +avx2 ===");
1579 println!(" raw __m256i intrinsics : {raw_ns:>7.3} ns/fqmul");
1580 println!(
1581 " Lanes16 lane API : {lane_ns:>7.3} ns/fqmul ({:.2}× of raw)",
1582 lane_ns / raw_ns.max(0.001)
1583 );
1584 println!(" (sink {lane_sink}; shared box — timing informational, correctness asserted)");
1585 }
1586
1587 #[cfg(target_arch = "x86_64")]
1588 #[target_feature(enable = "avx2")]
1589 unsafe fn raw_fqmul_chain(coeffs: [i16; 16], tw: [i16; 16], k: usize) -> u16 {
1590 use std::arch::x86_64::*;
1591 let mut v = _mm256_loadu_si256(coeffs.as_ptr() as *const __m256i);
1592 let b = _mm256_loadu_si256(tw.as_ptr() as *const __m256i);
1593 let qinv = _mm256_set1_epi16(QINV as i16);
1594 let q = _mm256_set1_epi16(Q as i16);
1595 for _ in 0..k {
1596 let lo = _mm256_mullo_epi16(v, b);
1597 let hi = _mm256_mulhi_epi16(v, b);
1598 let m = _mm256_mullo_epi16(lo, qinv);
1599 let th = _mm256_mulhi_epi16(m, q);
1600 v = _mm256_sub_epi16(hi, th);
1601 }
1602 let mut out = [0i16; 16];
1603 _mm256_storeu_si256(out.as_mut_ptr() as *mut __m256i, v);
1604 out[0] as u16
1605 }
1606}