Skip to main content

telltale_runtime/heap/
merkle.rs

1//! # Merkle Tree Utilities
2//!
3//! Merkle tree construction and verification for heap state.
4//!
5//! ## Overview
6//!
7//! This module provides Merkle tree operations for heap state verification:
8//! - Compute Merkle root from heap resources
9//! - Generate inclusion proofs
10//! - Verify inclusion proofs
11//!
12//! ## Lean Correspondence
13//!
14//! Proof-path direction and ordering semantics are mirrored by
15//! `lean/Runtime/Resources/HeapModel.lean`.
16//! Digest computation remains Rust-side and is checked through published vectors.
17
18use super::encoding::{
19    heap_commitment_preimage, merkle_node_preimage, nullifier_leaf_preimage, resource_leaf_preimage,
20};
21use super::heap_impl::Heap;
22use super::resource::{Resource, ResourceId};
23use std::marker::PhantomData;
24use telltale_types::{DefaultContentHasher, Hasher};
25
26/// Direction in a Merkle proof path.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Direction {
29    /// Sibling is on the left
30    Left,
31    /// Sibling is on the right
32    Right,
33}
34
35/// A single step in a Merkle proof.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ProofStep<H: Hasher = DefaultContentHasher> {
38    /// Direction: is this node a left or right sibling?
39    pub direction: Direction,
40    /// The sibling hash at this level
41    pub sibling_hash: H::Digest,
42}
43
44/// A Merkle inclusion proof.
45///
46/// The proof consists of sibling hashes from leaf to root.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct MerkleProof<H: Hasher = DefaultContentHasher> {
49    /// The leaf hash being proven
50    pub leaf_hash: H::Digest,
51    /// Proof path from leaf to root
52    pub path: Vec<ProofStep<H>>,
53    /// The expected root hash
54    pub root: H::Digest,
55}
56
57impl<H: Hasher> MerkleProof<H> {
58    /// Verify this proof.
59    pub fn verify(&self) -> bool {
60        let computed_root =
61            self.path
62                .iter()
63                .fold(self.leaf_hash.clone(), |current, step| {
64                    match step.direction {
65                        Direction::Left => merkle_node_hash::<H>(&step.sibling_hash, &current),
66                        Direction::Right => merkle_node_hash::<H>(&current, &step.sibling_hash),
67                    }
68                });
69        computed_root == self.root
70    }
71}
72
73/// Hash two child digests to get a Merkle parent digest.
74///
75/// The hasher consumes a tagged preimage that contains the left child and
76/// right child digests in order.
77pub fn merkle_node_hash<H: Hasher>(left: &H::Digest, right: &H::Digest) -> H::Digest {
78    let preimage = merkle_node_preimage(left.as_ref(), right.as_ref());
79    H::digest(&preimage)
80}
81
82/// Compute the digest of an active-resource leaf.
83///
84/// The hasher consumes a tagged preimage that contains the `ResourceId`
85/// digest and the canonical resource bytes.
86pub fn resource_leaf_hash<H: Hasher>(rid: &ResourceId<H>, resource: &Resource) -> H::Digest {
87    let resource_bytes = resource.canonical_bytes();
88    let preimage = resource_leaf_preimage(rid.as_bytes(), &resource_bytes);
89    H::digest(&preimage)
90}
91
92/// Compute the digest of a nullifier leaf.
93///
94/// The hasher consumes a tagged preimage that contains the `ResourceId`
95/// digest bytes.
96pub fn nullifier_leaf_hash<H: Hasher>(rid: &ResourceId<H>) -> H::Digest {
97    let preimage = nullifier_leaf_preimage(rid.as_bytes());
98    H::digest(&preimage)
99}
100
101/// Empty tree root (hash of empty).
102fn empty_root<H: Hasher>() -> H::Digest {
103    H::digest(&[])
104}
105
106/// Merkle tree structure for efficient proof generation.
107#[derive(Debug, Clone)]
108pub struct MerkleTree<H: Hasher = DefaultContentHasher> {
109    /// Root hash
110    pub root: H::Digest,
111    /// Leaf hashes in order
112    leaves: Vec<H::Digest>,
113    /// Internal nodes (for proof generation)
114    /// Stored level by level, bottom up
115    levels: Vec<Vec<H::Digest>>,
116    _hasher: PhantomData<H>,
117}
118
119impl<H: Hasher> MerkleTree<H> {
120    /// Build a Merkle tree from a list of leaf hashes.
121    pub fn from_leaves(leaves: Vec<H::Digest>) -> Self {
122        if leaves.is_empty() {
123            return Self {
124                root: empty_root::<H>(),
125                leaves: Vec::new(),
126                levels: Vec::new(),
127                _hasher: PhantomData,
128            };
129        }
130
131        let mut levels = vec![leaves.clone()];
132        let mut current_level = leaves.clone();
133
134        while current_level.len() > 1 {
135            if current_level.len() % 2 == 1 {
136                current_level.push(current_level.last().expect("non-empty level").clone());
137            }
138
139            let next_level: Vec<H::Digest> = current_level
140                .chunks(2)
141                .map(|pair| merkle_node_hash::<H>(&pair[0], &pair[1]))
142                .collect();
143
144            levels.push(next_level.clone());
145            current_level = next_level;
146        }
147
148        let root = current_level[0].clone();
149
150        Self {
151            root,
152            leaves,
153            levels,
154            _hasher: PhantomData,
155        }
156    }
157
158    /// Build a Merkle tree from a heap.
159    pub fn from_heap(heap: &Heap<H>) -> Self {
160        let leaves: Vec<H::Digest> = heap
161            .active_resources()
162            .map(|(rid, resource)| resource_leaf_hash::<H>(rid, resource))
163            .collect();
164        Self::from_leaves(leaves)
165    }
166
167    /// Get the number of leaves.
168    pub fn size(&self) -> usize {
169        self.leaves.len()
170    }
171
172    /// Generate an inclusion proof for a leaf at the given index.
173    pub fn prove(&self, index: usize) -> Option<MerkleProof<H>> {
174        if index >= self.leaves.len() {
175            return None;
176        }
177
178        let leaf_hash = self.leaves[index].clone();
179        let mut path = Vec::new();
180        let mut current_index = index;
181
182        for level in &self.levels[..self.levels.len().saturating_sub(1)] {
183            let sibling_index = if current_index % 2 == 0 {
184                current_index + 1
185            } else {
186                current_index - 1
187            };
188
189            let sibling_hash = if sibling_index < level.len() {
190                level[sibling_index].clone()
191            } else {
192                level[current_index].clone()
193            };
194
195            let direction = if current_index % 2 == 0 {
196                Direction::Right
197            } else {
198                Direction::Left
199            };
200
201            path.push(ProofStep {
202                direction,
203                sibling_hash,
204            });
205
206            current_index /= 2;
207        }
208
209        Some(MerkleProof {
210            leaf_hash,
211            path,
212            root: self.root.clone(),
213        })
214    }
215}
216
217/// Commitment to heap state (roots + counter).
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct HeapCommitment<H: Hasher = DefaultContentHasher> {
220    /// Merkle root of resources
221    pub resource_root: H::Digest,
222    /// Merkle root of nullifiers
223    pub nullifier_root: H::Digest,
224    /// Allocation counter
225    pub counter: u64,
226}
227
228impl<H: Hasher> HeapCommitment<H> {
229    /// Compute commitment from a heap.
230    pub fn from_heap(heap: &Heap<H>) -> Self {
231        let resource_leaves: Vec<H::Digest> = heap
232            .active_resources()
233            .map(|(rid, resource)| resource_leaf_hash::<H>(rid, resource))
234            .collect();
235        let resource_tree = MerkleTree::<H>::from_leaves(resource_leaves);
236
237        let nullifier_leaves: Vec<H::Digest> =
238            heap.consumed_ids().map(nullifier_leaf_hash::<H>).collect();
239        let nullifier_tree = MerkleTree::<H>::from_leaves(nullifier_leaves);
240
241        Self {
242            resource_root: resource_tree.root,
243            nullifier_root: nullifier_tree.root,
244            counter: heap.alloc_counter(),
245        }
246    }
247
248    /// Hash this commitment to a single value.
249    ///
250    /// The hasher consumes a tagged preimage that contains the resource root,
251    /// the nullifier root, and the allocation counter.
252    pub fn hash(&self) -> H::Digest {
253        let preimage = heap_commitment_preimage(
254            self.resource_root.as_ref(),
255            self.nullifier_root.as_ref(),
256            self.counter,
257        );
258        H::digest(&preimage)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use telltale_types::DefaultContentHasher;
266
267    fn to_hex(bytes: &[u8]) -> String {
268        bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
269    }
270
271    fn from_hex(hex: &str) -> Vec<u8> {
272        assert_eq!(hex.len() % 2, 0, "hex input must have even length");
273        hex.as_bytes()
274            .chunks(2)
275            .map(|pair| {
276                let hi = (pair[0] as char).to_digit(16).expect("valid hex") as u8;
277                let lo = (pair[1] as char).to_digit(16).expect("valid hex") as u8;
278                (hi << 4) | lo
279            })
280            .collect()
281    }
282
283    #[test]
284    fn test_empty_tree() {
285        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(vec![]);
286        assert_eq!(tree.root, empty_root::<DefaultContentHasher>());
287        assert_eq!(tree.size(), 0);
288    }
289
290    #[test]
291    fn test_single_leaf() {
292        let leaf = DefaultContentHasher::digest(b"hello");
293        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(vec![leaf]);
294        assert_eq!(tree.root, leaf);
295        assert_eq!(tree.size(), 1);
296    }
297
298    #[test]
299    fn test_two_leaves() {
300        let leaf1 = DefaultContentHasher::digest(b"hello");
301        let leaf2 = DefaultContentHasher::digest(b"world");
302        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(vec![leaf1, leaf2]);
303
304        let expected_root = merkle_node_hash::<DefaultContentHasher>(&leaf1, &leaf2);
305        assert_eq!(tree.root, expected_root);
306        assert_eq!(tree.size(), 2);
307    }
308
309    #[test]
310    fn test_four_leaves() {
311        let leaves: Vec<_> = (0_u8..4)
312            .map(|i| DefaultContentHasher::digest(&[i]))
313            .collect();
314        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(leaves.clone());
315
316        let h01 = merkle_node_hash::<DefaultContentHasher>(&leaves[0], &leaves[1]);
317        let h23 = merkle_node_hash::<DefaultContentHasher>(&leaves[2], &leaves[3]);
318        let expected_root = merkle_node_hash::<DefaultContentHasher>(&h01, &h23);
319
320        assert_eq!(tree.root, expected_root);
321        assert_eq!(tree.size(), 4);
322    }
323
324    #[test]
325    fn test_proof_generation_and_verification() {
326        let leaves: Vec<_> = (0_u8..4)
327            .map(|i| DefaultContentHasher::digest(&[i]))
328            .collect();
329        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(leaves);
330
331        for i in 0..4 {
332            let proof = tree.prove(i).expect("should generate proof");
333            assert!(proof.verify(), "proof for leaf {} should verify", i);
334        }
335    }
336
337    #[test]
338    fn test_proof_for_out_of_bounds() {
339        let leaves: Vec<_> = (0_u8..4)
340            .map(|i| DefaultContentHasher::digest(&[i]))
341            .collect();
342        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(leaves);
343
344        assert!(tree.prove(4).is_none());
345        assert!(tree.prove(100).is_none());
346    }
347
348    #[test]
349    fn test_odd_number_of_leaves() {
350        let leaves: Vec<_> = (0_u8..3)
351            .map(|i| DefaultContentHasher::digest(&[i]))
352            .collect();
353        let tree = MerkleTree::<DefaultContentHasher>::from_leaves(leaves);
354
355        for i in 0..3 {
356            let proof = tree.prove(i).expect("should generate proof");
357            assert!(proof.verify(), "proof for leaf {} should verify", i);
358        }
359    }
360
361    #[test]
362    fn test_heap_merkle_root() {
363        let heap = Heap::<DefaultContentHasher>::new();
364        let (_, heap) = heap.alloc_channel("Alice", "Bob");
365        let (_, heap) = heap.alloc_message("Alice", "Bob", "Hello", vec![], 0);
366
367        let root = MerkleTree::<DefaultContentHasher>::from_heap(&heap).root;
368        assert_ne!(root, empty_root::<DefaultContentHasher>());
369    }
370
371    #[test]
372    fn test_heap_commitment() {
373        let heap = Heap::<DefaultContentHasher>::new();
374        let (rid, heap) = heap.alloc_channel("Alice", "Bob");
375        let heap = heap.consume(&rid).unwrap();
376
377        let commitment = HeapCommitment::<DefaultContentHasher>::from_heap(&heap);
378
379        assert_eq!(commitment.counter, 1);
380    }
381
382    #[test]
383    fn test_leaf_and_node_fixed_vectors() {
384        let resource = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
385        let resource_id = ResourceId::<DefaultContentHasher>::from_resource(&resource, 1);
386        let resource_leaf = resource_leaf_hash::<DefaultContentHasher>(&resource_id, &resource);
387        let nullifier_leaf = nullifier_leaf_hash::<DefaultContentHasher>(&resource_id);
388
389        assert_eq!(
390            to_hex(resource_leaf.as_ref()),
391            "7526f89408115ad41ff135f26fd3c0be0ca91c93589c2f17cea2d8115bda9cf3"
392        );
393        assert_eq!(
394            to_hex(nullifier_leaf.as_ref()),
395            "c8ff09f3959b1a2d654b86e29d8f12df277db70f05256f0e433bf26a6c98acb1"
396        );
397
398        let sibling: [u8; 32] =
399            from_hex("981652f6785b2e7147a3ba489d050f3924700e808b9a7b229470754507c5a13c")
400                .try_into()
401                .expect("32-byte test vector");
402        let root = merkle_node_hash::<DefaultContentHasher>(&resource_leaf, &sibling);
403        assert_eq!(
404            to_hex(root.as_ref()),
405            "1555554394a0a595200d7717fa5c4710ce608dab3ffd793ff3644eecb99df1fa"
406        );
407    }
408
409    #[test]
410    fn test_heap_commitment_fixed_vector() {
411        let heap = Heap::<DefaultContentHasher>::new();
412        let (rid0, heap) = heap.alloc_channel("Alice", "Bob");
413        let (_rid1, heap) = heap.alloc_message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
414        let (_rid2, heap) = heap.alloc_session("Alice", 12345);
415        let heap = heap.consume(&rid0).unwrap();
416
417        let commitment = HeapCommitment::<DefaultContentHasher>::from_heap(&heap);
418
419        assert_eq!(
420            to_hex(commitment.resource_root.as_ref()),
421            "1555554394a0a595200d7717fa5c4710ce608dab3ffd793ff3644eecb99df1fa"
422        );
423        assert_eq!(
424            to_hex(commitment.nullifier_root.as_ref()),
425            "c045d1ac8bad212b29fffbe54d774d1401242771c75275fc30b6136b007f52d6"
426        );
427        assert_eq!(
428            to_hex(commitment.hash().as_ref()),
429            "57d07742f7a22b083e88dcae71804c9f03a727df11fe9d1d4c0aa553a34dac0d"
430        );
431    }
432
433    #[test]
434    fn test_merkle_proof_fixed_vector() {
435        let heap = Heap::<DefaultContentHasher>::new();
436        let (rid0, heap) = heap.alloc_channel("Alice", "Bob");
437        let (_rid1, heap) = heap.alloc_message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
438        let (_rid2, heap) = heap.alloc_session("Alice", 12345);
439        let heap = heap.consume(&rid0).unwrap();
440
441        let tree = MerkleTree::<DefaultContentHasher>::from_heap(&heap);
442        let proof = tree.prove(0).expect("proof should exist");
443
444        assert_eq!(
445            to_hex(proof.leaf_hash.as_ref()),
446            "7526f89408115ad41ff135f26fd3c0be0ca91c93589c2f17cea2d8115bda9cf3"
447        );
448        assert_eq!(proof.path.len(), 1);
449        assert_eq!(proof.path[0].direction, Direction::Right);
450        assert_eq!(
451            to_hex(proof.path[0].sibling_hash.as_ref()),
452            "981652f6785b2e7147a3ba489d050f3924700e808b9a7b229470754507c5a13c"
453        );
454        assert_eq!(
455            to_hex(proof.root.as_ref()),
456            "1555554394a0a595200d7717fa5c4710ce608dab3ffd793ff3644eecb99df1fa"
457        );
458        assert!(proof.verify());
459    }
460
461    #[test]
462    fn test_commitment_determinism() {
463        let heap1 = Heap::<DefaultContentHasher>::new();
464        let (_, heap1) = heap1.alloc_channel("Alice", "Bob");
465
466        let heap2 = Heap::<DefaultContentHasher>::new();
467        let (_, heap2) = heap2.alloc_channel("Alice", "Bob");
468
469        let c1 = HeapCommitment::<DefaultContentHasher>::from_heap(&heap1);
470        let c2 = HeapCommitment::<DefaultContentHasher>::from_heap(&heap2);
471
472        assert_eq!(c1, c2);
473        assert_eq!(c1.hash(), c2.hash());
474    }
475}