Skip to main content

binius_field/
packed_ghash_sq.rs

1// Copyright 2026 The Binius Developers
2
3//! Packed [`GhashSq256b`] in two layouts: sliced (struct-of-arrays) and interleaved
4//! (array-of-structs).
5//!
6//! [`GhashSq256b`] is the degree-two extension `a + b·Y` of the GHASH field, with `Y² = X·Y + X`.
7//! Both layouts reduce a batch multiply to three packed GHASH multiplies (Karatsuba over `Y`)
8//! rather than a schoolbook product per element, and both defer the GHASH reductions and the
9//! multiply-by-`X` — all `GF(2)`-linear — so an inner product reduces once at the end.
10//!
11//! - The **sliced** packings ([`SlicedGhashSq256b`]) store the `a` and `b` coordinates of every
12//!   lane in two separate packed GHASH registers via [`SlicedPackedField`]; `Mul` and the rest of
13//!   the [`PackedField`] surface come generically from [`SlicedPackedField`].
14//! - The **interleaved** packings ([`PackedGhashSq1x256b`] / [`PackedGhashSq2x256b`], i.e.
15//!   `PackedPrimitiveType<M256/M512, GhashSq256b>`) store each scalar as one contiguous 256-bit
16//!   value — the same layout as the scalar field. The width-one packing carries the field
17//!   arithmetic (and the scalar [`GhashSq256b`] derives its own from it), while the width-two
18//!   packing divides into two width-one lanes.
19//!
20//! In both, the coordinate register is a [`PackedPrimitiveType`], so the multiply-by-`X` in the
21//! reduction is a per-lane bit shift ([`ghash_mul_x`]) rather than a full field multiply.
22//!
23//! The width-one packing's widening multiply is architecture-specific — see [`GhashSqWideMul1x`],
24//! which selects between batching the Karatsuba diagonal into one 256-bit carry-less multiply and
25//! keeping the three products separate to defer the multiply-by-`X` past a reduction.
26
27use std::{
28	iter::Sum,
29	ops::{Add, AddAssign, Sub, SubAssign},
30};
31
32use bytemuck::TransparentWrapper;
33
34use crate::{
35	BinaryField128bGhash, Divisible, GhashSq256b, PackedBinaryGhash2x128b, PackedField, WideMul,
36	arch::{
37		Divide, GhashSqWideMul1x, M128, M256, M512, MulFromWideMul, PackedPrimitiveType,
38		portable::packed_macros::{portable_macros::*, *},
39	},
40	arithmetic_traits::{InvertOrZero, Square},
41	cast_base, cast_ext,
42	sliced_packed_field::SlicedPackedField,
43	underlier::{UnderlierType, WithUnderlier},
44};
45
46/// The packed GHASH coordinate register backing a `SlicedGhashSq256b<U>`.
47type Ghash<U> = PackedPrimitiveType<U, BinaryField128bGhash>;
48
49/// A GHASH² packing whose two GHASH coordinates pack into `PackedPrimitiveType<U, Ghash128b>`.
50pub type SlicedGhashSq256b<U> = SlicedPackedField<GhashSq256b, Ghash<U>, 2>;
51/// Packed `GhashSq256b` holding one extension scalar (the degenerate width-one packing).
52pub type SlicedGhashSq1x256b = SlicedGhashSq256b<M128>;
53/// Packed `GhashSq256b` holding two extension scalars.
54pub type SlicedGhashSq2x256b = SlicedGhashSq256b<M256>;
55/// Packed `GhashSq256b` holding four extension scalars.
56pub type SlicedGhashSq4x256b = SlicedGhashSq256b<M512>;
57
58/// The unreduced widening product of the coordinate GHASH multiply.
59type GhashWide<U> = <Ghash<U> as WideMul>::Output;
60
61/// Multiplies every 128-bit GHASH lane of an underlier by `X`.
62///
63/// `X` scaling is `GF(2)`-linear — a per-lane bit shift with a fixed compensation, not a field
64/// multiply — so this is far cheaper than a CLMUL. It reuses the scalar
65/// [`BinaryField128bGhash::mul_x`] on each 128-bit lane, which every supported underlier divides
66/// into.
67#[inline]
68fn ghash_mul_x<U: Divisible<M128>>(u: U) -> U {
69	U::from_iter(Divisible::<M128>::value_iter(u).map(|lane| {
70		BinaryField128bGhash::from_underlier(lane)
71			.mul_x()
72			.to_underlier()
73	}))
74}
75
76/// Multiplies every GHASH lane of a packed coordinate by `X`.
77#[inline]
78fn mul_x<U: UnderlierType + Divisible<M128>>(coord: Ghash<U>) -> Ghash<U> {
79	Ghash::<U>::from_underlier(ghash_mul_x(coord.to_underlier()))
80}
81
82/// The unreduced product of two GHASH² elements, as three separate GHASH products.
83///
84/// Holds the three Karatsuba GHASH widening products, deferring both the GHASH reductions and the
85/// multiply-by-`X`. Since those are all `GF(2)`-linear, an inner product over GHASH² accumulates
86/// these by XOR and reduces once at the end. Used both by the sliced packings and — where there is
87/// no wider carry-less multiply to batch the diagonal into — by the interleaved width-one packing.
88#[derive(Clone, Copy, Debug, Default)]
89pub struct SlicedGhashSqWide<W> {
90	/// Unreduced `a·e`, the low diagonal Karatsuba product.
91	pub(crate) t0: W,
92	/// Unreduced `b·f`, the high diagonal Karatsuba product.
93	pub(crate) t2: W,
94	/// Unreduced `(a+b)·(e+f)`, the Karatsuba cross product.
95	pub(crate) t1: W,
96}
97
98impl<W: Add<Output = W>> Add for SlicedGhashSqWide<W> {
99	type Output = Self;
100
101	#[inline]
102	fn add(self, rhs: Self) -> Self {
103		Self {
104			t0: self.t0 + rhs.t0,
105			t2: self.t2 + rhs.t2,
106			t1: self.t1 + rhs.t1,
107		}
108	}
109}
110
111impl<W: Sub<Output = W>> Sub for SlicedGhashSqWide<W> {
112	type Output = Self;
113
114	#[inline]
115	fn sub(self, rhs: Self) -> Self {
116		Self {
117			t0: self.t0 - rhs.t0,
118			t2: self.t2 - rhs.t2,
119			t1: self.t1 - rhs.t1,
120		}
121	}
122}
123
124impl<W: AddAssign> AddAssign for SlicedGhashSqWide<W> {
125	#[inline]
126	fn add_assign(&mut self, rhs: Self) {
127		self.t0 += rhs.t0;
128		self.t2 += rhs.t2;
129		self.t1 += rhs.t1;
130	}
131}
132
133impl<W: SubAssign> SubAssign for SlicedGhashSqWide<W> {
134	#[inline]
135	fn sub_assign(&mut self, rhs: Self) {
136		self.t0 -= rhs.t0;
137		self.t2 -= rhs.t2;
138		self.t1 -= rhs.t1;
139	}
140}
141
142impl<W: Default + Add<Output = W>> Sum for SlicedGhashSqWide<W> {
143	#[inline]
144	fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
145		iter.fold(Self::default(), |acc, x| acc + x)
146	}
147}
148
149impl<U> WideMul for SlicedGhashSq256b<U>
150where
151	U: UnderlierType + Divisible<M128>,
152	Ghash<U>: PackedField<Scalar = BinaryField128bGhash> + WideMul,
153{
154	type Output = SlicedGhashSqWide<GhashWide<U>>;
155
156	/// Karatsuba over `Y`: defers the three GHASH products `a·e`, `b·f`, `(a+b)·(e+f)`.
157	#[inline]
158	fn wide_mul(lhs: Self, rhs: Self) -> Self::Output {
159		let [a, b] = lhs.to_coords();
160		let [e, f] = rhs.to_coords();
161
162		SlicedGhashSqWide {
163			t0: <Ghash<U> as WideMul>::wide_mul(a, e),
164			t2: <Ghash<U> as WideMul>::wide_mul(b, f),
165			t1: <Ghash<U> as WideMul>::wide_mul(a + b, e + f),
166		}
167	}
168
169	/// Reduces the three products and folds `Y² = X·Y + X`. With the Karatsuba cross term recovered
170	/// as `t₁ + t₀ + t₂`: `z₀ = t₀ + X·t₂`, `z₁ = (t₁ + t₀ + t₂) + X·t₂ = z₀ + t₁ + t₂`.
171	#[inline]
172	fn reduce(wide: Self::Output) -> Self {
173		let t0 = <Ghash<U> as WideMul>::reduce(wide.t0);
174		let t2 = <Ghash<U> as WideMul>::reduce(wide.t2);
175		let t1 = <Ghash<U> as WideMul>::reduce(wide.t1);
176
177		let z0 = t0 + mul_x(t2);
178		Self::from_coords([z0, z0 + t1 + t2])
179	}
180}
181
182impl<U> Square for SlicedGhashSq256b<U>
183where
184	U: UnderlierType + Divisible<M128>,
185	Ghash<U>: PackedField<Scalar = BinaryField128bGhash>,
186{
187	/// `(a + b·Y)² = (a² + X·b²) + (X·b²)·Y` — the cross term vanishes in characteristic two, and
188	/// `Y² = X·Y + X`.
189	#[inline]
190	fn square(self) -> Self {
191		let [a, b] = self.to_coords();
192
193		let t0 = Square::square(a);
194		let t2 = Square::square(b);
195
196		let x_t2 = mul_x(t2);
197		Self::from_coords([t0 + x_t2, x_t2])
198	}
199}
200
201impl<U> InvertOrZero for SlicedGhashSq256b<U>
202where
203	U: UnderlierType + Divisible<M128>,
204	Ghash<U>: PackedField<Scalar = BinaryField128bGhash>,
205{
206	/// Inverts through the norm of the degree-two extension. The conjugate of `u = a + b·Y` sends
207	/// `Y` to the other root of `Y² + X·Y + X` (the roots sum to `X` and multiply to `X`), giving
208	/// `ū = (a + X·b) + b·Y`. Its norm `N = u·ū = a² + X·b·(a + b)` lies in GHASH, and
209	/// `u⁻¹ = ū·N⁻¹`. A zero lane has norm zero, so `invert_or_zero` returns zero there.
210	#[inline]
211	fn invert_or_zero(self) -> Self {
212		let [a, b] = self.to_coords();
213
214		let norm = Square::square(a) + mul_x(a * b + Square::square(b));
215		let norm_inv = norm.invert_or_zero();
216
217		Self::from_coords([(a + mul_x(b)) * norm_inv, b * norm_inv])
218	}
219}
220
221// ---------------------------------------------------------------------------
222// Interleaved (array-of-structs) packings: `PackedPrimitiveType<M256/M512, GhashSq256b>`.
223//
224// Unlike the sliced packings above, these store each GHASH² scalar as one contiguous 256-bit value
225// (low 128 bits = coefficient of `1`, high 128 bits = coefficient of `Y`) — the same layout as the
226// scalar `GhashSq256b`. The width-one M256 packing carries the field arithmetic (the scalar field
227// derives its own `Mul`/`Square`/`InvertOrZero`/`WideMul` from it via `binary_field!`); the
228// width-two M512 packing divides into two independent M256 lanes.
229//
230// The width-one `WideMul` itself is architecture-specific and lives with the other per-target
231// strategies under `arch`, reached here through the `GhashSqWideMul1x` alias.
232// ---------------------------------------------------------------------------
233
234/// The GHASH coordinates `[a, b]` of a width-one GHASH² element `a + b·Y`.
235///
236/// The coordinates already sit in the two 128-bit lanes of the 256-bit value, so this is a free
237/// reinterpretation followed by two lane reads.
238#[inline]
239pub(crate) fn ghash_sq_coords(elem: PackedGhashSq1x256b) -> [BinaryField128bGhash; 2] {
240	let coords = cast_base::<BinaryField128bGhash, _>(elem);
241	[coords.get(0), coords.get(1)]
242}
243
244/// Assembles a width-one GHASH² element `a + b·Y` from its GHASH coordinates `[a, b]`.
245#[inline]
246pub(crate) fn ghash_sq_from_coords(coords: [BinaryField128bGhash; 2]) -> PackedGhashSq1x256b {
247	cast_ext::<BinaryField128bGhash, _>(PackedBinaryGhash2x128b::from_scalars(coords))
248}
249
250/// [`Square`] strategy for [`PackedGhashSq1x256b`].
251#[repr(transparent)]
252#[derive(TransparentWrapper)]
253pub struct GhashSqSquare<T>(T);
254
255impl Square for GhashSqSquare<PackedGhashSq1x256b> {
256	/// `(a + b·Y)² = (a² + X·b²) + (X·b²)·Y` — the cross term vanishes in characteristic two.
257	#[inline]
258	fn square(self) -> Self {
259		let sq = Square::square(cast_base::<BinaryField128bGhash, _>(Self::peel(self)));
260
261		let x_t2 = sq.get(1).mul_x();
262		Self::wrap(ghash_sq_from_coords([sq.get(0) + x_t2, x_t2]))
263	}
264}
265
266/// [`InvertOrZero`] strategy for [`PackedGhashSq1x256b`].
267#[repr(transparent)]
268#[derive(TransparentWrapper)]
269pub struct GhashSqInvert<T>(T);
270
271impl InvertOrZero for GhashSqInvert<PackedGhashSq1x256b> {
272	/// Inverts through the norm: conjugate `ū = (a + X·b) + b·Y` (the roots of `Y² + X·Y + X` sum
273	/// to `X` and multiply to `X`), norm `N = a² + X·b·(a + b)`, and `u⁻¹ = ū·N⁻¹`.
274	#[inline]
275	fn invert_or_zero(self) -> Self {
276		let [a, b] = ghash_sq_coords(Self::peel(self));
277
278		let norm = Square::square(a) + (a * b + Square::square(b)).mul_x();
279		let norm_inv = norm.invert_or_zero();
280
281		Self::wrap(ghash_sq_from_coords([(a + b.mul_x()) * norm_inv, b * norm_inv]))
282	}
283}
284
285/// [`Divide`] strategy specializing the width-two M512 packing into two width-one M256 lanes.
286type GhashSqDivide2x<T> = Divide<M256, T, 2>;
287
288define_packed_binary_field!(
289	PackedGhashSq1x256b,
290	GhashSq256b,
291	M256,
292	(MulFromWideMul),
293	(GhashSqSquare),
294	(GhashSqInvert),
295	(GhashSqWideMul1x)
296);
297
298define_packed_binary_field!(
299	PackedGhashSq2x256b,
300	GhashSq256b,
301	M512,
302	(MulFromWideMul),
303	(GhashSqDivide2x),
304	(GhashSqDivide2x),
305	(GhashSqDivide2x)
306);
307
308#[cfg(test)]
309mod tests {
310	use rand::{Rng, SeedableRng, rngs::StdRng};
311
312	use super::*;
313	use crate::{
314		Divisible, Field, PackedField, Random,
315		arithmetic_traits::{InvertOrZero, Square},
316		field::FieldOps,
317	};
318
319	// Every packing of `GhashSq256b` must agree lane-by-lane with the scalar reference field, which
320	// is tested independently in `ghash_sq`. Each check is run for all three widths.
321
322	fn check_arithmetic<P: PackedField<Scalar = GhashSq256b>>(mut rng: impl Rng) {
323		let a = P::random(&mut rng);
324		let b = P::random(&mut rng);
325
326		let sum = a + b;
327		let diff = a - b;
328		let prod = a * b;
329		let sq = Square::square(a);
330		let inv = InvertOrZero::invert_or_zero(a);
331
332		for i in 0..P::WIDTH {
333			let (x, y) = (a.get(i), b.get(i));
334			assert_eq!(sum.get(i), x + y);
335			assert_eq!(diff.get(i), x - y);
336			assert_eq!(prod.get(i), x * y);
337			assert_eq!(sq.get(i), Square::square(x));
338			assert_eq!(inv.get(i), x.invert_or_zero());
339			// `invert_or_zero` is a genuine inverse away from zero.
340			if x != GhashSq256b::ZERO {
341				assert_eq!(x * inv.get(i), GhashSq256b::ONE);
342			}
343		}
344	}
345
346	fn check_wide_mul<P>(mut rng: impl Rng)
347	where
348		P: PackedField<Scalar = GhashSq256b> + WideMul,
349	{
350		// The deferred widening form must match the eager product, and accumulating before a single
351		// reduction must match summing the reductions (both the multiply-by-`X` and the GHASH
352		// reduction are `GF(2)`-linear).
353		let (a1, b1) = (P::random(&mut rng), P::random(&mut rng));
354		let (a2, b2) = (P::random(&mut rng), P::random(&mut rng));
355
356		assert_eq!(P::reduce(P::wide_mul(a1, b1)), a1 * b1);
357		let deferred = P::reduce(P::wide_mul(a1, b1) + P::wide_mul(a2, b2));
358		assert_eq!(deferred, a1 * b1 + a2 * b2);
359	}
360
361	fn check_scalar_ops<P: PackedField<Scalar = GhashSq256b>>(mut rng: impl Rng) {
362		let a = P::random(&mut rng);
363		let s = GhashSq256b::random(&mut rng);
364
365		let broadcast = <P as Divisible<GhashSq256b>>::broadcast(s);
366		let scaled = a * s;
367		for i in 0..P::WIDTH {
368			assert_eq!(broadcast.get(i), s);
369			assert_eq!(scaled.get(i), a.get(i) * s);
370		}
371
372		// `one` is the multiplicative identity in every lane.
373		assert_eq!(a * <P as FieldOps>::one(), a);
374	}
375
376	fn check_get_set_iter<P: PackedField<Scalar = GhashSq256b>>(mut rng: impl Rng) {
377		let mut a = P::random(&mut rng);
378		for i in 0..P::WIDTH {
379			let v = GhashSq256b::random(&mut rng);
380			a.set(i, v);
381			assert_eq!(a.get(i), v);
382		}
383
384		// `from_scalars(iter())` round-trips.
385		let scalars: Vec<_> = a.iter().collect();
386		assert_eq!(P::from_scalars(scalars.iter().copied()), a);
387	}
388
389	/// Reference [`PackedField::interleave`] over the scalar sequence, per the documented 2×2
390	/// block transpose: output `x ∈ {0, 1}` takes, at block position `t`, block `2·⌊t/2⌋ + x` from
391	/// the first operand when `t` is even and from the second when `t` is odd.
392	fn ref_interleave<S: Copy>(a: &[S], b: &[S], lbl: usize) -> (Vec<S>, Vec<S>) {
393		let s = 1usize << lbl;
394		let nb = a.len() / s;
395		let build = |x: usize| -> Vec<S> {
396			let mut out = Vec::with_capacity(a.len());
397			for t in 0..nb {
398				let (src, blk) = if t % 2 == 0 {
399					(a, t + x)
400				} else {
401					(b, t - 1 + x)
402				};
403				out.extend_from_slice(&src[blk * s..blk * s + s]);
404			}
405			out
406		};
407		(build(0), build(1))
408	}
409
410	/// Reference [`PackedField::unzip`] over the scalar sequence: concatenate the `nb` blocks of
411	/// the first operand then the `nb` of the second, and split the resulting `2·nb` blocks into
412	/// the even-indexed (first output) and odd-indexed (second output).
413	fn ref_unzip<S: Copy>(a: &[S], b: &[S], lbl: usize) -> (Vec<S>, Vec<S>) {
414		let s = 1usize << lbl;
415		let nb = a.len() / s;
416		let block = |i: usize| -> &[S] {
417			if i < nb {
418				&a[i * s..i * s + s]
419			} else {
420				&b[(i - nb) * s..(i - nb) * s + s]
421			}
422		};
423		let (mut out_a, mut out_b) = (Vec::with_capacity(a.len()), Vec::with_capacity(a.len()));
424		for i in 0..2 * nb {
425			if i % 2 == 0 {
426				out_a.extend_from_slice(block(i));
427			} else {
428				out_b.extend_from_slice(block(i));
429			}
430		}
431		(out_a, out_b)
432	}
433
434	fn check_interleave_unzip<P: PackedField<Scalar = GhashSq256b>>(mut rng: impl Rng) {
435		let a = P::random(&mut rng);
436		let b = P::random(&mut rng);
437		let (sa, sb): (Vec<_>, Vec<_>) = (a.iter().collect(), b.iter().collect());
438
439		for log_block_len in 0..P::LOG_WIDTH {
440			let (c, d) = a.interleave(b, log_block_len);
441			let (ec, ed) = ref_interleave(&sa, &sb, log_block_len);
442			assert_eq!(c, P::from_scalars(ec));
443			assert_eq!(d, P::from_scalars(ed));
444
445			let (u, v) = a.unzip(b, log_block_len);
446			let (eu, ev) = ref_unzip(&sa, &sb, log_block_len);
447			assert_eq!(u, P::from_scalars(eu));
448			assert_eq!(v, P::from_scalars(ev));
449		}
450	}
451
452	macro_rules! width_tests {
453		($mod:ident, $ty:ty) => {
454			mod $mod {
455				use super::*;
456
457				#[test]
458				fn arithmetic() {
459					for seed in 0..64 {
460						check_arithmetic::<$ty>(StdRng::seed_from_u64(seed));
461					}
462				}
463
464				#[test]
465				fn wide_mul() {
466					for seed in 0..64 {
467						check_wide_mul::<$ty>(StdRng::seed_from_u64(seed));
468					}
469				}
470
471				#[test]
472				fn scalar_ops() {
473					for seed in 0..64 {
474						check_scalar_ops::<$ty>(StdRng::seed_from_u64(seed));
475					}
476				}
477
478				#[test]
479				fn get_set_iter() {
480					for seed in 0..64 {
481						check_get_set_iter::<$ty>(StdRng::seed_from_u64(seed));
482					}
483				}
484
485				#[test]
486				fn interleave_unzip() {
487					for seed in 0..64 {
488						check_interleave_unzip::<$ty>(StdRng::seed_from_u64(seed));
489					}
490				}
491			}
492		};
493	}
494
495	width_tests!(width1, SlicedGhashSq1x256b);
496	width_tests!(width2, SlicedGhashSq2x256b);
497	width_tests!(width4, SlicedGhashSq4x256b);
498
499	// The interleaved `PackedPrimitiveType` packings must agree lane-by-lane with the same scalar
500	// reference field as the sliced packings above.
501	width_tests!(packed_width1, PackedGhashSq1x256b);
502	width_tests!(packed_width2, PackedGhashSq2x256b);
503}