Discovery wire format cleanup

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.
This commit is contained in:
Johnathan Corgan
2026-04-11 08:16:01 +00:00
parent 4aded9a238
commit e06015dd43
3 changed files with 245 additions and 74 deletions
+28 -2
View File
@@ -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<NodeAddr> = 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<NodeAddr> = 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;
+7 -16
View File
@@ -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);
+210 -56
View File
@@ -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<TlvEntry>,
}
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<u8>) -> 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<u8> {
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<Self, ProtocolError> {
// 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<TlvEntry>,
}
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<u8>) -> 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<u8> {
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]);
}
}