Skip to main content

binius_hash/
binary_merkle_tree.rs

1// Copyright 2024-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use std::{fmt::Debug, mem::MaybeUninit};
5
6use binius_field::Field;
7use binius_utils::{
8	checked_arithmetics::log2_strict_usize,
9	rand::par_rand,
10	rayon::{prelude::*, slice::ParallelSlice},
11};
12use digest::{Digest, FixedOutputReset, Output, block_api::BlockSizeUser};
13use rand::{CryptoRng, Rng, rngs::StdRng};
14
15use super::{
16	compress::CompressionFunction, parallel_compression::ParallelPseudoCompression,
17	parallel_digest::ParallelDigest,
18};
19
20/// A bundle of hash and compression types used to build and verify a binary Merkle tree.
21///
22/// Most callers want to vary the underlying hash family (SHA-256, etc.) as a single unit
23/// rather than independently picking a leaf hash, a compression function, and their parallel
24/// counterparts. `HashSuite` bundles the four related types so that user-facing prover and
25/// verifier APIs can take a single `H: HashSuite` parameter instead of two or three loose hash
26/// trait parameters.
27pub trait HashSuite {
28	/// Sequential hash used to compute leaf digests during verification.
29	type LeafHash: Digest + BlockSizeUser + FixedOutputReset + Send;
30	/// Sequential 2-to-1 compression used to fold inner Merkle nodes during verification.
31	type Compression: CompressionFunction<Output<Self::LeafHash>, 2> + Default;
32	/// Parallel counterpart of [`Self::LeafHash`] used during proving.
33	type ParLeafHash: ParallelDigest<Digest = Self::LeafHash> + Default;
34	/// Parallel counterpart of [`Self::Compression`] used during proving.
35	type ParCompression: ParallelPseudoCompression<Output<Self::LeafHash>, 2, Compression = Self::Compression>
36		+ Default;
37}
38
39#[derive(Debug, thiserror::Error)]
40pub enum Error {
41	#[error("Index exceeds Merkle tree base size: {max}")]
42	IndexOutOfRange { max: usize },
43	#[error("values length must be a multiple of the batch size")]
44	IncorrectBatchSize,
45	#[error("The argument length must be a power of two.")]
46	PowerOfTwoLengthRequired,
47	#[error("The layer does not exist in the Merkle tree")]
48	IncorrectLayerDepth,
49}
50
51/// A binary Merkle tree that commits batches of vectors.
52///
53/// The vector entries at each index in a batch are hashed together into leaf digests. Then a
54/// Merkle tree is constructed over the leaf digests. The implementation requires that the vector
55/// lengths are all equal to each other and a power of two.
56#[derive(Debug, Clone)]
57pub struct BinaryMerkleTree<D, F> {
58	/// Base-2 logarithm of the number of leaves
59	pub log_len: usize,
60	/// The inner nodes, arranged as a flattened array of layers with the root at the end
61	pub inner_nodes: Vec<D>,
62	/// Salt values for each leaf (if using hiding commitments)
63	pub salts: Vec<F>,
64}
65
66pub fn build<F, H, R>(
67	elements: &[F],
68	batch_size: usize,
69	salt_len: usize,
70	rng: R,
71) -> Result<BinaryMerkleTree<Output<H::LeafHash>, F>, Error>
72where
73	F: Field,
74	H: HashSuite,
75	R: Rng + CryptoRng,
76{
77	if !elements.len().is_multiple_of(batch_size) {
78		return Err(Error::IncorrectBatchSize);
79	}
80
81	let len = elements.len() / batch_size;
82
83	if !len.is_power_of_two() {
84		return Err(Error::PowerOfTwoLengthRequired);
85	}
86
87	build_from_iterator::<_, H, _, _>(
88		elements
89			.par_chunks(batch_size)
90			.map(|chunk| chunk.iter().copied()),
91		batch_size,
92		salt_len,
93		rng,
94	)
95}
96
97pub fn build_from_iterator<F, H, R, ParIter>(
98	iterated_chunks: ParIter,
99	n_items_per_input: usize,
100	salt_len: usize,
101	mut rng: R,
102) -> Result<BinaryMerkleTree<Output<H::LeafHash>, F>, Error>
103where
104	F: Field,
105	H: HashSuite,
106	R: Rng + CryptoRng,
107	ParIter: IndexedParallelIterator<Item: IntoIterator<Item = F, IntoIter: Send>>,
108{
109	let log_len = log2_strict_usize(iterated_chunks.len()); // precondition
110
111	// Generate salts if needed
112	let salts =
113		par_rand::<StdRng, _, _>(salt_len << log_len, &mut rng, F::random).collect::<Vec<_>>();
114
115	let total_length = (1 << (log_len + 1)) - 1;
116	let mut inner_nodes = Vec::with_capacity(total_length);
117	hash_leaves::<F, H, _>(
118		iterated_chunks,
119		n_items_per_input,
120		&mut inner_nodes.spare_capacity_mut()[..(1 << log_len)],
121		&salts,
122	);
123
124	let (prev_layer, mut remaining) = inner_nodes.spare_capacity_mut().split_at_mut(1 << log_len);
125
126	let mut prev_layer = unsafe {
127		// SAFETY: prev-layer was initialized by hash_leaves
128		prev_layer.assume_init_mut()
129	};
130	let parallel_compression = H::ParCompression::default();
131	for i in 1..(log_len + 1) {
132		let (next_layer, next_remaining) = remaining.split_at_mut(1 << (log_len - i));
133		remaining = next_remaining;
134
135		parallel_compression.parallel_compress(prev_layer, next_layer);
136
137		prev_layer = unsafe {
138			// SAFETY: next_layer was just initialized by compress_layer
139			next_layer.assume_init_mut()
140		};
141	}
142
143	unsafe {
144		// SAFETY: inner_nodes should be entirely initialized by now
145		// Note that we don't incrementally update inner_nodes.len() since
146		// that doesn't play well with using split_at_mut on spare capacity.
147		inner_nodes.set_len(total_length);
148	}
149	Ok(BinaryMerkleTree {
150		log_len,
151		inner_nodes,
152		salts,
153	})
154}
155
156impl<D: Clone, F> BinaryMerkleTree<D, F> {
157	pub fn root(&self) -> D {
158		self.inner_nodes
159			.last()
160			.expect("MerkleTree inner nodes can't be empty")
161			.clone()
162	}
163
164	/// Returns the salt values associated with a specific leaf index in the Merkle tree.
165	///
166	/// # Arguments
167	/// * `index` - The index of the leaf. Must be less than 2^log_len (the total number of leaves).
168	pub fn get_salt(&self, index: usize) -> &[F] {
169		assert!(index < (1 << self.log_len));
170		let salt_len = self.salts.len() >> self.log_len;
171		&self.salts[index * salt_len..(index + 1) * salt_len]
172	}
173
174	pub fn layer(&self, layer_depth: usize) -> Result<&[D], Error> {
175		if layer_depth > self.log_len {
176			return Err(Error::IncorrectLayerDepth);
177		}
178		let range_start = self.inner_nodes.len() + 1 - (1 << (layer_depth + 1));
179
180		Ok(&self.inner_nodes[range_start..range_start + (1 << layer_depth)])
181	}
182
183	/// Get a Merkle branch for the given index
184	///
185	/// Throws if the index is out of range
186	pub fn branch(&self, index: usize, layer_depth: usize) -> Result<Vec<D>, Error> {
187		if index >= 1 << self.log_len || layer_depth > self.log_len {
188			return Err(Error::IndexOutOfRange {
189				max: (1 << self.log_len) - 1,
190			});
191		}
192
193		let branch = (0..self.log_len - layer_depth)
194			.map(|j| {
195				let node_index = (((1 << j) - 1) << (self.log_len + 1 - j)) | (index >> j) ^ 1;
196				self.inner_nodes[node_index].clone()
197			})
198			.collect();
199
200		Ok(branch)
201	}
202}
203
204/// Hashes the elements in chunks of a vector into digests.
205///
206/// Given a vector of elements and an output buffer of N hash digests, this splits the elements
207/// into N equal-sized chunks and hashes each chunks into the corresponding output digest.
208///
209/// Each leaf is built from exactly `n_items_per_input` data elements (plus the per-leaf salt, when
210/// salts are present), so the leaf byte length is constant. This is passed to
211/// [`ParallelDigest::digest_with_const_len`] so the hasher can specialize for short leaves.
212///
213/// # Preconditions
214/// - Each iterator in `iterated_chunks` yields exactly `n_items_per_input` elements.
215#[tracing::instrument("hash_leaves", skip_all, level = "debug")]
216fn hash_leaves<F, H, ParIter>(
217	iterated_chunks: ParIter,
218	n_items_per_input: usize,
219	digests: &mut [MaybeUninit<Output<H::LeafHash>>],
220	salts: &[F],
221) where
222	F: Field,
223	H: HashSuite,
224	ParIter: IndexedParallelIterator<Item: IntoIterator<Item = F, IntoIter: Send>>,
225{
226	if salts.is_empty() {
227		// Need special-case handling when salts is empty, otherwise salt_len is 0 and par_chunks
228		// cannot handle chunk size of 0.
229		let hasher = H::ParLeafHash::default();
230		hasher.digest_with_const_len(n_items_per_input, iterated_chunks, digests);
231	} else {
232		assert!(salts.len().is_multiple_of(digests.len()));
233
234		let salt_len = salts.len() / digests.len();
235
236		// Create an iterator that chains each chunk with its salt
237		let salted_iter = iterated_chunks
238			.zip(salts.par_chunks(salt_len))
239			.map(|(chunk, salt)| chunk.into_iter().chain(salt.iter().copied()));
240
241		// Each salted leaf yields the data elements followed by the salt elements.
242		let hasher = H::ParLeafHash::default();
243		hasher.digest_with_const_len(n_items_per_input + salt_len, salted_iter, digests);
244	}
245}