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