diff --git a/crates/core/src/ring/connection_manager.rs b/crates/core/src/ring/connection_manager.rs index ca153b887..08b4f7928 100644 --- a/crates/core/src/ring/connection_manager.rs +++ b/crates/core/src/ring/connection_manager.rs @@ -748,19 +748,70 @@ impl ConnectionManager { #[cfg(test)] mod tests { use super::*; + use crate::router::Router; use crate::topology::rate::Rate; use crate::transport::TransportKeypair; + use std::collections::HashSet; use std::net::SocketAddr; use std::sync::atomic::AtomicU64; use std::time::Duration; + fn make_connection_manager( + own_addr: Option, + min_conn: usize, + max_conn: usize, + is_gateway: bool, + ) -> ConnectionManager { + let keypair = TransportKeypair::new(); + let own_location = if let Some(addr) = own_addr { + AtomicU64::new(u64::from_le_bytes( + Location::from_address(&addr).as_f64().to_le_bytes(), + )) + } else { + AtomicU64::new(u64::from_le_bytes((-1f64).to_le_bytes())) + }; + + ConnectionManager::init( + Rate::new_per_second(1_000_000.0), + Rate::new_per_second(1_000_000.0), + min_conn, + max_conn, + 7, + (keypair.public().clone(), own_addr, own_location), + is_gateway, + 10, + Duration::from_secs(60), + ) + } + + fn make_addr(port: u16) -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], port)) + } + + // ============ Basic ConnectionManager tests ============ + + #[test] + fn test_connection_manager_initial_state() { + let cm = make_connection_manager(Some(make_addr(8000)), 5, 20, false); + + assert_eq!(cm.connection_count(), 0); + assert_eq!(cm.get_own_addr(), Some(make_addr(8000))); + assert!(!cm.is_gateway()); + assert_eq!(cm.min_connections, 5); + assert_eq!(cm.max_connections, 20); + } + + #[test] + fn test_connection_manager_gateway_mode() { + let cm = make_connection_manager(Some(make_addr(8000)), 5, 20, true); + assert!(cm.is_gateway()); + } + #[test] fn rejects_self_connection() { - // Create a peer id for our node let keypair = TransportKeypair::new(); let addr: SocketAddr = "127.0.0.1:8000".parse().unwrap(); - // Create connection manager with this address let cm = ConnectionManager::init( Rate::new_per_second(1_000_000.0), Rate::new_per_second(1_000_000.0), @@ -777,10 +828,7 @@ mod tests { Duration::from_secs(60), ); - // Verify the connection manager has our address set assert_eq!(cm.get_own_addr(), Some(addr)); - - // Attempt to accept a connection from ourselves - should be rejected let location = Location::new(0.5); let accepted = cm.should_accept(location, addr); assert!(!accepted, "should_accept must reject self-connection"); @@ -788,11 +836,9 @@ mod tests { #[test] fn accepts_connection_from_different_peer() { - // Create our node's address let own_keypair = TransportKeypair::new(); let own_addr: SocketAddr = "127.0.0.1:8000".parse().unwrap(); - // Create connection manager with our address let cm = ConnectionManager::init( Rate::new_per_second(1_000_000.0), Rate::new_per_second(1_000_000.0), @@ -809,10 +855,7 @@ mod tests { Duration::from_secs(60), ); - // Create a different peer let other_addr: SocketAddr = "127.0.0.2:8001".parse().unwrap(); - - // Attempt to accept a connection from a different peer - should be accepted let location = Location::new(0.6); let accepted = cm.should_accept(location, other_addr); assert!( @@ -820,4 +863,440 @@ mod tests { "should_accept must accept connection from different peer" ); } + + // ============ should_accept tests ============ + + #[test] + fn test_should_accept_below_min_connections() { + let cm = make_connection_manager(Some(make_addr(8000)), 5, 20, false); + + // First connection should always be accepted + let addr1 = make_addr(8001); + let loc1 = Location::new(0.1); + assert!(cm.should_accept(loc1, addr1)); + + // Below min connections, should accept more + let addr2 = make_addr(8002); + let loc2 = Location::new(0.2); + assert!(cm.should_accept(loc2, addr2)); + } + + #[test] + fn test_should_accept_at_max_connections() { + // The should_accept logic calculates total_conn = reserved_before + 1 + open + // It rejects when total_conn >= max_connections + // So with max=4 and 3 open connections: total_conn = 0 + 1 + 3 = 4 >= 4, rejected + // Therefore we can only have max_connections - 1 open before the next is rejected + let cm = make_connection_manager(Some(make_addr(8000)), 1, 4, false); + let keypair = TransportKeypair::new(); + + // Accept and add first connection (open=1, next attempt: 0+1+1=2 < 4) + let addr1 = make_addr(8001); + let loc1 = Location::new(0.1); + assert!(cm.should_accept(loc1, addr1)); + cm.add_connection(loc1, addr1, keypair.public().clone(), true); + + // Accept and add second connection (open=2, next attempt: 0+1+2=3 < 4) + let addr2 = make_addr(8002); + let loc2 = Location::new(0.2); + assert!(cm.should_accept(loc2, addr2)); + cm.add_connection(loc2, addr2, keypair.public().clone(), true); + + // Accept and add third connection (open=3, next attempt: 0+1+3=4 >= 4) + let addr3 = make_addr(8003); + let loc3 = Location::new(0.3); + assert!(cm.should_accept(loc3, addr3)); + cm.add_connection(loc3, addr3, keypair.public().clone(), true); + + // Fourth connection should be rejected (open=3, total_conn=4 >= max=4) + let addr4 = make_addr(8004); + let loc4 = Location::new(0.4); + assert!(!cm.should_accept(loc4, addr4)); + } + + #[test] + fn test_should_accept_already_connected_peer() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr = make_addr(8001); + let loc = Location::new(0.1); + + // Accept and add connection + assert!(cm.should_accept(loc, addr)); + cm.add_connection(loc, addr, keypair.public().clone(), true); + + // Same peer should still return true (already connected) + assert!(cm.should_accept(loc, addr)); + } + + // ============ add_connection / prune_connection tests ============ + + #[test] + fn test_add_connection() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + + assert_eq!(cm.connection_count(), 0); + cm.add_connection(loc, addr, keypair.public().clone(), false); + assert_eq!(cm.connection_count(), 1); + } + + #[test] + fn test_prune_alive_connection() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + + cm.add_connection(loc, addr, keypair.public().clone(), false); + assert_eq!(cm.connection_count(), 1); + + let pruned_loc = cm.prune_alive_connection(addr); + assert_eq!(pruned_loc, Some(loc)); + assert_eq!(cm.connection_count(), 0); + } + + #[test] + fn test_prune_nonexistent_connection() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(9999); + let pruned_loc = cm.prune_alive_connection(addr); + assert!(pruned_loc.is_none()); + } + + // ============ Transient connection tests ============ + + #[test] + fn test_transient_connection_registration() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(8001); + assert_eq!(cm.transient_count(), 0); + assert!(!cm.is_transient(addr)); + + // Register transient + assert!(cm.try_register_transient(addr, Some(Location::new(0.5)))); + assert_eq!(cm.transient_count(), 1); + assert!(cm.is_transient(addr)); + } + + #[test] + fn test_transient_connection_budget_enforcement() { + // Create manager with budget of 2 + let keypair = TransportKeypair::new(); + let own_addr = make_addr(8000); + let cm = ConnectionManager::init( + Rate::new_per_second(1_000_000.0), + Rate::new_per_second(1_000_000.0), + 1, + 10, + 7, + ( + keypair.public().clone(), + Some(own_addr), + AtomicU64::new(u64::from_le_bytes(0.5f64.to_le_bytes())), + ), + false, + 2, // transient_budget = 2 + Duration::from_secs(60), + ); + + // First two should succeed + assert!(cm.try_register_transient(make_addr(8001), None)); + assert!(cm.try_register_transient(make_addr(8002), None)); + assert_eq!(cm.transient_count(), 2); + + // Third should fail (over budget) + assert!(!cm.try_register_transient(make_addr(8003), None)); + assert_eq!(cm.transient_count(), 2); + } + + #[test] + fn test_transient_connection_update_existing() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(8001); + assert!(cm.try_register_transient(addr, None)); + assert_eq!(cm.transient_count(), 1); + + // Updating existing should succeed without incrementing count + assert!(cm.try_register_transient(addr, Some(Location::new(0.3)))); + assert_eq!(cm.transient_count(), 1); + } + + #[test] + fn test_drop_transient() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(8001); + cm.try_register_transient(addr, Some(Location::new(0.5))); + assert_eq!(cm.transient_count(), 1); + + let entry = cm.drop_transient(addr); + assert!(entry.is_some()); + assert_eq!(cm.transient_count(), 0); + assert!(!cm.is_transient(addr)); + } + + #[test] + fn test_drop_nonexistent_transient() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let entry = cm.drop_transient(make_addr(9999)); + assert!(entry.is_none()); + } + + // ============ update_location tests ============ + + #[test] + fn test_update_location() { + let cm = make_connection_manager(None, 1, 10, false); + + // Initially no location + cm.update_location(Some(Location::new(0.5))); + // Location should be updated (we can't directly read it, but it shouldn't panic) + } + + #[test] + fn test_update_location_to_none() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + cm.update_location(None); + // Should not panic + } + + // ============ try_set_own_addr tests ============ + + #[test] + fn test_try_set_own_addr_when_unset() { + let cm = make_connection_manager(None, 1, 10, false); + + let addr = make_addr(8000); + let result = cm.try_set_own_addr(addr); + assert!(result.is_none()); // Returns None when successfully set + assert_eq!(cm.get_own_addr(), Some(addr)); + } + + #[test] + fn test_try_set_own_addr_when_already_set() { + let original_addr = make_addr(8000); + let cm = make_connection_manager(Some(original_addr), 1, 10, false); + + let new_addr = make_addr(9000); + let result = cm.try_set_own_addr(new_addr); + assert_eq!(result, Some(original_addr)); // Returns existing when already set + assert_eq!(cm.get_own_addr(), Some(original_addr)); // Unchanged + } + + // ============ update_peer_identity tests ============ + + #[test] + fn test_update_peer_identity() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + let new_keypair = TransportKeypair::new(); + + let old_addr = make_addr(8001); + let new_addr = make_addr(8002); + let loc = Location::new(0.5); + + // Add initial connection + cm.add_connection(loc, old_addr, keypair.public().clone(), false); + assert_eq!(cm.connection_count(), 1); + + // Update peer identity + let updated = cm.update_peer_identity(old_addr, new_addr, new_keypair.public().clone()); + assert!(updated); + assert_eq!(cm.connection_count(), 1); + + // Old address should no longer be connected + let connections = cm.get_connections_by_location(); + let conns = connections.get(&loc).unwrap(); + assert_eq!(conns[0].location.socket_addr(), Some(new_addr)); + } + + #[test] + fn test_update_peer_identity_same_addr() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + + cm.add_connection(loc, addr, keypair.public().clone(), false); + + // Same address should return false + let updated = cm.update_peer_identity(addr, addr, keypair.public().clone()); + assert!(!updated); + } + + #[test] + fn test_update_peer_identity_nonexistent() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let old_addr = make_addr(9999); + let new_addr = make_addr(9998); + + let updated = cm.update_peer_identity(old_addr, new_addr, keypair.public().clone()); + assert!(!updated); + } + + // ============ routing tests ============ + + #[test] + fn test_routing_empty_connections() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let router = Router::new(&[]); + + let empty_set: HashSet = HashSet::new(); + let result = cm.routing(Location::new(0.5), None, &empty_set, &router); + assert!(result.is_none()); + } + + #[test] + fn test_routing_with_connections() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + let router = Router::new(&[]); + + // Add a connection + let addr = make_addr(8001); + let loc = Location::new(0.5); + cm.add_connection(loc, addr, keypair.public().clone(), false); + + let empty_set: HashSet = HashSet::new(); + let result = cm.routing(Location::new(0.5), None, &empty_set, &router); + assert!(result.is_some()); + } + + #[test] + fn test_routing_skips_requester() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + let router = Router::new(&[]); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + cm.add_connection(loc, addr, keypair.public().clone(), false); + + // Request from the same address should not route back to itself + let empty_set: HashSet = HashSet::new(); + let result = cm.routing(Location::new(0.5), Some(addr), &empty_set, &router); + assert!(result.is_none()); + } + + #[test] + fn test_routing_skips_skip_list() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + let router = Router::new(&[]); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + cm.add_connection(loc, addr, keypair.public().clone(), false); + + // Put peer in skip list + let mut skip_list = HashSet::new(); + skip_list.insert(addr); + + let result = cm.routing(Location::new(0.5), None, &skip_list, &router); + assert!(result.is_none()); + } + + #[test] + fn test_routing_skips_transient() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + let router = Router::new(&[]); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + cm.add_connection(loc, addr, keypair.public().clone(), false); + + // Mark as transient + cm.try_register_transient(addr, Some(loc)); + + let empty_set: HashSet = HashSet::new(); + let result = cm.routing(Location::new(0.5), None, &empty_set, &router); + assert!(result.is_none()); + } + + // ============ get_connections_by_location tests ============ + + #[test] + fn test_get_connections_by_location() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr1 = make_addr(8001); + let addr2 = make_addr(8002); + let loc1 = Location::new(0.3); + let loc2 = Location::new(0.7); + + cm.add_connection(loc1, addr1, keypair.public().clone(), false); + cm.add_connection(loc2, addr2, keypair.public().clone(), false); + + let connections = cm.get_connections_by_location(); + assert_eq!(connections.len(), 2); + assert!(connections.contains_key(&loc1)); + assert!(connections.contains_key(&loc2)); + } + + // ============ has_connection_or_pending tests ============ + + #[test] + fn test_has_connection_or_pending() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + let keypair = TransportKeypair::new(); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + + assert!(!cm.has_connection_or_pending(addr)); + + // After accepting (but not adding), should be pending + cm.should_accept(loc, addr); + assert!(cm.has_connection_or_pending(addr)); + + // After adding, should still return true + cm.add_connection(loc, addr, keypair.public().clone(), true); + assert!(cm.has_connection_or_pending(addr)); + } + + // ============ record_pending_location tests ============ + + #[test] + fn test_record_pending_location() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(8001); + let loc = Location::new(0.5); + + cm.record_pending_location(addr, loc); + + // Location should be recorded + assert!(cm.location_for_peer.read().contains_key(&addr)); + } + + #[test] + fn test_record_pending_location_already_exists() { + let cm = make_connection_manager(Some(make_addr(8000)), 1, 10, false); + + let addr = make_addr(8001); + let loc1 = Location::new(0.5); + let loc2 = Location::new(0.7); + + cm.record_pending_location(addr, loc1); + cm.record_pending_location(addr, loc2); + + // Should keep original location + let locations = cm.location_for_peer.read(); + assert_eq!(locations.get(&addr), Some(&loc1)); + } } diff --git a/crates/core/src/ring/location.rs b/crates/core/src/ring/location.rs index f6ceb6edd..8ba63a97a 100644 --- a/crates/core/src/ring/location.rs +++ b/crates/core/src/ring/location.rs @@ -244,10 +244,313 @@ fn distribute_hash(x: u64) -> u64 { #[cfg(test)] mod test { - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::hash::Hash; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use super::*; + // ============ Location construction tests ============ + + #[test] + fn test_location_new_valid() { + let loc = Location::new(0.5); + assert_eq!(loc.as_f64(), 0.5); + } + + #[test] + fn test_location_new_boundaries() { + // Test boundary values + let loc_zero = Location::new(0.0); + assert_eq!(loc_zero.as_f64(), 0.0); + + let loc_one = Location::new(1.0); + assert_eq!(loc_one.as_f64(), 1.0); + } + + #[test] + fn test_location_new_rounded_within_range() { + let loc = Location::new_rounded(0.75); + assert_eq!(loc.as_f64(), 0.75); + } + + #[test] + fn test_location_new_rounded_wraps_above_one() { + // Values above 1.0 should wrap using rem_euclid + let loc = Location::new_rounded(1.25); + assert!((loc.as_f64() - 0.25).abs() < f64::EPSILON); + + let loc2 = Location::new_rounded(2.5); + assert!((loc2.as_f64() - 0.5).abs() < f64::EPSILON); + } + + #[test] + fn test_location_new_rounded_wraps_negative() { + // Negative values should wrap correctly + let loc = Location::new_rounded(-0.25); + assert!((loc.as_f64() - 0.75).abs() < f64::EPSILON); + + let loc2 = Location::new_rounded(-1.25); + assert!((loc2.as_f64() - 0.75).abs() < f64::EPSILON); + } + + #[test] + fn test_location_try_from_valid() { + let loc: Result = 0.5f64.try_into(); + assert!(loc.is_ok()); + assert_eq!(loc.unwrap().as_f64(), 0.5); + } + + #[test] + fn test_location_try_from_invalid_above() { + let loc: Result = 1.5f64.try_into(); + assert!(loc.is_err()); + } + + #[test] + fn test_location_try_from_invalid_below() { + let loc: Result = (-0.1f64).try_into(); + assert!(loc.is_err()); + } + + #[test] + fn test_location_random_in_range() { + // Generate multiple random locations and verify they're in range + for _ in 0..100 { + let loc = Location::random(); + assert!(loc.as_f64() >= 0.0 && loc.as_f64() <= 1.0); + } + } + + // ============ Location distance tests ============ + + #[test] + fn test_location_distance_same_location() { + let loc = Location::new(0.5); + let dist = loc.distance(loc); + assert_eq!(dist.as_f64(), 0.0); + } + + #[test] + fn test_location_distance_adjacent() { + let l0 = Location::new(0.0); + let l1 = Location::new(0.25); + assert_eq!(l0.distance(l1), Distance::new(0.25)); + } + + #[test] + fn test_location_distance_wrap_around() { + // Distance should wrap around the ring + let l0 = Location::new(0.0); + let l1 = Location::new(0.9); + // Direct distance would be 0.9, but wrap-around is 0.1 + assert_eq!(l0.distance(l1), Distance::new(0.1)); + } + + #[test] + fn test_location_distance_exactly_half() { + let l0 = Location::new(0.0); + let l1 = Location::new(0.5); + assert_eq!(l0.distance(l1), Distance::new(0.5)); + } + + #[test] + fn test_location_distance_symmetry() { + let l0 = Location::new(0.3); + let l1 = Location::new(0.7); + assert_eq!(l0.distance(l1), l1.distance(l0)); + } + + // ============ Location arithmetic tests ============ + + #[test] + fn test_location_add_distance() { + let loc = Location::new(0.5); + let dist = Distance::new(0.1); + let (neg, pos) = loc + dist; + + assert!((neg.as_f64() - 0.4).abs() < f64::EPSILON); + assert!((pos.as_f64() - 0.6).abs() < f64::EPSILON); + } + + #[test] + fn test_location_add_distance_at_boundary() { + let loc = Location::new(0.05); + let dist = Distance::new(0.1); + let (neg, pos) = loc + dist; + + // neg location wraps to negative (won't be valid without normalization) + assert!((neg.as_f64() - (-0.05)).abs() < f64::EPSILON); + assert!((pos.as_f64() - 0.15).abs() < f64::EPSILON); + } + + // ============ Location equality and ordering tests ============ + + #[test] + fn test_location_equality() { + let l1 = Location::new(0.5); + let l2 = Location::new(0.5); + assert_eq!(l1, l2); + } + + #[test] + fn test_location_equality_with_epsilon() { + // Locations should be equal if difference is less than f64::EPSILON + let l1 = Location::new(0.5); + let l2 = Location(0.5 + f64::EPSILON / 2.0); + assert_eq!(l1, l2); + } + + #[test] + fn test_location_ordering() { + let l1 = Location::new(0.3); + let l2 = Location::new(0.7); + assert!(l1 < l2); + assert!(l2 > l1); + } + + #[test] + fn test_location_sorting() { + let mut locs = [ + Location::new(0.7), + Location::new(0.2), + Location::new(0.9), + Location::new(0.1), + ]; + locs.sort(); + + assert_eq!(locs[0].as_f64(), 0.1); + assert_eq!(locs[1].as_f64(), 0.2); + assert_eq!(locs[2].as_f64(), 0.7); + assert_eq!(locs[3].as_f64(), 0.9); + } + + // ============ Location hashing tests ============ + + #[test] + fn test_location_hash_consistency() { + use std::collections::hash_map::DefaultHasher; + + let loc1 = Location::new(0.5); + let loc2 = Location::new(0.5); + + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + + loc1.hash(&mut hasher1); + loc2.hash(&mut hasher2); + + assert_eq!(hasher1.finish(), hasher2.finish()); + } + + #[test] + fn test_location_in_hashset() { + use std::collections::HashSet; + + let mut set = HashSet::new(); + set.insert(Location::new(0.1)); + set.insert(Location::new(0.2)); + set.insert(Location::new(0.1)); // duplicate + + assert_eq!(set.len(), 2); + } + + // ============ Location display tests ============ + + #[test] + fn test_location_display() { + let loc = Location::new(0.5); + assert_eq!(format!("{}", loc), "0.5"); + } + + // ============ Distance tests ============ + + #[test] + fn test_distance_new_valid() { + let d = Distance::new(0.3); + assert_eq!(d.as_f64(), 0.3); + } + + #[test] + fn test_distance_new_normalizes_above_half() { + // Distance values > 0.5 should be normalized (1.0 - value) + let d = Distance::new(0.7); + // Use approximate comparison due to floating point + assert!((d.as_f64() - 0.3).abs() < 1e-10); + } + + #[test] + fn test_distance_new_exactly_half() { + let d = Distance::new(0.5); + assert_eq!(d.as_f64(), 0.5); + } + + #[test] + fn test_distance_add() { + let d1 = Distance::new(0.1); + let d2 = Distance::new(0.2); + let sum = d1 + d2; + // Use approximate comparison due to floating point + assert!((sum.as_f64() - 0.3).abs() < 1e-10); + } + + #[test] + fn test_distance_add_wraps_above_half() { + let d1 = Distance::new(0.3); + let d2 = Distance::new(0.4); + let sum = d1 + d2; // 0.7 > 0.5, so wraps to 0.3 + // Use approximate comparison due to floating point + assert!((sum.as_f64() - 0.3).abs() < 1e-10); + } + + #[test] + fn test_distance_equality() { + let d1 = Distance::new(0.25); + let d2 = Distance::new(0.25); + assert_eq!(d1, d2); + } + + #[test] + fn test_distance_ordering() { + let d1 = Distance::new(0.1); + let d2 = Distance::new(0.3); + assert!(d1 < d2); + } + + #[test] + fn test_distance_display() { + let d = Distance::new(0.25); + assert_eq!(format!("{}", d), "0.25"); + } + + // ============ Contract key location tests ============ + + #[test] + fn test_location_from_contract_key() { + // Test that contract key bytes produce valid locations + let bytes = [0x12, 0x34, 0x56, 0x78]; + let loc = Location::from_contract_key(&bytes); + assert!(loc.as_f64() >= 0.0 && loc.as_f64() <= 1.0); + } + + #[test] + fn test_location_from_contract_key_consistency() { + let bytes = [0xAB, 0xCD, 0xEF, 0x01]; + let loc1 = Location::from_contract_key(&bytes); + let loc2 = Location::from_contract_key(&bytes); + assert_eq!(loc1, loc2); + } + + #[test] + fn test_location_from_contract_key_different_bytes() { + let bytes1 = [0x00, 0x00, 0x00, 0x01]; + let bytes2 = [0x00, 0x00, 0x00, 0x02]; + let loc1 = Location::from_contract_key(&bytes1); + let loc2 = Location::from_contract_key(&bytes2); + assert_ne!(loc1, loc2); + } + + // ============ IPv4 address tests ============ + #[test] fn test_ipv4_address_location_distribution() { use rand::prelude::*; @@ -316,6 +619,33 @@ mod test { assert_eq!(locations, expected_locations); } + #[test] + fn test_ipv4_last_byte_masked() { + // Two IPs that differ only in last byte should have same location + // (sybil mitigation) + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 12345); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)), 12345); + + let loc1 = Location::from_address(&addr1); + let loc2 = Location::from_address(&addr2); + + assert_eq!(loc1, loc2); + } + + #[test] + fn test_ipv4_different_subnets() { + // IPs in different /24 subnets should have different locations + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 12345); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1)), 12345); + + let loc1 = Location::from_address(&addr1); + let loc2 = Location::from_address(&addr2); + + assert_ne!(loc1, loc2); + } + + // ============ IPv6 address tests ============ + #[test] fn test_ipv6_address_location() { let addresses = [ @@ -357,6 +687,46 @@ mod test { assert_eq!(locations, expected_locations); } + #[test] + fn test_ipv6_uses_first_three_segments() { + // Two IPv6 addresses with same first 3 segments should have same location + let addr1 = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0x0001, 0, 0, 0, 1)), + 12345, + ); + let addr2 = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new( + 0x2001, 0xdb8, 0x1234, 0x9999, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, + )), + 12345, + ); + + let loc1 = Location::from_address(&addr1); + let loc2 = Location::from_address(&addr2); + + assert_eq!(loc1, loc2); + } + + #[test] + fn test_ipv6_different_prefix() { + // IPv6 addresses with different first 3 segments should have different locations + let addr1 = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1234, 0, 0, 0, 0, 1)), + 12345, + ); + let addr2 = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x5678, 0, 0, 0, 0, 1)), + 12345, + ); + + let loc1 = Location::from_address(&addr1); + let loc2 = Location::from_address(&addr2); + + assert_ne!(loc1, loc2); + } + + // ============ Original tests ============ + #[test] fn location_dist() { let l0 = Location(0.); diff --git a/crates/core/src/ring/peer_key_location.rs b/crates/core/src/ring/peer_key_location.rs index 5b5828a9a..ebdbbf26d 100644 --- a/crates/core/src/ring/peer_key_location.rs +++ b/crates/core/src/ring/peer_key_location.rs @@ -216,3 +216,254 @@ impl<'a> arbitrary::Arbitrary<'a> for PeerKeyLocation { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::transport::TransportKeypair; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + fn make_pub_key() -> TransportPublicKey { + TransportKeypair::new().public().clone() + } + + // ============ PeerAddr tests ============ + + #[test] + fn test_peer_addr_unknown() { + let addr = PeerAddr::Unknown; + assert!(addr.is_unknown()); + assert!(!addr.is_known()); + assert!(addr.as_known().is_none()); + } + + #[test] + fn test_peer_addr_known() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080); + let addr = PeerAddr::Known(socket); + assert!(addr.is_known()); + assert!(!addr.is_unknown()); + assert_eq!(addr.as_known(), Some(&socket)); + } + + #[test] + fn test_peer_addr_from_socket_addr() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 5000); + let addr: PeerAddr = socket.into(); + assert!(addr.is_known()); + assert_eq!(addr.as_known(), Some(&socket)); + } + + #[test] + fn test_peer_addr_display() { + let unknown = PeerAddr::Unknown; + assert_eq!(format!("{}", unknown), ""); + + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9000); + let known = PeerAddr::Known(socket); + assert_eq!(format!("{}", known), "127.0.0.1:9000"); + } + + #[test] + fn test_peer_addr_equality() { + let socket1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 1000); + let socket2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 1000); + let socket3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8)), 2000); + + assert_eq!(PeerAddr::Known(socket1), PeerAddr::Known(socket2)); + assert_ne!(PeerAddr::Known(socket1), PeerAddr::Known(socket3)); + assert_eq!(PeerAddr::Unknown, PeerAddr::Unknown); + assert_ne!(PeerAddr::Unknown, PeerAddr::Known(socket1)); + } + + // ============ PeerKeyLocation tests ============ + + #[test] + fn test_peer_key_location_new() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 8000); + let pkl = PeerKeyLocation::new(pub_key.clone(), addr); + + assert_eq!(pkl.pub_key(), &pub_key); + assert_eq!(pkl.socket_addr(), Some(addr)); + assert!(pkl.location().is_some()); + } + + #[test] + fn test_peer_key_location_with_unknown_addr() { + let pub_key = make_pub_key(); + let pkl = PeerKeyLocation::with_unknown_addr(pub_key.clone()); + + assert_eq!(pkl.pub_key(), &pub_key); + assert!(pkl.socket_addr().is_none()); + assert!(pkl.location().is_none()); + } + + #[test] + fn test_peer_key_location_set_addr() { + let pub_key = make_pub_key(); + let mut pkl = PeerKeyLocation::with_unknown_addr(pub_key.clone()); + + // Initially unknown + assert!(pkl.socket_addr().is_none()); + assert!(pkl.location().is_none()); + + // Set address + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50)), 12345); + pkl.set_addr(addr); + + // Now known + assert_eq!(pkl.socket_addr(), Some(addr)); + assert!(pkl.location().is_some()); + } + + #[test] + fn test_peer_key_location_location_computation() { + let pub_key = make_pub_key(); + // Use addresses in different /24 subnets since last byte is masked for sybil mitigation + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), 5000); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 2, 1)), 5000); + + let pkl1 = PeerKeyLocation::new(pub_key.clone(), addr1); + let pkl2 = PeerKeyLocation::new(pub_key.clone(), addr2); + + // Different /24 subnets should produce different locations + let loc1 = pkl1.location().unwrap(); + let loc2 = pkl2.location().unwrap(); + assert_ne!(loc1, loc2); + } + + #[test] + fn test_peer_key_location_ordering_both_known() { + let pub_key = make_pub_key(); + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 5000); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 5000); + + let pkl1 = PeerKeyLocation::new(pub_key.clone(), addr1); + let pkl2 = PeerKeyLocation::new(pub_key.clone(), addr2); + + // Ordering should follow address ordering + assert!(pkl1 < pkl2); + assert!(pkl2 > pkl1); + } + + #[test] + fn test_peer_key_location_ordering_unknown_last() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080); + + let known = PeerKeyLocation::new(pub_key.clone(), addr); + let unknown = PeerKeyLocation::with_unknown_addr(pub_key.clone()); + + // Known addresses should sort before unknown + assert!(known < unknown); + assert!(unknown > known); + } + + #[test] + fn test_peer_key_location_ordering_both_unknown() { + let pub_key1 = make_pub_key(); + let pub_key2 = make_pub_key(); + + let unknown1 = PeerKeyLocation::with_unknown_addr(pub_key1); + let unknown2 = PeerKeyLocation::with_unknown_addr(pub_key2); + + // Two unknowns should be equal in ordering (regardless of pub_key) + assert_eq!(unknown1.cmp(&unknown2), std::cmp::Ordering::Equal); + } + + #[test] + fn test_peer_key_location_sorting() { + let pub_key = make_pub_key(); + let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)), 5000); + let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 5000); + let addr3 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 5000); + + let mut peers = vec![ + PeerKeyLocation::with_unknown_addr(pub_key.clone()), + PeerKeyLocation::new(pub_key.clone(), addr1), + PeerKeyLocation::new(pub_key.clone(), addr2), + PeerKeyLocation::with_unknown_addr(pub_key.clone()), + PeerKeyLocation::new(pub_key.clone(), addr3), + ]; + + peers.sort(); + + // Known addresses first (sorted), then unknowns + assert_eq!(peers[0].socket_addr(), Some(addr2)); // 10.0.0.1 + assert_eq!(peers[1].socket_addr(), Some(addr3)); // 10.0.0.2 + assert_eq!(peers[2].socket_addr(), Some(addr1)); // 10.0.0.3 + assert!(peers[3].socket_addr().is_none()); // unknown + assert!(peers[4].socket_addr().is_none()); // unknown + } + + #[test] + fn test_peer_key_location_display_with_known_addr() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9000); + let pkl = PeerKeyLocation::new(pub_key, addr); + + let display = format!("{}", pkl); + // Should contain the address and location + assert!(display.contains("127.0.0.1:9000")); + assert!(display.contains("@")); // location marker + } + + #[test] + fn test_peer_key_location_display_with_unknown_addr() { + let pub_key = make_pub_key(); + let pkl = PeerKeyLocation::with_unknown_addr(pub_key); + + let display = format!("{}", pkl); + assert!(display.contains("")); + } + + #[test] + fn test_peer_key_location_equality() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080); + + let pkl1 = PeerKeyLocation::new(pub_key.clone(), addr); + let pkl2 = PeerKeyLocation::new(pub_key.clone(), addr); + + assert_eq!(pkl1, pkl2); + } + + #[test] + fn test_peer_key_location_clone() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 20, 30, 40)), 5555); + let pkl = PeerKeyLocation::new(pub_key, addr); + let cloned = pkl.clone(); + + assert_eq!(pkl, cloned); + assert_eq!(pkl.socket_addr(), cloned.socket_addr()); + } + + #[test] + fn test_peer_key_location_random() { + // Test that random() generates valid PeerKeyLocations + let pkl1 = PeerKeyLocation::random(); + let pkl2 = PeerKeyLocation::random(); + + // Both should have known addresses + assert!(pkl1.socket_addr().is_some()); + assert!(pkl2.socket_addr().is_some()); + + // Addresses should differ (with very high probability) + assert_ne!(pkl1.socket_addr(), pkl2.socket_addr()); + } + + #[test] + fn test_peer_key_location_with_ipv6() { + let pub_key = make_pub_key(); + let addr = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 8080, + ); + let pkl = PeerKeyLocation::new(pub_key, addr); + + assert_eq!(pkl.socket_addr(), Some(addr)); + assert!(pkl.location().is_some()); + } +} diff --git a/crates/core/src/ring/score.rs b/crates/core/src/ring/score.rs deleted file mode 100644 index 0cf705080..000000000 --- a/crates/core/src/ring/score.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Score type used for ranking peers and contracts - -#[derive(PartialEq, Clone, Copy)] -pub struct Score(pub f64); - -impl PartialOrd for Score { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Score { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) - } -} - -impl Eq for Score {}