Major feature addition. For full details read `./docs/development_history/DISTRIBUTION.md` Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
202 lines
6.3 KiB
Rust
202 lines
6.3 KiB
Rust
use distribution::kademlia::routing_table::RoutingTable;
|
|
use distribution::types::NodeId;
|
|
|
|
fn node(byte: u8) -> NodeId {
|
|
NodeId([byte; 32])
|
|
}
|
|
|
|
fn addr(port: u16) -> std::net::SocketAddr {
|
|
format!("127.0.0.1:{port}").parse().unwrap()
|
|
}
|
|
|
|
// ─── Basic operations ───────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn insert_and_contains() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
assert!(rt.insert(node(1), addr(8001)));
|
|
assert!(rt.contains(&node(1)));
|
|
assert!(!rt.contains(&node(2)));
|
|
}
|
|
|
|
#[test]
|
|
fn insert_self_is_rejected() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
assert!(!rt.insert(node(0), addr(8000)));
|
|
assert_eq!(rt.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_node() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
rt.insert(node(1), addr(8001));
|
|
assert!(rt.remove(&node(1)));
|
|
assert!(!rt.contains(&node(1)));
|
|
assert_eq!(rt.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_nonexistent_returns_false() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
assert!(!rt.remove(&node(1)));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_insert_updates_position() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
rt.insert(node(1), addr(8001));
|
|
rt.insert(node(2), addr(8002));
|
|
// Re-insert node 1 — should move to most-recently-seen
|
|
assert!(rt.insert(node(1), addr(8001)));
|
|
assert_eq!(rt.len(), 2);
|
|
}
|
|
|
|
// ─── Closest query ──────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn closest_returns_k_nearest_by_xor() {
|
|
let self_id = NodeId([0x00; 32]);
|
|
let mut rt = RoutingTable::new(self_id);
|
|
|
|
// Insert nodes with varying distances
|
|
for i in 1..=10u8 {
|
|
let mut bytes = [0u8; 32];
|
|
bytes[0] = i;
|
|
rt.insert(NodeId(bytes), addr(8000 + i as u16));
|
|
}
|
|
|
|
let target = NodeId([0x00; 32]); // same as self, closest by XOR
|
|
let closest = rt.closest(&target, 3);
|
|
assert_eq!(closest.len(), 3);
|
|
|
|
// XOR distance to [0x00...] is [i, 0, 0, ...] — smallest i first
|
|
assert_eq!(closest[0].node_id.0[0], 1);
|
|
assert_eq!(closest[1].node_id.0[0], 2);
|
|
assert_eq!(closest[2].node_id.0[0], 3);
|
|
}
|
|
|
|
#[test]
|
|
fn closest_returns_all_when_fewer_than_count() {
|
|
let mut rt = RoutingTable::new(node(0));
|
|
rt.insert(node(1), addr(8001));
|
|
rt.insert(node(2), addr(8002));
|
|
|
|
let closest = rt.closest(&node(0), 10);
|
|
assert_eq!(closest.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn closest_to_specific_target() {
|
|
let self_id = NodeId([0x00; 32]);
|
|
let mut rt = RoutingTable::new(self_id);
|
|
|
|
// Node A: XOR distance to target [0xFF...] is [0xFF ^ 0x01, ...] = [0xFE, ...]
|
|
let mut a = [0u8; 32];
|
|
a[0] = 0x01;
|
|
rt.insert(NodeId(a), addr(8001));
|
|
|
|
// Node B: XOR distance to target [0xFF...] is [0xFF ^ 0xFE, ...] = [0x01, ...]
|
|
let mut b = [0u8; 32];
|
|
b[0] = 0xFE;
|
|
rt.insert(NodeId(b), addr(8002));
|
|
|
|
let target = NodeId([0xFF; 32]);
|
|
let closest = rt.closest(&target, 1);
|
|
|
|
// B is closer to target (XOR = 0x01) than A (XOR = 0xFE)
|
|
assert_eq!(closest[0].node_id.0[0], 0xFE);
|
|
}
|
|
|
|
// ─── Bucket capacity and replacement ────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn bucket_overflow_goes_to_replacement_cache() {
|
|
// Use k=2 for easy testing
|
|
let self_id = NodeId([0x00; 32]);
|
|
let mut rt = RoutingTable::with_k(self_id, 2);
|
|
|
|
// Insert 3 nodes that all land in the same bucket
|
|
// All have first byte != 0, so XOR leading zeros = 0 → bucket 0
|
|
let mut bytes_a = [0u8; 32]; bytes_a[0] = 0x80;
|
|
let mut bytes_b = [0u8; 32]; bytes_b[0] = 0xC0;
|
|
let mut bytes_c = [0u8; 32]; bytes_c[0] = 0xA0;
|
|
|
|
assert!(rt.insert(NodeId(bytes_a), addr(8001))); // fits
|
|
assert!(rt.insert(NodeId(bytes_b), addr(8002))); // fits
|
|
assert!(!rt.insert(NodeId(bytes_c), addr(8003))); // goes to replacement
|
|
|
|
assert_eq!(rt.len(), 2);
|
|
assert!(rt.contains(&NodeId(bytes_a)));
|
|
assert!(rt.contains(&NodeId(bytes_b)));
|
|
assert!(!rt.contains(&NodeId(bytes_c)));
|
|
}
|
|
|
|
#[test]
|
|
fn removing_node_promotes_from_replacement() {
|
|
let self_id = NodeId([0x00; 32]);
|
|
let mut rt = RoutingTable::with_k(self_id, 2);
|
|
|
|
let mut bytes_a = [0u8; 32]; bytes_a[0] = 0x80;
|
|
let mut bytes_b = [0u8; 32]; bytes_b[0] = 0xC0;
|
|
let mut bytes_c = [0u8; 32]; bytes_c[0] = 0xA0;
|
|
|
|
rt.insert(NodeId(bytes_a), addr(8001));
|
|
rt.insert(NodeId(bytes_b), addr(8002));
|
|
rt.insert(NodeId(bytes_c), addr(8003)); // replacement
|
|
|
|
// Remove A — C should be promoted
|
|
rt.remove(&NodeId(bytes_a));
|
|
assert_eq!(rt.len(), 2);
|
|
assert!(rt.contains(&NodeId(bytes_b)));
|
|
assert!(rt.contains(&NodeId(bytes_c)));
|
|
}
|
|
|
|
// ─── XOR distance ordering ─────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn xor_distance_is_correct() {
|
|
let a = NodeId([0x00; 32]);
|
|
let b = NodeId([0xFF; 32]);
|
|
let dist = a.xor_distance(&b);
|
|
assert_eq!(dist, [0xFF; 32]);
|
|
}
|
|
|
|
#[test]
|
|
fn closest_ordering_is_stable_with_many_nodes() {
|
|
let self_id = NodeId([0x00; 32]);
|
|
let mut rt = RoutingTable::new(self_id);
|
|
|
|
// Insert 50 nodes with random-ish IDs
|
|
for i in 1..=50u8 {
|
|
let mut bytes = [0u8; 32];
|
|
bytes[0] = i;
|
|
bytes[1] = i.wrapping_mul(37);
|
|
rt.insert(NodeId(bytes), addr(8000 + i as u16));
|
|
}
|
|
|
|
let target = NodeId([0x10; 32]);
|
|
let closest = rt.closest(&target, 10);
|
|
|
|
// Verify sorted by XOR distance
|
|
for window in closest.windows(2) {
|
|
let d0 = window[0].node_id.xor_distance(&target);
|
|
let d1 = window[1].node_id.xor_distance(&target);
|
|
assert!(d0 <= d1, "closest results should be sorted by XOR distance");
|
|
}
|
|
}
|
|
|
|
// ─── Empty table ────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn empty_table_closest_returns_empty() {
|
|
let rt = RoutingTable::new(node(0));
|
|
let closest = rt.closest(&node(1), 10);
|
|
assert!(closest.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn empty_table_has_zero_len() {
|
|
let rt = RoutingTable::new(node(0));
|
|
assert_eq!(rt.len(), 0);
|
|
assert!(rt.is_empty());
|
|
}
|