From e06015dd43729eccf49bcda1f07a1817c6ae971e Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sat, 4 Apr 2026 20:36:48 +0000 Subject: [PATCH] Discovery wire format cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop origin_coords from LookupRequest — unused since reverse-path routing became primary. Saves 2+16*depth bytes per request. Wire up min_mtu: populated from TUN MTU config (default 1280) at origination. Transit nodes skip peers whose link MTU is below the request's min_mtu requirement. path_mtu on LookupResponse was already wired (transit min() applied). Add TLV extension to LookupRequest (after min_mtu) and LookupResponse (after proof). Uses same TlvEntry format as negotiation. Transit nodes forward TLV bytes verbatim. --- src/node/handlers/discovery.rs | 30 +++- src/node/tests/discovery.rs | 23 +-- src/protocol/discovery.rs | 266 ++++++++++++++++++++++++++------- 3 files changed, 245 insertions(+), 74 deletions(-) diff --git a/src/node/handlers/discovery.rs b/src/node/handlers/discovery.rs index 4e5ef57..65e9687 100644 --- a/src/node/handlers/discovery.rs +++ b/src/node/handlers/discovery.rs @@ -326,6 +326,7 @@ impl Node { } // Collect full tree peers whose bloom filter contains the target + let min_mtu = request.min_mtu; let forward_to: Vec = self .peers .iter() @@ -333,6 +334,7 @@ impl Node { peer.peer_profile() == crate::protocol::NodeProfile::Full && self.is_tree_peer(addr) && peer.may_reach(&request.target) + && self.peer_meets_mtu(peer, min_mtu) }) .map(|(addr, _)| *addr) .collect(); @@ -346,6 +348,7 @@ impl Node { peer.peer_profile() == crate::protocol::NodeProfile::Full && !self.is_tree_peer(addr) && peer.may_reach(&request.target) + && self.peer_meets_mtu(peer, min_mtu) }) .map(|(addr, _)| *addr) .collect(); @@ -404,8 +407,8 @@ impl Node { self.stats_mut().discovery.req_initiated += 1; let origin = *self.node_addr(); - let origin_coords = self.tree_state().my_coords().clone(); - let request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0); + let min_mtu = self.config.tun.mtu(); + let request = LookupRequest::generate(*target, origin, ttl, min_mtu); // Send only to full tree peers whose bloom filter contains the target let peer_addrs: Vec = self @@ -415,6 +418,7 @@ impl Node { peer.peer_profile() == crate::protocol::NodeProfile::Full && self.is_tree_peer(addr) && peer.may_reach(target) + && self.peer_meets_mtu(peer, request.min_mtu) }) .map(|(addr, _)| *addr) .collect(); @@ -593,6 +597,28 @@ impl Node { } } + /// Check if a peer's outgoing link MTU meets the min_mtu requirement. + /// + /// Returns true if min_mtu is 0 (no requirement) or if the peer's + /// transport link MTU is >= min_mtu. + fn peer_meets_mtu(&self, peer: &crate::peer::ActivePeer, min_mtu: u16) -> bool { + if min_mtu == 0 { + return true; + } + if let Some(tid) = peer.transport_id() + && let Some(transport) = self.transports.get(&tid) + { + let link_mtu = peer + .current_addr() + .map(|addr| transport.link_mtu(addr)) + .unwrap_or_else(|| transport.mtu()); + link_mtu >= min_mtu + } else { + // No transport info available — don't prune + true + } + } + /// Remove expired entries from the recent_requests cache. fn purge_expired_requests(&mut self, current_time_ms: u64) { let expiry_ms = self.config.node.discovery.recent_expiry_secs * 1000; diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index 7f8e80f..6f81dd3 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -32,9 +32,8 @@ async fn test_request_dedup() { 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, 0); + let request = LookupRequest::new(999, target, origin, 5, 0); let payload = &request.encode()[1..]; // skip msg_type byte // First request: accepted @@ -52,10 +51,9 @@ async fn test_request_target_is_self() { 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, 0); + let request = LookupRequest::new(777, my_addr, origin, 5, 0); let payload = &request.encode()[1..]; // Should succeed without panic (response send will fail silently @@ -70,9 +68,8 @@ async fn test_request_ttl_zero_not_forwarded() { 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, 0); + let request = LookupRequest::new(666, target, origin, 0, 0); let payload = &request.encode()[1..]; node.handle_lookup_request(&from, payload).await; @@ -331,8 +328,7 @@ async fn test_recent_request_expiry() { // 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, 0); + let request = LookupRequest::new(789, target, origin, 3, 0); let payload = &request.encode()[1..]; node.handle_lookup_request(&make_node_addr(0xAA), payload) .await; @@ -357,10 +353,8 @@ async fn test_request_forwarding_two_node() { let node0_addr = *nodes[0].node.node_addr(); let target = *nodes[1].node.node_addr(); // target node1 (in bloom filters) - 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, 0); + let request = LookupRequest::new(42, target, node0_addr, 5, 0); let payload = &request.encode()[1..]; // Handle on node0 as if we received it from outside @@ -481,10 +475,8 @@ async fn test_request_dedup_convergent_paths() { let node0_addr = *nodes[0].node.node_addr(); let target = *nodes[2].node.node_addr(); // target node2 (in bloom filters) - 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, 0); + let request = LookupRequest::new(300, target, node0_addr, 5, 0); let payload = &request.encode()[1..]; // Node0 handles the request (forwards to peers whose bloom filter @@ -841,9 +833,8 @@ 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 request = LookupRequest::new(100, target, origin, 5, 1386); let encoded = request.encode(); let decoded = LookupRequest::decode(&encoded[1..]).unwrap(); assert_eq!(decoded.min_mtu, 1386); diff --git a/src/protocol/discovery.rs b/src/protocol/discovery.rs index eb860ac..d01aec0 100644 --- a/src/protocol/discovery.rs +++ b/src/protocol/discovery.rs @@ -2,6 +2,7 @@ use crate::NodeAddr; use crate::protocol::error::ProtocolError; +use crate::protocol::negotiation::TlvEntry; use crate::protocol::session::{decode_coords, encode_coords}; use crate::tree::TreeCoordinate; use secp256k1::schnorr::Signature; @@ -19,13 +20,13 @@ pub struct LookupRequest { pub target: NodeAddr, /// Who's asking (for response routing). pub origin: NodeAddr, - /// Origin's coordinates (for return path). - 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, + /// Optional TLV extension entries. + pub tlv_entries: Vec, } impl LookupRequest { @@ -34,7 +35,6 @@ impl LookupRequest { request_id: u64, target: NodeAddr, origin: NodeAddr, - origin_coords: TreeCoordinate, ttl: u8, min_mtu: u16, ) -> Self { @@ -42,9 +42,9 @@ impl LookupRequest { request_id, target, origin, - origin_coords, ttl, min_mtu, + tlv_entries: Vec::new(), } } @@ -52,13 +52,18 @@ impl LookupRequest { pub fn generate( target: NodeAddr, origin: NodeAddr, - origin_coords: TreeCoordinate, ttl: u8, min_mtu: u16, ) -> Self { use rand::RngExt; let request_id = rand::rng().random(); - Self::new(request_id, target, origin, origin_coords, ttl, min_mtu) + Self::new(request_id, target, origin, ttl, min_mtu) + } + + /// Add a TLV entry. + pub fn with_tlv(mut self, field_num: u16, value: Vec) -> Self { + self.tlv_entries.push(TlvEntry { field_num, value }); + self } /// Decrement TTL for forwarding. @@ -79,10 +84,9 @@ impl LookupRequest { /// Encode as wire format (includes msg_type byte). /// - /// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1][min_mtu:2]` - /// `[origin_coords_cnt:2][origin_coords:16×n]` + /// Format: `[0x30][request_id:8][target:16][origin:16][ttl:1][min_mtu:2][tlv entries...]` pub fn encode(&self) -> Vec { - let mut buf = Vec::with_capacity(46 + self.origin_coords.depth() * 16); + let mut buf = Vec::with_capacity(44); buf.push(0x30); // msg_type buf.extend_from_slice(&self.request_id.to_le_bytes()); @@ -90,18 +94,23 @@ impl LookupRequest { 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); + + for entry in &self.tlv_entries { + buf.extend_from_slice(&entry.field_num.to_le_bytes()); + let len = entry.value.len() as u16; + buf.extend_from_slice(&len.to_le_bytes()); + buf.extend_from_slice(&entry.value); + } buf } /// 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) + min_mtu(2) - // + coords_count(2) = 45 bytes - if payload.len() < 45 { + // Minimum: request_id(8) + target(16) + origin(16) + ttl(1) + min_mtu(2) = 43 bytes + if payload.len() < 43 { return Err(ProtocolError::MessageTooShort { - expected: 45, + expected: 43, got: payload.len(), }); } @@ -135,22 +144,46 @@ impl LookupRequest { ); pos += 2; - let (origin_coords, _consumed) = decode_coords(&payload[pos..])?; + // Parse TLV entries from remaining bytes + let mut tlv_entries = Vec::new(); + while pos < payload.len() { + if pos + 4 > payload.len() { + return Err(ProtocolError::Malformed( + "truncated TLV header in LookupRequest".to_string(), + )); + } + let field_num = + u16::from_le_bytes(payload[pos..pos + 2].try_into().unwrap()); + let length = + u16::from_le_bytes(payload[pos + 2..pos + 4].try_into().unwrap()) + as usize; + pos += 4; + if pos + length > payload.len() { + return Err(ProtocolError::Malformed(format!( + "TLV field {field_num}: declared length {length} exceeds remaining data {}", + payload.len() - pos + ))); + } + let value = payload[pos..pos + length].to_vec(); + pos += length; + tlv_entries.push(TlvEntry { field_num, value }); + } Ok(Self { request_id, target, origin, - origin_coords, ttl, min_mtu, + tlv_entries, }) } } /// Response to a lookup request with target's coordinates. /// -/// Routed back to the origin using the origin_coords from the request. +/// Routed back to the origin using reverse-path routing or tree +/// routing toward the origin's NodeAddr. #[derive(Clone, Debug)] pub struct LookupResponse { /// Echoed request identifier. @@ -167,6 +200,8 @@ pub struct LookupResponse { pub target_coords: TreeCoordinate, /// Proof that target authorized this response (signature over request). pub proof: Signature, + /// Optional TLV extension entries. + pub tlv_entries: Vec, } impl LookupResponse { @@ -186,9 +221,16 @@ impl LookupResponse { path_mtu: u16::MAX, target_coords, proof, + tlv_entries: Vec::new(), } } + /// Add a TLV entry. + pub fn with_tlv(mut self, field_num: u16, value: Vec) -> Self { + self.tlv_entries.push(TlvEntry { field_num, value }); + self + } + /// Get the bytes that should be signed as proof. /// /// Format: request_id (8) || target (16) || coords_encoding (2 + 16×n) @@ -207,7 +249,7 @@ impl LookupResponse { /// Encode as wire format (includes msg_type byte). /// - /// Format: `[0x31][request_id:8][target:16][path_mtu:2][target_coords_cnt:2][target_coords:16×n][proof:64]` + /// Format: `[0x31][request_id:8][target:16][path_mtu:2][coords_cnt:2][coords:16×n][proof:64][tlv entries...]` pub fn encode(&self) -> Vec { let mut buf = Vec::with_capacity(93 + self.target_coords.depth() * 16); @@ -218,6 +260,13 @@ impl LookupResponse { encode_coords(&self.target_coords, &mut buf); buf.extend_from_slice(self.proof.as_ref()); + for entry in &self.tlv_entries { + buf.extend_from_slice(&entry.field_num.to_le_bytes()); + let len = entry.value.len() as u16; + buf.extend_from_slice(&len.to_le_bytes()); + buf.extend_from_slice(&entry.value); + } + buf } @@ -263,6 +312,32 @@ impl LookupResponse { } let proof = Signature::from_slice(&payload[pos..pos + 64]) .map_err(|_| ProtocolError::Malformed("bad proof signature".into()))?; + pos += 64; + + // Parse TLV entries from remaining bytes after proof + let mut tlv_entries = Vec::new(); + while pos < payload.len() { + if pos + 4 > payload.len() { + return Err(ProtocolError::Malformed( + "truncated TLV header in LookupResponse".to_string(), + )); + } + let field_num = + u16::from_le_bytes(payload[pos..pos + 2].try_into().unwrap()); + let length = + u16::from_le_bytes(payload[pos + 2..pos + 4].try_into().unwrap()) + as usize; + pos += 4; + if pos + length > payload.len() { + return Err(ProtocolError::Malformed(format!( + "TLV field {field_num}: declared length {length} exceeds remaining data {}", + payload.len() - pos + ))); + } + let value = payload[pos..pos + length].to_vec(); + pos += length; + tlv_entries.push(TlvEntry { field_num, value }); + } Ok(Self { request_id, @@ -270,6 +345,7 @@ impl LookupResponse { path_mtu, target_coords, proof, + tlv_entries, }) } } @@ -288,13 +364,28 @@ mod tests { TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap() } + fn make_test_sig() -> Signature { + use secp256k1::Secp256k1; + let secp = Secp256k1::new(); + let mut secret_bytes = [0u8; 32]; + rand::Rng::fill_bytes(&mut rand::rng(), &mut secret_bytes); + let secret_key = secp256k1::SecretKey::from_slice(&secret_bytes) + .expect("32 random bytes is a valid secret key"); + let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); + let target = make_node_addr(42); + let coords = make_coords(&[42, 1, 0]); + let proof_data = LookupResponse::proof_bytes(999, &target, &coords); + use sha2::Digest; + let digest: [u8; 32] = sha2::Sha256::digest(&proof_data).into(); + secp.sign_schnorr(&digest, &keypair) + } + #[test] fn test_lookup_request_forward() { let target = make_node_addr(1); let origin = make_node_addr(2); - let coords = make_coords(&[2, 0]); - let mut request = LookupRequest::new(123, target, origin, coords, 5, 0); + let mut request = LookupRequest::new(123, target, origin, 5, 0); assert!(request.can_forward()); assert!(request.forward()); @@ -305,9 +396,8 @@ mod tests { fn test_lookup_request_ttl_exhausted() { let target = make_node_addr(1); let origin = make_node_addr(2); - let coords = make_coords(&[2, 0]); - let mut request = LookupRequest::new(123, target, origin, coords, 1, 0); + let mut request = LookupRequest::new(123, target, origin, 1, 0); assert!(request.forward()); assert!(!request.can_forward()); @@ -318,10 +408,9 @@ mod tests { fn test_lookup_request_generate() { let target = make_node_addr(1); let origin = make_node_addr(2); - let coords = make_coords(&[2, 0]); - let req1 = LookupRequest::generate(target, origin, coords.clone(), 5, 0); - let req2 = LookupRequest::generate(target, origin, coords, 5, 0); + let req1 = LookupRequest::generate(target, origin, 5, 0); + let req2 = LookupRequest::generate(target, origin, 5, 0); // Random IDs should differ assert_ne!(req1.request_id, req2.request_id); @@ -347,9 +436,8 @@ mod tests { fn test_lookup_request_encode_decode_roundtrip() { let target = make_node_addr(10); let origin = make_node_addr(20); - let coords = make_coords(&[20, 0]); - let mut request = LookupRequest::new(12345, target, origin, coords, 8, 1386); + let mut request = LookupRequest::new(12345, target, origin, 8, 1386); request.forward(); let encoded = request.encode(); @@ -361,6 +449,7 @@ mod tests { assert_eq!(decoded.origin, origin); assert_eq!(decoded.ttl, 7); // decremented by forward() assert_eq!(decoded.min_mtu, 1386); + assert!(decoded.tlv_entries.is_empty()); } #[test] @@ -373,10 +462,9 @@ mod tests { 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 request = LookupRequest::new(100, target, origin, 5, mtu_val); let encoded = request.encode(); let decoded = LookupRequest::decode(&encoded[1..]).unwrap(); assert_eq!(decoded.min_mtu, mtu_val); @@ -384,23 +472,54 @@ mod tests { } #[test] - fn test_lookup_response_encode_decode_roundtrip() { - use secp256k1::Secp256k1; + fn test_lookup_request_with_tlv_roundtrip() { + let target = make_node_addr(10); + let origin = make_node_addr(20); + let request = LookupRequest::new(555, target, origin, 5, 1280) + .with_tlv(1, vec![0xAA, 0xBB]) + .with_tlv(256, vec![0x01, 0x02, 0x03, 0x04]); + + let encoded = request.encode(); + let decoded = LookupRequest::decode(&encoded[1..]).unwrap(); + + assert_eq!(decoded.request_id, 555); + assert_eq!(decoded.min_mtu, 1280); + assert_eq!(decoded.tlv_entries.len(), 2); + assert_eq!(decoded.tlv_entries[0].field_num, 1); + assert_eq!(decoded.tlv_entries[0].value, vec![0xAA, 0xBB]); + assert_eq!(decoded.tlv_entries[1].field_num, 256); + assert_eq!(decoded.tlv_entries[1].value, vec![0x01, 0x02, 0x03, 0x04]); + } + + #[test] + fn test_lookup_request_tlv_forward_compat() { + // Unknown field_nums should be preserved through decode→encode + let target = make_node_addr(10); + let origin = make_node_addr(20); + + let request = LookupRequest::new(777, target, origin, 5, 0) + .with_tlv(9999, vec![0xFF, 0xFE, 0xFD]); + + let encoded = request.encode(); + let mut decoded = LookupRequest::decode(&encoded[1..]).unwrap(); + + // Simulate transit: forward then re-encode + decoded.forward(); + let re_encoded = decoded.encode(); + let final_decoded = LookupRequest::decode(&re_encoded[1..]).unwrap(); + + assert_eq!(final_decoded.ttl, 4); + assert_eq!(final_decoded.tlv_entries.len(), 1); + assert_eq!(final_decoded.tlv_entries[0].field_num, 9999); + assert_eq!(final_decoded.tlv_entries[0].value, vec![0xFF, 0xFE, 0xFD]); + } + + #[test] + fn test_lookup_response_encode_decode_roundtrip() { let target = make_node_addr(42); let coords = make_coords(&[42, 1, 0]); - - // Create a dummy signature for testing - let secp = Secp256k1::new(); - let mut secret_bytes = [0u8; 32]; - rand::Rng::fill_bytes(&mut rand::rng(), &mut secret_bytes); - let secret_key = secp256k1::SecretKey::from_slice(&secret_bytes) - .expect("32 random bytes is a valid secret key"); - let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); - 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); + let sig = make_test_sig(); let response = LookupResponse::new(999, target, coords, sig); @@ -415,25 +534,14 @@ mod tests { assert_eq!(decoded.target, target); assert_eq!(decoded.path_mtu, u16::MAX); assert_eq!(decoded.proof, sig); + assert!(decoded.tlv_entries.is_empty()); } #[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 mut secret_bytes = [0u8; 32]; - rand::Rng::fill_bytes(&mut rand::rng(), &mut secret_bytes); - let secret_key = secp256k1::SecretKey::from_slice(&secret_bytes) - .expect("32 random bytes is a valid secret key"); - let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); - 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); + let sig = make_test_sig(); for mtu_val in [0u16, 1280, 1386, 9000, u16::MAX] { let mut response = LookupResponse::new(999, target, coords.clone(), sig); @@ -463,4 +571,50 @@ mod tests { assert!(LookupResponse::decode(&[]).is_err()); assert!(LookupResponse::decode(&[0u8; 50]).is_err()); } + + #[test] + fn test_lookup_response_with_tlv_roundtrip() { + let target = make_node_addr(42); + let coords = make_coords(&[42, 1, 0]); + let sig = make_test_sig(); + + let response = LookupResponse::new(999, target, coords, sig) + .with_tlv(1, vec![0xAA, 0xBB]) + .with_tlv(500, vec![0x01, 0x02, 0x03]); + + let encoded = response.encode(); + let decoded = LookupResponse::decode(&encoded[1..]).unwrap(); + + assert_eq!(decoded.request_id, 999); + assert_eq!(decoded.proof, sig); + assert_eq!(decoded.tlv_entries.len(), 2); + assert_eq!(decoded.tlv_entries[0].field_num, 1); + assert_eq!(decoded.tlv_entries[0].value, vec![0xAA, 0xBB]); + assert_eq!(decoded.tlv_entries[1].field_num, 500); + assert_eq!(decoded.tlv_entries[1].value, vec![0x01, 0x02, 0x03]); + } + + #[test] + fn test_lookup_response_tlv_forward_compat() { + // Unknown field_nums preserved through decode→modify path_mtu→encode + let target = make_node_addr(42); + let coords = make_coords(&[42, 1, 0]); + let sig = make_test_sig(); + + let response = LookupResponse::new(999, target, coords, sig) + .with_tlv(9999, vec![0xFF, 0xFE, 0xFD]); + + let encoded = response.encode(); + let mut decoded = LookupResponse::decode(&encoded[1..]).unwrap(); + + // Simulate transit: modify path_mtu then re-encode + decoded.path_mtu = 1280; + let re_encoded = decoded.encode(); + let final_decoded = LookupResponse::decode(&re_encoded[1..]).unwrap(); + + assert_eq!(final_decoded.path_mtu, 1280); + assert_eq!(final_decoded.tlv_entries.len(), 1); + assert_eq!(final_decoded.tlv_entries[0].field_num, 9999); + assert_eq!(final_decoded.tlv_entries[0].value, vec![0xFF, 0xFE, 0xFD]); + } }