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}