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