Skip to main content

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: 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 Cache System
15//!
16//! Provides decentralized peer discovery through local caching of known contacts.
17//! Uses ant-quic's BootstrapCache internally with additional Sybil protection
18//! via rate limiting and IP diversity enforcement.
19
20pub mod contact;
21pub mod manager;
22
23// Re-export the primary BootstrapManager (wraps ant-quic)
24pub use manager::BootstrapManager;
25pub use manager::{
26    BootstrapConfig, BootstrapStats, CacheConfig, DEFAULT_CLEANUP_INTERVAL, DEFAULT_MAX_CONTACTS,
27    DEFAULT_MERGE_INTERVAL, DEFAULT_QUALITY_UPDATE_INTERVAL,
28};
29
30// Re-export contact types
31pub use contact::{
32    ConnectionHistory, ContactEntry, QualityCalculator, QualityMetrics, QuicConnectionType,
33    QuicContactInfo, QuicQualityMetrics,
34};
35
36// Four-word address encoding (via four-word-networking crate)
37pub use four_word_networking as fourwords;
38use four_word_networking::FourWordAdaptiveEncoder;
39
40use crate::error::BootstrapError;
41use crate::{P2PError, Result};
42use std::net::IpAddr;
43use std::net::Ipv6Addr;
44use std::path::PathBuf;
45
46/// Default directory for storing bootstrap cache files
47pub const DEFAULT_CACHE_DIR: &str = ".cache/saorsa";
48
49/// Minimal facade around external four-word types
50#[derive(Debug, Clone)]
51pub struct FourWordAddress(pub String);
52
53impl FourWordAddress {
54    pub fn from_string(s: &str) -> Result<Self> {
55        let parts: Vec<&str> = s.split(['.', '-']).collect();
56        if parts.len() != 4 {
57            return Err(P2PError::Bootstrap(BootstrapError::InvalidData(
58                "Four-word address must have exactly 4 words"
59                    .to_string()
60                    .into(),
61            )));
62        }
63        Ok(FourWordAddress(parts.join("-")))
64    }
65
66    pub fn validate(&self, _encoder: &WordEncoder) -> bool {
67        let parts: Vec<&str> = self.0.split(['.', '-']).collect();
68        parts.len() == 4 && parts.iter().all(|part| !part.is_empty())
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct WordDictionary;
74
75#[derive(Debug, Clone)]
76pub struct WordEncoder;
77
78impl Default for WordEncoder {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl WordEncoder {
85    pub fn new() -> Self {
86        Self
87    }
88
89    pub fn encode_multiaddr_string(&self, multiaddr: &str) -> Result<FourWordAddress> {
90        let socket_addr: std::net::SocketAddr = multiaddr
91            .parse()
92            .map_err(|e| P2PError::Bootstrap(BootstrapError::InvalidData(format!("{e}").into())))?;
93        self.encode_socket_addr(&socket_addr)
94    }
95
96    pub fn decode_to_socket_addr(&self, words: &FourWordAddress) -> Result<std::net::SocketAddr> {
97        let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
98            P2PError::Bootstrap(BootstrapError::InvalidData(
99                format!("Encoder init failed: {e}").into(),
100            ))
101        })?;
102        let normalized = words.0.replace(' ', "-");
103        let decoded = encoder.decode(&normalized).map_err(|e| {
104            P2PError::Bootstrap(BootstrapError::InvalidData(
105                format!("Failed to decode four-word address: {e}").into(),
106            ))
107        })?;
108        decoded.parse::<std::net::SocketAddr>().map_err(|_| {
109            P2PError::Bootstrap(BootstrapError::InvalidData(
110                "Decoded address missing port".to_string().into(),
111            ))
112        })
113    }
114
115    pub fn encode_socket_addr(&self, addr: &std::net::SocketAddr) -> Result<FourWordAddress> {
116        let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
117            P2PError::Bootstrap(BootstrapError::InvalidData(
118                format!("Encoder init failed: {e}").into(),
119            ))
120        })?;
121        let encoded = encoder
122            .encode(&addr.to_string())
123            .map_err(|e| P2PError::Bootstrap(BootstrapError::InvalidData(format!("{e}").into())))?;
124        Ok(FourWordAddress(encoded.replace(' ', "-")))
125    }
126}
127
128/// Cache statistics for monitoring
129#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
130pub struct CacheStats {
131    /// Total number of contacts in the cache
132    pub total_contacts: usize,
133    /// Number of contacts with high quality scores
134    pub high_quality_contacts: usize,
135    /// Number of contacts with verified IPv6 identity
136    pub verified_contacts: usize,
137    /// Timestamp of the last cache merge operation
138    pub last_merge: chrono::DateTime<chrono::Utc>,
139    /// Timestamp of the last cache cleanup operation
140    pub last_cleanup: chrono::DateTime<chrono::Utc>,
141    /// Cache hit rate for peer discovery operations
142    pub cache_hit_rate: f64,
143    /// Average quality score across all contacts
144    pub average_quality_score: f64,
145
146    // QUIC-specific statistics
147    /// Number of contacts with QUIC networking support
148    pub iroh_contacts: usize,
149    /// Number of contacts with successful NAT traversal
150    pub nat_traversal_contacts: usize,
151    /// Average QUIC connection setup time (milliseconds)
152    pub avg_iroh_setup_time_ms: f64,
153    /// Most successful QUIC connection type
154    pub preferred_iroh_connection_type: Option<String>,
155}
156
157/// Convert an IP address to IPv6
158///
159/// IPv4 addresses are converted to IPv6-mapped format (::ffff:a.b.c.d)
160/// IPv6 addresses are returned as-is
161pub fn ip_to_ipv6(ip: &IpAddr) -> Ipv6Addr {
162    match ip {
163        IpAddr::V4(ipv4) => ipv4.to_ipv6_mapped(),
164        IpAddr::V6(ipv6) => *ipv6,
165    }
166}
167
168/// Get the home cache directory
169pub fn home_cache_dir() -> Result<PathBuf> {
170    let home = std::env::var("HOME")
171        .or_else(|_| std::env::var("USERPROFILE"))
172        .map_err(|_| {
173            P2PError::Bootstrap(BootstrapError::CacheError(
174                "Unable to determine home directory".to_string().into(),
175            ))
176        })?;
177
178    let cache_dir = PathBuf::from(home).join(DEFAULT_CACHE_DIR);
179
180    // Ensure cache directory exists
181    std::fs::create_dir_all(&cache_dir).map_err(|e| {
182        P2PError::Bootstrap(BootstrapError::CacheError(
183            format!("Failed to create cache directory: {e}").into(),
184        ))
185    })?;
186
187    Ok(cache_dir)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::rate_limit::JoinRateLimiterConfig;
194    use crate::security::IPDiversityConfig;
195    use tempfile::TempDir;
196
197    #[tokio::test]
198    async fn test_bootstrap_manager_creation() {
199        let temp_dir = TempDir::new().unwrap();
200        let config = CacheConfig {
201            cache_dir: temp_dir.path().to_path_buf(),
202            max_contacts: 1000,
203            ..CacheConfig::default()
204        };
205
206        let manager = BootstrapManager::with_full_config(
207            config,
208            JoinRateLimiterConfig::default(),
209            IPDiversityConfig::default(),
210        )
211        .await;
212        assert!(manager.is_ok());
213    }
214
215    #[tokio::test]
216    async fn test_home_cache_dir() {
217        let result = home_cache_dir();
218        assert!(result.is_ok());
219
220        let path = result.unwrap();
221        assert!(path.exists());
222        assert!(path.is_dir());
223    }
224}