#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::unchecked_time_subtraction,
reason = "M175: routing table — node arithmetic and time deltas use post-bootstrap Instants; remaining unchecked-time sites are test fixtures"
)]
use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::time::{Duration, Instant};
use irontide_core::Id20;
pub const K: usize = 8;
const MAX_BUCKETS: usize = 160;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeStatus {
Good,
Questionable,
Bad,
}
const ACTIVE_THRESHOLD: Duration = Duration::from_mins(15);
#[derive(Debug, Clone)]
pub struct RoutingNode {
pub id: Id20,
pub addr: SocketAddr,
pub last_seen: Instant,
pub fail_count: u32,
pub last_response: Option<Instant>,
pub last_query: Option<Instant>,
}
impl RoutingNode {
#[must_use]
pub fn status(&self) -> NodeStatus {
if self.fail_count >= 2 {
return NodeStatus::Bad;
}
let now = Instant::now();
let recent = |t: Instant| now.duration_since(t) <= ACTIVE_THRESHOLD;
let active = self.last_response.is_some_and(recent) || self.last_query.is_some_and(recent);
if active {
NodeStatus::Good
} else {
NodeStatus::Questionable
}
}
}
#[derive(Debug, Clone)]
struct KBucket {
nodes: Vec<RoutingNode>,
}
impl KBucket {
fn new() -> Self {
Self {
nodes: Vec::with_capacity(K),
}
}
fn is_full(&self) -> bool {
self.nodes.len() >= K
}
fn find(&self, id: &Id20) -> Option<usize> {
self.nodes.iter().position(|n| n.id == *id)
}
fn worst_node(&self) -> Option<usize> {
self.nodes
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.fail_count
.cmp(&b.fail_count)
.then(b.last_seen.cmp(&a.last_seen))
})
.map(|(i, _)| i)
}
}
const DEFAULT_MAX_NODES: usize = 512;
#[derive(Debug, Clone)]
pub struct RoutingTable {
own_id: Id20,
buckets: Vec<KBucket>,
ip_set: HashSet<IpAddr>,
restrict_ips: bool,
max_nodes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InsertResult {
Inserted,
BucketFull,
Rejected,
}
impl RoutingTable {
#[must_use]
pub fn new(own_id: Id20) -> Self {
Self::with_config(own_id, false, DEFAULT_MAX_NODES)
}
#[must_use]
pub fn new_with_config(own_id: Id20, restrict_ips: bool) -> Self {
Self::with_config(own_id, restrict_ips, DEFAULT_MAX_NODES)
}
#[must_use]
pub fn with_config(own_id: Id20, restrict_ips: bool, max_nodes: usize) -> Self {
Self {
own_id,
buckets: vec![KBucket::new()],
ip_set: HashSet::new(),
restrict_ips,
max_nodes,
}
}
#[must_use]
pub fn own_id(&self) -> &Id20 {
&self.own_id
}
#[must_use]
pub fn len(&self) -> usize {
self.buckets.iter().map(|b| b.nodes.len()).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn bucket_count(&self) -> usize {
self.buckets.len()
}
pub fn insert(&mut self, id: Id20, addr: SocketAddr) -> bool {
if id == self.own_id {
return false; }
self.insert_inner(id, addr)
}
fn insert_inner(&mut self, id: Id20, addr: SocketAddr) -> bool {
let ip = addr.ip();
if self.restrict_ips && self.ip_set.contains(&ip) {
let bucket_idx = self.bucket_index(&id);
if self.buckets[bucket_idx].find(&id).is_none() {
return false; }
}
let bucket_idx = self.bucket_index(&id);
if let Some(pos) = self.buckets[bucket_idx].find(&id) {
let old_ip = self.buckets[bucket_idx].nodes[pos].addr.ip();
self.buckets[bucket_idx].nodes[pos].last_seen = Instant::now();
self.buckets[bucket_idx].nodes[pos].addr = addr;
self.buckets[bucket_idx].nodes[pos].fail_count = 0;
if self.restrict_ips && old_ip != ip {
self.ip_set.remove(&old_ip);
self.ip_set.insert(ip);
}
return true;
}
let at_cap = self.len() >= self.max_nodes;
if at_cap {
if let Some(worst_idx) = self.buckets[bucket_idx].worst_node()
&& self.buckets[bucket_idx].nodes[worst_idx].fail_count > 0
{
if self.restrict_ips {
self.ip_set
.remove(&self.buckets[bucket_idx].nodes[worst_idx].addr.ip());
}
self.buckets[bucket_idx].nodes[worst_idx] = RoutingNode {
id,
addr,
last_seen: Instant::now(),
fail_count: 0,
last_response: None,
last_query: None,
};
if self.restrict_ips {
self.ip_set.insert(ip);
}
return true;
}
return false;
}
if !self.buckets[bucket_idx].is_full() {
self.buckets[bucket_idx].nodes.push(RoutingNode {
id,
addr,
last_seen: Instant::now(),
fail_count: 0,
last_response: None,
last_query: None,
});
if self.restrict_ips {
self.ip_set.insert(ip);
}
return true;
}
if let Some(worst_idx) = self.buckets[bucket_idx].worst_node()
&& self.buckets[bucket_idx].nodes[worst_idx].fail_count > 0
{
if self.restrict_ips {
self.ip_set
.remove(&self.buckets[bucket_idx].nodes[worst_idx].addr.ip());
}
self.buckets[bucket_idx].nodes[worst_idx] = RoutingNode {
id,
addr,
last_seen: Instant::now(),
fail_count: 0,
last_response: None,
last_query: None,
};
if self.restrict_ips {
self.ip_set.insert(ip);
}
return true;
}
if self.can_split(bucket_idx) {
self.split(bucket_idx);
return self.insert_inner(id, addr);
}
false
}
pub fn remove(&mut self, id: &Id20) -> bool {
let bucket_idx = self.bucket_index(id);
let bucket = &mut self.buckets[bucket_idx];
if let Some(pos) = bucket.find(id) {
if self.restrict_ips {
self.ip_set.remove(&bucket.nodes[pos].addr.ip());
}
bucket.nodes.remove(pos);
true
} else {
false
}
}
pub fn mark_seen(&mut self, id: &Id20) {
let bucket_idx = self.bucket_index(id);
if let Some(pos) = self.buckets[bucket_idx].find(id) {
self.buckets[bucket_idx].nodes[pos].last_seen = Instant::now();
self.buckets[bucket_idx].nodes[pos].fail_count = 0;
}
}
pub fn mark_failed(&mut self, id: &Id20) {
let bucket_idx = self.bucket_index(id);
if let Some(pos) = self.buckets[bucket_idx].find(id) {
self.buckets[bucket_idx].nodes[pos].fail_count += 1;
}
}
#[must_use]
pub fn closest(&self, target: &Id20, count: usize) -> Vec<RoutingNode> {
let mut all: Vec<&RoutingNode> = self.buckets.iter().flat_map(|b| &b.nodes).collect();
all.sort_by_key(|n| n.id.xor_distance(target));
all.into_iter().take(count).cloned().collect()
}
#[must_use]
pub fn stale_buckets(&self, max_age: std::time::Duration) -> Vec<usize> {
let now = Instant::now();
self.buckets
.iter()
.enumerate()
.filter(|(_, b)| {
b.nodes.is_empty()
|| b.nodes
.iter()
.all(|n| now.duration_since(n.last_seen) > max_age)
})
.map(|(i, _)| i)
.collect()
}
#[must_use]
pub fn random_id_in_bucket(&self, bucket_idx: usize) -> Id20 {
let mut id = self.own_id;
if bucket_idx < MAX_BUCKETS {
let byte_idx = bucket_idx / 8;
let bit_idx = 7 - (bucket_idx % 8);
id.0[byte_idx] ^= 1 << bit_idx;
}
id
}
#[must_use]
pub fn all_nodes(&self) -> Vec<(Id20, SocketAddr)> {
self.buckets
.iter()
.flat_map(|b| b.nodes.iter().map(|n| (n.id, n.addr)))
.collect()
}
#[must_use]
pub fn get(&self, id: &Id20) -> Option<&RoutingNode> {
let bucket_idx = self.bucket_index(id);
self.buckets[bucket_idx]
.find(id)
.map(|pos| &self.buckets[bucket_idx].nodes[pos])
}
pub fn get_mut(&mut self, id: &Id20) -> Option<&mut RoutingNode> {
let bucket_idx = self.bucket_index(id);
let pos = self.buckets[bucket_idx].find(id)?;
Some(&mut self.buckets[bucket_idx].nodes[pos])
}
pub fn mark_response(&mut self, id: &Id20) {
let bucket_idx = self.bucket_index(id);
if let Some(pos) = self.buckets[bucket_idx].find(id) {
self.buckets[bucket_idx].nodes[pos].last_response = Some(Instant::now());
self.buckets[bucket_idx].nodes[pos].fail_count = 0;
}
}
pub fn mark_query(&mut self, id: &Id20) {
let bucket_idx = self.bucket_index(id);
if let Some(pos) = self.buckets[bucket_idx].find(id) {
self.buckets[bucket_idx].nodes[pos].last_query = Some(Instant::now());
}
}
pub fn mark_all_questionable(&mut self) {
for bucket in &mut self.buckets {
for node in &mut bucket.nodes {
node.last_response = None;
node.last_query = None;
node.fail_count = 0;
}
}
}
#[must_use]
pub fn questionable_nodes(&self) -> Vec<(Id20, SocketAddr)> {
self.buckets
.iter()
.flat_map(|b| {
b.nodes
.iter()
.filter(|n| n.status() == NodeStatus::Questionable)
.map(|n| (n.id, n.addr))
})
.collect()
}
fn bucket_index(&self, id: &Id20) -> usize {
let distance = self.own_id.xor_distance(id);
let leading_zeros = leading_zeros_160(&distance);
leading_zeros.min(self.buckets.len() - 1)
}
fn can_split(&self, idx: usize) -> bool {
idx == self.buckets.len() - 1 && self.buckets.len() < MAX_BUCKETS
}
fn split(&mut self, idx: usize) {
debug_assert!(idx == self.buckets.len() - 1);
let old_bucket = &mut self.buckets[idx];
let nodes = std::mem::take(&mut old_bucket.nodes);
let mut near = KBucket::new();
let mut far = KBucket::new();
let new_depth = self.buckets.len();
for node in nodes {
let distance = self.own_id.xor_distance(&node.id);
let lz = leading_zeros_160(&distance);
if lz >= new_depth {
near.nodes.push(node);
} else {
far.nodes.push(node);
}
}
self.buckets[idx] = far;
self.buckets.push(near);
}
}
fn leading_zeros_160(id: &Id20) -> usize {
let mut zeros = 0;
for &byte in id.as_bytes() {
if byte == 0 {
zeros += 8;
} else {
zeros += byte.leading_zeros() as usize;
break;
}
}
zeros
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn id(byte: u8) -> Id20 {
let mut bytes = [0u8; 20];
bytes[19] = byte;
Id20(bytes)
}
fn addr(port: u16) -> SocketAddr {
format!("10.0.0.1:{port}").parse().unwrap()
}
#[test]
fn insert_and_retrieve() {
let mut rt = RoutingTable::new(Id20::ZERO);
assert!(rt.insert(id(1), addr(1)));
assert_eq!(rt.len(), 1);
assert!(rt.get(&id(1)).is_some());
}
#[test]
fn routing_node_status_does_not_panic_on_fresh_process() {
let now = Instant::now();
let node = RoutingNode {
id: id(1),
addr: addr(1),
last_seen: now,
fail_count: 0,
last_response: Some(now),
last_query: None,
};
assert_eq!(node.status(), NodeStatus::Good);
let stale = RoutingNode {
id: id(2),
addr: addr(2),
last_seen: now,
fail_count: 0,
last_response: None,
last_query: None,
};
assert_eq!(stale.status(), NodeStatus::Questionable);
}
#[test]
fn stale_buckets_does_not_panic_on_large_max_age() {
let rt = RoutingTable::new(Id20::ZERO);
let _ = rt.stale_buckets(std::time::Duration::from_hours(24));
}
#[test]
fn insert_self_rejected() {
let mut rt = RoutingTable::new(Id20::ZERO);
assert!(!rt.insert(Id20::ZERO, addr(1)));
assert_eq!(rt.len(), 0);
}
#[test]
fn update_existing_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.insert(id(1), addr(2)); assert_eq!(rt.len(), 1);
assert_eq!(rt.get(&id(1)).unwrap().addr, addr(2));
}
#[test]
fn remove_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
assert!(rt.remove(&id(1)));
assert_eq!(rt.len(), 0);
assert!(!rt.remove(&id(1))); }
#[test]
fn closest_nodes_sorted() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.insert(id(5), addr(5));
rt.insert(id(3), addr(3));
rt.insert(id(10), addr(10));
let closest = rt.closest(&Id20::ZERO, 3);
assert_eq!(closest.len(), 3);
assert_eq!(closest[0].id, id(1));
assert_eq!(closest[1].id, id(3));
assert_eq!(closest[2].id, id(5));
}
#[test]
fn closest_fewer_than_count() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
let closest = rt.closest(&Id20::ZERO, 10);
assert_eq!(closest.len(), 1);
}
#[test]
fn bucket_splitting() {
let mut rt = RoutingTable::new(Id20::ZERO);
for i in 1..=9u8 {
rt.insert(id(i), addr(u16::from(i)));
}
assert!(rt.bucket_count() > 1);
assert_eq!(rt.len(), 9);
}
#[test]
fn evict_failed_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
for i in 1..=K as u8 {
rt.insert(id(i), addr(u16::from(i)));
}
assert_eq!(rt.len(), K);
rt.mark_failed(&id(1));
rt.mark_failed(&id(1));
let new_id = id(100);
assert!(rt.insert(new_id, addr(100)));
assert!(rt.get(&new_id).is_some());
}
#[test]
fn mark_seen_resets_fail_count() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.mark_failed(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().fail_count, 1);
rt.mark_seen(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().fail_count, 0);
}
#[test]
fn stale_buckets_detection() {
let mut rt = RoutingTable::new(Id20::ZERO);
let stale = rt.stale_buckets(std::time::Duration::from_mins(15));
assert_eq!(stale.len(), 1);
rt.insert(id(1), addr(1));
let stale = rt.stale_buckets(std::time::Duration::from_mins(15));
assert!(stale.is_empty());
}
#[test]
fn leading_zeros_correct() {
assert_eq!(leading_zeros_160(&Id20::ZERO), 160);
assert_eq!(leading_zeros_160(&id(1)), 159);
assert_eq!(leading_zeros_160(&id(128)), 152);
let mut full = [0xFFu8; 20];
assert_eq!(leading_zeros_160(&Id20(full)), 0);
full[0] = 0x01;
full[1..].fill(0);
assert_eq!(leading_zeros_160(&Id20(full)), 7);
}
#[test]
fn random_id_in_bucket_differs() {
let rt = RoutingTable::new(Id20::ZERO);
let rand_id = rt.random_id_in_bucket(0);
assert_ne!(rand_id, Id20::ZERO);
}
#[test]
fn restrict_ips_rejects_second_node_same_ip() {
let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
let ip_addr: SocketAddr = "10.0.0.1:6881".parse().unwrap();
assert!(rt.insert(id(1), ip_addr));
let ip_addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
assert!(!rt.insert(id(2), ip_addr2));
assert_eq!(rt.len(), 1);
}
#[test]
fn restrict_ips_allows_same_node_update() {
let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
let addr1: SocketAddr = "10.0.0.1:6881".parse().unwrap();
let addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
assert!(rt.insert(id(1), addr1));
assert!(rt.insert(id(1), addr2));
assert_eq!(rt.len(), 1);
assert_eq!(rt.get(&id(1)).unwrap().addr, addr2);
}
#[test]
fn no_restrict_ips_allows_multiple_nodes_same_ip() {
let mut rt = RoutingTable::new_with_config(Id20::ZERO, false);
let addr1: SocketAddr = "10.0.0.1:6881".parse().unwrap();
let addr2: SocketAddr = "10.0.0.1:6882".parse().unwrap();
assert!(rt.insert(id(1), addr1));
assert!(rt.insert(id(2), addr2));
assert_eq!(rt.len(), 2);
}
#[test]
fn restrict_ips_remove_frees_ip_slot() {
let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
let addr: SocketAddr = "10.0.0.1:6881".parse().unwrap();
assert!(rt.insert(id(1), addr));
assert!(rt.remove(&id(1)));
assert!(rt.insert(id(2), addr));
assert_eq!(rt.len(), 1);
}
#[test]
fn node_status_bad_on_two_failures() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.mark_failed(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Questionable);
rt.mark_failed(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Bad);
}
#[test]
fn node_status_good_after_mark_response() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Questionable);
rt.mark_response(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
}
#[test]
fn node_status_good_after_mark_query() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.mark_query(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
}
#[test]
fn mark_response_resets_fail_count() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.mark_failed(&id(1));
rt.mark_failed(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Bad);
rt.mark_response(&id(1));
assert_eq!(rt.get(&id(1)).unwrap().fail_count, 0);
assert_eq!(rt.get(&id(1)).unwrap().status(), NodeStatus::Good);
}
#[test]
fn mark_response_noop_for_unknown_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.mark_response(&id(42));
}
#[test]
fn mark_query_noop_for_unknown_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.mark_query(&id(42));
}
#[test]
fn get_mut_finds_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
let node = rt.get_mut(&id(1));
assert!(node.is_some());
node.unwrap().fail_count = 99;
assert_eq!(rt.get(&id(1)).unwrap().fail_count, 99);
}
#[test]
fn get_mut_returns_none_for_missing_node() {
let mut rt = RoutingTable::new(Id20::ZERO);
assert!(rt.get_mut(&id(1)).is_none());
}
#[test]
fn questionable_nodes_filters_correctly() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1)); rt.insert(id(2), addr(2)); rt.insert(id(3), addr(3)); rt.mark_response(&id(2));
rt.mark_failed(&id(3));
rt.mark_failed(&id(3));
let q = rt.questionable_nodes();
assert_eq!(q.len(), 1);
assert_eq!(q[0].0, id(1));
}
#[test]
fn questionable_nodes_empty_when_all_good_or_bad() {
let mut rt = RoutingTable::new(Id20::ZERO);
rt.insert(id(1), addr(1));
rt.insert(id(2), addr(2));
rt.mark_response(&id(1));
rt.mark_failed(&id(2));
rt.mark_failed(&id(2));
assert!(rt.questionable_nodes().is_empty());
}
#[test]
fn worst_node_evicts_oldest_on_tied_fail_counts() {
let mut bucket = KBucket::new();
let now = Instant::now();
bucket.nodes.push(RoutingNode {
id: id(1),
addr: addr(1),
last_seen: now - std::time::Duration::from_secs(100),
fail_count: 1,
last_response: None,
last_query: None,
});
bucket.nodes.push(RoutingNode {
id: id(2),
addr: addr(2),
last_seen: now - std::time::Duration::from_secs(10),
fail_count: 1,
last_response: None,
last_query: None,
});
let worst = bucket.worst_node().unwrap();
assert_eq!(
bucket.nodes[worst].id,
id(1),
"oldest node should be evicted on tied fail counts"
);
}
#[test]
fn worst_node_prefers_highest_fail_count() {
let mut bucket = KBucket::new();
let now = Instant::now();
bucket.nodes.push(RoutingNode {
id: id(1),
addr: addr(1),
last_seen: now,
fail_count: 3,
last_response: None,
last_query: None,
});
bucket.nodes.push(RoutingNode {
id: id(2),
addr: addr(2),
last_seen: now - std::time::Duration::from_secs(1000),
fail_count: 1,
last_response: None,
last_query: None,
});
let worst = bucket.worst_node().unwrap();
assert_eq!(
bucket.nodes[worst].id,
id(1),
"highest fail_count should win regardless of age"
);
}
#[test]
fn restrict_ips_eviction_frees_ip_slot() {
let mut rt = RoutingTable::new_with_config(Id20::ZERO, true);
for i in 1..=K as u8 {
let a: SocketAddr = format!("10.0.0.{i}:6881").parse().unwrap();
rt.insert(id(i), a);
}
assert_eq!(rt.len(), K);
rt.mark_failed(&id(1));
rt.mark_failed(&id(1));
let new_addr: SocketAddr = "10.0.0.100:6881".parse().unwrap();
assert!(rt.insert(id(100), new_addr));
assert_eq!(rt.len(), K);
let old_addr: SocketAddr = "10.0.0.1:6882".parse().unwrap();
assert!(rt.insert(id(200), old_addr));
}
#[test]
fn routing_table_node_cap_rejects_at_limit() {
let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
for i in 1..=4u8 {
assert!(
rt.insert(id(i), addr(u16::from(i))),
"insert {i} should succeed"
);
}
assert_eq!(rt.len(), 4);
assert!(!rt.insert(id(5), addr(5)));
assert_eq!(rt.len(), 4);
}
#[test]
fn routing_table_node_cap_allows_eviction() {
let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
for i in 1..=4u8 {
rt.insert(id(i), addr(u16::from(i)));
}
assert_eq!(rt.len(), 4);
rt.mark_failed(&id(1));
assert!(rt.insert(id(5), addr(5)));
assert_eq!(rt.len(), 4);
assert!(rt.get(&id(5)).is_some());
assert!(rt.get(&id(1)).is_none());
}
#[test]
fn routing_table_node_cap_allows_update() {
let mut rt = RoutingTable::with_config(Id20::ZERO, false, 4);
for i in 1..=4u8 {
rt.insert(id(i), addr(u16::from(i)));
}
assert_eq!(rt.len(), 4);
assert!(rt.insert(id(2), addr(200)));
assert_eq!(rt.len(), 4);
assert_eq!(rt.get(&id(2)).unwrap().addr, addr(200));
}
#[test]
fn routing_table_default_cap_512() {
let rt = RoutingTable::new(Id20::ZERO);
assert_eq!(rt.max_nodes, DEFAULT_MAX_NODES);
assert_eq!(rt.max_nodes, 512);
}
#[test]
fn mark_all_questionable_resets_liveness() {
let own_id = Id20([0x00; 20]);
let mut rt = RoutingTable::new(own_id);
let node1 = Id20([0x80; 20]);
let node2 = Id20([0x40; 20]);
let addr1: SocketAddr = "192.0.2.1:6881".parse().unwrap();
let addr2: SocketAddr = "192.0.2.2:6881".parse().unwrap();
rt.insert(node1, addr1);
rt.insert(node2, addr2);
rt.mark_response(&node1);
rt.mark_response(&node2);
assert_eq!(rt.get(&node1).unwrap().status(), NodeStatus::Good);
assert_eq!(rt.get(&node2).unwrap().status(), NodeStatus::Good);
rt.mark_all_questionable();
assert_eq!(rt.get(&node1).unwrap().status(), NodeStatus::Questionable);
assert_eq!(rt.get(&node2).unwrap().status(), NodeStatus::Questionable);
assert_eq!(rt.get(&node1).unwrap().fail_count, 0);
assert_eq!(rt.get(&node2).unwrap().fail_count, 0);
}
}