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}