Skip to main content

binius_field/arch/portable/arithmetic/
itoh_tsujii.rs

1// Copyright 2026 The Binius Developers
2
3//! Itoh-Tsujii inversion for the GHASH field `GF(2^128)`.
4//!
5//! For a non-zero `x`, the inverse is `x^(2^128 - 2) = (x^(2^127 - 1))^2`. The exponent `2^127 - 1`
6//! is built up with an addition chain on the powers `beta_k := x^(2^k - 1)`, using the identity
7//!
8//! ```text
9//! beta_{a+b} = (beta_a)^(2^b) * beta_b.
10//! ```
11//!
12//! Squaring `beta_a` repeatedly `b` times (the `x -> x^(2^b)` power map) is an `F_2`-linear
13//! transformation. We precompute each power map as a [`BytewiseLookupTransformation`] (the [Method
14//! of Four Russians]), wrapped into a `GhashB128 -> GhashB128` transform, and hold them in a
15//! process-wide [`LazyLock`] so the tables are computed once and shared read-only across all
16//! threads.
17//!
18//! [Method of Four Russians]: <https://en.wikipedia.org/wiki/Method_of_Four_Russians>
19
20use std::{array, iter, ops::Mul, sync::LazyLock};
21
22use crate::{
23	BinaryField1b, Divisible, ExtensionField,
24	arch::M128,
25	arithmetic_traits::Square,
26	ghash::BinaryField128bGhash as GhashB128,
27	linear_transformation::{
28		BytewiseLookupTransformation, BytewiseLookupTransformationFactory,
29		InputWrappingTransformationFactory, LinearTransformationFactory,
30		OutputWrappingTransformationFactory, Transformation, WrappingTransformation,
31	},
32};
33
34/// Number of bits in a GHASH element.
35const FIELD_BITS: usize = 128;
36
37/// A precomputed `x -> x^(2^n)` power map as a byte-lookup transform on `GhashB128`.
38///
39/// The underlying [`BytewiseLookupTransformation`] operates on the underlier `M128`; the input and
40/// output wrappers lift it to a `GhashB128 -> GhashB128` transform.
41type GhashPowerMap =
42	WrappingTransformation<BytewiseLookupTransformation<M128, M128>, GhashB128, GhashB128>;
43
44/// The power maps needed by the Itoh-Tsujii addition chain for the GHASH field.
45///
46/// Each field holds the transform for one power map `x -> x^(2^n)`, for the values of `n` that
47/// appear in the chain (`pow_2_7` is reused for both the `7 -> 14` and `56 -> 63` steps).
48struct GhashPowerMapTables {
49	pow_2_3: GhashPowerMap,
50	pow_2_7: GhashPowerMap,
51	pow_2_14: GhashPowerMap,
52	pow_2_28: GhashPowerMap,
53	pow_2_63: GhashPowerMap,
54}
55
56impl GhashPowerMapTables {
57	fn new() -> Self {
58		Self {
59			pow_2_3: compute_power_map_transform(3),
60			pow_2_7: compute_power_map_transform(7),
61			pow_2_14: compute_power_map_transform(14),
62			pow_2_28: compute_power_map_transform(28),
63			pow_2_63: compute_power_map_transform(63),
64		}
65	}
66}
67
68static GHASH_POWER_MAP_TABLES: LazyLock<GhashPowerMapTables> =
69	LazyLock::new(GhashPowerMapTables::new);
70
71/// Build the byte-lookup transform for the power map `x -> x^(2^n)` over `GhashB128`.
72///
73/// The power map is the `F_2`-linear transformation whose matrix has one column per input bit
74/// (`compute_power_map_matrix`). [`BytewiseLookupTransformation`] turns that column set into
75/// byte-indexed lookup tables; the input/output wrappers make it accept and return `GhashB128`.
76fn compute_power_map_transform(n: usize) -> GhashPowerMap {
77	let matrix = compute_power_map_matrix(n);
78	OutputWrappingTransformationFactory::<_, GhashB128, GhashB128>::new(
79		InputWrappingTransformationFactory::<_, GhashB128, M128>::new(
80			BytewiseLookupTransformationFactory,
81		),
82	)
83	.create(&matrix)
84}
85
86/// Compute the matrix of the `F_2`-linear power map `x -> x^(2^n)`.
87///
88/// Column `i` is the image of the `i`-th basis element, i.e. `basis(i)^(2^n)`, obtained by squaring
89/// `n` times.
90fn compute_power_map_matrix(n: usize) -> [GhashB128; FIELD_BITS] {
91	array::from_fn(|i| {
92		let basis = <GhashB128 as ExtensionField<BinaryField1b>>::basis(i);
93		iter::successors(Some(basis), |basis_pow_2_i| Some(basis_pow_2_i.square()))
94			.nth(n)
95			.expect("closure always returns Some")
96	})
97}
98
99/// Invert each GHASH element (scalar or packed) via the Itoh-Tsujii algorithm.
100///
101/// Zero elements map to zero, matching `InvertOrZero` semantics.
102///
103/// The bound is phrased in terms of the field operations (`Square`, `Mul`) plus
104/// `Divisible<GhashB128>` rather than `P: PackedField`. `PackedField`'s blanket impl lists
105/// `InvertOrZero` in its where-clause, so requiring it here would form a trait-resolution cycle
106/// when this function backs the `InvertOrZero` impls. `Divisible<GhashB128>` carries no such
107/// obligation, keeps the function statically GHASH-typed, and is satisfied both by the GHASH packed
108/// fields and (reflexively) by the scalar `BinaryField128bGhash`, so the scalar inverts directly
109/// without routing through a packed type.
110pub fn invert_b128<P>(x: P) -> P
111where
112	P: Copy + Square + Mul<Output = P> + Divisible<GhashB128>,
113{
114	let tables = &*GHASH_POWER_MAP_TABLES;
115
116	// Addition chain for 127: 1, 2, 3, 6, 7, 14, 28, 56, 63, 126, 127.
117	let beta_1 = x;
118	let beta_2 = beta_1.square() * beta_1;
119	let beta_3 = beta_2.square() * beta_1;
120	let beta_6 = pow_2_n(beta_3, &tables.pow_2_3) * beta_3;
121	let beta_7 = beta_6.square() * beta_1;
122	let beta_14 = pow_2_n(beta_7, &tables.pow_2_7) * beta_7;
123	let beta_28 = pow_2_n(beta_14, &tables.pow_2_14) * beta_14;
124	let beta_56 = pow_2_n(beta_28, &tables.pow_2_28) * beta_28;
125	let beta_63 = pow_2_n(beta_56, &tables.pow_2_7) * beta_7;
126	let beta_126 = pow_2_n(beta_63, &tables.pow_2_63) * beta_63;
127	let beta_127 = beta_126.square() * beta_1;
128	// x^(-1) = (x^(2^127 - 1))^2.
129	beta_127.square()
130}
131
132/// Apply the power map `x -> x^(2^n)` to every GHASH scalar of `x`.
133fn pow_2_n<P>(x: P, power_map: &GhashPowerMap) -> P
134where
135	P: Divisible<GhashB128>,
136{
137	Divisible::<GhashB128>::from_iter(
138		Divisible::<GhashB128>::value_iter(x).map(|scalar| power_map.transform(&scalar)),
139	)
140}
141
142#[cfg(test)]
143mod tests {
144	use proptest::prelude::*;
145
146	use super::*;
147	use crate::{
148		Field, PackedField,
149		arch::{
150			packed_ghash_128::PackedBinaryGhash1x128b, packed_ghash_256::PackedBinaryGhash2x128b,
151		},
152	};
153
154	#[test]
155	fn test_compute_power_map_matrix_is_squaring() {
156		// The 2^1 power map is just squaring; column i must equal basis(i)^2.
157		let matrix = compute_power_map_matrix(1);
158		for i in 0..FIELD_BITS {
159			let basis = <GhashB128 as ExtensionField<BinaryField1b>>::basis(i);
160			assert_eq!(matrix[i], basis.square());
161		}
162	}
163
164	#[test]
165	fn test_power_map_transform_matches_repeated_squaring() {
166		let power_map = compute_power_map_transform(7);
167		for &raw in &[0u128, 1, 2, 0x87, 0x21ac73a21d46a21badd6747bcdfc5d4d] {
168			let x = GhashB128::from(raw);
169			let mut expected = x;
170			for _ in 0..7 {
171				expected = expected.square();
172			}
173			assert_eq!(power_map.transform(&x), expected);
174		}
175	}
176
177	#[test]
178	fn test_invert_b128_known_values() {
179		let one = PackedBinaryGhash1x128b::broadcast(GhashB128::ONE);
180		assert_eq!(invert_b128(one), one);
181
182		let zero = PackedBinaryGhash1x128b::broadcast(GhashB128::ZERO);
183		assert_eq!(invert_b128(zero), zero);
184	}
185
186	// `invert_b128` now backs `InvertOrZero` itself, so the multiplicative-inverse property (with
187	// `0 -> 0`) is the independent oracle: given a separately-tested `mul`, `x * x^-1 == 1` fully
188	// characterizes invert-or-zero.
189	proptest! {
190		#[test]
191		fn test_invert_b128_is_multiplicative_inverse_scalar(raw in any::<u128>()) {
192			let x = GhashB128::from(raw);
193			let inv = invert_b128(x);
194			if x == GhashB128::ZERO {
195				prop_assert_eq!(inv, GhashB128::ZERO);
196			} else {
197				prop_assert_eq!(x * inv, GhashB128::ONE);
198			}
199		}
200
201		#[test]
202		fn test_invert_b128_is_multiplicative_inverse_1x(raw in any::<u128>()) {
203			let scalar = GhashB128::from(raw);
204			let x = PackedBinaryGhash1x128b::broadcast(scalar);
205			let inv = invert_b128(x);
206			if scalar == GhashB128::ZERO {
207				prop_assert_eq!(inv, x);
208			} else {
209				prop_assert_eq!(x * inv, PackedBinaryGhash1x128b::broadcast(GhashB128::ONE));
210			}
211		}
212
213		#[test]
214		fn test_invert_b128_is_multiplicative_inverse_2x(a in any::<u128>(), b in any::<u128>()) {
215			let x = PackedBinaryGhash2x128b::from_scalars([a, b].map(GhashB128::from));
216			let inv = invert_b128(x);
217			let ones = PackedBinaryGhash2x128b::from_scalars(
218				[a, b].map(|raw| {
219					if GhashB128::from(raw) == GhashB128::ZERO {
220						GhashB128::ZERO
221					} else {
222						GhashB128::ONE
223					}
224				}),
225			);
226			prop_assert_eq!(x * inv, ones);
227		}
228	}
229}