Fix transit node coord_cache expiry breaking multi-hop routing

Transit nodes cache destination coordinates when they forward
SessionSetup messages (via try_warm_coord_cache). These coord_cache
entries have a 5-minute TTL, after which they expire. Once expired,
the transit node can no longer forward data packets for that
destination — find_next_hop returns None and the node sends
CoordsRequired errors back to the source. This creates a permanent
routing failure for any multi-hop path after 5 minutes of the initial
session establishment, even if traffic is actively flowing.

The root cause was that find_next_hop used coord_cache.get(), a
read-only lookup that checks expiry but never extends it. Active
forwarding did not keep the cache warm. Meanwhile, get_and_touch()
existed but only updated last_used without extending expires_at.

Fix:
- find_next_hop now calls coord_cache.get_and_touch() instead of get()
- get_and_touch now calls entry.refresh() instead of entry.touch(),
  which extends expires_at by the default TTL on each access
- find_next_hop signature changed from &self to &mut self to allow
  the mutable cache access

This ensures that as long as traffic flows through a transit node,
the coord_cache entries stay warm and routing continues to work.
Entries still expire after 5 minutes of inactivity as designed.
This commit is contained in:
Johnathan Corgan
2026-02-16 12:41:45 +00:00
parent 930f139787
commit 5987cbfb69
3 changed files with 64 additions and 19 deletions
+5 -5
View File
@@ -896,7 +896,7 @@ impl Node {
/// fallback). Without coordinates, the node cannot make loop-free
/// forwarding decisions. The caller should signal `CoordsRequired` back
/// to the source when `None` is returned for a non-local destination.
pub fn find_next_hop(&self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
pub fn find_next_hop(&mut self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
// 1. Local delivery
if dest_node_addr == self.node_addr() {
return None;
@@ -915,17 +915,17 @@ impl Node {
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms)
.or_else(|| self.route_cache.get(dest_node_addr).map(|c| c.coords()))?;
let dest_coords = self.coord_cache.get_and_touch(dest_node_addr, now_ms)
.or_else(|| self.route_cache.get(dest_node_addr).map(|c| c.coords()))?.clone();
// 3. Bloom filter candidates — requires dest_coords for loop-free selection
let candidates: Vec<&ActivePeer> = self.destination_in_filters(dest_node_addr);
if !candidates.is_empty() {
return self.select_best_candidate(&candidates, dest_coords);
return self.select_best_candidate(&candidates, &dest_coords);
}
// 4. Greedy tree routing fallback
let next_hop_id = self.tree_state.find_next_hop(dest_coords)?;
let next_hop_id = self.tree_state.find_next_hop(&dest_coords)?;
self.peers.get(&next_hop_id).filter(|p| p.can_send())
}