mod basis;
mod gf64;
mod powers;
mod sqrt;
pub(crate) use gf64::{poly_dot_64, poly_inverse_64, poly_mul_64, poly_sqrt_64, poly_square_64};
pub(crate) use powers::poly_dot_powers_128;
pub(crate) use sqrt::poly_sqrt_128;
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
mod inverse;
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
pub(crate) use inverse::poly_inverse_128;
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
pub(crate) use x86_64::poly_mul_192;
#[cfg_attr(
any(
all(target_arch = "x86_64", target_feature = "pclmulqdq"),
all(target_arch = "aarch64", target_feature = "aes"),
),
allow(dead_code)
)]
mod portable;
#[cfg(any(
test,
all(
target_arch = "x86_64",
target_feature = "vpclmulqdq",
any(target_feature = "avx2", target_feature = "avx512f")
)
))]
pub(crate) use basis::TAIL_128;
pub(crate) use basis::{
poly_to_tower_64, poly_to_tower_128, poly_to_tower_128_slice, tower_image_64, tower_image_128,
tower_to_poly_64, tower_to_poly_128, tower_to_poly_128_slice, try_map_tower_coordinates_into,
};
use crate::BinaryField64;
use crate::tower::TowerLevel;
#[cfg(all(target_arch = "aarch64", target_feature = "aes"))]
mod aarch64;
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
mod x86_64;
pub(crate) const HAS_HARDWARE_CLMUL: bool = cfg!(any(
all(target_arch = "x86_64", target_feature = "pclmulqdq"),
all(target_arch = "aarch64", target_feature = "aes"),
));
#[cfg(test)]
#[inline]
const fn scalar_clmul_64x64(a: u64, b: u64) -> u128 {
let a = a as u128;
let mut acc = 0u128;
let mut i = 0;
while i < 64 {
let selected = 0u128.wrapping_sub(((b >> i) & 1) as u128);
acc ^= (a << i) & selected;
i += 1;
}
acc
}
#[cfg(all(target_arch = "aarch64", target_feature = "aes"))]
use aarch64::clmul_64x64;
#[cfg(not(any(
all(target_arch = "x86_64", target_feature = "pclmulqdq"),
all(target_arch = "aarch64", target_feature = "aes"),
)))]
use portable::clmul_64x64;
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
use x86_64::clmul_64x64;
#[allow(clippy::missing_const_for_fn)]
#[inline]
fn clmul_128x128(a: u128, b: u128) -> (u128, u128) {
let (a0, a1) = (a as u64, (a >> 64) as u64);
let (b0, b1) = (b as u64, (b >> 64) as u64);
let low = clmul_64x64(a0, b0);
let high = clmul_64x64(a1, b1);
let middle = clmul_64x64(a0, b1) ^ clmul_64x64(a1, b0);
(low ^ (middle << 64), high ^ (middle >> 64))
}
#[inline]
const fn reduce_64(product: u128) -> u64 {
let low = product as u64;
let high = (product >> 64) as u64;
let folded = (high << 4) ^ (high << 3) ^ (high << 1) ^ high;
let spill = (high >> 60) ^ (high >> 61) ^ (high >> 63);
low ^ folded ^ ((spill << 4) ^ (spill << 3) ^ (spill << 1) ^ spill)
}
#[inline]
const fn reduce_128(low: u128, high: u128) -> u128 {
let folded = (high << 7) ^ (high << 2) ^ (high << 1) ^ high;
let spill = (high >> 121) ^ (high >> 126) ^ (high >> 127);
low ^ folded ^ ((spill << 7) ^ (spill << 2) ^ (spill << 1) ^ spill)
}
#[inline]
pub(crate) fn mul_64(a: u64, b: u64) -> u64 {
let product = clmul_64x64(basis::tower_to_poly_64(a), basis::tower_to_poly_64(b));
basis::poly_to_tower_64(reduce_64(product))
}
#[inline]
pub(crate) fn mul_pair_64(a: u64, b: u64, scalar: u64) -> (u64, u64) {
let scalar = basis::tower_to_poly_64(scalar);
let product = |value| {
basis::poly_to_tower_64(reduce_64(clmul_64x64(
basis::tower_to_poly_64(value),
scalar,
)))
};
(product(a), product(b))
}
#[inline]
fn composed_poly_mul_128(a: u128, b: u128) -> u128 {
let (low, high) = clmul_128x128(a, b);
reduce_128(low, high)
}
#[cfg(all(target_arch = "aarch64", target_feature = "aes"))]
pub(crate) use aarch64::{poly_dot_128, poly_mul_128, poly_mul_128_by_64, poly_square_128};
#[cfg(not(any(
all(target_arch = "x86_64", target_feature = "pclmulqdq"),
all(target_arch = "aarch64", target_feature = "aes"),
)))]
pub(crate) use portable::{poly_dot_128, poly_mul_128, poly_mul_128_by_64, poly_square_128};
#[cfg(all(target_arch = "x86_64", target_feature = "pclmulqdq"))]
pub(crate) use x86_64::{poly_dot_128, poly_mul_128, poly_mul_128_by_64, poly_square_128};
#[inline]
#[allow(clippy::missing_const_for_fn)]
pub(crate) fn poly_mul_128_batch(a: u128, b: u128) -> u128 {
#[cfg(all(target_arch = "aarch64", target_feature = "aes"))]
{
aarch64::poly_mul_128_batch(a, b)
}
#[cfg(not(all(target_arch = "aarch64", target_feature = "aes")))]
{
poly_mul_128(a, b)
}
}
#[inline]
pub(crate) fn mul_128(a: u128, b: u128) -> u128 {
let product = composed_poly_mul_128(basis::tower_to_poly_128(a), basis::tower_to_poly_128(b));
basis::poly_to_tower_128(product)
}
#[inline]
pub(crate) fn dot_product_32(pairs: impl Iterator<Item = (u32, u32)>) -> u32 {
let product = pairs.fold(0u64, |sum, (a, b)| {
sum ^ clmul_64x64(
basis::tower_to_poly_32(a) as u64,
basis::tower_to_poly_32(b) as u64,
) as u64
});
let fold = clmul_64x64(product >> 32, basis::TAIL_32 as u64) as u64;
let spill = clmul_64x64(fold >> 32, basis::TAIL_32 as u64) as u32;
basis::poly_to_tower_32(product as u32 ^ fold as u32 ^ spill)
}
#[inline]
pub(crate) fn dot_product_64(pairs: impl Iterator<Item = (u64, u64)>) -> u64 {
let product = pairs.fold(0, |sum, (a, b)| {
sum ^ clmul_64x64(basis::tower_to_poly_64(a), basis::tower_to_poly_64(b))
});
basis::poly_to_tower_64(reduce_64(product))
}
#[inline]
pub(crate) fn dot_product_128(pairs: impl Iterator<Item = (u128, u128)>) -> u128 {
let (low, high) = pairs.fold((0, 0), |(low, high), (a, b)| {
let (lo, hi) = clmul_128x128(basis::tower_to_poly_128(a), basis::tower_to_poly_128(b));
(low ^ lo, high ^ hi)
});
basis::poly_to_tower_128(reduce_128(low, high))
}
#[inline]
pub(crate) fn square_64(a: u64) -> u64 {
basis::tower_square_64(a)
}
#[inline]
pub(crate) fn square_128(a: u128) -> u128 {
let low = square_64(a as u64);
let high = square_64((a >> 64) as u64);
let scaled = BinaryField64::from_repr(high).mul_alpha().to_repr();
u128::from(low ^ high) | (u128::from(scaled) << 64)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::basis::{TAIL_64, TAIL_128, poly_mul};
use crate::tower::TowerLevel;
use crate::{BinaryField32, BinaryField64, BinaryField128};
fn square_64_through_the_polynomial_basis(a: u64) -> u64 {
let poly = super::basis::tower_to_poly_64(a);
super::basis::poly_to_tower_64(super::reduce_64(super::clmul_64x64(poly, poly)))
}
fn square_128_through_the_polynomial_basis(a: u128) -> u128 {
super::basis::poly_to_tower_128(super::poly_square_128(super::basis::tower_to_poly_128(a)))
}
fn vector_kernel_shape(a: u128, b: u128) -> u128 {
let ext = |x: u128, y: u128| (x >> 64) | (y << 64);
let clmul_low = |x: u128, y: u128| super::clmul_64x64(x as u64, y as u64);
let clmul_high = |x: u128, y: u128| super::clmul_64x64((x >> 64) as u64, (y >> 64) as u64);
let tail = (TAIL_128 as u64 as u128) | ((TAIL_128 as u64 as u128) << 64);
let swapped = ext(b, b);
let middle = clmul_low(a, swapped) ^ clmul_high(a, swapped);
let low = clmul_low(a, b) ^ ext(0, middle);
let high = clmul_high(a, b) ^ ext(middle, 0);
let folded = clmul_high(high, tail);
let spill = ext(folded, 0);
let carried = ext(0, folded);
low ^ clmul_low(high ^ spill, tail) ^ carried
}
const KERNEL_CORNERS: [u128; 10] = [
0,
1,
u128::MAX,
1 << 127,
1 << 64,
(1u128 << 64) - 1,
u128::MAX << 64,
0x8000_0000_0000_0001_8000_0000_0000_0001,
1 << 121,
TAIL_128,
];
#[test]
fn the_vector_kernel_algebra_matches_the_composition_on_extremes() {
for a in KERNEL_CORNERS {
for b in KERNEL_CORNERS {
assert_eq!(
vector_kernel_shape(a, b),
super::composed_poly_mul_128(a, b),
"{a:#x} * {b:#x}"
);
}
}
}
#[test]
fn the_squaring_tables_match_the_polynomial_basis_route_on_the_basis() {
for i in 0..64 {
let a = 1u64 << i;
assert_eq!(
super::square_64(a),
square_64_through_the_polynomial_basis(a),
"gf64 basis vector {i}"
);
}
for i in 0..128 {
let a = 1u128 << i;
assert_eq!(
super::square_128(a),
square_128_through_the_polynomial_basis(a),
"gf128 basis vector {i}"
);
}
for i in 0..64 {
for j in 0..64 {
let (a, b) = (1u64 << i, 1u64 << j);
assert_eq!(
super::square_64(a ^ b),
super::square_64(a) ^ super::square_64(b),
"gf64 {i},{j}"
);
}
}
for i in 0..128 {
for j in 0..128 {
let (a, b) = (1u128 << i, 1u128 << j);
assert_eq!(
super::square_128(a ^ b),
super::square_128(a) ^ super::square_128(b),
"gf128 {i},{j}"
);
}
}
}
fn scalar_clmul_128x128(a: u128, b: u128) -> (u128, u128) {
let mut low = 0u128;
let mut high = 0u128;
for i in 0..128 {
if (b >> i) & 1 == 1 {
low ^= a << i;
high ^= if i == 0 { 0 } else { a >> (128 - i) };
}
}
(low, high)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2000))]
#[test]
fn clmul_matches_reference(a: u128, b: u128) {
let x = BinaryField128::from_repr(a);
let y = BinaryField128::from_repr(b);
prop_assert_eq!(super::mul_128(a, b), x.reference_mul(y).to_repr());
prop_assert_eq!(super::poly_mul_128_batch(a, b), poly_mul(a, b, 128, TAIL_128));
}
#[test]
fn narrow_dot_product_matches_reference(a: u32, b: u32, c: u32, d: u32) {
let expected = BinaryField32::from_repr(a).reference_mul(BinaryField32::from_repr(b))
+ BinaryField32::from_repr(c).reference_mul(BinaryField32::from_repr(d));
prop_assert_eq!(super::dot_product_32([(a, b), (c, d)].into_iter()), expected.to_repr());
}
#[test]
fn clmul_64_matches_reference(a: u64, b: u64) {
let x = BinaryField64::from_repr(a);
let y = BinaryField64::from_repr(b);
prop_assert_eq!(super::mul_64(a, b), x.reference_mul(y).to_repr());
}
#[test]
fn clmul_square_matches_reference(a: u128) {
let x = BinaryField128::from_repr(a);
prop_assert_eq!(super::square_128(a), x.reference_mul(x).to_repr());
}
#[test]
fn clmul_64_square_matches_reference(a: u64) {
let x = BinaryField64::from_repr(a);
prop_assert_eq!(super::square_64(a), x.reference_mul(x).to_repr());
}
#[test]
fn the_squaring_tables_agree_with_the_polynomial_basis_route(a: u64, b: u128) {
prop_assert_eq!(super::square_64(a), square_64_through_the_polynomial_basis(a));
prop_assert_eq!(
super::square_128(b),
square_128_through_the_polynomial_basis(b),
);
}
#[test]
fn the_dispatched_carryless_product_matches_the_bit_serial_one(a: u64, b: u64) {
prop_assert_eq!(super::clmul_64x64(a, b), super::scalar_clmul_64x64(a, b));
}
#[test]
fn the_vector_kernel_algebra_matches_the_composition(a: u128, b: u128) {
prop_assert_eq!(vector_kernel_shape(a, b), super::composed_poly_mul_128(a, b));
}
#[test]
fn the_wide_product_matches_the_bit_serial_one(a: u128, b: u128) {
prop_assert_eq!(super::clmul_128x128(a, b), scalar_clmul_128x128(a, b));
}
#[test]
fn the_reduction_agrees_with_bit_serial_modular_multiplication(a: u128, b: u128) {
let (low, high) = scalar_clmul_128x128(a, b);
prop_assert_eq!(super::reduce_128(low, high), poly_mul(a, b, 128, TAIL_128));
let product = super::scalar_clmul_64x64(a as u64, b as u64);
prop_assert_eq!(
u128::from(super::reduce_64(product)),
poly_mul(u128::from(a as u64), u128::from(b as u64), 64, TAIL_64),
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(20_000))]
#[test]
fn the_selected_backend_matches_multiplication_from_the_modulus(a: u128, b: u128) {
prop_assert_eq!(super::poly_mul_128(a, b), poly_mul(a, b, 128, TAIL_128));
prop_assert_eq!(super::poly_square_128(a), poly_mul(a, a, 128, TAIL_128));
prop_assert_eq!(super::poly_mul_128_by_64(a, b as u64), poly_mul(a, b as u64 as u128, 128, TAIL_128));
}
}
#[test]
fn the_selected_backend_is_exact_on_the_extremes() {
const CORNERS: [u128; 8] = [
0,
1,
u128::MAX,
1 << 127,
1 << 64,
(1u128 << 64) - 1,
u128::MAX << 64,
0x8000_0000_0000_0001_8000_0000_0000_0001,
];
for a in CORNERS {
assert_eq!(
super::poly_square_128(a),
poly_mul(a, a, 128, TAIL_128),
"{a:#x} squared"
);
for b in CORNERS {
assert_eq!(
super::poly_mul_128(a, b),
poly_mul(a, b, 128, TAIL_128),
"{a:#x} * {b:#x}"
);
}
}
}
#[test]
fn hand_checkable_products() {
assert_eq!(super::mul_128(0b10, 0b10), 0b11);
assert_eq!(super::mul_64(0b10, 0b10), 0b11);
assert_eq!(super::mul_128(0, 0xdead_beef), 0);
assert_eq!(super::mul_64(0, 0xdead_beef), 0);
let a = 0x0123_4567_89ab_cdef_fedc_ba98_7654_3210u128;
assert_eq!(super::mul_128(a, 1), a);
assert_eq!(super::mul_128(1, a), a);
assert_eq!(super::mul_64(a as u64, 1), a as u64);
assert_eq!(super::mul_64(1, a as u64), a as u64);
}
}