Skip to main content

p2p_foundation/bootstrap/
contact.rs

1//! Contact Entry and Quality Scoring
2//!
3//! Manages peer contact information with comprehensive quality metrics for
4//! intelligent bootstrap peer selection.
5
6use crate::PeerId;
7use std::time::Duration;
8use std::collections::HashMap;
9use serde::{Deserialize, Serialize};
10
11/// A contact entry representing a known peer
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct ContactEntry {
14    /// Unique identifier for this peer
15    pub peer_id: PeerId,
16    /// List of network addresses where this peer can be reached
17    pub addresses: Vec<String>,
18    /// Timestamp when this peer was last seen online
19    pub last_seen: chrono::DateTime<chrono::Utc>,
20    /// Quality metrics for connection performance evaluation
21    pub quality_metrics: QualityMetrics,
22    /// List of capabilities supported by this peer
23    pub capabilities: Vec<String>,
24    /// Whether this peer's IPv6 identity has been verified
25    pub ipv6_identity_verified: bool,
26    /// Overall reputation score (0.0 to 1.0)
27    pub reputation_score: f64,
28    /// Historical connection data for this peer
29    pub connection_history: ConnectionHistory,
30}
31
32/// Quality metrics for peer evaluation
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct QualityMetrics {
35    /// Connection success rate (0.0 to 1.0)
36    pub success_rate: f64,
37    /// Average connection latency in milliseconds
38    pub avg_latency_ms: f64,
39    /// Computed overall quality score (0.0 to 1.0)
40    pub quality_score: f64,
41    /// Timestamp of the last connection attempt
42    pub last_connection_attempt: chrono::DateTime<chrono::Utc>,
43    /// Timestamp of the last successful connection
44    pub last_successful_connection: chrono::DateTime<chrono::Utc>,
45    /// Estimated uptime reliability score (0.0 to 1.0)
46    pub uptime_score: f64,
47}
48
49/// Connection history tracking
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51pub struct ConnectionHistory {
52    /// Total number of connection attempts made
53    pub total_attempts: u64,
54    /// Number of successful connections established
55    pub successful_connections: u64,
56    /// Number of failed connection attempts
57    pub failed_connections: u64,
58    /// Total time spent in successful sessions
59    pub total_session_time: Duration,
60    /// Last 10 latency measurements in milliseconds
61    pub recent_latencies: Vec<u64>,
62    /// Failure reasons and their occurrence counts
63    pub connection_failures: HashMap<String, u64>,
64}
65
66impl ContactEntry {
67    /// Create a new contact entry
68    pub fn new(peer_id: PeerId, addresses: Vec<String>) -> Self {
69        let now = chrono::Utc::now();
70        
71        Self {
72            peer_id,
73            addresses,
74            last_seen: now,
75            quality_metrics: QualityMetrics::new(),
76            capabilities: Vec::new(),
77            ipv6_identity_verified: false,
78            reputation_score: 0.5, // Neutral starting score
79            connection_history: ConnectionHistory::new(),
80        }
81    }
82    
83    /// Update quality metrics based on connection result
84    pub fn update_connection_result(&mut self, success: bool, latency_ms: Option<u64>, error: Option<String>) {
85        let now = chrono::Utc::now();
86        self.last_seen = now;
87        self.quality_metrics.last_connection_attempt = now;
88        self.connection_history.total_attempts += 1;
89        
90        if success {
91            self.connection_history.successful_connections += 1;
92            self.quality_metrics.last_successful_connection = now;
93            
94            if let Some(latency) = latency_ms {
95                self.add_latency_measurement(latency);
96            }
97        } else {
98            self.connection_history.failed_connections += 1;
99            
100            if let Some(err) = error {
101                *self.connection_history.connection_failures.entry(err).or_insert(0) += 1;
102            }
103        }
104        
105        self.update_success_rate();
106        self.update_latency_average();
107        self.recalculate_quality_score();
108    }
109    
110    /// Add a latency measurement
111    fn add_latency_measurement(&mut self, latency_ms: u64) {
112        self.connection_history.recent_latencies.push(latency_ms);
113        
114        // Keep only last 10 measurements
115        if self.connection_history.recent_latencies.len() > 10 {
116            self.connection_history.recent_latencies.remove(0);
117        }
118    }
119    
120    /// Update success rate
121    pub fn update_success_rate(&mut self) {
122        if self.connection_history.total_attempts > 0 {
123            self.quality_metrics.success_rate = 
124                self.connection_history.successful_connections as f64 / 
125                self.connection_history.total_attempts as f64;
126        }
127    }
128    
129    /// Update average latency
130    fn update_latency_average(&mut self) {
131        if !self.connection_history.recent_latencies.is_empty() {
132            let sum: u64 = self.connection_history.recent_latencies.iter().sum();
133            self.quality_metrics.avg_latency_ms = sum as f64 / self.connection_history.recent_latencies.len() as f64;
134        }
135    }
136    
137    /// Recalculate overall quality score
138    pub fn recalculate_quality_score(&mut self) {
139        let quality_calculator = QualityCalculator::new();
140        self.quality_metrics.quality_score = quality_calculator.calculate_quality(self);
141    }
142    
143    /// Update capabilities
144    pub fn update_capabilities(&mut self, capabilities: Vec<String>) {
145        self.capabilities = capabilities;
146        self.recalculate_quality_score();
147    }
148    
149    /// Update reputation score
150    pub fn update_reputation(&mut self, reputation: f64) {
151        self.reputation_score = reputation.clamp(0.0, 1.0);
152        self.recalculate_quality_score();
153    }
154    
155    /// Mark IPv6 identity as verified
156    pub fn mark_ipv6_verified(&mut self) {
157        self.ipv6_identity_verified = true;
158        self.recalculate_quality_score();
159    }
160    
161    /// Check if contact is considered stale
162    pub fn is_stale(&self, max_age: Duration) -> bool {
163        let now = chrono::Utc::now();
164        let age = now.signed_duration_since(self.last_seen);
165        age.to_std().unwrap_or(Duration::MAX) > max_age
166    }
167    
168    /// Get contact age in seconds
169    pub fn age_seconds(&self) -> u64 {
170        let now = chrono::Utc::now();
171        let age = now.signed_duration_since(self.last_seen);
172        age.num_seconds().max(0) as u64
173    }
174    
175    /// Check if contact has essential capabilities
176    pub fn has_capability(&self, capability: &str) -> bool {
177        self.capabilities.contains(&capability.to_string())
178    }
179    
180    /// Get a summary string for debugging
181    pub fn summary(&self) -> String {
182        format!(
183            "Peer {} (Quality: {:.2}, Success: {:.1}%, Latency: {:.0}ms, Verified: {})",
184            self.peer_id.to_string().chars().take(8).collect::<String>(),
185            self.quality_metrics.quality_score,
186            self.quality_metrics.success_rate * 100.0,
187            self.quality_metrics.avg_latency_ms,
188            self.ipv6_identity_verified
189        )
190    }
191}
192
193impl QualityMetrics {
194    /// Create new quality metrics with default values
195    pub fn new() -> Self {
196        let now = chrono::Utc::now();
197        
198        Self {
199            success_rate: 0.0,
200            avg_latency_ms: 0.0,
201            quality_score: 0.0,
202            last_connection_attempt: now,
203            last_successful_connection: now,
204            uptime_score: 0.5, // Neutral starting score
205        }
206    }
207    
208    /// Apply age decay to quality metrics
209    pub fn apply_age_decay(&mut self, decay_factor: f64) {
210        // Decay quality score over time to favor recent connections
211        self.quality_score *= decay_factor;
212        self.uptime_score *= decay_factor;
213    }
214}
215
216impl ConnectionHistory {
217    /// Create new connection history
218    pub fn new() -> Self {
219        Self {
220            total_attempts: 0,
221            successful_connections: 0,
222            failed_connections: 0,
223            total_session_time: Duration::from_secs(0),
224            recent_latencies: Vec::new(),
225            connection_failures: HashMap::new(),
226        }
227    }
228    
229    /// Add session time
230    pub fn add_session_time(&mut self, duration: Duration) {
231        self.total_session_time = self.total_session_time.saturating_add(duration);
232    }
233    
234    /// Get failure rate for specific error type
235    pub fn get_failure_rate(&self, error_type: &str) -> f64 {
236        let failures = self.connection_failures.get(error_type).copied().unwrap_or(0);
237        if self.total_attempts > 0 {
238            failures as f64 / self.total_attempts as f64
239        } else {
240            0.0
241        }
242    }
243}
244
245/// Quality calculator for computing peer scores
246pub struct QualityCalculator {
247    success_weight: f64,
248    latency_weight: f64,
249    recency_weight: f64,
250    reputation_weight: f64,
251    verification_bonus: f64,
252    capability_bonus: f64,
253}
254
255impl QualityCalculator {
256    /// Create new quality calculator with default weights
257    pub fn new() -> Self {
258        Self {
259            success_weight: 0.40,   // 40% - Connection success rate
260            latency_weight: 0.30,   // 30% - Network performance
261            recency_weight: 0.20,   // 20% - How recently seen
262            reputation_weight: 0.10, // 10% - Reputation score
263            verification_bonus: 0.05, // 5% bonus for verified identity
264            capability_bonus: 0.02,  // 2% bonus per important capability
265        }
266    }
267    
268    /// Calculate overall quality score for a contact
269    pub fn calculate_quality(&self, contact: &ContactEntry) -> f64 {
270        let mut score = 0.0;
271        
272        // Success rate component (0.0 to 1.0)
273        let success_component = contact.quality_metrics.success_rate * self.success_weight;
274        score += success_component;
275        
276        // Latency component (inverse of latency, normalized)
277        let latency_component = if contact.quality_metrics.avg_latency_ms > 0.0 {
278            let normalized_latency = (1000.0 / (contact.quality_metrics.avg_latency_ms + 100.0)).min(1.0);
279            normalized_latency * self.latency_weight
280        } else {
281            0.0
282        };
283        score += latency_component;
284        
285        // Recency component (exponential decay)
286        let age_seconds = contact.age_seconds() as f64;
287        let recency_component = (-age_seconds / 86400.0).exp() * self.recency_weight; // 24 hour half-life
288        score += recency_component;
289        
290        // Reputation component
291        let reputation_component = contact.reputation_score * self.reputation_weight;
292        score += reputation_component;
293        
294        // IPv6 verification bonus
295        if contact.ipv6_identity_verified {
296            score += self.verification_bonus;
297        }
298        
299        // Capability bonuses
300        let important_capabilities = ["dht", "mcp", "relay"];
301        let capability_count = important_capabilities.iter()
302            .filter(|&cap| contact.has_capability(cap))
303            .count();
304        score += capability_count as f64 * self.capability_bonus;
305        
306        // Clamp to valid range
307        score.clamp(0.0, 1.0)
308    }
309    
310    /// Calculate quality with custom weights
311    pub fn calculate_with_weights(
312        &self,
313        contact: &ContactEntry,
314        success_weight: f64,
315        latency_weight: f64,
316        recency_weight: f64,
317        reputation_weight: f64,
318    ) -> f64 {
319        let mut calculator = self.clone();
320        calculator.success_weight = success_weight;
321        calculator.latency_weight = latency_weight;
322        calculator.recency_weight = recency_weight;
323        calculator.reputation_weight = reputation_weight;
324        
325        calculator.calculate_quality(contact)
326    }
327}
328
329impl Clone for QualityCalculator {
330    fn clone(&self) -> Self {
331        Self {
332            success_weight: self.success_weight,
333            latency_weight: self.latency_weight,
334            recency_weight: self.recency_weight,
335            reputation_weight: self.reputation_weight,
336            verification_bonus: self.verification_bonus,
337            capability_bonus: self.capability_bonus,
338        }
339    }
340}
341
342impl Default for QualityCalculator {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_contact_entry_creation() {
354        let peer_id = PeerId::from("test-peer");
355        let addresses = vec!["/ip4/127.0.0.1/tcp/9000".to_string()];
356        
357        let contact = ContactEntry::new(peer_id.clone(), addresses.clone());
358        
359        assert_eq!(contact.peer_id, peer_id);
360        assert_eq!(contact.addresses, addresses);
361        assert_eq!(contact.quality_metrics.success_rate, 0.0);
362        assert!(!contact.ipv6_identity_verified);
363    }
364    
365    #[test]
366    fn test_quality_calculation() {
367        let mut contact = ContactEntry::new(
368            PeerId::from("test-peer"),
369            vec!["/ip4/127.0.0.1/tcp/9000".to_string()]
370        );
371        
372        // Simulate successful connections
373        contact.update_connection_result(true, Some(50), None);
374        contact.update_connection_result(true, Some(60), None);
375        contact.update_connection_result(false, None, Some("timeout".to_string()));
376        
377        assert!(contact.quality_metrics.success_rate > 0.5);
378        assert!(contact.quality_metrics.avg_latency_ms > 0.0);
379        assert!(contact.quality_metrics.quality_score > 0.0);
380    }
381    
382    #[test]
383    fn test_capability_bonus() {
384        let mut contact = ContactEntry::new(
385            PeerId::from("test-peer"),
386            vec!["/ip4/127.0.0.1/tcp/9000".to_string()]
387        );
388        
389        let initial_score = contact.quality_metrics.quality_score;
390        
391        contact.update_capabilities(vec!["dht".to_string(), "mcp".to_string()]);
392        
393        assert!(contact.quality_metrics.quality_score > initial_score);
394    }
395    
396    #[test]
397    fn test_stale_detection() {
398        let mut contact = ContactEntry::new(
399            PeerId::from("test-peer"),
400            vec!["/ip4/127.0.0.1/tcp/9000".to_string()]
401        );
402        
403        // Set last seen to 2 hours ago
404        contact.last_seen = chrono::Utc::now() - chrono::Duration::hours(2);
405        
406        assert!(contact.is_stale(Duration::from_secs(3600))); // 1 hour threshold
407        assert!(!contact.is_stale(Duration::from_secs(10800))); // 3 hour threshold
408    }
409    
410    #[test]
411    fn test_quality_decay() {
412        let mut metrics = QualityMetrics::new();
413        metrics.quality_score = 0.8;
414        metrics.uptime_score = 0.9;
415        
416        metrics.apply_age_decay(0.9);
417        
418        assert!(metrics.quality_score < 0.8);
419        assert!(metrics.uptime_score < 0.9);
420    }
421}