diff --git a/src/node/handlers/discovery.rs b/src/node/handlers/discovery.rs index 1a484e3..cf343f3 100644 --- a/src/node/handlers/discovery.rs +++ b/src/node/handlers/discovery.rs @@ -7,7 +7,7 @@ use crate::node::{Node, RecentRequest}; use crate::protocol::{LookupRequest, LookupResponse}; use crate::{NodeAddr, PeerIdentity}; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, trace, warn}; impl Node { /// Handle an incoming LookupRequest from a peer. @@ -153,10 +153,10 @@ impl Node { let target_pubkey = match self.lookup_by_fips_prefix(&prefix) { Some((_addr, pubkey)) => pubkey, None => { - error!( + warn!( request_id = response.request_id, target = %self.peer_display_name(&target), - "identity_cache miss for lookup target — this is a bug" + "identity_cache miss for lookup target, cannot verify proof" ); return; } diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index ae99622..9825940 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -731,7 +731,14 @@ impl Node { "CoordsRequired response rate-limited, skipping standalone CoordsWarmup"); } - self.maybe_initiate_lookup(&msg.dest_addr).await; + // Only trigger discovery if we have the target's identity cached — + // otherwise we can't verify the LookupResponse proof. + if self.has_cached_identity(&msg.dest_addr) { + self.maybe_initiate_lookup(&msg.dest_addr).await; + } else { + debug!(dest = %msg.dest_addr, + "Skipping discovery after CoordsRequired: no cached identity for target"); + } // Reset coords warmup counter so the next N packets also include // COORDS_PRESENT, re-warming transit caches along the path. @@ -783,8 +790,16 @@ impl Node { // Invalidate stale cached coordinates self.coord_cache.remove(&msg.dest_addr); - // Trigger re-discovery to get fresh coordinates - self.maybe_initiate_lookup(&msg.dest_addr).await; + // Trigger re-discovery to get fresh coordinates, but only if we have + // the target's identity cached — otherwise we can't verify the + // LookupResponse proof. This avoids a race when the XK responder + // receives PathBroken before msg3 completes (identity unknown). + if self.has_cached_identity(&msg.dest_addr) { + self.maybe_initiate_lookup(&msg.dest_addr).await; + } else { + debug!(dest = %msg.dest_addr, + "Skipping discovery after PathBroken: no cached identity for target"); + } // Reset coords warmup counter so the next N packets include // COORDS_PRESENT, re-warming transit caches along the new path. diff --git a/src/node/mod.rs b/src/node/mod.rs index 9ef4e0b..9d705b9 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -963,6 +963,13 @@ impl Node { } } + /// Check if a node's identity is in the cache (without LRU touch). + pub(crate) fn has_cached_identity(&self, addr: &NodeAddr) -> bool { + let mut prefix = [0u8; 15]; + prefix.copy_from_slice(&addr.as_bytes()[0..15]); + self.identity_cache.contains_key(&prefix) + } + /// Number of identity cache entries. pub fn identity_cache_len(&self) -> usize { self.identity_cache.len() diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index 01b31aa..455699d 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -262,7 +262,7 @@ async fn test_response_proof_verification_failure() { #[tokio::test] async fn test_response_identity_cache_miss() { // Verify that a response is discarded when the origin lacks the - // target's pubkey in identity_cache (should never happen in practice). + // target's pubkey in identity_cache (e.g., XK responder before msg3). let mut node = make_node(); let from = make_node_addr(0xAA);