Skip to main content

dig_message/
registry.rs

1//! The extensible message-type registry (SPEC §4) — the runtime seam through which every downstream
2//! subsystem (dig-chat, dig-email, dig-video, peer-RPC, IPC) plugs its own message types into the one
3//! base protocol WITHOUT dig-message depending on any of them.
4//!
5//! Three pieces make the seam:
6//! - [`MessageBand`] + [`MessageType::band`] — the normative id allocation (each subsystem owns a
7//!   256-wide band), so a reader can classify any id it sees.
8//! - [`MessageKind`] — the compile-time contract a type declares: its reserved id + its typed,
9//!   byte-deterministic [`Streamable`] payload.
10//! - [`MessageRegistry`] — the runtime table that maps a [`MessageType`] to a decode-and-handle
11//!   closure, populated additively.
12//!
13//! Forward compatibility (SPEC §4, the §5.1 additive-only spirit) is the load-bearing property: a
14//! `message_type` the registry has never seen MUST fail CLEANLY — [`MessageError::UnsupportedType`] for
15//! a request/stream shape (so the caller replies with an error), a silent drop for a one-shot/response
16//! shape — and MUST NEVER panic. An old reader therefore keeps working when newer senders introduce
17//! new types.
18
19use std::collections::HashMap;
20
21use chia_traits::Streamable;
22
23use crate::envelope::{InteractionShape, MessageType};
24use crate::error::{MessageError, Result};
25
26/// The base of the core band (handshake / ack / error / keepalive) (SPEC §4).
27pub const BAND_CORE: u32 = 0x0000_0000;
28/// The base of the peer-RPC band (peer-to-peer request/response) (SPEC §4).
29pub const BAND_PEER_RPC: u32 = 0x0000_0100;
30/// The base of the dig-chat band (SPEC §4, #768).
31pub const BAND_DIG_CHAT: u32 = 0x0000_0200;
32/// The base of the dig-email band (SPEC §4, #794).
33pub const BAND_DIG_EMAIL: u32 = 0x0000_0300;
34/// The base of the dig-video-chat signaling band (SPEC §4, #795).
35pub const BAND_DIG_VIDEO: u32 = 0x0000_0400;
36/// The base of the presence / directed data-request band (SPEC §4).
37pub const BAND_PRESENCE: u32 = 0x0000_0500;
38/// The base of the dig-ipc-protocol band (authenticated local dig-app ↔ dig-node IPC) (SPEC §4).
39pub const BAND_IPC: u32 = 0x0000_0600;
40/// The base of the dig-social-graph band — the social-graph connection manager's directed connection
41/// offers/acceptances (SPEC §4, #1192, #991 SG-2). RESERVED: no concrete ids allocated yet beyond
42/// the first-consumer `MSG_TYPE_CONNECTION_OFFER` (#991), which lives in dig-social-graph.
43pub const BAND_SOCIAL_GRAPH: u32 = 0x0000_0700;
44/// The base of the node↔relay sealed control band — register / hole-punch / retainer traffic between a
45/// DIG Node and its relay (SPEC §4, #1199). RESERVED: owned by the dig-relay/dig-node relay-control
46/// wire, no concrete ids allocated yet.
47pub const BAND_RELAY_CONTROL: u32 = 0x0000_0800;
48/// The base of the relay↔relay mesh band — frames exchanged between relay instances forming the relay
49/// mesh (SPEC §4, #1200). RESERVED: owned by dig-relay's mesh wire, no concrete ids allocated yet.
50pub const BAND_RELAY_MESH: u32 = 0x0000_0900;
51/// The base of the experimental / vendor band — never shipped as a canonical type (SPEC §4).
52pub const BAND_EXPERIMENTAL: u32 = 0x1000_0000;
53
54/// A subsystem's reserved id band (SPEC §4). Each named band is owned by one subsystem and allocated
55/// additively within it; ids that fall in no allocated subsystem band classify as [`MessageBand::Reserved`].
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum MessageBand {
58    /// Handshake, ack, error, keepalive (`0x0000_0000..=0x0000_00FF`).
59    Core,
60    /// Peer-to-peer request/response (`0x0000_0100..=0x0000_01FF`).
61    PeerRpc,
62    /// dig-chat (`0x0000_0200..=0x0000_02FF`).
63    DigChat,
64    /// dig-email (`0x0000_0300..=0x0000_03FF`).
65    DigEmail,
66    /// dig-video-chat signaling (`0x0000_0400..=0x0000_04FF`).
67    DigVideo,
68    /// Presence / directed data-request (`0x0000_0500..=0x0000_05FF`).
69    Presence,
70    /// dig-ipc-protocol (`0x0000_0600..=0x0000_06FF`).
71    Ipc,
72    /// dig-social-graph connection manager (`0x0000_0700..=0x0000_07FF`, #1192).
73    SocialGraph,
74    /// Node↔relay sealed control — register/hole-punch/retainer (`0x0000_0800..=0x0000_08FF`, #1199).
75    RelayControl,
76    /// Relay↔relay mesh frames (`0x0000_0900..=0x0000_09FF`, #1200).
77    RelayMesh,
78    /// Experimental / vendor, never canonical (`>= 0x1000_0000`).
79    Experimental,
80    /// A currently-unallocated id, reserved for a future subsystem band (SPEC §4).
81    Reserved,
82}
83
84impl MessageType {
85    /// Core: connection handshake (SPEC §4 core band).
86    pub const CORE_HANDSHAKE: MessageType = MessageType(BAND_CORE);
87    /// Core: generic acknowledgement (SPEC §4 core band).
88    pub const CORE_ACK: MessageType = MessageType(BAND_CORE + 1);
89    /// Core: protocol-level error report (SPEC §4 core band).
90    pub const CORE_ERROR: MessageType = MessageType(BAND_CORE + 2);
91    /// Core: liveness keepalive (SPEC §4 core band).
92    pub const CORE_KEEPALIVE: MessageType = MessageType(BAND_CORE + 3);
93
94    /// Classify this id into its reserved [`MessageBand`] (SPEC §4). Total — every `u32` maps to a
95    /// band, so a reader can always classify an id it does not otherwise recognize.
96    #[must_use]
97    pub fn band(self) -> MessageBand {
98        match self.0 {
99            0x0000_0000..=0x0000_00FF => MessageBand::Core,
100            0x0000_0100..=0x0000_01FF => MessageBand::PeerRpc,
101            0x0000_0200..=0x0000_02FF => MessageBand::DigChat,
102            0x0000_0300..=0x0000_03FF => MessageBand::DigEmail,
103            0x0000_0400..=0x0000_04FF => MessageBand::DigVideo,
104            0x0000_0500..=0x0000_05FF => MessageBand::Presence,
105            0x0000_0600..=0x0000_06FF => MessageBand::Ipc,
106            0x0000_0700..=0x0000_07FF => MessageBand::SocialGraph,
107            0x0000_0800..=0x0000_08FF => MessageBand::RelayControl,
108            0x0000_0900..=0x0000_09FF => MessageBand::RelayMesh,
109            0x1000_0000..=0xFFFF_FFFF => MessageBand::Experimental,
110            _ => MessageBand::Reserved,
111        }
112    }
113}
114
115/// The compile-time contract a message type declares to plug into the registry (SPEC §4). A downstream
116/// crate implements it for each of its payload types; dig-message never depends on that crate.
117///
118/// The [`Payload`](MessageKind::Payload) is a Chia-[`Streamable`] type so its bytes are byte-
119/// deterministic across the Rust and wasm/JS targets (SPEC §1); [`TYPE_ID`](MessageKind::TYPE_ID) is
120/// the reserved id from the owning subsystem's band ([`MessageBand`]).
121pub trait MessageKind {
122    /// The reserved [`MessageType`] id for this kind (from the subsystem's band, SPEC §4).
123    const TYPE_ID: MessageType;
124
125    /// The typed, byte-deterministic payload carried by this message type (SPEC §1, §4).
126    type Payload: Streamable;
127}
128
129/// The outcome of dispatching a decoded payload (SPEC §4). Both variants are success — an unknown
130/// one-shot dropping is the intended forward-compat behavior, not an error.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Dispatch {
133    /// A registered handler decoded and processed the payload.
134    Handled,
135    /// The type was unknown and the shape was one-shot/response, so the message was silently dropped
136    /// (SPEC §4). No handler ran and no error is surfaced.
137    Dropped,
138}
139
140/// A registered decode-and-handle closure: it decodes the payload bytes into the kind's Streamable
141/// payload and processes it, propagating any decode/handler failure as a [`MessageError`].
142type Handler = Box<dyn Fn(&[u8]) -> Result<()> + Send + Sync>;
143
144/// The runtime message-type table (SPEC §4). Maps a [`MessageType`] to its decode-and-handle closure,
145/// populated additively by each subsystem at startup.
146///
147/// It is deliberately additive-only: re-registering an already-present id is refused with
148/// [`MessageError::DuplicateType`] rather than silently overwriting, upholding the SPEC §4 rule that an
149/// id, once assigned, is never renumbered or repurposed. Registering a NEW id never disturbs the
150/// handlers already present.
151#[derive(Default)]
152pub struct MessageRegistry {
153    handlers: HashMap<MessageType, Handler>,
154}
155
156impl MessageRegistry {
157    /// An empty registry.
158    #[must_use]
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Register a [`MessageKind`] with the closure that processes its decoded payload.
164    ///
165    /// The stored handler decodes the on-wire bytes into `K::Payload` (Streamable) before invoking
166    /// `handler`, so callers work with the typed payload, never raw bytes.
167    ///
168    /// # Errors
169    /// [`MessageError::DuplicateType`] if the kind's [`MessageKind::TYPE_ID`] is already registered
170    /// (SPEC §4 additive-only — never silently repurpose an id).
171    pub fn register<K, F>(&mut self, handler: F) -> Result<()>
172    where
173        K: MessageKind,
174        F: Fn(K::Payload) -> Result<()> + Send + Sync + 'static,
175    {
176        let type_id = K::TYPE_ID;
177        if self.handlers.contains_key(&type_id) {
178            return Err(MessageError::DuplicateType(type_id.0));
179        }
180        let handler: Handler = Box::new(move |bytes: &[u8]| {
181            let payload =
182                K::Payload::from_bytes(bytes).map_err(|e| MessageError::Codec(e.to_string()))?;
183            handler(payload)
184        });
185        self.handlers.insert(type_id, handler);
186        Ok(())
187    }
188
189    /// Whether a [`MessageType`] has a registered handler.
190    #[must_use]
191    pub fn contains(&self, message_type: MessageType) -> bool {
192        self.handlers.contains_key(&message_type)
193    }
194
195    /// The number of registered types.
196    #[must_use]
197    pub fn len(&self) -> usize {
198        self.handlers.len()
199    }
200
201    /// Whether no types are registered.
202    #[must_use]
203    pub fn is_empty(&self) -> bool {
204        self.handlers.is_empty()
205    }
206
207    /// Route a decoded payload to its registered handler, applying the SPEC §4 unknown-type rule.
208    ///
209    /// `payload` is the already-opened, decompressed type-payload bytes (the seal/decompression are
210    /// WU2/§1.1 concerns upstream of dispatch). `shape` decides how an unknown type fails.
211    ///
212    /// # Errors
213    /// - [`MessageError::UnsupportedType`] when the type is unregistered AND the shape is a request or
214    ///   stream frame (so the caller can reply UNSUPPORTED_TYPE).
215    /// - Whatever the registered handler returns (a decode failure or a handler-reported error).
216    ///
217    /// An unregistered type with a one-shot/response shape is NOT an error: it returns
218    /// [`Dispatch::Dropped`] (silent drop). Dispatch NEVER panics on an unknown type.
219    pub fn dispatch(
220        &self,
221        message_type: MessageType,
222        shape: InteractionShape,
223        payload: &[u8],
224    ) -> Result<Dispatch> {
225        match self.handlers.get(&message_type) {
226            Some(handler) => handler(payload).map(|()| Dispatch::Handled),
227            None => match shape {
228                InteractionShape::Request | InteractionShape::StreamFrame => {
229                    Err(MessageError::UnsupportedType(message_type.0))
230                }
231                InteractionShape::OneShot | InteractionShape::Response => Ok(Dispatch::Dropped),
232            },
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use chia_streamable_macro::Streamable;
241    use std::sync::atomic::{AtomicU32, Ordering};
242    use std::sync::Arc;
243
244    /// A minimal Streamable payload standing in for a real dig-chat text message in these tests.
245    #[derive(Debug, Clone, PartialEq, Eq, Streamable)]
246    struct ChatText {
247        body: Vec<u8>,
248    }
249
250    /// A dig-chat text message kind, reserved in the dig-chat band (SPEC §4).
251    struct ChatTextKind;
252    impl MessageKind for ChatTextKind {
253        const TYPE_ID: MessageType = MessageType(BAND_DIG_CHAT);
254        type Payload = ChatText;
255    }
256
257    /// A second, distinct kind (peer-RPC ping) used to prove additive registration is non-disturbing.
258    #[derive(Debug, Clone, PartialEq, Eq, Streamable)]
259    struct Ping {
260        nonce: u64,
261    }
262    struct PingKind;
263    impl MessageKind for PingKind {
264        const TYPE_ID: MessageType = MessageType(BAND_PEER_RPC);
265        type Payload = Ping;
266    }
267
268    #[test]
269    fn register_then_dispatch_decodes_and_routes_to_the_handler() {
270        let seen = Arc::new(AtomicU32::new(0));
271        let sink = Arc::clone(&seen);
272
273        let mut registry = MessageRegistry::new();
274        registry
275            .register::<ChatTextKind, _>(move |msg: ChatText| {
276                sink.store(msg.body.len() as u32, Ordering::SeqCst);
277                Ok(())
278            })
279            .unwrap();
280
281        let payload = ChatText {
282            body: vec![1, 2, 3, 4, 5],
283        }
284        .to_bytes()
285        .unwrap();
286
287        let outcome = registry
288            .dispatch(ChatTextKind::TYPE_ID, InteractionShape::Request, &payload)
289            .unwrap();
290
291        assert_eq!(outcome, Dispatch::Handled);
292        assert_eq!(
293            seen.load(Ordering::SeqCst),
294            5,
295            "handler saw the decoded payload"
296        );
297    }
298
299    #[test]
300    fn unknown_type_request_returns_unsupported_type() {
301        let registry = MessageRegistry::new();
302        let unknown = MessageType(BAND_DIG_EMAIL + 0x42);
303        assert_eq!(
304            registry
305                .dispatch(unknown, InteractionShape::Request, &[])
306                .unwrap_err(),
307            MessageError::UnsupportedType(unknown.0)
308        );
309    }
310
311    #[test]
312    fn unknown_type_stream_frame_returns_unsupported_type() {
313        let registry = MessageRegistry::new();
314        let unknown = MessageType(BAND_DIG_VIDEO + 7);
315        assert_eq!(
316            registry
317                .dispatch(unknown, InteractionShape::StreamFrame, &[])
318                .unwrap_err(),
319            MessageError::UnsupportedType(unknown.0)
320        );
321    }
322
323    #[test]
324    fn unknown_type_one_shot_is_silently_dropped_not_an_error() {
325        let registry = MessageRegistry::new();
326        let unknown = MessageType(BAND_EXPERIMENTAL);
327        assert_eq!(
328            registry
329                .dispatch(unknown, InteractionShape::OneShot, &[])
330                .unwrap(),
331            Dispatch::Dropped
332        );
333    }
334
335    #[test]
336    fn unknown_type_response_is_silently_dropped() {
337        let registry = MessageRegistry::new();
338        let unknown = MessageType(BAND_PRESENCE + 1);
339        assert_eq!(
340            registry
341                .dispatch(unknown, InteractionShape::Response, &[])
342                .unwrap(),
343            Dispatch::Dropped
344        );
345    }
346
347    #[test]
348    fn registering_a_new_type_never_disturbs_existing_registrations() {
349        let mut registry = MessageRegistry::new();
350        registry
351            .register::<ChatTextKind, _>(|_: ChatText| Ok(()))
352            .unwrap();
353        assert!(registry.contains(ChatTextKind::TYPE_ID));
354
355        // Additively add a second, unrelated type.
356        registry.register::<PingKind, _>(|_: Ping| Ok(())).unwrap();
357
358        assert_eq!(registry.len(), 2);
359        assert!(
360            registry.contains(ChatTextKind::TYPE_ID),
361            "the first type is undisturbed"
362        );
363        assert!(registry.contains(PingKind::TYPE_ID));
364    }
365
366    #[test]
367    fn re_registering_the_same_id_is_refused_additive_only() {
368        let mut registry = MessageRegistry::new();
369        registry
370            .register::<ChatTextKind, _>(|_: ChatText| Ok(()))
371            .unwrap();
372        assert_eq!(
373            registry
374                .register::<ChatTextKind, _>(|_: ChatText| Ok(()))
375                .unwrap_err(),
376            MessageError::DuplicateType(ChatTextKind::TYPE_ID.0)
377        );
378        assert_eq!(
379            registry.len(),
380            1,
381            "the failed re-registration left the table unchanged"
382        );
383    }
384
385    #[test]
386    fn a_handler_decode_failure_propagates_and_never_panics() {
387        let mut registry = MessageRegistry::new();
388        registry.register::<PingKind, _>(|_: Ping| Ok(())).unwrap();
389        // A `Ping` is a u64 (8 bytes); a 3-byte frame cannot decode.
390        let err = registry
391            .dispatch(PingKind::TYPE_ID, InteractionShape::Request, &[1, 2, 3])
392            .unwrap_err();
393        assert!(matches!(err, MessageError::Codec(_)));
394    }
395
396    #[test]
397    fn a_handler_reported_error_propagates() {
398        let mut registry = MessageRegistry::new();
399        registry
400            .register::<PingKind, _>(|_: Ping| Err(MessageError::UnsupportedType(0)))
401            .unwrap();
402        let payload = Ping { nonce: 9 }.to_bytes().unwrap();
403        assert!(registry
404            .dispatch(PingKind::TYPE_ID, InteractionShape::OneShot, &payload)
405            .is_err());
406    }
407
408    #[test]
409    fn every_reserved_band_classifies_at_its_boundaries() {
410        let cases = [
411            (BAND_CORE, MessageBand::Core),
412            (BAND_CORE + 0xFF, MessageBand::Core),
413            (BAND_PEER_RPC, MessageBand::PeerRpc),
414            (BAND_PEER_RPC + 0xFF, MessageBand::PeerRpc),
415            (BAND_DIG_CHAT, MessageBand::DigChat),
416            (BAND_DIG_CHAT + 0xFF, MessageBand::DigChat),
417            (BAND_DIG_EMAIL, MessageBand::DigEmail),
418            (BAND_DIG_VIDEO, MessageBand::DigVideo),
419            (BAND_PRESENCE, MessageBand::Presence),
420            (BAND_IPC, MessageBand::Ipc),
421            (BAND_IPC + 0xFF, MessageBand::Ipc),
422            (BAND_SOCIAL_GRAPH, MessageBand::SocialGraph),
423            (BAND_SOCIAL_GRAPH + 0xFF, MessageBand::SocialGraph),
424            (BAND_RELAY_CONTROL, MessageBand::RelayControl),
425            (BAND_RELAY_CONTROL + 0xFF, MessageBand::RelayControl),
426            (BAND_RELAY_MESH, MessageBand::RelayMesh),
427            (BAND_RELAY_MESH + 0xFF, MessageBand::RelayMesh),
428            (BAND_EXPERIMENTAL, MessageBand::Experimental),
429            (0xFFFF_FFFF, MessageBand::Experimental),
430        ];
431        for (id, expected) in cases {
432            assert_eq!(MessageType(id).band(), expected, "id {id:#010x}");
433        }
434    }
435
436    #[test]
437    fn unallocated_ids_classify_as_reserved() {
438        // Just past the last named subsystem band (relay-mesh ends at 0x09FF) and below experimental.
439        for id in [0x0000_0A00, 0x0000_1000, 0x00FF_FFFF, 0x0FFF_FFFF] {
440            assert_eq!(
441                MessageType(id).band(),
442                MessageBand::Reserved,
443                "id {id:#010x}"
444            );
445        }
446    }
447
448    #[test]
449    fn named_core_type_constants_land_in_the_core_band() {
450        for mt in [
451            MessageType::CORE_HANDSHAKE,
452            MessageType::CORE_ACK,
453            MessageType::CORE_ERROR,
454            MessageType::CORE_KEEPALIVE,
455        ] {
456            assert_eq!(mt.band(), MessageBand::Core);
457        }
458    }
459}