#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FieldElement(pub(crate) [u64; 3]);
impl core::ops::Add for FieldElement {
type Output = Self;
fn add(self, other: Self) -> Self {
FieldElement([
self.0[0] ^ other.0[0],
self.0[1] ^ other.0[1],
self.0[2] ^ other.0[2],
])
}
}
impl FieldElement {
pub const ZERO: FieldElement = FieldElement([0, 0, 0]);
pub const ONE: FieldElement = FieldElement([1, 0, 0]);
#[must_use]
pub fn from_be_bytes(bytes: &[u8]) -> Self {
let mut limbs = [0u64; 3];
for (i, &byte) in bytes.iter().rev().enumerate() {
let limb = i / 8;
let shift = (i % 8) * 8;
limbs[limb] |= u64::from(byte) << shift;
}
FieldElement(limbs)
}
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn to_be_bytes(self) -> [u8; 21] {
let mut out = [0u8; 21];
for (i, byte) in out.iter_mut().rev().enumerate() {
let limb = i / 8;
let shift = (i % 8) * 8;
*byte = (self.0[limb] >> shift) as u8;
}
out
}
#[must_use]
pub fn multiply(self, other: Self) -> Self {
#[cfg(all(
feature = "std",
not(kani),
any(target_arch = "x86_64", target_arch = "aarch64")
))]
if crate::hazmat::gf2m_wide::clmul_native::feature_available() {
let wide = unsafe { poly_mul_wide_hw(&self.0, &other.0) };
return reduce(wide);
}
reduce(poly_mul_wide(&self.0, &other.0))
}
#[must_use]
pub fn square(self) -> Self {
reduce(square_wide(&self.0))
}
#[must_use]
#[allow(clippy::similar_names)] pub fn invert(self) -> Self {
let sq_n = |mut x: Self, n: u32| -> Self {
for _ in 0..n {
x = x.square();
}
x
};
let t1 = self; let t2 = t1.square().multiply(t1); let t3 = sq_n(t2, 1).multiply(t1); let t6 = sq_n(t3, 3).multiply(t3); let t12 = sq_n(t6, 6).multiply(t6); let t24 = sq_n(t12, 12).multiply(t12); let t27 = sq_n(t24, 3).multiply(t3); let t54 = sq_n(t27, 27).multiply(t27); let t81 = sq_n(t54, 27).multiply(t27); let t162 = sq_n(t81, 81).multiply(t81);
t162.square() }
}
fn poly_mul_wide(a: &[u64; 3], b: &[u64; 3]) -> [u64; 6] {
let mut acc = [0u64; 6];
let mut shifted = [b[0], b[1], b[2], 0u64, 0u64, 0u64];
for bit_index in 0..163u32 {
let limb = (bit_index / 64) as usize;
let bit = bit_index % 64;
let bit_value = (a[limb] >> bit) & 1;
let mask = 0u64.wrapping_sub(bit_value); for i in 0..6 {
acc[i] ^= shifted[i] & mask;
}
shl1(&mut shifted);
}
acc
}
#[cfg(all(feature = "std", target_arch = "x86_64"))]
#[target_feature(enable = "pclmulqdq")]
#[allow(
clippy::cast_possible_wrap,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
unsafe fn poly_mul_wide_hw(a: &[u64; 3], b: &[u64; 3]) -> [u64; 6] {
use std::arch::x86_64::{
_mm_clmulepi64_si128, _mm_cvtsi128_si64, _mm_set_epi64x, _mm_srli_si128,
};
let mut out = [0u64; 6];
for i in 0..3 {
for j in 0..3 {
let ma = _mm_set_epi64x(0, a[i] as i64);
let mb = _mm_set_epi64x(0, b[j] as i64);
let prod = _mm_clmulepi64_si128(ma, mb, 0x00);
let lo = _mm_cvtsi128_si64(prod) as u64;
let hi = _mm_cvtsi128_si64(_mm_srli_si128::<8>(prod)) as u64;
out[i + j] ^= lo;
out[i + j + 1] ^= hi;
}
}
out
}
#[cfg(all(feature = "std", target_arch = "aarch64"))]
#[target_feature(enable = "aes")]
unsafe fn poly_mul_wide_hw(a: &[u64; 3], b: &[u64; 3]) -> [u64; 6] {
use std::arch::aarch64::vmull_p64;
let mut out = [0u64; 6];
for i in 0..3 {
for j in 0..3 {
let prod: u128 = vmull_p64(a[i], b[j]);
out[i + j] ^= prod as u64;
out[i + j + 1] ^= (prod >> 64) as u64;
}
}
out
}
#[cfg(test)]
fn multiply_sw(a: FieldElement, b: FieldElement) -> FieldElement {
reduce(poly_mul_wide(&a.0, &b.0))
}
fn spread32to64(x: u32) -> u64 {
let mut x = u64::from(x);
x = (x | (x << 16)) & 0x0000_FFFF_0000_FFFF;
x = (x | (x << 8)) & 0x00FF_00FF_00FF_00FF;
x = (x | (x << 4)) & 0x0F0F_0F0F_0F0F_0F0F;
x = (x | (x << 2)) & 0x3333_3333_3333_3333;
x = (x | (x << 1)) & 0x5555_5555_5555_5555;
x
}
fn square_wide(a: &[u64; 3]) -> [u64; 6] {
let mut out = [0u64; 6];
for i in 0..3 {
#[allow(clippy::cast_possible_truncation)]
let lo = a[i] as u32;
#[allow(clippy::cast_possible_truncation)]
let hi = (a[i] >> 32) as u32;
out[2 * i] = spread32to64(lo);
out[2 * i + 1] = spread32to64(hi);
}
out
}
fn shl1(x: &mut [u64; 6]) {
let mut carry = 0u64;
for limb in x.iter_mut() {
let next_carry = *limb >> 63;
*limb = (*limb << 1) | carry;
carry = next_carry;
}
}
fn reduce(mut c: [u64; 6]) -> FieldElement {
for j in (3..=5).rev() {
let zz = c[j];
c[j] = 0;
c[j - 2] ^= (zz >> 28) ^ (zz >> 29) ^ (zz >> 32) ^ (zz >> 35);
c[j - 3] ^= (zz << 36) ^ (zz << 35) ^ (zz << 32) ^ (zz << 29);
}
for _ in 0..2 {
let overflow = c[2] >> 35;
c[2] = (c[2] << 29) >> 29;
c[0] ^= overflow ^ (overflow << 3) ^ (overflow << 6) ^ (overflow << 7);
}
FieldElement([c[0], c[1], c[2]])
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn spread32to64_places_each_bit_at_double_position() {
for bit in 0..32u32 {
let x = 1u32 << bit;
assert_eq!(spread32to64(x), 1u64 << (2 * bit), "bit {bit}");
}
}
#[test]
fn spread32to64_of_zero_and_all_ones() {
assert_eq!(spread32to64(0), 0);
assert_eq!(spread32to64(u32::MAX), 0x5555_5555_5555_5555);
}
#[test]
fn square_wide_matches_multiply_wide_at_limb_boundaries() {
for bit in [0u32, 1, 63, 64, 65, 127, 128, 129, 162] {
let limb = (bit / 64) as usize;
let shift = bit % 64;
let mut a = [0u64; 3];
a[limb] = 1u64 << shift;
assert_eq!(square_wide(&a), poly_mul_wide(&a, &a), "bit {bit}");
}
}
#[test]
fn square_wide_matches_multiply_wide_for_all_bits_set() {
let a = [u64::MAX, u64::MAX, (1u64 << 35) - 1];
assert_eq!(square_wide(&a), poly_mul_wide(&a, &a));
}
fn invert_direct(a: FieldElement) -> FieldElement {
let mut result = FieldElement::ONE;
for _ in 0..162 {
result = result.square();
result = result.multiply(a);
}
result.square()
}
proptest! {
#[test]
fn invert_matches_invert_direct(bytes in prop::collection::vec(any::<u8>(), 21)) {
let mut arr = [0u8; 21];
arr.copy_from_slice(&bytes);
arr[0] &= 0x07; let a = FieldElement::from_be_bytes(&arr);
prop_assume!(a != FieldElement::ZERO);
prop_assert_eq!(a.invert(), invert_direct(a));
}
}
proptest! {
#[test]
fn multiply_matches_explicit_software_path(
a_bytes in prop::collection::vec(any::<u8>(), 21),
b_bytes in prop::collection::vec(any::<u8>(), 21),
) {
let mut a_arr = [0u8; 21];
a_arr.copy_from_slice(&a_bytes);
a_arr[0] &= 0x07;
let mut b_arr = [0u8; 21];
b_arr.copy_from_slice(&b_bytes);
b_arr[0] &= 0x07;
let a = FieldElement::from_be_bytes(&a_arr);
let b = FieldElement::from_be_bytes(&b_arr);
prop_assert_eq!(a.multiply(b), multiply_sw(a, b));
}
}
#[test]
fn invert_matches_invert_direct_at_edge_values() {
assert_eq!(FieldElement::ONE.invert(), invert_direct(FieldElement::ONE));
let mut top_bit = [0u64; 3];
top_bit[2] = 1u64 << 34; let a = FieldElement(top_bit);
assert_eq!(a.invert(), invert_direct(a));
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
fn naive_reduce(mut c: [u64; 6]) -> FieldElement {
for degree in (163u32..384).rev() {
let limb = (degree / 64) as usize;
let bit = degree % 64;
if (c[limb] >> bit) & 1 == 1 {
c[limb] ^= 1u64 << bit;
let shift = degree - 163;
for term in [7u32, 6, 3, 0] {
let d = shift + term;
let l = (d / 64) as usize;
let b = d % 64;
c[l] ^= 1u64 << b;
}
}
}
let mut out = [0u64; 3];
out.copy_from_slice(&c[..3]);
FieldElement(out)
}
#[kani::proof]
fn reduce_output_is_fully_reduced() {
let c: [u64; 6] = kani::any();
let r = reduce(c);
assert_eq!(r.0[2] >> 35, 0);
}
#[kani::proof]
fn reduce_matches_naive_bit_loop() {
let c: [u64; 6] = kani::any();
assert_eq!(reduce(c), naive_reduce(c));
}
#[kani::proof]
fn spread32to64_is_exact_bit_doubling() {
let x: u32 = kani::any();
let r = spread32to64(x);
for i in 0..32u32 {
let bit = u64::from((x >> i) & 1);
let placed = (r >> (2 * i)) & 1;
assert_eq!(placed, bit);
let odd = (r >> (2 * i + 1)) & 1;
assert_eq!(odd, 0);
}
}
}
#[cfg(all(test, any(target_arch = "x86_64", target_arch = "aarch64")))]
mod clmul_spike {
use super::{multiply_sw, poly_mul_wide, FieldElement};
use crate::hazmat::gf2m_wide::clmul_native::{clmul64, feature_available};
use proptest::prelude::*;
fn schoolbook_clmul_poly_mul_wide(a: &[u64; 3], b: &[u64; 3]) -> [u64; 6] {
let mut out = [0u64; 6];
for i in 0..3 {
for j in 0..3 {
let (lo, hi) = unsafe { clmul64(a[i], b[j]) };
out[i + j] ^= lo;
out[i + j + 1] ^= hi;
}
}
out
}
fn arb_narrow() -> impl Strategy<Value = [u64; 3]> {
prop::collection::vec(any::<u64>(), 3).prop_map(|v| {
let mut out = [0u64; 3];
out.copy_from_slice(&v);
out[2] &= (1u64 << 35) - 1;
out
})
}
proptest! {
#[test]
fn clmul_poly_mul_wide_matches_software_reference(a in arb_narrow(), b in arb_narrow()) {
if !feature_available() {
return Ok(());
}
let hw = schoolbook_clmul_poly_mul_wide(&a, &b);
let sw = poly_mul_wide(&a, &b);
prop_assert_eq!(hw, sw);
}
#[test]
fn poly_mul_wide_hw_matches_software_reference(a in arb_narrow(), b in arb_narrow()) {
if !feature_available() {
return Ok(());
}
let hw = unsafe { super::poly_mul_wide_hw(&a, &b) };
let sw = poly_mul_wide(&a, &b);
prop_assert_eq!(hw, sw);
}
}
#[test]
#[ignore]
fn isolated_timing_dispatch_vs_explicit_software_multiply() {
if !feature_available() {
eprintln!("gf2m163: hardware clmul feature not available on this CPU, skipping");
return;
}
use std::hint::black_box;
use std::time::Instant;
const N: u32 = 2_000_000;
let a = FieldElement([
0x1111_1111_1111_1111u64,
0x2222_2222_2222_2222,
0x3333_3333_3333,
]);
let b = FieldElement([
0x5555_5555_5555_5555u64,
0x6666_6666_6666_6666,
0x7777_7777_7777,
]);
let start = Instant::now();
let mut acc = a;
for _ in 0..N {
acc = black_box(multiply_sw(acc, black_box(b)));
}
let sw_elapsed = start.elapsed();
black_box(acc);
let start = Instant::now();
let mut acc = a;
for _ in 0..N {
acc = black_box(acc.multiply(black_box(b)));
}
let hw_elapsed = start.elapsed();
black_box(acc);
let sw_ns = sw_elapsed.as_nanos() as f64 / f64::from(N);
let hw_ns = hw_elapsed.as_nanos() as f64 / f64::from(N);
eprintln!(
"gf2m163: explicit software multiply = {sw_ns:.1} ns/op | multiply() (hw dispatch) = {hw_ns:.1} ns/op | speedup = {:.2}x",
sw_ns / hw_ns
);
}
}