Fix identity cache miss during lookup response verification

Guard discovery triggers in handle_path_broken() and
handle_coords_required() with has_cached_identity() check. When the
XK responder receives an error signal before msg3 completes, the
initiator's identity is unknown, making LookupResponse proof
verification impossible. Skip discovery in this case — the handshake
retry mechanism handles recovery.

Downgrade the identity_cache miss log from ERROR to WARN since it's a
known race condition, not a bug.
This commit is contained in:
Johnathan Corgan
2026-02-23 18:44:27 +00:00
parent 0d93a19e07
commit e3b5bd0bcd
4 changed files with 29 additions and 7 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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()

View File

@@ -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);