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/ResourceModel.lean`.
17//! The specific Rust types (`ResourceId`, `Resource`, `HeapError`) are Rust-only.
18
19use sha2::{Digest, Sha256};
20use std::fmt;
21
22/// Unique identifier for heap-allocated resources.
23///
24/// ResourceId is derived from the content hash of the resource,
25/// combined with an allocation counter to ensure uniqueness even
26/// for identical content.
27#[derive(Clone, PartialEq, Eq, Hash)]
28pub struct ResourceId {
29    /// The content hash (SHA-256)
30    hash: [u8; 32],
31    /// Allocation counter (for uniqueness of identical content)
32    counter: u64,
33}
34
35impl ResourceId {
36    /// Create a new ResourceId from hash and counter.
37    pub fn new(hash: [u8; 32], counter: u64) -> Self {
38        Self { hash, counter }
39    }
40
41    /// Create a ResourceId from a resource and allocation counter.
42    pub fn from_resource(resource: &Resource, counter: u64) -> Self {
43        let content_bytes = resource.to_bytes();
44        let counter_bytes = counter.to_le_bytes();
45
46        let mut hasher = Sha256::new();
47        hasher.update(content_bytes);
48        hasher.update(counter_bytes);
49        let hash: [u8; 32] = hasher.finalize().into();
50
51        Self { hash, counter }
52    }
53
54    /// Display as a short hex string.
55    pub fn to_short_hex(&self) -> String {
56        let hex: String = self.hash[..4]
57            .iter()
58            .map(|b| format!("{:02x}", b))
59            .collect();
60        format!("{}:{}", hex, self.counter)
61    }
62
63    /// Get the raw hash bytes.
64    #[must_use]
65    pub fn hash(&self) -> [u8; 32] {
66        self.hash
67    }
68
69    /// Get the allocation counter.
70    #[must_use]
71    pub fn counter(&self) -> u64 {
72        self.counter
73    }
74}
75
76impl fmt::Debug for ResourceId {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "ResourceId({})", self.to_short_hex())
79    }
80}
81
82impl fmt::Display for ResourceId {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}", self.to_short_hex())
85    }
86}
87
88impl PartialOrd for ResourceId {
89    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
90        Some(self.cmp(other))
91    }
92}
93
94impl Ord for ResourceId {
95    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
96        match self.hash.cmp(&other.hash) {
97            std::cmp::Ordering::Equal => self.counter.cmp(&other.counter),
98            ord => ord,
99        }
100    }
101}
102
103/// A message payload with label and data.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct MessagePayload {
106    /// The message label
107    pub label: String,
108    /// The serialized payload
109    pub payload: Vec<u8>,
110}
111
112impl MessagePayload {
113    /// Create a new message payload.
114    pub fn new(label: impl Into<String>, payload: Vec<u8>) -> Self {
115        Self {
116            label: label.into(),
117            payload,
118        }
119    }
120}
121
122/// State of a communication channel between two roles.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct ChannelState {
125    /// The sender role
126    pub sender: String,
127    /// The receiver role
128    pub receiver: String,
129    /// Pending messages in the channel
130    pub queue: Vec<MessagePayload>,
131}
132
133impl ChannelState {
134    /// Create a new empty channel.
135    pub fn new(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
136        Self {
137            sender: sender.into(),
138            receiver: receiver.into(),
139            queue: Vec::new(),
140        }
141    }
142
143    /// Get the number of pending messages.
144    pub fn queue_size(&self) -> usize {
145        self.queue.len()
146    }
147}
148
149/// A message resource representing a message in transit.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct Message {
152    /// Source role
153    pub source: String,
154    /// Destination role
155    pub dest: String,
156    /// Message content
157    pub content: MessagePayload,
158    /// Sequence number for ordering
159    pub seq_no: u64,
160}
161
162impl Message {
163    /// Create a new message.
164    pub fn new(
165        source: impl Into<String>,
166        dest: impl Into<String>,
167        label: impl Into<String>,
168        payload: Vec<u8>,
169        seq_no: u64,
170    ) -> Self {
171        Self {
172            source: source.into(),
173            dest: dest.into(),
174            content: MessagePayload::new(label, payload),
175            seq_no,
176        }
177    }
178}
179
180/// Resource kinds that can be allocated on the heap.
181///
182/// All resources are immutable and content-addressed.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum Resource {
185    /// A communication channel between roles
186    Channel(ChannelState),
187    /// A message in transit
188    Message(Message),
189    /// Current session state for a role
190    Session {
191        /// The role name
192        role: String,
193        /// Hash of the session type (LocalTypeR)
194        type_hash: u64,
195    },
196    /// An arbitrary value
197    Value {
198        /// Type tag
199        tag: String,
200        /// Serialized value
201        data: Vec<u8>,
202    },
203}
204
205impl Resource {
206    /// Create a channel resource.
207    pub fn channel(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
208        Resource::Channel(ChannelState::new(sender, receiver))
209    }
210
211    /// Create a message resource.
212    pub fn message(
213        source: impl Into<String>,
214        dest: impl Into<String>,
215        label: impl Into<String>,
216        payload: Vec<u8>,
217        seq_no: u64,
218    ) -> Self {
219        Resource::Message(Message::new(source, dest, label, payload, seq_no))
220    }
221
222    /// Create a session resource.
223    pub fn session(role: impl Into<String>, type_hash: u64) -> Self {
224        Resource::Session {
225            role: role.into(),
226            type_hash,
227        }
228    }
229
230    /// Create a value resource.
231    pub fn value(tag: impl Into<String>, data: Vec<u8>) -> Self {
232        Resource::Value {
233            tag: tag.into(),
234            data,
235        }
236    }
237
238    /// Get the resource kind as a string.
239    pub fn kind(&self) -> &'static str {
240        match self {
241            Resource::Channel(_) => "channel",
242            Resource::Message(_) => "message",
243            Resource::Session { .. } => "session",
244            Resource::Value { .. } => "value",
245        }
246    }
247
248    /// Serialize the resource to bytes for content addressing.
249    ///
250    /// This is used for computing the content hash. A production
251    /// implementation would use DAG-CBOR or similar canonical serialization.
252    pub fn to_bytes(&self) -> Vec<u8> {
253        let mut bytes = Vec::new();
254
255        match self {
256            Resource::Channel(cs) => {
257                bytes.extend_from_slice(b"channel:");
258                bytes.extend_from_slice(cs.sender.as_bytes());
259                bytes.push(0);
260                bytes.extend_from_slice(cs.receiver.as_bytes());
261                bytes.push(0);
262                bytes.extend_from_slice(&cs.queue.len().to_le_bytes());
263            }
264            Resource::Message(msg) => {
265                bytes.extend_from_slice(b"message:");
266                bytes.extend_from_slice(msg.source.as_bytes());
267                bytes.push(0);
268                bytes.extend_from_slice(msg.dest.as_bytes());
269                bytes.push(0);
270                bytes.extend_from_slice(msg.content.label.as_bytes());
271                bytes.push(0);
272                bytes.extend_from_slice(&msg.seq_no.to_le_bytes());
273            }
274            Resource::Session { role, type_hash } => {
275                bytes.extend_from_slice(b"session:");
276                bytes.extend_from_slice(role.as_bytes());
277                bytes.push(0);
278                bytes.extend_from_slice(&type_hash.to_le_bytes());
279            }
280            Resource::Value { tag, data } => {
281                bytes.extend_from_slice(b"value:");
282                bytes.extend_from_slice(tag.as_bytes());
283                bytes.push(0);
284                bytes.extend_from_slice(&data.len().to_le_bytes());
285                bytes.extend_from_slice(data);
286            }
287        }
288
289        bytes
290    }
291}
292
293impl fmt::Display for Resource {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        write!(f, "Resource({})", self.kind())
296    }
297}
298
299/// Errors that can occur during heap operations.
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub enum HeapError {
302    /// Resource not found in heap
303    NotFound(ResourceId),
304    /// Resource already consumed (nullified)
305    AlreadyConsumed(ResourceId),
306    /// Resource already exists with this ID
307    AlreadyExists(ResourceId),
308    /// Invalid resource type for operation
309    TypeMismatch { expected: String, got: String },
310    /// Generic error with message
311    Other(String),
312}
313
314impl fmt::Display for HeapError {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match self {
317            HeapError::NotFound(rid) => write!(f, "Resource not found: {}", rid),
318            HeapError::AlreadyConsumed(rid) => write!(f, "Resource already consumed: {}", rid),
319            HeapError::AlreadyExists(rid) => write!(f, "Resource already exists: {}", rid),
320            HeapError::TypeMismatch { expected, got } => {
321                write!(f, "Type mismatch: expected {}, got {}", expected, got)
322            }
323            HeapError::Other(msg) => write!(f, "{}", msg),
324        }
325    }
326}
327
328impl std::error::Error for HeapError {}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_resource_id_creation() {
336        let r1 = Resource::channel("Alice", "Bob");
337        let r2 = Resource::channel("Alice", "Bob");
338
339        let id1 = ResourceId::from_resource(&r1, 0);
340        let id2 = ResourceId::from_resource(&r2, 0);
341        let id3 = ResourceId::from_resource(&r1, 1);
342
343        // Same resource, same counter → same ID
344        assert_eq!(id1, id2);
345        // Same resource, different counter → different ID
346        assert_ne!(id1, id3);
347    }
348
349    #[test]
350    fn test_resource_id_ordering() {
351        let r = Resource::channel("Alice", "Bob");
352        let id1 = ResourceId::from_resource(&r, 0);
353        let id2 = ResourceId::from_resource(&r, 1);
354
355        // Same hash, different counter
356        assert!(id1 < id2);
357    }
358
359    #[test]
360    fn test_resource_to_bytes() {
361        let channel = Resource::channel("Alice", "Bob");
362        let bytes = channel.to_bytes();
363        assert!(bytes.starts_with(b"channel:"));
364
365        let msg = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 42);
366        let bytes = msg.to_bytes();
367        assert!(bytes.starts_with(b"message:"));
368    }
369
370    #[test]
371    fn test_message_creation() {
372        let msg = Message::new("Alice", "Bob", "Ping", vec![1, 2, 3], 1);
373        assert_eq!(msg.source, "Alice");
374        assert_eq!(msg.dest, "Bob");
375        assert_eq!(msg.content.label, "Ping");
376        assert_eq!(msg.seq_no, 1);
377    }
378
379    #[test]
380    fn test_channel_state() {
381        let mut channel = ChannelState::new("Alice", "Bob");
382        assert_eq!(channel.queue_size(), 0);
383
384        channel.queue.push(MessagePayload::new("Test", vec![]));
385        assert_eq!(channel.queue_size(), 1);
386    }
387
388    #[test]
389    fn test_heap_error_display() {
390        let rid = ResourceId::new([0u8; 32], 42);
391        let err = HeapError::NotFound(rid);
392        assert!(err.to_string().contains("not found"));
393    }
394}