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;
35
36#[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 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
141pub const DEFAULT_MAX_CONTACTS: usize = 30_000;
143pub const DEFAULT_CACHE_DIR: &str = ".cache/p2p_foundation";
145pub const DEFAULT_MERGE_INTERVAL: Duration = Duration::from_secs(30);
147pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(3600);
149pub const DEFAULT_QUALITY_UPDATE_INTERVAL: Duration = Duration::from_secs(300);
151
152pub struct BootstrapManager {
154 cache: BootstrapCache,
155 merge_coordinator: MergeCoordinator,
156 word_encoder: WordEncoder,
157}
158
159impl BootstrapManager {
160 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 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 pub async fn get_bootstrap_peers(&self, count: usize) -> Result<Vec<ContactEntry>> {
193 self.cache.get_bootstrap_peers(count).await
194 }
195
196 pub async fn add_contact(&mut self, contact: ContactEntry) -> Result<()> {
198 self.cache.add_contact(contact).await
199 }
200
201 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 pub async fn start_background_tasks(&mut self) -> Result<()> {
212 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 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 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 pub async fn get_stats(&self) -> Result<CacheStats> {
255 self.cache.get_stats().await
256 }
257
258 pub async fn force_merge(&self) -> Result<MergeResult> {
260 self.merge_coordinator
261 .merge_instance_caches(&self.cache)
262 .await
263 }
264
265 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 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 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 pub fn word_encoder(&self) -> &WordEncoder {
300 &self.word_encoder
301 }
302
303 pub fn get_well_known_word_addresses(&self) -> Vec<(FourWordAddress, std::net::SocketAddr)> {
305 let well_known_addrs = vec![
306 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
327pub struct CacheStats {
328 pub total_contacts: usize,
330 pub high_quality_contacts: usize,
332 pub verified_contacts: usize,
334 pub last_merge: chrono::DateTime<chrono::Utc>,
336 pub last_cleanup: chrono::DateTime<chrono::Utc>,
338 pub cache_hit_rate: f64,
340 pub average_quality_score: f64,
342
343 pub iroh_contacts: usize,
346 pub nat_traversal_contacts: usize,
348 pub avg_iroh_setup_time_ms: f64,
350 pub preferred_iroh_connection_type: Option<String>,
352}
353
354fn 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 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}