Skip to main content

net/adapter/net/subprotocol/
descriptor.rs

1//! Subprotocol descriptor and version types.
2//!
3//! A subprotocol is identified by a `u16` ID in the Net header and
4//! described by a `SubprotocolDescriptor` carrying name, version, and
5//! compatibility metadata.
6
7use bytes::{Buf, BufMut};
8
9/// Semantic version for subprotocol negotiation.
10///
11/// Two peers are compatible if both peers' version >= the other's
12/// `min_compatible`. Wire format: 2 bytes (major, minor).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct SubprotocolVersion {
15    /// Major version — breaking changes increment this.
16    pub major: u8,
17    /// Minor version — backward-compatible changes.
18    pub minor: u8,
19}
20
21impl SubprotocolVersion {
22    /// Create a new version.
23    pub const fn new(major: u8, minor: u8) -> Self {
24        Self { major, minor }
25    }
26
27    /// Check if this version is compatible with a peer's minimum requirement.
28    #[inline]
29    pub fn satisfies(self, min_required: Self) -> bool {
30        self >= min_required
31    }
32
33    /// Serialize to 2 bytes.
34    #[inline]
35    pub fn to_bytes(self) -> [u8; 2] {
36        [self.major, self.minor]
37    }
38
39    /// Deserialize from 2 bytes.
40    #[inline]
41    pub fn from_bytes(data: &[u8; 2]) -> Self {
42        Self {
43            major: data[0],
44            minor: data[1],
45        }
46    }
47}
48
49impl std::fmt::Display for SubprotocolVersion {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}.{}", self.major, self.minor)
52    }
53}
54
55/// Metadata describing a registered subprotocol.
56#[derive(Debug, Clone)]
57pub struct SubprotocolDescriptor {
58    /// Unique protocol ID (from Net header `subprotocol_id` field).
59    pub id: u16,
60    /// Human-readable name (e.g., "causal", "migration", "vendor-x-inference").
61    pub name: String,
62    /// Version of this handler.
63    pub version: SubprotocolVersion,
64    /// Minimum compatible version accepted from peers.
65    pub min_compatible: SubprotocolVersion,
66    /// Whether this node can process packets for this subprotocol
67    /// (false = opaque forwarding only, no local handler).
68    pub handler_present: bool,
69}
70
71impl SubprotocolDescriptor {
72    /// Create a new descriptor.
73    pub fn new(id: u16, name: impl Into<String>, version: SubprotocolVersion) -> Self {
74        Self {
75            id,
76            name: name.into(),
77            version,
78            min_compatible: version,
79            handler_present: true,
80        }
81    }
82
83    /// Set the minimum compatible version.
84    ///
85    /// Enforces the wire-format invariant
86    /// `min_compatible <= version`. Allowing
87    /// `min_compatible > version` would break
88    /// `is_compatible_with`'s contract — every honest peer
89    /// computes `local.version.satisfies(other.min_compatible)`,
90    /// which silently fails for any version of `local` once
91    /// `other.min_compatible > other.version`. On the wire-format
92    /// side that enables a phantom-incompatibility DoS where a
93    /// peer advertises `min_compatible=255.255` against
94    /// `version=1.0` and unilaterally evicts the subprotocol from
95    /// negotiation. The constructor (`new`) initializes
96    /// `min_compatible = version` so the invariant holds by
97    /// default; this setter clamps `min` to `self.version` if a
98    /// caller passes a higher value.
99    pub fn with_min_compatible(mut self, min: SubprotocolVersion) -> Self {
100        self.min_compatible = if min > self.version {
101            self.version
102        } else {
103            min
104        };
105        self
106    }
107
108    /// Mark as forwarding-only (no local handler).
109    pub fn forwarding_only(mut self) -> Self {
110        self.handler_present = false;
111        self
112    }
113
114    /// Check if two descriptors are version-compatible.
115    ///
116    /// Both sides must satisfy the other's minimum requirement.
117    pub fn is_compatible_with(&self, other: &Self) -> bool {
118        self.id == other.id
119            && self.version.satisfies(other.min_compatible)
120            && other.version.satisfies(self.min_compatible)
121    }
122
123    /// Capability tag for this subprotocol (e.g., "subprotocol:0x0400").
124    pub fn capability_tag(&self) -> String {
125        format!("subprotocol:{:#06x}", self.id)
126    }
127}
128
129impl std::fmt::Display for SubprotocolDescriptor {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(f, "{}({:#06x}) v{}", self.name, self.id, self.version)
132    }
133}
134
135/// Wire format entry for manifest exchange (6 bytes per entry).
136///
137/// ```text
138/// id:             2 bytes (u16)
139/// version:        2 bytes (major, minor)
140/// min_compatible: 2 bytes (major, minor)
141/// ```
142pub const MANIFEST_ENTRY_SIZE: usize = 6;
143
144/// Serialize a descriptor to a manifest entry (6 bytes).
145pub fn write_manifest_entry(desc: &SubprotocolDescriptor, buf: &mut impl BufMut) {
146    buf.put_u16_le(desc.id);
147    buf.put_slice(&desc.version.to_bytes());
148    buf.put_slice(&desc.min_compatible.to_bytes());
149}
150
151/// Deserialize a manifest entry from bytes.
152///
153/// Rejects entries that violate the wire-format invariant
154/// `min_compatible <= version`. Without this guard, a peer could
155/// advertise `version=1.0, min_compatible=255.255` and every
156/// honest peer's `negotiate()` would mark the subprotocol
157/// `incompatible` (because `local.version.satisfies(remote.min)`
158/// fails for any local), unilaterally evicting that subprotocol
159/// from negotiation between the victim and its peers — a
160/// phantom-incompatibility DoS that requires no actual presence
161/// on the channel. Returning `None` for such entries makes them
162/// surface as a parse error to the caller (the manifest is
163/// already structured to skip parse failures gracefully).
164pub fn read_manifest_entry(
165    buf: &mut impl Buf,
166) -> Option<(u16, SubprotocolVersion, SubprotocolVersion)> {
167    if buf.remaining() < MANIFEST_ENTRY_SIZE {
168        return None;
169    }
170    let id = buf.get_u16_le();
171    let version = SubprotocolVersion::new(buf.get_u8(), buf.get_u8());
172    let min_compat = SubprotocolVersion::new(buf.get_u8(), buf.get_u8());
173    if min_compat > version {
174        return None;
175    }
176    Some((id, version, min_compat))
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_version_ordering() {
185        let v1_0 = SubprotocolVersion::new(1, 0);
186        let v1_1 = SubprotocolVersion::new(1, 1);
187        let v2_0 = SubprotocolVersion::new(2, 0);
188
189        assert!(v1_0 < v1_1);
190        assert!(v1_1 < v2_0);
191        assert!(v1_0 < v2_0);
192    }
193
194    #[test]
195    fn test_version_satisfies() {
196        let v1_0 = SubprotocolVersion::new(1, 0);
197        let v1_1 = SubprotocolVersion::new(1, 1);
198        let v2_0 = SubprotocolVersion::new(2, 0);
199
200        assert!(v1_1.satisfies(v1_0)); // 1.1 >= 1.0
201        assert!(v2_0.satisfies(v1_0)); // 2.0 >= 1.0
202        assert!(!v1_0.satisfies(v1_1)); // 1.0 < 1.1
203    }
204
205    #[test]
206    fn test_descriptor_compatibility() {
207        let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 1))
208            .with_min_compatible(SubprotocolVersion::new(1, 0));
209        let b = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
210
211        assert!(a.is_compatible_with(&b)); // a(1.1) >= b.min(1.0) AND b(1.0) >= a.min(1.0)
212    }
213
214    #[test]
215    fn test_descriptor_incompatible() {
216        let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(2, 0))
217            .with_min_compatible(SubprotocolVersion::new(2, 0));
218        let b = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
219
220        assert!(!a.is_compatible_with(&b)); // b(1.0) < a.min(2.0)
221    }
222
223    #[test]
224    fn test_descriptor_different_id() {
225        let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
226        let b = SubprotocolDescriptor::new(0x0500, "migration", SubprotocolVersion::new(1, 0));
227
228        assert!(!a.is_compatible_with(&b));
229    }
230
231    #[test]
232    fn test_capability_tag() {
233        let d = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
234        assert_eq!(d.capability_tag(), "subprotocol:0x0400");
235    }
236
237    #[test]
238    fn test_manifest_entry_roundtrip() {
239        let desc = SubprotocolDescriptor::new(0x1234, "test", SubprotocolVersion::new(3, 7))
240            .with_min_compatible(SubprotocolVersion::new(2, 0));
241
242        let mut buf = Vec::new();
243        write_manifest_entry(&desc, &mut buf);
244        assert_eq!(buf.len(), MANIFEST_ENTRY_SIZE);
245
246        let mut cursor = &buf[..];
247        let (id, version, min_compat) = read_manifest_entry(&mut cursor).unwrap();
248        assert_eq!(id, 0x1234);
249        assert_eq!(version, SubprotocolVersion::new(3, 7));
250        assert_eq!(min_compat, SubprotocolVersion::new(2, 0));
251    }
252
253    #[test]
254    fn test_version_display() {
255        assert_eq!(format!("{}", SubprotocolVersion::new(1, 2)), "1.2");
256    }
257
258    #[test]
259    fn test_descriptor_display() {
260        let d = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
261        assert_eq!(format!("{}", d), "causal(0x0400) v1.0");
262    }
263
264    // ========================================================================
265    // read_manifest_entry / with_min_compatible must reject
266    // min_compatible > version (phantom-incompatibility DoS)
267    // ========================================================================
268
269    /// A manifest entry advertising `version=1.0, min_compat=255.255`
270    /// is rejected by `read_manifest_entry`. Pre-fix, every honest
271    /// peer would mark the subprotocol `incompatible` (because
272    /// `local.version.satisfies(remote.min_compat)` fails for any
273    /// local), letting an attacker unilaterally evict subprotocols.
274    #[test]
275    fn read_manifest_entry_rejects_min_compatible_above_version() {
276        // Hand-craft a manifest entry where min_compat > version.
277        // Wire layout: id(2) | version(2) | min_compat(2)
278        let mut buf = Vec::new();
279        buf.extend_from_slice(&0x1234u16.to_le_bytes()); // id
280        buf.extend_from_slice(&[1, 0]); // version 1.0
281        buf.extend_from_slice(&[255, 255]); // min_compat 255.255
282
283        let mut cursor = &buf[..];
284        let parsed = read_manifest_entry(&mut cursor);
285        assert!(
286            parsed.is_none(),
287            "read_manifest_entry must reject min_compat > version",
288        );
289    }
290
291    /// `min_compatible == version` is accepted (it's the default
292    /// produced by `SubprotocolDescriptor::new`). Pins the
293    /// inclusive boundary so a future tightening that flips the
294    /// `>` to `>=` doesn't reject legitimate descriptors that
295    /// haven't bumped past their floor yet.
296    #[test]
297    fn read_manifest_entry_accepts_min_compatible_equal_to_version() {
298        let mut buf = Vec::new();
299        buf.extend_from_slice(&0x4242u16.to_le_bytes());
300        buf.extend_from_slice(&[3, 7]);
301        buf.extend_from_slice(&[3, 7]);
302
303        let mut cursor = &buf[..];
304        let (id, version, min_compat) =
305            read_manifest_entry(&mut cursor).expect("equal min_compat must be accepted");
306        assert_eq!(id, 0x4242);
307        assert_eq!(version, SubprotocolVersion::new(3, 7));
308        assert_eq!(min_compat, SubprotocolVersion::new(3, 7));
309    }
310
311    /// `with_min_compatible` clamps to `self.version` instead of
312    /// allowing a higher floor than the descriptor's own version.
313    /// Without the clamp, a local builder could produce a
314    /// descriptor that violates `is_compatible_with`'s wire-format
315    /// contract (no peer at any version could satisfy
316    /// `min > version`).
317    #[test]
318    fn with_min_compatible_clamps_to_version() {
319        let desc = SubprotocolDescriptor::new(0x1000, "x", SubprotocolVersion::new(1, 0))
320            .with_min_compatible(SubprotocolVersion::new(2, 5));
321        assert_eq!(
322            desc.min_compatible,
323            SubprotocolVersion::new(1, 0),
324            "with_min_compatible must clamp to self.version",
325        );
326    }
327
328    /// A legitimate downward-floor `min_compat <= version` is
329    /// preserved by the clamp.
330    #[test]
331    fn with_min_compatible_preserves_lower_floor() {
332        let desc = SubprotocolDescriptor::new(0x1000, "x", SubprotocolVersion::new(2, 5))
333            .with_min_compatible(SubprotocolVersion::new(1, 0));
334        assert_eq!(desc.min_compatible, SubprotocolVersion::new(1, 0));
335    }
336}