Skip to main content

binius_hash/
sha256.rs

1// Copyright 2024-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! SHA-256 compression function for use in Merkle tree constructions.
5
6use std::mem::MaybeUninit;
7
8use binius_utils::{
9	FixedSizeSerializeBytes, SerializeBytes,
10	rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator},
11};
12use bytemuck::{bytes_of_mut, must_cast};
13use digest::Digest;
14use sha2::{Sha256, block_api::compress256, digest::Output};
15
16use super::{
17	binary_merkle_tree::HashSuite,
18	compress::CompressionFunction,
19	parallel_compression::ParallelPseudoCompression,
20	parallel_digest::{ParallelDigest, ParallelDigestAdapter},
21	sha256_x4::compress256_x4,
22};
23
24/// Hashes every leaf through the four-way interleaved SHA-256 kernel.
25///
26/// The leaves serialize into one contiguous buffer, then hash four at a time.
27/// The serialize pass is cheap next to the compression work it feeds.
28///
29/// The caller guarantees the leaf count is a nonzero multiple of four.
30/// So every group of four is full.
31#[cfg(all(target_arch = "aarch64", target_feature = "sha2"))]
32fn digest_with_const_len_x4<I: IntoIterator<Item: FixedSizeSerializeBytes>>(
33	n_items_per_input: usize,
34	source: impl IndexedParallelIterator<Item = I>,
35	out: &mut [MaybeUninit<Output<Sha256>>],
36) {
37	use binius_utils::rayon::slice::{ParallelSlice, ParallelSliceMut};
38
39	let leaf_len = n_items_per_input * <I::Item as FixedSizeSerializeBytes>::BYTE_SIZE;
40
41	// Serialize each leaf's bytes into its slot of a contiguous buffer, one leaf per task.
42	let mut leaf_bytes = vec![0u8; out.len() * leaf_len];
43	source
44		.zip(leaf_bytes.par_chunks_mut(leaf_len))
45		.for_each(|(items, dst)| {
46			let mut cursor = &mut dst[..];
47			for item in items {
48				item.serialize(&mut cursor)
49					.expect("pre-condition: items serialize without error");
50			}
51			debug_assert!(cursor.is_empty(), "pre-condition: each leaf serializes to leaf_len");
52		});
53
54	// Hash four adjacent leaves at once, writing the four digests into their output slots.
55	out.par_chunks_mut(4)
56		.zip(leaf_bytes.par_chunks(4 * leaf_len))
57		.for_each(|(out4, bytes4)| {
58			let inputs: [&[u8]; 4] =
59				std::array::from_fn(|i| &bytes4[i * leaf_len..(i + 1) * leaf_len]);
60			let digests = crate::sha256_x4::sha256_x4(inputs);
61			for (slot, digest) in out4.iter_mut().zip(digests) {
62				let mut hash = Output::<Sha256>::default();
63				hash.copy_from_slice(&digest);
64				slot.write(hash);
65			}
66		});
67}
68
69/// SHA-256 initial hash values, used as the starting state for a raw block compression.
70const SHA256_IV: [u32; 8] = [
71	0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
72];
73
74/// The largest leaf, in bytes, that still fits (together with SHA-256 padding) in a single
75/// 64-byte block: one byte for the `0x80` terminator and eight for the big-endian bit length.
76const SINGLE_BLOCK_MAX_LEN: usize = 64 - 1 - 8;
77
78/// A two-to-one compression function for SHA-256 digests.
79#[derive(Debug, Clone)]
80pub struct Sha256Compression {
81	initial_state: [u32; 8],
82}
83
84impl Default for Sha256Compression {
85	fn default() -> Self {
86		let initial_state_bytes = Sha256::digest(b"BINIUS SHA-256 COMPRESS");
87		let mut initial_state = [0u32; 8];
88		bytes_of_mut(&mut initial_state).copy_from_slice(&initial_state_bytes);
89		Self { initial_state }
90	}
91}
92
93impl CompressionFunction<Output<Sha256>, 2> for Sha256Compression {
94	fn compress(&self, input: [Output<Sha256>; 2]) -> Output<Sha256> {
95		let mut ret = self.initial_state;
96		let mut block = [0u8; 64];
97		block[..32].copy_from_slice(input[0].as_slice());
98		block[32..].copy_from_slice(input[1].as_slice());
99		compress256(&mut ret, &[block]);
100		must_cast::<[u32; 8], [u8; 32]>(ret).into()
101	}
102}
103
104/// SHA-256 [`HashSuite`]: SHA-256 leaves and a SHA-256 compression function for inner nodes.
105#[derive(Debug, Clone, Default)]
106pub struct Sha256HashSuite;
107
108impl HashSuite for Sha256HashSuite {
109	type LeafHash = Sha256;
110	type Compression = Sha256Compression;
111	type ParLeafHash = ParallelSha256Digest;
112	type ParCompression = ParallelSha256Compression;
113}
114
115/// Parallel SHA-256 two-to-one compression for the inner nodes of a Merkle tree.
116///
117/// Groups of four independent compressions run through the interleaved four-way block kernel.
118/// A trailing group smaller than four compresses one node at a time.
119/// Every output byte equals compressing each node on its own with the scalar function.
120#[derive(Debug, Clone, Default)]
121pub struct ParallelSha256Compression {
122	/// The scalar two-to-one compression whose output the grouped path reproduces exactly.
123	compression: Sha256Compression,
124}
125
126impl ParallelPseudoCompression<Output<Sha256>, 2> for ParallelSha256Compression {
127	type Compression = Sha256Compression;
128
129	fn compression(&self) -> &Self::Compression {
130		&self.compression
131	}
132
133	fn parallel_compress(
134		&self,
135		inputs: &[Output<Sha256>],
136		out: &mut [MaybeUninit<Output<Sha256>>],
137	) {
138		use binius_utils::rayon::slice::{ParallelSlice, ParallelSliceMut};
139
140		assert_eq!(inputs.len(), 2 * out.len(), "Input length must be N * output length");
141
142		// Split into full groups of four compressions and a short tail.
143		//
144		//     inputs:  [ 8 digests | 8 digests | ... | tail (< 8) ]
145		//     out:     [ 4 nodes   | 4 nodes   | ... | tail (< 4) ]
146		let n_groups = out.len() / 4;
147		let (input_groups, input_tail) = inputs.split_at(8 * n_groups);
148		let (out_groups, out_tail) = out.split_at_mut(4 * n_groups);
149
150		// Each group packs four sibling pairs into four one-block messages.
151		// One interleaved call then advances all four states at once.
152		input_groups
153			.par_chunks_exact(8)
154			.zip(out_groups.par_chunks_exact_mut(4))
155			.for_each(|(pairs, out4)| {
156				// Pack each sibling pair into one 64-byte message block: left child then right.
157				let mut blocks = [[0u8; 64]; 4];
158				for (block, pair) in blocks.iter_mut().zip(pairs.chunks_exact(2)) {
159					block[..32].copy_from_slice(&pair[0]);
160					block[32..].copy_from_slice(&pair[1]);
161				}
162
163				// All four lanes start from the same fixed initial state.
164				let mut states = [self.compression.initial_state; 4];
165				compress256_x4(&mut states, [&blocks[0], &blocks[1], &blocks[2], &blocks[3]]);
166
167				// Serialize each advanced state in native word order, as the scalar path does.
168				for (slot, state) in out4.iter_mut().zip(states) {
169					slot.write(must_cast::<[u32; 8], [u8; 32]>(state).into());
170				}
171			});
172
173		// The tail (at most three nodes) compresses one pair at a time.
174		for (slot, pair) in out_tail.iter_mut().zip(input_tail.chunks_exact(2)) {
175			slot.write(self.compression.compress([pair[0], pair[1]]));
176		}
177	}
178}
179
180/// A [`ParallelDigest`] for SHA-256 that specializes
181/// [`digest_with_const_len`](ParallelDigest::digest_with_const_len) for short, fixed-length
182/// leaves.
183///
184/// When every leaf serializes to at most `SINGLE_BLOCK_MAX_LEN` bytes, the whole leaf — message,
185/// padding, and length suffix — fits in one 64-byte block, so the digest is a single call to the
186/// raw [`compress256`] block function starting from the SHA-256 IV. This skips the `update`/
187/// `finalize` bookkeeping that the generic [`ParallelDigestAdapter`] performs per leaf.
188///
189/// Longer leaves fall back to [`ParallelDigestAdapter`].
190#[derive(Debug, Clone, Default)]
191pub struct ParallelSha256Digest;
192
193impl ParallelDigest for ParallelSha256Digest {
194	type Digest = Sha256;
195
196	fn new() -> Self {
197		Self
198	}
199
200	fn digest<I: IntoIterator<Item: SerializeBytes>>(
201		&self,
202		source: impl IndexedParallelIterator<Item = I>,
203		out: &mut [MaybeUninit<Output<Sha256>>],
204	) {
205		ParallelDigestAdapter::<Sha256>::new().digest(source, out);
206	}
207
208	fn digest_with_const_len<I: IntoIterator<Item: FixedSizeSerializeBytes>>(
209		&self,
210		n_items_per_input: usize,
211		source: impl IndexedParallelIterator<Item = I>,
212		out: &mut [MaybeUninit<Output<Sha256>>],
213	) {
214		// On aarch64 with the SHA extension, hash four leaves at once with the interleaved kernel.
215		// It needs full groups of four, which every power-of-two leaf count of at least four meets.
216		#[cfg(all(target_arch = "aarch64", target_feature = "sha2"))]
217		if out.len() >= 4 && out.len().is_multiple_of(4) {
218			digest_with_const_len_x4(n_items_per_input, source, out);
219			return;
220		}
221
222		let leaf_len = n_items_per_input * <I::Item as FixedSizeSerializeBytes>::BYTE_SIZE;
223		if leaf_len > SINGLE_BLOCK_MAX_LEN {
224			self.digest(source, out);
225			return;
226		}
227
228		// Precompute the padding suffix once: a `0x80` terminator immediately after the message,
229		// then zeros, then the 64-bit big-endian message bit length. Because `leaf_len` is constant
230		// for every leaf, this suffix is identical across leaves; each leaf only overwrites the
231		// `leaf_len`-byte message prefix.
232		let mut block_template = [0u8; 64];
233		block_template[leaf_len] = 0x80;
234		block_template[56..64].copy_from_slice(&((leaf_len as u64) * 8).to_be_bytes());
235
236		source
237			.zip(out.par_iter_mut())
238			.for_each_with(block_template, |block, (items, out)| {
239				// Overwrite the message prefix; the padding suffix stays untouched.
240				let mut cursor = &mut block[..leaf_len];
241				let mut n_items = 0;
242				for item in items {
243					item.serialize(&mut cursor)
244						.expect("pre-condition: items must serialize without error");
245					n_items += 1;
246				}
247				debug_assert_eq!(n_items, n_items_per_input);
248				debug_assert!(cursor.is_empty(), "pre-condition: each leaf serializes to leaf_len");
249
250				let mut state = SHA256_IV;
251				compress256(&mut state, std::slice::from_ref(&*block));
252
253				// SHA-256 emits its state words in big-endian byte order.
254				let mut digest = Output::<Sha256>::default();
255				for (chunk, word) in digest.chunks_exact_mut(4).zip(state) {
256					chunk.copy_from_slice(&word.to_be_bytes());
257				}
258				out.write(digest);
259			});
260	}
261}
262
263#[cfg(test)]
264mod tests {
265	use std::iter::repeat_with;
266
267	use binius_utils::rayon::iter::{IntoParallelRefIterator, ParallelIterator};
268	use rand::{Rng, RngExt, SeedableRng, rngs::StdRng};
269
270	use super::*;
271	use crate::parallel_compression::ParallelCompressionAdaptor;
272
273	#[test]
274	fn test_parallel_sha256_compression_matches_adaptor() {
275		let mut rng = StdRng::seed_from_u64(0);
276
277		// Invariant: the grouped four-way path equals per-node scalar compression byte for byte.
278		//
279		// Node counts crossing every regime of the grouping:
280		//
281		//     1, 2, 3   → tail only (the top Merkle layers)
282		//     4         → exactly one full group
283		//     5, 7      → full group plus tail
284		//     8, 64     → several full groups (64 = a wide tree layer)
285		for n_nodes in [1usize, 2, 3, 4, 5, 7, 8, 64] {
286			// Two random child digests per output node.
287			let inputs: Vec<Output<Sha256>> = repeat_with(|| {
288				let mut digest = Output::<Sha256>::default();
289				rng.fill_bytes(&mut digest);
290				digest
291			})
292			.take(2 * n_nodes)
293			.collect();
294
295			// Compress with the grouped four-way path.
296			let grouped = ParallelSha256Compression::default();
297			let mut got = repeat_with(MaybeUninit::<Output<Sha256>>::uninit)
298				.take(n_nodes)
299				.collect::<Vec<_>>();
300			grouped.parallel_compress(&inputs, &mut got);
301
302			// Compress every node one at a time through the scalar function as the reference.
303			let adaptor = ParallelCompressionAdaptor::new(Sha256Compression::default());
304			let mut want = repeat_with(MaybeUninit::<Output<Sha256>>::uninit)
305				.take(n_nodes)
306				.collect::<Vec<_>>();
307			adaptor.parallel_compress(&inputs, &mut want);
308
309			for (i, (got_i, want_i)) in got.iter().zip(&want).enumerate() {
310				// Safety: the compression calls above initialize every output slot.
311				let (got_i, want_i) =
312					unsafe { (got_i.assume_init_ref(), want_i.assume_init_ref()) };
313				assert_eq!(got_i, want_i, "mismatch at node {i} of {n_nodes}");
314			}
315		}
316	}
317
318	/// Checks that the specialized digest matches `Sha256::digest` over the serialized leaf bytes,
319	/// covering both the single-block fast path and the multi-block fallback.
320	#[test]
321	fn test_parallel_sha256_matches_serial() {
322		let mut rng = StdRng::seed_from_u64(0);
323		// `u128` serializes to 16 little-endian bytes, so leaf lengths are 16, 32, 48 (single
324		// block) and 64 (> SINGLE_BLOCK_MAX_LEN, exercises the fallback).
325		for n_items_per_input in [1, 2, 3, 4] {
326			let n_leaves = 50;
327			let leaves: Vec<Vec<u128>> = (0..n_leaves)
328				.map(|_| {
329					(0..n_items_per_input)
330						.map(|_| rng.random::<u128>())
331						.collect()
332				})
333				.collect();
334
335			let digest = ParallelSha256Digest::new();
336			let mut results = repeat_with(MaybeUninit::<Output<Sha256>>::uninit)
337				.take(n_leaves)
338				.collect::<Vec<_>>();
339			digest.digest_with_const_len(
340				n_items_per_input,
341				leaves.par_iter().map(|leaf| leaf.iter().copied()),
342				&mut results,
343			);
344
345			for (result, leaf) in results.into_iter().zip(&leaves) {
346				let mut bytes = Vec::new();
347				for &item in leaf {
348					bytes.extend_from_slice(&item.to_le_bytes());
349				}
350				assert_eq!(unsafe { result.assume_init() }, <Sha256 as Digest>::digest(&bytes));
351			}
352		}
353	}
354}