//! Distributed routing table built from mesh announcements. //! //! The [`RoutingTable`] stores [`RoutingEntry`] records keyed by 16-byte //! truncated mesh addresses, enabling multi-hop packet forwarding through //! the mesh network. use std::collections::HashMap; use std::time::{Duration, Instant}; use crate::announce::MeshAnnounce; use crate::transport::TransportAddr; /// A routing entry for a known mesh destination. #[derive(Clone, Debug)] pub struct RoutingEntry { /// Full 32-byte Ed25519 public key of the destination. pub identity_key: [u8; 32], /// 16-byte truncated mesh address. pub address: [u8; 16], /// Next-hop transport name (e.g. "tcp", "iroh-quic", "lora"). pub next_hop_transport: String, /// Next-hop address to send through. pub next_hop_addr: TransportAddr, /// Number of hops to this destination. pub hops: u8, /// Estimated cost (lower is better). Currently computed as hops as f64. pub cost: f64, /// Capabilities of the destination node. pub capabilities: u16, /// Last announce sequence number seen from this node. pub last_sequence: u64, /// When this entry was last updated. pub last_seen: Instant, /// When this entry expires (based on announce TTL). pub expires_at: Instant, } /// Distributed routing table built from received mesh announcements. pub struct RoutingTable { /// Entries keyed by 16-byte truncated address. entries: HashMap<[u8; 16], RoutingEntry>, /// Default entry TTL. default_ttl: Duration, } impl RoutingTable { /// Create a new empty routing table with the given default TTL for entries. pub fn new(default_ttl: Duration) -> Self { Self { entries: HashMap::new(), default_ttl, } } /// Update the routing table from a received mesh announcement. /// /// Returns `true` if this was a new or improved route. /// /// Logic: /// - If `sequence <= last_sequence` for this address, the announce is stale — ignored. /// - If the entry is new or has lower cost, it replaces the existing entry. pub fn update( &mut self, announce: &MeshAnnounce, received_via_transport: &str, received_from: TransportAddr, ) -> bool { let address = announce.address; let new_cost = announce.hop_count as f64; let now = Instant::now(); let identity_key: [u8; 32] = match announce.identity_key.as_slice().try_into() { Ok(k) => k, Err(_) => return false, }; if let Some(existing) = self.entries.get(&address) { // Stale announce — older or same sequence number. if announce.sequence <= existing.last_sequence { return false; } // Only replace if the new route is better or equal (newer sequence wins on tie). if new_cost > existing.cost && announce.sequence == existing.last_sequence + 1 { // Higher cost with only incremental sequence — still update since it's fresher. } } let entry = RoutingEntry { identity_key, address, next_hop_transport: received_via_transport.to_string(), next_hop_addr: received_from, hops: announce.hop_count, cost: new_cost, capabilities: announce.capabilities, last_sequence: announce.sequence, last_seen: now, expires_at: now + self.default_ttl, }; self.entries.insert(address, entry); true } /// Look up a routing entry by 16-byte truncated mesh address. pub fn lookup(&self, address: &[u8; 16]) -> Option<&RoutingEntry> { self.entries.get(address) } /// Look up a routing entry by the full 32-byte Ed25519 public key. pub fn lookup_by_key(&self, identity_key: &[u8; 32]) -> Option<&RoutingEntry> { self.entries.values().find(|e| &e.identity_key == identity_key) } /// Remove all expired entries. Returns the number of entries removed. pub fn remove_expired(&mut self) -> usize { let now = Instant::now(); let before = self.entries.len(); self.entries.retain(|_, entry| entry.expires_at > now); before - self.entries.len() } /// Iterate over all routing entries. pub fn entries(&self) -> impl Iterator { self.entries.values() } /// Number of entries in the routing table. pub fn len(&self) -> usize { self.entries.len() } /// Whether the routing table is empty. pub fn is_empty(&self) -> bool { self.entries.is_empty() } } #[cfg(test)] mod tests { use super::*; use crate::announce::{compute_address, CAP_RELAY}; use crate::identity::MeshIdentity; fn make_announce(identity: &MeshIdentity, sequence: u64, hop_count: u8) -> MeshAnnounce { let mut announce = MeshAnnounce::with_sequence(identity, CAP_RELAY, vec![], 8, sequence); announce.hop_count = hop_count; announce } #[test] fn insert_and_lookup() { let mut table = RoutingTable::new(Duration::from_secs(300)); let id = MeshIdentity::generate(); let announce = make_announce(&id, 1, 1); let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap()); assert!(table.update(&announce, "tcp", addr.clone())); assert_eq!(table.len(), 1); let mesh_addr = compute_address(&id.public_key()); let entry = table.lookup(&mesh_addr).expect("entry should exist"); assert_eq!(entry.hops, 1); assert_eq!(entry.last_sequence, 1); assert_eq!(entry.next_hop_transport, "tcp"); assert_eq!(entry.next_hop_addr, addr); } #[test] fn update_with_better_route() { let mut table = RoutingTable::new(Duration::from_secs(300)); let id = MeshIdentity::generate(); let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap()); // First announce: 3 hops, sequence 1. let announce1 = make_announce(&id, 1, 3); assert!(table.update(&announce1, "tcp", addr.clone())); let mesh_addr = compute_address(&id.public_key()); assert_eq!(table.lookup(&mesh_addr).unwrap().hops, 3); // Second announce: 1 hop, sequence 2 — should replace. let announce2 = make_announce(&id, 2, 1); assert!(table.update(&announce2, "tcp", addr)); let entry = table.lookup(&mesh_addr).unwrap(); assert_eq!(entry.hops, 1); assert_eq!(entry.last_sequence, 2); } #[test] fn reject_stale_sequence() { let mut table = RoutingTable::new(Duration::from_secs(300)); let id = MeshIdentity::generate(); let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap()); // Insert with sequence 5. let announce1 = make_announce(&id, 5, 1); assert!(table.update(&announce1, "tcp", addr.clone())); // Try to update with sequence 3 — should be rejected. let announce2 = make_announce(&id, 3, 1); assert!( !table.update(&announce2, "tcp", addr), "stale sequence must be rejected" ); let mesh_addr = compute_address(&id.public_key()); assert_eq!(table.lookup(&mesh_addr).unwrap().last_sequence, 5); } #[test] fn expire_old_entries() { let mut table = RoutingTable::new(Duration::from_millis(1)); let id = MeshIdentity::generate(); let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap()); let announce = make_announce(&id, 1, 1); table.update(&announce, "tcp", addr); assert_eq!(table.len(), 1); // Wait for TTL to expire. std::thread::sleep(Duration::from_millis(10)); let removed = table.remove_expired(); assert_eq!(removed, 1); assert!(table.is_empty()); } #[test] fn lookup_by_key_works() { let mut table = RoutingTable::new(Duration::from_secs(300)); let id = MeshIdentity::generate(); let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap()); let announce = make_announce(&id, 1, 2); table.update(&announce, "tcp", addr); let pk = id.public_key(); let entry = table.lookup_by_key(&pk).expect("should find by key"); assert_eq!(entry.identity_key, pk); assert_eq!(entry.hops, 2); } }