Skip to main content

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