Skip to main content

ml_kem/
algebra.rs

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
14/// An element of GF(q).
15pub(crate) type Elem = module_lattice::Elem<BaseField>;
16
17/// An element of the ring `R_q`, i.e., a polynomial over `Z_q` of degree 255
18pub(crate) type Polynomial = module_lattice::Polynomial<BaseField>;
19
20/// A vector of polynomials of length `K`.
21pub(crate) type Vector<K> = module_lattice::Vector<BaseField, K>;
22
23/// An element of the ring `T_q` i.e. a tuple of 128 elements of the direct sum components of `T_q`.
24pub(crate) type NttPolynomial = module_lattice::NttPolynomial<BaseField>;
25
26/// A vector of K NTT-domain polynomials.
27pub(crate) type NttVector<K> = module_lattice::NttVector<BaseField, K>;
28
29/// A K x K matrix of NTT-domain polynomials.  Each vector represents a row of the matrix, so that
30/// multiplying on the right just requires iteration.
31pub(crate) type NttMatrix<K> = module_lattice::NttMatrix<BaseField, K, K>;
32
33/// Algorithm 7: `SampleNTT(B)`
34pub(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            // Fill the buffer
52            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
104/// Algorithm 8: `SamplePolyCBD_eta(B)`
105///
106/// To avoid all the bitwise manipulation in the algorithm as written, we reuse the logic in
107/// `ByteDecode`.  We decode the PRF output into integers with `2 * eta` bits, then use
108/// `count_ones` to perform the summation described in the algorithm.
109///
110/// The PRF output is secret, so each coefficient is computed arithmetically: no memory access
111/// or branch depends on the sampled bits (in particular, there is no lookup table).
112pub(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
125/// Map `2 * eta` sampled bits to a CBD coefficient: the number of ones in the low `eta` bits
126/// minus the number of ones in the high `eta` bits, reduced mod `q`.
127fn 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    // x, y <= eta, so `x + Q - y` is in `[Q - eta, Q + eta]` and one conditional subtraction
136    // fully reduces it.
137    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
153/// The Number Theoretic Transform (NTT) is a variant of the Discrete Fourier Transform (DFT)
154/// defined over a finite field that turns costly polynomial multiplications into simple
155/// coefficient-wise multiplications modulo a fixed prime.
156pub(crate) trait Ntt {
157    type Output;
158    fn ntt(&self) -> Self::Output;
159}
160
161/// One layer of the forward NTT butterfly.
162///
163/// `LEN` is the butterfly half-length and `ITERATIONS = 128 / LEN` is the number of
164/// butterfly groups in the layer. Making both compile-time constants lets the compiler
165/// eliminate the iterator length calculation (`256 / (2 * LEN)`) that `step_by` would
166/// otherwise compute with a `UDIV` instruction.
167#[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
182/// Algorithm 9: `NTT`
183impl 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/// The inverse NTT is the reverse of the Number Theoretic Transform, converting coefficient-wise
211/// products back into standard polynomial form while preserving correctness modulo the same prime.
212#[allow(clippy::module_name_repetitions)]
213pub(crate) trait NttInverse {
214    type Output;
215    fn ntt_inverse(&self) -> Self::Output;
216}
217
218/// One layer of the inverse NTT butterfly.
219///
220/// See [`ntt_layer`] for the rationale behind the const generics.
221#[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
239/// Algorithm 10: `NTT^{-1}`
240impl 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
267/// Algorithm 11: `MultiplyNTTs`
268impl 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/// Algorithm 12: `BaseCaseMultiply`
290///
291/// This is a hot loop.  We promote to u64 so that we can do the absolute minimum number of
292/// modular reductions, since these are the expensive operation.
293#[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/// Since the powers of zeta used in the `NTT` and `MultiplyNTTs` are fixed, we use pre-computed
309/// tables to avoid the need to compute the exponentiations at runtime.
310///
311/// * `ZETA_POW_BITREV[i] = zeta^{BitRev_7(i)}`
312/// * `GAMMA[i] = zeta^{2 BitRev_7(i) + 1}`
313///
314/// Note that the const environment here imposes some annoying conditions.  Because operator
315/// overloading can't be const, we have to do all the reductions here manually.  Because `for` loops
316/// are forbidden in `const` functions, we do them manually with `while` loops.
317///
318/// The values computed here match those provided in Appendix A of FIPS 203.
319/// `ZETA_POW_BITREV` corresponds to the first table, and `GAMMA` to the second table.
320#[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    // Compute the powers of zeta
335    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    // Reorder the powers according to bitrev7
346    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    /// A polynomial with only a scalar component, to make simple test cases
381    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    /// Multiplication in `R_q`, modulo X^256 + 1
388    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    /// Transpose `NttMatrix`
405    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        // Verify that NTT and NTT^-1 are actually inverses
431        let f_unhat = f_hat.ntt_inverse();
432        assert_eq!(f, f_unhat);
433
434        // Verify that NTT is a homomorphism with regard to addition
435        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        // Verify that NTT is a homomorphism with regard to multiplication
441        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        // Verify vector addition
450        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        // Verify dot product
456        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        // Verify inequality (catches PartialEq mutation that returns true unconditionally)
461        assert_ne!(v1, v2);
462        assert_ne!(v1, v3);
463        assert_ne!(v2, v3);
464    }
465
466    #[test]
467    fn ntt_matrix() {
468        // Verify matrix multiplication by a vector
469        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        // Verify transpose
480        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    // To verify the accuracy of sampling, we use a theorem related to the law of large numbers,
489    // which bounds the convergence of the Kullback-Liebler distance between the empirical
490    // distribution and the hypothesized distribution.
491    //
492    // Theorem (Cover & Thomas, 1991, Theorem 12.2.1): Let $X_1, \ldots, X_n$ be i.i.d. $~P(x)$.
493    // Then:
494    //
495    //   Pr{ D(P_{x^n} || P) > \epsilon } \leq 2^{ -n ( \epsilon - |X|^{ log(n+1) / n } ) }
496    //
497    // So if we test by computing D(P_{x^n} || P) and requiring the value to be below a threshold
498    // \epsilon, then an unbiased sampling should pass with overwhelming probability 1 - 2^{-k},
499    // for some k based on \epsilon, |X|, and n.
500    //
501    // If we take k = 256 and n = 256, then we can solve for the required threshold \epsilon:
502    //
503    //   \epsilon = 1 + |X|^{ 0.03125 }
504    //
505    // For the cases we're interested in here:
506    //
507    //   CBD(eta = 2) => |X| = 5   => epsilon ~= 2.0516
508    //   CBD(eta = 2) => |X| = 7   => epsilon ~= 2.0627
509    //   Uniform byte => |X| = 256 => epsilon ~= 2.1892
510    //
511    // Taking epsilon = 2.05 makes us conservative enough in all cases, without significantly
512    // increasing the probability of false negatives.
513    const KL_THRESHOLD: f64 = 2.05;
514
515    // The centered binomial distributions are calculated as:
516    //
517    //   bin_\eta(k) = (2\eta \choose k + \eta) 2^{-2\eta}
518    //
519    // for k in $-\eta, \ldots, \eta$.  The cases of interest here are \eta = 2, 3.
520    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        // Verify data and compute the empirical distribution
554        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        // We require roughly Q/2 samples to verify the uniform distribution.  This is because for
571        // M < N, the uniform distribution over a subset of M elements has KL distance:
572        //
573        //   M sum(p * log(q / p)) = log(q / p) = log(N / M)
574        //
575        // Since Q ~= 2^11 and 256 == 2^8, we need 2^3 == 8 runs of 256 to get out of the bad
576        // regime and get a meaningful measurement.
577        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        // Eta = 2
589        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        // Eta = 3
595        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    /// Check `cbd_coefficient` against Algorithm 8 (`x - y mod q`, where `x` and `y` are the sums
602    /// of the first and last `eta` bits) for every possible input.
603    #[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}