Skip to main content

pot_o_extensions/
mdns_discovery.rs

1//! mDNS (Multicast DNS) service discovery for PoT-O validators.
2//!
3//! Enables validators on the same local network to automatically discover each other
4//! without requiring a Bootstrap Registry. Validators register themselves as mDNS services
5//! and can discover peers via mDNS queries.
6
7use flume::RecvTimeoutError;
8use mdns_sd::{ServiceDaemon, ServiceInfo};
9use std::net::IpAddr;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13// ---------------------------------------------------------------------------
14// Types
15// ---------------------------------------------------------------------------
16
17/// Information about a discovered peer validator
18#[derive(Clone, Debug)]
19pub struct PeerDiscovery {
20    /// Unique identifier for the validator node
21    pub node_id: String,
22    /// Hostname (FQDN) of the validator
23    pub hostname: String,
24    /// IP address of the validator
25    pub ip: IpAddr,
26    /// Port on which the validator is listening
27    pub port: u16,
28}
29
30/// mDNS service discovery for validator peer discovery
31pub struct MdnsDiscovery {
32    /// Unique identifier for this validator
33    node_id: String,
34    /// Port on which this validator listens
35    port: u16,
36    /// mDNS service type for PoT-O validators
37    service_type: String,
38    /// mDNS daemon for service registration and discovery
39    daemon: Arc<Mutex<Option<ServiceDaemon>>>,
40}
41
42// ---------------------------------------------------------------------------
43// Implementation
44// ---------------------------------------------------------------------------
45
46impl MdnsDiscovery {
47    /// Service type string for PoT-O validators following mDNS conventions
48    const SERVICE_TYPE: &'static str = "_pot-o-validator._tcp.local.";
49
50    /// Create a new mDNS discovery instance and start the daemon.
51    ///
52    /// # Arguments
53    /// * `node_id` - Unique identifier for this validator
54    /// * `port` - Port on which this validator listens
55    ///
56    /// # Returns
57    /// Result with MdnsDiscovery instance or error if daemon fails to start
58    pub fn new(node_id: &str, port: u16) -> Result<Self, Box<dyn std::error::Error>> {
59        // Create and start the mDNS daemon
60        let daemon =
61            ServiceDaemon::new().map_err(|e| format!("Failed to create mDNS daemon: {}", e))?;
62
63        Ok(Self {
64            node_id: node_id.to_string(),
65            port,
66            service_type: Self::SERVICE_TYPE.to_string(),
67            daemon: Arc::new(Mutex::new(Some(daemon))),
68        })
69    }
70
71    /// Get the node ID of this validator
72    pub fn node_id(&self) -> &str {
73        &self.node_id
74    }
75
76    /// Get the port of this validator
77    pub fn port(&self) -> u16 {
78        self.port
79    }
80
81    /// Get the mDNS service type
82    pub fn service_type(&self) -> &str {
83        &self.service_type
84    }
85
86    /// Register this validator as an mDNS service.
87    ///
88    /// # Arguments
89    /// * `hostname` - Hostname/FQDN for this validator (e.g., "validator-1.local")
90    ///
91    /// # Returns
92    /// Result indicating success or error
93    pub fn register_service(&self, hostname: &str) -> Result<(), Box<dyn std::error::Error>> {
94        let daemon_lock = self
95            .daemon
96            .lock()
97            .map_err(|e| format!("Failed to lock daemon: {}", e))?;
98
99        let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
100
101        // Create service info with validator details
102        let service_info = ServiceInfo::new(
103            Self::SERVICE_TYPE,
104            &format!("{}._pot-o-validator._tcp.local.", self.node_id),
105            &self.node_id,
106            hostname,
107            self.port,
108            None,
109        )
110        .map_err(|e| format!("Failed to create ServiceInfo: {}", e))?;
111
112        // Register the service with the daemon
113        daemon
114            .register(service_info)
115            .map_err(|e| format!("Failed to register service: {}", e))?;
116
117        Ok(())
118    }
119
120    /// Discover other validators on the local network via mDNS.
121    ///
122    /// # Arguments
123    /// * `timeout_secs` - How long to wait for discovery responses
124    ///
125    /// # Returns
126    /// Result with vector of discovered PeerDiscovery instances
127    pub fn discover_peers(
128        &self,
129        timeout_secs: u64,
130    ) -> Result<Vec<PeerDiscovery>, Box<dyn std::error::Error>> {
131        let daemon_lock = self
132            .daemon
133            .lock()
134            .map_err(|e| format!("Failed to lock daemon: {}", e))?;
135
136        let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
137
138        // Browse for all PoT-O validator services
139        let receiver = daemon
140            .browse(Self::SERVICE_TYPE)
141            .map_err(|e| format!("Failed to browse services: {}", e))?;
142
143        let mut peers = Vec::new();
144        let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
145
146        // Collect discovery events until timeout
147        loop {
148            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
149            if remaining.is_zero() {
150                break;
151            }
152
153            match receiver.recv_timeout(remaining) {
154                Ok(mdns_sd::ServiceEvent::ServiceResolved(info)) => {
155                    // Extract peer information from resolved service
156                    if let Some(peer) = self.extract_peer_from_service(&info) {
157                        // Avoid adding ourselves to the peer list
158                        if peer.node_id != self.node_id {
159                            peers.push(peer);
160                        }
161                    }
162                }
163                Ok(mdns_sd::ServiceEvent::ServiceRemoved(_, _)) => {
164                    // Service was removed, could filter from peers if we track them
165                }
166                Ok(mdns_sd::ServiceEvent::SearchStarted(_)) => {
167                    // Search has started, ignore
168                }
169                Ok(mdns_sd::ServiceEvent::ServiceFound(_, _)) => {
170                    // Service found but not yet resolved, ignore (we'll get ServiceResolved)
171                }
172                Ok(mdns_sd::ServiceEvent::SearchStopped(_)) => {
173                    // Search stopped, could exit discovery here
174                    break;
175                }
176                Err(RecvTimeoutError::Timeout) => {
177                    break; // Timeout reached, exit discovery
178                }
179                Err(_) => {
180                    // Other receive errors, exit discovery
181                    break;
182                }
183            }
184        }
185
186        Ok(peers)
187    }
188
189    /// Unregister this validator from mDNS (cleanup).
190    ///
191    /// # Returns
192    /// Result indicating success or error
193    pub fn unregister_service(&self) -> Result<(), Box<dyn std::error::Error>> {
194        let daemon_lock = self
195            .daemon
196            .lock()
197            .map_err(|e| format!("Failed to lock daemon: {}", e))?;
198
199        let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
200
201        let service_name = format!("{}._pot-o-validator._tcp.local.", self.node_id);
202        daemon
203            .unregister(&service_name)
204            .map_err(|e| format!("Failed to unregister service: {}", e))?;
205
206        Ok(())
207    }
208
209    /// Extract peer discovery information from an mDNS ServiceInfo.
210    fn extract_peer_from_service(&self, info: &ServiceInfo) -> Option<PeerDiscovery> {
211        // Get the fullname (e.g., "validator-1._pot-o-validator._tcp.local.")
212        let fullname = info.get_fullname();
213
214        // Extract node_id by removing the service type suffix
215        let node_id = fullname
216            .strip_suffix("._pot-o-validator._tcp.local.")?
217            .to_string();
218
219        // Get hostname
220        let hostname = info.get_hostname().to_string();
221
222        // Get IP address (first available IPv4)
223        let ip = info
224            .get_addresses()
225            .iter()
226            .next()
227            .map(|ipv4| IpAddr::V4(*ipv4))?;
228
229        let port = info.get_port();
230
231        Some(PeerDiscovery {
232            node_id,
233            hostname,
234            ip,
235            port,
236        })
237    }
238}
239
240// Ensure MdnsDiscovery is Send + Sync for use in async contexts
241#[allow(dead_code)]
242fn _assert_send_sync()
243where
244    MdnsDiscovery: Send + Sync,
245{
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_service_type_constant() {
254        assert_eq!(MdnsDiscovery::SERVICE_TYPE, "_pot-o-validator._tcp.local.");
255    }
256
257    #[test]
258    fn test_peer_discovery_creation() {
259        let peer = PeerDiscovery {
260            node_id: "test-node".to_string(),
261            hostname: "test-node.local".to_string(),
262            ip: "127.0.0.1".parse().unwrap(),
263            port: 5555,
264        };
265
266        assert_eq!(peer.node_id, "test-node");
267        assert_eq!(peer.port, 5555);
268    }
269}