Skip to main content

binius_hash/
sha256_x4.rs

1// Copyright 2026 The Binius Developers
2
3//! Four-way interleaved SHA-256 over the ARMv8 crypto extension.
4//!
5//! The SHA unit is pipelined, so one dependent hash chain leaves it under-full.
6//! Interleaving four independent hashes fills the pipeline and raises throughput.
7//!
8//! The target is bulk Merkle leaf hashing, where the leaves of a level are independent.
9//! The `sha2` crate hashes one stream at a time, so that headroom goes unused.
10//!
11//! The output equals [`sha2::Sha256`] byte for byte, pinned by the proptests below.
12//! Off aarch64, or without the `sha2` feature, it falls back to four `sha2` hashes.
13
14/// A SHA-256 digest.
15pub type Hash = [u8; 32];
16
17/// Compresses one 64-byte block into each of four independent SHA-256 states, in place.
18///
19/// This is the four-way analogue of the portable single-stream block function.
20/// Each state absorbs its own block through the full 64 rounds plus the Davies-Meyer add.
21/// No padding or length suffix is involved.
22/// The caller owns the block contents.
23///
24/// On aarch64 with the `sha2` feature the four lanes interleave over one SHA unit.
25/// Elsewhere the lanes compress one at a time through the portable block function.
26#[inline]
27pub fn compress256_x4(states: &mut [[u32; 8]; 4], blocks: [&[u8; 64]; 4]) {
28	#[cfg(all(target_arch = "aarch64", target_feature = "sha2"))]
29	{
30		// SAFETY: the `sha2` target feature is statically enabled, so the crypto intrinsics exist.
31		unsafe { neon::compress4_states(states, blocks) }
32	}
33	#[cfg(not(all(target_arch = "aarch64", target_feature = "sha2")))]
34	{
35		for (state, block) in states.iter_mut().zip(blocks) {
36			sha2::block_api::compress256(state, std::slice::from_ref(block));
37		}
38	}
39}
40
41/// Hashes four equal-length byte inputs into four standard SHA-256 digests.
42///
43/// The inputs hash as four independent streams.
44/// On aarch64 with the `sha2` feature those streams interleave over one SHA unit.
45///
46/// # Panics
47///
48/// Panics if the four inputs are not all the same length.
49#[inline]
50pub fn sha256_x4(inputs: [&[u8]; 4]) -> [Hash; 4] {
51	let len = inputs[0].len();
52	assert!(
53		inputs.iter().all(|input| input.len() == len),
54		"the four inputs must have equal length"
55	);
56
57	#[cfg(all(target_arch = "aarch64", target_feature = "sha2"))]
58	{
59		// SAFETY: the `sha2` target feature is statically enabled, so the crypto intrinsics exist.
60		unsafe { neon::hash4_equal_len(inputs) }
61	}
62	#[cfg(not(all(target_arch = "aarch64", target_feature = "sha2")))]
63	{
64		scalar::hash4_equal_len(inputs)
65	}
66}
67
68/// The portable fallback: four independent [`sha2::Sha256`] hashes.
69///
70/// Used off aarch64, or without the `sha2` target feature, where the NEON kernel is unavailable.
71#[cfg(not(all(target_arch = "aarch64", target_feature = "sha2")))]
72mod scalar {
73	use sha2::{Digest, Sha256};
74
75	use super::Hash;
76
77	/// Hashes four equal-length inputs with the portable `sha2` implementation.
78	#[inline]
79	pub fn hash4_equal_len(inputs: [&[u8]; 4]) -> [Hash; 4] {
80		inputs.map(|input| Sha256::digest(input).into())
81	}
82}
83
84/// The interleaved kernel built on the ARM SHA-256 crypto intrinsics.
85#[cfg(all(target_arch = "aarch64", target_feature = "sha2"))]
86mod neon {
87	use core::arch::aarch64::{
88		uint32x4_t, vaddq_u32, vdupq_n_u32, vld1q_u8, vld1q_u32, vreinterpretq_u8_u32,
89		vreinterpretq_u32_u8, vrev32q_u8, vsha256h2q_u32, vsha256hq_u32, vsha256su0q_u32,
90		vsha256su1q_u32, vst1q_u8, vst1q_u32,
91	};
92
93	use super::Hash;
94
95	/// The 64 SHA-256 round constants.
96	const K: [u32; 64] = [
97		0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
98		0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
99		0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
100		0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
101		0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
102		0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
103		0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
104		0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
105		0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
106		0xc67178f2,
107	];
108
109	/// The SHA-256 initial state, split into the low half (a, b, c, d).
110	const IV_ABCD: [u32; 4] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a];
111	/// The SHA-256 initial state, split into the high half (e, f, g, h).
112	const IV_EFGH: [u32; 4] = [0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
113
114	/// Compresses one 64-byte block into each of four independent SHA-256 states.
115	///
116	/// The four states advance together, one interleaved round at a time.
117	/// Four independent hashes then occupy the SHA pipeline at once.
118	///
119	/// # Safety
120	///
121	/// The caller must enable the `sha2` target feature so the crypto intrinsics are defined.
122	/// Each `blocks[i]` must point to at least 64 readable bytes.
123	#[inline(always)]
124	unsafe fn compress4(
125		abcd: &mut [uint32x4_t; 4],
126		efgh: &mut [uint32x4_t; 4],
127		blocks: [*const u8; 4],
128	) {
129		unsafe {
130			// Load each block's 16 message words, byte-swapped to big-endian as SHA-256 expects.
131			let mut msg0 = [vdupq_n_u32(0); 4];
132			let mut msg1 = [vdupq_n_u32(0); 4];
133			let mut msg2 = [vdupq_n_u32(0); 4];
134			let mut msg3 = [vdupq_n_u32(0); 4];
135			for i in 0..4 {
136				msg0[i] = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(blocks[i])));
137				msg1[i] = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(blocks[i].add(16))));
138				msg2[i] = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(blocks[i].add(32))));
139				msg3[i] = vreinterpretq_u32_u8(vrev32q_u8(vld1q_u8(blocks[i].add(48))));
140			}
141
142			// Save the incoming state to add back as the Davies-Meyer feed-forward at the end.
143			let abcd_save = *abcd;
144			let efgh_save = *efgh;
145
146			// One group of four rounds across all four states, at round-constant offset `ki`.
147			macro_rules! rounds4 {
148				($msg:expr, $ki:expr) => {{
149					let kv = vld1q_u32(K.as_ptr().add($ki));
150					for i in 0..4 {
151						let wk = vaddq_u32($msg[i], kv);
152						let t = abcd[i];
153						abcd[i] = vsha256hq_u32(abcd[i], efgh[i], wk);
154						efgh[i] = vsha256h2q_u32(efgh[i], t, wk);
155					}
156				}};
157			}
158
159			// Extend the message schedule by four words across all four states.
160			macro_rules! sched {
161				($m0:expr, $m1:expr, $m2:expr, $m3:expr) => {
162					for i in 0..4 {
163						$m0[i] = vsha256su1q_u32(vsha256su0q_u32($m0[i], $m1[i]), $m2[i], $m3[i]);
164					}
165				};
166			}
167
168			// Rounds 0..16 consume the raw message words.
169			rounds4!(msg0, 0);
170			rounds4!(msg1, 4);
171			rounds4!(msg2, 8);
172			rounds4!(msg3, 12);
173			// Rounds 16..64: schedule the next four words, then consume them.
174			for r in 1..4 {
175				sched!(msg0, msg1, msg2, msg3);
176				sched!(msg1, msg2, msg3, msg0);
177				sched!(msg2, msg3, msg0, msg1);
178				sched!(msg3, msg0, msg1, msg2);
179				rounds4!(msg0, 16 * r);
180				rounds4!(msg1, 16 * r + 4);
181				rounds4!(msg2, 16 * r + 8);
182				rounds4!(msg3, 16 * r + 12);
183			}
184
185			// Davies-Meyer: add the saved state back into the compressed state.
186			for i in 0..4 {
187				abcd[i] = vaddq_u32(abcd[i], abcd_save[i]);
188				efgh[i] = vaddq_u32(efgh[i], efgh_save[i]);
189			}
190		}
191	}
192
193	/// Compresses one 64-byte block into each of four independent word states, in place.
194	///
195	/// The state words load and store in native word order.
196	/// No byte-swap of the state happens on either side, matching the portable block function.
197	/// Only the message block bytes swap to big-endian, inside the shared round kernel.
198	///
199	/// # Safety
200	///
201	/// The caller must enable the `sha2` target feature so the crypto intrinsics are defined.
202	#[inline]
203	pub unsafe fn compress4_states(states: &mut [[u32; 8]; 4], blocks: [&[u8; 64]; 4]) {
204		unsafe {
205			// Split each 8-word state into its (a, b, c, d) and (e, f, g, h) register halves.
206			let mut abcd = [vdupq_n_u32(0); 4];
207			let mut efgh = [vdupq_n_u32(0); 4];
208			for i in 0..4 {
209				abcd[i] = vld1q_u32(states[i].as_ptr());
210				efgh[i] = vld1q_u32(states[i].as_ptr().add(4));
211			}
212
213			// One interleaved 64-round compression, Davies-Meyer add included.
214			compress4(&mut abcd, &mut efgh, blocks.map(|block| block.as_ptr()));
215
216			// Write the advanced states back in native word order.
217			for i in 0..4 {
218				vst1q_u32(states[i].as_mut_ptr(), abcd[i]);
219				vst1q_u32(states[i].as_mut_ptr().add(4), efgh[i]);
220			}
221		}
222	}
223
224	/// Hashes four equal-length inputs with the interleaved NEON compression.
225	///
226	/// # Safety
227	///
228	/// The caller must enable the `sha2` target feature so the crypto intrinsics are defined.
229	#[inline]
230	pub unsafe fn hash4_equal_len(inputs: [&[u8]; 4]) -> [Hash; 4] {
231		let len = inputs[0].len();
232
233		unsafe {
234			let mut abcd = [vld1q_u32(IV_ABCD.as_ptr()); 4];
235			let mut efgh = [vld1q_u32(IV_EFGH.as_ptr()); 4];
236
237			// Absorb every full 64-byte block of the message.
238			let n_full = len / 64;
239			for blk in 0..n_full {
240				let base = blk * 64;
241				compress4(
242					&mut abcd,
243					&mut efgh,
244					[
245						inputs[0].as_ptr().add(base),
246						inputs[1].as_ptr().add(base),
247						inputs[2].as_ptr().add(base),
248						inputs[3].as_ptr().add(base),
249					],
250				);
251			}
252
253			// Build the padded tail: leftover bytes, then 0x80, then zeros, then the 64-bit BE
254			// length. One padding block when the leftover is <= 55 bytes, otherwise two.
255			let rem = len % 64;
256			let bit_len = (len as u64) * 8;
257			let n_tail = if rem < 56 { 1 } else { 2 };
258			let mut tails = [[0u8; 128]; 4];
259			for i in 0..4 {
260				tails[i][..rem].copy_from_slice(&inputs[i][len - rem..]);
261				tails[i][rem] = 0x80;
262				tails[i][n_tail * 64 - 8..n_tail * 64].copy_from_slice(&bit_len.to_be_bytes());
263			}
264			for blk in 0..n_tail {
265				let base = blk * 64;
266				compress4(
267					&mut abcd,
268					&mut efgh,
269					[
270						tails[0].as_ptr().add(base),
271						tails[1].as_ptr().add(base),
272						tails[2].as_ptr().add(base),
273						tails[3].as_ptr().add(base),
274					],
275				);
276			}
277
278			// Serialize each state as big-endian a..h, the standard SHA-256 digest byte order.
279			let mut out = [[0u8; 32]; 4];
280			for i in 0..4 {
281				let be_lo = vrev32q_u8(vreinterpretq_u8_u32(abcd[i]));
282				let be_hi = vrev32q_u8(vreinterpretq_u8_u32(efgh[i]));
283				vst1q_u8(out[i].as_mut_ptr(), be_lo);
284				vst1q_u8(out[i].as_mut_ptr().add(16), be_hi);
285			}
286			out
287		}
288	}
289}
290
291#[cfg(test)]
292mod tests {
293	use proptest::prelude::*;
294	use sha2::{Digest, Sha256, block_api::compress256};
295
296	use super::{compress256_x4, sha256_x4};
297
298	proptest! {
299		#[test]
300		fn compress256_x4_matches_the_block_reference(
301			states in prop::array::uniform4(prop::array::uniform8(any::<u32>())),
302			blocks in prop::array::uniform4(any::<[u8; 64]>()),
303		) {
304			// Advance four independent random states through the four-way kernel.
305			// Random states pin the raw-compression contract, not just the fixed IV.
306			let mut got = states;
307			compress256_x4(&mut got, [&blocks[0], &blocks[1], &blocks[2], &blocks[3]]);
308
309			// Each lane must equal the portable single-stream block function on its own input.
310			for i in 0..4 {
311				let mut want = states[i];
312				compress256(&mut want, std::slice::from_ref(&blocks[i]));
313				prop_assert_eq!(got[i], want, "mismatch at lane {}", i);
314			}
315		}
316	}
317
318	proptest! {
319		#[test]
320		fn matches_sha2_reference(
321			inputs in prop::array::uniform4(prop::collection::vec(any::<u8>(), 0..300)),
322		) {
323			// All four inputs must share a length, so pad each to the longest with zeros.
324			let len = inputs.iter().map(Vec::len).max().unwrap();
325			let padded: [Vec<u8>; 4] = std::array::from_fn(|i| {
326				let mut v = inputs[i].clone();
327				v.resize(len, 0);
328				v
329			});
330			let refs: [&[u8]; 4] = std::array::from_fn(|i| padded[i].as_slice());
331
332			let got = sha256_x4(refs);
333			for i in 0..4 {
334				let want: [u8; 32] = Sha256::digest(refs[i]).into();
335				prop_assert_eq!(got[i], want, "mismatch at lane {}", i);
336			}
337		}
338	}
339
340	#[test]
341	fn matches_sha2_at_padding_boundaries() {
342		for &len in &[0usize, 1, 55, 56, 63, 64, 65, 119, 120, 128, 256] {
343			// Distinct bytes per lane so the lanes cannot accidentally coincide.
344			let data: [Vec<u8>; 4] = std::array::from_fn(|i| {
345				(0..len)
346					.map(|j| (j as u8).wrapping_add(i as u8 * 37))
347					.collect()
348			});
349			let refs: [&[u8]; 4] = std::array::from_fn(|i| data[i].as_slice());
350
351			let got = sha256_x4(refs);
352			for i in 0..4 {
353				let want: [u8; 32] = Sha256::digest(refs[i]).into();
354				assert_eq!(got[i], want, "mismatch at len {len}, lane {i}");
355			}
356		}
357	}
358}