saorsa_core/bootstrap/
discovery.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Bootstrap Discovery Module
15//!
16//! Provides multiple mechanisms for discovering bootstrap nodes:
17//! 1. Hardcoded well-known bootstrap nodes
18//! 2. Four-word address resolution
19//! 3. DNS-based discovery (future)
20//! 4. Peer exchange from connected nodes
21
22use crate::address::NetworkAddress;
23use crate::bootstrap::{FourWordAddress, WordEncoder};
24use anyhow::Result;
25use std::collections::HashMap;
26use std::net::SocketAddr;
27use tracing::{debug, info, warn};
28
29/// Well-known bootstrap nodes for the P2P Foundation network
30#[derive(Debug, Clone)]
31pub struct BootstrapDiscovery {
32    word_encoder: WordEncoder,
33    hardcoded_nodes: HashMap<String, NetworkAddress>,
34    custom_nodes: Vec<NetworkAddress>,
35}
36
37impl BootstrapDiscovery {
38    /// Create a new bootstrap discovery instance with default well-known nodes
39    pub fn new() -> Self {
40        let mut hardcoded_nodes = HashMap::new();
41
42        // Digital Ocean bootstrap nodes (will be updated with real addresses)
43        hardcoded_nodes.insert(
44            "foundation.main.bootstrap".to_string(),
45            NetworkAddress::from_ipv4(std::net::Ipv4Addr::new(147, 182, 203, 123), 9000),
46        );
47
48        hardcoded_nodes.insert(
49            "foundation.backup.lighthouse".to_string(),
50            NetworkAddress::from_ipv4(std::net::Ipv4Addr::new(147, 182, 203, 124), 9000),
51        );
52
53        // IPv6 primary bootstrap (Digital Ocean IPv6)
54        hardcoded_nodes.insert(
55            "global.fast.eagle".to_string(),
56            NetworkAddress::from_ipv6(
57                "2604:a880:400:d1:0:2:40d7:9001"
58                    .parse()
59                    .unwrap_or(std::net::Ipv6Addr::LOCALHOST),
60                9000,
61            ),
62        );
63
64        // IPv4 fallback bootstrap
65        hardcoded_nodes.insert(
66            "reliable.sturdy.anchor".to_string(),
67            NetworkAddress::from_ipv4(std::net::Ipv4Addr::new(8, 8, 8, 8), 9000),
68        );
69
70        Self {
71            word_encoder: WordEncoder::new(),
72            hardcoded_nodes,
73            custom_nodes: Vec::new(),
74        }
75    }
76
77    /// Add a custom bootstrap node
78    pub fn add_bootstrap(&mut self, addr: NetworkAddress) {
79        self.custom_nodes.push(addr);
80    }
81
82    /// Resolve a four-word address to a socket address  
83    pub fn resolve_four_words(&self, four_words: &str) -> Result<std::net::SocketAddr> {
84        // First check if it's a hardcoded well-known address
85        if let Some(addr) = self.hardcoded_nodes.get(four_words) {
86            debug!(
87                "Resolved hardcoded four-word address: {} -> {}",
88                four_words, addr
89            );
90            // Convert from NetworkAddress to SocketAddr
91            return Ok(addr.socket_addr());
92        }
93
94        // Try to decode as a generated four-word address
95        let word_address = FourWordAddress::from_string(four_words)
96            .map_err(|e| anyhow::anyhow!("Invalid four-word address format: {}", e))?;
97
98        // Decode four-word address to IP+port
99        let socket_addr = self
100            .word_encoder
101            .decode_to_socket_addr(&word_address)
102            .map_err(|e| anyhow::anyhow!("Failed to decode four-word address: {}", e))?;
103
104        Ok(socket_addr)
105    }
106
107    /// Get all available bootstrap addresses
108    pub fn get_bootstrap_addresses(&self) -> Vec<NetworkAddress> {
109        let mut addresses = Vec::new();
110
111        // Add hardcoded nodes
112        addresses.extend(self.hardcoded_nodes.values().cloned());
113
114        // Add custom nodes
115        addresses.extend(self.custom_nodes.clone());
116
117        addresses
118    }
119
120    /// Get well-known four-word addresses
121    pub fn get_well_known_four_words(&self) -> Vec<String> {
122        self.hardcoded_nodes.keys().cloned().collect()
123    }
124
125    /// Discover bootstrap nodes using multiple methods
126    pub async fn discover_bootstraps(&self) -> Result<Vec<NetworkAddress>> {
127        let mut discovered = Vec::new();
128
129        info!("๐Ÿ” Discovering bootstrap nodes...");
130
131        // Start with hardcoded nodes
132        let hardcoded = self.get_bootstrap_addresses();
133        info!("๐Ÿ“ Found {} hardcoded bootstrap nodes", hardcoded.len());
134        discovered.extend(hardcoded);
135
136        // TODO: Add DNS-based discovery
137        // TODO: Add peer exchange discovery
138        // TODO: Add DHT-based discovery
139
140        if discovered.is_empty() {
141            warn!("โš ๏ธ  No bootstrap nodes discovered, network may be unreachable");
142        } else {
143            info!("โœ… Discovered {} total bootstrap nodes", discovered.len());
144        }
145
146        Ok(discovered)
147    }
148
149    /// Test connectivity to bootstrap nodes
150    pub async fn test_bootstrap_connectivity(&self) -> Result<Vec<(NetworkAddress, bool)>> {
151        let bootstraps = self.get_bootstrap_addresses();
152        let mut results = Vec::new();
153
154        info!(
155            "๐Ÿงช Testing connectivity to {} bootstrap nodes",
156            bootstraps.len()
157        );
158
159        for addr in bootstraps {
160            let reachable = self.test_single_bootstrap(&addr).await;
161            results.push((addr.clone(), reachable));
162
163            if reachable {
164                debug!("โœ… Bootstrap node reachable: {}", addr);
165            } else {
166                warn!("โŒ Bootstrap node unreachable: {}", addr);
167            }
168        }
169
170        let reachable_count = results.iter().filter(|(_, reachable)| *reachable).count();
171        info!(
172            "๐Ÿ“Š Bootstrap connectivity: {}/{} nodes reachable",
173            reachable_count,
174            results.len()
175        );
176
177        Ok(results)
178    }
179
180    /// Test connectivity to a single bootstrap node
181    async fn test_single_bootstrap(&self, _addr: &NetworkAddress) -> bool {
182        // TODO: Implement actual connectivity test
183        // This would attempt to establish a connection to the bootstrap node
184        // For now, return true as a placeholder
185        true
186    }
187
188    /// Update the hardcoded bootstrap list (for dynamic updates)
189    pub fn update_hardcoded_bootstraps(&mut self, new_bootstraps: HashMap<String, NetworkAddress>) {
190        info!(
191            "๐Ÿ”„ Updating hardcoded bootstrap list with {} entries",
192            new_bootstraps.len()
193        );
194        self.hardcoded_nodes = new_bootstraps;
195    }
196}
197
198impl Default for BootstrapDiscovery {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204/// Bootstrap configuration for different deployment scenarios
205#[derive(Debug, Clone)]
206pub struct BootstrapConfig {
207    /// Enable hardcoded bootstrap discovery
208    pub enable_hardcoded: bool,
209    /// Enable four-word address resolution
210    pub enable_four_words: bool,
211    /// Enable DNS-based discovery
212    pub enable_dns: bool,
213    /// Custom bootstrap addresses
214    pub custom_bootstraps: Vec<NetworkAddress>,
215    /// Fallback behavior when no bootstraps are available
216    pub fallback_behavior: FallbackBehavior,
217}
218
219#[derive(Debug, Clone)]
220pub enum FallbackBehavior {
221    /// Continue without bootstrap (may have limited connectivity)
222    ContinueWithoutBootstrap,
223    /// Retry discovery after a delay
224    RetryAfterDelay(std::time::Duration),
225    /// Fail if no bootstraps available
226    FailIfUnavailable,
227}
228
229impl Default for BootstrapConfig {
230    fn default() -> Self {
231        Self {
232            enable_hardcoded: true,
233            enable_four_words: true,
234            enable_dns: true,
235            custom_bootstraps: Vec::new(),
236            fallback_behavior: FallbackBehavior::RetryAfterDelay(std::time::Duration::from_secs(
237                30,
238            )),
239        }
240    }
241}
242
243/// Enhanced bootstrap discovery with configuration
244pub struct ConfigurableBootstrapDiscovery {
245    discovery: BootstrapDiscovery,
246    config: BootstrapConfig,
247}
248
249impl ConfigurableBootstrapDiscovery {
250    /// Create a new configurable bootstrap discovery
251    pub fn new(config: BootstrapConfig) -> Self {
252        let mut discovery = BootstrapDiscovery::new();
253
254        // Add custom bootstrap nodes
255        for addr in &config.custom_bootstraps {
256            discovery.add_bootstrap(addr.clone());
257        }
258
259        Self { discovery, config }
260    }
261
262    /// Discover bootstrap nodes with configuration options
263    pub async fn discover(&self) -> Result<Vec<NetworkAddress>> {
264        self.discover_internal(0).await
265    }
266
267    /// Internal discovery with retry limit to prevent infinite recursion
268    async fn discover_internal(&self, retry_count: u32) -> Result<Vec<NetworkAddress>> {
269        let mut addresses = Vec::new();
270
271        if self.config.enable_hardcoded {
272            let hardcoded = self.discovery.get_bootstrap_addresses();
273            addresses.extend(hardcoded);
274        }
275
276        // Add custom bootstraps
277        addresses.extend(self.config.custom_bootstraps.clone());
278
279        if addresses.is_empty() && retry_count < 3 {
280            match &self.config.fallback_behavior {
281                FallbackBehavior::ContinueWithoutBootstrap => {
282                    warn!("โš ๏ธ  No bootstrap nodes available, continuing without bootstrap");
283                }
284                FallbackBehavior::RetryAfterDelay(duration) => {
285                    warn!(
286                        "โš ๏ธ  No bootstrap nodes available, retrying after {:?} (attempt {})",
287                        duration,
288                        retry_count + 1
289                    );
290                    tokio::time::sleep(*duration).await;
291                    return Box::pin(self.discover_internal(retry_count + 1)).await;
292                }
293                FallbackBehavior::FailIfUnavailable => {
294                    return Err(anyhow::anyhow!(
295                        "No bootstrap nodes available and fallback disabled"
296                    ));
297                }
298            }
299        }
300
301        Ok(addresses)
302    }
303
304    /// Resolve four-word address if enabled
305    pub fn resolve_four_words(&self, four_words: &str) -> Result<SocketAddr> {
306        if !self.config.enable_four_words {
307            return Err(anyhow::anyhow!("Four-word address resolution disabled"));
308        }
309
310        self.discovery.resolve_four_words(four_words)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_bootstrap_discovery_creation() {
320        let discovery = BootstrapDiscovery::new();
321        let addresses = discovery.get_bootstrap_addresses();
322        assert!(
323            !addresses.is_empty(),
324            "Should have hardcoded bootstrap addresses"
325        );
326    }
327
328    #[test]
329    fn test_four_word_resolution() {
330        let discovery = BootstrapDiscovery::new();
331
332        // Test hardcoded four-word addresses
333        let result = discovery.resolve_four_words("foundation.main.bootstrap");
334        assert!(result.is_ok(), "Should resolve hardcoded four-word address");
335    }
336
337    #[test]
338    fn test_custom_bootstrap_addition() {
339        let mut discovery = BootstrapDiscovery::new();
340        let custom_addr =
341            NetworkAddress::from_ipv4(std::net::Ipv4Addr::new(192, 168, 1, 100), 9000);
342
343        let initial_count = discovery.get_bootstrap_addresses().len();
344        discovery.add_bootstrap(custom_addr.clone());
345        let final_count = discovery.get_bootstrap_addresses().len();
346
347        assert_eq!(
348            final_count,
349            initial_count + 1,
350            "Should add custom bootstrap"
351        );
352        assert!(discovery.get_bootstrap_addresses().contains(&custom_addr));
353    }
354
355    #[tokio::test]
356    async fn test_configurable_discovery() {
357        let config = BootstrapConfig::default();
358        let discovery = ConfigurableBootstrapDiscovery::new(config);
359
360        let addresses = discovery
361            .discover()
362            .await
363            .expect("valid discovery operation");
364        assert!(!addresses.is_empty(), "Should discover bootstrap addresses");
365    }
366
367    #[test]
368    fn test_well_known_addresses() {
369        let discovery = BootstrapDiscovery::new();
370        let four_words = discovery.get_well_known_four_words();
371
372        assert!(four_words.contains(&"foundation.main.bootstrap".to_string()));
373        assert!(four_words.contains(&"global.fast.eagle".to_string()));
374    }
375}