Skip to main content

dig_nat/
wire.rs

1//! Relay protocol wire types — **vendored, byte-identical** to `dig-relay`'s `src/wire.rs`,
2//! `dig-node`'s `relay::RelayMessage`, and `dig-gossip`'s `relay_types` (requirements
3//! **RLY-001** through **RLY-007**).
4//!
5//! # Provenance & contract
6//!
7//! The canonical relay wire lives in `dig-gossip` (`src/relay/relay_types.rs`); `dig-relay` is the
8//! SERVER, `dig-node`/`dig-nat` are CLIENTS of the same JSON-over-WebSocket wire. These types are
9//! copied here verbatim rather than depending on `dig-gossip` because the wire depends only on
10//! `serde` + `std`, whereas `dig-gossip` pulls the entire L2/Chia stack just to expose two structs.
11//! The `#[serde(tag = "type")]` discriminators + field names MUST stay byte-identical to the
12//! server's so both speak the same JSON; this is pinned by `tests/wire_conformance.rs`. The
13//! superproject `SYSTEM.md` records the change-impact edge: a change to the relay wire must be
14//! mirrored across all four copies in the same unit of work.
15
16use std::net::SocketAddr;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use serde::{Deserialize, Serialize};
20
21/// Complete relay protocol message enum — JSON over WebSocket, `#[serde(tag = "type")]`.
22// Field-level docs are intentionally omitted on this VENDORED type: the fields are the wire
23// contract, kept byte-identical to the four copies (dig-relay, dig-node, dig-gossip, dig-nat), and
24// documenting them per-copy would invite drift. The variant docs above each `#[serde(rename)]`
25// carry the RLY-00x meaning; the field names ARE the JSON keys.
26#[allow(missing_docs)]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type")]
29pub enum RelayMessage {
30    // -- RLY-001: Registration --
31    /// Client → Relay: register after WebSocket connect.
32    #[serde(rename = "register")]
33    Register {
34        peer_id: String,
35        network_id: String,
36        protocol_version: u32,
37    },
38
39    /// Relay → Client: registration acknowledgement.
40    #[serde(rename = "register_ack")]
41    RegisterAck {
42        success: bool,
43        message: String,
44        connected_peers: usize,
45    },
46
47    /// Client → Relay: graceful disconnect.
48    #[serde(rename = "unregister")]
49    Unregister { peer_id: String },
50
51    // -- RLY-002: Targeted message forwarding --
52    /// Client → Relay → Client: forward to a specific peer.
53    #[serde(rename = "relay_message")]
54    RelayGossipMessage {
55        from: String,
56        to: String,
57        payload: Vec<u8>,
58        seq: u64,
59    },
60
61    // -- RLY-003: Broadcast --
62    /// Client → Relay → All: broadcast to all relay peers.
63    #[serde(rename = "broadcast")]
64    Broadcast {
65        from: String,
66        payload: Vec<u8>,
67        exclude: Vec<String>,
68    },
69
70    // -- Peer notifications --
71    /// Relay → Client: new peer connected to relay.
72    #[serde(rename = "peer_connected")]
73    PeerConnected { peer: RelayPeerInfo },
74
75    /// Relay → Client: peer disconnected from relay.
76    #[serde(rename = "peer_disconnected")]
77    PeerDisconnected { peer_id: String },
78
79    // -- RLY-005: Peer list --
80    /// Client → Relay: request connected peer list.
81    #[serde(rename = "get_peers")]
82    GetPeers { network_id: Option<String> },
83
84    /// Relay → Client: peer list response.
85    #[serde(rename = "peers")]
86    Peers { peers: Vec<RelayPeerInfo> },
87
88    // -- RLY-006: Keepalive --
89    /// Bidirectional keepalive.
90    #[serde(rename = "ping")]
91    Ping { timestamp: u64 },
92
93    /// Keepalive response.
94    #[serde(rename = "pong")]
95    Pong { timestamp: u64 },
96
97    // -- RLY-007: NAT traversal --
98    /// Client → Relay: request hole-punch coordination.
99    #[serde(rename = "hole_punch_request")]
100    HolePunchRequest {
101        peer_id: String,
102        target_peer_id: String,
103        external_addr: SocketAddr,
104    },
105
106    /// Relay → Client: hole-punch coordination (the other peer's external address).
107    #[serde(rename = "hole_punch_coordinate")]
108    HolePunchCoordinate {
109        peer_id: String,
110        external_addr: SocketAddr,
111    },
112
113    /// Client → Relay: hole-punch result.
114    #[serde(rename = "hole_punch_result")]
115    HolePunchResult { peer_id: String, success: bool },
116
117    // -- Error --
118    /// Relay → Client: error notification.
119    #[serde(rename = "error")]
120    Error { code: u32, message: String },
121}
122
123/// Peer info as tracked by the relay server. `#[serde]` field names are part of the wire contract
124/// (vendored byte-identical — see the module docs; field-level docs omitted to avoid drift).
125#[allow(missing_docs)]
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct RelayPeerInfo {
128    pub peer_id: String,
129    pub network_id: String,
130    pub protocol_version: u32,
131    pub connected_at: u64,
132    pub last_seen: u64,
133}
134
135impl RelayPeerInfo {
136    /// Build a `RelayPeerInfo` stamped with the current unix time for `connected_at`/`last_seen`.
137    pub fn new(peer_id: String, network_id: String, protocol_version: u32) -> Self {
138        let now = unix_secs();
139        Self {
140            peer_id,
141            network_id,
142            protocol_version,
143            connected_at: now,
144            last_seen: now,
145        }
146    }
147}
148
149/// Current unix time in seconds (saturating). Mirrors dig-gossip's metric timestamp helper.
150fn unix_secs() -> u64 {
151    SystemTime::now()
152        .duration_since(UNIX_EPOCH)
153        .map(|d| d.as_secs())
154        .unwrap_or(0)
155}