Skip to main content

telltale_runtime/heap/
resource.rs

1//! # Resource Types
2//!
3//! Content-addressed resources for the protocol heap.
4//!
5//! ## Overview
6//!
7//! This module defines content-addressed resources for heap-based state management.
8//! Resources are immutable values identified by their content hash, enabling:
9//! - Replay protection (can't process same message twice)
10//! - Byzantine fault detection (prove protocol violations)
11//! - ZK compatibility (state is a Merkle tree)
12//! - Deterministic execution (heap operations are pure)
13//!
14//! ## Lean Correspondence
15//!
16//! Resource concepts correspond to `lean/Runtime/Resources/HeapModel.lean`.
17//! The Lean parity lane mirrors canonical bytes and tagged preimages for the
18//! resource family. The concrete Rust digest type and heap error surface remain
19//! operational Rust artifacts.
20
21use super::encoding::{
22    resource_id_preimage, tag_channel_state, tag_message, tag_message_payload,
23    tag_resource_channel, tag_resource_message, tag_resource_session, tag_resource_value,
24    CanonicalHeapEncoder, CanonicalHeapEncoding,
25};
26use std::fmt;
27use std::hash::{Hash, Hasher as StdHasher};
28use std::marker::PhantomData;
29use telltale_types::{DefaultContentHasher, Hasher};
30
31/// Unique identifier for heap-allocated resources.
32///
33/// `ResourceId` is derived from a tagged hash preimage that contains the
34/// canonical resource encoding and the allocation counter.
35/// This keeps repeated allocations of identical semantic resources distinct.
36#[derive(Clone)]
37pub struct ResourceId<H: Hasher = DefaultContentHasher> {
38    /// The hashed resource identity
39    hash: H::Digest,
40    /// Allocation counter (for uniqueness of identical content)
41    counter: u64,
42    _hasher: PhantomData<H>,
43}
44
45impl<H: Hasher> ResourceId<H> {
46    /// Create a new ResourceId from hash and counter.
47    pub fn new(hash: H::Digest, counter: u64) -> Self {
48        Self {
49            hash,
50            counter,
51            _hasher: PhantomData,
52        }
53    }
54
55    /// Create a `ResourceId` from a resource and allocation counter.
56    ///
57    /// The hasher consumes a tagged preimage that contains the canonical
58    /// resource bytes and the little-endian counter.
59    pub fn from_resource(resource: &Resource, counter: u64) -> Self {
60        let content_bytes = resource.canonical_bytes();
61        let preimage = resource_id_preimage(&content_bytes, counter);
62        let hash = H::digest(&preimage);
63
64        Self {
65            hash,
66            counter,
67            _hasher: PhantomData,
68        }
69    }
70
71    /// Display as a short hex string.
72    pub fn to_short_hex(&self) -> String {
73        let hex: String = self.hash.as_ref()[..4]
74            .iter()
75            .map(|b| format!("{:02x}", b))
76            .collect();
77        format!("{}:{}", hex, self.counter)
78    }
79
80    /// Get the raw hash bytes.
81    #[must_use]
82    pub fn hash(&self) -> &H::Digest {
83        &self.hash
84    }
85
86    /// Get the raw hash bytes as a slice.
87    #[must_use]
88    pub fn as_bytes(&self) -> &[u8] {
89        self.hash.as_ref()
90    }
91
92    /// Get the allocation counter.
93    #[must_use]
94    pub fn counter(&self) -> u64 {
95        self.counter
96    }
97}
98
99impl<H: Hasher> PartialEq for ResourceId<H> {
100    fn eq(&self, other: &Self) -> bool {
101        self.hash.as_ref() == other.hash.as_ref() && self.counter == other.counter
102    }
103}
104
105impl<H: Hasher> Eq for ResourceId<H> {}
106
107impl<H: Hasher> Hash for ResourceId<H> {
108    fn hash<S: StdHasher>(&self, state: &mut S) {
109        self.hash.as_ref().hash(state);
110        self.counter.hash(state);
111    }
112}
113
114impl<H: Hasher> fmt::Debug for ResourceId<H> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(
117            f,
118            "ResourceId<{}>({})",
119            H::algorithm_name(),
120            self.to_short_hex()
121        )
122    }
123}
124
125impl<H: Hasher> fmt::Display for ResourceId<H> {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "{}", self.to_short_hex())
128    }
129}
130
131impl<H: Hasher> PartialOrd for ResourceId<H> {
132    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
133        Some(self.cmp(other))
134    }
135}
136
137impl<H: Hasher> Ord for ResourceId<H> {
138    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139        match self.hash.as_ref().cmp(other.hash.as_ref()) {
140            std::cmp::Ordering::Equal => self.counter.cmp(&other.counter),
141            ord => ord,
142        }
143    }
144}
145
146/// A message payload with label and data.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct MessagePayload {
149    /// The message label
150    pub label: String,
151    /// The serialized payload
152    pub payload: Vec<u8>,
153}
154
155impl CanonicalHeapEncoding for MessagePayload {
156    fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
157        encoder.string(&self.label);
158        encoder.bytes(&self.payload);
159    }
160
161    fn canonical_tag(&self) -> u8 {
162        tag_message_payload()
163    }
164}
165
166impl MessagePayload {
167    /// Create a new message payload.
168    pub fn new(label: impl Into<String>, payload: Vec<u8>) -> Self {
169        Self {
170            label: label.into(),
171            payload,
172        }
173    }
174}
175
176/// State of a communication channel between two roles.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ChannelState {
179    /// The sender role
180    pub sender: String,
181    /// The receiver role
182    pub receiver: String,
183    /// Pending messages in the channel
184    pub queue: Vec<MessagePayload>,
185}
186
187impl CanonicalHeapEncoding for ChannelState {
188    fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
189        encoder.string(&self.sender);
190        encoder.string(&self.receiver);
191        encoder.u32(self.queue.len() as u32);
192        for payload in &self.queue {
193            encoder.nested(payload);
194        }
195    }
196
197    fn canonical_tag(&self) -> u8 {
198        tag_channel_state()
199    }
200}
201
202impl ChannelState {
203    /// Create a new empty channel.
204    pub fn new(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
205        Self {
206            sender: sender.into(),
207            receiver: receiver.into(),
208            queue: Vec::new(),
209        }
210    }
211
212    /// Get the number of pending messages.
213    pub fn queue_size(&self) -> usize {
214        self.queue.len()
215    }
216}
217
218/// A message resource representing a message in transit.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct Message {
221    /// Source role
222    pub source: String,
223    /// Destination role
224    pub dest: String,
225    /// Message content
226    pub content: MessagePayload,
227    /// Sequence number for ordering
228    pub seq_no: u64,
229}
230
231impl CanonicalHeapEncoding for Message {
232    fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
233        encoder.string(&self.source);
234        encoder.string(&self.dest);
235        encoder.nested(&self.content);
236        encoder.u64(self.seq_no);
237    }
238
239    fn canonical_tag(&self) -> u8 {
240        tag_message()
241    }
242}
243
244impl Message {
245    /// Create a new message.
246    pub fn new(
247        source: impl Into<String>,
248        dest: impl Into<String>,
249        label: impl Into<String>,
250        payload: Vec<u8>,
251        seq_no: u64,
252    ) -> Self {
253        Self {
254            source: source.into(),
255            dest: dest.into(),
256            content: MessagePayload::new(label, payload),
257            seq_no,
258        }
259    }
260}
261
262/// Resource kinds that can be allocated on the heap.
263///
264/// All resources are immutable and content-addressed.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum Resource {
267    /// A communication channel between roles
268    Channel(ChannelState),
269    /// A message in transit
270    Message(Message),
271    /// Current session state for a role
272    Session {
273        /// The role name
274        role: String,
275        /// Hash of the session type (LocalTypeR)
276        type_hash: u64,
277    },
278    /// An arbitrary value
279    Value {
280        /// Type tag
281        tag: String,
282        /// Serialized value
283        data: Vec<u8>,
284    },
285}
286
287impl Resource {
288    /// Create a channel resource.
289    pub fn channel(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
290        Resource::Channel(ChannelState::new(sender, receiver))
291    }
292
293    /// Create a message resource.
294    pub fn message(
295        source: impl Into<String>,
296        dest: impl Into<String>,
297        label: impl Into<String>,
298        payload: Vec<u8>,
299        seq_no: u64,
300    ) -> Self {
301        Resource::Message(Message::new(source, dest, label, payload, seq_no))
302    }
303
304    /// Create a session resource.
305    pub fn session(role: impl Into<String>, type_hash: u64) -> Self {
306        Resource::Session {
307            role: role.into(),
308            type_hash,
309        }
310    }
311
312    /// Create a value resource.
313    pub fn value(tag: impl Into<String>, data: Vec<u8>) -> Self {
314        Resource::Value {
315            tag: tag.into(),
316            data,
317        }
318    }
319
320    /// Get the resource kind as a string.
321    pub fn kind(&self) -> &'static str {
322        match self {
323            Resource::Channel(_) => "channel",
324            Resource::Message(_) => "message",
325            Resource::Session { .. } => "session",
326            Resource::Value { .. } => "value",
327        }
328    }
329
330    /// Serialize the resource to canonical heap bytes.
331    ///
332    /// The heap derives resource identity and Merkle leaves from this
333    /// versioned canonical encoding.
334    pub fn canonical_bytes(&self) -> Vec<u8> {
335        self.to_canonical_bytes()
336    }
337}
338
339impl CanonicalHeapEncoding for Resource {
340    fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
341        match self {
342            Resource::Channel(channel) => encoder.nested(channel),
343            Resource::Message(message) => encoder.nested(message),
344            Resource::Session { role, type_hash } => {
345                encoder.string(role);
346                encoder.u64(*type_hash);
347            }
348            Resource::Value { tag, data } => {
349                encoder.string(tag);
350                encoder.bytes(data);
351            }
352        }
353    }
354
355    fn canonical_tag(&self) -> u8 {
356        match self {
357            Resource::Channel(_) => tag_resource_channel(),
358            Resource::Message(_) => tag_resource_message(),
359            Resource::Session { .. } => tag_resource_session(),
360            Resource::Value { .. } => tag_resource_value(),
361        }
362    }
363}
364
365impl fmt::Display for Resource {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        write!(f, "Resource({})", self.kind())
368    }
369}
370
371/// Errors that can occur during heap operations.
372#[derive(Clone, PartialEq, Eq)]
373pub enum HeapError<H: Hasher = DefaultContentHasher> {
374    /// Resource not found in heap
375    NotFound(ResourceId<H>),
376    /// Resource already consumed (nullified)
377    AlreadyConsumed(ResourceId<H>),
378    /// Resource already exists with this ID
379    AlreadyExists(ResourceId<H>),
380    /// Invalid resource type for operation
381    TypeMismatch { expected: String, got: String },
382    /// Generic error with message
383    Other(String),
384}
385
386impl<H: Hasher> fmt::Debug for HeapError<H> {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        match self {
389            HeapError::NotFound(rid) => f.debug_tuple("NotFound").field(rid).finish(),
390            HeapError::AlreadyConsumed(rid) => f.debug_tuple("AlreadyConsumed").field(rid).finish(),
391            HeapError::AlreadyExists(rid) => f.debug_tuple("AlreadyExists").field(rid).finish(),
392            HeapError::TypeMismatch { expected, got } => f
393                .debug_struct("TypeMismatch")
394                .field("expected", expected)
395                .field("got", got)
396                .finish(),
397            HeapError::Other(msg) => f.debug_tuple("Other").field(msg).finish(),
398        }
399    }
400}
401
402impl<H: Hasher> fmt::Display for HeapError<H> {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            HeapError::NotFound(rid) => write!(f, "Resource not found: {}", rid),
406            HeapError::AlreadyConsumed(rid) => write!(f, "Resource already consumed: {}", rid),
407            HeapError::AlreadyExists(rid) => write!(f, "Resource already exists: {}", rid),
408            HeapError::TypeMismatch { expected, got } => {
409                write!(f, "Type mismatch: expected {}, got {}", expected, got)
410            }
411            HeapError::Other(msg) => write!(f, "{}", msg),
412        }
413    }
414}
415
416impl<H: Hasher> std::error::Error for HeapError<H> {}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crate::heap::{HEAP_ENCODING_MAGIC, HEAP_ENCODING_VERSION};
422    use telltale_types::{Blake3Hasher, DefaultContentHasher, Hasher};
423
424    fn to_hex(bytes: &[u8]) -> String {
425        bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
426    }
427
428    #[test]
429    fn test_resource_id_creation() {
430        let r1 = Resource::channel("Alice", "Bob");
431        let r2 = Resource::channel("Alice", "Bob");
432
433        let id1 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 0);
434        let id2 = ResourceId::<DefaultContentHasher>::from_resource(&r2, 0);
435        let id3 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 1);
436
437        // Same resource, same counter → same ID
438        assert_eq!(id1, id2);
439        // Same resource, different counter → different ID
440        assert_ne!(id1, id3);
441    }
442
443    #[test]
444    fn test_resource_id_fixed_vector() {
445        let resource = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
446        let resource_id = ResourceId::<DefaultContentHasher>::from_resource(&resource, 1);
447
448        assert_eq!(
449            to_hex(resource.canonical_bytes().as_slice()),
450            "545448500100415454485001003005000000416c69636503000000426f62545448500100100500000048656c6c6f030000000102030700000000000000"
451        );
452        assert_eq!(
453            to_hex(resource_id.as_bytes()),
454            "5a22a0e61e5faa3ea4c2bee86a92761eea62364727a77a4ed7c3a24c456afd8e"
455        );
456    }
457
458    #[test]
459    fn test_resource_id_ordering() {
460        let id1 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 0);
461        let id2 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 1);
462
463        // When the digest matches, ordering falls back to the counter.
464        assert!(id1 < id2);
465    }
466
467    #[test]
468    fn test_resource_canonical_bytes_use_versioned_encoding() {
469        let channel = Resource::channel("Alice", "Bob");
470        let bytes = channel.canonical_bytes();
471        assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
472        assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
473
474        let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 42);
475        let bytes = message.canonical_bytes();
476        assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
477        assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
478    }
479
480    #[test]
481    fn test_channel_encoding_includes_full_queue_payloads() {
482        let channel = Resource::Channel(ChannelState {
483            sender: "Alice".into(),
484            receiver: "Bob".into(),
485            queue: vec![
486                MessagePayload::new("Ping", vec![1, 2, 3]),
487                MessagePayload::new("Pong", vec![4, 5, 6]),
488            ],
489        });
490        let bytes = channel.canonical_bytes();
491
492        assert!(bytes.windows(4).any(|window| window == b"Ping"));
493        assert!(bytes.windows(4).any(|window| window == b"Pong"));
494        assert!(bytes.windows(3).any(|window| window == [1, 2, 3]));
495        assert!(bytes.windows(3).any(|window| window == [4, 5, 6]));
496    }
497
498    #[test]
499    fn test_message_encoding_includes_full_payload_bytes() {
500        let message = Resource::message("Alice", "Bob", "Hello", vec![7, 8, 9, 10], 42);
501        let bytes = message.canonical_bytes();
502
503        assert!(bytes.windows(5).any(|window| window == b"Hello"));
504        assert!(bytes.windows(4).any(|window| window == [7, 8, 9, 10]));
505    }
506
507    #[test]
508    fn test_canonical_encoding_is_stable_per_resource_kind() {
509        let channel = Resource::Channel(ChannelState {
510            sender: "Alice".into(),
511            receiver: "Bob".into(),
512            queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
513        });
514        let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
515        let session = Resource::session("Alice", 12345);
516        let value = Resource::value("blob", vec![9, 8, 7]);
517
518        assert_eq!(channel.canonical_bytes(), channel.canonical_bytes());
519        assert_eq!(message.canonical_bytes(), message.canonical_bytes());
520        assert_eq!(session.canonical_bytes(), session.canonical_bytes());
521        assert_eq!(value.canonical_bytes(), value.canonical_bytes());
522    }
523
524    #[test]
525    fn test_canonical_encoding_distinguishes_semantic_changes() {
526        let left = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
527        let right = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 4], 7);
528
529        assert_ne!(left.canonical_bytes(), right.canonical_bytes());
530    }
531
532    #[test]
533    fn test_canonical_encoding_matches_for_semantically_identical_values() {
534        let left = Resource::Channel(ChannelState {
535            sender: "Alice".into(),
536            receiver: "Bob".into(),
537            queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
538        });
539        let right = Resource::Channel(ChannelState {
540            sender: "Alice".into(),
541            receiver: "Bob".into(),
542            queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
543        });
544
545        assert_eq!(left.canonical_bytes(), right.canonical_bytes());
546    }
547
548    #[test]
549    fn test_message_creation() {
550        let msg = Message::new("Alice", "Bob", "Ping", vec![1, 2, 3], 1);
551        assert_eq!(msg.source, "Alice");
552        assert_eq!(msg.dest, "Bob");
553        assert_eq!(msg.content.label, "Ping");
554        assert_eq!(msg.seq_no, 1);
555    }
556
557    #[test]
558    fn test_channel_state() {
559        let mut channel = ChannelState::new("Alice", "Bob");
560        assert_eq!(channel.queue_size(), 0);
561
562        channel.queue.push(MessagePayload::new("Test", vec![]));
563        assert_eq!(channel.queue_size(), 1);
564    }
565
566    #[test]
567    fn test_heap_error_display() {
568        let rid = ResourceId::<DefaultContentHasher>::new([0u8; 32], 42);
569        let err = HeapError::NotFound(rid);
570        assert!(err.to_string().contains("not found"));
571    }
572
573    #[test]
574    fn test_default_heap_hasher_is_blake3() {
575        let expected = Blake3Hasher::digest(b"heap");
576        let actual = DefaultContentHasher::digest(b"heap");
577        assert_eq!(expected, actual);
578    }
579}