Skip to main content

saorsa_core/bootstrap/
contact.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//! Contact Entry and Quality Scoring
15//!
16//! Manages peer contact information with comprehensive quality metrics for
17//! intelligent bootstrap peer selection.
18
19use crate::PeerId;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::net::SocketAddr;
23use std::time::Duration;
24
25/// QUIC-specific contact information for direct connectivity
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct QuicContactInfo {
28    /// Direct socket addresses that can be used to reach this peer
29    pub direct_addresses: Vec<SocketAddr>,
30    /// Quality information about QUIC connections
31    pub quic_quality: QuicQualityMetrics,
32    /// Last successful QUIC connection timestamp
33    pub last_quic_connection: chrono::DateTime<chrono::Utc>,
34    /// Connection types that have been successful with this peer
35    pub successful_connection_types: Vec<QuicConnectionType>,
36}
37
38/// Quality metrics specific to QUIC connections
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40pub struct QuicQualityMetrics {
41    /// Average response time in milliseconds
42    pub avg_response_time_ms: f64,
43    /// Average throughput in Mbps
44    pub avg_throughput_mbps: f64,
45    /// Connection success rate (0.0 to 1.0)
46    pub connection_success_rate: f64,
47    /// Average time to establish QUIC connection (milliseconds)
48    pub avg_connection_setup_time_ms: f64,
49    /// Success rate for different connection types
50    pub connection_type_success_rates: HashMap<QuicConnectionType, f64>,
51}
52
53/// Types of connections that QUIC can establish
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
55pub enum QuicConnectionType {
56    /// Direct IPv4 connection
57    DirectIPv4,
58    /// Direct IPv6 connection
59    DirectIPv6,
60}
61
62/// A contact entry representing a known peer
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub struct ContactEntry {
65    /// Unique identifier for this peer
66    pub peer_id: PeerId,
67    /// List of socket addresses where this peer can be reached
68    pub addresses: Vec<SocketAddr>,
69    /// Timestamp when this peer was last seen online
70    pub last_seen: chrono::DateTime<chrono::Utc>,
71    /// Quality metrics for connection performance evaluation
72    pub quality_metrics: QualityMetrics,
73    /// List of capabilities supported by this peer
74    pub capabilities: Vec<String>,
75    /// Whether this peer's IPv6 identity has been verified
76    pub ipv6_identity_verified: bool,
77    /// Overall reputation score (0.0 to 1.0)
78    pub reputation_score: f64,
79    /// Historical connection data for this peer
80    pub connection_history: ConnectionHistory,
81
82    // QUIC-specific contact information
83    /// QUIC contact data for direct connectivity
84    pub quic_contact: Option<QuicContactInfo>,
85}
86
87/// Quality metrics for peer evaluation
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct QualityMetrics {
90    /// Connection success rate (0.0 to 1.0)
91    pub success_rate: f64,
92    /// Average connection latency in milliseconds
93    pub avg_latency_ms: f64,
94    /// Computed overall quality score (0.0 to 1.0)
95    pub quality_score: f64,
96    /// Timestamp of the last connection attempt
97    pub last_connection_attempt: chrono::DateTime<chrono::Utc>,
98    /// Timestamp of the last successful connection
99    pub last_successful_connection: chrono::DateTime<chrono::Utc>,
100    /// Estimated uptime reliability score (0.0 to 1.0)
101    pub uptime_score: f64,
102}
103
104/// Connection history tracking
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106pub struct ConnectionHistory {
107    /// Total number of connection attempts made
108    pub total_attempts: u64,
109    /// Number of successful connections established
110    pub successful_connections: u64,
111    /// Number of failed connection attempts
112    pub failed_connections: u64,
113    /// Total time spent in successful sessions
114    pub total_session_time: Duration,
115    /// Last 10 latency measurements in milliseconds
116    pub recent_latencies: Vec<u64>,
117    /// Failure reasons and their occurrence counts
118    pub connection_failures: HashMap<String, u64>,
119}
120
121impl ContactEntry {
122    /// Create a new contact entry
123    pub fn new(peer_id: PeerId, addresses: Vec<SocketAddr>) -> Self {
124        let now = chrono::Utc::now();
125
126        Self {
127            peer_id,
128            addresses,
129            last_seen: now,
130            quality_metrics: QualityMetrics::new(),
131            capabilities: Vec::new(),
132            ipv6_identity_verified: false,
133            reputation_score: 0.5, // Neutral starting score
134            connection_history: ConnectionHistory::new(),
135            quic_contact: None, // No QUIC contact info initially
136        }
137    }
138
139    /// Create a new contact entry with QUIC information
140    pub fn new_with_quic(
141        peer_id: PeerId,
142        addresses: Vec<SocketAddr>,
143        quic_info: QuicContactInfo,
144    ) -> Self {
145        let now = chrono::Utc::now();
146
147        Self {
148            peer_id,
149            addresses,
150            last_seen: now,
151            quality_metrics: QualityMetrics::new(),
152            capabilities: Vec::new(),
153            ipv6_identity_verified: false,
154            reputation_score: 0.5, // Neutral starting score
155            connection_history: ConnectionHistory::new(),
156            quic_contact: Some(quic_info),
157        }
158    }
159
160    /// Update quality metrics based on connection result
161    pub fn update_connection_result(
162        &mut self,
163        success: bool,
164        latency_ms: Option<u64>,
165        error: Option<String>,
166    ) {
167        let now = chrono::Utc::now();
168        self.last_seen = now;
169        self.quality_metrics.last_connection_attempt = now;
170        self.connection_history.total_attempts += 1;
171
172        if success {
173            self.connection_history.successful_connections += 1;
174            self.quality_metrics.last_successful_connection = now;
175
176            if let Some(latency) = latency_ms {
177                self.add_latency_measurement(latency);
178            }
179        } else {
180            self.connection_history.failed_connections += 1;
181
182            if let Some(err) = error {
183                *self
184                    .connection_history
185                    .connection_failures
186                    .entry(err)
187                    .or_insert(0) += 1;
188            }
189        }
190
191        self.update_success_rate();
192        self.update_latency_average();
193        self.recalculate_quality_score();
194    }
195
196    /// Add a latency measurement
197    fn add_latency_measurement(&mut self, latency_ms: u64) {
198        self.connection_history.recent_latencies.push(latency_ms);
199
200        // Keep only last 10 measurements
201        if self.connection_history.recent_latencies.len() > 10 {
202            self.connection_history.recent_latencies.remove(0);
203        }
204    }
205
206    /// Update success rate
207    pub fn update_success_rate(&mut self) {
208        if self.connection_history.total_attempts > 0 {
209            self.quality_metrics.success_rate = self.connection_history.successful_connections
210                as f64
211                / self.connection_history.total_attempts as f64;
212        }
213    }
214
215    /// Update average latency
216    fn update_latency_average(&mut self) {
217        if !self.connection_history.recent_latencies.is_empty() {
218            let sum: u64 = self.connection_history.recent_latencies.iter().sum();
219            self.quality_metrics.avg_latency_ms =
220                sum as f64 / self.connection_history.recent_latencies.len() as f64;
221        }
222    }
223
224    /// Recalculate overall quality score
225    pub fn recalculate_quality_score(&mut self) {
226        let quality_calculator = QualityCalculator::new();
227        self.quality_metrics.quality_score = quality_calculator.calculate_quality(self);
228    }
229
230    /// Update capabilities
231    pub fn update_capabilities(&mut self, capabilities: Vec<String>) {
232        self.capabilities = capabilities;
233        self.recalculate_quality_score();
234    }
235
236    /// Update reputation score
237    pub fn update_reputation(&mut self, reputation: f64) {
238        self.reputation_score = reputation.clamp(0.0, 1.0);
239        self.recalculate_quality_score();
240    }
241
242    /// Mark IPv6 identity as verified
243    pub fn mark_ipv6_verified(&mut self) {
244        self.ipv6_identity_verified = true;
245        self.recalculate_quality_score();
246    }
247
248    /// Check if contact is considered stale
249    pub fn is_stale(&self, max_age: Duration) -> bool {
250        let now = chrono::Utc::now();
251        let age = now.signed_duration_since(self.last_seen);
252        age.to_std().unwrap_or(Duration::MAX) > max_age
253    }
254
255    /// Get contact age in seconds
256    pub fn age_seconds(&self) -> u64 {
257        let now = chrono::Utc::now();
258        let age = now.signed_duration_since(self.last_seen);
259        age.num_seconds().max(0) as u64
260    }
261
262    /// Check if contact has essential capabilities
263    pub fn has_capability(&self, capability: &str) -> bool {
264        self.capabilities.contains(&capability.to_string())
265    }
266
267    /// Get a summary string for debugging
268    pub fn summary(&self) -> String {
269        let quic_info = if let Some(ref quic_contact) = self.quic_contact {
270            format!(
271                " QUIC: Setup:{:.0}ms",
272                quic_contact.quic_quality.avg_connection_setup_time_ms
273            )
274        } else {
275            " QUIC: None".to_string()
276        };
277
278        format!(
279            "Peer {} (Quality: {:.2}, Success: {:.1}%, Latency: {:.0}ms, Verified: {}{})",
280            self.peer_id.to_string().chars().take(8).collect::<String>(),
281            self.quality_metrics.quality_score,
282            self.quality_metrics.success_rate * 100.0,
283            self.quality_metrics.avg_latency_ms,
284            self.ipv6_identity_verified,
285            quic_info
286        )
287    }
288
289    /// Update or set QUIC contact information
290    pub fn update_quic_contact(&mut self, quic_info: QuicContactInfo) {
291        self.quic_contact = Some(quic_info);
292        self.recalculate_quality_score(); // QUIC may affect overall quality
293    }
294
295    /// Update QUIC connection result
296    pub fn update_quic_connection_result(
297        &mut self,
298        connection_type: QuicConnectionType,
299        success: bool,
300        setup_time_ms: Option<u64>,
301    ) {
302        if let Some(ref mut quic_contact) = self.quic_contact {
303            let now = chrono::Utc::now();
304
305            if success {
306                quic_contact.last_quic_connection = now;
307
308                // Add to successful connection types if not already present
309                if !quic_contact
310                    .successful_connection_types
311                    .contains(&connection_type)
312                {
313                    quic_contact
314                        .successful_connection_types
315                        .push(connection_type.clone());
316                }
317
318                // Update setup time
319                if let Some(setup_time) = setup_time_ms {
320                    let current_avg = quic_contact.quic_quality.avg_connection_setup_time_ms;
321                    quic_contact.quic_quality.avg_connection_setup_time_ms = if current_avg == 0.0 {
322                        setup_time as f64
323                    } else {
324                        (current_avg + setup_time as f64) / 2.0 // Simple moving average
325                    };
326                }
327            }
328
329            // Update connection type success rates
330            let current_rate = quic_contact
331                .quic_quality
332                .connection_type_success_rates
333                .get(&connection_type)
334                .copied()
335                .unwrap_or(0.0);
336            let new_rate = if current_rate == 0.0 {
337                if success { 1.0 } else { 0.0 }
338            } else {
339                (current_rate + if success { 1.0 } else { 0.0 }) / 2.0
340            };
341            quic_contact
342                .quic_quality
343                .connection_type_success_rates
344                .insert(connection_type.clone(), new_rate);
345
346            self.recalculate_quality_score();
347        }
348    }
349
350    /// Get QUIC direct addresses if available
351    pub fn quic_direct_addresses(&self) -> Option<&Vec<SocketAddr>> {
352        self.quic_contact
353            .as_ref()
354            .map(|contact| &contact.direct_addresses)
355    }
356
357    /// Check if peer supports specific QUIC connection type
358    pub fn supports_quic_connection_type(&self, connection_type: &QuicConnectionType) -> bool {
359        self.quic_contact
360            .as_ref()
361            .map(|contact| {
362                contact
363                    .successful_connection_types
364                    .contains(connection_type)
365            })
366            .unwrap_or(false)
367    }
368
369    /// Get QUIC quality score (0.0 to 1.0)
370    pub fn quic_quality_score(&self) -> f64 {
371        if let Some(ref quic_contact) = self.quic_contact {
372            let setup_score = if quic_contact.quic_quality.avg_connection_setup_time_ms > 0.0 {
373                // Lower setup time = higher score
374                (5000.0 / (quic_contact.quic_quality.avg_connection_setup_time_ms + 1000.0))
375                    .min(1.0)
376            } else {
377                0.5 // Neutral if no data
378            };
379            let type_diversity_score = quic_contact.successful_connection_types.len() as f64 / 2.0; // Up to 2 types (IPv4/IPv6)
380            let success_score = quic_contact.quic_quality.connection_success_rate;
381
382            // Weighted average
383            (setup_score * 0.4 + type_diversity_score * 0.3 + success_score * 0.3).clamp(0.0, 1.0)
384        } else {
385            0.0 // No QUIC information
386        }
387    }
388}
389
390impl Default for QualityMetrics {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396impl QualityMetrics {
397    /// Create new quality metrics with default values
398    pub fn new() -> Self {
399        let now = chrono::Utc::now();
400
401        Self {
402            success_rate: 0.0,
403            avg_latency_ms: 0.0,
404            quality_score: 0.0,
405            last_connection_attempt: now,
406            last_successful_connection: now,
407            uptime_score: 0.5, // Neutral starting score
408        }
409    }
410
411    /// Apply age decay to quality metrics
412    pub fn apply_age_decay(&mut self, decay_factor: f64) {
413        // Decay quality score over time to favor recent connections
414        self.quality_score *= decay_factor;
415        self.uptime_score *= decay_factor;
416    }
417}
418
419impl Default for ConnectionHistory {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425impl ConnectionHistory {
426    /// Create new connection history
427    pub fn new() -> Self {
428        Self {
429            total_attempts: 0,
430            successful_connections: 0,
431            failed_connections: 0,
432            total_session_time: Duration::from_secs(0),
433            recent_latencies: Vec::new(),
434            connection_failures: HashMap::new(),
435        }
436    }
437
438    /// Add session time
439    pub fn add_session_time(&mut self, duration: Duration) {
440        self.total_session_time = self.total_session_time.saturating_add(duration);
441    }
442
443    /// Get failure rate for specific error type
444    pub fn get_failure_rate(&self, error_type: &str) -> f64 {
445        let failures = self
446            .connection_failures
447            .get(error_type)
448            .copied()
449            .unwrap_or(0);
450        if self.total_attempts > 0 {
451            failures as f64 / self.total_attempts as f64
452        } else {
453            0.0
454        }
455    }
456}
457
458/// Quality calculator for computing peer scores
459pub struct QualityCalculator {
460    success_weight: f64,
461    latency_weight: f64,
462    recency_weight: f64,
463    reputation_weight: f64,
464    verification_bonus: f64,
465    capability_bonus: f64,
466}
467
468impl QualityCalculator {
469    /// Create new quality calculator with default weights
470    pub fn new() -> Self {
471        Self {
472            success_weight: 0.40,     // 40% - Connection success rate
473            latency_weight: 0.30,     // 30% - Network performance
474            recency_weight: 0.20,     // 20% - How recently seen
475            reputation_weight: 0.10,  // 10% - Reputation score
476            verification_bonus: 0.05, // 5% bonus for verified identity
477            capability_bonus: 0.02,   // 2% bonus per important capability
478        }
479    }
480
481    /// Calculate overall quality score for a contact
482    pub fn calculate_quality(&self, contact: &ContactEntry) -> f64 {
483        let mut score = 0.0;
484
485        // Success rate component (0.0 to 1.0)
486        let success_component = contact.quality_metrics.success_rate * self.success_weight;
487        score += success_component;
488
489        // Latency component (inverse of latency, normalized)
490        let latency_component = if contact.quality_metrics.avg_latency_ms > 0.0 {
491            let normalized_latency =
492                (1000.0 / (contact.quality_metrics.avg_latency_ms + 100.0)).min(1.0);
493            normalized_latency * self.latency_weight
494        } else {
495            0.0
496        };
497        score += latency_component;
498
499        // Recency component (exponential decay)
500        let age_seconds = contact.age_seconds() as f64;
501        let recency_component = (-age_seconds / 86400.0).exp() * self.recency_weight; // 24 hour half-life
502        score += recency_component;
503
504        // Reputation component
505        let reputation_component = contact.reputation_score * self.reputation_weight;
506        score += reputation_component;
507
508        // IPv6 verification bonus
509        if contact.ipv6_identity_verified {
510            score += self.verification_bonus;
511        }
512
513        // Capability bonuses
514        let important_capabilities = ["dht", "relay"];
515        let capability_count = important_capabilities
516            .iter()
517            .filter(|&cap| contact.has_capability(cap))
518            .count();
519        score += capability_count as f64 * self.capability_bonus;
520
521        // QUIC connectivity bonus
522        if let Some(ref quic_contact) = contact.quic_contact {
523            let quic_score = contact.quic_quality_score();
524            // Give 10% bonus for having QUIC + quality-based multiplier
525            let quic_bonus = 0.10 * quic_score;
526            score += quic_bonus;
527
528            // Bonus for connection type diversity (more ways to connect = better)
529            let diversity_bonus =
530                (quic_contact.successful_connection_types.len() as f64 / 2.0) * 0.03;
531            score += diversity_bonus;
532        }
533
534        // Clamp to valid range
535        score.clamp(0.0, 1.0)
536    }
537
538    /// Calculate quality with custom weights
539    pub fn calculate_with_weights(
540        &self,
541        contact: &ContactEntry,
542        success_weight: f64,
543        latency_weight: f64,
544        recency_weight: f64,
545        reputation_weight: f64,
546    ) -> f64 {
547        let mut calculator = self.clone();
548        calculator.success_weight = success_weight;
549        calculator.latency_weight = latency_weight;
550        calculator.recency_weight = recency_weight;
551        calculator.reputation_weight = reputation_weight;
552
553        calculator.calculate_quality(contact)
554    }
555}
556
557impl Clone for QualityCalculator {
558    fn clone(&self) -> Self {
559        Self {
560            success_weight: self.success_weight,
561            latency_weight: self.latency_weight,
562            recency_weight: self.recency_weight,
563            reputation_weight: self.reputation_weight,
564            verification_bonus: self.verification_bonus,
565            capability_bonus: self.capability_bonus,
566        }
567    }
568}
569
570impl Default for QualityCalculator {
571    fn default() -> Self {
572        Self::new()
573    }
574}
575
576impl QuicContactInfo {
577    /// Create new QUIC contact info
578    pub fn new(direct_addresses: Vec<std::net::SocketAddr>) -> Self {
579        let now = chrono::Utc::now();
580
581        Self {
582            direct_addresses,
583            quic_quality: QuicQualityMetrics::new(),
584            last_quic_connection: now,
585            successful_connection_types: Vec::new(),
586        }
587    }
588
589    /// Update direct addresses
590    pub fn update_direct_addresses(&mut self, addresses: Vec<std::net::SocketAddr>) {
591        self.direct_addresses = addresses;
592    }
593
594    /// Check if this contact has any connectivity options
595    pub fn has_connectivity_options(&self) -> bool {
596        !self.direct_addresses.is_empty()
597    }
598}
599
600impl QuicQualityMetrics {
601    /// Create new QUIC quality metrics with default values
602    pub fn new() -> Self {
603        Self {
604            avg_response_time_ms: 0.0,
605            avg_throughput_mbps: 0.0,
606            connection_success_rate: 0.0,
607            avg_connection_setup_time_ms: 0.0,
608            connection_type_success_rates: HashMap::new(),
609        }
610    }
611
612    /// Get overall QUIC connectivity score
613    pub fn overall_score(&self) -> f64 {
614        let speed_score = if self.avg_connection_setup_time_ms > 0.0 {
615            (5000.0 / (self.avg_connection_setup_time_ms + 1000.0)).min(1.0)
616        } else {
617            0.5
618        };
619        let reliability_score = if !self.connection_type_success_rates.is_empty() {
620            self.connection_type_success_rates.values().sum::<f64>()
621                / self.connection_type_success_rates.len() as f64
622        } else {
623            0.0
624        };
625        let success_score = self.connection_success_rate;
626
627        // Weighted combination
628        (speed_score * 0.4 + reliability_score * 0.3 + success_score * 0.3).clamp(0.0, 1.0)
629    }
630}
631
632impl Default for QuicQualityMetrics {
633    fn default() -> Self {
634        Self::new()
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    #[test]
643    fn test_contact_entry_creation() {
644        let peer_id = PeerId::from("test-peer");
645        let addresses = vec!["127.0.0.1:9000".parse().unwrap()];
646
647        let contact = ContactEntry::new(peer_id.clone(), addresses.clone());
648
649        assert_eq!(contact.peer_id, peer_id);
650        assert_eq!(contact.addresses, addresses);
651        assert_eq!(contact.quality_metrics.success_rate, 0.0);
652        assert!(!contact.ipv6_identity_verified);
653    }
654
655    #[test]
656    fn test_quality_calculation() {
657        let mut contact = ContactEntry::new(
658            PeerId::from("test-peer"),
659            vec!["127.0.0.1:9000".parse().unwrap()],
660        );
661
662        // Simulate successful connections
663        contact.update_connection_result(true, Some(50), None);
664        contact.update_connection_result(true, Some(60), None);
665        contact.update_connection_result(false, None, Some("timeout".to_string()));
666
667        assert!(contact.quality_metrics.success_rate > 0.5);
668        assert!(contact.quality_metrics.avg_latency_ms > 0.0);
669        assert!(contact.quality_metrics.quality_score > 0.0);
670    }
671
672    #[test]
673    fn test_capability_bonus() {
674        let mut contact = ContactEntry::new(
675            PeerId::from("test-peer"),
676            vec!["127.0.0.1:9000".parse().unwrap()],
677        );
678
679        let initial_score = contact.quality_metrics.quality_score;
680
681        contact.update_capabilities(vec!["dht".to_string()]);
682
683        assert!(contact.quality_metrics.quality_score > initial_score);
684    }
685
686    #[test]
687    fn test_stale_detection() {
688        let mut contact = ContactEntry::new(
689            PeerId::from("test-peer"),
690            vec!["127.0.0.1:9000".parse().unwrap()],
691        );
692
693        // Set last seen to 2 hours ago
694        contact.last_seen = chrono::Utc::now() - chrono::Duration::hours(2);
695
696        assert!(contact.is_stale(Duration::from_secs(3600))); // 1 hour threshold
697        assert!(!contact.is_stale(Duration::from_secs(10800))); // 3 hour threshold
698    }
699
700    #[test]
701    fn test_quality_decay() {
702        let mut metrics = QualityMetrics::new();
703        metrics.quality_score = 0.8;
704        metrics.uptime_score = 0.9;
705
706        metrics.apply_age_decay(0.9);
707
708        assert!(metrics.quality_score < 0.8);
709        assert!(metrics.uptime_score < 0.9);
710    }
711
712    #[test]
713    fn test_quic_contact_creation() {
714        let addresses = vec!["127.0.0.1:9000".parse().unwrap()];
715
716        let quic_contact = QuicContactInfo::new(addresses.clone());
717
718        assert_eq!(quic_contact.direct_addresses, addresses);
719        assert!(quic_contact.has_connectivity_options());
720    }
721
722    #[test]
723    fn test_contact_with_quic_info() {
724        let addresses = vec!["127.0.0.1:9000".parse().unwrap()];
725        let quic_info = QuicContactInfo::new(addresses);
726
727        let contact = ContactEntry::new_with_quic(
728            PeerId::from("test-peer"),
729            vec!["127.0.0.1:9000".parse().unwrap()],
730            quic_info,
731        );
732
733        assert!(contact.quic_contact.is_some());
734        assert!(contact.quic_direct_addresses().is_some());
735        assert!(has_connectivity_options(&contact));
736    }
737
738    #[test]
739    fn test_quic_connection_result_update() {
740        let addresses = vec!["127.0.0.1:9000".parse().unwrap()];
741        let quic_info = QuicContactInfo::new(addresses);
742
743        let mut contact = ContactEntry::new_with_quic(
744            PeerId::from("test-peer"),
745            vec!["127.0.0.1:9000".parse().unwrap()],
746            quic_info,
747        );
748
749        // Simulate successful QUIC connection
750        contact.update_quic_connection_result(
751            QuicConnectionType::DirectIPv4,
752            true,
753            Some(250), // 250ms setup time
754        );
755
756        assert_eq!(
757            contact
758                .quic_contact
759                .as_ref()
760                .unwrap()
761                .quic_quality
762                .avg_connection_setup_time_ms,
763            250.0
764        );
765        assert!(contact.supports_quic_connection_type(&QuicConnectionType::DirectIPv4));
766
767        let quic_quality = contact.quic_quality_score();
768        assert!(quic_quality > 0.0);
769    }
770
771    #[test]
772    fn test_quic_quality_affects_overall_score() {
773        let addresses = vec!["127.0.0.1:9000".parse().unwrap()];
774        let quic_info = QuicContactInfo::new(addresses);
775
776        // Contact without QUIC
777        let mut contact_no_quic = ContactEntry::new(
778            PeerId::from("test-peer-no-quic"),
779            vec!["127.0.0.1:9000".parse().unwrap()],
780        );
781        contact_no_quic.update_connection_result(true, Some(100), None);
782
783        // Contact with QUIC
784        let mut contact_with_quic = ContactEntry::new_with_quic(
785            PeerId::from("test-peer-with-quic"),
786            vec!["127.0.0.1:9000".parse().unwrap()],
787            quic_info,
788        );
789        contact_with_quic.update_connection_result(true, Some(100), None);
790        contact_with_quic.update_quic_connection_result(
791            QuicConnectionType::DirectIPv4,
792            true,
793            Some(200),
794        );
795
796        // QUIC contact should have higher quality score
797        assert!(
798            contact_with_quic.quality_metrics.quality_score
799                > contact_no_quic.quality_metrics.quality_score
800        );
801    }
802
803    fn has_connectivity_options(contact: &ContactEntry) -> bool {
804        contact
805            .quic_contact
806            .as_ref()
807            .map(|quic| !quic.direct_addresses.is_empty())
808            .unwrap_or(false)
809    }
810}