1pub 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};
33pub use four_word_networking as fourwords;
35use four_word_networking::FourWordAdaptiveEncoder;
36
37#[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 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 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
129pub const DEFAULT_MAX_CONTACTS: usize = 30_000;
131pub const DEFAULT_CACHE_DIR: &str = ".cache/p2p_foundation";
133pub const DEFAULT_MERGE_INTERVAL: Duration = Duration::from_secs(30);
135pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(3600);
137pub const DEFAULT_QUALITY_UPDATE_INTERVAL: Duration = Duration::from_secs(300);
139
140pub struct BootstrapManager {
142 cache: BootstrapCache,
143 merge_coordinator: MergeCoordinator,
144 word_encoder: WordEncoder,
145}
146
147impl BootstrapManager {
148 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 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 pub async fn get_bootstrap_peers(&self, count: usize) -> Result<Vec<ContactEntry>> {
181 self.cache.get_bootstrap_peers(count).await
182 }
183
184 pub async fn add_contact(&mut self, contact: ContactEntry) -> Result<()> {
186 self.cache.add_contact(contact).await
187 }
188
189 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 pub async fn start_background_tasks(&mut self) -> Result<()> {
200 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 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 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 pub async fn get_stats(&self) -> Result<CacheStats> {
243 self.cache.get_stats().await
244 }
245
246 pub async fn force_merge(&self) -> Result<MergeResult> {
248 self.merge_coordinator
249 .merge_instance_caches(&self.cache)
250 .await
251 }
252
253 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 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 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 pub fn word_encoder(&self) -> &WordEncoder {
288 &self.word_encoder
289 }
290
291 pub fn get_well_known_word_addresses(&self) -> Vec<(FourWordAddress, std::net::SocketAddr)> {
293 let well_known_addrs = vec![
294 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
315pub struct CacheStats {
316 pub total_contacts: usize,
318 pub high_quality_contacts: usize,
320 pub verified_contacts: usize,
322 pub last_merge: chrono::DateTime<chrono::Utc>,
324 pub last_cleanup: chrono::DateTime<chrono::Utc>,
326 pub cache_hit_rate: f64,
328 pub average_quality_score: f64,
330
331 pub iroh_contacts: usize,
334 pub nat_traversal_contacts: usize,
336 pub avg_iroh_setup_time_ms: f64,
338 pub preferred_iroh_connection_type: Option<String>,
340}
341
342fn 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 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}