Skip to main content

ipfrs_network/
overlay_network.rs

1//! Structured overlay network manager supporting multiple topology strategies.
2//!
3//! This module provides [`OverlayNetworkManager`], which models a logical overlay
4//! network on top of physical connectivity. It supports Chord finger-table routing,
5//! Pastry prefix routing, Kademlia greedy forwarding, FullMesh direct routing, and
6//! spanning Tree routing—all computed purely in-memory without I/O.
7//!
8//! # Routing algorithms
9//!
10//! | Topology  | Route selection | Latency estimate |
11//! |-----------|-----------------|-----------------|
12//! | FullMesh  | direct 1-hop    | 20 ms           |
13//! | Chord     | finger table logâ‚‚(N) hops | 20 ms/hop |
14//! | Tree      | BFS in spanning tree | 20 ms/hop |
15//! | Pastry    | greedy numeric proximity | 20 ms/hop |
16//! | Kademlia  | greedy numeric proximity | 20 ms/hop |
17
18use std::collections::{HashMap, HashSet, VecDeque};
19
20use thiserror::Error;
21
22// ---------------------------------------------------------------------------
23// Error type
24// ---------------------------------------------------------------------------
25
26/// Errors returned by [`OverlayNetworkManager`].
27#[derive(Debug, Error, Clone, PartialEq, Eq)]
28pub enum OverlayError {
29    /// The specified node ID is not present in the overlay.
30    #[error("node not found: {0}")]
31    NodeNotFound(u64),
32
33    /// The overlay has reached `max_nodes` and cannot accept new members.
34    #[error("overlay network is full")]
35    NetworkFull,
36
37    /// No route exists between the two node IDs.
38    #[error("no route found from {from} to {to}")]
39    RouteNotFound {
40        /// Source node.
41        from: u64,
42        /// Destination node.
43        to: u64,
44    },
45
46    /// Source and destination are the same node.
47    #[error("cannot route a node to itself")]
48    SelfRoute,
49}
50
51// ---------------------------------------------------------------------------
52// Topology enum
53// ---------------------------------------------------------------------------
54
55/// Overlay topology strategy used by [`OverlayNetworkManager`].
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum OverlayTopology {
58    /// Chord-style DHT ring with `fingers` finger-table entries per node.
59    Chord {
60        /// Number of finger-table entries per node.
61        fingers: usize,
62    },
63    /// Pastry-style prefix routing.
64    Pastry {
65        /// Size of the leaf set (closest nodes by ID).
66        leaf_set_size: usize,
67        /// Number of rows in the routing table.
68        routing_table_rows: usize,
69    },
70    /// Kademlia-style XOR-metric greedy routing (higher-level overlay).
71    Kademlia {
72        /// Replication factor (bucket size).
73        k: usize,
74        /// Parallelism factor for lookups.
75        alpha: usize,
76    },
77    /// Every node is directly connected to every other node (small clusters).
78    FullMesh,
79    /// Spanning tree overlay with a fixed branching factor.
80    Tree {
81        /// Number of children per parent in the spanning tree.
82        branching_factor: usize,
83    },
84}
85
86impl OverlayTopology {
87    /// Human-readable name of the topology.
88    pub fn name(&self) -> &'static str {
89        match self {
90            OverlayTopology::Chord { .. } => "Chord",
91            OverlayTopology::Pastry { .. } => "Pastry",
92            OverlayTopology::Kademlia { .. } => "Kademlia",
93            OverlayTopology::FullMesh => "FullMesh",
94            OverlayTopology::Tree { .. } => "Tree",
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// Node type
101// ---------------------------------------------------------------------------
102
103/// A participant in the overlay network.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct OverlayNode {
106    /// Unique numeric node identifier (used as the ring/XOR key).
107    pub node_id: u64,
108    /// Network address (e.g. multiaddr or IP:port string).
109    pub address: String,
110    /// Unix-millisecond timestamp when the node joined.
111    pub joined_at: u64,
112    /// Unix-millisecond timestamp of the last received heartbeat.
113    pub last_heartbeat: u64,
114    /// Arbitrary key-value metadata attached to the node.
115    pub metadata: HashMap<String, String>,
116}
117
118impl OverlayNode {
119    /// Create a new overlay node.
120    pub fn new(node_id: u64, address: impl Into<String>, now: u64) -> Self {
121        Self {
122            node_id,
123            address: address.into(),
124            joined_at: now,
125            last_heartbeat: now,
126            metadata: HashMap::new(),
127        }
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Route type
133// ---------------------------------------------------------------------------
134
135/// A computed route through the overlay network.
136#[derive(Debug, Clone, PartialEq)]
137pub struct OverlayRoute {
138    /// Ordered list of node IDs that form the path (including source and destination).
139    pub hops: Vec<u64>,
140    /// Estimated end-to-end latency in milliseconds (`hop_count * 20.0`).
141    pub latency_estimate_ms: f64,
142    /// Number of hops in the route.
143    pub hop_count: usize,
144}
145
146impl OverlayRoute {
147    fn new(hops: Vec<u64>) -> Self {
148        // hop_count is the number of inter-node edges, i.e. len - 1, but we define
149        // it as the number of nodes in the path minus 1 (edges traversed).
150        let hop_count = hops.len().saturating_sub(1);
151        let latency_estimate_ms = hop_count as f64 * 20.0;
152        Self {
153            hops,
154            latency_estimate_ms,
155            hop_count,
156        }
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Message type
162// ---------------------------------------------------------------------------
163
164/// A message routed through the overlay network.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct OverlayMessage {
167    /// FNV-1a hash of `(from XOR to XOR created_at)`.
168    pub id: u64,
169    /// Sender node ID.
170    pub from: u64,
171    /// Receiver node ID.
172    pub to: u64,
173    /// Time-to-live: maximum hops before the message is discarded.
174    pub ttl: u8,
175    /// Byte length of the message payload.
176    pub payload_size: usize,
177    /// Unix-millisecond creation timestamp.
178    pub created_at: u64,
179}
180
181impl OverlayMessage {
182    /// Create a new overlay message, computing the FNV-1a ID automatically.
183    pub fn new(from: u64, to: u64, ttl: u8, payload_size: usize, created_at: u64) -> Self {
184        let id = fnv1a_hash(from ^ to ^ created_at);
185        Self {
186            id,
187            from,
188            to,
189            ttl,
190            payload_size,
191            created_at,
192        }
193    }
194}
195
196/// Inline FNV-1a hash for a single 64-bit value.
197#[inline]
198fn fnv1a_hash(value: u64) -> u64 {
199    const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
200    const FNV_PRIME: u64 = 1_099_511_628_211;
201    let bytes = value.to_le_bytes();
202    let mut hash = FNV_OFFSET_BASIS;
203    for b in bytes {
204        hash ^= b as u64;
205        hash = hash.wrapping_mul(FNV_PRIME);
206    }
207    hash
208}
209
210// ---------------------------------------------------------------------------
211// Stats type
212// ---------------------------------------------------------------------------
213
214/// Snapshot of overlay network statistics.
215#[derive(Debug, Clone, PartialEq)]
216pub struct OverlayStats {
217    /// Number of nodes currently in the overlay.
218    pub node_count: usize,
219    /// Mean age (in ms) of the last heartbeat across all nodes.
220    pub avg_heartbeat_age_ms: f64,
221    /// Estimated maximum path length across the overlay.
222    pub estimated_diameter: f64,
223    /// Human-readable name of the active topology.
224    pub topology_name: String,
225}
226
227// ---------------------------------------------------------------------------
228// Manager
229// ---------------------------------------------------------------------------
230
231/// Manages a logical overlay network of nodes with topology-aware routing.
232///
233/// The manager is intentionally synchronous and allocation-light: it keeps
234/// a flat `HashMap` of nodes and computes routes on demand without spawning
235/// tasks or doing I/O.
236///
237/// # Example
238///
239/// ```rust
240/// use ipfrs_network::{OverlayNetworkManager, OverlayTopology, OverlayNode};
241///
242/// let mut mgr = OverlayNetworkManager::new(1, OverlayTopology::FullMesh, 128);
243/// let node = OverlayNode::new(2, "127.0.0.1:4001", 0);
244/// mgr.join(node).expect("join should succeed in example");
245/// let route = mgr.route(1, 2, 0).expect("route should succeed in example");
246/// assert_eq!(route.hop_count, 1);
247/// ```
248#[derive(Debug)]
249pub struct OverlayNetworkManager {
250    /// Active topology strategy.
251    topology: OverlayTopology,
252    /// All nodes known to the overlay, keyed by `node_id`.
253    nodes: HashMap<u64, OverlayNode>,
254    /// Node ID of this local instance (always treated as present).
255    local_id: u64,
256    /// Hard upper bound on nodes that can join.
257    max_nodes: usize,
258}
259
260impl OverlayNetworkManager {
261    /// Create a new overlay network manager.
262    ///
263    /// The local node (identified by `local_id`) is implicitly part of the
264    /// overlay but is **not** inserted into `nodes`; callers should [`join`]
265    /// it if they want it visible to routing queries.
266    ///
267    /// [`join`]: OverlayNetworkManager::join
268    pub fn new(local_id: u64, topology: OverlayTopology, max_nodes: usize) -> Self {
269        Self {
270            topology,
271            nodes: HashMap::new(),
272            local_id,
273            max_nodes,
274        }
275    }
276
277    // ------------------------------------------------------------------
278    // Membership
279    // ------------------------------------------------------------------
280
281    /// Add a node to the overlay.
282    ///
283    /// Returns [`OverlayError::NetworkFull`] if `max_nodes` has been reached.
284    /// If the node is already present its record is silently updated.
285    pub fn join(&mut self, node: OverlayNode) -> Result<(), OverlayError> {
286        // Allow update of existing node without checking capacity.
287        if let std::collections::hash_map::Entry::Occupied(mut e) = self.nodes.entry(node.node_id) {
288            e.insert(node);
289            return Ok(());
290        }
291        if self.nodes.len() >= self.max_nodes {
292            return Err(OverlayError::NetworkFull);
293        }
294        self.nodes.insert(node.node_id, node);
295        Ok(())
296    }
297
298    /// Remove a node from the overlay.
299    ///
300    /// Returns [`OverlayError::NodeNotFound`] if the node is not present.
301    pub fn leave(&mut self, node_id: u64) -> Result<(), OverlayError> {
302        self.nodes
303            .remove(&node_id)
304            .map(|_| ())
305            .ok_or(OverlayError::NodeNotFound(node_id))
306    }
307
308    /// Record a heartbeat for a node, updating its `last_heartbeat` timestamp.
309    pub fn heartbeat(&mut self, node_id: u64, now: u64) -> Result<(), OverlayError> {
310        self.nodes
311            .get_mut(&node_id)
312            .map(|n| n.last_heartbeat = now)
313            .ok_or(OverlayError::NodeNotFound(node_id))
314    }
315
316    /// Remove nodes whose `last_heartbeat` is older than `max_age_ms` milliseconds.
317    ///
318    /// Returns the number of nodes evicted.
319    pub fn evict_stale(&mut self, max_age_ms: u64, now: u64) -> usize {
320        let cutoff = now.saturating_sub(max_age_ms);
321        let stale: Vec<u64> = self
322            .nodes
323            .values()
324            .filter(|n| n.last_heartbeat < cutoff)
325            .map(|n| n.node_id)
326            .collect();
327        let count = stale.len();
328        for id in stale {
329            self.nodes.remove(&id);
330        }
331        count
332    }
333
334    /// Return the total number of nodes in the overlay.
335    pub fn node_count(&self) -> usize {
336        self.nodes.len()
337    }
338
339    // ------------------------------------------------------------------
340    // Routing
341    // ------------------------------------------------------------------
342
343    /// Compute a route from `from` to `to` using the active topology.
344    ///
345    /// Returns [`OverlayError::SelfRoute`] when `from == to`, and
346    /// [`OverlayError::NodeNotFound`] when either endpoint is absent.
347    pub fn route(&self, from: u64, to: u64, now: u64) -> Result<OverlayRoute, OverlayError> {
348        let _ = now; // reserved for future timestamp-aware routing
349        if from == to {
350            return Err(OverlayError::SelfRoute);
351        }
352        if !self.nodes.contains_key(&from) {
353            return Err(OverlayError::NodeNotFound(from));
354        }
355        if !self.nodes.contains_key(&to) {
356            return Err(OverlayError::NodeNotFound(to));
357        }
358
359        match &self.topology {
360            OverlayTopology::FullMesh => self.route_full_mesh(from, to),
361            OverlayTopology::Chord { fingers } => self.route_chord(from, to, *fingers),
362            OverlayTopology::Tree { branching_factor } => {
363                self.route_tree(from, to, *branching_factor)
364            }
365            OverlayTopology::Pastry { .. } | OverlayTopology::Kademlia { .. } => {
366                self.route_greedy_proximity(from, to)
367            }
368        }
369    }
370
371    /// FullMesh: direct one-hop route between any two present nodes.
372    fn route_full_mesh(&self, from: u64, to: u64) -> Result<OverlayRoute, OverlayError> {
373        Ok(OverlayRoute::new(vec![from, to]))
374    }
375
376    /// Chord: route via finger table.
377    ///
378    /// The finger table for node `n` contains up to `fingers` entries.
379    /// Entry `i` points to the node whose ID is nearest (in ring arithmetic)
380    /// to `n + 2^i`. Routing greedily advances via fingers until the
381    /// destination is reached or no better finger exists.
382    fn route_chord(
383        &self,
384        from: u64,
385        to: u64,
386        fingers: usize,
387    ) -> Result<OverlayRoute, OverlayError> {
388        const MAX_HOPS: usize = 64;
389        let mut path = vec![from];
390        let mut current = from;
391
392        // Collect all IDs for efficient lookups.
393        let all_ids: Vec<u64> = self.nodes.keys().copied().collect();
394
395        // Distance on the Chord ring: how far `target` is clockwise from `base`.
396        let ring_distance = |base: u64, target: u64| -> u64 {
397            if target >= base {
398                target - base
399            } else {
400                u64::MAX - base + target + 1
401            }
402        };
403
404        // Find the node whose ID is closest to `ideal` in a clockwise sense.
405        let best_finger = |node: u64, ideal: u64| -> u64 {
406            all_ids
407                .iter()
408                .copied()
409                .min_by_key(|&id| ring_distance(ideal, id))
410                .unwrap_or(node)
411        };
412
413        while current != to && path.len() <= MAX_HOPS {
414            // Look for the best finger to bring us closer to `to`.
415            let mut best_next = current;
416            let current_dist = ring_distance(current, to);
417
418            let effective_fingers = fingers.max(1);
419            for i in 0..effective_fingers {
420                // ideal successor for finger i: current + 2^i (wrapping)
421                let ideal = current.wrapping_add(1u64 << (i.min(63)));
422                let finger_node = best_finger(current, ideal);
423                let finger_dist = ring_distance(finger_node, to);
424                if finger_dist < current_dist || finger_node == to {
425                    // pick the finger that gets us closest (smallest ring-dist to dest)
426                    let best_dist = ring_distance(best_next, to);
427                    if best_next == current || finger_dist < best_dist {
428                        best_next = finger_node;
429                    }
430                }
431            }
432
433            if best_next == current || path.contains(&best_next) {
434                // No progress; check if `to` is directly reachable (i.e., in the node set)
435                if self.nodes.contains_key(&to) && !path.contains(&to) {
436                    path.push(to);
437                    break;
438                }
439                return Err(OverlayError::RouteNotFound { from, to });
440            }
441            path.push(best_next);
442            current = best_next;
443        }
444
445        if current != to {
446            return Err(OverlayError::RouteNotFound { from, to });
447        }
448        Ok(OverlayRoute::new(path))
449    }
450
451    /// Tree: BFS through the spanning tree to find the path from `from` to `to`.
452    ///
453    /// Nodes are ordered by `joined_at` to form a deterministic tree; root
454    /// is the node with the earliest `joined_at`. Each parent has up to
455    /// `branching_factor` children.
456    fn route_tree(
457        &self,
458        from: u64,
459        to: u64,
460        branching_factor: usize,
461    ) -> Result<OverlayRoute, OverlayError> {
462        // Build the spanning tree adjacency list.
463        // Nodes sorted by joined_at (tie-break by node_id).
464        let mut sorted: Vec<&OverlayNode> = self.nodes.values().collect();
465        sorted.sort_by_key(|n| (n.joined_at, n.node_id));
466
467        let bf = branching_factor.max(1);
468
469        // Build parent→children mapping.
470        let mut children: HashMap<u64, Vec<u64>> = HashMap::new();
471        // parent of each node
472        let mut parent: HashMap<u64, Option<u64>> = HashMap::new();
473
474        for (idx, node) in sorted.iter().enumerate() {
475            if idx == 0 {
476                parent.insert(node.node_id, None);
477            } else {
478                // Parent index in the sorted list (0-based, 1-indexed children of root)
479                let parent_idx = (idx - 1) / bf;
480                let parent_id = sorted[parent_idx].node_id;
481                parent.insert(node.node_id, Some(parent_id));
482                children.entry(parent_id).or_default().push(node.node_id);
483            }
484        }
485
486        // BFS from `from` to `to` in the undirected tree.
487        let mut visited: HashSet<u64> = HashSet::new();
488        let mut queue: VecDeque<(u64, Vec<u64>)> = VecDeque::new();
489        queue.push_back((from, vec![from]));
490        visited.insert(from);
491
492        while let Some((current, path)) = queue.pop_front() {
493            if current == to {
494                return Ok(OverlayRoute::new(path));
495            }
496            // Explore children
497            if let Some(kids) = children.get(&current) {
498                for &child in kids {
499                    if !visited.contains(&child) {
500                        visited.insert(child);
501                        let mut new_path = path.clone();
502                        new_path.push(child);
503                        queue.push_back((child, new_path));
504                    }
505                }
506            }
507            // Explore parent
508            if let Some(Some(par)) = parent.get(&current) {
509                if !visited.contains(par) {
510                    visited.insert(*par);
511                    let mut new_path = path.clone();
512                    new_path.push(*par);
513                    queue.push_back((*par, new_path));
514                }
515            }
516        }
517
518        Err(OverlayError::RouteNotFound { from, to })
519    }
520
521    /// Greedy forwarding by numeric (absolute) proximity — used for Pastry and Kademlia.
522    ///
523    /// At each hop we pick the neighbor (in the overlay's neighbor set for the
524    /// current node) that is numerically closest to `to`. We cap at 32 hops to
525    /// guarantee termination.
526    fn route_greedy_proximity(&self, from: u64, to: u64) -> Result<OverlayRoute, OverlayError> {
527        const MAX_HOPS: usize = 32;
528        let mut path = vec![from];
529        let mut current = from;
530        let mut visited: HashSet<u64> = HashSet::new();
531        visited.insert(from);
532
533        while current != to && path.len() <= MAX_HOPS {
534            let nbrs = self.neighbors_of(current);
535            // Find the neighbor closest to `to` (by absolute XOR distance).
536            let best = nbrs
537                .into_iter()
538                .filter(|id| !visited.contains(id))
539                .min_by_key(|&id| xor_distance(id, to));
540
541            match best {
542                None => {
543                    // No unvisited neighbors; check direct connection
544                    if self.nodes.contains_key(&to) && !path.contains(&to) {
545                        path.push(to);
546                        return Ok(OverlayRoute::new(path));
547                    }
548                    return Err(OverlayError::RouteNotFound { from, to });
549                }
550                Some(next) => {
551                    if next == to {
552                        path.push(to);
553                        return Ok(OverlayRoute::new(path));
554                    }
555                    // Check that we are making progress (getting closer).
556                    let current_dist = xor_distance(current, to);
557                    let next_dist = xor_distance(next, to);
558                    if next_dist >= current_dist {
559                        // Stuck; jump directly if reachable.
560                        if self.nodes.contains_key(&to) {
561                            path.push(to);
562                            return Ok(OverlayRoute::new(path));
563                        }
564                        return Err(OverlayError::RouteNotFound { from, to });
565                    }
566                    visited.insert(next);
567                    path.push(next);
568                    current = next;
569                }
570            }
571        }
572
573        if current == to {
574            Ok(OverlayRoute::new(path))
575        } else {
576            Err(OverlayError::RouteNotFound { from, to })
577        }
578    }
579
580    // ------------------------------------------------------------------
581    // Neighbors
582    // ------------------------------------------------------------------
583
584    /// Return the set of logical neighbors for `node_id` under the active topology.
585    ///
586    /// Returns an empty `Vec` if the node is not present.
587    pub fn neighbors(&self, node_id: u64) -> Vec<u64> {
588        if !self.nodes.contains_key(&node_id) {
589            return Vec::new();
590        }
591        self.neighbors_of(node_id)
592    }
593
594    /// Internal neighbor computation (does not check node existence).
595    fn neighbors_of(&self, node_id: u64) -> Vec<u64> {
596        match &self.topology {
597            OverlayTopology::FullMesh => {
598                // Every other node is a neighbor.
599                self.nodes
600                    .keys()
601                    .copied()
602                    .filter(|&id| id != node_id)
603                    .collect()
604            }
605            OverlayTopology::Chord { fingers } => self.chord_fingers(node_id, *fingers),
606            OverlayTopology::Tree { branching_factor } => {
607                self.tree_neighbors(node_id, *branching_factor)
608            }
609            OverlayTopology::Pastry { leaf_set_size, .. } => {
610                self.closest_n_neighbors(node_id, *leaf_set_size)
611            }
612            OverlayTopology::Kademlia { k, .. } => self.closest_n_neighbors(node_id, *k),
613        }
614    }
615
616    /// Chord finger table entries for `node_id`.
617    fn chord_fingers(&self, node_id: u64, fingers: usize) -> Vec<u64> {
618        let effective = fingers.max(1);
619        let all_ids: Vec<u64> = self
620            .nodes
621            .keys()
622            .copied()
623            .filter(|&id| id != node_id)
624            .collect();
625        let mut seen: HashSet<u64> = HashSet::new();
626        let mut result = Vec::new();
627
628        for i in 0..effective {
629            let ideal = node_id.wrapping_add(1u64 << (i.min(63)));
630            // Node closest to `ideal` on the ring (clockwise distance).
631            let best = all_ids.iter().copied().min_by_key(|&id| {
632                if id >= ideal {
633                    id - ideal
634                } else {
635                    u64::MAX - ideal + id + 1
636                }
637            });
638            if let Some(id) = best {
639                if seen.insert(id) {
640                    result.push(id);
641                }
642            }
643        }
644        result
645    }
646
647    /// Return up to `n` nodes numerically closest to `node_id` (by XOR distance).
648    fn closest_n_neighbors(&self, node_id: u64, n: usize) -> Vec<u64> {
649        let mut candidates: Vec<u64> = self
650            .nodes
651            .keys()
652            .copied()
653            .filter(|&id| id != node_id)
654            .collect();
655        candidates.sort_by_key(|&id| xor_distance(id, node_id));
656        candidates.truncate(n);
657        candidates
658    }
659
660    /// Neighbors of `node_id` in the spanning tree (parent + children).
661    fn tree_neighbors(&self, node_id: u64, branching_factor: usize) -> Vec<u64> {
662        let mut sorted: Vec<&OverlayNode> = self.nodes.values().collect();
663        sorted.sort_by_key(|n| (n.joined_at, n.node_id));
664        let bf = branching_factor.max(1);
665
666        let pos = match sorted.iter().position(|n| n.node_id == node_id) {
667            Some(p) => p,
668            None => return Vec::new(),
669        };
670
671        let mut neighbors = Vec::new();
672
673        // Parent
674        if pos > 0 {
675            let parent_idx = (pos - 1) / bf;
676            neighbors.push(sorted[parent_idx].node_id);
677        }
678
679        // Children
680        let first_child = pos * bf + 1;
681        for c in first_child..first_child + bf {
682            if c < sorted.len() {
683                neighbors.push(sorted[c].node_id);
684            }
685        }
686
687        neighbors
688    }
689
690    // ------------------------------------------------------------------
691    // Diameter estimate
692    // ------------------------------------------------------------------
693
694    /// Estimate the maximum path length (diameter) across the overlay.
695    ///
696    /// | Topology | Formula |
697    /// |----------|---------|
698    /// | FullMesh | 1.0 |
699    /// | Chord / Kademlia / Pastry | logâ‚‚(max(n, 2)) |
700    /// | Tree | log_{bf}(max(n, 2)) |
701    pub fn diameter_estimate(&self) -> f64 {
702        let n = self.nodes.len().max(2) as f64;
703        match &self.topology {
704            OverlayTopology::FullMesh => 1.0,
705            OverlayTopology::Chord { .. }
706            | OverlayTopology::Kademlia { .. }
707            | OverlayTopology::Pastry { .. } => n.log2(),
708            OverlayTopology::Tree { branching_factor } => {
709                let bf = (*branching_factor).max(2) as f64;
710                n.log(bf)
711            }
712        }
713    }
714
715    // ------------------------------------------------------------------
716    // Stats
717    // ------------------------------------------------------------------
718
719    /// Return a snapshot of statistics at the given wall-clock timestamp `now`.
720    pub fn stats(&self, now: u64) -> OverlayStats {
721        let node_count = self.nodes.len();
722        let avg_heartbeat_age_ms = if node_count == 0 {
723            0.0
724        } else {
725            let total_age: u64 = self
726                .nodes
727                .values()
728                .map(|n| now.saturating_sub(n.last_heartbeat))
729                .sum();
730            total_age as f64 / node_count as f64
731        };
732        OverlayStats {
733            node_count,
734            avg_heartbeat_age_ms,
735            estimated_diameter: self.diameter_estimate(),
736            topology_name: self.topology.name().to_string(),
737        }
738    }
739
740    // ------------------------------------------------------------------
741    // Accessors
742    // ------------------------------------------------------------------
743
744    /// Return a reference to the node record for `node_id`, if present.
745    pub fn get_node(&self, node_id: u64) -> Option<&OverlayNode> {
746        self.nodes.get(&node_id)
747    }
748
749    /// Return the local node ID.
750    pub fn local_id(&self) -> u64 {
751        self.local_id
752    }
753
754    /// Return a reference to the active topology.
755    pub fn topology(&self) -> &OverlayTopology {
756        &self.topology
757    }
758}
759
760// ---------------------------------------------------------------------------
761// Helper: XOR distance
762// ---------------------------------------------------------------------------
763
764/// XOR distance between two node IDs.
765#[inline]
766fn xor_distance(a: u64, b: u64) -> u64 {
767    a ^ b
768}
769
770// ---------------------------------------------------------------------------
771// Tests
772// ---------------------------------------------------------------------------
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    // ------------------------------------------------------------------
779    // Helpers
780    // ------------------------------------------------------------------
781
782    fn make_node(id: u64, joined_at: u64) -> OverlayNode {
783        OverlayNode::new(id, format!("127.0.0.1:{}", 4000 + id), joined_at)
784    }
785
786    fn populate(mgr: &mut OverlayNetworkManager, ids: &[u64], base_time: u64) {
787        for (i, &id) in ids.iter().enumerate() {
788            mgr.join(make_node(id, base_time + i as u64))
789                .expect("test: join should succeed during populate");
790        }
791    }
792
793    // ------------------------------------------------------------------
794    // OverlayNode tests
795    // ------------------------------------------------------------------
796
797    #[test]
798    fn test_overlay_node_new() {
799        let node = make_node(42, 1000);
800        assert_eq!(node.node_id, 42);
801        assert_eq!(node.joined_at, 1000);
802        assert_eq!(node.last_heartbeat, 1000);
803        assert!(node.metadata.is_empty());
804    }
805
806    #[test]
807    fn test_overlay_node_address() {
808        let node = OverlayNode::new(7, "/ip4/1.2.3.4/tcp/4001", 0);
809        assert_eq!(node.address, "/ip4/1.2.3.4/tcp/4001");
810    }
811
812    // ------------------------------------------------------------------
813    // OverlayMessage tests
814    // ------------------------------------------------------------------
815
816    #[test]
817    fn test_overlay_message_id_deterministic() {
818        let m1 = OverlayMessage::new(1, 2, 10, 100, 999);
819        let m2 = OverlayMessage::new(1, 2, 10, 100, 999);
820        assert_eq!(m1.id, m2.id);
821    }
822
823    #[test]
824    fn test_overlay_message_id_changes_with_inputs() {
825        let m1 = OverlayMessage::new(1, 2, 10, 100, 999);
826        let m2 = OverlayMessage::new(1, 3, 10, 100, 999);
827        assert_ne!(m1.id, m2.id);
828    }
829
830    #[test]
831    fn test_overlay_message_fields() {
832        let msg = OverlayMessage::new(10, 20, 5, 256, 12345);
833        assert_eq!(msg.from, 10);
834        assert_eq!(msg.to, 20);
835        assert_eq!(msg.ttl, 5);
836        assert_eq!(msg.payload_size, 256);
837        assert_eq!(msg.created_at, 12345);
838    }
839
840    // ------------------------------------------------------------------
841    // OverlayRoute tests
842    // ------------------------------------------------------------------
843
844    #[test]
845    fn test_overlay_route_direct() {
846        let r = OverlayRoute::new(vec![1, 2]);
847        assert_eq!(r.hop_count, 1);
848        assert!((r.latency_estimate_ms - 20.0).abs() < 1e-9);
849    }
850
851    #[test]
852    fn test_overlay_route_multi_hop() {
853        let r = OverlayRoute::new(vec![1, 2, 3, 4]);
854        assert_eq!(r.hop_count, 3);
855        assert!((r.latency_estimate_ms - 60.0).abs() < 1e-9);
856    }
857
858    #[test]
859    fn test_overlay_route_single_node() {
860        let r = OverlayRoute::new(vec![5]);
861        assert_eq!(r.hop_count, 0);
862        assert!((r.latency_estimate_ms - 0.0).abs() < 1e-9);
863    }
864
865    // ------------------------------------------------------------------
866    // Join / Leave / Heartbeat
867    // ------------------------------------------------------------------
868
869    #[test]
870    fn test_join_success() {
871        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
872        assert!(mgr.join(make_node(1, 0)).is_ok());
873        assert_eq!(mgr.node_count(), 1);
874    }
875
876    #[test]
877    fn test_join_network_full() {
878        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 2);
879        mgr.join(make_node(1, 0))
880            .expect("test: first join should succeed");
881        mgr.join(make_node(2, 0))
882            .expect("test: second join should succeed");
883        let err = mgr.join(make_node(3, 0)).unwrap_err();
884        assert_eq!(err, OverlayError::NetworkFull);
885    }
886
887    #[test]
888    fn test_join_updates_existing_node() {
889        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
890        mgr.join(make_node(1, 0))
891            .expect("test: initial join should succeed");
892        // Join again with updated heartbeat; should succeed and update.
893        let mut updated = make_node(1, 0);
894        updated.last_heartbeat = 9999;
895        mgr.join(updated).expect("test: update join should succeed");
896        assert_eq!(mgr.node_count(), 1);
897        assert_eq!(
898            mgr.get_node(1)
899                .expect("test: node 1 should be present")
900                .last_heartbeat,
901            9999
902        );
903    }
904
905    #[test]
906    fn test_leave_success() {
907        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
908        mgr.join(make_node(1, 0))
909            .expect("test: join should succeed");
910        assert!(mgr.leave(1).is_ok());
911        assert_eq!(mgr.node_count(), 0);
912    }
913
914    #[test]
915    fn test_leave_not_found() {
916        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
917        let err = mgr.leave(99).unwrap_err();
918        assert_eq!(err, OverlayError::NodeNotFound(99));
919    }
920
921    #[test]
922    fn test_heartbeat_updates_timestamp() {
923        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
924        mgr.join(make_node(1, 0))
925            .expect("test: join should succeed");
926        mgr.heartbeat(1, 5000)
927            .expect("test: heartbeat should succeed");
928        assert_eq!(
929            mgr.get_node(1)
930                .expect("test: node 1 should be present after join")
931                .last_heartbeat,
932            5000
933        );
934    }
935
936    #[test]
937    fn test_heartbeat_not_found() {
938        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 10);
939        let err = mgr.heartbeat(99, 0).unwrap_err();
940        assert_eq!(err, OverlayError::NodeNotFound(99));
941    }
942
943    // ------------------------------------------------------------------
944    // Eviction
945    // ------------------------------------------------------------------
946
947    #[test]
948    fn test_evict_stale_removes_old_nodes() {
949        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
950        mgr.join(make_node(1, 0))
951            .expect("test: join node 1 should succeed");
952        mgr.join(make_node(2, 0))
953            .expect("test: join node 2 should succeed");
954        mgr.heartbeat(1, 190)
955            .expect("test: heartbeat for node 1 should succeed"); // node 1: heartbeat at t=190 — fresh
956                                                                  // node 2 still at t=0 (stale)
957        let removed = mgr.evict_stale(50, 200); // cutoff = 150, node2 @ 0 < 150 → stale; node1 @ 190 >= 150 → fresh
958        assert_eq!(removed, 1);
959        assert!(mgr.get_node(1).is_some());
960        assert!(mgr.get_node(2).is_none());
961    }
962
963    #[test]
964    fn test_evict_stale_none_removed() {
965        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
966        mgr.join(make_node(1, 1000))
967            .expect("test: join should succeed");
968        let removed = mgr.evict_stale(500, 1200);
969        assert_eq!(removed, 0); // cutoff = 700, heartbeat 1000 > 700
970    }
971
972    #[test]
973    fn test_evict_all() {
974        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
975        populate(&mut mgr, &[1, 2, 3, 4, 5], 0);
976        let removed = mgr.evict_stale(0, 9999); // cutoff = 9999, all < 9999
977        assert_eq!(removed, 5);
978        assert_eq!(mgr.node_count(), 0);
979    }
980
981    // ------------------------------------------------------------------
982    // FullMesh routing
983    // ------------------------------------------------------------------
984
985    #[test]
986    fn test_full_mesh_route_direct() {
987        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
988        populate(&mut mgr, &[1, 2, 3], 0);
989        let route = mgr
990            .route(1, 2, 0)
991            .expect("test: full mesh route should succeed");
992        assert_eq!(route.hop_count, 1);
993        assert_eq!(route.hops, vec![1, 2]);
994    }
995
996    #[test]
997    fn test_full_mesh_route_self_error() {
998        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
999        mgr.join(make_node(1, 0))
1000            .expect("test: join should succeed");
1001        assert_eq!(mgr.route(1, 1, 0).unwrap_err(), OverlayError::SelfRoute);
1002    }
1003
1004    #[test]
1005    fn test_full_mesh_route_unknown_source() {
1006        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1007        mgr.join(make_node(2, 0))
1008            .expect("test: join should succeed");
1009        assert_eq!(
1010            mgr.route(99, 2, 0).unwrap_err(),
1011            OverlayError::NodeNotFound(99)
1012        );
1013    }
1014
1015    #[test]
1016    fn test_full_mesh_route_unknown_dest() {
1017        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1018        mgr.join(make_node(1, 0))
1019            .expect("test: join should succeed");
1020        assert_eq!(
1021            mgr.route(1, 99, 0).unwrap_err(),
1022            OverlayError::NodeNotFound(99)
1023        );
1024    }
1025
1026    // ------------------------------------------------------------------
1027    // Chord routing
1028    // ------------------------------------------------------------------
1029
1030    #[test]
1031    fn test_chord_route_two_nodes() {
1032        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 4 }, 100);
1033        mgr.join(make_node(0, 0))
1034            .expect("test: join node 0 should succeed");
1035        mgr.join(make_node(100, 0))
1036            .expect("test: join node 100 should succeed");
1037        let route = mgr
1038            .route(0, 100, 0)
1039            .expect("test: chord route should succeed");
1040        assert!(route.hop_count >= 1);
1041        assert_eq!(
1042            *route
1043                .hops
1044                .first()
1045                .expect("test: route hops should be non-empty"),
1046            0
1047        );
1048        assert_eq!(
1049            *route
1050                .hops
1051                .last()
1052                .expect("test: route hops should have last element"),
1053            100
1054        );
1055    }
1056
1057    #[test]
1058    fn test_chord_route_self_error() {
1059        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 8 }, 100);
1060        mgr.join(make_node(5, 0))
1061            .expect("test: join should succeed");
1062        assert_eq!(mgr.route(5, 5, 0).unwrap_err(), OverlayError::SelfRoute);
1063    }
1064
1065    #[test]
1066    fn test_chord_route_many_nodes() {
1067        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 8 }, 100);
1068        // Space nodes across the u64 ring.
1069        let ids: Vec<u64> = (0..16).map(|i: u64| i * (u64::MAX / 16)).collect();
1070        populate(&mut mgr, &ids, 0);
1071        let route = mgr
1072            .route(ids[0], ids[8], 0)
1073            .expect("test: chord route across many nodes should succeed");
1074        assert_eq!(
1075            *route
1076                .hops
1077                .first()
1078                .expect("test: route hops should be non-empty"),
1079            ids[0]
1080        );
1081        assert_eq!(
1082            *route
1083                .hops
1084                .last()
1085                .expect("test: route hops should have last element"),
1086            ids[8]
1087        );
1088        // Should reach in log2(16) = 4 hops or fewer.
1089        assert!(route.hop_count <= 16);
1090    }
1091
1092    // ------------------------------------------------------------------
1093    // Tree routing
1094    // ------------------------------------------------------------------
1095
1096    #[test]
1097    fn test_tree_route_parent_to_child() {
1098        let mut mgr = OverlayNetworkManager::new(
1099            0,
1100            OverlayTopology::Tree {
1101                branching_factor: 2,
1102            },
1103            100,
1104        );
1105        // joined_at order: 10, 20, 30, 40
1106        // tree: 10 is root, children: 20,30. 20's child: 40.
1107        mgr.join(make_node(10, 10))
1108            .expect("test: join node 10 should succeed");
1109        mgr.join(make_node(20, 20))
1110            .expect("test: join node 20 should succeed");
1111        mgr.join(make_node(30, 30))
1112            .expect("test: join node 30 should succeed");
1113        mgr.join(make_node(40, 40))
1114            .expect("test: join node 40 should succeed");
1115        // Route from root (10) to grandchild (40): 10 -> 20 -> 40
1116        let route = mgr
1117            .route(10, 40, 0)
1118            .expect("test: tree route from root to grandchild should succeed");
1119        assert_eq!(
1120            *route
1121                .hops
1122                .first()
1123                .expect("test: route hops should be non-empty"),
1124            10
1125        );
1126        assert_eq!(
1127            *route
1128                .hops
1129                .last()
1130                .expect("test: route hops should have last element"),
1131            40
1132        );
1133        assert_eq!(route.hop_count, 2);
1134    }
1135
1136    #[test]
1137    fn test_tree_route_sibling_via_parent() {
1138        let mut mgr = OverlayNetworkManager::new(
1139            0,
1140            OverlayTopology::Tree {
1141                branching_factor: 2,
1142            },
1143            100,
1144        );
1145        mgr.join(make_node(1, 1))
1146            .expect("test: join node 1 should succeed");
1147        mgr.join(make_node(2, 2))
1148            .expect("test: join node 2 should succeed");
1149        mgr.join(make_node(3, 3))
1150            .expect("test: join node 3 should succeed");
1151        // tree: 1 is root, 2 and 3 are children.
1152        // Route from 2 to 3 must go via 1 (root).
1153        let route = mgr
1154            .route(2, 3, 0)
1155            .expect("test: tree route between siblings should succeed");
1156        assert_eq!(
1157            *route
1158                .hops
1159                .first()
1160                .expect("test: route hops should be non-empty"),
1161            2
1162        );
1163        assert_eq!(
1164            *route
1165                .hops
1166                .last()
1167                .expect("test: route hops should have last element"),
1168            3
1169        );
1170        assert_eq!(route.hop_count, 2); // 2 -> 1 -> 3
1171    }
1172
1173    #[test]
1174    fn test_tree_route_self_error() {
1175        let mut mgr = OverlayNetworkManager::new(
1176            0,
1177            OverlayTopology::Tree {
1178                branching_factor: 3,
1179            },
1180            100,
1181        );
1182        mgr.join(make_node(5, 0))
1183            .expect("test: join should succeed");
1184        assert_eq!(mgr.route(5, 5, 0).unwrap_err(), OverlayError::SelfRoute);
1185    }
1186
1187    // ------------------------------------------------------------------
1188    // Pastry / Kademlia (greedy proximity) routing
1189    // ------------------------------------------------------------------
1190
1191    #[test]
1192    fn test_pastry_route_direct() {
1193        let mut mgr = OverlayNetworkManager::new(
1194            0,
1195            OverlayTopology::Pastry {
1196                leaf_set_size: 4,
1197                routing_table_rows: 4,
1198            },
1199            100,
1200        );
1201        mgr.join(make_node(1, 0))
1202            .expect("test: join node 1 should succeed");
1203        mgr.join(make_node(2, 0))
1204            .expect("test: join node 2 should succeed");
1205        let route = mgr
1206            .route(1, 2, 0)
1207            .expect("test: pastry route should succeed");
1208        assert_eq!(
1209            *route
1210                .hops
1211                .first()
1212                .expect("test: route hops should be non-empty"),
1213            1
1214        );
1215        assert_eq!(
1216            *route
1217                .hops
1218                .last()
1219                .expect("test: route hops should have last element"),
1220            2
1221        );
1222    }
1223
1224    #[test]
1225    fn test_kademlia_route_multi_hop() {
1226        let mut mgr =
1227            OverlayNetworkManager::new(0, OverlayTopology::Kademlia { k: 4, alpha: 3 }, 100);
1228        // Craft IDs so greedy XOR forwarding makes progress.
1229        // IDs: 0, 4, 6, 7.  XOR distances from 0 to 7: 7; from 4 to 7: 3; from 6 to 7: 1.
1230        populate(&mut mgr, &[0, 4, 6, 7], 0);
1231        let route = mgr
1232            .route(0, 7, 0)
1233            .expect("test: kademlia route should succeed");
1234        assert_eq!(
1235            *route
1236                .hops
1237                .first()
1238                .expect("test: route hops should be non-empty"),
1239            0
1240        );
1241        assert_eq!(
1242            *route
1243                .hops
1244                .last()
1245                .expect("test: route hops should have last element"),
1246            7
1247        );
1248    }
1249
1250    #[test]
1251    fn test_kademlia_route_self_error() {
1252        let mut mgr =
1253            OverlayNetworkManager::new(0, OverlayTopology::Kademlia { k: 8, alpha: 3 }, 100);
1254        mgr.join(make_node(10, 0))
1255            .expect("test: join should succeed");
1256        assert_eq!(mgr.route(10, 10, 0).unwrap_err(), OverlayError::SelfRoute);
1257    }
1258
1259    // ------------------------------------------------------------------
1260    // Neighbors
1261    // ------------------------------------------------------------------
1262
1263    #[test]
1264    fn test_neighbors_full_mesh_all() {
1265        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1266        populate(&mut mgr, &[1, 2, 3, 4], 0);
1267        let mut nbrs = mgr.neighbors(1);
1268        nbrs.sort();
1269        assert_eq!(nbrs, vec![2, 3, 4]);
1270    }
1271
1272    #[test]
1273    fn test_neighbors_unknown_node_empty() {
1274        let mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1275        assert!(mgr.neighbors(999).is_empty());
1276    }
1277
1278    #[test]
1279    fn test_neighbors_kademlia_limited_to_k() {
1280        let mut mgr =
1281            OverlayNetworkManager::new(0, OverlayTopology::Kademlia { k: 3, alpha: 1 }, 100);
1282        populate(&mut mgr, &[0, 1, 2, 3, 4, 5, 6], 0);
1283        let nbrs = mgr.neighbors(0);
1284        assert!(nbrs.len() <= 3);
1285    }
1286
1287    #[test]
1288    fn test_neighbors_tree_root_has_only_children() {
1289        let mut mgr = OverlayNetworkManager::new(
1290            0,
1291            OverlayTopology::Tree {
1292                branching_factor: 2,
1293            },
1294            100,
1295        );
1296        // root=1 (joined first), children=2,3
1297        mgr.join(make_node(1, 1))
1298            .expect("test: join node 1 should succeed");
1299        mgr.join(make_node(2, 2))
1300            .expect("test: join node 2 should succeed");
1301        mgr.join(make_node(3, 3))
1302            .expect("test: join node 3 should succeed");
1303        let mut nbrs = mgr.neighbors(1);
1304        nbrs.sort();
1305        assert_eq!(nbrs, vec![2, 3]);
1306    }
1307
1308    #[test]
1309    fn test_neighbors_chord_finger_count() {
1310        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 4 }, 100);
1311        populate(&mut mgr, &[0, 10, 20, 30, 40, 50, 60, 70], 0);
1312        let nbrs = mgr.neighbors(0);
1313        // Should have at most 4 distinct finger entries.
1314        assert!(nbrs.len() <= 4);
1315    }
1316
1317    // ------------------------------------------------------------------
1318    // Diameter estimate
1319    // ------------------------------------------------------------------
1320
1321    #[test]
1322    fn test_diameter_full_mesh() {
1323        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1324        populate(&mut mgr, &[1, 2, 3, 4, 5], 0);
1325        assert!((mgr.diameter_estimate() - 1.0).abs() < 1e-9);
1326    }
1327
1328    #[test]
1329    fn test_diameter_chord_log2() {
1330        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 8 }, 1000);
1331        let ids: Vec<u64> = (1..=16).collect();
1332        populate(&mut mgr, &ids, 0);
1333        // log2(16) = 4.0
1334        let d = mgr.diameter_estimate();
1335        assert!((d - 4.0).abs() < 1e-9);
1336    }
1337
1338    #[test]
1339    fn test_diameter_tree() {
1340        let mut mgr = OverlayNetworkManager::new(
1341            0,
1342            OverlayTopology::Tree {
1343                branching_factor: 4,
1344            },
1345            1000,
1346        );
1347        // 16 nodes, log_4(16) = 2.0
1348        let ids: Vec<u64> = (1..=16).collect();
1349        populate(&mut mgr, &ids, 0);
1350        let d = mgr.diameter_estimate();
1351        assert!((d - 2.0).abs() < 1e-9);
1352    }
1353
1354    #[test]
1355    fn test_diameter_kademlia_log2() {
1356        let mut mgr =
1357            OverlayNetworkManager::new(0, OverlayTopology::Kademlia { k: 8, alpha: 3 }, 1000);
1358        let ids: Vec<u64> = (1..=8).collect();
1359        populate(&mut mgr, &ids, 0);
1360        // log2(8) = 3.0
1361        let d = mgr.diameter_estimate();
1362        assert!((d - 3.0).abs() < 1e-9);
1363    }
1364
1365    // ------------------------------------------------------------------
1366    // Stats
1367    // ------------------------------------------------------------------
1368
1369    #[test]
1370    fn test_stats_empty_overlay() {
1371        let mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1372        let s = mgr.stats(1000);
1373        assert_eq!(s.node_count, 0);
1374        assert!((s.avg_heartbeat_age_ms - 0.0).abs() < 1e-9);
1375        assert_eq!(s.topology_name, "FullMesh");
1376    }
1377
1378    #[test]
1379    fn test_stats_heartbeat_age() {
1380        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1381        mgr.join(make_node(1, 0))
1382            .expect("test: join node 1 should succeed"); // heartbeat=0
1383        mgr.join(make_node(2, 0))
1384            .expect("test: join node 2 should succeed"); // heartbeat=0
1385        let s = mgr.stats(1000); // now=1000
1386                                 // avg_heartbeat_age = (1000 + 1000) / 2 = 1000
1387        assert!((s.avg_heartbeat_age_ms - 1000.0).abs() < 1e-9);
1388    }
1389
1390    #[test]
1391    fn test_stats_topology_name_chord() {
1392        let mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 4 }, 100);
1393        assert_eq!(mgr.stats(0).topology_name, "Chord");
1394    }
1395
1396    #[test]
1397    fn test_stats_topology_name_pastry() {
1398        let mgr = OverlayNetworkManager::new(
1399            0,
1400            OverlayTopology::Pastry {
1401                leaf_set_size: 4,
1402                routing_table_rows: 4,
1403            },
1404            100,
1405        );
1406        assert_eq!(mgr.stats(0).topology_name, "Pastry");
1407    }
1408
1409    // ------------------------------------------------------------------
1410    // Accessors
1411    // ------------------------------------------------------------------
1412
1413    #[test]
1414    fn test_local_id() {
1415        let mgr = OverlayNetworkManager::new(42, OverlayTopology::FullMesh, 100);
1416        assert_eq!(mgr.local_id(), 42);
1417    }
1418
1419    #[test]
1420    fn test_topology_accessor() {
1421        let topo = OverlayTopology::Chord { fingers: 3 };
1422        let mgr = OverlayNetworkManager::new(0, topo.clone(), 100);
1423        assert_eq!(mgr.topology(), &topo);
1424    }
1425
1426    #[test]
1427    fn test_get_node_present() {
1428        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1429        mgr.join(make_node(7, 42))
1430            .expect("test: join should succeed");
1431        let node = mgr
1432            .get_node(7)
1433            .expect("test: node 7 should be present after join");
1434        assert_eq!(node.node_id, 7);
1435        assert_eq!(node.joined_at, 42);
1436    }
1437
1438    #[test]
1439    fn test_get_node_absent() {
1440        let mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1441        assert!(mgr.get_node(999).is_none());
1442    }
1443
1444    // ------------------------------------------------------------------
1445    // FNV-1a hash
1446    // ------------------------------------------------------------------
1447
1448    #[test]
1449    fn test_fnv1a_hash_different_inputs() {
1450        assert_ne!(fnv1a_hash(0), fnv1a_hash(1));
1451        assert_ne!(fnv1a_hash(u64::MAX), fnv1a_hash(0));
1452    }
1453
1454    #[test]
1455    fn test_fnv1a_hash_deterministic() {
1456        assert_eq!(fnv1a_hash(12345), fnv1a_hash(12345));
1457    }
1458
1459    // ------------------------------------------------------------------
1460    // XOR distance
1461    // ------------------------------------------------------------------
1462
1463    #[test]
1464    fn test_xor_distance_identity() {
1465        assert_eq!(xor_distance(7, 7), 0);
1466    }
1467
1468    #[test]
1469    fn test_xor_distance_symmetric() {
1470        assert_eq!(xor_distance(3, 5), xor_distance(5, 3));
1471    }
1472
1473    // ------------------------------------------------------------------
1474    // Edge cases
1475    // ------------------------------------------------------------------
1476
1477    #[test]
1478    fn test_evict_stale_overflow_safe() {
1479        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::FullMesh, 100);
1480        // now < max_age_ms: cutoff saturates at 0, nothing removed
1481        mgr.join(make_node(1, 0))
1482            .expect("test: join should succeed");
1483        let removed = mgr.evict_stale(u64::MAX, 0);
1484        assert_eq!(removed, 0);
1485    }
1486
1487    #[test]
1488    fn test_chord_route_two_equal_nodes_no_panic() {
1489        let mut mgr = OverlayNetworkManager::new(0, OverlayTopology::Chord { fingers: 1 }, 100);
1490        mgr.join(make_node(1, 0))
1491            .expect("test: join node 1 should succeed");
1492        mgr.join(make_node(2, 0))
1493            .expect("test: join node 2 should succeed");
1494        // Should either route or return a known error, not panic.
1495        let result = mgr.route(1, 2, 0);
1496        assert!(result.is_ok() || matches!(result, Err(OverlayError::RouteNotFound { .. })));
1497    }
1498
1499    #[test]
1500    fn test_pastry_route_self_error() {
1501        let mut mgr = OverlayNetworkManager::new(
1502            0,
1503            OverlayTopology::Pastry {
1504                leaf_set_size: 4,
1505                routing_table_rows: 4,
1506            },
1507            100,
1508        );
1509        mgr.join(make_node(5, 0))
1510            .expect("test: join should succeed");
1511        assert_eq!(mgr.route(5, 5, 0).unwrap_err(), OverlayError::SelfRoute);
1512    }
1513
1514    #[test]
1515    fn test_tree_single_branching_factor_one() {
1516        let mut mgr = OverlayNetworkManager::new(
1517            0,
1518            OverlayTopology::Tree {
1519                branching_factor: 1,
1520            },
1521            100,
1522        );
1523        // With bf=1, the tree is a linked list sorted by joined_at.
1524        mgr.join(make_node(1, 1))
1525            .expect("test: join node 1 should succeed");
1526        mgr.join(make_node(2, 2))
1527            .expect("test: join node 2 should succeed");
1528        mgr.join(make_node(3, 3))
1529            .expect("test: join node 3 should succeed");
1530        // Route 1->3 must traverse 1->2->3
1531        let route = mgr
1532            .route(1, 3, 0)
1533            .expect("test: tree route with bf=1 should succeed");
1534        assert_eq!(route.hop_count, 2);
1535    }
1536
1537    #[test]
1538    fn test_overlay_route_latency_proportional_to_hops() {
1539        let r2 = OverlayRoute::new(vec![1, 2, 3]);
1540        let r4 = OverlayRoute::new(vec![1, 2, 3, 4, 5]);
1541        assert!((r4.latency_estimate_ms - 2.0 * r2.latency_estimate_ms).abs() < 1e-9);
1542    }
1543}