ipfrs_network/
geo_routing.rs

1//! Geographic routing optimization for IPFRS network
2//!
3//! This module provides geographic-aware peer selection and routing to optimize
4//! network latency by preferring geographically closer peers.
5//!
6//! ## Features
7//!
8//! - **GeoIP Lookup**: Determine peer geographic location from IP addresses
9//! - **Distance Calculation**: Great-circle distance using Haversine formula
10//! - **Proximity Ranking**: Rank peers by geographic proximity
11//! - **Regional Clustering**: Group peers by geographic regions
12//! - **Latency Prediction**: Estimate latency based on distance
13//!
14//! ## Example
15//!
16//! ```rust
17//! use ipfrs_network::geo_routing::{GeoRouter, GeoLocation, GeoRouterConfig};
18//! use std::net::IpAddr;
19//!
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! let config = GeoRouterConfig::default();
22//! let mut router = GeoRouter::new(config);
23//!
24//! // Add peer locations
25//! let peer1 = libp2p::PeerId::random();
26//! let peer2 = libp2p::PeerId::random();
27//!
28//! router.update_peer_location(peer1, GeoLocation::new(40.7128, -74.0060)); // New York
29//! router.update_peer_location(peer2, GeoLocation::new(51.5074, -0.1278));   // London
30//!
31//! // Get proximity-ranked peers from San Francisco
32//! let sf_location = GeoLocation::new(37.7749, -122.4194);
33//! let ranked = router.rank_peers_by_proximity(&sf_location);
34//!
35//! println!("Closest peer: {:?}", ranked.first());
36//! # Ok(())
37//! # }
38//! ```
39
40use dashmap::DashMap;
41use libp2p::PeerId;
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44use std::net::IpAddr;
45use std::sync::Arc;
46
47/// Geographic location with latitude and longitude
48#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
49pub struct GeoLocation {
50    /// Latitude in degrees (-90 to 90)
51    pub latitude: f64,
52    /// Longitude in degrees (-180 to 180)
53    pub longitude: f64,
54}
55
56impl GeoLocation {
57    /// Create a new geographic location
58    ///
59    /// # Arguments
60    ///
61    /// * `latitude` - Latitude in degrees (-90 to 90)
62    /// * `longitude` - Longitude in degrees (-180 to 180)
63    ///
64    /// # Panics
65    ///
66    /// Panics if latitude or longitude are out of valid ranges
67    pub fn new(latitude: f64, longitude: f64) -> Self {
68        assert!(
69            (-90.0..=90.0).contains(&latitude),
70            "Latitude must be between -90 and 90"
71        );
72        assert!(
73            (-180.0..=180.0).contains(&longitude),
74            "Longitude must be between -180 and 180"
75        );
76        Self {
77            latitude,
78            longitude,
79        }
80    }
81
82    /// Calculate great-circle distance to another location in kilometers
83    /// using the Haversine formula
84    pub fn distance_to(&self, other: &GeoLocation) -> f64 {
85        const EARTH_RADIUS_KM: f64 = 6371.0;
86
87        let lat1 = self.latitude.to_radians();
88        let lat2 = other.latitude.to_radians();
89        let delta_lat = (other.latitude - self.latitude).to_radians();
90        let delta_lon = (other.longitude - self.longitude).to_radians();
91
92        let a = (delta_lat / 2.0).sin().powi(2)
93            + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
94        let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
95
96        EARTH_RADIUS_KM * c
97    }
98
99    /// Estimate network latency in milliseconds based on distance
100    ///
101    /// Uses a simple model: base_latency + (distance_km / speed_of_light_factor)
102    /// Speed of light in fiber is roughly 200,000 km/s, giving ~5ms per 1000km
103    pub fn estimate_latency_ms(&self, other: &GeoLocation) -> f64 {
104        const BASE_LATENCY_MS: f64 = 10.0; // Base latency for processing
105        const MS_PER_1000_KM: f64 = 5.0; // Network propagation delay
106
107        let distance = self.distance_to(other);
108        BASE_LATENCY_MS + (distance / 1000.0) * MS_PER_1000_KM
109    }
110}
111
112/// Geographic region for clustering peers
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
114pub enum GeoRegion {
115    /// North America
116    NorthAmerica,
117    /// South America
118    SouthAmerica,
119    /// Europe
120    Europe,
121    /// Asia
122    Asia,
123    /// Africa
124    Africa,
125    /// Oceania (Australia, Pacific Islands)
126    Oceania,
127    /// Unknown region
128    Unknown,
129}
130
131impl GeoRegion {
132    /// Determine region from geographic location
133    pub fn from_location(location: &GeoLocation) -> Self {
134        let lat = location.latitude;
135        let lon = location.longitude;
136
137        // Simple region classification based on lat/lon ranges
138        if (-170.0..=-30.0).contains(&lon) {
139            // Americas
140            if lat >= 15.0 {
141                GeoRegion::NorthAmerica
142            } else {
143                GeoRegion::SouthAmerica
144            }
145        } else if (-30.0..=60.0).contains(&lon) {
146            // Europe and Africa
147            if lat >= 35.0 {
148                GeoRegion::Europe
149            } else {
150                GeoRegion::Africa
151            }
152        } else if (60.0..=150.0).contains(&lon) {
153            // Asia and Oceania
154            if lat >= -10.0 {
155                GeoRegion::Asia
156            } else {
157                GeoRegion::Oceania
158            }
159        } else {
160            GeoRegion::Unknown
161        }
162    }
163
164    /// Get representative location for region (approximate center)
165    #[allow(dead_code)]
166    pub fn representative_location(&self) -> GeoLocation {
167        match self {
168            GeoRegion::NorthAmerica => GeoLocation::new(39.8283, -98.5795), // US center
169            GeoRegion::SouthAmerica => GeoLocation::new(-14.2350, -51.9253), // Brazil
170            GeoRegion::Europe => GeoLocation::new(50.0, 10.0),              // Germany
171            GeoRegion::Asia => GeoLocation::new(34.0, 100.0),               // Central China
172            GeoRegion::Africa => GeoLocation::new(1.0, 18.0),               // Central Africa
173            GeoRegion::Oceania => GeoLocation::new(-25.2744, 133.7751),     // Australia
174            GeoRegion::Unknown => GeoLocation::new(0.0, 0.0),
175        }
176    }
177}
178
179/// Peer with geographic metadata
180#[derive(Debug, Clone)]
181pub struct GeoPeer {
182    /// Peer ID
183    pub peer_id: PeerId,
184    /// Geographic location
185    pub location: GeoLocation,
186    /// Region
187    pub region: GeoRegion,
188    /// Distance from reference point (if applicable)
189    pub distance_km: Option<f64>,
190}
191
192impl GeoPeer {
193    /// Create a new GeoPeer
194    pub fn new(peer_id: PeerId, location: GeoLocation) -> Self {
195        let region = GeoRegion::from_location(&location);
196        Self {
197            peer_id,
198            location,
199            region,
200            distance_km: None,
201        }
202    }
203
204    /// Set distance from a reference point
205    pub fn with_distance(mut self, distance_km: f64) -> Self {
206        self.distance_km = Some(distance_km);
207        self
208    }
209}
210
211/// Configuration for geographic router
212#[derive(Debug, Clone)]
213pub struct GeoRouterConfig {
214    /// Maximum distance in km to consider peers as "nearby"
215    pub nearby_threshold_km: f64,
216    /// Prefer peers in same region with this bonus (subtracted from distance)
217    pub same_region_bonus_km: f64,
218    /// Enable region-based clustering
219    pub enable_region_clustering: bool,
220    /// Maximum number of peers to track per region
221    pub max_peers_per_region: usize,
222}
223
224impl Default for GeoRouterConfig {
225    fn default() -> Self {
226        Self {
227            nearby_threshold_km: 500.0,   // 500km radius for "nearby"
228            same_region_bonus_km: 1000.0, // Same region gets 1000km bonus
229            enable_region_clustering: true,
230            max_peers_per_region: 100,
231        }
232    }
233}
234
235impl GeoRouterConfig {
236    /// Configuration optimized for low latency
237    pub fn low_latency() -> Self {
238        Self {
239            nearby_threshold_km: 1000.0,
240            same_region_bonus_km: 2000.0,
241            enable_region_clustering: true,
242            max_peers_per_region: 50,
243        }
244    }
245
246    /// Configuration optimized for global distribution
247    pub fn global() -> Self {
248        Self {
249            nearby_threshold_km: 5000.0,
250            same_region_bonus_km: 500.0,
251            enable_region_clustering: true,
252            max_peers_per_region: 200,
253        }
254    }
255
256    /// Configuration for regional focus
257    pub fn regional() -> Self {
258        Self {
259            nearby_threshold_km: 200.0,
260            same_region_bonus_km: 3000.0,
261            enable_region_clustering: true,
262            max_peers_per_region: 150,
263        }
264    }
265}
266
267/// Geographic router for proximity-based peer selection
268pub struct GeoRouter {
269    /// Configuration
270    config: GeoRouterConfig,
271    /// Peer locations
272    peer_locations: Arc<DashMap<PeerId, GeoLocation>>,
273    /// Peers grouped by region
274    region_peers: Arc<DashMap<GeoRegion, Vec<PeerId>>>,
275    /// Statistics
276    stats: Arc<parking_lot::RwLock<GeoRouterStats>>,
277}
278
279/// Statistics for geographic router
280#[derive(Debug, Clone, Default)]
281pub struct GeoRouterStats {
282    /// Total peers tracked
283    pub total_peers: usize,
284    /// Peers per region
285    pub peers_per_region: HashMap<GeoRegion, usize>,
286    /// Total proximity queries
287    pub proximity_queries: u64,
288    /// Total region lookups
289    pub region_lookups: u64,
290}
291
292impl GeoRouter {
293    /// Create a new geographic router
294    pub fn new(config: GeoRouterConfig) -> Self {
295        Self {
296            config,
297            peer_locations: Arc::new(DashMap::new()),
298            region_peers: Arc::new(DashMap::new()),
299            stats: Arc::new(parking_lot::RwLock::new(GeoRouterStats::default())),
300        }
301    }
302
303    /// Update or add a peer's location
304    pub fn update_peer_location(&self, peer_id: PeerId, location: GeoLocation) {
305        // Remove from old region if exists
306        if let Some(old_location) = self.peer_locations.get(&peer_id) {
307            let old_region = GeoRegion::from_location(&old_location);
308            if let Some(mut peers) = self.region_peers.get_mut(&old_region) {
309                peers.retain(|p| p != &peer_id);
310            }
311        }
312
313        // Update location
314        self.peer_locations.insert(peer_id, location);
315
316        // Add to new region
317        if self.config.enable_region_clustering {
318            let region = GeoRegion::from_location(&location);
319            self.region_peers.entry(region).or_default().push(peer_id);
320
321            // Enforce max peers per region
322            if let Some(mut peers) = self.region_peers.get_mut(&region) {
323                if peers.len() > self.config.max_peers_per_region {
324                    peers.truncate(self.config.max_peers_per_region);
325                }
326            }
327        }
328
329        // Update stats
330        let mut stats = self.stats.write();
331        stats.total_peers = self.peer_locations.len();
332        stats.peers_per_region.clear();
333        for entry in self.region_peers.iter() {
334            stats
335                .peers_per_region
336                .insert(*entry.key(), entry.value().len());
337        }
338    }
339
340    /// Remove a peer's location
341    pub fn remove_peer(&self, peer_id: &PeerId) {
342        if let Some((_, location)) = self.peer_locations.remove(peer_id) {
343            let region = GeoRegion::from_location(&location);
344            if let Some(mut peers) = self.region_peers.get_mut(&region) {
345                peers.retain(|p| p != peer_id);
346            }
347
348            // Update stats
349            let mut stats = self.stats.write();
350            stats.total_peers = self.peer_locations.len();
351            stats.peers_per_region.insert(
352                region,
353                self.region_peers.get(&region).map(|p| p.len()).unwrap_or(0),
354            );
355        }
356    }
357
358    /// Get location for a peer
359    pub fn get_peer_location(&self, peer_id: &PeerId) -> Option<GeoLocation> {
360        self.peer_locations.get(peer_id).map(|loc| *loc)
361    }
362
363    /// Get all peers in a region
364    pub fn get_peers_in_region(&self, region: GeoRegion) -> Vec<PeerId> {
365        self.stats.write().region_lookups += 1;
366        self.region_peers
367            .get(&region)
368            .map(|peers| peers.clone())
369            .unwrap_or_default()
370    }
371
372    /// Rank all known peers by proximity to a location
373    pub fn rank_peers_by_proximity(&self, reference: &GeoLocation) -> Vec<GeoPeer> {
374        self.stats.write().proximity_queries += 1;
375
376        let mut peers: Vec<GeoPeer> = self
377            .peer_locations
378            .iter()
379            .map(|entry| {
380                let peer_id = *entry.key();
381                let location = *entry.value();
382                let distance = reference.distance_to(&location);
383                GeoPeer::new(peer_id, location).with_distance(distance)
384            })
385            .collect();
386
387        // Apply same-region bonus
388        if self.config.enable_region_clustering {
389            let ref_region = GeoRegion::from_location(reference);
390            for peer in &mut peers {
391                if peer.region == ref_region {
392                    if let Some(dist) = peer.distance_km.as_mut() {
393                        *dist = (*dist - self.config.same_region_bonus_km).max(0.0);
394                    }
395                }
396            }
397        }
398
399        // Sort by distance
400        peers.sort_by(|a, b| {
401            a.distance_km
402                .unwrap_or(f64::INFINITY)
403                .partial_cmp(&b.distance_km.unwrap_or(f64::INFINITY))
404                .unwrap_or(std::cmp::Ordering::Equal)
405        });
406
407        peers
408    }
409
410    /// Get nearby peers within threshold distance
411    pub fn get_nearby_peers(&self, reference: &GeoLocation) -> Vec<GeoPeer> {
412        let all_peers = self.rank_peers_by_proximity(reference);
413        all_peers
414            .into_iter()
415            .filter(|peer| {
416                peer.distance_km
417                    .map(|d| d <= self.config.nearby_threshold_km)
418                    .unwrap_or(false)
419            })
420            .collect()
421    }
422
423    /// Estimate IP location using simple heuristics (placeholder)
424    ///
425    /// In production, this would use a GeoIP database like MaxMind GeoLite2
426    #[allow(dead_code)]
427    pub fn estimate_ip_location(&self, _ip: IpAddr) -> Option<GeoLocation> {
428        // Placeholder: In production, integrate with GeoIP database
429        // For now, return None to indicate unknown location
430        None
431    }
432
433    /// Get statistics
434    pub fn stats(&self) -> GeoRouterStats {
435        self.stats.read().clone()
436    }
437
438    /// Clear all peer locations
439    #[allow(dead_code)]
440    pub fn clear(&self) {
441        self.peer_locations.clear();
442        self.region_peers.clear();
443        let mut stats = self.stats.write();
444        stats.total_peers = 0;
445        stats.peers_per_region.clear();
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_geo_location_new() {
455        let loc = GeoLocation::new(40.7128, -74.0060);
456        assert_eq!(loc.latitude, 40.7128);
457        assert_eq!(loc.longitude, -74.0060);
458    }
459
460    #[test]
461    #[should_panic(expected = "Latitude must be between -90 and 90")]
462    fn test_geo_location_invalid_latitude() {
463        GeoLocation::new(100.0, 0.0);
464    }
465
466    #[test]
467    #[should_panic(expected = "Longitude must be between -180 and 180")]
468    fn test_geo_location_invalid_longitude() {
469        GeoLocation::new(0.0, 200.0);
470    }
471
472    #[test]
473    fn test_distance_calculation() {
474        let ny = GeoLocation::new(40.7128, -74.0060);
475        let london = GeoLocation::new(51.5074, -0.1278);
476
477        let distance = ny.distance_to(&london);
478        // NY to London is approximately 5570 km
479        assert!((distance - 5570.0).abs() < 100.0, "Distance: {}", distance);
480    }
481
482    #[test]
483    fn test_distance_same_location() {
484        let loc = GeoLocation::new(40.7128, -74.0060);
485        let distance = loc.distance_to(&loc);
486        assert!(distance < 0.1, "Same location distance should be ~0");
487    }
488
489    #[test]
490    fn test_latency_estimation() {
491        let sf = GeoLocation::new(37.7749, -122.4194);
492        let tokyo = GeoLocation::new(35.6762, 139.6503);
493
494        let latency = sf.estimate_latency_ms(&tokyo);
495        // SF to Tokyo is ~8300 km, should be ~50-60ms
496        assert!(latency > 40.0 && latency < 70.0, "Latency: {}", latency);
497    }
498
499    #[test]
500    fn test_region_from_location() {
501        // Test North America
502        let ny = GeoLocation::new(40.7128, -74.0060);
503        assert_eq!(GeoRegion::from_location(&ny), GeoRegion::NorthAmerica);
504
505        // Test Europe
506        let london = GeoLocation::new(51.5074, -0.1278);
507        assert_eq!(GeoRegion::from_location(&london), GeoRegion::Europe);
508
509        // Test Asia
510        let tokyo = GeoLocation::new(35.6762, 139.6503);
511        assert_eq!(GeoRegion::from_location(&tokyo), GeoRegion::Asia);
512
513        // Test South America
514        let sao_paulo = GeoLocation::new(-23.5505, -46.6333);
515        assert_eq!(
516            GeoRegion::from_location(&sao_paulo),
517            GeoRegion::SouthAmerica
518        );
519    }
520
521    #[test]
522    fn test_geo_router_basic() {
523        let config = GeoRouterConfig::default();
524        let router = GeoRouter::new(config);
525
526        let peer1 = PeerId::random();
527        let peer2 = PeerId::random();
528
529        router.update_peer_location(peer1, GeoLocation::new(40.7128, -74.0060));
530        router.update_peer_location(peer2, GeoLocation::new(51.5074, -0.1278));
531
532        assert_eq!(router.stats().total_peers, 2);
533    }
534
535    #[test]
536    fn test_proximity_ranking() {
537        let config = GeoRouterConfig::default();
538        let router = GeoRouter::new(config);
539
540        let peer_ny = PeerId::random();
541        let peer_la = PeerId::random();
542        let peer_london = PeerId::random();
543
544        router.update_peer_location(peer_ny, GeoLocation::new(40.7128, -74.0060));
545        router.update_peer_location(peer_la, GeoLocation::new(34.0522, -118.2437));
546        router.update_peer_location(peer_london, GeoLocation::new(51.5074, -0.1278));
547
548        // From SF, LA should be closest, then NY, then London
549        let sf = GeoLocation::new(37.7749, -122.4194);
550        let ranked = router.rank_peers_by_proximity(&sf);
551
552        assert_eq!(ranked.len(), 3);
553        assert_eq!(ranked[0].peer_id, peer_la);
554        assert_eq!(ranked[1].peer_id, peer_ny);
555        assert_eq!(ranked[2].peer_id, peer_london);
556    }
557
558    #[test]
559    fn test_nearby_peers() {
560        let config = GeoRouterConfig {
561            nearby_threshold_km: 500.0,
562            ..Default::default()
563        };
564        let router = GeoRouter::new(config);
565
566        let peer_nearby = PeerId::random();
567        let peer_far = PeerId::random();
568
569        // NY and Philadelphia are ~130 km apart
570        router.update_peer_location(peer_nearby, GeoLocation::new(39.9526, -75.1652)); // Philly
571                                                                                       // London is far from NY
572        router.update_peer_location(peer_far, GeoLocation::new(51.5074, -0.1278));
573
574        let ny = GeoLocation::new(40.7128, -74.0060);
575        let nearby = router.get_nearby_peers(&ny);
576
577        assert_eq!(nearby.len(), 1);
578        assert_eq!(nearby[0].peer_id, peer_nearby);
579    }
580
581    #[test]
582    fn test_region_clustering() {
583        let config = GeoRouterConfig::default();
584        let router = GeoRouter::new(config);
585
586        let peer1 = PeerId::random();
587        let peer2 = PeerId::random();
588        let peer3 = PeerId::random();
589
590        router.update_peer_location(peer1, GeoLocation::new(40.7128, -74.0060)); // NY
591        router.update_peer_location(peer2, GeoLocation::new(34.0522, -118.2437)); // LA
592        router.update_peer_location(peer3, GeoLocation::new(51.5074, -0.1278)); // London
593
594        let na_peers = router.get_peers_in_region(GeoRegion::NorthAmerica);
595        let eu_peers = router.get_peers_in_region(GeoRegion::Europe);
596
597        assert_eq!(na_peers.len(), 2);
598        assert_eq!(eu_peers.len(), 1);
599        assert!(na_peers.contains(&peer1));
600        assert!(na_peers.contains(&peer2));
601        assert!(eu_peers.contains(&peer3));
602    }
603
604    #[test]
605    fn test_remove_peer() {
606        let config = GeoRouterConfig::default();
607        let router = GeoRouter::new(config);
608
609        let peer = PeerId::random();
610        router.update_peer_location(peer, GeoLocation::new(40.7128, -74.0060));
611        assert_eq!(router.stats().total_peers, 1);
612
613        router.remove_peer(&peer);
614        assert_eq!(router.stats().total_peers, 0);
615        assert!(router.get_peer_location(&peer).is_none());
616    }
617
618    #[test]
619    fn test_same_region_bonus() {
620        let config = GeoRouterConfig {
621            same_region_bonus_km: 1000.0,
622            enable_region_clustering: true,
623            ..Default::default()
624        };
625        let router = GeoRouter::new(config);
626
627        let peer_same_region = PeerId::random();
628        let peer_diff_region = PeerId::random();
629
630        // Both are similar distance from NY, but one is in NA, one in Europe
631        router.update_peer_location(peer_same_region, GeoLocation::new(34.0522, -118.2437)); // LA
632        router.update_peer_location(peer_diff_region, GeoLocation::new(51.5074, -0.1278)); // London
633
634        let ny = GeoLocation::new(40.7128, -74.0060);
635        let ranked = router.rank_peers_by_proximity(&ny);
636
637        // LA should rank first due to same-region bonus, even though distance is similar
638        assert_eq!(ranked[0].peer_id, peer_same_region);
639    }
640
641    #[test]
642    fn test_max_peers_per_region() {
643        let config = GeoRouterConfig {
644            max_peers_per_region: 2,
645            enable_region_clustering: true,
646            ..Default::default()
647        };
648        let router = GeoRouter::new(config);
649
650        // Add 3 peers in North America
651        for _ in 0..3 {
652            let peer = PeerId::random();
653            router.update_peer_location(peer, GeoLocation::new(40.0, -100.0));
654        }
655
656        let na_peers = router.get_peers_in_region(GeoRegion::NorthAmerica);
657        assert!(na_peers.len() <= 2, "Should enforce max peers per region");
658    }
659
660    #[test]
661    fn test_statistics_tracking() {
662        let config = GeoRouterConfig::default();
663        let router = GeoRouter::new(config);
664
665        let peer1 = PeerId::random();
666        let peer2 = PeerId::random();
667
668        router.update_peer_location(peer1, GeoLocation::new(40.7128, -74.0060));
669        router.update_peer_location(peer2, GeoLocation::new(51.5074, -0.1278));
670
671        let stats = router.stats();
672        assert_eq!(stats.total_peers, 2);
673        assert_eq!(stats.proximity_queries, 0);
674
675        router.rank_peers_by_proximity(&GeoLocation::new(0.0, 0.0));
676        let stats = router.stats();
677        assert_eq!(stats.proximity_queries, 1);
678    }
679
680    #[test]
681    fn test_config_presets() {
682        let low_latency = GeoRouterConfig::low_latency();
683        assert_eq!(low_latency.nearby_threshold_km, 1000.0);
684
685        let global = GeoRouterConfig::global();
686        assert_eq!(global.nearby_threshold_km, 5000.0);
687
688        let regional = GeoRouterConfig::regional();
689        assert_eq!(regional.nearby_threshold_km, 200.0);
690    }
691}