Skip to main content

tasm_lib/hashing/
merkle_verify.rs

1use std::collections::HashMap;
2
3use triton_vm::prelude::*;
4
5use crate::prelude::*;
6use crate::traits::basic_snippet::Reviewer;
7use crate::traits::basic_snippet::SignOffFingerprint;
8
9/// Verify membership in a [Merkle tree](twenty_first::prelude::MerkleTree).
10///
11/// Verify that a leaf lives in a Merkle tree, given the tree's root, its
12/// height, the leaf's index, and the leaf itself. The authentication path is
13/// non-deterministically divined. This algorithm asserts that the leaf is a
14/// member of the tree; phrased differently, if membership could not be
15/// established, it crashes the VM.
16///
17/// ### Behavior
18///
19/// ```text
20/// BEFORE: _ [root: Digest] tree_height leaf_index [leaf: Digest]
21/// AFTER:  _
22/// ```
23///
24/// ### Preconditions
25///
26/// - all input arguments are properly [`BFieldCodec`] encoded
27///
28/// ### Postconditions
29///
30/// None.
31#[derive(Clone, Debug)]
32pub struct MerkleVerify;
33
34impl MerkleVerify {
35    pub const TREE_TOO_HIGH_ERROR_ID: i128 = 0;
36    pub const OUT_OF_BOUNDS_LEAF_ERROR_ID: i128 = 1;
37    pub const ROOT_MISMATCH_ERROR_ID: i128 = 2;
38}
39
40impl BasicSnippet for MerkleVerify {
41    fn parameters(&self) -> Vec<(DataType, String)> {
42        vec![
43            (DataType::Digest, "root".to_string()),
44            (DataType::U32, "tree_height".to_string()),
45            (DataType::U32, "leaf_index".to_string()),
46            (DataType::Digest, "leaf".to_string()),
47        ]
48    }
49
50    fn return_values(&self) -> Vec<(DataType, String)> {
51        vec![]
52    }
53
54    fn entrypoint(&self) -> String {
55        "tasmlib_hashing_merkle_verify".to_string()
56    }
57
58    fn code(&self, _: &mut Library) -> Vec<LabelledInstruction> {
59        let entrypoint = self.entrypoint();
60        let traverse_tree = format!("{entrypoint}_traverse_tree");
61        let tree_height_is_not_zero = format!("{entrypoint}_tree_height_is_not_zero");
62        triton_asm!(
63            // BEFORE: _ [root; 5] tree_height leaf_index [leaf; 5]
64            // AFTER:  _
65            {entrypoint}:
66                /* Assert reasonable tree height
67                 *
68                 * Don't rely only on
69                 * 1. `pow`'s implicit check that the exponent is a u32,
70                 * 2. `assert leaf_index < num_leaves`.
71                 * Since bfe!(2)^192 == 1 and 192 < u32::MAX, weird things are possible. For
72                 * example, the number of leafs for a tree of height 193 would incorrectly be
73                 * computed as 2.
74                 * Any attack would probably still require a hash collision to work, but there's
75                 * no point in leaving a potential attack vector open.
76                 */
77                push 32
78                dup 7
79                lt
80                assert error_id {Self::TREE_TOO_HIGH_ERROR_ID}
81
82                /* Calculate node index from tree height and leaf index */
83                dup 6
84                push 2
85                pow
86                // _ [root; 5] tree_height leaf_index [leaf; 5] num_leaves
87
88                dup 0 dup 7 lt
89                // _ [root; 5] tree_height leaf_index [leaf; 5] num_leaves (leaf_index < num_leaves)
90
91                assert error_id {Self::OUT_OF_BOUNDS_LEAF_ERROR_ID}
92                // _ [root; 5] tree_height leaf_index [leaf; 5] num_leaves
93
94                pick 6
95                add
96                // _ [root; 5] tree_height [leaf; 5] node_index
97
98                place 5
99                // _ [root; 5] tree_height node_index [leaf; 5]
100
101                pick 6
102                skiz
103                    call {tree_height_is_not_zero}
104                // _ [root; 5] [0|1] [calculated_root; 5]
105
106                /* compare calculated and provided root */
107                pick 5
108                pop 1
109                assert_vector error_id {Self::ROOT_MISMATCH_ERROR_ID}
110                pop 5
111
112                return
113
114            // BEFORE: _ node_index [leaf; 5]
115            {tree_height_is_not_zero}:
116                push 1
117                place 6
118                // _ 1 node_index [leaf; 5]
119
120                call {traverse_tree}
121                // _ 1 1 [calculated_root; 5]
122
123                pick 6
124                pop 1
125
126                return
127
128            {traverse_tree}:
129                merkle_step
130                recurse_or_return
131        )
132    }
133
134    fn sign_offs(&self) -> HashMap<Reviewer, SignOffFingerprint> {
135        let mut sign_offs = HashMap::new();
136        sign_offs.insert(Reviewer("ferdinand"), 0xc5311e6579152b58.into());
137        sign_offs
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use std::collections::VecDeque;
144
145    use proptest::collection::vec;
146
147    use super::*;
148    use crate::test_helpers::negative_test;
149    use crate::test_prelude::*;
150
151    impl ReadOnlyAlgorithm for MerkleVerify {
152        fn rust_shadow(
153            &self,
154            stack: &mut Vec<BFieldElement>,
155            _: &HashMap<BFieldElement, BFieldElement>,
156            _: VecDeque<BFieldElement>,
157            mut nd_digests: VecDeque<Digest>,
158        ) -> Result<(), RustShadowError> {
159            // BEFORE: _ [root; 5] tree_height leaf_index [leaf; 5]
160            // AFTER:  _
161            let leaf = pop_encodable(stack)?;
162            let leaf_index = pop_encodable::<u32>(stack)?;
163            let tree_height = pop_encodable::<u32>(stack)?;
164            let root = pop_encodable(stack)?;
165
166            let num_leaves = 1_u32
167                .checked_shl(tree_height)
168                .ok_or(RustShadowError::ArithmeticOverflow)?;
169            if leaf_index >= num_leaves {
170                return Err(RustShadowError::InvalidProof);
171            }
172
173            let mut node_digest = leaf;
174            let mut node_index = leaf_index + num_leaves;
175            while node_index != 1 {
176                let sibling = nd_digests
177                    .pop_front()
178                    .ok_or(RustShadowError::InvalidProof)?;
179                let node_is_left_sibling = node_index.is_multiple_of(2);
180                node_digest = if node_is_left_sibling {
181                    Tip5::hash_pair(node_digest, sibling)
182                } else {
183                    Tip5::hash_pair(sibling, node_digest)
184                };
185                node_index /= 2;
186            }
187            if node_digest != root {
188                return Err(RustShadowError::InvalidProof);
189            }
190
191            Ok(())
192        }
193
194        fn pseudorandom_initial_state(
195            &self,
196            seed: [u8; 32],
197            maybe_bench_case: Option<BenchmarkCase>,
198        ) -> ReadOnlyAlgorithmInitialState {
199            // BEFORE: _ [root; 5] tree_height leaf_index [leaf; 5]
200            let mut rng = StdRng::from_seed(seed);
201            let tree_height = match maybe_bench_case {
202                Some(BenchmarkCase::CommonCase) => 6,
203                Some(BenchmarkCase::WorstCase) => 20,
204                None => rng.random_range(1..20),
205            };
206
207            // sample unconstrained inputs directly
208            let num_leaves = 1 << tree_height;
209            let leaf_index = rng.random_range(0..num_leaves);
210            let path = (0..tree_height).map(|_| rng.random()).collect_vec();
211            let leaf = rng.random();
212
213            // walk up tree to calculate root
214            let mut current_node = leaf;
215            let mut node_index = leaf_index + num_leaves;
216            for &sibling in &path {
217                let node_is_left_sibling = node_index % 2 == 0;
218                current_node = if node_is_left_sibling {
219                    Tip5::hash_pair(current_node, sibling)
220                } else {
221                    Tip5::hash_pair(sibling, current_node)
222                };
223                node_index /= 2;
224            }
225            let root = current_node;
226
227            let mut stack = Self.init_stack_for_isolated_run();
228            push_encodable(&mut stack, &root);
229            push_encodable(&mut stack, &tree_height);
230            push_encodable(&mut stack, &leaf_index);
231            push_encodable(&mut stack, &leaf);
232
233            let nondeterminism = NonDeterminism::default().with_digests(path);
234            ReadOnlyAlgorithmInitialState {
235                stack,
236                nondeterminism,
237            }
238        }
239    }
240
241    #[macro_rules_attr::apply(test)]
242    fn merkle_verify_test() {
243        ShadowedReadOnlyAlgorithm::new(MerkleVerify).test()
244    }
245
246    #[macro_rules_attr::apply(proptest)]
247    fn merkle_tree_verification_fails_if_leaf_is_disturbed_slightly(
248        seed: [u8; 32],
249        #[strategy(0_usize..5)] perturbation_index: usize,
250        #[filter(#perturbation != 0)] perturbation: i8,
251    ) {
252        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
253        let top_of_stack = initial_state.stack.len() - 1;
254        initial_state.stack[top_of_stack - perturbation_index] += bfe!(perturbation);
255
256        test_assertion_failure(
257            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
258            initial_state.into(),
259            &[2],
260        );
261    }
262
263    #[macro_rules_attr::apply(proptest)]
264    fn merkle_tree_verification_fails_if_leaf_index_is_disturbed_slightly(
265        seed: [u8; 32],
266        #[filter(#perturbation != 0)] perturbation: i8,
267    ) {
268        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
269        let top_of_stack = initial_state.stack.len() - 1;
270        let leaf_index_index = top_of_stack - 5;
271        initial_state.stack[leaf_index_index] += bfe!(perturbation);
272
273        // out-of-range leaf indices are tested separately
274        let leaf_index = initial_state.stack[leaf_index_index];
275        prop_assume!(u32::try_from(leaf_index).is_ok());
276
277        test_assertion_failure(
278            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
279            initial_state.into(),
280            &[1, 2],
281        );
282    }
283
284    #[macro_rules_attr::apply(proptest)]
285    fn merkle_tree_verification_fails_if_leaf_index_is_out_of_range(
286        seed: [u8; 32],
287        #[strategy(u64::from(u32::MAX)..=BFieldElement::MAX)]
288        #[map(BFieldElement::new)]
289        leaf_index: BFieldElement,
290    ) {
291        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
292        let top_of_stack = initial_state.stack.len() - 1;
293        let leaf_index_index = top_of_stack - 5;
294        initial_state.stack[leaf_index_index] = leaf_index;
295
296        negative_test(
297            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
298            initial_state.into(),
299            &[OpStackError::FailedU32Conversion(leaf_index).into()],
300        );
301    }
302
303    #[macro_rules_attr::apply(proptest)]
304    fn merkle_tree_verification_fails_if_tree_height_is_disturbed_slightly(
305        seed: [u8; 32],
306        #[strategy(-32_i8..32)]
307        #[filter(#perturbation != 0)]
308        perturbation: i8,
309        #[strategy(vec(arb(), #perturbation.clamp(0, 32) as usize))]
310        additional_bogus_tree_nodes: Vec<Digest>,
311    ) {
312        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
313        let top_of_stack = initial_state.stack.len() - 1;
314        let tree_height_index = top_of_stack - 6;
315        initial_state.stack[tree_height_index] += bfe!(perturbation);
316
317        // out-of-range tree heights are tested separately
318        let perturbed_tree_height = initial_state.stack[tree_height_index];
319        prop_assume!(u32::try_from(perturbed_tree_height).is_ok());
320        prop_assume!(perturbed_tree_height.value() < 32);
321
322        // if the expected tree height is increased, additional internal nodes are needed
323        initial_state
324            .nondeterminism
325            .digests
326            .extend(additional_bogus_tree_nodes);
327
328        let expected_errors = [
329            MerkleVerify::OUT_OF_BOUNDS_LEAF_ERROR_ID,
330            MerkleVerify::ROOT_MISMATCH_ERROR_ID,
331        ];
332        test_assertion_failure(
333            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
334            initial_state.into(),
335            &expected_errors,
336        );
337    }
338
339    #[macro_rules_attr::apply(proptest)]
340    fn merkle_tree_verification_fails_if_tree_height_is_too_large(
341        seed: [u8; 32],
342        #[strategy(32_u32..)] tree_height: u32,
343    ) {
344        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
345        let top_of_stack = initial_state.stack.len() - 1;
346        let tree_height_index = top_of_stack - 6;
347        initial_state.stack[tree_height_index] = bfe!(tree_height);
348
349        test_assertion_failure(
350            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
351            initial_state.into(),
352            &[MerkleVerify::TREE_TOO_HIGH_ERROR_ID],
353        );
354    }
355
356    #[macro_rules_attr::apply(proptest)]
357    fn merkle_tree_verification_fails_if_tree_height_is_way_too_large(
358        seed: [u8; 32],
359        #[strategy(u64::from(u32::MAX)..=BFieldElement::MAX)]
360        #[map(BFieldElement::new)]
361        tree_height: BFieldElement,
362    ) {
363        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
364        let top_of_stack = initial_state.stack.len() - 1;
365        let tree_height_index = top_of_stack - 6;
366        initial_state.stack[tree_height_index] = tree_height;
367
368        negative_test(
369            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
370            initial_state.into(),
371            &[OpStackError::FailedU32Conversion(tree_height).into()],
372        );
373    }
374
375    #[macro_rules_attr::apply(proptest)]
376    fn merkle_tree_verification_fails_if_root_is_disturbed_slightly(
377        seed: [u8; 32],
378        #[strategy(7_usize..12)] perturbation_index: usize,
379        #[filter(#perturbation != 0)] perturbation: i8,
380    ) {
381        let mut initial_state = MerkleVerify.pseudorandom_initial_state(seed, None);
382        let top_of_stack = initial_state.stack.len() - 1;
383        initial_state.stack[top_of_stack - perturbation_index] += bfe!(perturbation);
384
385        test_assertion_failure(
386            &ShadowedReadOnlyAlgorithm::new(MerkleVerify),
387            initial_state.into(),
388            &[MerkleVerify::ROOT_MISMATCH_ERROR_ID],
389        );
390    }
391}
392
393#[cfg(test)]
394mod bench {
395    use super::*;
396    use crate::test_prelude::*;
397
398    #[macro_rules_attr::apply(test)]
399    fn benchmark() {
400        ShadowedReadOnlyAlgorithm::new(MerkleVerify).bench()
401    }
402}