mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
Discovery protocol: LookupRequest/LookupResponse handlers
Implement the coordinate discovery protocol with flood-based lookup and reverse-path response routing. Wire format: LookupRequest (0x30) encode/decode with TTL, visited bloom filter, origin coords. LookupResponse (0x31) encode/decode with target coords and Schnorr proof signature. Handler logic: request dedup by request_id, visited filter loop prevention, TTL enforcement, lazy purge of expired entries (10s). Response routing: originator caches route in route_cache, transit nodes reverse-path forward via recent_requests. Node state: RecentRequest struct, route_cache (RouteCache), and recent_requests map for dedup + reverse-path forwarding. 13 handler tests (9 unit + 4 integration) plus 4 protocol tests. 392 tests pass, clean build.
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
//! Discovery protocol tests: LookupRequest and LookupResponse.
|
||||
//!
|
||||
//! Unit tests for handler logic (dedup, visited filter, TTL, response
|
||||
//! caching) and integration tests for multi-node forwarding and
|
||||
//! reverse-path response routing.
|
||||
|
||||
use super::*;
|
||||
use crate::node::RecentRequest;
|
||||
use crate::protocol::{LookupRequest, LookupResponse};
|
||||
use crate::tree::TreeCoordinate;
|
||||
use spanning_tree::{cleanup_nodes, process_available_packets, run_tree_test};
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — LookupRequest Handler
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_decode_error() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
// Too-short payload: should log error and return without panic
|
||||
node.handle_lookup_request(&from, &[0x00; 5]).await;
|
||||
assert!(node.recent_requests.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_dedup() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let request = LookupRequest::new(999, target, origin, coords, 5);
|
||||
let payload = &request.encode()[1..]; // skip msg_type byte
|
||||
|
||||
// First request: accepted
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
assert_eq!(node.recent_requests.len(), 1);
|
||||
|
||||
// Duplicate request: dropped
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
assert_eq!(node.recent_requests.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_visited_filter_self() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let mut request = LookupRequest::new(888, target, origin, coords, 5);
|
||||
// Mark ourselves as already visited
|
||||
request.visited.insert(node.node_addr());
|
||||
|
||||
let payload = &request.encode()[1..];
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
|
||||
// Request was recorded (dedup happens before visited check)
|
||||
// but the handler should have stopped after detecting self in visited filter
|
||||
assert!(node.recent_requests.contains_key(&888));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_target_is_self() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let my_addr = *node.node_addr();
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
// Request targeting us
|
||||
let request = LookupRequest::new(777, my_addr, origin, coords, 5);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Should succeed without panic (response send will fail silently
|
||||
// since we have no peers to route toward origin)
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
assert!(node.recent_requests.contains_key(&777));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_ttl_zero_not_forwarded() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
|
||||
let request = LookupRequest::new(666, target, origin, coords, 0);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
node.handle_lookup_request(&from, payload).await;
|
||||
// Request recorded, but not forwarded (TTL=0, and no peers anyway)
|
||||
assert!(node.recent_requests.contains_key(&666));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — LookupResponse Handler
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_decode_error() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
node.handle_lookup_response(&from, &[0x00; 10]).await;
|
||||
// No panic, no route cached
|
||||
assert!(node.route_cache.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_originator_caches_route() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let target = make_node_addr(0xBB);
|
||||
let root = make_node_addr(0xF0);
|
||||
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
||||
|
||||
// Create a valid response with a real proof signature
|
||||
let proof_data = LookupResponse::proof_bytes(555, &target);
|
||||
let target_identity = Identity::generate();
|
||||
let proof = target_identity.sign(&proof_data);
|
||||
|
||||
let response = LookupResponse::new(555, target, coords.clone(), proof);
|
||||
let payload = &response.encode()[1..]; // skip msg_type
|
||||
|
||||
// No entry in recent_requests for 555 → we're the originator
|
||||
assert!(!node.recent_requests.contains_key(&555));
|
||||
|
||||
node.handle_lookup_response(&from, payload).await;
|
||||
|
||||
// Route should be cached
|
||||
assert!(node.route_cache.contains(&target));
|
||||
let cached = node.route_cache.get(&target).unwrap();
|
||||
assert_eq!(cached.coords(), &coords);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_response_transit_needs_recent_request() {
|
||||
let mut node = make_node();
|
||||
let from = make_node_addr(0xAA);
|
||||
let target = make_node_addr(0xBB);
|
||||
let root = make_node_addr(0xF0);
|
||||
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
||||
|
||||
let proof_data = LookupResponse::proof_bytes(444, &target);
|
||||
let target_identity = Identity::generate();
|
||||
let proof = target_identity.sign(&proof_data);
|
||||
|
||||
let response = LookupResponse::new(444, target, coords, proof);
|
||||
let payload = &response.encode()[1..];
|
||||
|
||||
// Simulate being a transit node: record a recent_request for this ID
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
node.recent_requests.insert(
|
||||
444,
|
||||
RecentRequest::new(make_node_addr(0xDD), now_ms),
|
||||
);
|
||||
|
||||
// Handle response — should try to reverse-path forward to 0xDD
|
||||
// (will fail silently since 0xDD is not an actual peer)
|
||||
node.handle_lookup_response(&from, payload).await;
|
||||
|
||||
// Should NOT cache in route_cache (we're transit, not originator)
|
||||
assert!(!node.route_cache.contains(&target));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests — RecentRequest Expiry
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recent_request_expiry() {
|
||||
let mut node = make_node();
|
||||
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
|
||||
// Insert an old request (11 seconds ago)
|
||||
node.recent_requests.insert(
|
||||
123,
|
||||
RecentRequest::new(make_node_addr(1), now_ms - 11_000),
|
||||
);
|
||||
|
||||
// Insert a recent request
|
||||
node.recent_requests.insert(
|
||||
456,
|
||||
RecentRequest::new(make_node_addr(2), now_ms),
|
||||
);
|
||||
|
||||
assert_eq!(node.recent_requests.len(), 2);
|
||||
|
||||
// Trigger purge via a new lookup request
|
||||
let target = make_node_addr(0xBB);
|
||||
let origin = make_node_addr(0xCC);
|
||||
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
|
||||
let request = LookupRequest::new(789, target, origin, coords, 3);
|
||||
let payload = &request.encode()[1..];
|
||||
node.handle_lookup_request(&make_node_addr(0xAA), payload).await;
|
||||
|
||||
// Old entry (123) should be purged, recent entry (456) and new entry (789) kept
|
||||
assert!(!node.recent_requests.contains_key(&123));
|
||||
assert!(node.recent_requests.contains_key(&456));
|
||||
assert!(node.recent_requests.contains_key(&789));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests — Multi-Node Forwarding
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_forwarding_two_node() {
|
||||
// Set up a two-node topology: node0 — node1
|
||||
// Send a LookupRequest from node0 targeting some unknown node.
|
||||
// Node1 should receive the forwarded request.
|
||||
let edges = vec![(0, 1)];
|
||||
let mut nodes = run_tree_test(2, &edges, false).await;
|
||||
|
||||
let node0_addr = *nodes[0].node.node_addr();
|
||||
let target = make_node_addr(0xEE); // unknown node
|
||||
let root = make_node_addr(0);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![node0_addr, root]).unwrap();
|
||||
let request = LookupRequest::new(42, target, node0_addr, coords, 5);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Handle on node0 as if we received it from outside
|
||||
nodes[0]
|
||||
.node
|
||||
.handle_lookup_request(&node0_addr, payload)
|
||||
.await;
|
||||
|
||||
// Process packets — node1 should receive the forwarded request
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
let count = process_available_packets(&mut nodes).await;
|
||||
assert!(count > 0, "Expected forwarded LookupRequest to arrive at node 1");
|
||||
|
||||
// Node1 should have recorded the request
|
||||
assert!(
|
||||
nodes[1].node.recent_requests.contains_key(&42),
|
||||
"Node 1 should have recorded the forwarded request"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_target_found_generates_response() {
|
||||
// Set up a two-node topology: node0 — node1
|
||||
// Node0 initiates a lookup targeting node1.
|
||||
// Node1 receives, detects it's the target, generates a LookupResponse.
|
||||
// Response routes back to node0 which caches the coordinates.
|
||||
let edges = vec![(0, 1)];
|
||||
let mut nodes = run_tree_test(2, &edges, false).await;
|
||||
|
||||
let node1_addr = *nodes[1].node.node_addr();
|
||||
|
||||
// Node0 initiates lookup (doesn't record in recent_requests)
|
||||
nodes[0].node.initiate_lookup(&node1_addr, 5).await;
|
||||
|
||||
// Process packets in rounds to allow request + response
|
||||
for _ in 0..4 {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
process_available_packets(&mut nodes).await;
|
||||
}
|
||||
|
||||
// Node0 should have cached node1's route (it originated the request)
|
||||
assert!(
|
||||
nodes[0].node.route_cache.contains(&node1_addr),
|
||||
"Node 0 should have cached node 1's route from LookupResponse"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_three_node_chain() {
|
||||
// Topology: node0 — node1 — node2
|
||||
// Node0 initiates a lookup targeting node2.
|
||||
// Request should propagate: node0 → node1 → node2.
|
||||
// Node2 generates response, reverse-path: node2 → node1 → node0.
|
||||
let edges = vec![(0, 1), (1, 2)];
|
||||
let mut nodes = run_tree_test(3, &edges, false).await;
|
||||
|
||||
let node2_addr = *nodes[2].node.node_addr();
|
||||
|
||||
// Node0 initiates lookup (doesn't record in recent_requests)
|
||||
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
||||
|
||||
// Process packets in rounds to allow multi-hop propagation + response
|
||||
// Chain: node0→node1→node2 (request), node2→node1→node0 (response)
|
||||
for _ in 0..10 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
process_available_packets(&mut nodes).await;
|
||||
}
|
||||
|
||||
// Node1 should have been a transit node (has the request_id in recent_requests)
|
||||
assert!(
|
||||
!nodes[1].node.recent_requests.is_empty(),
|
||||
"Node 1 should have recorded the forwarded request"
|
||||
);
|
||||
|
||||
// Node2 should have received the request (it's the target)
|
||||
assert!(
|
||||
!nodes[2].node.recent_requests.is_empty(),
|
||||
"Node 2 should have received the request"
|
||||
);
|
||||
|
||||
// Node0 should have cached node2's route
|
||||
assert!(
|
||||
nodes[0].node.route_cache.contains(&node2_addr),
|
||||
"Node 0 should have cached node 2's route through 3-node chain"
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_dedup_convergent_paths() {
|
||||
// Topology: triangle (node0 — node1, node0 — node2, node1 — node2)
|
||||
// A request from node0 reaches node2 via two paths: 0→1→2 and 0→2.
|
||||
// The second arrival at node2 should be deduped.
|
||||
let edges = vec![(0, 1), (0, 2), (1, 2)];
|
||||
let mut nodes = run_tree_test(3, &edges, false).await;
|
||||
|
||||
let node0_addr = *nodes[0].node.node_addr();
|
||||
let target = make_node_addr(0xEE);
|
||||
let root = make_node_addr(0);
|
||||
|
||||
let coords = TreeCoordinate::from_addrs(vec![node0_addr, root]).unwrap();
|
||||
let request = LookupRequest::new(300, target, node0_addr, coords, 5);
|
||||
let payload = &request.encode()[1..];
|
||||
|
||||
// Node0 handles the request (forwards to both node1 and node2)
|
||||
nodes[0]
|
||||
.node
|
||||
.handle_lookup_request(&node0_addr, payload)
|
||||
.await;
|
||||
|
||||
// Process several rounds
|
||||
for _ in 0..5 {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
process_available_packets(&mut nodes).await;
|
||||
}
|
||||
|
||||
// Both node1 and node2 should have recorded the request
|
||||
assert!(nodes[1].node.recent_requests.contains_key(&300));
|
||||
assert!(nodes[2].node.recent_requests.contains_key(&300));
|
||||
|
||||
// The request should appear exactly once in each node's recent_requests
|
||||
// (dedup prevents duplicate processing via convergent paths)
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
|
||||
mod bloom;
|
||||
mod disconnect;
|
||||
mod discovery;
|
||||
mod forwarding;
|
||||
mod handshake;
|
||||
mod routing;
|
||||
|
||||
Reference in New Issue
Block a user