Skip to content

Commit db4c440

Browse files
committed
[frontend] Add merkle tree verifier
1 parent 914b028 commit db4c440

File tree

2 files changed

+334
-0
lines changed

2 files changed

+334
-0
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
use super::hashing::circuit_tree_hash;
2+
use crate::{
3+
circuits::{keccak::Keccak, multiplexer::multi_wire_multiplex},
4+
compiler::{CircuitBuilder, Wire},
5+
};
6+
7+
/// Verifies a Merkle tree authentication path.
8+
///
9+
/// This circuit verifies that a given leaf hash is part of a Merkle tree
10+
/// by reconstructing the path from leaf to root using the provided
11+
/// authentication path (sibling hashes).
12+
///
13+
/// # Arguments
14+
///
15+
/// * `builder` - Circuit builder for constructing constraints
16+
/// * `domain_param` - Cryptographic domain parameter (32 bytes as 4x64-bit LE wires)
17+
/// * `domain_param_len` - Actual byte length of the parameter (must be less than or equal to
18+
/// domain_param.len() * 8)
19+
/// * `leaf_hash` - The leaf hash to verify (32 bytes as 4x64-bit LE wires)
20+
/// * `leaf_index` - Index of the leaf in the tree (as a wire)
21+
/// * `auth_path` - Authentication path: sibling hashes from leaf to root
22+
/// * `root_hash` - Expected root hash (32 bytes as 4x64-bit LE wires)
23+
///
24+
/// # Returns
25+
///
26+
/// A vector of Keccak hashers that need witness population
27+
pub fn circuit_merkle_path(
28+
builder: &CircuitBuilder,
29+
domain_param: &[Wire],
30+
domain_param_len: usize,
31+
leaf_hash: &[Wire; 4],
32+
leaf_index: Wire,
33+
auth_path: &[[Wire; 4]],
34+
root_hash: &[Wire; 4],
35+
) -> Vec<Keccak> {
36+
assert!(
37+
domain_param_len <= domain_param.len() * 8,
38+
"domain_param_len {} exceeds maximum capacity {} of domain_param wires",
39+
domain_param_len,
40+
domain_param.len() * 8
41+
);
42+
43+
let tree_height = auth_path.len();
44+
let mut hashers = Vec::with_capacity(tree_height);
45+
let mut current_hash = *leaf_hash;
46+
let mut current_index = leaf_index;
47+
let one = builder.add_constant_64(1);
48+
49+
// Process each level of the tree
50+
for level in 0..tree_height {
51+
let sibling_hash = auth_path[level];
52+
53+
// Determine if current node is left or right child
54+
// If current_index is even (LSB = 0), current is left child
55+
// If current_index is odd (LSB = 1), current is right child
56+
let is_left = builder.bnot(builder.band(current_index, one));
57+
58+
// Select left and right hashes based on position
59+
// If is_left: left = current, right = sibling
60+
// If !is_left: left = sibling, right = current
61+
let left_hash = multi_wire_multiplex(builder, &[&sibling_hash, &current_hash], is_left)
62+
.try_into()
63+
.expect("multi_wire_multiplex should return 4 wires");
64+
let right_hash = multi_wire_multiplex(builder, &[&current_hash, &sibling_hash], is_left)
65+
.try_into()
66+
.expect("multi_wire_multiplex should return 4 wires");
67+
68+
// Compute parent index: parent_index = current_index / 2
69+
let parent_index = builder.shr(current_index, 1);
70+
71+
// Create output hash wire for this level
72+
let parent_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_witness());
73+
74+
// Create tree node hasher
75+
let level_wire = builder.add_constant_64(level as u64);
76+
let hasher = circuit_tree_hash(
77+
builder,
78+
domain_param.to_vec(),
79+
domain_param_len,
80+
left_hash,
81+
right_hash,
82+
level_wire,
83+
parent_index,
84+
parent_hash,
85+
);
86+
87+
hashers.push(hasher);
88+
89+
// Move up the tree
90+
current_hash = parent_hash;
91+
current_index = parent_index;
92+
}
93+
94+
// Assert that the final hash matches the expected root
95+
builder.assert_eq_v("merkle_root_check", current_hash, *root_hash);
96+
97+
hashers
98+
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use binius_core::Word;
103+
use sha3::{Digest, Keccak256};
104+
105+
use super::*;
106+
use crate::{
107+
circuits::hash_based_sig::hashing::{TREE_TWEAK, build_tree_hash},
108+
constraint_verifier::verify_constraints,
109+
util::pack_bytes_into_wires_le,
110+
};
111+
112+
/// Helper to compute tree node hash using Keccak
113+
fn hash_tree_node_keccak(
114+
param: &[u8],
115+
left: &[u8; 32],
116+
right: &[u8; 32],
117+
level: u32,
118+
index: u32,
119+
) -> [u8; 32] {
120+
let mut hasher = Keccak256::new();
121+
hasher.update(param);
122+
hasher.update([TREE_TWEAK]);
123+
hasher.update(level.to_le_bytes());
124+
hasher.update(index.to_le_bytes());
125+
hasher.update(left);
126+
hasher.update(right);
127+
hasher.finalize().into()
128+
}
129+
130+
#[test]
131+
fn test_circuit_merkle_path_verification() {
132+
// Build a simple 4-leaf tree for testing
133+
// Tree structure:
134+
// root
135+
// / \
136+
// n2 n3
137+
// / \ / \
138+
// l0 l1 l2 l3
139+
140+
let builder = CircuitBuilder::new();
141+
142+
// Create input wires
143+
let domain_param: Vec<Wire> = (0..4).map(|_| builder.add_inout()).collect();
144+
let leaf_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
145+
let leaf_index = builder.add_inout();
146+
let root_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
147+
148+
// Authentication path (2 levels for 4-leaf tree)
149+
let auth_path: Vec<[Wire; 4]> = (0..2)
150+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
151+
.collect();
152+
153+
// Create the verification circuit
154+
let hashers = circuit_merkle_path(
155+
&builder,
156+
&domain_param,
157+
domain_param.len() * 8,
158+
&leaf_hash,
159+
leaf_index,
160+
&auth_path,
161+
&root_hash,
162+
);
163+
164+
let circuit = builder.build();
165+
let mut w = circuit.new_witness_filler();
166+
167+
// Set up test data
168+
let param_bytes = b"test_merkle_tree_parameter!!!!!!";
169+
pack_bytes_into_wires_le(&mut w, &domain_param, param_bytes);
170+
171+
// Create leaf hashes
172+
let leaf0 = b"leaf_0_hash_value_32_bytes!!!!!!";
173+
let leaf1 = b"leaf_1_hash_value_32_bytes!!!!!!";
174+
let leaf2 = b"leaf_2_hash_value_32_bytes!!!!!!";
175+
let leaf3 = b"leaf_3_hash_value_32_bytes!!!!!!";
176+
177+
// Compute internal nodes
178+
let node2 = hash_tree_node_keccak(param_bytes, leaf0, leaf1, 0, 0);
179+
let node3 = hash_tree_node_keccak(param_bytes, leaf2, leaf3, 0, 1);
180+
let root = hash_tree_node_keccak(param_bytes, &node2, &node3, 1, 0);
181+
182+
// Test verification for leaf 1 (index 1)
183+
// Path: [leaf0 (sibling at level 0), node3 (sibling at level 1)]
184+
pack_bytes_into_wires_le(&mut w, &leaf_hash, leaf1);
185+
w[leaf_index] = Word::from_u64(1);
186+
pack_bytes_into_wires_le(&mut w, &auth_path[0], leaf0);
187+
pack_bytes_into_wires_le(&mut w, &auth_path[1], &node3);
188+
pack_bytes_into_wires_le(&mut w, &root_hash, &root);
189+
190+
// Populate hashers
191+
// Level 0: hash(leaf0, leaf1) with index 0
192+
let hasher0 = &hashers[0];
193+
let message0 = build_tree_hash(param_bytes, leaf0, leaf1, 0, 0);
194+
hasher0.populate_message(&mut w, &message0);
195+
hasher0.populate_digest(&mut w, node2);
196+
197+
// Level 1: hash(node2, node3) with index 0
198+
let hasher1 = &hashers[1];
199+
let message1 = build_tree_hash(param_bytes, &node2, &node3, 1, 0);
200+
hasher1.populate_message(&mut w, &message1);
201+
hasher1.populate_digest(&mut w, root);
202+
203+
// Populate witness and verify
204+
circuit.populate_wire_witness(&mut w).unwrap();
205+
206+
let cs = circuit.constraint_system();
207+
verify_constraints(cs, &w.into_value_vec()).unwrap();
208+
}
209+
210+
#[test]
211+
fn test_negative_circuit_merkle_invalid_auth_path() {
212+
// Circuit does not verify invalid auth path
213+
let builder = CircuitBuilder::new();
214+
215+
let param: Vec<Wire> = (0..4).map(|_| builder.add_inout()).collect();
216+
let leaf_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
217+
let leaf_index = builder.add_inout();
218+
let root_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
219+
let auth_path: Vec<[Wire; 4]> = (0..2)
220+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
221+
.collect();
222+
223+
let hashers = circuit_merkle_path(
224+
&builder,
225+
&param,
226+
param.len() * 8,
227+
&leaf_hash,
228+
leaf_index,
229+
&auth_path,
230+
&root_hash,
231+
);
232+
233+
let circuit = builder.build();
234+
let mut w = circuit.new_witness_filler();
235+
236+
let param_bytes = b"test_merkle_tree_parameter!!!!!!";
237+
pack_bytes_into_wires_le(&mut w, &param, param_bytes);
238+
239+
let leaf0 = b"leaf_0_hash_value_32_bytes!!!!!!";
240+
let leaf1 = b"leaf_1_hash_value_32_bytes!!!!!!";
241+
let leaf2 = b"leaf_2_hash_value_32_bytes!!!!!!";
242+
let leaf3 = b"leaf_3_hash_value_32_bytes!!!!!!";
243+
244+
let node2 = hash_tree_node_keccak(param_bytes, leaf0, leaf1, 0, 0);
245+
let node3 = hash_tree_node_keccak(param_bytes, leaf2, leaf3, 0, 1);
246+
let root = hash_tree_node_keccak(param_bytes, &node2, &node3, 1, 0);
247+
248+
// Test verification for leaf 2 (index 2)
249+
// Path: [leaf3 (sibling at level 0), node2 incorrect!]
250+
pack_bytes_into_wires_le(&mut w, &leaf_hash, leaf2);
251+
w[leaf_index] = Word::from_u64(2);
252+
pack_bytes_into_wires_le(&mut w, &auth_path[0], leaf3);
253+
pack_bytes_into_wires_le(&mut w, &auth_path[1], &node3);
254+
pack_bytes_into_wires_le(&mut w, &root_hash, &root);
255+
256+
// Populate hashers
257+
// Level 0: hash(leaf2, leaf3) with index 1
258+
let hasher0 = &hashers[0];
259+
let message0 = build_tree_hash(param_bytes, leaf2, leaf3, 0, 1);
260+
hasher0.populate_message(&mut w, &message0);
261+
hasher0.populate_digest(&mut w, node3);
262+
263+
// Level 1: hash(node2, node3) with index 0
264+
let hasher1 = &hashers[1];
265+
let message1 = build_tree_hash(param_bytes, &node2, &node3, 1, 0);
266+
hasher1.populate_message(&mut w, &message1);
267+
hasher1.populate_digest(&mut w, root);
268+
269+
assert!(circuit.populate_wire_witness(&mut w).is_err());
270+
}
271+
272+
#[test]
273+
fn test_negative_circuit_merkle_invalid_hash_population() {
274+
// The circuit does not verify an invalid hash population
275+
let builder = CircuitBuilder::new();
276+
277+
let param: Vec<Wire> = (0..4).map(|_| builder.add_inout()).collect();
278+
let leaf_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
279+
let leaf_index = builder.add_inout();
280+
let root_hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout());
281+
let auth_path: Vec<[Wire; 4]> = (0..2)
282+
.map(|_| std::array::from_fn(|_| builder.add_inout()))
283+
.collect();
284+
285+
let hashers = circuit_merkle_path(
286+
&builder,
287+
&param,
288+
param.len() * 8,
289+
&leaf_hash,
290+
leaf_index,
291+
&auth_path,
292+
&root_hash,
293+
);
294+
295+
let circuit = builder.build();
296+
let mut w = circuit.new_witness_filler();
297+
298+
let param_bytes = b"test_merkle_tree_parameter!!!!!!";
299+
pack_bytes_into_wires_le(&mut w, &param, param_bytes);
300+
301+
let leaf0 = b"leaf_0_hash_value_32_bytes!!!!!!";
302+
let leaf1 = b"leaf_1_hash_value_32_bytes!!!!!!";
303+
let leaf2 = b"leaf_2_hash_value_32_bytes!!!!!!";
304+
let leaf3 = b"leaf_3_hash_value_32_bytes!!!!!!";
305+
306+
let node2 = hash_tree_node_keccak(param_bytes, leaf0, leaf1, 0, 0);
307+
let node3 = hash_tree_node_keccak(param_bytes, leaf2, leaf3, 0, 1);
308+
let root = hash_tree_node_keccak(param_bytes, &node2, &node3, 1, 0);
309+
310+
// Test verification for leaf 2 (index 2)
311+
// Path: [leaf3 (sibling at level 0), node2 (sibling at level 1)]
312+
pack_bytes_into_wires_le(&mut w, &leaf_hash, leaf2);
313+
w[leaf_index] = Word::from_u64(2);
314+
pack_bytes_into_wires_le(&mut w, &auth_path[0], leaf3);
315+
pack_bytes_into_wires_le(&mut w, &auth_path[1], &node2);
316+
pack_bytes_into_wires_le(&mut w, &root_hash, &root);
317+
318+
// Populate hashers
319+
// Level 0: hash(leaf2, leaf3) with index 1
320+
let hasher0 = &hashers[0];
321+
let message0 = build_tree_hash(param_bytes, leaf2, leaf3, 0, 1);
322+
hasher0.populate_message(&mut w, &message0);
323+
hasher0.populate_digest(&mut w, node3);
324+
325+
// Level 1: hash(node2, leaf3) with index 0 - incorrect!
326+
let hasher1 = &hashers[1];
327+
let message1 = build_tree_hash(param_bytes, &node2, leaf3, 1, 0);
328+
hasher1.populate_message(&mut w, &message1);
329+
hasher1.populate_digest(&mut w, root);
330+
331+
assert!(circuit.populate_wire_witness(&mut w).is_err());
332+
}
333+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod chain_verification;
22
pub mod codeword;
33
pub mod hashing;
4+
pub mod merkle_tree;
45
pub mod winternitz_ots;

0 commit comments

Comments
 (0)