1use crate::{
2 B32,
3 crypto::{PRF, PrfOutput, XOF},
4 param::CbdSamplingSize,
5};
6use array::{Array, ArraySize, typenum::U256};
7use module_lattice::{Encode, Field, MultiplyNtt, Truncate};
8use sha3::digest::XofReader;
9
10module_lattice::define_field!(BaseField, u16, u32, u64, 3329);
11
12pub(crate) type Int = <BaseField as Field>::Int;
13
14pub(crate) type Elem = module_lattice::Elem<BaseField>;
16
17pub(crate) type Polynomial = module_lattice::Polynomial<BaseField>;
19
20pub(crate) type Vector<K> = module_lattice::Vector<BaseField, K>;
22
23pub(crate) type NttPolynomial = module_lattice::NttPolynomial<BaseField>;
25
26pub(crate) type NttVector<K> = module_lattice::NttVector<BaseField, K>;
28
29pub(crate) type NttMatrix<K> = module_lattice::NttMatrix<BaseField, K, K>;
32
33pub(crate) fn sample_ntt(B: &mut impl XofReader) -> NttPolynomial {
35 struct FieldElementReader<'a> {
36 xof: &'a mut dyn XofReader,
37 data: [u8; 96],
38 start: usize,
39 next: Option<Int>,
40 }
41
42 impl<'a> FieldElementReader<'a> {
43 fn new(xof: &'a mut impl XofReader) -> Self {
44 let mut out = Self {
45 xof,
46 data: [0u8; 96],
47 start: 0,
48 next: None,
49 };
50
51 out.xof.read(&mut out.data);
53
54 out
55 }
56
57 fn next(&mut self) -> Elem {
58 if let Some(val) = self.next {
59 self.next = None;
60 return Elem::new(val);
61 }
62
63 loop {
64 if self.start == self.data.len() {
65 self.xof.read(&mut self.data);
66 self.start = 0;
67 }
68
69 let end = self.start + 3;
70 let b = &self.data[self.start..end];
71 self.start = end;
72
73 let d1 = Int::from(b[0]) + ((Int::from(b[1]) & 0xf) << 8);
74 let d2 = (Int::from(b[1]) >> 4) + (Int::from(b[2]) << 4);
75
76 if d1 < BaseField::Q {
77 if d2 < BaseField::Q {
78 self.next = Some(d2);
79 }
80 return Elem::new(d1);
81 }
82
83 if d2 < BaseField::Q {
84 return Elem::new(d2);
85 }
86 }
87 }
88 }
89
90 let mut reader = FieldElementReader::new(B);
91 NttPolynomial::new(Array::from_fn(|_| reader.next()))
92}
93
94pub(crate) fn matrix_sample_ntt<K: ArraySize>(rho: &B32, transpose: bool) -> NttMatrix<K> {
95 NttMatrix::new(Array::from_fn(|i| {
96 NttVector::new(Array::from_fn(|j| {
97 let (i, j) = if transpose { (j, i) } else { (i, j) };
98 let mut xof = XOF(rho, Truncate::truncate(j), Truncate::truncate(i));
99 sample_ntt(&mut xof)
100 }))
101 }))
102}
103
104pub(crate) fn sample_poly_cbd<Eta>(B: &PrfOutput<Eta>) -> Polynomial
113where
114 Eta: CbdSamplingSize,
115{
116 let vals: Polynomial = Encode::<Eta::SampleSize>::decode(B);
117 Polynomial::new(
118 vals.0
119 .iter()
120 .map(|val| cbd_coefficient::<Eta>(val.0))
121 .collect(),
122 )
123}
124
125fn cbd_coefficient<Eta>(bits: Int) -> Elem
128where
129 Eta: CbdSamplingSize,
130{
131 let low_mask: Int = (1 << Eta::USIZE) - 1;
132 let x = <Int as Truncate<u32>>::truncate((bits & low_mask).count_ones());
133 let y = <Int as Truncate<u32>>::truncate((bits >> Eta::USIZE).count_ones());
134
135 Elem::new(BaseField::small_reduce(x + BaseField::Q - y))
138}
139
140pub(crate) fn sample_poly_vec_cbd<Eta, K>(sigma: &B32, start_n: u8) -> Vector<K>
141where
142 Eta: CbdSamplingSize,
143 K: ArraySize,
144{
145 Vector::new(Array::from_fn(|i| {
146 let i: u8 = Truncate::truncate(i);
147 let N = start_n + i;
148 let prf_output = PRF::<Eta>(sigma, N);
149 sample_poly_cbd::<Eta>(&prf_output)
150 }))
151}
152
153pub(crate) trait Ntt {
157 type Output;
158 fn ntt(&self) -> Self::Output;
159}
160
161#[inline(always)]
168fn ntt_layer<const LEN: usize, const ITERATIONS: usize>(f: &mut Array<Elem, U256>, k: &mut usize) {
169 for i in 0..ITERATIONS {
170 let start = i * 2 * LEN;
171 let zeta = ZETA_POW_BITREV[*k];
172 *k += 1;
173
174 for j in start..(start + LEN) {
175 let t = zeta * f[j + LEN];
176 f[j + LEN] = f[j] - t;
177 f[j] = f[j] + t;
178 }
179 }
180}
181
182impl Ntt for Polynomial {
184 type Output = NttPolynomial;
185
186 fn ntt(&self) -> NttPolynomial {
187 let mut k = 1;
188 let mut f = self.0;
189
190 ntt_layer::<128, 1>(&mut f, &mut k);
191 ntt_layer::<64, 2>(&mut f, &mut k);
192 ntt_layer::<32, 4>(&mut f, &mut k);
193 ntt_layer::<16, 8>(&mut f, &mut k);
194 ntt_layer::<8, 16>(&mut f, &mut k);
195 ntt_layer::<4, 32>(&mut f, &mut k);
196 ntt_layer::<2, 64>(&mut f, &mut k);
197
198 f.into()
199 }
200}
201
202impl<K: ArraySize> Ntt for Vector<K> {
203 type Output = NttVector<K>;
204
205 fn ntt(&self) -> NttVector<K> {
206 NttVector::new(self.0.iter().map(Ntt::ntt).collect())
207 }
208}
209
210#[allow(clippy::module_name_repetitions)]
213pub(crate) trait NttInverse {
214 type Output;
215 fn ntt_inverse(&self) -> Self::Output;
216}
217
218#[inline(always)]
222fn ntt_inverse_layer<const LEN: usize, const ITERATIONS: usize>(
223 f: &mut Array<Elem, U256>,
224 k: &mut usize,
225) {
226 for i in 0..ITERATIONS {
227 let start = i * 2 * LEN;
228 let zeta = ZETA_POW_BITREV[*k];
229 *k -= 1;
230
231 for j in start..(start + LEN) {
232 let t = f[j];
233 f[j] = t + f[j + LEN];
234 f[j + LEN] = zeta * (f[j + LEN] - t);
235 }
236 }
237}
238
239impl NttInverse for NttPolynomial {
241 type Output = Polynomial;
242
243 fn ntt_inverse(&self) -> Polynomial {
244 let mut f: Array<Elem, U256> = self.0.clone();
245 let mut k = 127;
246
247 ntt_inverse_layer::<2, 64>(&mut f, &mut k);
248 ntt_inverse_layer::<4, 32>(&mut f, &mut k);
249 ntt_inverse_layer::<8, 16>(&mut f, &mut k);
250 ntt_inverse_layer::<16, 8>(&mut f, &mut k);
251 ntt_inverse_layer::<32, 4>(&mut f, &mut k);
252 ntt_inverse_layer::<64, 2>(&mut f, &mut k);
253 ntt_inverse_layer::<128, 1>(&mut f, &mut k);
254
255 Elem::new(3303) * &Polynomial::new(f)
256 }
257}
258
259impl<K: ArraySize> NttInverse for NttVector<K> {
260 type Output = Vector<K>;
261
262 fn ntt_inverse(&self) -> Vector<K> {
263 Vector::new(self.0.iter().map(NttInverse::ntt_inverse).collect())
264 }
265}
266
267impl MultiplyNtt for BaseField {
269 fn multiply_ntt(lhs: &NttPolynomial, rhs: &NttPolynomial) -> NttPolynomial {
270 let mut out = NttPolynomial::new(Array::default());
271
272 for i in 0..128 {
273 let (c0, c1) = base_case_multiply(
274 lhs.0[2 * i],
275 lhs.0[2 * i + 1],
276 rhs.0[2 * i],
277 rhs.0[2 * i + 1],
278 i,
279 );
280
281 out.0[2 * i] = c0;
282 out.0[2 * i + 1] = c1;
283 }
284
285 out
286 }
287}
288
289#[inline]
294fn base_case_multiply(a0: Elem, a1: Elem, b0: Elem, b1: Elem, i: usize) -> (Elem, Elem) {
295 let a0 = u32::from(a0.0);
296 let a1 = u32::from(a1.0);
297 let b0 = u32::from(b0.0);
298 let b1 = u32::from(b1.0);
299 let g = u32::from(GAMMA[i].0);
300
301 let b1g = u32::from(BaseField::barrett_reduce(b1 * g));
302
303 let c0 = BaseField::barrett_reduce(a0 * b0 + a1 * b1g);
304 let c1 = BaseField::barrett_reduce(a0 * b1 + a1 * b0);
305 (Elem::new(c0), Elem::new(c1))
306}
307
308#[allow(clippy::integer_division_remainder_used, reason = "constant")]
321const ZETA_POW_BITREV: [Elem; 128] = {
322 const ZETA: u64 = 17;
323
324 const fn bitrev7(x: usize) -> usize {
325 ((x >> 6) % 2)
326 | (((x >> 5) % 2) << 1)
327 | (((x >> 4) % 2) << 2)
328 | (((x >> 3) % 2) << 3)
329 | (((x >> 2) % 2) << 4)
330 | (((x >> 1) % 2) << 5)
331 | ((x % 2) << 6)
332 }
333
334 let mut pow = [Elem::new(0); 128];
336 let mut i = 0;
337 let mut curr = 1u64;
338
339 while i < 128 {
340 pow[i] = Elem::new((curr & 0xFFFF) as u16);
341 i += 1;
342 curr = (curr * ZETA) % BaseField::QLL;
343 }
344
345 let mut pow_bitrev = [Elem::new(0); 128];
347 let mut i = 0;
348 while i < 128 {
349 pow_bitrev[i] = pow[bitrev7(i)];
350 i += 1;
351 }
352 pow_bitrev
353};
354
355#[allow(clippy::integer_division_remainder_used, reason = "constant")]
356const GAMMA: [Elem; 128] = {
357 const ZETA: u64 = 17;
358 let mut gamma = [Elem::new(0); 128];
359 let mut i = 0;
360 while i < 128 {
361 let zpr = ZETA_POW_BITREV[i].0 as u64;
362 let g = (zpr * zpr * ZETA) % BaseField::QLL;
363 gamma[i] = Elem::new((g & 0xFFFF) as u16);
364 i += 1;
365 }
366 gamma
367};
368
369#[cfg(test)]
370mod test {
371 use super::{
372 Array, B32, BaseField, Elem, Field, Int, Ntt, NttInverse, NttMatrix, NttPolynomial,
373 NttVector, PRF, Polynomial, U256, XOF,
374 };
375 use array::{
376 ArraySize, Flatten,
377 typenum::{U2, U3, U8},
378 };
379
380 fn const_ntt(x: Int) -> NttPolynomial {
382 let mut p = Polynomial::default();
383 p.0[0] = Elem::new(x);
384 p.ntt()
385 }
386
387 fn poly_mul(lhs: &Polynomial, rhs: &Polynomial) -> Polynomial {
389 let mut out = Polynomial::default();
390 for (i, x) in lhs.0.iter().enumerate() {
391 for (j, y) in rhs.0.iter().enumerate() {
392 let (sign, index) = if i + j < 256 {
393 (Elem::new(1), i + j)
394 } else {
395 (Elem::new(BaseField::Q - 1), i + j - 256)
396 };
397
398 out.0[index] = out.0[index] + (sign * *x * *y);
399 }
400 }
401 out
402 }
403
404 fn matrix_transpose<K: ArraySize>(matrix: &NttMatrix<K>) -> NttMatrix<K> {
406 NttMatrix::new(Array::from_fn(|i| {
407 NttVector::new(Array::from_fn(|j| matrix.0[j].0[i].clone()))
408 }))
409 }
410
411 #[test]
412 #[allow(clippy::cast_possible_truncation)]
413 fn polynomial_ops() {
414 let f = Polynomial::new(Array::from_fn(|i| Elem::new(i as Int)));
415 let g = Polynomial::new(Array::from_fn(|i| Elem::new(2 * i as Int)));
416 let sum = Polynomial::new(Array::from_fn(|i| Elem::new(3 * i as Int)));
417 assert_eq!((&f + &g), sum);
418 assert_eq!((&sum - &g), f);
419 assert_eq!(Elem::new(3) * &f, sum);
420 }
421
422 #[test]
423 #[allow(clippy::cast_possible_truncation, clippy::similar_names)]
424 fn ntt() {
425 let f = Polynomial::new(Array::from_fn(|i| Elem::new(i as Int)));
426 let g = Polynomial::new(Array::from_fn(|i| Elem::new(2 * i as Int)));
427 let f_hat = f.ntt();
428 let g_hat = g.ntt();
429
430 let f_unhat = f_hat.ntt_inverse();
432 assert_eq!(f, f_unhat);
433
434 let fg = &f + &g;
436 let f_hat_g_hat = &f_hat + &g_hat;
437 let fg_unhat = f_hat_g_hat.ntt_inverse();
438 assert_eq!(fg, fg_unhat);
439
440 let fg = poly_mul(&f, &g);
442 let f_hat_g_hat = &f_hat * &g_hat;
443 let fg_unhat = f_hat_g_hat.ntt_inverse();
444 assert_eq!(fg, fg_unhat);
445 }
446
447 #[test]
448 fn ntt_vector() {
449 let v1: NttVector<U3> = NttVector::new(Array([const_ntt(1), const_ntt(1), const_ntt(1)]));
451 let v2: NttVector<U3> = NttVector::new(Array([const_ntt(2), const_ntt(2), const_ntt(2)]));
452 let v3: NttVector<U3> = NttVector::new(Array([const_ntt(3), const_ntt(3), const_ntt(3)]));
453 assert_eq!((&v1 + &v2), v3);
454
455 assert_eq!((&v1 * &v2), const_ntt(6));
457 assert_eq!((&v1 * &v3), const_ntt(9));
458 assert_eq!((&v2 * &v3), const_ntt(18));
459
460 assert_ne!(v1, v2);
462 assert_ne!(v1, v3);
463 assert_ne!(v2, v3);
464 }
465
466 #[test]
467 fn ntt_matrix() {
468 let a: NttMatrix<U3> = NttMatrix::new(Array([
470 NttVector::new(Array([const_ntt(1), const_ntt(2), const_ntt(3)])),
471 NttVector::new(Array([const_ntt(4), const_ntt(5), const_ntt(6)])),
472 NttVector::new(Array([const_ntt(7), const_ntt(8), const_ntt(9)])),
473 ]));
474 let v_in: NttVector<U3> = NttVector::new(Array([const_ntt(1), const_ntt(2), const_ntt(3)]));
475 let v_out: NttVector<U3> =
476 NttVector::new(Array([const_ntt(14), const_ntt(32), const_ntt(50)]));
477 assert_eq!(&a * &v_in, v_out);
478
479 let aT = NttMatrix::new(Array([
481 NttVector::new(Array([const_ntt(1), const_ntt(4), const_ntt(7)])),
482 NttVector::new(Array([const_ntt(2), const_ntt(5), const_ntt(8)])),
483 NttVector::new(Array([const_ntt(3), const_ntt(6), const_ntt(9)])),
484 ]));
485 assert_eq!(matrix_transpose(&a), aT);
486 }
487
488 const KL_THRESHOLD: f64 = 2.05;
514
515 type Distribution = [f64; Q_SIZE];
521 const Q_SIZE: usize = BaseField::Q as usize;
522 static CBD2: Distribution = {
523 let mut dist = [0.0; Q_SIZE];
524 dist[Q_SIZE - 2] = 1.0 / 16.0;
525 dist[Q_SIZE - 1] = 4.0 / 16.0;
526 dist[0] = 6.0 / 16.0;
527 dist[1] = 4.0 / 16.0;
528 dist[2] = 1.0 / 16.0;
529 dist
530 };
531 static CBD3: Distribution = {
532 let mut dist = [0.0; Q_SIZE];
533 dist[Q_SIZE - 3] = 1.0 / 64.0;
534 dist[Q_SIZE - 2] = 6.0 / 64.0;
535 dist[Q_SIZE - 1] = 15.0 / 64.0;
536 dist[0] = 20.0 / 64.0;
537 dist[1] = 15.0 / 64.0;
538 dist[2] = 6.0 / 64.0;
539 dist[3] = 1.0 / 64.0;
540 dist
541 };
542 static UNIFORM: Distribution = [1.0 / (BaseField::Q as f64); Q_SIZE];
543
544 fn kl_divergence(p: &Distribution, q: &Distribution) -> f64 {
545 p.iter()
546 .zip(q.iter())
547 .map(|(p, q)| if *p == 0.0 { 0.0 } else { p * (p / q).log2() })
548 .sum()
549 }
550
551 #[allow(clippy::cast_precision_loss, clippy::large_stack_arrays)]
552 fn test_sample(sample: &[Elem], ref_dist: &Distribution) {
553 let mut sample_dist: Distribution = [0.0; Q_SIZE];
555 let bump: f64 = 1.0 / (sample.len() as f64);
556 for x in sample {
557 assert!(x.0 < BaseField::Q);
558 assert!(ref_dist[x.0 as usize] > 0.0);
559
560 sample_dist[x.0 as usize] += bump;
561 }
562
563 let d = kl_divergence(&sample_dist, ref_dist);
564 assert!(d < KL_THRESHOLD);
565 }
566
567 #[test]
568 #[allow(clippy::cast_possible_truncation)]
569 fn sample_uniform() {
570 let rho = B32::default();
578 let sample: Array<Array<Elem, U256>, U8> = Array::from_fn(|i| {
579 let mut xof = XOF(&rho, 0, i as u8);
580 super::sample_ntt(&mut xof).into()
581 });
582
583 test_sample(&sample.flatten(), &UNIFORM);
584 }
585
586 #[test]
587 fn sample_poly_cbd() {
588 let sigma = B32::default();
590 let prf_output = PRF::<U2>(&sigma, 0);
591 let sample = super::sample_poly_cbd::<U2>(&prf_output).0;
592 test_sample(&sample, &CBD2);
593
594 let sigma = B32::default();
596 let prf_output = PRF::<U3>(&sigma, 0);
597 let sample = super::sample_poly_cbd::<U3>(&prf_output).0;
598 test_sample(&sample, &CBD3);
599 }
600
601 #[test]
604 #[allow(
605 clippy::integer_division_remainder_used,
606 reason = "test oracle on public values"
607 )]
608 fn cbd_coefficient_matches_definition() {
609 fn check<Eta: crate::param::CbdSamplingSize>() {
610 let eta = Eta::U32;
611 for bits in 0..(1u16 << (2 * eta)) {
612 let x: u16 = (0..eta).map(|i| (bits >> i) & 1).sum();
613 let y: u16 = (eta..2 * eta).map(|i| (bits >> i) & 1).sum();
614 let expected = (x + BaseField::Q - y) % BaseField::Q;
615 assert_eq!(super::cbd_coefficient::<Eta>(bits), Elem::new(expected));
616 }
617 }
618
619 check::<U2>();
620 check::<U3>();
621 }
622}