binius_hash/groestl/arch/
portable.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3#![allow(clippy::needless_range_loop)]
4
5use std::array;
6
7use binius_field::{
8	arch::packed_aes_64::PackedAESBinaryField8x8b, AESTowerField8b, Field,
9	PackedAESBinaryField64x8b, PackedExtensionIndexable, PackedField,
10};
11use lazy_static::lazy_static;
12
13use super::groestl_table::TABLE;
14
15const ROUND_SIZE: usize = 10;
16
17/// The shift of a given index of the state of P permutation as per the `ShiftBytes` step
18#[inline(always)]
19const fn shift_p_func(row: usize, col: usize) -> usize {
20	let new_row = row;
21	let new_col = (row + col) % 8;
22	new_col * 8 + new_row
23}
24
25/// The shift of a given index of the state of Q permutation as per the `ShiftBytes` step
26#[inline(always)]
27const fn shift_q_func(row: usize, col: usize) -> usize {
28	let new_row = row;
29	let new_col = (col + 2 * row - row / 4 + 1) % 8;
30	new_col * 8 + new_row
31}
32
33lazy_static! {
34	static ref ROW_0_SELECT: [PackedAESBinaryField64x8b; 10] = array::from_fn(|r| {
35		PackedAESBinaryField64x8b::from_fn(|i| {
36			let selector = i % 8;
37			if selector == 0 {
38				AESTowerField8b::from(r as u8)
39			} else {
40				AESTowerField8b::ZERO
41			}
42		})
43	});
44	static ref ROW_7_SELECT: [PackedAESBinaryField64x8b; 10] = array::from_fn(|r| {
45		PackedAESBinaryField64x8b::from_fn(|i| {
46			let selector = i % 8;
47			if selector == 7 {
48				AESTowerField8b::from(r as u8)
49			} else {
50				AESTowerField8b::ZERO
51			}
52		})
53	});
54	static ref ROUND_CONSTANT_P: PackedAESBinaryField64x8b =
55		PackedAESBinaryField64x8b::from_fn(|i| {
56			let selector = i % 8;
57			if selector == 0 {
58				AESTowerField8b::new(0x10 * (i / 8) as u8)
59			} else {
60				AESTowerField8b::ZERO
61			}
62		});
63	static ref ROUND_CONSTANT_Q: PackedAESBinaryField64x8b =
64		PackedAESBinaryField64x8b::from_fn(|i| {
65			let selector = i % 8;
66			if selector == 7 {
67				AESTowerField8b::new(0xff ^ (0x10 * (i / 8) as u8))
68			} else {
69				AESTowerField8b::new(0xff)
70			}
71		});
72}
73
74/// Portable version of the Grøstl256 hash function's P and Q permutations that uses the
75/// implementation of section `8.1.2` from [Grøstl](https://www.groestl.info/Groestl.pdf)
76#[derive(Debug, Clone, Default)]
77pub struct Groestl256Core;
78
79impl Groestl256Core {
80	#[inline(always)]
81	fn add_round_constants_q(
82		&self,
83		x: PackedAESBinaryField64x8b,
84		r: usize,
85	) -> PackedAESBinaryField64x8b {
86		x + ROW_7_SELECT[r] + *ROUND_CONSTANT_Q
87	}
88
89	#[inline(always)]
90	fn add_round_constants_p(
91		&self,
92		x: PackedAESBinaryField64x8b,
93		r: usize,
94	) -> PackedAESBinaryField64x8b {
95		x + ROW_0_SELECT[r] + *ROUND_CONSTANT_P
96	}
97
98	#[inline(always)]
99	fn sub_mix_shift(
100		&self,
101		x: PackedAESBinaryField64x8b,
102		shift_func: fn(usize, usize) -> usize,
103	) -> PackedAESBinaryField64x8b {
104		let x = [x];
105		let input: &[AESTowerField8b] = PackedAESBinaryField64x8b::unpack_base_scalars(&x);
106		let mut state_arr = [PackedAESBinaryField64x8b::zero()];
107		let state: &mut [AESTowerField8b] =
108			PackedAESBinaryField64x8b::unpack_base_scalars_mut(&mut state_arr);
109
110		for col in 0..8 {
111			let mut final_col: PackedAESBinaryField8x8b = PackedAESBinaryField8x8b::zero();
112			for row in 0..8 {
113				let shifted = shift_func(row, col);
114				final_col += PackedAESBinaryField8x8b::from_underlier(
115					TABLE[row][input[shifted].val() as usize],
116				);
117			}
118			let final_col = [final_col];
119			state[col * 8..col * 8 + 8]
120				.copy_from_slice(PackedAESBinaryField8x8b::unpack_base_scalars(&final_col));
121		}
122
123		state_arr[0]
124	}
125
126	/// This function can be used to create the compression function of Grøstl256 hash efficiently
127	/// from the P and Q permutations
128	pub fn permutation_pq(
129		&self,
130		p: PackedAESBinaryField64x8b,
131		q: PackedAESBinaryField64x8b,
132	) -> (PackedAESBinaryField64x8b, PackedAESBinaryField64x8b) {
133		let mut p = p;
134		let mut q = q;
135		for r in 0..ROUND_SIZE {
136			p = self.add_round_constants_p(p, r);
137			q = self.add_round_constants_q(q, r);
138			p = self.sub_mix_shift(p, shift_p_func);
139			q = self.sub_mix_shift(q, shift_q_func);
140		}
141
142		(p, q)
143	}
144
145	/// This function is simply the P permutation from Grøstl256 that is intended to be used in the
146	/// output transformation stage of hash function at finalization
147	pub fn permutation_p(&self, p: PackedAESBinaryField64x8b) -> PackedAESBinaryField64x8b {
148		let mut p = p;
149		for r in 0..ROUND_SIZE {
150			p = self.add_round_constants_p(p, r);
151			p = self.sub_mix_shift(p, shift_p_func);
152		}
153		p
154	}
155}
156
157#[cfg(test)]
158mod tests {
159	use std::array;
160
161	use super::*;
162
163	#[test]
164	fn test_permutation_peq() {
165		let expectedp: [u64; 8] = [
166			0x3c82be9a692fc68a,
167			0x0bcb7ee32d38376a,
168			0x02bc3221a92c42f5,
169			0xb00d24521eb9f4f6,
170			0xbe1e23fee0be4378,
171			0x7f8dc5bb346400d9,
172			0x5b54cf26259832b7,
173			0xb9ff91384b23b6ef,
174		];
175		let expectedq: [u64; 8] = [
176			0x08cce1f96d30d072,
177			0xc59e24a275252ca5,
178			0x078b6474e25e7576,
179			0x29659cf868d046c1,
180			0x81703d4bbae7369b,
181			0x3d03ee6d9462745d,
182			0xa0688a2d116c3c6e,
183			0xb764b88eb2cc185f,
184		];
185
186		let input: [PackedAESBinaryField64x8b; 2] = array::from_fn(|off| {
187			PackedAESBinaryField64x8b::from_fn(|i| AESTowerField8b::new((64 * off + i) as u8))
188		});
189
190		let instance = Groestl256Core;
191		let (pout, qout) = instance.permutation_pq(input[0], input[1]);
192
193		let pout = (0..8)
194			.map(|i| {
195				u64::from_be_bytes(
196					(0..8)
197						.map(|j| pout.get(i * 8 + j).val())
198						.collect::<Vec<_>>()
199						.try_into()
200						.unwrap(),
201				)
202			})
203			.collect::<Vec<_>>();
204		let pout: [u64; 8] = pout.try_into().unwrap();
205		assert_eq!(expectedp, pout);
206
207		let qout = (0..8)
208			.map(|i| {
209				u64::from_be_bytes(
210					(0..8)
211						.map(|j| qout.get(i * 8 + j).val())
212						.collect::<Vec<_>>()
213						.try_into()
214						.unwrap(),
215				)
216			})
217			.collect::<Vec<_>>();
218		let qout: [u64; 8] = qout.try_into().unwrap();
219		assert_eq!(expectedq, qout);
220	}
221}