1use crate::constants::*;
8use crate::relay::{RelayInfo, RelayManager};
9use offline_protocol_core::Message;
10use std::collections::hash_map::DefaultHasher;
11use std::collections::HashMap;
12use std::hash::{Hash, Hasher};
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone)]
17pub struct NeighborInfo {
18 pub peer_id: String,
20
21 pub rssi: i16,
23
24 pub hops_to_destination: Option<u8>,
26
27 pub link_quality: u8,
29
30 pub relay_info: Option<RelayInfo>,
32}
33
34#[derive(Debug, Clone)]
36pub struct GossipConfig {
37 pub enabled: bool,
39
40 pub target_fanout: usize,
43
44 pub min_probability: f32,
47
48 pub small_network_threshold: usize,
51
52 pub priority_bypass: bool,
54}
55
56impl Default for GossipConfig {
57 fn default() -> Self {
58 Self {
59 enabled: true,
60 target_fanout: 4,
61 min_probability: 0.15,
62 small_network_threshold: 10,
63 priority_bypass: true,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct GradientRoutingConfig {
71 pub enabled: bool,
73 pub max_routes_per_destination: usize,
75 pub route_ttl_secs: u64,
77 pub max_routing_table_size: usize,
79}
80
81impl Default for GradientRoutingConfig {
82 fn default() -> Self {
83 Self {
84 enabled: true,
85 max_routes_per_destination: 3,
86 route_ttl_secs: 300, max_routing_table_size: 1000,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
96pub struct RouteEntry {
97 pub next_hop: String,
99 pub hop_count: u8,
101 pub last_seen: Instant,
103 pub quality: f32,
105 pub sequence_number: u32,
108}
109
110#[derive(Debug)]
113pub struct GradientRoutingTable {
114 config: GradientRoutingConfig,
115 routes: HashMap<String, Vec<RouteEntry>>,
117 neighbor_destinations: HashMap<String, Vec<String>>,
119}
120
121impl GradientRoutingTable {
122 pub fn new() -> Self {
124 Self::with_config(GradientRoutingConfig::default())
125 }
126
127 pub fn with_config(config: GradientRoutingConfig) -> Self {
129 Self {
130 config,
131 routes: HashMap::new(),
132 neighbor_destinations: HashMap::new(),
133 }
134 }
135
136 pub fn learn_route(
140 &mut self,
141 destination: &str,
142 next_hop: &str,
143 hop_count: u8,
144 quality: f32,
145 sequence_number: u32,
146 ) {
147 if !self.config.enabled {
148 return;
149 }
150
151 let now = Instant::now();
152
153 let routes = self.routes.entry(destination.to_string()).or_default();
155
156 if let Some(existing) = routes.iter_mut().find(|r| r.next_hop == next_hop) {
158 let accept = seq_is_newer(sequence_number, existing.sequence_number)
160 || (sequence_number == existing.sequence_number && hop_count <= existing.hop_count);
161 if accept {
162 existing.hop_count = hop_count;
163 existing.last_seen = now;
164 existing.quality = quality;
165 existing.sequence_number = sequence_number;
166 }
167 return;
168 }
169
170 if routes.len() >= self.config.max_routes_per_destination {
172 routes.sort_by(|a, b| {
174 seq_cmp(b.sequence_number, a.sequence_number)
175 .then_with(|| {
176 b.quality
177 .partial_cmp(&a.quality)
178 .unwrap_or(std::cmp::Ordering::Equal)
179 })
180 .then_with(|| a.hop_count.cmp(&b.hop_count))
181 });
182 if let Some(evicted) = routes.pop() {
183 if let Some(dests) = self.neighbor_destinations.get_mut(&evicted.next_hop) {
185 dests.retain(|d| d != destination);
186 if dests.is_empty() {
187 self.neighbor_destinations.remove(&evicted.next_hop);
188 }
189 }
190 }
191 }
192
193 routes.push(RouteEntry {
194 next_hop: next_hop.to_string(),
195 hop_count,
196 last_seen: now,
197 quality,
198 sequence_number,
199 });
200
201 let dests = self
203 .neighbor_destinations
204 .entry(next_hop.to_string())
205 .or_default();
206 if !dests.contains(&destination.to_string()) {
207 dests.push(destination.to_string());
208 }
209
210 self.enforce_size_limit();
212 }
213
214 pub fn get_route(&self, destination: &str) -> Option<&RouteEntry> {
216 if !self.config.enabled {
217 return None;
218 }
219
220 let routes = self.routes.get(destination)?;
221 let ttl = Duration::from_secs(self.config.route_ttl_secs);
222 let now = Instant::now();
223
224 routes
227 .iter()
228 .filter(|r| now.duration_since(r.last_seen) < ttl)
229 .max_by(|a, b| {
230 seq_cmp(a.sequence_number, b.sequence_number)
231 .then_with(|| {
232 a.quality
233 .partial_cmp(&b.quality)
234 .unwrap_or(std::cmp::Ordering::Equal)
235 })
236 .then_with(|| b.hop_count.cmp(&a.hop_count))
237 })
238 }
239
240 pub fn get_routes(&self, destination: &str) -> Vec<&RouteEntry> {
242 if !self.config.enabled {
243 return Vec::new();
244 }
245
246 let Some(routes) = self.routes.get(destination) else {
247 return Vec::new();
248 };
249
250 let ttl = Duration::from_secs(self.config.route_ttl_secs);
251 let now = Instant::now();
252
253 routes
254 .iter()
255 .filter(|r| now.duration_since(r.last_seen) < ttl)
256 .collect()
257 }
258
259 pub fn has_route(&self, destination: &str) -> bool {
261 self.get_route(destination).is_some()
262 }
263
264 pub fn remove_neighbor(&mut self, neighbor: &str) {
266 for routes in self.routes.values_mut() {
268 routes.retain(|r| r.next_hop != neighbor);
269 }
270
271 self.routes.retain(|_, v| !v.is_empty());
273
274 self.neighbor_destinations.remove(neighbor);
276 }
277
278 pub fn cleanup_expired(&mut self) {
280 let ttl = Duration::from_secs(self.config.route_ttl_secs);
281 let now = Instant::now();
282
283 for routes in self.routes.values_mut() {
284 routes.retain(|r| now.duration_since(r.last_seen) < ttl);
285 }
286
287 self.routes.retain(|_, v| !v.is_empty());
288
289 self.neighbor_destinations.clear();
291 for (dest, routes) in &self.routes {
292 for route in routes {
293 self.neighbor_destinations
294 .entry(route.next_hop.clone())
295 .or_default()
296 .push(dest.clone());
297 }
298 }
299 }
300
301 pub fn destination_count(&self) -> usize {
303 self.routes.len()
304 }
305
306 pub fn route_count(&self) -> usize {
308 self.routes.values().map(|v| v.len()).sum()
309 }
310
311 fn enforce_size_limit(&mut self) {
316 let route_ttl_secs = self.config.route_ttl_secs as f64;
317 while self.route_count() > self.config.max_routing_table_size {
318 let worst = self
320 .routes
321 .iter()
322 .flat_map(|(dest, routes)| routes.iter().map(move |r| (dest.clone(), r)))
323 .min_by(|(_, a), (_, b)| {
324 let score_a = composite_eviction_score(a, route_ttl_secs);
325 let score_b = composite_eviction_score(b, route_ttl_secs);
326 score_a
327 .partial_cmp(&score_b)
328 .unwrap_or(std::cmp::Ordering::Equal)
329 });
330
331 if let Some((dest, route)) = worst {
332 let next_hop = route.next_hop.clone();
333 if let Some(routes) = self.routes.get_mut(&dest) {
334 routes.retain(|r| r.next_hop != next_hop);
335 if routes.is_empty() {
336 self.routes.remove(&dest);
337 }
338 }
339 if let Some(dests) = self.neighbor_destinations.get_mut(&next_hop) {
341 dests.retain(|d| d != &dest);
342 if dests.is_empty() {
343 self.neighbor_destinations.remove(&next_hop);
344 }
345 }
346 } else {
347 break;
348 }
349 }
350 }
351}
352
353fn seq_is_newer(new: u32, old: u32) -> bool {
356 new != old && new.wrapping_sub(old) < (1u32 << 31)
358}
359
360fn seq_cmp(a: u32, b: u32) -> std::cmp::Ordering {
372 if a == b {
373 std::cmp::Ordering::Equal
374 } else if seq_is_newer(a, b) {
375 std::cmp::Ordering::Greater
376 } else {
377 std::cmp::Ordering::Less
378 }
379}
380
381fn composite_eviction_score(route: &RouteEntry, route_ttl_secs: f64) -> f64 {
384 let age_secs = route.last_seen.elapsed().as_secs_f64();
385 let recency_factor = 1.0 - (age_secs / route_ttl_secs).min(1.0);
386 route.quality as f64 * recency_factor
387}
388
389impl Default for GradientRoutingTable {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct PathConfig {
398 pub forward_to_top_k: usize,
400
401 pub max_congestion_level: f32,
403
404 pub gossip: GossipConfig,
406
407 pub gradient_routing: GradientRoutingConfig,
409}
410
411impl Default for PathConfig {
412 fn default() -> Self {
413 Self {
414 forward_to_top_k: DEFAULT_FORWARD_TO_TOP_K,
415 max_congestion_level: DEFAULT_MAX_CONGESTION_LEVEL,
416 gossip: GossipConfig::default(),
417 gradient_routing: GradientRoutingConfig::default(),
418 }
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct PathScore {
425 pub signal: f32,
427
428 pub proximity: f32,
430
431 pub capacity: f32,
433
434 pub energy: f32,
436
437 pub total: f32,
439}
440
441#[derive(Debug, Clone)]
443pub struct ForwardingDecision {
444 pub peers: Vec<String>,
446 pub gossip_applied: bool,
448 pub probability: f32,
450 pub dropped_count: usize,
452}
453
454pub struct PathSelector {
456 config: PathConfig,
457 relay_manager: RelayManager,
458 local_device_id: String,
460 routing_table: GradientRoutingTable,
462}
463
464impl PathSelector {
465 pub fn new() -> Self {
467 Self::with_config(PathConfig::default(), RelayManager::new())
468 }
469
470 pub fn with_config(config: PathConfig, relay_manager: RelayManager) -> Self {
472 let routing_table = GradientRoutingTable::with_config(config.gradient_routing.clone());
473 Self {
474 config,
475 relay_manager,
476 local_device_id: String::new(),
477 routing_table,
478 }
479 }
480
481 pub fn set_local_device_id(&mut self, device_id: impl Into<String>) {
483 self.local_device_id = device_id.into();
484 }
485
486 pub fn compute_forwarding_probability(&self, visible_peer_count: usize) -> f32 {
491 let gossip = &self.config.gossip;
492
493 if !gossip.enabled || visible_peer_count <= gossip.small_network_threshold {
494 return 1.0;
495 }
496
497 let raw_probability = gossip.target_fanout as f32 / visible_peer_count as f32;
498 raw_probability.max(gossip.min_probability).min(1.0)
499 }
500
501 fn should_forward_to_peer(&self, message_id: &str, peer_id: &str, probability: f32) -> bool {
507 if probability >= 1.0 {
508 return true;
509 }
510 if probability <= 0.0 {
511 return false;
512 }
513
514 let mut hasher = DefaultHasher::new();
516 message_id.hash(&mut hasher);
517 peer_id.hash(&mut hasher);
518 self.local_device_id.hash(&mut hasher);
519 let hash = hasher.finish();
520
521 let hash_probability = (hash as f64 / u64::MAX as f64) as f32;
523
524 hash_probability < probability
525 }
526
527 pub fn select_paths_with_gossip(
540 &self,
541 message: &Message,
542 neighbors: &[NeighborInfo],
543 total_visible_peers: usize,
544 ) -> ForwardingDecision {
545 if neighbors.is_empty() {
546 return ForwardingDecision {
547 peers: Vec::new(),
548 gossip_applied: false,
549 probability: 1.0,
550 dropped_count: 0,
551 };
552 }
553
554 let bypass_gossip = self.config.gossip.priority_bypass
556 && matches!(
557 message.priority,
558 offline_protocol_core::MessagePriority::High
559 | offline_protocol_core::MessagePriority::Critical
560 );
561
562 let probability = if bypass_gossip {
564 1.0
565 } else {
566 self.compute_forwarding_probability(total_visible_peers.max(neighbors.len()))
567 };
568
569 let gossip_applied = probability < 1.0;
570
571 let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
573 .iter()
574 .map(|neighbor| {
575 let score = self.calculate_path_score(message, neighbor);
576 (neighbor.peer_id.clone(), score)
577 })
578 .collect();
579
580 scored_neighbors.retain(|(peer_id, _)| {
582 if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
583 if let Some(relay_info) = &neighbor.relay_info {
584 return relay_info.congestion_level <= self.config.max_congestion_level;
585 }
586 }
587 true
588 });
589
590 scored_neighbors.sort_by(|a, b| {
592 b.1.total
593 .partial_cmp(&a.1.total)
594 .unwrap_or(std::cmp::Ordering::Equal)
595 });
596
597 let candidates: Vec<String> = scored_neighbors
599 .into_iter()
600 .take(self.config.forward_to_top_k)
601 .map(|(peer_id, _)| peer_id)
602 .collect();
603
604 let initial_count = candidates.len();
606 let message_id = message.id.as_str();
607 let peers: Vec<String> = if gossip_applied {
608 candidates
609 .into_iter()
610 .filter(|peer_id| {
611 self.should_forward_to_peer(&message_id, peer_id.as_str(), probability)
612 })
613 .collect()
614 } else {
615 candidates
616 };
617
618 let dropped_count = initial_count.saturating_sub(peers.len());
619
620 ForwardingDecision {
621 peers,
622 gossip_applied,
623 probability,
624 dropped_count,
625 }
626 }
627
628 pub fn select_paths(&self, message: &Message, neighbors: &[NeighborInfo]) -> Vec<String> {
642 if neighbors.is_empty() {
643 return Vec::new();
644 }
645
646 let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
648 .iter()
649 .map(|neighbor| {
650 let score = self.calculate_path_score(message, neighbor);
651 (neighbor.peer_id.clone(), score)
652 })
653 .collect();
654
655 scored_neighbors.retain(|(peer_id, _)| {
657 if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
658 if let Some(relay_info) = &neighbor.relay_info {
659 return relay_info.congestion_level <= self.config.max_congestion_level;
660 }
661 }
662 true
663 });
664
665 scored_neighbors.sort_by(|a, b| {
667 b.1.total
668 .partial_cmp(&a.1.total)
669 .unwrap_or(std::cmp::Ordering::Equal)
670 });
671
672 scored_neighbors
674 .into_iter()
675 .take(self.config.forward_to_top_k)
676 .map(|(peer_id, _)| peer_id)
677 .collect()
678 }
679
680 fn calculate_path_score(&self, message: &Message, neighbor: &NeighborInfo) -> PathScore {
682 let signal_score = self.calculate_signal_score(neighbor);
683 let proximity_score = self.calculate_proximity_score(message, neighbor);
684 let capacity_score = self.calculate_capacity_score(neighbor);
685 let energy_score = self.calculate_energy_score(neighbor);
686
687 let total = (signal_score * SIGNAL_WEIGHT)
688 + (proximity_score * PROXIMITY_WEIGHT)
689 + (capacity_score * CAPACITY_WEIGHT)
690 + (energy_score * ENERGY_WEIGHT);
691
692 PathScore {
693 signal: signal_score,
694 proximity: proximity_score,
695 capacity: capacity_score,
696 energy: energy_score,
697 total,
698 }
699 }
700
701 fn calculate_signal_score(&self, neighbor: &NeighborInfo) -> f32 {
703 let rssi = neighbor.rssi;
704
705 if rssi >= EXCELLENT_RSSI_THRESHOLD {
706 EXCELLENT_SIGNAL_SCORE
707 } else if rssi >= GOOD_RSSI_THRESHOLD {
708 GOOD_SIGNAL_BASE
709 + ((rssi - GOOD_RSSI_THRESHOLD) as f32 * 30.0
710 / (EXCELLENT_RSSI_THRESHOLD - GOOD_RSSI_THRESHOLD) as f32)
711 } else if rssi >= FAIR_RSSI_THRESHOLD {
712 FAIR_SIGNAL_BASE
713 + ((rssi - FAIR_RSSI_THRESHOLD) as f32 * 30.0
714 / (GOOD_RSSI_THRESHOLD - FAIR_RSSI_THRESHOLD) as f32)
715 } else {
716 ((rssi - POOR_RSSI_MAX).max(0) as f32 * FAIR_SIGNAL_BASE
717 / (FAIR_RSSI_THRESHOLD - POOR_RSSI_MAX) as f32)
718 .max(0.0)
719 }
720 }
721
722 fn calculate_proximity_score(&self, message: &Message, neighbor: &NeighborInfo) -> f32 {
724 if let Some(hops) = neighbor.hops_to_destination {
725 let remaining_ttl = message.ttl.value();
727
728 if hops == 0 {
729 100.0
731 } else {
732 let score = if hops < remaining_ttl {
734 100.0 - (hops as f32 / remaining_ttl as f32 * 50.0)
735 } else {
736 20.0
738 };
739 score.max(0.0)
740 }
741 } else {
742 neighbor.link_quality as f32
744 }
745 }
746
747 fn calculate_capacity_score(&self, neighbor: &NeighborInfo) -> f32 {
749 if let Some(relay_info) = &neighbor.relay_info {
750 let relay_score = self.relay_manager.calculate_relay_score(relay_info);
751 relay_score.min(EXCELLENT_SIGNAL_SCORE)
752 } else {
753 NON_RELAY_BASIC_CAPACITY
754 }
755 }
756
757 fn calculate_energy_score(&self, neighbor: &NeighborInfo) -> f32 {
759 if let Some(relay_info) = &neighbor.relay_info {
760 if relay_info.is_charging {
761 CHARGING_BATTERY_SCORE
762 } else {
763 relay_info.battery_level as f32
764 }
765 } else {
766 NON_RELAY_ASSUMED_BATTERY
767 }
768 }
769
770 pub fn select_best_path(
772 &self,
773 message: &Message,
774 neighbors: &[NeighborInfo],
775 ) -> Option<String> {
776 self.select_paths(message, neighbors).into_iter().next()
777 }
778
779 pub fn should_route_around_congestion(&self, neighbor: &NeighborInfo) -> bool {
781 if let Some(relay_info) = &neighbor.relay_info {
782 relay_info.congestion_level > self.config.max_congestion_level
783 } else {
784 false
785 }
786 }
787
788 pub fn config(&self) -> &PathConfig {
790 &self.config
791 }
792
793 pub fn learn_route_from_message(
799 &mut self,
800 message: &Message,
801 from_neighbor: &str,
802 quality: f32,
803 ) {
804 let sender = message.sender.as_str();
805 let hop_count = message.hop_count.value();
806 self.routing_table.learn_route(
807 sender,
808 from_neighbor,
809 hop_count,
810 quality,
811 0, );
813 }
814
815 pub fn get_route_to(&self, destination: &str) -> Option<&RouteEntry> {
817 self.routing_table.get_route(destination)
818 }
819
820 pub fn has_route_to(&self, destination: &str) -> bool {
822 self.routing_table.has_route(destination)
823 }
824
825 pub fn select_directed_path(
828 &self,
829 message: &Message,
830 neighbors: &[NeighborInfo],
831 ) -> Option<String> {
832 let recipient = message.recipient.as_str();
833
834 for neighbor in neighbors {
836 if neighbor.peer_id == recipient {
837 return Some(neighbor.peer_id.clone());
838 }
839 }
840
841 if let Some(route) = self.routing_table.get_route(recipient) {
843 if neighbors.iter().any(|n| n.peer_id == route.next_hop) {
845 return Some(route.next_hop.clone());
846 }
847 }
848
849 self.select_best_path(message, neighbors)
851 }
852
853 pub fn remove_neighbor_routes(&mut self, neighbor: &str) {
855 self.routing_table.remove_neighbor(neighbor);
856 }
857
858 pub fn cleanup_routes(&mut self) {
860 self.routing_table.cleanup_expired();
861 }
862
863 pub fn routing_stats(&self) -> (usize, usize) {
865 (
866 self.routing_table.destination_count(),
867 self.routing_table.route_count(),
868 )
869 }
870
871 pub fn routing_table_mut(&mut self) -> &mut GradientRoutingTable {
873 &mut self.routing_table
874 }
875}
876
877impl Default for PathSelector {
878 fn default() -> Self {
879 Self::new()
880 }
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886 use crate::relay::{RelayInfo, RelayRole};
887 use offline_protocol_core::{AppId, UserId};
888
889 fn create_test_message() -> Message {
890 Message::new(
891 UserId::new("alice").unwrap(),
892 UserId::new("bob").unwrap(),
893 AppId::new("test").unwrap(),
894 "Test message",
895 )
896 }
897
898 fn create_neighbor(id: &str, rssi: i16, hops: Option<u8>, congestion: f32) -> NeighborInfo {
899 NeighborInfo {
900 peer_id: id.to_string(),
901 rssi,
902 hops_to_destination: hops,
903 link_quality: 80,
904 relay_info: Some(RelayInfo {
905 connection_count: 5,
906 battery_level: 70,
907 is_charging: false,
908 role: RelayRole::Relay,
909 link_quality: 80,
910 queue_depth: 10,
911 congestion_level: congestion,
912 }),
913 }
914 }
915
916 #[test]
917 fn test_path_selection_basic() {
918 let selector = PathSelector::new();
919 let message = create_test_message();
920
921 let neighbors = vec![
922 create_neighbor("peer1", -60, Some(2), 0.2),
923 create_neighbor("peer2", -70, Some(3), 0.3),
924 create_neighbor("peer3", -55, Some(1), 0.1),
925 ];
926
927 let paths = selector.select_paths(&message, &neighbors);
928
929 assert!(!paths.is_empty());
931 assert!(paths.len() <= 3);
932
933 assert_eq!(paths[0], "peer3");
935 }
936
937 #[test]
938 fn test_congestion_filtering() {
939 let selector = PathSelector::new();
940 let message = create_test_message();
941
942 let neighbors = vec![
943 create_neighbor("peer1", -60, Some(2), 0.9), create_neighbor("peer2", -65, Some(3), 0.3),
945 ];
946
947 let paths = selector.select_paths(&message, &neighbors);
948
949 assert!(!paths.contains(&"peer1".to_string()));
951 }
952
953 #[test]
954 fn test_direct_destination_preferred() {
955 let selector = PathSelector::new();
956 let message = create_test_message();
957
958 let neighbors = vec![
959 create_neighbor("peer1", -70, Some(5), 0.3),
960 create_neighbor("peer2", -70, Some(0), 0.1), ];
962
963 let paths = selector.select_paths(&message, &neighbors);
964
965 assert_eq!(paths[0], "peer2");
967 }
968
969 #[test]
970 fn test_signal_score_calculation() {
971 let selector = PathSelector::new();
972
973 let excellent = create_neighbor("peer1", -40, None, 0.1);
974 let score = selector.calculate_signal_score(&excellent);
975 assert_eq!(score, 100.0);
976
977 let poor = create_neighbor("peer2", -90, None, 0.1);
978 let score = selector.calculate_signal_score(&poor);
979 assert!(score < 40.0);
980 }
981
982 #[test]
983 fn test_empty_neighbors() {
984 let selector = PathSelector::new();
985 let message = create_test_message();
986
987 let paths = selector.select_paths(&message, &[]);
988 assert!(paths.is_empty());
989 }
990
991 #[test]
992 fn test_select_best_path() {
993 let selector = PathSelector::new();
994 let message = create_test_message();
995
996 let neighbors = vec![
997 create_neighbor("peer1", -60, Some(2), 0.2),
998 create_neighbor("peer2", -55, Some(1), 0.1),
999 ];
1000
1001 let best = selector.select_best_path(&message, &neighbors);
1002 assert_eq!(best, Some("peer2".to_string()));
1003 }
1004
1005 #[test]
1006 fn test_route_around_congestion() {
1007 let selector = PathSelector::new();
1008
1009 let congested = create_neighbor("peer1", -60, Some(2), 0.9);
1010 assert!(selector.should_route_around_congestion(&congested));
1011
1012 let normal = create_neighbor("peer2", -60, Some(2), 0.3);
1013 assert!(!selector.should_route_around_congestion(&normal));
1014 }
1015
1016 #[test]
1017 fn test_gossip_probability_small_network() {
1018 let selector = PathSelector::new();
1019
1020 assert_eq!(selector.compute_forwarding_probability(5), 1.0);
1022 assert_eq!(selector.compute_forwarding_probability(10), 1.0);
1023 }
1024
1025 #[test]
1026 fn test_gossip_probability_large_network() {
1027 let selector = PathSelector::new();
1028
1029 let prob = selector.compute_forwarding_probability(40);
1033 assert!((prob - 0.15).abs() < 0.01);
1034
1035 let prob = selector.compute_forwarding_probability(20);
1037 assert!((prob - 0.2).abs() < 0.01);
1038
1039 let prob = selector.compute_forwarding_probability(100);
1041 assert!((prob - 0.15).abs() < 0.01);
1042 }
1043
1044 #[test]
1045 fn test_gossip_disabled() {
1046 let config = PathConfig {
1047 gossip: GossipConfig {
1048 enabled: false,
1049 ..Default::default()
1050 },
1051 ..Default::default()
1052 };
1053 let selector = PathSelector::with_config(config, RelayManager::new());
1054
1055 assert_eq!(selector.compute_forwarding_probability(1000), 1.0);
1057 }
1058
1059 #[test]
1060 fn test_gossip_forwarding_deterministic() {
1061 let mut selector = PathSelector::new();
1062 selector.set_local_device_id("device1");
1063
1064 let message = create_test_message();
1065 let neighbors = vec![
1066 create_neighbor("peer1", -60, Some(2), 0.2),
1067 create_neighbor("peer2", -60, Some(2), 0.2),
1068 create_neighbor("peer3", -60, Some(2), 0.2),
1069 ];
1070
1071 let decision1 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1073 let decision2 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1074
1075 assert_eq!(decision1.peers, decision2.peers);
1076 assert!(decision1.gossip_applied);
1077 }
1078
1079 #[test]
1080 fn test_gossip_priority_bypass() {
1081 use offline_protocol_core::MessagePriority;
1082
1083 let mut selector = PathSelector::new();
1084 selector.set_local_device_id("device1");
1085
1086 let high_priority_message = Message::builder(
1087 UserId::new("alice").unwrap(),
1088 UserId::new("bob").unwrap(),
1089 AppId::new("test").unwrap(),
1090 )
1091 .content("Test")
1092 .priority(MessagePriority::High)
1093 .build();
1094
1095 let neighbors = vec![
1096 create_neighbor("peer1", -60, Some(2), 0.2),
1097 create_neighbor("peer2", -60, Some(2), 0.2),
1098 ];
1099
1100 let decision = selector.select_paths_with_gossip(&high_priority_message, &neighbors, 100);
1102 assert!(!decision.gossip_applied);
1103 assert_eq!(decision.probability, 1.0);
1104 }
1105
1106 #[test]
1107 fn test_gossip_reduces_forwarding_count() {
1108 let mut selector = PathSelector::new();
1109 selector.set_local_device_id("test-device");
1110
1111 let neighbors: Vec<NeighborInfo> = (0..20)
1112 .map(|i| create_neighbor(&format!("peer{}", i), -60, Some(2), 0.2))
1113 .collect();
1114
1115 let message = create_test_message();
1116
1117 let decision = selector.select_paths_with_gossip(&message, &neighbors, 100);
1120
1121 assert!(decision.gossip_applied);
1123 assert!((decision.probability - 0.15).abs() < 0.01);
1125 }
1126
1127 #[test]
1130 fn test_dsdv_prefer_higher_sequence_number() {
1131 let mut table = GradientRoutingTable::new();
1132 let dest = "alice";
1133
1134 table.learn_route(dest, "peer_a", 2, 0.9, 1);
1135 table.learn_route(dest, "peer_b", 3, 0.8, 2);
1136
1137 let route = table.get_route(dest).expect("should have route");
1138 assert_eq!(route.next_hop, "peer_b");
1139 assert_eq!(route.sequence_number, 2);
1140 }
1141
1142 #[test]
1143 fn test_dsdv_reject_stale_update_same_next_hop() {
1144 let mut table = GradientRoutingTable::new();
1145 let dest = "bob";
1146
1147 table.learn_route(dest, "peer_a", 2, 0.9, 2);
1148 table.learn_route(dest, "peer_a", 5, 0.5, 1); let route = table.get_route(dest).expect("should have route");
1151 assert_eq!(route.next_hop, "peer_a");
1152 assert_eq!(route.hop_count, 2);
1153 assert_eq!(route.sequence_number, 2);
1154 }
1155
1156 #[test]
1157 fn test_dsdv_accept_same_sequence_better_hop_count() {
1158 let mut table = GradientRoutingTable::new();
1159 let dest = "carol";
1160
1161 table.learn_route(dest, "peer_a", 4, 0.7, 2);
1162 table.learn_route(dest, "peer_a", 2, 0.8, 2); let route = table.get_route(dest).expect("should have route");
1165 assert_eq!(route.hop_count, 2);
1166 assert_eq!(route.sequence_number, 2);
1167 }
1168
1169 #[test]
1170 fn test_global_eviction_prefers_low_quality_routes() {
1171 let config = GradientRoutingConfig {
1172 max_routing_table_size: 3,
1173 max_routes_per_destination: 3,
1174 route_ttl_secs: 300,
1175 ..Default::default()
1176 };
1177 let mut table = GradientRoutingTable::with_config(config);
1178
1179 table.learn_route("alice", "hop_good", 1, 0.9, 0);
1181 table.learn_route("bob", "hop_low1", 3, 0.1, 0);
1183 table.learn_route("carol", "hop_low2", 4, 0.1, 0);
1184
1185 assert_eq!(table.route_count(), 3);
1186
1187 table.learn_route("dave", "hop_new", 2, 0.5, 0);
1189 assert_eq!(table.route_count(), 3);
1190
1191 assert!(
1193 table.has_route("alice"),
1194 "High-quality route to alice should survive eviction"
1195 );
1196 assert!(table.has_route("dave"), "Newly inserted route should exist");
1198 }
1199
1200 #[test]
1201 fn test_dsdv_eviction_removes_worst_route() {
1202 let config = GradientRoutingConfig {
1203 max_routes_per_destination: 2,
1204 ..Default::default()
1205 };
1206 let mut table = GradientRoutingTable::with_config(config);
1207 let dest = "dest";
1208
1209 table.learn_route(dest, "hop1", 1, 0.9, 10);
1210 table.learn_route(dest, "hop2", 2, 0.8, 10);
1211 table.learn_route(dest, "hop3", 3, 0.7, 5);
1213
1214 let route = table.get_route(dest).expect("should have route");
1215 assert_eq!(route.next_hop, "hop1");
1216 let routes = table.get_routes(dest);
1217 assert_eq!(routes.len(), 2);
1218 assert!(routes.iter().any(|r| r.next_hop == "hop1"));
1219 assert!(routes.iter().any(|r| r.next_hop == "hop3"));
1220 assert!(
1221 !routes.iter().any(|r| r.next_hop == "hop2"),
1222 "hop2 (worst of original two) should be evicted"
1223 );
1224 }
1225
1226 #[test]
1229 fn test_seq_is_newer_basic() {
1230 assert!(seq_is_newer(2, 1));
1231 assert!(seq_is_newer(100, 50));
1232 assert!(!seq_is_newer(1, 2));
1233 assert!(!seq_is_newer(5, 5)); }
1235
1236 #[test]
1237 fn test_seq_is_newer_wrapping() {
1238 assert!(seq_is_newer(0, u32::MAX));
1240 assert!(seq_is_newer(1, u32::MAX));
1241 assert!(seq_is_newer(5, u32::MAX - 2));
1242 assert!(!seq_is_newer(u32::MAX, 0));
1244 assert!(!seq_is_newer(u32::MAX - 2, 5));
1245 }
1246
1247 #[test]
1248 fn test_dsdv_wrapping_sequence_update() {
1249 let mut table = GradientRoutingTable::new();
1250 let dest = "alice";
1251
1252 table.learn_route(dest, "peer_a", 2, 0.9, u32::MAX - 1);
1254
1255 table.learn_route(dest, "peer_a", 2, 0.9, 0);
1257
1258 let route = table.get_route(dest).expect("should have route");
1259 assert_eq!(
1260 route.sequence_number, 0,
1261 "Wrapped sequence should be accepted"
1262 );
1263 }
1264
1265 #[test]
1266 fn test_seq_cmp_basic_and_wrapping() {
1267 use std::cmp::Ordering;
1268 assert_eq!(seq_cmp(5, 5), Ordering::Equal);
1269 assert_eq!(seq_cmp(10, 5), Ordering::Greater);
1270 assert_eq!(seq_cmp(5, 10), Ordering::Less);
1271 assert_eq!(seq_cmp(0, u32::MAX), Ordering::Greater);
1273 assert_eq!(seq_cmp(u32::MAX, 0), Ordering::Less);
1274 }
1275
1276 #[test]
1277 fn test_get_route_prefers_wrapped_sequence() {
1278 let mut table = GradientRoutingTable::new();
1279 let dest = "alice";
1280
1281 table.learn_route(dest, "peer_old", 2, 0.9, u32::MAX - 1);
1283 table.learn_route(dest, "peer_new", 3, 0.7, 1);
1284
1285 let route = table.get_route(dest).expect("should have route");
1286 assert_eq!(
1287 route.next_hop, "peer_new",
1288 "get_route must prefer wrapped (newer) sequence number"
1289 );
1290 assert_eq!(route.sequence_number, 1);
1291 }
1292
1293 #[test]
1294 fn test_per_dest_eviction_respects_wrapping() {
1295 let config = GradientRoutingConfig {
1296 max_routes_per_destination: 2,
1297 ..Default::default()
1298 };
1299 let mut table = GradientRoutingTable::with_config(config);
1300 let dest = "bob";
1301
1302 table.learn_route(dest, "hop_old", 2, 0.8, u32::MAX - 5);
1304 table.learn_route(dest, "hop_mid", 2, 0.8, u32::MAX);
1305
1306 table.learn_route(dest, "hop_new", 2, 0.8, 2);
1308
1309 let routes = table.get_routes(dest);
1310 assert_eq!(routes.len(), 2);
1311 assert!(
1312 routes.iter().any(|r| r.next_hop == "hop_new"),
1313 "Wrapped-sequence route must survive"
1314 );
1315 assert!(
1316 routes.iter().any(|r| r.next_hop == "hop_mid"),
1317 "Second-best sequence route must survive"
1318 );
1319 assert!(
1320 !routes.iter().any(|r| r.next_hop == "hop_old"),
1321 "Oldest sequence route should be evicted"
1322 );
1323 }
1324
1325 #[test]
1326 fn test_seq_is_newer_half_space_boundary() {
1327 let half = 1u32 << 31;
1330 assert!(
1331 !seq_is_newer(half, 0),
1332 "Half-space distance is ambiguous, should not be newer"
1333 );
1334 assert!(!seq_is_newer(0, half), "Reverse half-space also not newer");
1335 }
1336}