Skip to main content

binius_field/
ghash_sq.rs

1// Copyright 2026 The Binius Developers
2
3//! Binary field implementation of GF(2^256) as a degree-two extension of the GHASH field.
4//!
5//! Elements are pairs `(a, b)` representing `a + b·Y`, where `a` and `b` are elements of
6//! [`BinaryField128bGhash`]. The extension is defined by the irreducible polynomial
7//! `Y² + X·Y + X` over GHASH, so that `Y² = X·Y + X`.
8//!
9//! The field is backed by [`M256`], with the low 128 bits holding the coefficient of `1` (`a`) and
10//! the high 128 bits holding the coefficient of `Y` (`b`). This is the same layout as
11//! [`PackedBinaryGhash2x128b`] (two GHASH lanes in an `M256`) and matches the `{1, Y}` basis used
12//! by the `ExtensionField<BinaryField128bGhash>` implementation.
13//!
14//! Reducing with `Y² = X·Y + X` multiplies by `X` (a left shift) rather than by `X⁻¹`, and the
15//! multiply-by-`X` folds into the reduction. Multiplication batches the two GHASH products that
16//! share the AVX2 256-bit CLMUL into a single [`PackedBinaryGhash2x128b`] multiply (the
17//! `mul_m256i_hybrid` algorithm).
18
19use std::{
20	fmt::{Debug, Display, Formatter},
21	iter::{Product, Sum},
22	ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign},
23};
24
25use binius_utils::{
26	DeserializeBytes, FixedSizeSerializeBytes, SerializationError, SerializeBytes,
27	bytes::{Buf, BufMut},
28};
29use bytemuck::{Pod, Zeroable};
30
31use super::{
32	binary_field::{BinaryField, BinaryField1b, binary_field, impl_field_extension},
33	extension::ExtensionField,
34};
35use crate::{
36	BinaryField128bGhash, Divisible, Field, PackedBinaryGhash2x128b, PackedField,
37	arch::{M128, M256, m256_from_u128s},
38	mul_by_binary_field_1b,
39	underlier::U1,
40};
41
42// The multiplicative generator is `Y` (low 128 bits = 0, high 128 bits = 1).
43// `tests::test_multiplicative_generator` verifies it generates GF(2^256)* against the known
44// factorization of 2^256 - 1.
45//
46// The field's `Mul`/`Square`/`InvertOrZero`/`WideMul` are the width-one packing's arithmetic,
47// derived by the `binary_field!` macro from [`PackedGhashSq1x256b`] (`PackedPrimitiveType<M256,
48// GhashSq256b>`), whose implementation lives in `packed_ghash_sq`.
49binary_field!(pub GhashSq256b(M256), m256_from_u128s(0, 1));
50
51unsafe impl Pod for GhashSq256b {}
52
53impl GhashSq256b {
54	/// Splits the element into its `(a, b)` coefficients over GHASH, where `self = a + b·Y`.
55	#[inline]
56	fn to_coeffs(self) -> [BinaryField128bGhash; 2] {
57		// `GhashSq256b` and `PackedBinaryGhash2x128b` share the `M256` underlier and lane layout
58		// (low lane = coefficient of `1`, high lane = coefficient of `Y`), so this reinterprets.
59		let packed = PackedBinaryGhash2x128b::from_underlier(self.0);
60		[packed.get(0), packed.get(1)]
61	}
62
63	/// Builds an element from its `(a, b)` coefficients over GHASH, so that `self = a + b·Y`.
64	#[inline]
65	fn from_coeffs(coeffs: [BinaryField128bGhash; 2]) -> Self {
66		Self(PackedBinaryGhash2x128b::from_scalars(coeffs).to_underlier())
67	}
68}
69
70// Degree-two extension over GHASH: the low 128 bits are the coefficient of `1`, the high 128 bits
71// the coefficient of `Y`. `square_transpose` uses the packed fast path via
72// `PackedBinaryGhash2x128b`.
73impl_field_extension!(BinaryField128bGhash(M128) < @1 => GhashSq256b(M256));
74
75// Extension over GF(2): the 256 underlier bits are the coordinates in the `BinaryField1b` basis.
76impl_field_extension!(BinaryField1b(U1) < @8 => GhashSq256b(M256));
77
78mul_by_binary_field_1b!(GhashSq256b);
79
80// Scalar multiplication by a GHASH subfield element scales both extension coordinates.
81impl Mul<BinaryField128bGhash> for GhashSq256b {
82	type Output = Self;
83
84	#[inline]
85	fn mul(self, rhs: BinaryField128bGhash) -> Self::Output {
86		let [a, b] = self.to_coeffs();
87		Self::from_coeffs([a * rhs, b * rhs])
88	}
89}
90
91impl SerializeBytes for GhashSq256b {
92	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
93		self.0.serialize(write_buf)
94	}
95}
96
97impl DeserializeBytes for GhashSq256b {
98	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
99	where
100		Self: Sized,
101	{
102		Ok(Self(DeserializeBytes::deserialize(read_buf)?))
103	}
104}
105
106impl FixedSizeSerializeBytes for GhashSq256b {
107	const BYTE_SIZE: usize = 32;
108}
109
110#[cfg(test)]
111mod tests {
112	use proptest::prelude::*;
113
114	use super::*;
115	use crate::arithmetic_traits::{InvertOrZero, Square, WideMul};
116
117	/// `X`, the generator of the GHASH field in the standard polynomial basis.
118	const GHASH_X: u128 = 0x02;
119
120	/// Prime factorization of `2²⁵⁶ - 1` (the multiplicative group order). These are the
121	/// Fermat-number factors `F₀..F₇`: `2²⁵⁶ - 1 = ∏_{k=0}^{7} (2^{2^k} + 1)`.
122	const ORDER_PRIME_FACTORS: [u128; 11] = [
123		3,
124		5,
125		17,
126		257,
127		65537,
128		641,
129		6700417,
130		274177,
131		67280421310721,
132		59649589127497217,
133		5704689200685129054721,
134	];
135
136	fn ghash_sq(a: u128, b: u128) -> GhashSq256b {
137		GhashSq256b::from_coeffs([BinaryField128bGhash::new(a), BinaryField128bGhash::new(b)])
138	}
139
140	fn arb_elem() -> impl Strategy<Value = GhashSq256b> {
141		any::<[u128; 2]>().prop_map(|[a, b]| ghash_sq(a, b))
142	}
143
144	/// Independent reference for [`ExtensionField::square_transpose`]: transposes the
145	/// `DEGREE × DEGREE` matrix whose row `i` is the `F`-basis expansion of `values[i]`. Built only
146	/// from the (separately tested) `iter_bases`/`from_bases` accessors, to check the packed
147	/// `square_transpose` fast path.
148	fn naive_square_transpose<F: BinaryField>(values: &[GhashSq256b]) -> Vec<GhashSq256b>
149	where
150		GhashSq256b: ExtensionField<F>,
151	{
152		let degree = <GhashSq256b as ExtensionField<F>>::DEGREE;
153		assert_eq!(values.len(), degree);
154		let coords: Vec<F> = values
155			.iter()
156			.flat_map(|v| ExtensionField::<F>::iter_bases(v))
157			.collect();
158		(0..degree)
159			.map(|i| {
160				<GhashSq256b as ExtensionField<F>>::from_bases(
161					(0..degree).map(|j| coords[j * degree + i]),
162				)
163			})
164			.collect()
165	}
166
167	/// Computes `(2²⁵⁶ - 1) / p` as little-endian `u64` limbs via bit-by-bit long division. The
168	/// remainder stays below `p` (≤ 73 bits), so it fits in a `u128`.
169	fn order_cofactor(p: u128) -> [u64; 4] {
170		let mut quotient = [0u64; 4];
171		let mut rem: u128 = 0;
172		// The dividend `2²⁵⁶ - 1` is 256 set bits, processed most-significant first.
173		for bit in (0..256).rev() {
174			rem = (rem << 1) | 1;
175			let q_bit = if rem >= p {
176				rem -= p;
177				1u64
178			} else {
179				0
180			};
181			quotient[bit / 64] |= q_bit << (bit % 64);
182		}
183		quotient
184	}
185
186	/// A nonzero element generates the full multiplicative group iff `g^((2²⁵⁶-1)/p) ≠ 1` for
187	/// every prime `p` dividing the group order.
188	fn is_generator(g: GhashSq256b) -> bool {
189		ORDER_PRIME_FACTORS
190			.iter()
191			.all(|&p| Field::pow(&g, order_cofactor(p)) != GhashSq256b::ONE)
192	}
193
194	#[test]
195	fn test_quadratic_relation() {
196		// The extension is defined by `Y² + X·Y + X = 0`, i.e. `Y² = X·Y + X`, whose coordinates in
197		// the `{1, Y}` basis are `(X, X)`.
198		let y = ghash_sq(0, 1);
199		assert_eq!(y * y, ghash_sq(GHASH_X, GHASH_X));
200	}
201
202	#[test]
203	fn test_subfield_embedding() {
204		// Products of GHASH-subfield elements agree with GHASH multiplication.
205		let a = BinaryField128bGhash::new(0x0123456789abcdef0123456789abcdef);
206		let b = BinaryField128bGhash::new(0xfedcba9876543210fedcba9876543210);
207		assert_eq!(GhashSq256b::from(a) * GhashSq256b::from(b), GhashSq256b::from(a * b),);
208	}
209
210	#[test]
211	fn test_multiplicative_generator() {
212		assert!(
213			is_generator(GhashSq256b::MULTIPLICATIVE_GENERATOR),
214			"baked MULTIPLICATIVE_GENERATOR is not a generator of GF(2^256)*",
215		);
216	}
217
218	#[test]
219	#[ignore = "search utility: prints a valid generator literal to bake into the field"]
220	fn find_generator() {
221		for b in 1u128..256 {
222			for a in 0u128..256 {
223				let candidate = ghash_sq(a, b);
224				if is_generator(candidate) {
225					let [low, high] = candidate.to_coeffs();
226					panic!(
227						"found generator: a={a:#x}, b={b:#x} -> m256_from_u128s({:#034x}, {:#034x})",
228						u128::from(low.val()),
229						u128::from(high.val()),
230					);
231				}
232			}
233		}
234		panic!("no generator found in search range");
235	}
236
237	proptest! {
238		#[test]
239		fn test_mul_commutative(a in arb_elem(), b in arb_elem()) {
240			prop_assert_eq!(a * b, b * a);
241		}
242
243		#[test]
244		fn test_mul_associative(a in arb_elem(), b in arb_elem(), c in arb_elem()) {
245			prop_assert_eq!((a * b) * c, a * (b * c));
246		}
247
248		#[test]
249		fn test_mul_distributive(a in arb_elem(), b in arb_elem(), c in arb_elem()) {
250			prop_assert_eq!(a * (b + c), a * b + a * c);
251		}
252
253		#[test]
254		fn test_mul_identity(a in arb_elem()) {
255			prop_assert_eq!(a * GhashSq256b::ONE, a);
256		}
257
258		#[test]
259		fn test_square_equals_mul(a in arb_elem()) {
260			prop_assert_eq!(Square::square(a), a * a);
261		}
262
263		#[test]
264		fn test_wide_mul_deferred_reduction(
265			pairs in prop::collection::vec((arb_elem(), arb_elem()), 1..16),
266		) {
267			// Inner product over GHASH^2: the deferred form must equal the eager form.
268			//
269			//     eager:    sum_i reduce(wide_mul(a_i, b_i))  =  sum_i a_i * b_i
270			//     deferred: reduce( sum_i wide_mul(a_i, b_i) )
271			//
272			// This holds because the GHASH reduction and the multiply-by-`X` are both GF(2)-linear.
273			// So a sum of products costs one reduction, not one per term.
274			let eager: GhashSq256b = pairs.iter().map(|&(a, b)| a * b).sum();
275			let deferred = GhashSq256b::reduce(
276				pairs.iter().map(|&(a, b)| GhashSq256b::wide_mul(a, b)).sum(),
277			);
278			prop_assert_eq!(deferred, eager);
279		}
280
281		#[test]
282		fn test_invert(a in arb_elem()) {
283			let inv = a.invert_or_zero();
284			if a == GhashSq256b::ZERO {
285				prop_assert_eq!(inv, GhashSq256b::ZERO);
286			} else {
287				prop_assert_eq!(a * inv, GhashSq256b::ONE);
288			}
289		}
290
291		#[test]
292		fn test_serialization_roundtrip(a in arb_elem()) {
293			let mut buf = Vec::new();
294			a.serialize(&mut buf).unwrap();
295			prop_assert_eq!(buf.len(), GhashSq256b::BYTE_SIZE);
296			let b = GhashSq256b::deserialize(buf.as_slice()).unwrap();
297			prop_assert_eq!(a, b);
298		}
299
300		#[test]
301		fn test_ghash_extension_bases_roundtrip(a in arb_elem()) {
302			let bases: Vec<BinaryField128bGhash> =
303				ExtensionField::<BinaryField128bGhash>::iter_bases(&a).collect();
304			prop_assert_eq!(bases.len(), 2);
305			prop_assert_eq!(
306				<GhashSq256b as ExtensionField<BinaryField128bGhash>>::from_bases(bases),
307				a,
308			);
309		}
310
311		#[test]
312		fn test_b1b_extension_bases_roundtrip(a in arb_elem()) {
313			let bases: Vec<BinaryField1b> =
314				ExtensionField::<BinaryField1b>::iter_bases(&a).collect();
315			prop_assert_eq!(bases.len(), 256);
316			prop_assert_eq!(
317				<GhashSq256b as ExtensionField<BinaryField1b>>::from_bases(bases),
318				a,
319			);
320		}
321
322		#[test]
323		fn test_square_transpose_ghash(a in arb_elem(), b in arb_elem()) {
324			let mut values = [a, b];
325			let expected = naive_square_transpose::<BinaryField128bGhash>(&values);
326			<GhashSq256b as ExtensionField<BinaryField128bGhash>>::square_transpose(&mut values);
327			prop_assert_eq!(values.as_slice(), expected.as_slice());
328		}
329
330		#[test]
331		fn test_square_transpose_b1b(values in prop::collection::vec(arb_elem(), 256)) {
332			let mut values = values;
333			let expected = naive_square_transpose::<BinaryField1b>(&values);
334			<GhashSq256b as ExtensionField<BinaryField1b>>::square_transpose(&mut values);
335			prop_assert_eq!(values, expected);
336		}
337	}
338}