Skip to main content

p3_binary_field/
poly192.rs

1//! `GF(2^192)` as the cubic extension `y^3 + y + 1` of the 64-bit polynomial-basis field.
2//!
3//! A cubic over a finite field is irreducible exactly when it has no root there.
4//!
5//! A root of this one would generate `GF(2^3)` inside `GF(2^64)`, and `3` does not divide `64`.
6//!
7//! The pairing matters for soundness budgeting.
8//!
9//! A 64-bit column field halves committed volume, and 192 bits of challenge leave headroom.
10
11use alloc::vec::Vec;
12use core::fmt::{self, Debug, Display, Formatter};
13use core::iter::{Product, Sum};
14use core::mem::ManuallyDrop;
15use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
16
17use num_bigint::BigUint;
18use p3_field::extension::HasFrobenius;
19use p3_field::op_assign_macros::{
20    impl_add_assign, impl_add_base_field, impl_div_methods, impl_mul_methods, impl_sub_assign,
21    impl_sub_base_field, ring_sum,
22};
23use p3_field::{
24    Algebra, AlgebraIdentity, BasedVectorSpace, ExtensionField, Field, Packable,
25    PackedFieldExtension, Powers, PrimeCharacteristicRing, RawDataSerializable,
26};
27use rand::Rng;
28use rand::distr::{Distribution, StandardUniform};
29use serde::{Deserialize, Serialize};
30
31use crate::gf2::characteristic_two_methods;
32use crate::{Gf2, Poly64};
33
34/// The number of coordinates over the coefficient field.
35const DEGREE: usize = 3;
36
37/// `GF(2^192)`, as three coordinates over `GF(2^64)` in the basis `1, y, y^2`.
38///
39/// The defining relation is `y^3 = y + 1`, which also gives `y^4 = y^2 + y`.
40///
41/// Everything below reduces with those two rewrites and nothing else.
42#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(transparent)]
44#[repr(transparent)]
45#[must_use]
46pub struct Poly192([Poly64; DEGREE]);
47
48impl Poly192 {
49    /// The number of bits of an element.
50    pub(crate) const BITS: usize = DEGREE * 64;
51
52    /// The element with the given coordinates in the basis `1, y, y^2`.
53    #[inline]
54    pub const fn new(coefficients: [Poly64; DEGREE]) -> Self {
55        Self(coefficients)
56    }
57
58    /// The coordinates of this element in the basis `1, y, y^2`.
59    #[inline]
60    pub const fn coefficients(self) -> [Poly64; DEGREE] {
61        self.0
62    }
63
64    /// The element with all its weight on one coordinate.
65    #[inline]
66    const fn embed(value: Poly64) -> Self {
67        Self([value, Poly64::ZERO, Poly64::ZERO])
68    }
69
70    /// The product assembled from the coefficient field's own reduced products.
71    ///
72    /// Compiled everywhere, so its tests and its benchmark run even where a backend wins.
73    #[doc(hidden)]
74    #[inline]
75    pub fn composed_mul(self, rhs: Self) -> Self {
76        let ([a0, a1, a2], [b0, b1, b2]) = (self.0, rhs.0);
77
78        // The three diagonal products.
79        let c0 = a0 * b0;
80        let c1 = a1 * b1;
81        let c2 = a2 * b2;
82
83        // One product per off-diagonal pair, each carrying both of that pair's cross terms.
84        let d01 = (a0 + a1) * (b0 + b1);
85        let d02 = (a0 + a2) * (b0 + b2);
86        let d12 = (a1 + a2) * (b1 + b2);
87
88        // The three middle coefficients of the unreduced product.
89        let p1 = d01 + c0 + c1;
90        let p2 = d02 + c0 + c1 + c2;
91        let p3 = d12 + c1 + c2;
92
93        Self([c0 + p3, p1 + p3 + c2, p2 + c2])
94    }
95
96    /// The inverse of this element, with zero sent to zero.
97    ///
98    /// Every step runs whatever the operand is, so the cost says nothing about the value.
99    #[inline]
100    pub fn invert_or_zero(self) -> Self {
101        self.try_inverse().unwrap_or(Self::ZERO)
102    }
103}
104
105impl Packable for Poly192 {}
106
107impl Display for Poly192 {
108    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109        write!(f, "{} + {}*y + {}*y^2", self.0[0], self.0[1], self.0[2])
110    }
111}
112
113impl Debug for Poly192 {
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        Debug::fmt(&self.0, f)
116    }
117}
118
119impl Distribution<Poly192> for StandardUniform {
120    #[inline]
121    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Poly192 {
122        Poly192(core::array::from_fn(|_| self.sample(rng)))
123    }
124}
125
126impl Add for Poly192 {
127    type Output = Self;
128
129    #[inline]
130    #[allow(clippy::suspicious_arithmetic_impl)]
131    fn add(self, rhs: Self) -> Self {
132        // Addition in characteristic 2 is `XOR`, coordinate by coordinate.
133        Self(core::array::from_fn(|i| self.0[i] + rhs.0[i]))
134    }
135}
136
137impl Sub for Poly192 {
138    type Output = Self;
139
140    #[inline]
141    #[allow(clippy::suspicious_arithmetic_impl)]
142    fn sub(self, rhs: Self) -> Self {
143        // Subtraction coincides with addition in characteristic 2.
144        self + rhs
145    }
146}
147
148impl Neg for Poly192 {
149    type Output = Self;
150
151    #[inline]
152    fn neg(self) -> Self {
153        // `-x = x` in characteristic 2.
154        self
155    }
156}
157
158impl Mul for Poly192 {
159    type Output = Self;
160
161    /// # Algorithm
162    ///
163    /// Karatsuba over three limbs: six products instead of the schoolbook nine.
164    ///
165    /// ```text
166    ///     c_i    = a_i b_i
167    ///     d_ij   = (a_i + a_j) (b_i + b_j)
168    ///     p_1    = d_01 + c_0 + c_1
169    ///     p_2    = d_02 + c_0 + c_1 + c_2
170    ///     p_3    = d_12 + c_1 + c_2
171    /// ```
172    ///
173    /// The unreduced product spans degrees 0 to 4, and the modulus folds the top two down:
174    ///
175    /// ```text
176    ///     y^3    = y + 1
177    ///     y^4    = y^2 + y
178    /// ```
179    #[inline]
180    fn mul(self, rhs: Self) -> Self {
181        #[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
182        {
183            let limbs = |x: Self| x.0.map(Poly64::to_bits);
184            Self(crate::clmul::poly_mul_192(limbs(self), limbs(rhs)).map(Poly64::new))
185        }
186        #[cfg(not(all(target_arch = "x86_64", target_feature = "pclmulqdq")))]
187        {
188            self.composed_mul(rhs)
189        }
190    }
191}
192
193impl Mul<Poly64> for Poly192 {
194    type Output = Self;
195
196    /// Scaling stays inside each coordinate, so it costs three products rather than six.
197    #[inline]
198    fn mul(self, rhs: Poly64) -> Self {
199        Self(self.0.map(|c| c * rhs))
200    }
201}
202
203impl Mul<Poly192> for Poly64 {
204    type Output = Poly192;
205
206    #[inline]
207    fn mul(self, rhs: Poly192) -> Poly192 {
208        rhs * self
209    }
210}
211
212impl PrimeCharacteristicRing for Poly192 {
213    #[inline]
214    fn zero_vec(len: usize) -> Vec<Self> {
215        let mut values = ManuallyDrop::new(alloc::vec![[0u64; DEGREE]; len]);
216        // SAFETY: the transparent wrapper has exactly the integer's layout, and zero is
217        // canonical. The allocation retains its original size and alignment.
218        unsafe { Vec::from_raw_parts(values.as_mut_ptr().cast(), values.len(), values.capacity()) }
219    }
220
221    type PrimeSubfield = Gf2;
222
223    const ZERO: Self = Self::embed(Poly64::ZERO);
224    const ONE: Self = Self::embed(Poly64::ONE);
225    // The characteristic is 2, so `TWO = ONE + ONE = ZERO`.
226    const TWO: Self = Self::embed(Poly64::ZERO);
227    // The characteristic is 2, so `NEG_ONE = ONE`.
228    const NEG_ONE: Self = Self::embed(Poly64::ONE);
229
230    #[inline]
231    fn from_prime_subfield(f: Self::PrimeSubfield) -> Self {
232        Self::embed(Poly64::from_prime_subfield(f))
233    }
234
235    /// Squaring is `F_2`-linear, so the cross terms vanish and only the fold survives.
236    ///
237    /// ```text
238    ///     (a_0 + a_1 y + a_2 y^2)^2  = a_0^2 + a_1^2 y^2 + a_2^2 y^4
239    ///                                = a_0^2 + a_2^2 y + (a_1^2 + a_2^2) y^2
240    /// ```
241    #[inline]
242    fn square(&self) -> Self {
243        let [a0, a1, a2] = self.0;
244        let (s0, s1, s2) = (a0.square(), a1.square(), a2.square());
245        Self([s0, s2, s1 + s2])
246    }
247
248    characteristic_two_methods!();
249}
250
251impl Field for Poly192 {
252    type Packing = Self;
253
254    // An element of the multiplicative group whose order is the whole group.
255    const GENERATOR: Self = Self([Poly64::new(5), Poly64::new(3), Poly64::new(1)]);
256
257    /// # Algorithm
258    ///
259    /// Multiplication by a fixed element is a `3 x 3` matrix over the coefficient field.
260    ///
261    /// Its columns are where that multiplication sends the three basis elements:
262    ///
263    /// ```text
264    ///     a * 1   = ( a_0,        a_1,        a_2       )
265    ///     a * y   = ( a_2,        a_0 + a_2,  a_1       )
266    ///     a * y^2 = ( a_1,        a_1 + a_2,  a_0 + a_2 )
267    /// ```
268    ///
269    /// The inverse is the first column of that matrix inverted.
270    ///
271    /// The adjugate gives it as the top row of cofactors over the determinant.
272    ///
273    /// The determinant is the norm down to the coefficient field, and it vanishes only at zero.
274    #[inline]
275    fn try_inverse(&self) -> Option<Self> {
276        let [a0, a1, a2] = self.0;
277
278        // The entry the modulus makes appear three times in the matrix.
279        let diagonal = a0 + a2;
280
281        // The cofactors along the top row.
282        let c0 = diagonal.square() + a1.square() + a1 * a2;
283        let c1 = a1 * diagonal + (a1 + a2) * a2;
284        let c2 = a1.square() + diagonal * a2;
285
286        // Expanding along that row gives the determinant.
287        let norm = a0 * c0 + a2 * c1 + a1 * c2;
288
289        // Every step runs whatever the operand is, so the cost says nothing about the value.
290        let candidate = Self([c0, c1, c2]) * norm.invert_or_zero();
291        (!norm.is_zero()).then_some(candidate)
292    }
293
294    /// Squaring is a triangular map on the coordinates, so its inverse is one too.
295    ///
296    /// ```text
297    ///     b_0 = a_0^2,  b_1 = a_2^2,  b_2 = a_1^2 + a_2^2
298    /// ```
299    ///
300    /// Reading that backwards costs three coefficient-field square roots and one addition.
301    #[inline]
302    fn try_sqrt(&self) -> Option<Self> {
303        let [b0, b1, b2] = self.0;
304        let root = |x: Poly64| {
305            x.try_sqrt()
306                .expect("every element of a binary field is a square")
307        };
308        Some(Self([root(b0), root(b1 + b2), root(b1)]))
309    }
310
311    #[inline]
312    fn order() -> BigUint {
313        BigUint::from(1u8) << Self::BITS
314    }
315
316    /// An element of `GF(2^n)` is exactly `n` bits wide.
317    #[inline]
318    fn bits() -> usize {
319        Self::BITS
320    }
321
322    /// The enumeration by bit pattern.
323    ///
324    /// The coordinates over `GF(2)` run through the coefficient fields in order.
325    ///
326    /// An index narrower than 64 bits therefore lands entirely in the first of them.
327    #[inline]
328    fn interpolation_node(i: usize) -> Self {
329        Self::embed(Poly64::interpolation_node(i))
330    }
331}
332
333impl RawDataSerializable for Poly192 {
334    const NUM_BYTES: usize = DEGREE * Poly64::NUM_BYTES;
335
336    #[inline]
337    fn into_bytes(self) -> impl IntoIterator<Item = u8> {
338        self.0.into_iter().flat_map(RawDataSerializable::into_bytes)
339    }
340}
341
342impl From<Poly64> for Poly192 {
343    /// The coefficient field sits at the constant coordinate.
344    #[inline]
345    fn from(x: Poly64) -> Self {
346        Self::embed(x)
347    }
348}
349
350impl From<Gf2> for Poly192 {
351    /// `GF(2)` is the prime subfield, embedded as `{ZERO, ONE}`.
352    #[inline]
353    fn from(x: Gf2) -> Self {
354        Self::from_prime_subfield(x)
355    }
356}
357
358impl Mul<Gf2> for Poly192 {
359    type Output = Self;
360
361    #[inline]
362    fn mul(self, rhs: Gf2) -> Self {
363        // The prime subfield has two elements, so this keeps the element or clears it.
364        if rhs.is_one() { self } else { Self::ZERO }
365    }
366}
367
368impl Mul<Poly192> for Gf2 {
369    type Output = Poly192;
370
371    #[inline]
372    fn mul(self, rhs: Poly192) -> Poly192 {
373        rhs * self
374    }
375}
376
377impl_add_assign!(Poly192);
378impl_sub_assign!(Poly192);
379impl_mul_methods!(Poly192);
380impl_div_methods!(Poly192, Poly192);
381impl_div_methods!(Poly192, Poly64);
382ring_sum!(Poly192);
383
384impl_add_base_field!(Poly192, Poly64);
385impl_sub_base_field!(Poly192, Poly64);
386impl_add_base_field!(Poly192, Gf2);
387impl_sub_base_field!(Poly192, Gf2);
388
389impl Algebra<Poly64> for Poly192 {}
390
391impl Algebra<Gf2> for Poly192 {}
392
393impl AlgebraIdentity<Poly64> for Poly192 {
394    fn algebra_id() -> Vec<u8> {
395        b"p3-binary-cubic-v1:y^3+y+1:64:192".to_vec()
396    }
397}
398
399impl BasedVectorSpace<Poly64> for Poly192 {
400    const DIMENSION: usize = DEGREE;
401
402    #[inline]
403    fn as_basis_coefficients_slice(&self) -> &[Poly64] {
404        &self.0
405    }
406
407    #[inline]
408    fn from_basis_coefficients_fn<F: FnMut(usize) -> Poly64>(f: F) -> Self {
409        Self(core::array::from_fn(f))
410    }
411
412    #[inline]
413    fn from_basis_coefficients_iter<I: ExactSizeIterator<Item = Poly64>>(iter: I) -> Option<Self> {
414        (iter.len() == DEGREE).then(|| {
415            // The reported length is not a guarantee, so zipping bounds the fill either way.
416            let mut coefficients = [Poly64::ZERO; DEGREE];
417            for (slot, value) in coefficients.iter_mut().zip(iter) {
418                *slot = value;
419            }
420            Self(coefficients)
421        })
422    }
423
424    /// A whole vector of elements is one contiguous run of coordinates, so this is a copy.
425    #[inline]
426    fn flatten_to_base(vec: Vec<Self>) -> Vec<Poly64> {
427        let mut out = Vec::with_capacity(vec.len() * DEGREE);
428        for element in vec {
429            out.extend_from_slice(&element.0);
430        }
431        out
432    }
433}
434
435impl ExtensionField<Poly64> for Poly192 {
436    type ExtensionPacking = Self;
437
438    #[inline]
439    fn is_in_basefield(&self) -> bool {
440        // The coefficient field is exactly the elements with no weight above the constant.
441        self.0[1].is_zero() && self.0[2].is_zero()
442    }
443
444    #[inline]
445    fn as_base(&self) -> Option<Poly64> {
446        <Self as ExtensionField<Poly64>>::is_in_basefield(self).then_some(self.0[0])
447    }
448}
449
450/// One element per vector, so every lane index is zero.
451impl PackedFieldExtension<Poly64, Self> for Poly192 {
452    #[inline]
453    fn from_ext_fn(f: impl Fn(usize) -> Self) -> Self {
454        f(0)
455    }
456
457    #[inline]
458    fn from_ext_slice(slice: &[Self]) -> Self {
459        assert_eq!(slice.len(), 1);
460        slice[0]
461    }
462
463    #[inline]
464    fn extract(&self, lane: usize) -> Self {
465        assert_eq!(lane, 0, "lane index out of range");
466        *self
467    }
468
469    #[inline]
470    fn add_assign_lane(&mut self, lane: usize, value: Self) {
471        assert_eq!(lane, 0, "lane index out of range");
472        *self += value;
473    }
474
475    #[inline]
476    fn packed_ext_powers(base: Self) -> Powers<Self> {
477        base.powers()
478    }
479}
480
481impl HasFrobenius<Poly64> for Poly192 {
482    /// The map raising to the size of the coefficient field.
483    ///
484    /// It fixes every coefficient and permutes the basis.
485    ///
486    /// The three roots of the modulus are `y`, `y^2` and `y^2 + y`, so:
487    ///
488    /// ```text
489    ///     y^(2^64)                 =  y^2
490    ///     a_0 + a_1 y + a_2 y^2    ->  a_0 + a_2 y + (a_1 + a_2) y^2
491    /// ```
492    ///
493    /// So one application is one addition and a permutation, with no products at all.
494    #[inline]
495    fn frobenius(&self) -> Self {
496        let [a0, a1, a2] = self.0;
497        Self([a0, a2, a1 + a2])
498    }
499
500    #[inline]
501    fn repeated_frobenius(&self, count: usize) -> Self {
502        // The Galois group is cyclic of order three, so the count reduces modulo it.
503        match count % DEGREE {
504            0 => *self,
505            1 => self.frobenius(),
506            _ => self.frobenius().frobenius(),
507        }
508    }
509
510    #[inline]
511    fn pseudo_inv(&self) -> Self {
512        // Inversion through the adjugate beats the exponentiation the contract is phrased in.
513        self.invert_or_zero()
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use p3_field::extension::HasFrobenius;
520    use p3_field::{BasedVectorSpace, ExtensionField, Field, PrimeCharacteristicRing};
521    use proptest::prelude::*;
522
523    use super::{DEGREE, Poly192};
524    use crate::Poly64;
525
526    /// An element from three raw bit patterns.
527    fn element(a: [u64; 3]) -> Poly192 {
528        Poly192::new(a.map(Poly64::new))
529    }
530
531    /// The `j`-th vector of the basis over the coefficient field.
532    fn basis_element(j: usize) -> Poly192 {
533        <Poly192 as BasedVectorSpace<Poly64>>::ith_basis_element(j).unwrap()
534    }
535
536    /// Schoolbook multiplication, nine products and the modulus applied term by term.
537    ///
538    /// The routine under test recombines a Karatsuba schedule and folds the top two.
539    ///
540    /// This shares neither of those steps.
541    fn schoolbook(a: Poly192, b: Poly192) -> Poly192 {
542        let (x, y) = (a.coefficients(), b.coefficients());
543
544        // The nine term products, placed by total degree.
545        let mut raw = [Poly64::ZERO; 5];
546        for (i, &xi) in x.iter().enumerate() {
547            for (j, &yj) in y.iter().enumerate() {
548                raw[i + j] += xi * yj;
549            }
550        }
551
552        // Reduce from the top down, rewriting each overflowing power once.
553        //
554        //     y^4  =  y^3 * y  =  (y + 1) * y  =  y^2 + y
555        //     y^3  =  y + 1
556        raw[2] += raw[4];
557        raw[1] += raw[4];
558        raw[1] += raw[3];
559        raw[0] += raw[3];
560
561        Poly192::new([raw[0], raw[1], raw[2]])
562    }
563
564    /// The inverse by Gaussian elimination on the multiplication matrix.
565    ///
566    /// The routine under test uses closed-form cofactors instead, so the two share no algebra.
567    fn inverse_by_elimination(a: Poly192) -> Option<Poly192> {
568        // Column `j` is where multiplication by the element sends basis vector `j`.
569        let columns: [Poly192; DEGREE] = core::array::from_fn(|j| a * basis_element(j));
570
571        // The augmented system, solving for the coordinates that multiply back to one.
572        let mut rows: [[Poly64; DEGREE + 1]; DEGREE] = core::array::from_fn(|i| {
573            core::array::from_fn(|j| {
574                if j < DEGREE {
575                    columns[j].coefficients()[i]
576                } else {
577                    Poly64::from_bool(i == 0)
578                }
579            })
580        });
581
582        for pivot in 0..DEGREE {
583            // Bring a row with a nonzero pivot entry into place, or report singularity.
584            let found = (pivot..DEGREE).find(|&r| !rows[r][pivot].is_zero())?;
585            rows.swap(pivot, found);
586
587            // Normalize the pivot row, then clear the column everywhere else.
588            let scale = rows[pivot][pivot].inverse();
589            for entry in &mut rows[pivot][pivot..] {
590                *entry *= scale;
591            }
592            let pivot_row = rows[pivot];
593            for (r, row) in rows.iter_mut().enumerate() {
594                if r == pivot || row[pivot].is_zero() {
595                    continue;
596                }
597                let factor = row[pivot];
598                for (entry, &above) in row[pivot..].iter_mut().zip(&pivot_row[pivot..]) {
599                    *entry += above * factor;
600                }
601            }
602        }
603
604        Some(Poly192::new(core::array::from_fn(|i| rows[i][DEGREE])))
605    }
606
607    #[test]
608    fn the_modulus_reduces_the_way_the_relation_says() {
609        let y = element([0, 1, 0]);
610
611        // y^2 is still a basis element.
612        assert_eq!(y * y, element([0, 0, 1]));
613
614        // y^3 = y + 1, which is the defining relation.
615        assert_eq!(y * y * y, element([1, 1, 0]));
616
617        // y^4 = y^2 + y, one more step of the same rewrite.
618        assert_eq!(y * y * y * y, element([0, 1, 1]));
619    }
620
621    #[test]
622    fn the_modulus_has_no_root_in_the_coefficient_field() {
623        // Invariant: a cubic over a finite field factors only if it has a root there.
624        //
625        // A root would generate GF(2^3), and 3 does not divide 64, so none exists.
626        //
627        // The argument is checked where a search can reach it.
628        //
629        // That is the 256 elements whose coordinates fit in a byte.
630        for c in 0..=u8::MAX {
631            let x = Poly64::new(u64::from(c));
632            assert_ne!(x * x * x + x + Poly64::ONE, Poly64::ZERO, "{c}");
633        }
634    }
635
636    #[test]
637    fn the_frobenius_fixes_the_coefficient_field_and_has_order_three() {
638        // Fixture state: the coefficient field embedded at the constant coordinate.
639        for bits in [0u64, 1, 0x1b, u64::MAX] {
640            let embedded = Poly192::from(Poly64::new(bits));
641            assert_eq!(embedded.frobenius(), embedded, "{bits:#x}");
642        }
643
644        // It moves everything outside that field, and cycles after three steps.
645        let y = element([0, 1, 0]);
646        assert_ne!(y.frobenius(), y);
647        assert_eq!(y.frobenius(), element([0, 0, 1]));
648        assert_eq!(y.frobenius().frobenius().frobenius(), y);
649    }
650
651    #[test]
652    fn zero_vectors_preserve_layout_and_support_growth() {
653        for len in [0, 1, 33, 1024] {
654            let mut values = Poly192::zero_vec(len);
655            assert_eq!(values.len(), len);
656            assert!(values.iter().all(|x| *x == Poly192::ZERO));
657            values.push(Poly192::ONE);
658            values.reserve(100);
659            assert_eq!(values.pop(), Some(Poly192::ONE));
660        }
661    }
662
663    proptest! {
664        #![proptest_config(ProptestConfig::with_cases(1000))]
665
666        #[test]
667        fn the_karatsuba_product_matches_the_schoolbook_one(a: [u64; 3], b: [u64; 3]) {
668            let (x, y) = (element(a), element(b));
669            prop_assert_eq!(x * y, schoolbook(x, y));
670        }
671
672        #[test]
673        fn the_square_matches_the_product_with_itself(a: [u64; 3]) {
674            let x = element(a);
675            prop_assert_eq!(x.square(), schoolbook(x, x));
676        }
677
678        #[test]
679        fn the_cofactor_inverse_matches_the_eliminated_one(a: [u64; 3]) {
680            let x = element(a);
681            prop_assert_eq!(x.try_inverse(), inverse_by_elimination(x));
682        }
683
684        #[test]
685        fn a_nonzero_element_times_its_inverse_is_one(a: [u64; 3]) {
686            let x = element(a);
687            match x.try_inverse() {
688                Some(inverse) => prop_assert_eq!(x * inverse, Poly192::ONE),
689                None => prop_assert_eq!(x, Poly192::ZERO),
690            }
691        }
692
693        #[test]
694        fn the_square_root_squares_back(a: [u64; 3]) {
695            let x = element(a);
696            let root = x.try_sqrt().expect("every element of a binary field is a square");
697            prop_assert_eq!(root.square(), x);
698        }
699
700        /// The Frobenius must be a ring homomorphism, not merely an additive one.
701        #[test]
702        fn the_frobenius_is_a_ring_homomorphism(a: [u64; 3], b: [u64; 3]) {
703            let (x, y) = (element(a), element(b));
704            prop_assert_eq!((x + y).frobenius(), x.frobenius() + y.frobenius());
705            prop_assert_eq!((x * y).frobenius(), x.frobenius() * y.frobenius());
706        }
707
708        /// Raising to the coefficient field's size must be what the coordinate shuffle does.
709        #[test]
710        fn the_frobenius_is_the_power_map_it_claims_to_be(a: [u64; 3]) {
711            let x = element(a);
712            prop_assert_eq!(x.frobenius(), x.exp_power_of_2(64));
713        }
714
715        /// Scaling by a coefficient must agree with embedding the scalar first.
716        #[test]
717        fn scaling_by_a_coefficient_agrees_with_the_full_product(a: [u64; 3], s: u64) {
718            let (x, scalar) = (element(a), Poly64::new(s));
719            prop_assert_eq!(x * scalar, x * Poly192::from(scalar));
720        }
721
722        #[test]
723        fn the_coefficients_round_trip(a: [u64; 3]) {
724            let x = element(a);
725            let coefficients = BasedVectorSpace::<Poly64>::as_basis_coefficients_slice(&x);
726            prop_assert_eq!(
727                <Poly192 as BasedVectorSpace<Poly64>>::from_basis_coefficients_slice(coefficients),
728                Some(x)
729            );
730            prop_assert_eq!(
731                ExtensionField::<Poly64>::is_in_basefield(&x),
732                a[1] == 0 && a[2] == 0
733            );
734        }
735    }
736}