1use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
21use std::sync::{Arc, Mutex};
22use std::time::Duration;
23
24use futures_util::{SinkExt, StreamExt};
25use tokio_tungstenite::tungstenite::Message;
26
27use crate::wire::RelayMessage;
28
29pub const DEFAULT_NETWORK_ID: &str = "DIG_MAINNET";
32
33pub const RELAY_PROTOCOL_VERSION: u32 = 1;
35
36const BASE_BACKOFF_SECS: u64 = 5;
38const MAX_BACKOFF_SECS: u64 = 300;
40const PING_INTERVAL_SECS: u64 = 30;
42
43pub fn backoff_secs(consecutive_failures: u32) -> u64 {
46 backoff_secs_with(consecutive_failures, BASE_BACKOFF_SECS, MAX_BACKOFF_SECS)
47}
48
49fn backoff_secs_with(consecutive_failures: u32, base: u64, cap: u64) -> u64 {
52 let shifted = base.checked_shl(consecutive_failures).unwrap_or(cap);
53 shifted.clamp(base, cap)
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct Backoff {
59 pub base_secs: u64,
61 pub cap_secs: u64,
63}
64
65impl Default for Backoff {
66 fn default() -> Self {
67 Backoff {
68 base_secs: BASE_BACKOFF_SECS,
69 cap_secs: MAX_BACKOFF_SECS,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum RelayState {
83 Disabled,
85 Connecting,
87 Connected,
89 Disconnected,
91}
92
93impl RelayState {
94 pub fn as_str(self) -> &'static str {
96 match self {
97 RelayState::Disabled => "disabled",
98 RelayState::Connecting => "connecting",
99 RelayState::Connected => "connected",
100 RelayState::Disconnected => "disconnected",
101 }
102 }
103
104 fn to_u8(self) -> u8 {
105 match self {
106 RelayState::Disabled => 0,
107 RelayState::Connecting => 1,
108 RelayState::Connected => 2,
109 RelayState::Disconnected => 3,
110 }
111 }
112
113 fn from_u8(v: u8) -> Self {
114 match v {
115 0 => RelayState::Disabled,
116 1 => RelayState::Connecting,
117 2 => RelayState::Connected,
118 _ => RelayState::Disconnected,
119 }
120 }
121}
122
123#[derive(Debug)]
127pub struct RelayStatus {
128 state: AtomicU8,
129 reconnect_attempts: AtomicU32,
130 connected_peers: AtomicU64,
131 last_error: Mutex<Option<String>>,
132}
133
134impl Default for RelayStatus {
135 fn default() -> Self {
136 RelayStatus {
137 state: AtomicU8::new(RelayState::Disconnected.to_u8()),
138 reconnect_attempts: AtomicU32::new(0),
139 connected_peers: AtomicU64::new(0),
140 last_error: Mutex::new(None),
141 }
142 }
143}
144
145impl RelayStatus {
146 pub fn new() -> Arc<Self> {
148 Arc::new(RelayStatus::default())
149 }
150
151 pub fn state(&self) -> RelayState {
153 RelayState::from_u8(self.state.load(Ordering::Relaxed))
154 }
155
156 fn transition_to(&self, next: RelayState) -> bool {
159 let prev = self.state.swap(next.to_u8(), Ordering::Relaxed);
160 prev != next.to_u8()
161 }
162
163 pub fn set_disabled(&self) {
165 if self.transition_to(RelayState::Disabled) {
166 tracing::info!("relay reservation disabled (DIG_RELAY_URL=off)");
167 }
168 }
169
170 pub fn set_connecting(&self) {
172 if self.transition_to(RelayState::Connecting) {
173 tracing::debug!("relay connecting");
174 }
175 }
176
177 pub fn set_connected(&self, connected_peers: u64) {
179 self.connected_peers
180 .store(connected_peers, Ordering::Relaxed);
181 self.reconnect_attempts.store(0, Ordering::Relaxed);
182 *self.last_error.lock().unwrap() = None;
183 if self.transition_to(RelayState::Connected) {
184 tracing::info!(connected_peers, "relay reservation established");
185 }
186 }
187
188 pub fn set_disconnected(&self, error: Option<String>) {
192 self.reconnect_attempts.fetch_add(1, Ordering::Relaxed);
193 if let Some(e) = &error {
194 *self.last_error.lock().unwrap() = Some(e.clone());
195 }
196 let changed = self.transition_to(RelayState::Disconnected);
197 if changed {
198 match &error {
199 Some(e) => tracing::warn!(
200 error = %e,
201 "relay reservation lost — node still serving; retrying in background"
202 ),
203 None => tracing::info!("relay reservation closed — retrying in background"),
204 }
205 }
206 }
207
208 pub fn is_connected(&self) -> bool {
210 self.state() == RelayState::Connected
211 }
212
213 pub fn reconnect_attempts(&self) -> u32 {
215 self.reconnect_attempts.load(Ordering::Relaxed)
216 }
217
218 pub fn snapshot_json(&self, endpoint: &str, peer_id: &str) -> serde_json::Value {
221 let state = self.state();
222 serde_json::json!({
223 "state": state.as_str(),
224 "connected": state == RelayState::Connected,
225 "endpoint": endpoint,
226 "peer_id": peer_id,
227 "reconnect_attempts": self.reconnect_attempts.load(Ordering::Relaxed),
228 "connected_peers": self.connected_peers.load(Ordering::Relaxed),
229 "last_error": *self.last_error.lock().unwrap(),
230 })
231 }
232}
233
234pub fn relay_url_from_env() -> String {
237 std::env::var("DIG_RELAY_URL")
238 .ok()
239 .filter(|s| !s.trim().is_empty())
240 .filter(|s| !is_off_token(s))
241 .unwrap_or_else(|| dig_constants::DIG_RELAY_URL.to_string())
242}
243
244pub fn relay_enabled() -> bool {
247 match std::env::var("DIG_RELAY_URL") {
248 Ok(v) => !is_off_token(&v),
249 Err(_) => true,
250 }
251}
252
253fn is_off_token(v: &str) -> bool {
255 let v = v.trim();
256 v.eq_ignore_ascii_case("off") || v.eq_ignore_ascii_case("disabled")
257}
258
259fn now_secs() -> u64 {
261 std::time::SystemTime::now()
262 .duration_since(std::time::UNIX_EPOCH)
263 .map(|d| d.as_secs())
264 .unwrap_or(0)
265}
266
267pub async fn run_relay_connection(
271 endpoint: String,
272 peer_id: String,
273 network_id: String,
274 status: Arc<RelayStatus>,
275) {
276 run_relay_connection_with(endpoint, peer_id, network_id, status, Backoff::default()).await
277}
278
279pub async fn run_relay_connection_with(
282 endpoint: String,
283 peer_id: String,
284 network_id: String,
285 status: Arc<RelayStatus>,
286 backoff: Backoff,
287) {
288 let mut consecutive_failures: u32 = 0;
289 loop {
290 status.set_connecting();
291 match connect_once(&endpoint, &peer_id, &network_id, &status).await {
292 Ok(()) => {
293 consecutive_failures = 0;
294 status.set_disconnected(None);
295 }
296 Err(e) => {
297 consecutive_failures = consecutive_failures.saturating_add(1);
298 status.set_disconnected(Some(e));
299 }
300 }
301 let delay = backoff_secs_with(consecutive_failures, backoff.base_secs, backoff.cap_secs);
303 tokio::time::sleep(Duration::from_secs(delay)).await;
304 }
305}
306
307async fn connect_once(
309 endpoint: &str,
310 peer_id: &str,
311 network_id: &str,
312 status: &Arc<RelayStatus>,
313) -> Result<(), String> {
314 let (ws, _resp) = tokio_tungstenite::connect_async(endpoint)
315 .await
316 .map_err(|e| format!("connect: {e}"))?;
317 let (mut write, mut read) = ws.split();
318
319 let register = RelayMessage::Register {
321 peer_id: peer_id.to_string(),
322 network_id: network_id.to_string(),
323 protocol_version: RELAY_PROTOCOL_VERSION,
324 };
325 send(&mut write, ®ister).await?;
326
327 let mut ping = tokio::time::interval(Duration::from_secs(PING_INTERVAL_SECS));
328 ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
329 ping.tick().await; loop {
332 tokio::select! {
333 _ = ping.tick() => {
334 send(&mut write, &RelayMessage::Ping { timestamp: now_secs() }).await?;
335 }
336 frame = read.next() => {
337 match frame {
338 None => return Ok(()),
339 Some(Err(e)) => return Err(format!("read: {e}")),
340 Some(Ok(Message::Close(_))) => return Ok(()),
341 Some(Ok(Message::Ping(p))) => {
342 write.send(Message::Pong(p)).await.map_err(|e| format!("pong: {e}"))?;
343 }
344 Some(Ok(Message::Pong(_))) | Some(Ok(Message::Frame(_))) => {}
345 Some(Ok(Message::Text(t))) => {
346 handle_incoming(t.into_bytes(), &mut write, status).await?;
347 }
348 Some(Ok(Message::Binary(b))) => {
349 handle_incoming(b, &mut write, status).await?;
350 }
351 }
352 }
353 }
354 }
355}
356
357async fn handle_incoming<W>(
359 bytes: Vec<u8>,
360 write: &mut W,
361 status: &Arc<RelayStatus>,
362) -> Result<(), String>
363where
364 W: SinkExt<Message> + Unpin,
365 <W as futures_util::Sink<Message>>::Error: std::fmt::Display,
366{
367 let Ok(msg) = serde_json::from_slice::<RelayMessage>(&bytes) else {
368 return Ok(()); };
370 match msg {
371 RelayMessage::RegisterAck {
372 success,
373 message,
374 connected_peers,
375 } => {
376 if success {
377 status.set_connected(connected_peers as u64);
378 } else {
379 return Err(format!("register rejected: {message}"));
380 }
381 }
382 RelayMessage::Ping { timestamp } => {
383 send(write, &RelayMessage::Pong { timestamp }).await?;
384 }
385 RelayMessage::Error { code, message } => {
386 return Err(format!("relay error {code}: {message}"));
387 }
388 other => tracing::debug!(?other, "relay message ignored by reservation loop"),
389 }
390 Ok(())
391}
392
393async fn send<W>(write: &mut W, msg: &RelayMessage) -> Result<(), String>
395where
396 W: SinkExt<Message> + Unpin,
397 <W as futures_util::Sink<Message>>::Error: std::fmt::Display,
398{
399 let txt = serde_json::to_string(msg).map_err(|e| format!("encode: {e}"))?;
400 write
401 .send(Message::Text(txt))
402 .await
403 .map_err(|e| format!("send: {e}"))
404}