Skip to main content

telltale_runtime/heap/
heap_impl.rs

1//! # Heap Implementation
2//!
3//! Deterministic heap for explicit resource management.
4//!
5//! ## Overview
6//!
7//! This module provides a pure, deterministic heap for managing protocol resources.
8//! The heap uses BTreeMap/BTreeSet for O(log n) operations with deterministic iteration order.
9//!
10//! Key properties:
11//! - **Functional and Mutable APIs**: Both persistent and in-place update paths exist
12//! - **Deterministic**: Same operations produce identical results
13//! - **Content-addressed**: Resources identified by content hash
14//! - **Nullifier tracking**: Consumed resources are tracked to prevent double-spending
15//!
16//! ## Lean Correspondence
17//!
18//! Resource concepts correspond to `lean/Runtime/Resources/HeapModel.lean`.
19//! The Lean parity lane mirrors deterministic heap replay and ordering
20//! behavior, while the Rust runtime remains the digest-producing implementation.
21
22use super::resource::{HeapError, Resource, ResourceId};
23use std::collections::{BTreeMap, BTreeSet};
24use telltale_types::{DefaultContentHasher, Hasher};
25
26/// Deterministic heap for protocol resources.
27///
28/// Uses BTreeMap for O(log n) operations with deterministic iteration order.
29/// The nullifiers set tracks consumed resources to prevent double-spending.
30#[derive(Debug, Clone, Default)]
31pub struct Heap<H: Hasher = DefaultContentHasher> {
32    /// Map from ResourceId to Resource
33    resources: BTreeMap<ResourceId<H>, Resource>,
34    /// Set of consumed (nullified) resource IDs
35    nullifiers: BTreeSet<ResourceId<H>>,
36    /// Counter for generating unique ResourceIds
37    counter: u64,
38}
39
40impl<H: Hasher> Heap<H> {
41    /// Create an empty heap.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Get the number of retained resource entries.
47    ///
48    /// Consumed resources are still retained until removed explicitly.
49    pub fn size(&self) -> usize {
50        self.resources.len()
51    }
52
53    /// Get the number of nullified resources.
54    pub fn nullified_count(&self) -> usize {
55        self.nullifiers.len()
56    }
57
58    /// Check if a resource exists in the heap (not necessarily active).
59    pub fn contains(&self, rid: &ResourceId<H>) -> bool {
60        self.resources.contains_key(rid)
61    }
62
63    /// Check if a resource has been consumed (nullified).
64    pub fn is_consumed(&self, rid: &ResourceId<H>) -> bool {
65        self.nullifiers.contains(rid)
66    }
67
68    /// Check if a resource is active (exists and not consumed).
69    pub fn is_active(&self, rid: &ResourceId<H>) -> bool {
70        self.contains(rid) && !self.is_consumed(rid)
71    }
72
73    /// Get the current allocation counter.
74    pub fn alloc_counter(&self) -> u64 {
75        self.counter
76    }
77
78    /// Allocate a new resource on the heap.
79    ///
80    /// Returns the new ResourceId and updated heap.
81    /// The ResourceId is derived from the resource content and allocation counter.
82    pub fn alloc(&self, resource: Resource) -> (ResourceId<H>, Heap<H>) {
83        let rid = ResourceId::<H>::from_resource(&resource, self.counter);
84        let mut new_heap = self.clone();
85        new_heap.resources.insert(rid.clone(), resource);
86        new_heap.counter += 1;
87        (rid, new_heap)
88    }
89
90    /// Allocate a resource mutably (for efficiency when building heaps).
91    pub fn alloc_mut(&mut self, resource: Resource) -> ResourceId<H> {
92        let rid = ResourceId::<H>::from_resource(&resource, self.counter);
93        self.resources.insert(rid.clone(), resource);
94        self.counter += 1;
95        rid
96    }
97
98    /// Read a resource from the heap.
99    ///
100    /// Returns an error if the resource doesn't exist or has been consumed.
101    pub fn read(&self, rid: &ResourceId<H>) -> Result<&Resource, HeapError<H>> {
102        if self.is_consumed(rid) {
103            return Err(HeapError::AlreadyConsumed(rid.clone()));
104        }
105        self.resources
106            .get(rid)
107            .ok_or_else(|| HeapError::NotFound(rid.clone()))
108    }
109
110    /// Consume a resource, adding it to the nullifier set.
111    ///
112    /// Returns an error if the resource doesn't exist or has already been consumed.
113    /// The resource remains in the heap but is marked as consumed.
114    pub fn consume(&self, rid: &ResourceId<H>) -> Result<Heap<H>, HeapError<H>> {
115        if self.is_consumed(rid) {
116            return Err(HeapError::AlreadyConsumed(rid.clone()));
117        }
118        if !self.contains(rid) {
119            return Err(HeapError::NotFound(rid.clone()));
120        }
121        let mut new_heap = self.clone();
122        new_heap.nullifiers.insert(rid.clone());
123        Ok(new_heap)
124    }
125
126    /// Consume a resource mutably.
127    pub fn consume_mut(&mut self, rid: &ResourceId<H>) -> Result<(), HeapError<H>> {
128        if self.is_consumed(rid) {
129            return Err(HeapError::AlreadyConsumed(rid.clone()));
130        }
131        if !self.contains(rid) {
132            return Err(HeapError::NotFound(rid.clone()));
133        }
134        self.nullifiers.insert(rid.clone());
135        Ok(())
136    }
137
138    /// Remove a resource from the heap entirely (for cleanup).
139    ///
140    /// Unlike `consume`, this removes the resource from both maps.
141    /// Returns an error if the resource doesn't exist.
142    pub fn remove(&self, rid: &ResourceId<H>) -> Result<Heap<H>, HeapError<H>> {
143        if !self.contains(rid) {
144            return Err(HeapError::NotFound(rid.clone()));
145        }
146        let mut new_heap = self.clone();
147        new_heap.resources.remove(rid);
148        new_heap.nullifiers.remove(rid);
149        Ok(new_heap)
150    }
151
152    /// Get all active (non-consumed) resources.
153    pub fn active_resources(&self) -> impl Iterator<Item = (&ResourceId<H>, &Resource)> {
154        self.resources
155            .iter()
156            .filter(|(rid, _)| !self.nullifiers.contains(*rid))
157    }
158
159    /// Get all consumed resource IDs.
160    pub fn consumed_ids(&self) -> impl Iterator<Item = &ResourceId<H>> {
161        self.nullifiers.iter()
162    }
163
164    /// Allocate multiple resources at once.
165    pub fn alloc_many(
166        &self,
167        resources: impl IntoIterator<Item = Resource>,
168    ) -> (Vec<ResourceId<H>>, Heap<H>) {
169        let mut new_heap = self.clone();
170        let rids: Vec<_> = resources
171            .into_iter()
172            .map(|r| new_heap.alloc_mut(r))
173            .collect();
174        (rids, new_heap)
175    }
176
177    /// Try to consume multiple resources atomically.
178    ///
179    /// If any consumption fails, returns the error without modifying the heap.
180    pub fn consume_many(&self, rids: &[ResourceId<H>]) -> Result<Heap<H>, HeapError<H>> {
181        // First validate all can be consumed
182        for rid in rids {
183            if self.is_consumed(rid) {
184                return Err(HeapError::AlreadyConsumed(rid.clone()));
185            }
186            if !self.contains(rid) {
187                return Err(HeapError::NotFound(rid.clone()));
188            }
189        }
190        // Then consume all
191        let mut new_heap = self.clone();
192        for rid in rids {
193            new_heap.nullifiers.insert(rid.clone());
194        }
195        Ok(new_heap)
196    }
197
198    /// Create a channel resource and allocate it.
199    pub fn alloc_channel(&self, sender: &str, receiver: &str) -> (ResourceId<H>, Heap<H>) {
200        self.alloc(Resource::channel(sender, receiver))
201    }
202
203    /// Create a message resource and allocate it.
204    pub fn alloc_message(
205        &self,
206        source: &str,
207        dest: &str,
208        label: &str,
209        payload: Vec<u8>,
210        seq_no: u64,
211    ) -> (ResourceId<H>, Heap<H>) {
212        self.alloc(Resource::message(source, dest, label, payload, seq_no))
213    }
214
215    /// Create a session state resource and allocate it.
216    pub fn alloc_session(&self, role: &str, type_hash: u64) -> (ResourceId<H>, Heap<H>) {
217        self.alloc(Resource::session(role, type_hash))
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::heap::{HeapCommitment, MerkleProof, MerkleTree};
225    use telltale_types::DefaultContentHasher;
226
227    #[test]
228    fn test_heap_alloc() {
229        let heap = Heap::<DefaultContentHasher>::new();
230        assert_eq!(heap.size(), 0);
231        assert_eq!(heap.alloc_counter(), 0);
232
233        let resource = Resource::channel("Alice", "Bob");
234        let (rid, heap) = heap.alloc(resource);
235
236        assert_eq!(heap.size(), 1);
237        assert_eq!(heap.alloc_counter(), 1);
238        assert!(heap.contains(&rid));
239        assert!(heap.is_active(&rid));
240    }
241
242    #[test]
243    fn test_heap_read() {
244        let heap = Heap::<DefaultContentHasher>::new();
245        let resource = Resource::channel("Alice", "Bob");
246        let (rid, heap) = heap.alloc(resource);
247
248        let read_resource = heap.read(&rid).unwrap();
249        assert_eq!(read_resource.kind(), "channel");
250
251        // Reading nonexistent resource fails
252        let fake_rid = ResourceId::<DefaultContentHasher>::new([0u8; 32], 999);
253        assert!(heap.read(&fake_rid).is_err());
254    }
255
256    #[test]
257    fn test_heap_consume() {
258        let heap = Heap::<DefaultContentHasher>::new();
259        let resource = Resource::channel("Alice", "Bob");
260        let (rid, heap) = heap.alloc(resource);
261
262        assert!(!heap.is_consumed(&rid));
263        let heap = heap.consume(&rid).unwrap();
264        assert!(heap.is_consumed(&rid));
265        assert!(!heap.is_active(&rid));
266
267        // Can still read consumed resources for verification
268        assert!(heap.contains(&rid));
269
270        // But read() should fail
271        assert!(heap.read(&rid).is_err());
272
273        // Double consume fails
274        assert!(heap.consume(&rid).is_err());
275    }
276
277    #[test]
278    fn test_heap_remove() {
279        let heap = Heap::<DefaultContentHasher>::new();
280        let resource = Resource::channel("Alice", "Bob");
281        let (rid, heap) = heap.alloc(resource);
282
283        let heap = heap.remove(&rid).unwrap();
284        assert!(!heap.contains(&rid));
285        assert_eq!(heap.size(), 0);
286    }
287
288    #[test]
289    fn test_heap_alloc_many() {
290        let heap = Heap::<DefaultContentHasher>::new();
291        let resources = vec![
292            Resource::channel("Alice", "Bob"),
293            Resource::channel("Bob", "Carol"),
294            Resource::message("Alice", "Bob", "Hello", vec![], 0),
295        ];
296
297        let (rids, heap) = heap.alloc_many(resources);
298
299        assert_eq!(rids.len(), 3);
300        assert_eq!(heap.size(), 3);
301        assert_eq!(heap.alloc_counter(), 3);
302    }
303
304    #[test]
305    fn test_heap_consume_many() {
306        let heap = Heap::<DefaultContentHasher>::new();
307        let (rids, heap) = heap.alloc_many(vec![
308            Resource::channel("Alice", "Bob"),
309            Resource::channel("Bob", "Carol"),
310        ]);
311
312        let heap = heap.consume_many(&rids).unwrap();
313        assert!(heap.is_consumed(&rids[0]));
314        assert!(heap.is_consumed(&rids[1]));
315        assert_eq!(heap.nullified_count(), 2);
316    }
317
318    #[test]
319    fn test_heap_active_resources() {
320        let heap = Heap::<DefaultContentHasher>::new();
321        let (rids, heap) = heap.alloc_many(vec![
322            Resource::channel("Alice", "Bob"),
323            Resource::channel("Bob", "Carol"),
324        ]);
325
326        let heap = heap.consume(&rids[0]).unwrap();
327
328        let active: Vec<_> = heap.active_resources().collect();
329        assert_eq!(active.len(), 1);
330        assert_eq!(active[0].0, &rids[1]);
331    }
332
333    #[test]
334    fn test_heap_determinism() {
335        let h1 = Heap::<DefaultContentHasher>::new();
336        let h2 = Heap::<DefaultContentHasher>::new();
337
338        let r1 = Resource::channel("Alice", "Bob");
339        let r2 = Resource::channel("Alice", "Bob");
340
341        let (rid1, h1) = h1.alloc(r1);
342        let (rid2, h2) = h2.alloc(r2);
343
344        // Same operations → same results
345        assert_eq!(rid1, rid2);
346        assert_eq!(
347            HeapCommitment::<DefaultContentHasher>::from_heap(&h1).hash(),
348            HeapCommitment::<DefaultContentHasher>::from_heap(&h2).hash()
349        );
350    }
351
352    #[test]
353    fn test_consume_many_is_order_independent() {
354        let heap = Heap::<DefaultContentHasher>::new();
355        let (rids, heap) = heap.alloc_many(vec![
356            Resource::channel("Alice", "Bob"),
357            Resource::channel("Bob", "Carol"),
358            Resource::session("Alice", 7),
359        ]);
360
361        let left = heap
362            .consume_many(&[rids[0].clone(), rids[1].clone()])
363            .unwrap();
364        let right = heap
365            .consume_many(&[rids[1].clone(), rids[0].clone()])
366            .unwrap();
367
368        assert_eq!(
369            HeapCommitment::<DefaultContentHasher>::from_heap(&left),
370            HeapCommitment::<DefaultContentHasher>::from_heap(&right)
371        );
372        let consumed_left: Vec<_> = left.consumed_ids().cloned().collect();
373        let consumed_right: Vec<_> = right.consumed_ids().cloned().collect();
374        assert_eq!(consumed_left, consumed_right);
375    }
376
377    #[test]
378    fn test_repeated_operation_sequences_yield_identical_proofs_and_commitments() {
379        fn build_fixture() -> (
380            Vec<ResourceId<DefaultContentHasher>>,
381            HeapCommitment<DefaultContentHasher>,
382            MerkleProof<DefaultContentHasher>,
383        ) {
384            let heap = Heap::<DefaultContentHasher>::new();
385            let (rid0, heap) = heap.alloc_channel("Alice", "Bob");
386            let (rid1, heap) = heap.alloc_message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
387            let (rid2, heap) = heap.alloc_session("Alice", 12345);
388            let heap = heap.consume(&rid0).unwrap();
389
390            let commitment = HeapCommitment::<DefaultContentHasher>::from_heap(&heap);
391            let proof = MerkleTree::<DefaultContentHasher>::from_heap(&heap)
392                .prove(0)
393                .expect("proof should exist");
394            (vec![rid0, rid1, rid2], commitment, proof)
395        }
396
397        let left = build_fixture();
398        let right = build_fixture();
399
400        assert_eq!(left.0, right.0);
401        assert_eq!(left.1, right.1);
402        assert_eq!(left.2, right.2);
403    }
404
405    #[test]
406    fn test_remove_changes_commitment_and_clears_nullifier_history() {
407        let heap = Heap::<DefaultContentHasher>::new();
408        let (rid, heap) = heap.alloc_channel("Alice", "Bob");
409        let consumed = heap.consume(&rid).unwrap();
410        let removed = consumed.remove(&rid).unwrap();
411
412        let consumed_commitment = HeapCommitment::<DefaultContentHasher>::from_heap(&consumed);
413        let removed_commitment = HeapCommitment::<DefaultContentHasher>::from_heap(&removed);
414
415        assert_ne!(consumed_commitment, removed_commitment);
416        assert_eq!(removed.nullified_count(), 0);
417        assert_eq!(removed.size(), 0);
418    }
419
420    #[test]
421    fn test_helper_methods() {
422        let heap = Heap::<DefaultContentHasher>::new();
423
424        let (rid1, heap) = heap.alloc_channel("Alice", "Bob");
425        assert!(matches!(heap.read(&rid1), Ok(Resource::Channel(_))));
426
427        let (rid2, heap) = heap.alloc_message("Alice", "Bob", "Hello", vec![1, 2, 3], 0);
428        assert!(matches!(heap.read(&rid2), Ok(Resource::Message(_))));
429
430        let (rid3, heap) = heap.alloc_session("Alice", 12345);
431        assert!(matches!(heap.read(&rid3), Ok(Resource::Session { .. })));
432
433        assert_eq!(heap.size(), 3);
434    }
435}