diff --git a/src/cache/coord_cache.rs b/src/cache/coord_cache.rs index f77ca83..0360cf6 100644 --- a/src/cache/coord_cache.rs +++ b/src/cache/coord_cache.rs @@ -79,6 +79,32 @@ impl CoordCache { self.entries.insert(addr, entry); } + /// Insert or update a cache entry with path MTU information. + /// + /// Used by discovery response handling to store the discovered path MTU + /// alongside the target's coordinates. + pub fn insert_with_path_mtu( + &mut self, + addr: NodeAddr, + coords: TreeCoordinate, + current_time_ms: u64, + path_mtu: u16, + ) { + if let Some(entry) = self.entries.get_mut(&addr) { + entry.update(coords, current_time_ms, self.default_ttl_ms); + entry.set_path_mtu(path_mtu); + return; + } + + if self.entries.len() >= self.max_entries { + self.evict_one(current_time_ms); + } + + let mut entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms); + entry.set_path_mtu(path_mtu); + self.entries.insert(addr, entry); + } + /// Insert with a custom TTL. pub fn insert_with_ttl( &mut self, diff --git a/src/cache/entry.rs b/src/cache/entry.rs index c82f62d..ec8f6f6 100644 --- a/src/cache/entry.rs +++ b/src/cache/entry.rs @@ -13,6 +13,12 @@ pub struct CacheEntry { last_used: u64, /// When this entry expires (Unix milliseconds). expires_at: u64, + /// Path MTU discovered during lookup (if available). + /// + /// Set from the `LookupResponse.path_mtu` field when a discovery + /// response is cached. `None` when populated from SessionSetup or + /// other sources that don't carry path MTU information. + path_mtu: Option, } impl CacheEntry { @@ -23,6 +29,7 @@ impl CacheEntry { created_at: current_time_ms, last_used: current_time_ms, expires_at: current_time_ms.saturating_add(ttl_ms), + path_mtu: None, } } @@ -46,6 +53,16 @@ impl CacheEntry { self.expires_at } + /// Get the path MTU discovered during lookup, if available. + pub fn path_mtu(&self) -> Option { + self.path_mtu + } + + /// Set the path MTU discovered during lookup. + pub fn set_path_mtu(&mut self, mtu: u16) { + self.path_mtu = Some(mtu); + } + /// Check if this entry has expired. pub fn is_expired(&self, current_time_ms: u64) -> bool { current_time_ms > self.expires_at diff --git a/src/node/handlers/discovery.rs b/src/node/handlers/discovery.rs index dc2ba44..1a484e3 100644 --- a/src/node/handlers/discovery.rs +++ b/src/node/handlers/discovery.rs @@ -92,14 +92,14 @@ impl Node { /// Processing steps: /// 1. Decode and validate /// 2. Check recent_requests to determine if we originated or are forwarding - /// 3. If originator: verify proof signature, then cache target_coords in coord_cache - /// 4. If transit: reverse-path forward to from_peer + /// 3. If originator: verify proof signature, then cache target_coords and path_mtu in coord_cache + /// 4. If transit: apply path_mtu min(outgoing_link_mtu), reverse-path forward to from_peer pub(in crate::node) async fn handle_lookup_response( &mut self, from: &NodeAddr, payload: &[u8], ) { - let response = match LookupResponse::decode(payload) { + let mut response = match LookupResponse::decode(payload) { Ok(resp) => resp, Err(e) => { debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupResponse"); @@ -114,10 +114,23 @@ impl Node { // Transit node: reverse-path forward let from_peer = recent.from_peer; + // Apply path_mtu min() from the outgoing link's transport MTU + if let Some(peer) = self.peers.get(&from_peer) + && let Some(tid) = peer.transport_id() + && let Some(transport) = self.transports.get(&tid) + { + if let Some(addr) = peer.current_addr() { + response.path_mtu = response.path_mtu.min(transport.link_mtu(addr)); + } else { + response.path_mtu = response.path_mtu.min(transport.mtu()); + } + } + debug!( request_id = response.request_id, target = %self.peer_display_name(&response.target), next_hop = %self.peer_display_name(&from_peer), + path_mtu = response.path_mtu, "Reverse-path forwarding LookupResponse" ); @@ -132,6 +145,7 @@ impl Node { } else { // We originated this request — verify proof before caching let target = response.target; + let path_mtu = response.path_mtu; // Look up the target's public key from identity_cache let mut prefix = [0u8; 15]; @@ -169,13 +183,15 @@ impl Node { request_id = response.request_id, target = %self.peer_display_name(&target), depth = response.target_coords.depth(), + path_mtu = path_mtu, "Received LookupResponse, proof verified, caching route" ); - self.coord_cache.insert( + self.coord_cache.insert_with_path_mtu( target, response.target_coords, now_ms, + path_mtu, ); // Clean up pending lookup tracking @@ -321,7 +337,7 @@ impl Node { pub(in crate::node) async fn initiate_lookup(&mut self, target: &NodeAddr, ttl: u8) { let origin = *self.node_addr(); let origin_coords = self.tree_state().my_coords().clone(); - let mut request = LookupRequest::generate(*target, origin, origin_coords, ttl); + let mut request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0); // Add ourselves to the visited filter so forwarding nodes // won't send the request back to us diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index e58d541..01b31aa 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -34,7 +34,7 @@ async fn test_request_dedup() { 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 request = LookupRequest::new(999, target, origin, coords, 5, 0); let payload = &request.encode()[1..]; // skip msg_type byte // First request: accepted @@ -54,7 +54,7 @@ async fn test_request_visited_filter_self() { 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); + let mut request = LookupRequest::new(888, target, origin, coords, 5, 0); // Mark ourselves as already visited request.visited.insert(node.node_addr()); @@ -75,7 +75,7 @@ async fn test_request_target_is_self() { 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 request = LookupRequest::new(777, my_addr, origin, coords, 5, 0); let payload = &request.encode()[1..]; // Should succeed without panic (response send will fail silently @@ -92,7 +92,7 @@ async fn test_request_ttl_zero_not_forwarded() { 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 request = LookupRequest::new(666, target, origin, coords, 0, 0); let payload = &request.encode()[1..]; node.handle_lookup_request(&from, payload).await; @@ -362,7 +362,7 @@ async fn test_recent_request_expiry() { 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 request = LookupRequest::new(789, target, origin, coords, 3, 0); let payload = &request.encode()[1..]; node.handle_lookup_request(&make_node_addr(0xAA), payload).await; @@ -389,7 +389,7 @@ async fn test_request_forwarding_two_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 request = LookupRequest::new(42, target, node0_addr, coords, 5, 0); let payload = &request.encode()[1..]; // Handle on node0 as if we received it from outside @@ -509,7 +509,7 @@ async fn test_request_dedup_convergent_paths() { 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 request = LookupRequest::new(300, target, node0_addr, coords, 5, 0); let payload = &request.encode()[1..]; // Node0 handles the request (forwards to both node1 and node2) @@ -695,3 +695,194 @@ async fn test_discovery_100_nodes() { cleanup_nodes(&mut nodes).await; } + +// ============================================================================ +// Integration Tests — MTU Propagation +// ============================================================================ + +#[tokio::test] +async fn test_response_path_mtu_two_node() { + // Two-node topology: node0 — node1 + // Node0 initiates lookup for node1. The response should carry path_mtu + // reflecting the transport MTU (1280 in tests) clamped by transit. + // In a two-node setup: node1 (target) initializes path_mtu=u16::MAX, + // then the response is sent directly to node0. Since node1 is the + // target and sends directly, the transit logic does not apply for the + // first hop (the target sends directly). But node0 is the originator + // and doesn't apply transit MTU. So path_mtu should be u16::MAX in + // this simple case (no transit nodes to clamp it). + let edges = vec![(0, 1)]; + let mut nodes = run_tree_test(2, &edges, false).await; + + let node1_addr = *nodes[1].node.node_addr(); + + nodes[0].node.initiate_lookup(&node1_addr, 5).await; + + for _ in 0..4 { + tokio::time::sleep(Duration::from_millis(50)).await; + process_available_packets(&mut nodes).await; + } + + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + assert!( + nodes[0].node.coord_cache().contains(&node1_addr, now_ms), + "Node 0 should have cached node 1's route" + ); + + // Check that path_mtu was stored in the cache entry + let entry = nodes[0].node.coord_cache().get_entry(&node1_addr).unwrap(); + let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery"); + // In a 2-node setup, no transit node applies the min() so path_mtu stays u16::MAX + assert_eq!( + path_mtu, + u16::MAX, + "Two-node path_mtu should be u16::MAX (no transit nodes to clamp)" + ); + + cleanup_nodes(&mut nodes).await; +} + +#[tokio::test] +async fn test_response_path_mtu_three_node_chain() { + // Topology: node0 — node1 — node2 + // Node0 initiates lookup for node2. The response travels node2→node1→node0. + // Node1 is a transit node and applies path_mtu = min(u16::MAX, link_mtu). + // With test transport MTU of 1280, the final path_mtu at node0 should be 1280. + 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(); + let node2_pubkey = nodes[2].node.identity().pubkey_full(); + + nodes[0].node.register_identity(node2_addr, node2_pubkey); + + nodes[0].node.initiate_lookup(&node2_addr, 8).await; + + for _ in 0..10 { + tokio::time::sleep(Duration::from_millis(100)).await; + process_available_packets(&mut nodes).await; + } + + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + assert!( + nodes[0].node.coord_cache().contains(&node2_addr, now_ms), + "Node 0 should have cached node 2's route" + ); + + // Node1 is transit and applies min(u16::MAX, 1280) = 1280 + let entry = nodes[0].node.coord_cache().get_entry(&node2_addr).unwrap(); + let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery"); + assert_eq!( + path_mtu, 1280, + "Three-node chain path_mtu should reflect transit node's transport MTU (1280)" + ); + + cleanup_nodes(&mut nodes).await; +} + +// ============================================================================ +// Unit Tests — Cache Entry path_mtu +// ============================================================================ + +#[tokio::test] +async fn test_cache_entry_path_mtu_stored() { + // Verify that insert_with_path_mtu stores the path_mtu in the cache entry + let mut node = make_node(); + let target = make_node_addr(0xBB); + + let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap(); + + let now_ms = 1000u64; + node.coord_cache_mut().insert_with_path_mtu( + target, + coords, + now_ms, + 1280, + ); + + let entry = node.coord_cache().get_entry(&target).unwrap(); + assert_eq!(entry.path_mtu(), Some(1280)); +} + +#[tokio::test] +async fn test_cache_entry_no_path_mtu_from_regular_insert() { + // Verify that regular insert() does not set path_mtu + let mut node = make_node(); + let target = make_node_addr(0xBB); + + let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap(); + + let now_ms = 1000u64; + node.coord_cache_mut().insert(target, coords, now_ms); + + let entry = node.coord_cache().get_entry(&target).unwrap(); + assert_eq!(entry.path_mtu(), None); +} + +// ============================================================================ +// Unit Tests — LookupRequest min_mtu field +// ============================================================================ + +#[tokio::test] +async fn test_request_min_mtu_preserved_through_encode_decode() { + // Verify min_mtu survives encode/decode in the handler test context + 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(100, target, origin, coords, 5, 1386); + let encoded = request.encode(); + let decoded = LookupRequest::decode(&encoded[1..]).unwrap(); + assert_eq!(decoded.min_mtu, 1386); +} + +// ============================================================================ +// Unit Tests — LookupResponse path_mtu in originator handling +// ============================================================================ + +#[tokio::test] +async fn test_originator_stores_path_mtu_in_cache() { + // Verify that the originator stores path_mtu from the response in coord_cache + let mut node = make_node(); + let from = make_node_addr(0xAA); + + let target_identity = Identity::generate(); + let target = *target_identity.node_addr(); + let root = make_node_addr(0xF0); + let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap(); + + node.register_identity(target, target_identity.pubkey_full()); + + let proof_data = LookupResponse::proof_bytes(800, &target, &coords); + let proof = target_identity.sign(&proof_data); + + let mut response = LookupResponse::new(800, target, coords.clone(), proof); + // Simulate transit having reduced path_mtu + response.path_mtu = 1280; + + let payload = &response.encode()[1..]; + + node.handle_lookup_response(&from, payload).await; + + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + assert!(node.coord_cache().contains(&target, now_ms)); + + let entry = node.coord_cache().get_entry(&target).unwrap(); + assert_eq!( + entry.path_mtu(), + Some(1280), + "Originator should store path_mtu from LookupResponse in cache" + ); +} diff --git a/src/protocol/discovery.rs b/src/protocol/discovery.rs index 96f0c4a..6bb20b1 100644 --- a/src/protocol/discovery.rs +++ b/src/protocol/discovery.rs @@ -23,6 +23,9 @@ pub struct LookupRequest { pub origin_coords: TreeCoordinate, /// Remaining propagation hops. pub ttl: u8, + /// Minimum transport MTU the origin requires for a viable route. + /// 0 means no requirement. + pub min_mtu: u16, /// Visited nodes filter (loop prevention). pub visited: BloomFilter, } @@ -35,6 +38,7 @@ impl LookupRequest { origin: NodeAddr, origin_coords: TreeCoordinate, ttl: u8, + min_mtu: u16, ) -> Self { // Small filter for visited tracking let visited = BloomFilter::with_params(256 * 8, 5).expect("valid params"); @@ -44,6 +48,7 @@ impl LookupRequest { origin, origin_coords, ttl, + min_mtu, visited, } } @@ -54,10 +59,11 @@ impl LookupRequest { origin: NodeAddr, origin_coords: TreeCoordinate, ttl: u8, + min_mtu: u16, ) -> Self { use rand::Rng; let request_id = rand::thread_rng().r#gen(); - Self::new(request_id, target, origin, origin_coords, ttl) + Self::new(request_id, target, origin, origin_coords, ttl, min_mtu) } /// Decrement TTL and add self to visited. @@ -84,18 +90,19 @@ impl LookupRequest { /// Encode as wire format (includes msg_type byte). /// - /// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1]` + /// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1][min_mtu:2]` /// `[origin_coords_cnt:2][origin_coords:16×n]` /// `[visited_hash_cnt:1][visited_bits:256]` pub fn encode(&self) -> Vec { let visited_bytes = self.visited.as_bytes(); - let mut buf = Vec::with_capacity(44 + self.origin_coords.depth() * 16 + 1 + visited_bytes.len()); + let mut buf = Vec::with_capacity(46 + self.origin_coords.depth() * 16 + 1 + visited_bytes.len()); buf.push(0x30); // msg_type buf.extend_from_slice(&self.request_id.to_le_bytes()); buf.extend_from_slice(self.target.as_bytes()); buf.extend_from_slice(self.origin.as_bytes()); buf.push(self.ttl); + buf.extend_from_slice(&self.min_mtu.to_le_bytes()); encode_coords(&self.origin_coords, &mut buf); buf.push(self.visited.hash_count()); buf.extend_from_slice(visited_bytes); @@ -105,11 +112,11 @@ impl LookupRequest { /// Decode from wire format (after msg_type byte has been consumed). pub fn decode(payload: &[u8]) -> Result { - // Minimum: request_id(8) + target(16) + origin(16) + ttl(1) - // + coords_count(2) + hash_count(1) = 44 bytes - if payload.len() < 44 { + // Minimum: request_id(8) + target(16) + origin(16) + ttl(1) + min_mtu(2) + // + coords_count(2) + hash_count(1) = 46 bytes + if payload.len() < 46 { return Err(ProtocolError::MessageTooShort { - expected: 44, + expected: 46, got: payload.len(), }); } @@ -136,6 +143,13 @@ impl LookupRequest { let ttl = payload[pos]; pos += 1; + let min_mtu = u16::from_le_bytes( + payload[pos..pos + 2] + .try_into() + .map_err(|_| ProtocolError::Malformed("bad min_mtu".into()))?, + ); + pos += 2; + let (origin_coords, consumed) = decode_coords(&payload[pos..])?; pos += consumed; @@ -162,6 +176,7 @@ impl LookupRequest { origin, origin_coords, ttl, + min_mtu, visited, }) } @@ -176,6 +191,12 @@ pub struct LookupResponse { pub request_id: u64, /// The target node. pub target: NodeAddr, + /// Minimum transport MTU along the response path. + /// + /// Initialized to `u16::MAX` by the target. Each transit node applies + /// `path_mtu = path_mtu.min(outgoing_link_mtu)` when forwarding. + /// NOT included in the proof signature (transit annotation). + pub path_mtu: u16, /// Target's coordinates in the tree. pub target_coords: TreeCoordinate, /// Proof that target authorized this response (signature over request). @@ -184,6 +205,9 @@ pub struct LookupResponse { impl LookupResponse { /// Create a new lookup response. + /// + /// `path_mtu` is initialized to `u16::MAX` by the target; transit + /// nodes reduce it as they forward. pub fn new( request_id: u64, target: NodeAddr, @@ -193,6 +217,7 @@ impl LookupResponse { Self { request_id, target, + path_mtu: u16::MAX, target_coords, proof, } @@ -212,13 +237,14 @@ impl LookupResponse { /// Encode as wire format (includes msg_type byte). /// - /// Format: `[0x31][request_id:8][target:16][target_coords_cnt:2][target_coords:16×n][proof:64]` + /// Format: `[0x31][request_id:8][target:16][path_mtu:2][target_coords_cnt:2][target_coords:16×n][proof:64]` pub fn encode(&self) -> Vec { - let mut buf = Vec::with_capacity(91 + self.target_coords.depth() * 16); + let mut buf = Vec::with_capacity(93 + self.target_coords.depth() * 16); buf.push(0x31); // msg_type buf.extend_from_slice(&self.request_id.to_le_bytes()); buf.extend_from_slice(self.target.as_bytes()); + buf.extend_from_slice(&self.path_mtu.to_le_bytes()); encode_coords(&self.target_coords, &mut buf); buf.extend_from_slice(self.proof.as_ref()); @@ -227,10 +253,10 @@ impl LookupResponse { /// Decode from wire format (after msg_type byte has been consumed). pub fn decode(payload: &[u8]) -> Result { - // Minimum: request_id(8) + target(16) + coords_count(2) + proof(64) = 90 - if payload.len() < 90 { + // Minimum: request_id(8) + target(16) + path_mtu(2) + coords_count(2) + proof(64) = 92 + if payload.len() < 92 { return Err(ProtocolError::MessageTooShort { - expected: 90, + expected: 92, got: payload.len(), }); } @@ -249,6 +275,13 @@ impl LookupResponse { let target = NodeAddr::from_bytes(target_bytes); pos += 16; + let path_mtu = u16::from_le_bytes( + payload[pos..pos + 2] + .try_into() + .map_err(|_| ProtocolError::Malformed("bad path_mtu".into()))?, + ); + pos += 2; + let (target_coords, consumed) = decode_coords(&payload[pos..])?; pos += consumed; @@ -264,6 +297,7 @@ impl LookupResponse { Ok(Self { request_id, target, + path_mtu, target_coords, proof, }) @@ -291,7 +325,7 @@ mod tests { let coords = make_coords(&[2, 0]); let forwarder = make_node_addr(3); - let mut request = LookupRequest::new(123, target, origin, coords, 5); + let mut request = LookupRequest::new(123, target, origin, coords, 5, 0); assert!(request.can_forward()); assert!(!request.was_visited(&forwarder)); @@ -308,7 +342,7 @@ mod tests { let origin = make_node_addr(2); let coords = make_coords(&[2, 0]); - let mut request = LookupRequest::new(123, target, origin, coords, 1); + let mut request = LookupRequest::new(123, target, origin, coords, 1, 0); assert!(request.forward(&make_node_addr(3))); assert!(!request.can_forward()); @@ -321,8 +355,8 @@ mod tests { let origin = make_node_addr(2); let coords = make_coords(&[2, 0]); - let req1 = LookupRequest::generate(target, origin, coords.clone(), 5); - let req2 = LookupRequest::generate(target, origin, coords, 5); + let req1 = LookupRequest::generate(target, origin, coords.clone(), 5, 0); + let req2 = LookupRequest::generate(target, origin, coords, 5, 0); // Random IDs should differ assert_ne!(req1.request_id, req2.request_id); @@ -350,7 +384,7 @@ mod tests { let origin = make_node_addr(20); let coords = make_coords(&[20, 0]); - let mut request = LookupRequest::new(12345, target, origin, coords.clone(), 8); + let mut request = LookupRequest::new(12345, target, origin, coords.clone(), 8, 1386); request.forward(&make_node_addr(30)); let encoded = request.encode(); @@ -361,13 +395,28 @@ mod tests { assert_eq!(decoded.target, target); assert_eq!(decoded.origin, origin); assert_eq!(decoded.ttl, 7); // decremented by forward() + assert_eq!(decoded.min_mtu, 1386); assert!(decoded.was_visited(&make_node_addr(30))); } #[test] fn test_lookup_request_decode_too_short() { assert!(LookupRequest::decode(&[]).is_err()); - assert!(LookupRequest::decode(&[0u8; 40]).is_err()); + assert!(LookupRequest::decode(&[0u8; 42]).is_err()); + } + + #[test] + fn test_lookup_request_min_mtu_boundary_values() { + let target = make_node_addr(10); + let origin = make_node_addr(20); + let coords = make_coords(&[20, 0]); + + for mtu_val in [0u16, 1386, u16::MAX] { + let request = LookupRequest::new(100, target, origin, coords.clone(), 5, mtu_val); + let encoded = request.encode(); + let decoded = LookupRequest::decode(&encoded[1..]).unwrap(); + assert_eq!(decoded.min_mtu, mtu_val); + } } #[test] @@ -387,15 +436,56 @@ mod tests { let response = LookupResponse::new(999, target, coords.clone(), sig); + // Default path_mtu should be u16::MAX + assert_eq!(response.path_mtu, u16::MAX); + let encoded = response.encode(); assert_eq!(encoded[0], 0x31); let decoded = LookupResponse::decode(&encoded[1..]).unwrap(); assert_eq!(decoded.request_id, 999); assert_eq!(decoded.target, target); + assert_eq!(decoded.path_mtu, u16::MAX); assert_eq!(decoded.proof, sig); } + #[test] + fn test_lookup_response_path_mtu_roundtrip() { + use secp256k1::Secp256k1; + + let target = make_node_addr(42); + let coords = make_coords(&[42, 1, 0]); + + let secp = Secp256k1::new(); + let keypair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng()); + let proof_data = LookupResponse::proof_bytes(999, &target, &coords); + use sha2::Digest; + let digest: [u8; 32] = sha2::Sha256::digest(&proof_data).into(); + let sig = secp.sign_schnorr(&digest, &keypair); + + for mtu_val in [0u16, 1280, 1386, 9000, u16::MAX] { + let mut response = LookupResponse::new(999, target, coords.clone(), sig); + response.path_mtu = mtu_val; + + let encoded = response.encode(); + let decoded = LookupResponse::decode(&encoded[1..]).unwrap(); + assert_eq!(decoded.path_mtu, mtu_val); + } + } + + #[test] + fn test_lookup_response_path_mtu_not_in_proof_bytes() { + // Verify that proof_bytes does NOT include path_mtu + let target = make_node_addr(42); + let coords = make_coords(&[42, 1, 0]); + + let bytes = LookupResponse::proof_bytes(12345, &target, &coords); + + // proof_bytes format: request_id(8) + target(16) + coords_encoding(2 + 3*16) = 74 + // No path_mtu(2) in here + assert_eq!(bytes.len(), 74); + } + #[test] fn test_lookup_response_decode_too_short() { assert!(LookupResponse::decode(&[]).is_err());