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