//! Local address advertisement. //! //! Allows a node to announce its address to peers so //! they can update their routing tables with the correct //! endpoint. //! //! Used after NAT detection to inform peers of the //! node's externally-visible address. use std::collections::HashMap; use std::time::{Duration, Instant}; use crate::id::NodeId; // ── Constants ──────────────────────────────────────── /// TTL for advertisements. pub const ADVERTISE_TTL: Duration = Duration::from_secs(300); /// Timeout for a single advertisement attempt. pub const ADVERTISE_TIMEOUT: Duration = Duration::from_secs(2); /// Interval between refresh cycles. pub const ADVERTISE_REFRESH_INTERVAL: Duration = Duration::from_secs(100); /// A pending outgoing advertisement. #[derive(Debug)] struct PendingAd { sent_at: Instant, } /// A received advertisement from a peer. #[derive(Debug)] struct ReceivedAd { received_at: Instant, } /// Address advertisement manager. pub struct Advertise { /// Pending outgoing advertisements by nonce. pending: HashMap, /// Received advertisements by peer ID. received: HashMap, } impl Advertise { pub fn new(_local_id: NodeId) -> Self { Self { pending: HashMap::new(), received: HashMap::new(), } } /// Start an advertisement to a peer. /// /// Returns the nonce to include in the message. pub fn start_advertise(&mut self, nonce: u32) -> u32 { self.pending.insert( nonce, PendingAd { sent_at: Instant::now(), }, ); nonce } /// Handle an advertisement reply (our ad was accepted). /// /// Returns `true` if the nonce matched a pending ad. pub fn recv_reply(&mut self, nonce: u32) -> bool { self.pending.remove(&nonce).is_some() } /// Handle an incoming advertisement from a peer. /// /// Records that this peer has advertised to us. pub fn recv_advertise(&mut self, peer_id: NodeId) { self.received.insert( peer_id, ReceivedAd { received_at: Instant::now(), }, ); } /// Check if a peer has advertised to us recently. pub fn has_advertised(&self, peer_id: &NodeId) -> bool { self.received .get(peer_id) .map(|ad| ad.received_at.elapsed() < ADVERTISE_TTL) .unwrap_or(false) } /// Remove expired pending ads and stale received ads. pub fn refresh(&mut self) { self.pending .retain(|_, ad| ad.sent_at.elapsed() < ADVERTISE_TIMEOUT); self.received .retain(|_, ad| ad.received_at.elapsed() < ADVERTISE_TTL); } /// Number of pending outgoing advertisements. pub fn pending_count(&self) -> usize { self.pending.len() } /// Number of active received advertisements. pub fn received_count(&self) -> usize { self.received .values() .filter(|ad| ad.received_at.elapsed() < ADVERTISE_TTL) .count() } } #[cfg(test)] mod tests { use super::*; #[test] fn advertise_and_reply() { let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); adv.start_advertise(42); assert_eq!(adv.pending_count(), 1); assert!(adv.recv_reply(42)); assert_eq!(adv.pending_count(), 0); } #[test] fn unknown_reply_ignored() { let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); assert!(!adv.recv_reply(999)); } #[test] fn recv_advertisement() { let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); let peer = NodeId::from_bytes([0x02; 32]); assert!(!adv.has_advertised(&peer)); adv.recv_advertise(peer); assert!(adv.has_advertised(&peer)); assert_eq!(adv.received_count(), 1); } #[test] fn refresh_clears_stale() { let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); // Insert already-expired pending ad adv.pending.insert( 1, PendingAd { sent_at: Instant::now() - Duration::from_secs(10), }, ); // Insert already-expired received ad let peer = NodeId::from_bytes([0x02; 32]); adv.received.insert( peer, ReceivedAd { received_at: Instant::now() - Duration::from_secs(600), }, ); adv.refresh(); assert_eq!(adv.pending_count(), 0); assert_eq!(adv.received_count(), 0); } }