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 // The node's advertised gossip LISTEN candidate address(es), IPv6-first (§5.2). Additive
38 // since protocol v1 (NC-6 soft-fork): appended LAST, default-empty + skip-when-empty so the
39 // wire stays byte-identical for pre-#924 peers. Byte-identical to dig-relay-protocol 0.2.0.
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 listen_addrs: Vec<SocketAddr>,
42 },
43
44 /// Relay → Client: registration acknowledgement.
45 #[serde(rename = "register_ack")]
46 RegisterAck {
47 success: bool,
48 message: String,
49 connected_peers: usize,
50 },
51
52 /// Client → Relay: graceful disconnect.
53 #[serde(rename = "unregister")]
54 Unregister { peer_id: String },
55
56 // -- RLY-002: Targeted message forwarding --
57 /// Client → Relay → Client: forward to a specific peer.
58 #[serde(rename = "relay_message")]
59 RelayGossipMessage {
60 from: String,
61 to: String,
62 payload: Vec<u8>,
63 seq: u64,
64 },
65
66 // -- RLY-003: Broadcast --
67 /// Client → Relay → All: broadcast to all relay peers.
68 #[serde(rename = "broadcast")]
69 Broadcast {
70 from: String,
71 payload: Vec<u8>,
72 exclude: Vec<String>,
73 },
74
75 // -- Peer notifications --
76 /// Relay → Client: new peer connected to relay.
77 #[serde(rename = "peer_connected")]
78 PeerConnected { peer: RelayPeerInfo },
79
80 /// Relay → Client: peer disconnected from relay.
81 #[serde(rename = "peer_disconnected")]
82 PeerDisconnected { peer_id: String },
83
84 // -- RLY-005: Peer list --
85 /// Client → Relay: request connected peer list.
86 #[serde(rename = "get_peers")]
87 GetPeers { network_id: Option<String> },
88
89 /// Relay → Client: peer list response.
90 #[serde(rename = "peers")]
91 Peers { peers: Vec<RelayPeerInfo> },
92
93 // -- RLY-006: Keepalive --
94 /// Bidirectional keepalive.
95 #[serde(rename = "ping")]
96 Ping { timestamp: u64 },
97
98 /// Keepalive response.
99 #[serde(rename = "pong")]
100 Pong { timestamp: u64 },
101
102 // -- RLY-007: NAT traversal --
103 /// Client → Relay: request hole-punch coordination.
104 #[serde(rename = "hole_punch_request")]
105 HolePunchRequest {
106 peer_id: String,
107 target_peer_id: String,
108 external_addr: SocketAddr,
109 },
110
111 /// Relay → Client: hole-punch coordination (the other peer's external address).
112 #[serde(rename = "hole_punch_coordinate")]
113 HolePunchCoordinate {
114 peer_id: String,
115 external_addr: SocketAddr,
116 },
117
118 /// Client → Relay: hole-punch result.
119 #[serde(rename = "hole_punch_result")]
120 HolePunchResult { peer_id: String, success: bool },
121
122 // -- Error --
123 /// Relay → Client: error notification.
124 #[serde(rename = "error")]
125 Error { code: u32, message: String },
126}
127
128/// Peer info as tracked by the relay server. `#[serde]` field names are part of the wire contract
129/// (vendored byte-identical — see the module docs; field-level docs omitted to avoid drift).
130#[allow(missing_docs)]
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct RelayPeerInfo {
133 pub peer_id: String,
134 pub network_id: String,
135 pub protocol_version: u32,
136 pub connected_at: u64,
137 pub last_seen: u64,
138 // Relay-resolved dialable candidate address(es) for this peer, IPv6-first (§5.2) — the relay
139 // substitutes the observed reflexive IP for any unspecified/loopback/private advertised
140 // `listen_addr` host (keeping the port). Additive since protocol v1 (NC-6 soft-fork): appended
141 // LAST, default-empty + skip-when-empty. Byte-identical to dig-relay-protocol 0.2.0.
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 pub addresses: Vec<SocketAddr>,
144}
145
146impl RelayPeerInfo {
147 /// Build a `RelayPeerInfo` stamped with the current unix time for `connected_at`/`last_seen`.
148 pub fn new(peer_id: String, network_id: String, protocol_version: u32) -> Self {
149 let now = unix_secs();
150 Self {
151 peer_id,
152 network_id,
153 protocol_version,
154 connected_at: now,
155 last_seen: now,
156 addresses: Vec::new(),
157 }
158 }
159}
160
161/// Current unix time in seconds (saturating). Mirrors dig-gossip's metric timestamp helper.
162fn unix_secs() -> u64 {
163 SystemTime::now()
164 .duration_since(UNIX_EPOCH)
165 .map(|d| d.as_secs())
166 .unwrap_or(0)
167}