Skip to main content

scp2p_core/
wire.rs

1// Copyright (c) 2024-2026 Vanyo Vanev / Tech Art Ltd
2// SPDX-License-Identifier: MPL-2.0
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7use serde::{Deserialize, Serialize};
8use std::convert::TryFrom;
9
10use crate::{manifest::PublicShareSummary, peer::PeerAddr};
11
12// ── Integer-keyed CBOR helpers ──────────────────────────────────────────
13//
14// High-frequency wire payloads are encoded as CBOR maps with integer keys
15// (rather than string field names) to reduce bandwidth.
16
17mod int_cbor {
18    use ciborium::Value;
19
20    /// Extract a `Vec<(Value, Value)>` map from a ciborium `Value`.
21    pub fn into_map(val: Value) -> Result<Vec<(Value, Value)>, String> {
22        match val {
23            Value::Map(m) => Ok(m),
24            other => Err(format!("expected CBOR map, got {:?}", other)),
25        }
26    }
27
28    /// Find a field in a CBOR map by integer key.
29    ///
30    /// The `str_key` parameter is unused for matching but kept as a
31    /// label for error messages in callers.
32    pub fn find_field<'a>(
33        map: &'a [(Value, Value)],
34        int_key: i64,
35        _str_key: &str,
36    ) -> Option<&'a Value> {
37        map.iter()
38            .find(|(k, _)| {
39                k.as_integer()
40                    .map(|i| i128::from(i) == int_key as i128)
41                    .unwrap_or(false)
42            })
43            .map(|(_, v)| v)
44    }
45
46    /// Extract a required byte-array field of exactly `N` bytes.
47    pub fn extract_byte_array<const N: usize>(
48        map: &[(Value, Value)],
49        int_key: i64,
50        str_key: &str,
51    ) -> Result<[u8; N], String> {
52        let val =
53            find_field(map, int_key, str_key).ok_or_else(|| format!("missing field {str_key}"))?;
54        let bytes = val
55            .as_bytes()
56            .ok_or_else(|| format!("field {str_key}: expected bytes"))?;
57        if bytes.len() != N {
58            return Err(format!(
59                "field {str_key}: expected {N} bytes, got {}",
60                bytes.len()
61            ));
62        }
63        let mut out = [0u8; N];
64        out.copy_from_slice(bytes);
65        Ok(out)
66    }
67
68    /// Extract a required byte buffer field.
69    pub fn extract_bytes(
70        map: &[(Value, Value)],
71        int_key: i64,
72        str_key: &str,
73    ) -> Result<Vec<u8>, String> {
74        let val =
75            find_field(map, int_key, str_key).ok_or_else(|| format!("missing field {str_key}"))?;
76        val.as_bytes()
77            .cloned()
78            .ok_or_else(|| format!("field {str_key}: expected bytes"))
79    }
80
81    /// Extract a required unsigned integer field.
82    pub fn extract_u64(map: &[(Value, Value)], int_key: i64, str_key: &str) -> Result<u64, String> {
83        let val =
84            find_field(map, int_key, str_key).ok_or_else(|| format!("missing field {str_key}"))?;
85        match val.as_integer() {
86            Some(i) => {
87                let n: i128 = i.into();
88                u64::try_from(n).map_err(|_| format!("field {str_key}: integer out of u64 range"))
89            }
90            None => Err(format!("field {str_key}: expected integer")),
91        }
92    }
93
94    /// Extract a required u32 field.
95    pub fn extract_u32(map: &[(Value, Value)], int_key: i64, str_key: &str) -> Result<u32, String> {
96        extract_u64(map, int_key, str_key)?
97            .try_into()
98            .map_err(|_| format!("field {str_key}: integer out of u32 range"))
99    }
100
101    /// Encode a byte array as `(integer_key, Value::Bytes)` pair.
102    pub fn kv_bytes(key: i64, bytes: &[u8]) -> (Value, Value) {
103        (Value::Integer(key.into()), Value::Bytes(bytes.to_vec()))
104    }
105
106    /// Encode a u64 as `(integer_key, Value::Integer)` pair.
107    pub fn kv_u64(key: i64, n: u64) -> (Value, Value) {
108        (
109            Value::Integer(key.into()),
110            Value::Integer((n as i64).into()),
111        )
112    }
113
114    /// Encode a u32 as `(integer_key, Value::Integer)` pair.
115    pub fn kv_u32(key: i64, n: u32) -> (Value, Value) {
116        kv_u64(key, n as u64)
117    }
118
119    /// Encode a serde-serializable value as `(integer_key, Value)` pair.
120    pub fn kv_serde<T: serde::Serialize>(key: i64, val: &T) -> Result<(Value, Value), String> {
121        // Serialize to CBOR bytes, then parse back as Value.
122        let cbor_bytes = crate::cbor::to_vec(val).map_err(|e| format!("serialize nested: {e}"))?;
123        let v: Value =
124            crate::cbor::from_slice(&cbor_bytes).map_err(|e| format!("parse nested value: {e}"))?;
125        Ok((Value::Integer(key.into()), v))
126    }
127
128    /// Deserialize a nested serde type from a CBOR Value.
129    pub fn deser_value<T: serde::de::DeserializeOwned>(
130        map: &[(Value, Value)],
131        int_key: i64,
132        str_key: &str,
133    ) -> Result<T, String> {
134        let val =
135            find_field(map, int_key, str_key).ok_or_else(|| format!("missing field {str_key}"))?;
136        // Round-trip through CBOR bytes.
137        let cbor_bytes = crate::cbor::to_vec(val)
138            .map_err(|e| format!("field {str_key}: serialize for deser: {e}"))?;
139        crate::cbor::from_slice(&cbor_bytes)
140            .map_err(|e| format!("field {str_key}: deserialize: {e}"))
141    }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct Envelope {
146    pub r#type: u16,
147    pub req_id: u32,
148    pub flags: u16,
149    #[serde(with = "serde_bytes")]
150    pub payload: Vec<u8>,
151}
152
153pub const FLAG_RESPONSE: u16 = 0x0001;
154pub const FLAG_ERROR: u16 = 0x0002;
155
156/// Default upper bound for serialized envelope size accepted from the wire.
157pub const MAX_ENVELOPE_BYTES: usize = 2 * 1024 * 1024;
158/// Default upper bound for decoded payload bytes accepted from the wire.
159pub const MAX_ENVELOPE_PAYLOAD_BYTES: usize = 1024 * 1024;
160
161impl Envelope {
162    pub fn encode(&self) -> anyhow::Result<Vec<u8>> {
163        Ok(crate::cbor::to_vec(self)?)
164    }
165
166    pub fn decode(bytes: &[u8]) -> anyhow::Result<Self> {
167        Self::decode_with_limits(bytes, MAX_ENVELOPE_BYTES, MAX_ENVELOPE_PAYLOAD_BYTES)
168    }
169
170    pub fn decode_with_limits(
171        bytes: &[u8],
172        max_envelope_bytes: usize,
173        max_payload_bytes: usize,
174    ) -> anyhow::Result<Self> {
175        if bytes.len() > max_envelope_bytes {
176            anyhow::bail!(
177                "envelope exceeds max size: {} > {}",
178                bytes.len(),
179                max_envelope_bytes
180            );
181        }
182
183        let envelope: Self = crate::cbor::from_slice(bytes)?;
184        if envelope.payload.len() > max_payload_bytes {
185            anyhow::bail!(
186                "envelope payload exceeds max size: {} > {}",
187                envelope.payload.len(),
188                max_payload_bytes
189            );
190        }
191        Ok(envelope)
192    }
193
194    /// Decode the envelope payload into a typed protocol message.
195    pub fn decode_typed(&self) -> anyhow::Result<WirePayload> {
196        WirePayload::decode(self.r#type, &self.payload)
197    }
198
199    /// Build an envelope from a typed protocol payload.
200    pub fn from_typed(req_id: u32, flags: u16, payload: &WirePayload) -> anyhow::Result<Self> {
201        Ok(Self {
202            r#type: u16::from(payload.msg_type()),
203            req_id,
204            flags,
205            payload: payload.encode()?,
206        })
207    }
208}
209
210#[repr(u16)]
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212pub enum MsgType {
213    /// Peer exchange with known-good peer addresses.
214    PexOffer = 100,
215    /// Request peer exchange records from a remote peer.
216    PexRequest = 101,
217    /// Kademlia FIND_NODE query.
218    FindNode = 200,
219    /// Kademlia FIND_VALUE query.
220    FindValue = 201,
221    /// Kademlia STORE request.
222    Store = 202,
223    /// Subscription manifest request.
224    GetManifest = 400,
225    /// Serialized manifest response.
226    ManifestData = 401,
227    /// Request public-share summaries from a reachable peer.
228    ListPublicShares = 402,
229    /// Public-share listing response.
230    PublicShareList = 403,
231    /// Ask a reachable peer whether it is joined to a specific community.
232    GetCommunityStatus = 404,
233    /// Response for a specific community membership probe.
234    CommunityStatus = 405,
235    /// Request public-share summaries for a specific joined community.
236    ListCommunityPublicShares = 406,
237    /// Community-scoped public-share listing response.
238    CommunityPublicShareList = 407,
239    /// Relay registration request.
240    RelayRegister = 450,
241    /// Relay registration acknowledgement.
242    RelayRegistered = 451,
243    /// Relay connection request.
244    RelayConnect = 452,
245    /// Relay stream frame.
246    RelayStream = 453,
247    /// Provider hint response for a content object.
248    Providers = 498,
249    /// Provider hint advertisement for a content object.
250    HaveContent = 499,
251    /// Chunk request.
252    GetChunk = 500,
253    /// Chunk payload response.
254    ChunkData = 501,
255    /// Request chunk hash list for a content object.
256    GetChunkHashes = 502,
257    /// Chunk hash list response.
258    ChunkHashList = 503,
259    /// Relay-PEX: request known relays.
260    RelayListRequest = 460,
261    /// Relay-PEX: response with known relay announcements.
262    RelayListResponse = 461,
263}
264
265impl MsgType {
266    /// Stable `u16` registry for protocol envelope types.
267    pub const ALL: [Self; 25] = [
268        Self::PexOffer,
269        Self::PexRequest,
270        Self::FindNode,
271        Self::FindValue,
272        Self::Store,
273        Self::GetManifest,
274        Self::ManifestData,
275        Self::ListPublicShares,
276        Self::PublicShareList,
277        Self::GetCommunityStatus,
278        Self::CommunityStatus,
279        Self::ListCommunityPublicShares,
280        Self::CommunityPublicShareList,
281        Self::RelayRegister,
282        Self::RelayRegistered,
283        Self::RelayConnect,
284        Self::RelayStream,
285        Self::RelayListRequest,
286        Self::RelayListResponse,
287        Self::Providers,
288        Self::HaveContent,
289        Self::GetChunk,
290        Self::ChunkData,
291        Self::GetChunkHashes,
292        Self::ChunkHashList,
293    ];
294}
295
296impl From<MsgType> for u16 {
297    fn from(value: MsgType) -> Self {
298        value as u16
299    }
300}
301
302impl TryFrom<u16> for MsgType {
303    type Error = anyhow::Error;
304
305    fn try_from(value: u16) -> Result<Self, Self::Error> {
306        match value {
307            100 => Ok(Self::PexOffer),
308            101 => Ok(Self::PexRequest),
309            200 => Ok(Self::FindNode),
310            201 => Ok(Self::FindValue),
311            202 => Ok(Self::Store),
312            400 => Ok(Self::GetManifest),
313            401 => Ok(Self::ManifestData),
314            402 => Ok(Self::ListPublicShares),
315            403 => Ok(Self::PublicShareList),
316            404 => Ok(Self::GetCommunityStatus),
317            405 => Ok(Self::CommunityStatus),
318            406 => Ok(Self::ListCommunityPublicShares),
319            407 => Ok(Self::CommunityPublicShareList),
320            450 => Ok(Self::RelayRegister),
321            451 => Ok(Self::RelayRegistered),
322            452 => Ok(Self::RelayConnect),
323            453 => Ok(Self::RelayStream),
324            460 => Ok(Self::RelayListRequest),
325            461 => Ok(Self::RelayListResponse),
326            498 => Ok(Self::Providers),
327            499 => Ok(Self::HaveContent),
328            500 => Ok(Self::GetChunk),
329            501 => Ok(Self::ChunkData),
330            502 => Ok(Self::GetChunkHashes),
331            503 => Ok(Self::ChunkHashList),
332            _ => anyhow::bail!("unknown message type {value}"),
333        }
334    }
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
338pub struct PexOffer {
339    pub peers: Vec<PeerAddr>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
343pub struct PexRequest {
344    pub max_peers: u8,
345}
346
347/// Kademlia FIND_NODE query.  Wire format: integer-keyed CBOR map `{0: bytes}`.
348#[derive(Debug, Clone, PartialEq, Eq)]
349pub struct FindNode {
350    pub target_node_id: [u8; 20],
351}
352
353impl Serialize for FindNode {
354    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355        ciborium::Value::Map(vec![int_cbor::kv_bytes(0, &self.target_node_id)])
356            .serialize(serializer)
357    }
358}
359
360impl<'de> Deserialize<'de> for FindNode {
361    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
362        let val = ciborium::Value::deserialize(deserializer)?;
363        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
364        Ok(FindNode {
365            target_node_id: int_cbor::extract_byte_array(&map, 0, "target_node_id")
366                .map_err(serde::de::Error::custom)?,
367        })
368    }
369}
370
371/// Kademlia FIND_NODE result.  Wire format: integer-keyed CBOR map `{0: array}`.
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct FindNodeResult {
374    pub peers: Vec<PeerAddr>,
375}
376
377impl Serialize for FindNodeResult {
378    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
379        ciborium::Value::Map(vec![
380            int_cbor::kv_serde(0, &self.peers).map_err(serde::ser::Error::custom)?,
381        ])
382        .serialize(serializer)
383    }
384}
385
386impl<'de> Deserialize<'de> for FindNodeResult {
387    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
388        let val = ciborium::Value::deserialize(deserializer)?;
389        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
390        Ok(FindNodeResult {
391            peers: int_cbor::deser_value(&map, 0, "peers").map_err(serde::de::Error::custom)?,
392        })
393    }
394}
395
396/// Kademlia FIND_VALUE query.  Wire format: integer-keyed CBOR map `{0: bytes}`.
397#[derive(Debug, Clone, PartialEq, Eq)]
398pub struct FindValue {
399    pub key: [u8; 32],
400}
401
402impl Serialize for FindValue {
403    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
404        ciborium::Value::Map(vec![int_cbor::kv_bytes(0, &self.key)]).serialize(serializer)
405    }
406}
407
408impl<'de> Deserialize<'de> for FindValue {
409    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
410        let val = ciborium::Value::deserialize(deserializer)?;
411        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
412        Ok(FindValue {
413            key: int_cbor::extract_byte_array(&map, 0, "key").map_err(serde::de::Error::custom)?,
414        })
415    }
416}
417
418/// Kademlia STORE request.  Wire format: `{0: key, 1: value, 2: ttl_secs}`.
419#[derive(Debug, Clone, PartialEq, Eq)]
420pub struct Store {
421    pub key: [u8; 32],
422    pub value: Vec<u8>,
423    pub ttl_secs: u64,
424}
425
426impl Serialize for Store {
427    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
428        ciborium::Value::Map(vec![
429            int_cbor::kv_bytes(0, &self.key),
430            int_cbor::kv_bytes(1, &self.value),
431            int_cbor::kv_u64(2, self.ttl_secs),
432        ])
433        .serialize(serializer)
434    }
435}
436
437impl<'de> Deserialize<'de> for Store {
438    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
439        let val = ciborium::Value::deserialize(deserializer)?;
440        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
441        Ok(Store {
442            key: int_cbor::extract_byte_array(&map, 0, "key").map_err(serde::de::Error::custom)?,
443            value: int_cbor::extract_bytes(&map, 1, "value").map_err(serde::de::Error::custom)?,
444            ttl_secs: int_cbor::extract_u64(&map, 2, "ttl_secs")
445                .map_err(serde::de::Error::custom)?,
446        })
447    }
448}
449
450/// FIND_VALUE result.  Wire format: `{0: value_or_null, 1: closer_peers}`.
451#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct FindValueResult {
453    pub value: Option<Store>,
454    pub closer_peers: Vec<PeerAddr>,
455}
456
457impl Serialize for FindValueResult {
458    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
459        let val_entry = match &self.value {
460            Some(s) => int_cbor::kv_serde(0, s).map_err(serde::ser::Error::custom)?,
461            None => (ciborium::Value::Integer(0.into()), ciborium::Value::Null),
462        };
463        ciborium::Value::Map(vec![
464            val_entry,
465            int_cbor::kv_serde(1, &self.closer_peers).map_err(serde::ser::Error::custom)?,
466        ])
467        .serialize(serializer)
468    }
469}
470
471impl<'de> Deserialize<'de> for FindValueResult {
472    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
473        let val = ciborium::Value::deserialize(deserializer)?;
474        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
475        let value_field = int_cbor::find_field(&map, 0, "value");
476        let value = match value_field {
477            Some(ciborium::Value::Null) | None => None,
478            Some(_) => {
479                Some(int_cbor::deser_value(&map, 0, "value").map_err(serde::de::Error::custom)?)
480            }
481        };
482        Ok(FindValueResult {
483            value,
484            closer_peers: int_cbor::deser_value(&map, 1, "closer_peers")
485                .map_err(serde::de::Error::custom)?,
486        })
487    }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
491pub struct GetManifest {
492    pub manifest_id: [u8; 32],
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
496pub struct ManifestData {
497    pub manifest_id: [u8; 32],
498    #[serde(with = "serde_bytes")]
499    pub bytes: Vec<u8>,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
503pub struct ListPublicShares {
504    pub max_entries: u16,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
508pub struct PublicShareList {
509    pub shares: Vec<PublicShareSummary>,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
513pub struct GetCommunityStatus {
514    pub community_share_id: [u8; 32],
515    pub community_share_pubkey: [u8; 32],
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
519pub struct CommunityStatus {
520    pub community_share_id: [u8; 32],
521    pub joined: bool,
522    /// Serialized `CommunityMembershipToken` (CBOR bytes), if the node
523    /// holds a cryptographic proof of membership.  Absent means
524    /// membership is self-asserted (v0.1 default).
525    #[serde(default, with = "serde_bytes")]
526    pub membership_proof: Option<Vec<u8>>,
527    /// Human-readable community name, if the responding node knows it.
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub name: Option<String>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
533pub struct ListCommunityPublicShares {
534    pub community_share_id: [u8; 32],
535    pub community_share_pubkey: [u8; 32],
536    pub max_entries: u16,
537    /// Ed25519 public key of the requesting node's stable identity.
538    ///
539    /// Required when the serving node has `community_strict_mode` enabled.
540    /// May be omitted for permissive-mode servers.
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub requester_node_pubkey: Option<[u8; 32]>,
543    /// Serialized `CommunityMembershipToken` (CBOR bytes) proving the
544    /// requester is a member of the requested community.
545    ///
546    /// Required when the serving node has `community_strict_mode` enabled.
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    pub requester_membership_proof: Option<Vec<u8>>,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
552pub struct CommunityPublicShareList {
553    pub community_share_id: [u8; 32],
554    pub shares: Vec<PublicShareSummary>,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
558pub struct RelayRegister {
559    #[serde(default)]
560    pub relay_slot_id: Option<u64>,
561    /// When `true` the sender is a firewalled node that wants the relay
562    /// to keep this connection open and forward incoming requests from
563    /// other peers through it.  The relay transitions the connection to
564    /// relay-bridge mode after replying with `RelayRegistered`.
565    #[serde(default)]
566    pub tunnel: bool,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
570pub struct RelayRegistered {
571    pub relay_slot_id: u64,
572    pub expires_at: u64,
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
576pub struct RelayConnect {
577    pub relay_slot_id: u64,
578}
579
580#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
581#[serde(rename_all = "lowercase")]
582pub enum RelayPayloadKind {
583    #[default]
584    Control,
585    Content,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589pub struct RelayStream {
590    pub relay_slot_id: u64,
591    pub stream_id: u32,
592    #[serde(default)]
593    pub kind: RelayPayloadKind,
594    #[serde(with = "serde_bytes")]
595    pub payload: Vec<u8>,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
599pub struct HaveContent {
600    pub content_id: [u8; 32],
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604pub struct Providers {
605    pub content_id: [u8; 32],
606    pub providers: Vec<PeerAddr>,
607    pub updated_at: u64,
608}
609
610/// DHT value listing peer addresses of nodes that have joined a community.
611///
612/// Stored under `community_info_key(share_id)`.  When a remote peer
613/// receives a STORE for this key, it merges the incoming member list
614/// with the existing one so the relay accumulates all members.
615#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
616pub struct CommunityMembers {
617    pub community_share_id: [u8; 32],
618    pub members: Vec<PeerAddr>,
619    pub updated_at: u64,
620}
621
622/// Chunk request.  Wire format: `{0: content_id, 1: chunk_index}`.
623#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct GetChunk {
625    pub content_id: [u8; 32],
626    pub chunk_index: u32,
627}
628
629impl Serialize for GetChunk {
630    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
631        ciborium::Value::Map(vec![
632            int_cbor::kv_bytes(0, &self.content_id),
633            int_cbor::kv_u32(1, self.chunk_index),
634        ])
635        .serialize(serializer)
636    }
637}
638
639impl<'de> Deserialize<'de> for GetChunk {
640    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
641        let val = ciborium::Value::deserialize(deserializer)?;
642        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
643        Ok(GetChunk {
644            content_id: int_cbor::extract_byte_array(&map, 0, "content_id")
645                .map_err(serde::de::Error::custom)?,
646            chunk_index: int_cbor::extract_u32(&map, 1, "chunk_index")
647                .map_err(serde::de::Error::custom)?,
648        })
649    }
650}
651
652/// Chunk payload response.  Wire format: `{0: content_id, 1: chunk_index, 2: bytes}`.
653#[derive(Debug, Clone, PartialEq, Eq)]
654pub struct ChunkData {
655    pub content_id: [u8; 32],
656    pub chunk_index: u32,
657    pub bytes: Vec<u8>,
658}
659
660impl Serialize for ChunkData {
661    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
662        ciborium::Value::Map(vec![
663            int_cbor::kv_bytes(0, &self.content_id),
664            int_cbor::kv_u32(1, self.chunk_index),
665            int_cbor::kv_bytes(2, &self.bytes),
666        ])
667        .serialize(serializer)
668    }
669}
670
671impl<'de> Deserialize<'de> for ChunkData {
672    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
673        let val = ciborium::Value::deserialize(deserializer)?;
674        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
675        Ok(ChunkData {
676            content_id: int_cbor::extract_byte_array(&map, 0, "content_id")
677                .map_err(serde::de::Error::custom)?,
678            chunk_index: int_cbor::extract_u32(&map, 1, "chunk_index")
679                .map_err(serde::de::Error::custom)?,
680            bytes: int_cbor::extract_bytes(&map, 2, "bytes").map_err(serde::de::Error::custom)?,
681        })
682    }
683}
684
685/// Request the chunk hash list for a content object.  Wire format: `{0: content_id}`.
686#[derive(Debug, Clone, PartialEq, Eq)]
687pub struct GetChunkHashes {
688    pub content_id: [u8; 32],
689}
690
691impl Serialize for GetChunkHashes {
692    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
693        ciborium::Value::Map(vec![int_cbor::kv_bytes(0, &self.content_id)]).serialize(serializer)
694    }
695}
696
697impl<'de> Deserialize<'de> for GetChunkHashes {
698    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
699        let val = ciborium::Value::deserialize(deserializer)?;
700        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
701        Ok(GetChunkHashes {
702            content_id: int_cbor::extract_byte_array(&map, 0, "content_id")
703                .map_err(serde::de::Error::custom)?,
704        })
705    }
706}
707
708/// Chunk hash list response.  Wire format: `{0: content_id, 1: hashes}`.
709#[derive(Debug, Clone, PartialEq, Eq)]
710pub struct ChunkHashList {
711    pub content_id: [u8; 32],
712    pub hashes: Vec<[u8; 32]>,
713}
714
715impl Serialize for ChunkHashList {
716    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
717        ciborium::Value::Map(vec![
718            int_cbor::kv_bytes(0, &self.content_id),
719            int_cbor::kv_serde(1, &self.hashes).map_err(serde::ser::Error::custom)?,
720        ])
721        .serialize(serializer)
722    }
723}
724
725impl<'de> Deserialize<'de> for ChunkHashList {
726    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
727        let val = ciborium::Value::deserialize(deserializer)?;
728        let map = int_cbor::into_map(val).map_err(serde::de::Error::custom)?;
729        Ok(ChunkHashList {
730            content_id: int_cbor::extract_byte_array(&map, 0, "content_id")
731                .map_err(serde::de::Error::custom)?,
732            hashes: int_cbor::deser_value(&map, 1, "hashes").map_err(serde::de::Error::custom)?,
733        })
734    }
735}
736
737/// Relay-PEX: request known relay announcements from a peer.
738#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
739pub struct RelayListRequest {
740    /// Maximum number of relay announcements to return.
741    pub max_count: u16,
742}
743
744/// Relay-PEX: response with known relay announcements.
745///
746/// Each entry is a full signed `RelayAnnouncement` so recipients
747/// can validate independently.
748#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
749pub struct RelayListResponse {
750    pub announcements: Vec<crate::relay::RelayAnnouncement>,
751}
752
753/// Typed envelope payloads used by dispatcher-style message handling.
754#[derive(Debug, Clone, PartialEq, Eq)]
755pub enum WirePayload {
756    PexOffer(PexOffer),
757    PexRequest(PexRequest),
758    FindNode(FindNode),
759    FindValue(FindValue),
760    Store(Store),
761    GetManifest(GetManifest),
762    ManifestData(ManifestData),
763    ListPublicShares(ListPublicShares),
764    PublicShareList(PublicShareList),
765    GetCommunityStatus(GetCommunityStatus),
766    CommunityStatus(CommunityStatus),
767    ListCommunityPublicShares(ListCommunityPublicShares),
768    CommunityPublicShareList(CommunityPublicShareList),
769    RelayRegister(RelayRegister),
770    RelayRegistered(RelayRegistered),
771    RelayConnect(RelayConnect),
772    RelayStream(RelayStream),
773    RelayListRequest(RelayListRequest),
774    RelayListResponse(RelayListResponse),
775    Providers(Providers),
776    HaveContent(HaveContent),
777    GetChunk(GetChunk),
778    ChunkData(ChunkData),
779    GetChunkHashes(GetChunkHashes),
780    ChunkHashList(ChunkHashList),
781}
782
783impl WirePayload {
784    pub fn msg_type(&self) -> MsgType {
785        match self {
786            Self::PexOffer(_) => MsgType::PexOffer,
787            Self::PexRequest(_) => MsgType::PexRequest,
788            Self::FindNode(_) => MsgType::FindNode,
789            Self::FindValue(_) => MsgType::FindValue,
790            Self::Store(_) => MsgType::Store,
791            Self::GetManifest(_) => MsgType::GetManifest,
792            Self::ManifestData(_) => MsgType::ManifestData,
793            Self::ListPublicShares(_) => MsgType::ListPublicShares,
794            Self::PublicShareList(_) => MsgType::PublicShareList,
795            Self::GetCommunityStatus(_) => MsgType::GetCommunityStatus,
796            Self::CommunityStatus(_) => MsgType::CommunityStatus,
797            Self::ListCommunityPublicShares(_) => MsgType::ListCommunityPublicShares,
798            Self::CommunityPublicShareList(_) => MsgType::CommunityPublicShareList,
799            Self::RelayRegister(_) => MsgType::RelayRegister,
800            Self::RelayRegistered(_) => MsgType::RelayRegistered,
801            Self::RelayConnect(_) => MsgType::RelayConnect,
802            Self::RelayStream(_) => MsgType::RelayStream,
803            Self::RelayListRequest(_) => MsgType::RelayListRequest,
804            Self::RelayListResponse(_) => MsgType::RelayListResponse,
805            Self::Providers(_) => MsgType::Providers,
806            Self::HaveContent(_) => MsgType::HaveContent,
807            Self::GetChunk(_) => MsgType::GetChunk,
808            Self::ChunkData(_) => MsgType::ChunkData,
809            Self::GetChunkHashes(_) => MsgType::GetChunkHashes,
810            Self::ChunkHashList(_) => MsgType::ChunkHashList,
811        }
812    }
813
814    pub fn encode(&self) -> anyhow::Result<Vec<u8>> {
815        Ok(match self {
816            Self::PexOffer(msg) => crate::cbor::to_vec(msg)?,
817            Self::PexRequest(msg) => crate::cbor::to_vec(msg)?,
818            Self::FindNode(msg) => crate::cbor::to_vec(msg)?,
819            Self::FindValue(msg) => crate::cbor::to_vec(msg)?,
820            Self::Store(msg) => crate::cbor::to_vec(msg)?,
821            Self::GetManifest(msg) => crate::cbor::to_vec(msg)?,
822            Self::ManifestData(msg) => crate::cbor::to_vec(msg)?,
823            Self::ListPublicShares(msg) => crate::cbor::to_vec(msg)?,
824            Self::PublicShareList(msg) => crate::cbor::to_vec(msg)?,
825            Self::GetCommunityStatus(msg) => crate::cbor::to_vec(msg)?,
826            Self::CommunityStatus(msg) => crate::cbor::to_vec(msg)?,
827            Self::ListCommunityPublicShares(msg) => crate::cbor::to_vec(msg)?,
828            Self::CommunityPublicShareList(msg) => crate::cbor::to_vec(msg)?,
829            Self::RelayRegister(msg) => crate::cbor::to_vec(msg)?,
830            Self::RelayRegistered(msg) => crate::cbor::to_vec(msg)?,
831            Self::RelayConnect(msg) => crate::cbor::to_vec(msg)?,
832            Self::RelayStream(msg) => crate::cbor::to_vec(msg)?,
833            Self::RelayListRequest(msg) => crate::cbor::to_vec(msg)?,
834            Self::RelayListResponse(msg) => crate::cbor::to_vec(msg)?,
835            Self::Providers(msg) => crate::cbor::to_vec(msg)?,
836            Self::HaveContent(msg) => crate::cbor::to_vec(msg)?,
837            Self::GetChunk(msg) => crate::cbor::to_vec(msg)?,
838            Self::ChunkData(msg) => crate::cbor::to_vec(msg)?,
839            Self::GetChunkHashes(msg) => crate::cbor::to_vec(msg)?,
840            Self::ChunkHashList(msg) => crate::cbor::to_vec(msg)?,
841        })
842    }
843
844    pub fn decode(message_type: u16, payload: &[u8]) -> anyhow::Result<Self> {
845        let msg_type = MsgType::try_from(message_type)?;
846        Ok(match msg_type {
847            MsgType::PexOffer => Self::PexOffer(crate::cbor::from_slice(payload)?),
848            MsgType::PexRequest => Self::PexRequest(crate::cbor::from_slice(payload)?),
849            MsgType::FindNode => Self::FindNode(crate::cbor::from_slice(payload)?),
850            MsgType::FindValue => Self::FindValue(crate::cbor::from_slice(payload)?),
851            MsgType::Store => Self::Store(crate::cbor::from_slice(payload)?),
852            MsgType::GetManifest => Self::GetManifest(crate::cbor::from_slice(payload)?),
853            MsgType::ManifestData => Self::ManifestData(crate::cbor::from_slice(payload)?),
854            MsgType::ListPublicShares => Self::ListPublicShares(crate::cbor::from_slice(payload)?),
855            MsgType::PublicShareList => Self::PublicShareList(crate::cbor::from_slice(payload)?),
856            MsgType::GetCommunityStatus => {
857                Self::GetCommunityStatus(crate::cbor::from_slice(payload)?)
858            }
859            MsgType::CommunityStatus => Self::CommunityStatus(crate::cbor::from_slice(payload)?),
860            MsgType::ListCommunityPublicShares => {
861                Self::ListCommunityPublicShares(crate::cbor::from_slice(payload)?)
862            }
863            MsgType::CommunityPublicShareList => {
864                Self::CommunityPublicShareList(crate::cbor::from_slice(payload)?)
865            }
866            MsgType::RelayRegister => Self::RelayRegister(crate::cbor::from_slice(payload)?),
867            MsgType::RelayRegistered => Self::RelayRegistered(crate::cbor::from_slice(payload)?),
868            MsgType::RelayConnect => Self::RelayConnect(crate::cbor::from_slice(payload)?),
869            MsgType::RelayStream => Self::RelayStream(crate::cbor::from_slice(payload)?),
870            MsgType::RelayListRequest => Self::RelayListRequest(crate::cbor::from_slice(payload)?),
871            MsgType::RelayListResponse => {
872                Self::RelayListResponse(crate::cbor::from_slice(payload)?)
873            }
874            MsgType::Providers => Self::Providers(crate::cbor::from_slice(payload)?),
875            MsgType::HaveContent => Self::HaveContent(crate::cbor::from_slice(payload)?),
876            MsgType::GetChunk => Self::GetChunk(crate::cbor::from_slice(payload)?),
877            MsgType::ChunkData => Self::ChunkData(crate::cbor::from_slice(payload)?),
878            MsgType::GetChunkHashes => Self::GetChunkHashes(crate::cbor::from_slice(payload)?),
879            MsgType::ChunkHashList => Self::ChunkHashList(crate::cbor::from_slice(payload)?),
880        })
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use crate::{ids::NodeId, peer::TransportProtocol};
888
889    // ── Integer-keyed CBOR encoding verification tests ──────────────
890
891    /// Verify that FindNode serializes with integer key 0 (not string "target_node_id").
892    #[test]
893    fn int_cbor_find_node_uses_integer_keys() {
894        let msg = FindNode {
895            target_node_id: [0xAA; 20],
896        };
897        let bytes = crate::cbor::to_vec(&msg).expect("encode");
898        let val: ciborium::Value = crate::cbor::from_slice(&bytes).expect("parse value");
899        let map = val.as_map().expect("should be map");
900        // Key should be integer 0, not string
901        assert!(
902            map[0].0.as_integer().is_some(),
903            "FindNode key should be integer, got {:?}",
904            map[0].0
905        );
906        assert_eq!(i128::from(map[0].0.as_integer().unwrap()), 0);
907    }
908
909    /// Verify that Store serializes with integer keys 0, 1, 2.
910    #[test]
911    fn int_cbor_store_uses_integer_keys() {
912        let msg = Store {
913            key: [0xBB; 32],
914            value: vec![1, 2, 3],
915            ttl_secs: 300,
916        };
917        let bytes = crate::cbor::to_vec(&msg).expect("encode");
918        let val: ciborium::Value = crate::cbor::from_slice(&bytes).expect("parse value");
919        let map = val.as_map().expect("should be map");
920        assert_eq!(map.len(), 3);
921        for (i, (k, _)) in map.iter().enumerate() {
922            let int_key = k.as_integer().expect("key should be integer");
923            assert_eq!(i128::from(int_key), i as i128);
924        }
925    }
926
927    /// Verify GetChunk and ChunkData use integer keys and roundtrip correctly.
928    #[test]
929    fn int_cbor_chunk_messages_integer_keys_and_roundtrip() {
930        let get = GetChunk {
931            content_id: [0xEE; 32],
932            chunk_index: 42,
933        };
934        let bytes = crate::cbor::to_vec(&get).expect("encode");
935        let val: ciborium::Value = crate::cbor::from_slice(&bytes).expect("parse");
936        let map = val.as_map().expect("should be map");
937        assert!(map[0].0.as_integer().is_some());
938        assert!(map[1].0.as_integer().is_some());
939        let rt: GetChunk = crate::cbor::from_slice(&bytes).expect("roundtrip");
940        assert_eq!(rt, get);
941
942        let data = ChunkData {
943            content_id: [0xFF; 32],
944            chunk_index: 7,
945            bytes: vec![10, 20, 30],
946        };
947        let data_bytes = crate::cbor::to_vec(&data).expect("encode");
948        let data_val: ciborium::Value = crate::cbor::from_slice(&data_bytes).expect("parse");
949        let data_map = data_val.as_map().expect("should be map");
950        assert_eq!(data_map.len(), 3);
951        for (k, _) in data_map {
952            assert!(k.as_integer().is_some(), "all keys should be integers");
953        }
954        let data_rt: ChunkData = crate::cbor::from_slice(&data_bytes).expect("roundtrip");
955        assert_eq!(data_rt, data);
956    }
957
958    #[test]
959    fn envelope_roundtrip() {
960        let payload = PexRequest { max_peers: 32 };
961        let envelope = Envelope {
962            r#type: MsgType::PexRequest as u16,
963            req_id: 7,
964            flags: 0,
965            payload: crate::cbor::to_vec(&payload).expect("encode payload"),
966        };
967
968        let encoded = envelope.encode().expect("encode envelope");
969        let decoded = Envelope::decode(&encoded).expect("decode envelope");
970        let decoded_payload: PexRequest =
971            crate::cbor::from_slice(&decoded.payload).expect("decode payload");
972
973        assert_eq!(decoded.r#type, MsgType::PexRequest as u16);
974        assert_eq!(decoded_payload, payload);
975    }
976
977    #[test]
978    fn pex_offer_roundtrip() {
979        let offer = PexOffer {
980            peers: vec![PeerAddr {
981                ip: "10.0.0.5".parse().expect("valid ip"),
982                port: 7001,
983                transport: TransportProtocol::Tcp,
984                pubkey_hint: Some([1u8; 32]),
985                relay_via: None,
986            }],
987        };
988
989        let encoded = crate::cbor::to_vec(&offer).expect("encode pex offer");
990        let decoded: PexOffer = crate::cbor::from_slice(&encoded).expect("decode pex offer");
991        assert_eq!(decoded, offer);
992    }
993
994    #[test]
995    fn dht_messages_roundtrip() {
996        let find_node = FindNode {
997            target_node_id: NodeId([1u8; 20]).0,
998        };
999        let find_node_roundtrip: FindNode =
1000            crate::cbor::from_slice(&crate::cbor::to_vec(&find_node).expect("encode find node"))
1001                .expect("decode find node");
1002        assert_eq!(find_node_roundtrip, find_node);
1003
1004        let store = Store {
1005            key: [9u8; 32],
1006            value: vec![1, 2, 3],
1007            ttl_secs: 60,
1008        };
1009        let find_value_result = FindValueResult {
1010            value: Some(store),
1011            closer_peers: vec![PeerAddr {
1012                ip: "192.168.1.2".parse().expect("valid ip"),
1013                port: 7000,
1014                transport: TransportProtocol::Quic,
1015                pubkey_hint: None,
1016                relay_via: None,
1017            }],
1018        };
1019
1020        let encoded = crate::cbor::to_vec(&find_value_result).expect("encode find value result");
1021        let decoded: FindValueResult =
1022            crate::cbor::from_slice(&encoded).expect("decode find value result");
1023        assert_eq!(decoded, find_value_result);
1024    }
1025
1026    #[test]
1027    fn relay_messages_roundtrip() {
1028        let reg = RelayRegister {
1029            relay_slot_id: Some(42),
1030            tunnel: false,
1031        };
1032        let reg_rt: RelayRegister =
1033            crate::cbor::from_slice(&crate::cbor::to_vec(&reg).expect("encode relay register"))
1034                .expect("decode relay register");
1035        assert_eq!(reg_rt, reg);
1036
1037        let reg_empty: RelayRegister = crate::cbor::from_slice(
1038            &crate::cbor::to_vec(&crate::cbor::Value::Map(Vec::new()))
1039                .expect("encode legacy empty register map"),
1040        )
1041        .expect("decode legacy empty register map");
1042        assert_eq!(reg_empty.relay_slot_id, None);
1043
1044        let registered = RelayRegistered {
1045            relay_slot_id: 7,
1046            expires_at: 99,
1047        };
1048        let registered_rt: RelayRegistered = crate::cbor::from_slice(
1049            &crate::cbor::to_vec(&registered).expect("encode relay registered"),
1050        )
1051        .expect("decode relay registered");
1052        assert_eq!(registered_rt, registered);
1053
1054        let stream = RelayStream {
1055            relay_slot_id: 7,
1056            stream_id: 1,
1057            kind: RelayPayloadKind::Control,
1058            payload: vec![1, 2, 3],
1059        };
1060        let stream_rt: RelayStream =
1061            crate::cbor::from_slice(&crate::cbor::to_vec(&stream).expect("encode relay stream"))
1062                .expect("decode relay stream");
1063        assert_eq!(stream_rt, stream);
1064
1065        assert!(
1066            crate::cbor::from_slice::<RelayStream>(
1067                &crate::cbor::to_vec(&(7u64, 9u32, serde_bytes::ByteBuf::from(vec![8u8, 7])))
1068                    .expect("encode legacy stream tuple"),
1069            )
1070            .is_err(),
1071            "legacy tuple encoding should not decode as struct"
1072        );
1073    }
1074
1075    #[test]
1076    fn provider_messages_roundtrip() {
1077        let have = HaveContent {
1078            content_id: [4u8; 32],
1079        };
1080        let have_rt: HaveContent =
1081            crate::cbor::from_slice(&crate::cbor::to_vec(&have).expect("encode have"))
1082                .expect("decode have");
1083        assert_eq!(have_rt, have);
1084
1085        let prov = Providers {
1086            content_id: [4u8; 32],
1087            providers: vec![PeerAddr {
1088                ip: "10.1.0.2".parse().expect("valid ip"),
1089                port: 7777,
1090                transport: TransportProtocol::Quic,
1091                pubkey_hint: None,
1092                relay_via: None,
1093            }],
1094            updated_at: 123,
1095        };
1096        let prov_rt: Providers =
1097            crate::cbor::from_slice(&crate::cbor::to_vec(&prov).expect("encode providers"))
1098                .expect("decode providers");
1099        assert_eq!(prov_rt, prov);
1100    }
1101
1102    #[test]
1103    fn public_share_messages_roundtrip() {
1104        let request = ListPublicShares { max_entries: 25 };
1105        let request_rt: ListPublicShares = crate::cbor::from_slice(
1106            &crate::cbor::to_vec(&request).expect("encode public list req"),
1107        )
1108        .expect("decode public list req");
1109        assert_eq!(request_rt, request);
1110
1111        let response = PublicShareList {
1112            shares: vec![PublicShareSummary {
1113                share_id: [1u8; 32],
1114                share_pubkey: [2u8; 32],
1115                latest_seq: 7,
1116                latest_manifest_id: [3u8; 32],
1117                title: Some("Public Catalog".into()),
1118                description: Some("shared".into()),
1119            }],
1120        };
1121        let response_rt: PublicShareList = crate::cbor::from_slice(
1122            &crate::cbor::to_vec(&response).expect("encode public list response"),
1123        )
1124        .expect("decode public list response");
1125        assert_eq!(response_rt, response);
1126    }
1127
1128    #[test]
1129    fn community_status_messages_roundtrip() {
1130        let request = GetCommunityStatus {
1131            community_share_id: [4u8; 32],
1132            community_share_pubkey: [5u8; 32],
1133        };
1134        let request_rt: GetCommunityStatus =
1135            crate::cbor::from_slice(&crate::cbor::to_vec(&request).expect("encode"))
1136                .expect("decode");
1137        assert_eq!(request_rt, request);
1138
1139        let response = CommunityStatus {
1140            community_share_id: [4u8; 32],
1141            joined: true,
1142            membership_proof: None,
1143            name: None,
1144        };
1145        let response_rt: CommunityStatus =
1146            crate::cbor::from_slice(&crate::cbor::to_vec(&response).expect("encode"))
1147                .expect("decode");
1148        assert_eq!(response_rt, response);
1149
1150        // Verify name field survives roundtrip.
1151        let response_with_name = CommunityStatus {
1152            community_share_id: [4u8; 32],
1153            joined: true,
1154            membership_proof: None,
1155            name: Some("Pavlikeni".to_string()),
1156        };
1157        let rt2: CommunityStatus =
1158            crate::cbor::from_slice(&crate::cbor::to_vec(&response_with_name).expect("encode"))
1159                .expect("decode");
1160        assert_eq!(rt2.name, Some("Pavlikeni".to_string()));
1161    }
1162
1163    #[test]
1164    fn community_public_share_messages_roundtrip() {
1165        let request = ListCommunityPublicShares {
1166            community_share_id: [6u8; 32],
1167            community_share_pubkey: [7u8; 32],
1168            max_entries: 12,
1169            requester_node_pubkey: None,
1170            requester_membership_proof: None,
1171        };
1172        let request_rt: ListCommunityPublicShares =
1173            crate::cbor::from_slice(&crate::cbor::to_vec(&request).expect("encode"))
1174                .expect("decode");
1175        assert_eq!(request_rt, request);
1176
1177        let response = CommunityPublicShareList {
1178            community_share_id: [6u8; 32],
1179            shares: vec![PublicShareSummary {
1180                share_id: [8u8; 32],
1181                share_pubkey: [9u8; 32],
1182                latest_seq: 2,
1183                latest_manifest_id: [10u8; 32],
1184                title: Some("Community Public".into()),
1185                description: None,
1186            }],
1187        };
1188        let response_rt: CommunityPublicShareList =
1189            crate::cbor::from_slice(&crate::cbor::to_vec(&response).expect("encode"))
1190                .expect("decode");
1191        assert_eq!(response_rt, response);
1192    }
1193
1194    #[test]
1195    fn chunk_messages_roundtrip() {
1196        let get = GetChunk {
1197            content_id: [9u8; 32],
1198            chunk_index: 3,
1199        };
1200        let get_encoded = crate::cbor::to_vec(&get).expect("encode get chunk");
1201        let get_decoded: GetChunk =
1202            crate::cbor::from_slice(&get_encoded).expect("decode get chunk");
1203        assert_eq!(get_decoded, get);
1204
1205        let data = ChunkData {
1206            content_id: [9u8; 32],
1207            chunk_index: 3,
1208            bytes: vec![1, 2, 3],
1209        };
1210        let data_encoded = crate::cbor::to_vec(&data).expect("encode chunk data");
1211        let data_decoded: ChunkData =
1212            crate::cbor::from_slice(&data_encoded).expect("decode chunk data");
1213        assert_eq!(data_decoded, data);
1214    }
1215
1216    #[test]
1217    fn chunk_hash_messages_roundtrip() {
1218        let get = GetChunkHashes {
1219            content_id: [11u8; 32],
1220        };
1221        let get_rt: GetChunkHashes =
1222            crate::cbor::from_slice(&crate::cbor::to_vec(&get).expect("encode")).expect("decode");
1223        assert_eq!(get_rt, get);
1224
1225        let list = ChunkHashList {
1226            content_id: [11u8; 32],
1227            hashes: vec![[1u8; 32], [2u8; 32]],
1228        };
1229        let list_rt: ChunkHashList =
1230            crate::cbor::from_slice(&crate::cbor::to_vec(&list).expect("encode")).expect("decode");
1231        assert_eq!(list_rt, list);
1232    }
1233
1234    #[test]
1235    fn msg_type_registry_roundtrip_and_unique_values() {
1236        let mut sorted_values = MsgType::ALL
1237            .iter()
1238            .copied()
1239            .map(u16::from)
1240            .collect::<Vec<u16>>();
1241
1242        for msg_type in MsgType::ALL {
1243            let wire_value = u16::from(msg_type);
1244            let roundtrip = MsgType::try_from(wire_value).expect("registry roundtrip");
1245            assert_eq!(roundtrip, msg_type);
1246        }
1247
1248        let expected_len = sorted_values.len();
1249        sorted_values.sort_unstable();
1250        sorted_values.dedup();
1251        assert_eq!(sorted_values.len(), expected_len);
1252    }
1253
1254    #[test]
1255    fn envelope_decode_rejects_large_payload_limit() {
1256        let envelope = Envelope {
1257            r#type: MsgType::ChunkData as u16,
1258            req_id: 9,
1259            flags: 0,
1260            payload: vec![7u8; 32],
1261        };
1262        let encoded = envelope.encode().expect("encode envelope");
1263
1264        let err = Envelope::decode_with_limits(&encoded, 1024, 16)
1265            .expect_err("payload limit should reject envelope");
1266        assert!(err.to_string().contains("payload exceeds max size"));
1267    }
1268
1269    #[test]
1270    fn envelope_decode_rejects_large_serialized_limit() {
1271        let envelope = Envelope {
1272            r#type: MsgType::PexOffer as u16,
1273            req_id: 10,
1274            flags: 0,
1275            payload: vec![1u8; 8],
1276        };
1277        let encoded = envelope.encode().expect("encode envelope");
1278
1279        let err = Envelope::decode_with_limits(&encoded, 2, 1024)
1280            .expect_err("envelope bytes limit should reject envelope");
1281        assert!(err.to_string().contains("envelope exceeds max size"));
1282    }
1283
1284    #[test]
1285    fn typed_payload_dispatch_roundtrip_for_all_registered_types() {
1286        let cases = vec![
1287            WirePayload::PexOffer(PexOffer {
1288                peers: vec![PeerAddr {
1289                    ip: "10.0.0.2".parse().expect("valid ip"),
1290                    port: 1234,
1291                    transport: TransportProtocol::Tcp,
1292                    pubkey_hint: None,
1293                    relay_via: None,
1294                }],
1295            }),
1296            WirePayload::PexRequest(PexRequest { max_peers: 8 }),
1297            WirePayload::FindNode(FindNode {
1298                target_node_id: NodeId([1u8; 20]).0,
1299            }),
1300            WirePayload::FindValue(FindValue { key: [2u8; 32] }),
1301            WirePayload::Store(Store {
1302                key: [3u8; 32],
1303                value: vec![1, 2, 3],
1304                ttl_secs: 15,
1305            }),
1306            WirePayload::GetManifest(GetManifest {
1307                manifest_id: [4u8; 32],
1308            }),
1309            WirePayload::ManifestData(ManifestData {
1310                manifest_id: [5u8; 32],
1311                bytes: vec![9, 8, 7],
1312            }),
1313            WirePayload::ListPublicShares(ListPublicShares { max_entries: 5 }),
1314            WirePayload::PublicShareList(PublicShareList {
1315                shares: vec![PublicShareSummary {
1316                    share_id: [6u8; 32],
1317                    share_pubkey: [7u8; 32],
1318                    latest_seq: 8,
1319                    latest_manifest_id: [9u8; 32],
1320                    title: Some("pub".into()),
1321                    description: None,
1322                }],
1323            }),
1324            WirePayload::GetCommunityStatus(GetCommunityStatus {
1325                community_share_id: [14u8; 32],
1326                community_share_pubkey: [15u8; 32],
1327            }),
1328            WirePayload::CommunityStatus(CommunityStatus {
1329                community_share_id: [14u8; 32],
1330                joined: true,
1331                membership_proof: None,
1332                name: None,
1333            }),
1334            WirePayload::ListCommunityPublicShares(ListCommunityPublicShares {
1335                community_share_id: [16u8; 32],
1336                community_share_pubkey: [17u8; 32],
1337                max_entries: 4,
1338                requester_node_pubkey: None,
1339                requester_membership_proof: None,
1340            }),
1341            WirePayload::CommunityPublicShareList(CommunityPublicShareList {
1342                community_share_id: [16u8; 32],
1343                shares: vec![PublicShareSummary {
1344                    share_id: [18u8; 32],
1345                    share_pubkey: [19u8; 32],
1346                    latest_seq: 5,
1347                    latest_manifest_id: [20u8; 32],
1348                    title: Some("community".into()),
1349                    description: Some("public".into()),
1350                }],
1351            }),
1352            WirePayload::RelayRegister(RelayRegister {
1353                relay_slot_id: Some(77),
1354                tunnel: false,
1355            }),
1356            WirePayload::RelayRegistered(RelayRegistered {
1357                relay_slot_id: 77,
1358                expires_at: 88,
1359            }),
1360            WirePayload::RelayConnect(RelayConnect { relay_slot_id: 11 }),
1361            WirePayload::RelayStream(RelayStream {
1362                relay_slot_id: 11,
1363                stream_id: 3,
1364                kind: RelayPayloadKind::Control,
1365                payload: vec![5, 4, 3],
1366            }),
1367            WirePayload::Providers(Providers {
1368                content_id: [10u8; 32],
1369                providers: vec![PeerAddr {
1370                    ip: "10.0.0.3".parse().expect("valid ip"),
1371                    port: 9999,
1372                    transport: TransportProtocol::Quic,
1373                    pubkey_hint: Some([1u8; 32]),
1374                    relay_via: None,
1375                }],
1376                updated_at: 321,
1377            }),
1378            WirePayload::HaveContent(HaveContent {
1379                content_id: [11u8; 32],
1380            }),
1381            WirePayload::GetChunk(GetChunk {
1382                content_id: [12u8; 32],
1383                chunk_index: 2,
1384            }),
1385            WirePayload::ChunkData(ChunkData {
1386                content_id: [13u8; 32],
1387                chunk_index: 2,
1388                bytes: vec![6, 6, 6],
1389            }),
1390            WirePayload::GetChunkHashes(GetChunkHashes {
1391                content_id: [14u8; 32],
1392            }),
1393            WirePayload::ChunkHashList(ChunkHashList {
1394                content_id: [14u8; 32],
1395                hashes: vec![[1u8; 32], [2u8; 32]],
1396            }),
1397        ];
1398
1399        for (idx, message) in cases.iter().enumerate() {
1400            let envelope = Envelope::from_typed(idx as u32, 0, message).expect("build envelope");
1401            let decoded = envelope.decode_typed().expect("decode typed payload");
1402            assert_eq!(&decoded, message);
1403        }
1404    }
1405}