Skip to main content

net/adapter/net/behavior/fold/
dispatch.rs

1//! Inbound dispatch — route envelope bytes to the right
2//! [`Fold<K>`] by `kind` u16, verify the signature, hand off to
3//! the typed apply path.
4//!
5//! The [`FoldRegistry`] is the type-erased entry point. Each
6//! [`Fold<K>`] is wrapped in a [`FoldDispatchAdapter<K>`] that
7//! implements the non-generic [`FoldDispatch`] trait by decoding
8//! + verifying the envelope, cross-checking the decoded `kind`
9//!   against the adapter's [`FoldKind::KIND_ID`] (catches
10//!   misregistered folds and crossed-channel publishes), and
11//!   calling [`Fold::apply`] on the verified envelope.
12//!
13//! The registry holds an `Arc<dyn FoldDispatch>` per registered
14//! `kind`. The dispatch hot path takes one `RwLock<HashMap>` read
15//! lock for the lookup; the per-fold apply takes its own write
16//! lock internally.
17
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use parking_lot::RwLock;
22
23use super::metrics::FoldStats;
24use super::state::ApplyOutcome;
25use super::wire::SignedAnnouncement;
26use super::wire::WireError;
27use super::{Fold, FoldKind};
28use crate::adapter::net::identity::EntityId;
29
30/// Wire subprotocol slot for fold-channel traffic. The per-kind
31/// demux happens inside the [`FoldRegistry`] (routing on the
32/// envelope's `kind` u16), so one subprotocol covers every
33/// `FoldKind`. Slots `0x1001..=0x10FF` are reserved for parallel
34/// fold envelope shapes if a future design needs one.
35pub const SUBPROTOCOL_FOLD: u16 = 0x1000;
36
37/// Type-erased view of a single [`Fold<K>`] instance, suitable
38/// for storage in a `HashMap<u16, Arc<dyn FoldDispatch>>`.
39/// Implemented by [`FoldDispatchAdapter<K>`] for every concrete
40/// `K: FoldKind`.
41pub trait FoldDispatch: Send + Sync {
42    /// `KIND_ID` of the wrapped fold. Returned by the adapter
43    /// so the registry can cross-check on `register` and the
44    /// dispatch path can reject envelopes whose decoded `kind`
45    /// disagrees with the routing key.
46    fn kind_id(&self) -> u16;
47
48    /// Decode + verify + apply. Returns the apply outcome so
49    /// metrics / audit can attribute the result; surfaces
50    /// `WireError` for codec / verification failures and wraps
51    /// any apply-side `FoldError` via `WireError::Apply`.
52    fn dispatch(
53        &self,
54        bytes: &[u8],
55        publisher: &crate::adapter::net::identity::EntityId,
56    ) -> Result<ApplyOutcome, WireError>;
57
58    /// Type-erased [`Fold::stats`]. The operator surface
59    /// aggregates these across the registry via
60    /// [`FoldRegistry::stats`] so a single `net fold list` call
61    /// returns one row per registered fold.
62    fn stats(&self) -> FoldStats;
63}
64
65/// Adapter that lifts a typed [`Fold<K>`] into the non-generic
66/// [`FoldDispatch`] trait. Holds an `Arc<Fold<K>>` so multiple
67/// dispatchers (and the application code that queries the fold
68/// directly) share the same instance.
69pub struct FoldDispatchAdapter<K: FoldKind> {
70    fold: Arc<Fold<K>>,
71}
72
73impl<K: FoldKind> FoldDispatchAdapter<K> {
74    /// Wrap a typed fold for registry insertion.
75    pub fn new(fold: Arc<Fold<K>>) -> Self {
76        Self { fold }
77    }
78
79    /// Borrow the underlying fold. Useful for tests that
80    /// register a fold via the registry but want to inspect
81    /// state through the typed API.
82    pub fn fold(&self) -> &Arc<Fold<K>> {
83        &self.fold
84    }
85}
86
87impl<K: FoldKind> FoldDispatch for FoldDispatchAdapter<K> {
88    fn kind_id(&self) -> u16 {
89        K::KIND_ID
90    }
91
92    fn stats(&self) -> FoldStats {
93        self.fold.stats()
94    }
95
96    fn dispatch(
97        &self,
98        bytes: &[u8],
99        publisher: &crate::adapter::net::identity::EntityId,
100    ) -> Result<ApplyOutcome, WireError> {
101        // Decode + verify in one shot. `decode_and_verify` runs
102        // the postcard decode, the length / placeholder /
103        // public-key checks, and the Ed25519 verify; the rest of
104        // this function operates on a known-good envelope.
105        let ann = SignedAnnouncement::<K::Payload>::decode_and_verify(bytes, publisher)?;
106
107        // Cross-check the envelope's `kind` field against the
108        // wrapped fold's `KIND_ID`. The registry routed us here
109        // by the wire `kind` byte, so a mismatch means either
110        // (a) the registry was constructed wrong (e.g. a fold
111        // was registered under the wrong key) or (b) the
112        // envelope was hand-crafted to lie about its kind.
113        // Either way, refusing the apply is the safe move; the
114        // signature already verified against the publisher so
115        // we surface the mismatch back to the caller for logging.
116        if ann.kind != K::KIND_ID {
117            return Err(WireError::KindMismatch {
118                got: ann.kind,
119                expected: K::KIND_ID,
120            });
121        }
122
123        Ok(self.fold.apply(ann)?)
124    }
125}
126
127/// Registry of [`FoldDispatch`] adapters keyed by
128/// [`FoldKind::KIND_ID`]. The central connection between an
129/// inbound channel message (raw bytes + publisher identity) and
130/// the right fold's apply path. Construct typed [`Fold<K>`]
131/// instances, wrap each in a [`FoldDispatchAdapter<K>`], and
132/// register them here.
133pub struct FoldRegistry {
134    folds: RwLock<HashMap<u16, Arc<dyn FoldDispatch>>>,
135}
136
137impl FoldRegistry {
138    /// Construct an empty registry.
139    pub fn new() -> Self {
140        Self {
141            folds: RwLock::new(HashMap::new()),
142        }
143    }
144
145    /// Register a typed fold under its [`FoldKind::KIND_ID`].
146    /// Returns the previously-registered dispatcher under the
147    /// same kind if any, so callers that legitimately want to
148    /// replace a fold (e.g. swap a new index implementation in
149    /// during operator-driven reconfiguration) can drop the
150    /// old one cleanly.
151    pub fn register<K: FoldKind>(&self, fold: Arc<Fold<K>>) -> Option<Arc<dyn FoldDispatch>> {
152        let adapter = Arc::new(FoldDispatchAdapter::new(fold));
153        self.folds
154            .write()
155            .insert(K::KIND_ID, adapter as Arc<dyn FoldDispatch>)
156    }
157
158    /// Remove a fold by kind. Returns the dropped dispatcher if
159    /// one was registered.
160    pub fn deregister(&self, kind: u16) -> Option<Arc<dyn FoldDispatch>> {
161        self.folds.write().remove(&kind)
162    }
163
164    /// Number of registered folds.
165    pub fn len(&self) -> usize {
166        self.folds.read().len()
167    }
168
169    /// Whether the registry has no folds registered.
170    pub fn is_empty(&self) -> bool {
171        self.folds.read().is_empty()
172    }
173
174    /// Look up a registered dispatcher by kind. Used by tests
175    /// and by the channel-integration adapter; the hot path uses
176    /// [`Self::dispatch`] directly.
177    pub fn get(&self, kind: u16) -> Option<Arc<dyn FoldDispatch>> {
178        self.folds.read().get(&kind).cloned()
179    }
180
181    /// Aggregate [`FoldStats`] across every registered fold.
182    /// The operator surface (`net fold list`, the Deck FOLDS
183    /// panel) calls this once per sample tick. Returns in
184    /// unspecified order; callers that want a canonical sort sort
185    /// themselves.
186    pub fn stats(&self) -> Vec<FoldStats> {
187        self.folds
188            .read()
189            .values()
190            .map(|adapter| adapter.stats())
191            .collect()
192    }
193
194    /// Dispatch an inbound wire envelope to the right fold.
195    ///
196    /// The dispatch is two-step:
197    /// 1. A private `peek_kind` helper reads the leading
198    ///    `kind: u16` varint to pick the right adapter. This is
199    ///    unavoidable: the per-fold adapter is typed on
200    ///    `K::Payload`, so we can't run the full envelope decode
201    ///    until we know `K`.
202    /// 2. The matched adapter runs the full
203    ///    [`SignedAnnouncement::decode_and_verify`] (which also
204    ///    re-reads the `kind` field as part of the struct
205    ///    decode) and then `Fold::apply`.
206    ///
207    /// The leading varint thus pays for itself twice — once for
208    /// routing, once during the typed decode — but the cost is
209    /// ~10 ns of postcard varint work next to a ~50 µs Ed25519
210    /// verify on the same envelope. Worth flagging here so
211    /// future readers don't chase it as a hot-path concern.
212    pub fn dispatch(
213        &self,
214        bytes: &[u8],
215        publisher: &crate::adapter::net::identity::EntityId,
216    ) -> Result<ApplyOutcome, DispatchError> {
217        let kind = peek_kind(bytes).ok_or(DispatchError::Truncated)?;
218        let adapter = self.get(kind).ok_or(DispatchError::UnknownKind(kind))?;
219        adapter
220            .dispatch(bytes, publisher)
221            .map_err(DispatchError::Wire)
222    }
223}
224
225impl Default for FoldRegistry {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// Hook the mesh's inbound channel-dispatch path uses to route
232/// fold announcements. `mesh.rs::dispatch_packet` installs an
233/// `Arc<dyn FoldChannelRouter>` (typically a [`FoldRegistry`])
234/// and routes every event from a `SUBPROTOCOL_FOLD` packet
235/// through it.
236///
237/// The trait abstracts the registry away from the mesh so tests
238/// can stub the router with a counting / inspecting impl.
239/// `publisher` is the [`EntityId`] resolved at dispatch time
240/// from the inbound session's `node_id` via the mesh's
241/// `peer_entity_ids` map; the router uses it to verify the
242/// announcement's signature.
243pub trait FoldChannelRouter: Send + Sync {
244    /// Route one wire envelope to the right fold. Errors are
245    /// surfaced so the mesh dispatch arm can log + bump metrics;
246    /// the mesh never lets a router error escape into the rest
247    /// of the inbound pipeline (single-packet failures must not
248    /// take down the dispatch loop).
249    fn try_route(&self, publisher: &EntityId, bytes: &[u8]) -> Result<ApplyOutcome, DispatchError>;
250
251    /// Aggregated [`FoldStats`] for every fold the router
252    /// addresses. The operator surface (`net fold list`, the
253    /// Deck FOLDS panel, the Prometheus exporter) calls into
254    /// the router-trait object to read stats without knowing
255    /// the underlying concrete type. Implementations that
256    /// don't track per-fold stats return an empty `Vec`.
257    fn stats(&self) -> Vec<FoldStats>;
258}
259
260impl FoldChannelRouter for FoldRegistry {
261    fn try_route(&self, publisher: &EntityId, bytes: &[u8]) -> Result<ApplyOutcome, DispatchError> {
262        self.dispatch(bytes, publisher)
263    }
264
265    fn stats(&self) -> Vec<FoldStats> {
266        FoldRegistry::stats(self)
267    }
268}
269
270/// Top-level errors the [`FoldRegistry::dispatch`] surfaces.
271/// Distinct from [`WireError`] because the registry layer
272/// surfaces routing-shaped failures (no fold for this kind,
273/// truncated envelope) separately from per-fold codec /
274/// verification errors.
275#[derive(Debug, thiserror::Error)]
276pub enum DispatchError {
277    /// Envelope was shorter than the 1-3 byte postcard varint
278    /// the wire `kind` field occupies (an empty buffer, or a
279    /// continuation-byte-promised followup that's missing).
280    #[error("envelope truncated before kind varint completes")]
281    Truncated,
282
283    /// No fold registered for the envelope's `kind`. The
284    /// dispatch layer logs + drops; the `kind` is surfaced so
285    /// operator dashboards can pick up "publisher is on the
286    /// wrong wire schema."
287    #[error("no fold registered for kind {0:#06x}")]
288    UnknownKind(u16),
289
290    /// Per-fold codec / verification / apply error. Wraps the
291    /// underlying [`WireError`] so the caller can pattern-match
292    /// against the specific failure mode.
293    #[error("wire / verify / apply failed: {0}")]
294    Wire(#[from] WireError),
295}
296
297/// Read the wire `kind: u16` varint from the head of an
298/// envelope buffer. Returns `None` for buffers that don't
299/// carry a complete varint at the head — the registry surfaces
300/// that as `DispatchError::Truncated`.
301///
302/// Uses `postcard::take_from_bytes` so the varint shape stays in
303/// lockstep with whatever postcard version the codec uses.
304fn peek_kind(bytes: &[u8]) -> Option<u16> {
305    let (kind, _rest) = postcard::take_from_bytes::<u16>(bytes).ok()?;
306    Some(kind)
307}