saorsa_core/bootstrap/
mod.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: saorsalabs@gmail.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 Cache System
15//!
16//! Provides decentralized peer discovery through local caching of known contacts.
17//! Eliminates dependency on central bootstrap servers by maintaining a high-quality
18//! cache of up to 30,000 peer contacts with automatic conflict resolution for
19//! multiple concurrent instances.
20
21pub mod cache;
22pub mod contact;
23pub mod discovery;
24pub mod merge;
25
26pub use cache::{BootstrapCache, CacheConfig, CacheError};
27pub use contact::{
28    ContactEntry, QualityCalculator, QualityMetrics, QuicConnectionType, QuicContactInfo,
29    QuicQualityMetrics,
30};
31pub use discovery::{BootstrapConfig, BootstrapDiscovery, ConfigurableBootstrapDiscovery};
32pub use merge::{MergeCoordinator, MergeResult};
33// Use real four-word-networking crate types behind a thin facade
34pub use four_word_networking as fourwords;
35
36/// Minimal facade around external four-word types
37#[derive(Debug, Clone)]
38pub struct FourWordAddress(pub String);
39
40impl FourWordAddress {
41    pub fn from_string(s: &str) -> Result<Self> {
42        let parts: Vec<&str> = s.split(['.', '-']).collect();
43        if parts.len() != 4 {
44            return Err(P2PError::Bootstrap(
45                crate::error::BootstrapError::InvalidData(
46                    "Four-word address must have exactly 4 words"
47                        .to_string()
48                        .into(),
49                ),
50            ));
51        }
52        Ok(FourWordAddress(parts.join("-")))
53    }
54
55    pub fn validate(&self, _encoder: &WordEncoder) -> bool {
56        let parts: Vec<&str> = self.0.split(['.', '-']).collect();
57        parts.len() == 4 && parts.iter().all(|part| !part.is_empty())
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct WordDictionary;
63
64#[derive(Debug, Clone)]
65pub struct WordEncoder;
66
67impl Default for WordEncoder {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl WordEncoder {
74    pub fn new() -> Self {
75        Self
76    }
77
78    pub fn encode_multiaddr_string(&self, multiaddr: &str) -> Result<FourWordAddress> {
79        // Map multiaddr to IPv4:port if possible, else hash deterministically
80        let socket_addr: std::net::SocketAddr = multiaddr.parse().map_err(|e| {
81            P2PError::Bootstrap(crate::error::BootstrapError::InvalidData(
82                format!("{e}").into(),
83            ))
84        })?;
85        self.encode_socket_addr(&socket_addr)
86    }
87
88    pub fn decode_to_socket_addr(&self, words: &FourWordAddress) -> Result<std::net::SocketAddr> {
89        let parts: Vec<&str> = words.0.split(['.', '-']).collect();
90        if parts.len() != 4 {
91            return Err(P2PError::Bootstrap(
92                crate::error::BootstrapError::InvalidData(
93                    "Invalid four-word address".to_string().into(),
94                ),
95            ));
96        }
97        let encoding = four_word_networking::FourWordEncoding::new(
98            parts[0].to_string(),
99            parts[1].to_string(),
100            parts[2].to_string(),
101            parts[3].to_string(),
102        );
103        if let Ok((ip, port)) = four_word_networking::FourWordEncoder::new().decode_ipv4(&encoding)
104        {
105            Ok(std::net::SocketAddr::from((ip, port)))
106        } else {
107            Err(P2PError::Bootstrap(
108                crate::error::BootstrapError::InvalidData(
109                    "Decoding four-word address failed".to_string().into(),
110                ),
111            ))
112        }
113    }
114
115    pub fn encode_socket_addr(&self, addr: &std::net::SocketAddr) -> Result<FourWordAddress> {
116        match addr {
117            std::net::SocketAddr::V4(v4) => {
118                let enc = four_word_networking::FourWordEncoder::new()
119                    .encode_ipv4(*v4.ip(), v4.port())
120                    .map_err(|e| {
121                        P2PError::Bootstrap(crate::error::BootstrapError::InvalidData(
122                            format!("{e}").into(),
123                        ))
124                    })?;
125                Ok(FourWordAddress(enc.to_string().replace(' ', "-")))
126            }
127            std::net::SocketAddr::V6(_) => Err(P2PError::Bootstrap(
128                crate::error::BootstrapError::InvalidData(
129                    "IPv6 not supported by four-word encoder".to_string().into(),
130                ),
131            )),
132        }
133    }
134}
135
136use crate::error::BootstrapError;
137use crate::{P2PError, PeerId, Result};
138use std::path::PathBuf;
139use std::time::Duration;
140
141/// Default cache configuration
142pub const DEFAULT_MAX_CONTACTS: usize = 30_000;
143/// Default directory for storing bootstrap cache files
144pub const DEFAULT_CACHE_DIR: &str = ".cache/p2p_foundation";
145/// Default interval for merging instance cache files
146pub const DEFAULT_MERGE_INTERVAL: Duration = Duration::from_secs(30);
147/// Default interval for cleaning up stale contacts (1 hour)
148pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(3600);
149/// Default interval for updating contact quality scores (5 minutes)
150pub const DEFAULT_QUALITY_UPDATE_INTERVAL: Duration = Duration::from_secs(300);
151
152/// Bootstrap cache initialization and management
153pub struct BootstrapManager {
154    cache: BootstrapCache,
155    merge_coordinator: MergeCoordinator,
156    word_encoder: WordEncoder,
157}
158
159impl BootstrapManager {
160    /// Create a new bootstrap manager with default configuration
161    pub async fn new() -> Result<Self> {
162        let cache_dir = home_cache_dir()?;
163        let config = CacheConfig::default();
164
165        let cache = BootstrapCache::new(cache_dir.clone(), config).await?;
166        let merge_coordinator = MergeCoordinator::new(cache_dir)?;
167        let word_encoder = WordEncoder::new();
168
169        Ok(Self {
170            cache,
171            merge_coordinator,
172            word_encoder,
173        })
174    }
175
176    /// Create a new bootstrap manager with custom configuration
177    pub async fn with_config(config: CacheConfig) -> Result<Self> {
178        let cache_dir = home_cache_dir()?;
179
180        let cache = BootstrapCache::new(cache_dir.clone(), config).await?;
181        let merge_coordinator = MergeCoordinator::new(cache_dir)?;
182        let word_encoder = WordEncoder::new();
183
184        Ok(Self {
185            cache,
186            merge_coordinator,
187            word_encoder,
188        })
189    }
190
191    /// Get bootstrap peers for initial connection
192    pub async fn get_bootstrap_peers(&self, count: usize) -> Result<Vec<ContactEntry>> {
193        self.cache.get_bootstrap_peers(count).await
194    }
195
196    /// Add a discovered peer to the cache
197    pub async fn add_contact(&mut self, contact: ContactEntry) -> Result<()> {
198        self.cache.add_contact(contact).await
199    }
200
201    /// Update contact performance metrics
202    pub async fn update_contact_metrics(
203        &mut self,
204        peer_id: &PeerId,
205        metrics: QualityMetrics,
206    ) -> Result<()> {
207        self.cache.update_contact_metrics(peer_id, metrics).await
208    }
209
210    /// Start background maintenance tasks
211    pub async fn start_background_tasks(&mut self) -> Result<()> {
212        // Start periodic merge of instance caches
213        let cache_clone = self.cache.clone();
214        let merge_coordinator = self.merge_coordinator.clone();
215
216        tokio::spawn(async move {
217            let mut interval = tokio::time::interval(DEFAULT_MERGE_INTERVAL);
218            loop {
219                interval.tick().await;
220                if let Err(e) = merge_coordinator.merge_instance_caches(&cache_clone).await {
221                    tracing::warn!("Failed to merge instance caches: {}", e);
222                }
223            }
224        });
225
226        // Start quality score updates
227        let cache_clone = self.cache.clone();
228        tokio::spawn(async move {
229            let mut interval = tokio::time::interval(DEFAULT_QUALITY_UPDATE_INTERVAL);
230            loop {
231                interval.tick().await;
232                if let Err(e) = cache_clone.update_quality_scores().await {
233                    tracing::warn!("Failed to update quality scores: {}", e);
234                }
235            }
236        });
237
238        // Start cleanup task
239        let cache_clone = self.cache.clone();
240        tokio::spawn(async move {
241            let mut interval = tokio::time::interval(DEFAULT_CLEANUP_INTERVAL);
242            loop {
243                interval.tick().await;
244                if let Err(e) = cache_clone.cleanup_stale_entries().await {
245                    tracing::warn!("Failed to cleanup stale entries: {}", e);
246                }
247            }
248        });
249
250        Ok(())
251    }
252
253    /// Get cache statistics
254    pub async fn get_stats(&self) -> Result<CacheStats> {
255        self.cache.get_stats().await
256    }
257
258    /// Force a cache merge operation
259    pub async fn force_merge(&self) -> Result<MergeResult> {
260        self.merge_coordinator
261            .merge_instance_caches(&self.cache)
262            .await
263    }
264
265    /// Convert socket address to four-word address
266    pub fn encode_address(&self, socket_addr: &std::net::SocketAddr) -> Result<FourWordAddress> {
267        self.word_encoder
268            .encode_socket_addr(socket_addr)
269            .map_err(|e| {
270                crate::P2PError::Bootstrap(crate::error::BootstrapError::InvalidData(
271                    format!("Failed to encode socket address: {e}").into(),
272                ))
273            })
274    }
275
276    /// Convert four-word address to socket address
277    pub fn decode_address(&self, words: &FourWordAddress) -> Result<std::net::SocketAddr> {
278        self.word_encoder.decode_to_socket_addr(words).map_err(|e| {
279            crate::P2PError::Bootstrap(crate::error::BootstrapError::InvalidData(
280                format!("Failed to decode four-word address: {e}").into(),
281            ))
282        })
283    }
284
285    /// Validate four-word address format
286    pub fn validate_words(&self, words: &FourWordAddress) -> Result<()> {
287        if words.validate(&self.word_encoder) {
288            Ok(())
289        } else {
290            Err(crate::P2PError::Bootstrap(
291                crate::error::BootstrapError::InvalidData(
292                    "Invalid four-word address format".to_string().into(),
293                ),
294            ))
295        }
296    }
297
298    /// Get the word encoder for direct access
299    pub fn word_encoder(&self) -> &WordEncoder {
300        &self.word_encoder
301    }
302
303    /// Get well-known bootstrap addresses as four-word addresses
304    pub fn get_well_known_word_addresses(&self) -> Vec<(FourWordAddress, std::net::SocketAddr)> {
305        let well_known_addrs = vec![
306            // Primary bootstrap nodes with well-known addresses
307            std::net::SocketAddr::from(([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888], 9000)),
308            std::net::SocketAddr::from(([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8844], 9001)),
309            std::net::SocketAddr::from(([0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111], 9002)),
310        ];
311
312        well_known_addrs
313            .into_iter()
314            .filter_map(|socket_addr| {
315                if let Ok(words) = self.encode_address(&socket_addr) {
316                    Some((words, socket_addr))
317                } else {
318                    None
319                }
320            })
321            .collect()
322    }
323}
324
325/// Cache statistics for monitoring
326#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
327pub struct CacheStats {
328    /// Total number of contacts in the cache
329    pub total_contacts: usize,
330    /// Number of contacts with high quality scores
331    pub high_quality_contacts: usize,
332    /// Number of contacts with verified IPv6 identity
333    pub verified_contacts: usize,
334    /// Timestamp of the last cache merge operation
335    pub last_merge: chrono::DateTime<chrono::Utc>,
336    /// Timestamp of the last cache cleanup operation
337    pub last_cleanup: chrono::DateTime<chrono::Utc>,
338    /// Cache hit rate for peer discovery operations
339    pub cache_hit_rate: f64,
340    /// Average quality score across all contacts
341    pub average_quality_score: f64,
342
343    // QUIC-specific statistics
344    /// Number of contacts with QUIC networking support
345    pub iroh_contacts: usize,
346    /// Number of contacts with successful NAT traversal (deprecated)
347    pub nat_traversal_contacts: usize,
348    /// Average QUIC connection setup time (milliseconds)
349    pub avg_iroh_setup_time_ms: f64,
350    /// Most successful QUIC connection type
351    pub preferred_iroh_connection_type: Option<String>,
352}
353
354/// Get the home cache directory
355fn home_cache_dir() -> Result<PathBuf> {
356    let home = std::env::var("HOME")
357        .or_else(|_| std::env::var("USERPROFILE"))
358        .map_err(|_| {
359            P2PError::Bootstrap(BootstrapError::CacheError(
360                "Unable to determine home directory".to_string().into(),
361            ))
362        })?;
363
364    let cache_dir = PathBuf::from(home).join(DEFAULT_CACHE_DIR);
365
366    // Ensure cache directory exists
367    std::fs::create_dir_all(&cache_dir).map_err(|e| {
368        P2PError::Bootstrap(BootstrapError::CacheError(
369            format!("Failed to create cache directory: {e}").into(),
370        ))
371    })?;
372
373    Ok(cache_dir)
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use tempfile::TempDir;
380
381    #[tokio::test]
382    async fn test_bootstrap_manager_creation() {
383        let temp_dir = TempDir::new().unwrap();
384        let config = CacheConfig {
385            cache_dir: temp_dir.path().to_path_buf(),
386            max_contacts: 1000,
387            ..CacheConfig::default()
388        };
389
390        let manager = BootstrapManager::with_config(config).await;
391        assert!(manager.is_ok());
392    }
393
394    #[tokio::test]
395    async fn test_home_cache_dir() {
396        let result = home_cache_dir();
397        assert!(result.is_ok());
398
399        let path = result.unwrap();
400        assert!(path.exists());
401        assert!(path.is_dir());
402    }
403}