Skip to content

Commit da79d4a

Browse files
committed
TLV PoC
1 parent 7b368f2 commit da79d4a

File tree

3 files changed

+175
-6
lines changed

3 files changed

+175
-6
lines changed

gossip/src/contact_info.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
pub use solana_client::connection_cache::Protocol;
22
use {
3-
crate::{crds_data::MAX_WALLCLOCK, legacy_contact_info::LegacyContactInfo},
3+
crate::{
4+
crds_data::MAX_WALLCLOCK,
5+
define_tlv_enum,
6+
legacy_contact_info::LegacyContactInfo,
7+
tlv::{self, TlvRecord},
8+
},
49
assert_matches::{assert_matches, debug_assert_matches},
510
serde::{Deserialize, Deserializer, Serialize},
611
solana_pubkey::Pubkey,
@@ -113,8 +118,10 @@ struct SocketEntry {
113118
offset: u16, // Port offset with respect to the previous entry.
114119
}
115120

116-
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
117-
enum Extension {}
121+
// The Extension enum for optional features in gossip
122+
define_tlv_enum! (pub(crate) enum Extension {
123+
1=>SupportSenderSignatures(u8),
124+
});
118125

119126
// As part of deserialization, self.addrs and self.sockets should be cross
120127
// verified and self.cache needs to be populated. This type serves as a
@@ -132,8 +139,9 @@ struct ContactInfoLite {
132139
addrs: Vec<IpAddr>,
133140
#[serde(with = "short_vec")]
134141
sockets: Vec<SocketEntry>,
142+
#[allow(dead_code)]
135143
#[serde(with = "short_vec")]
136-
extensions: Vec<Extension>,
144+
extensions: Vec<TlvRecord>,
137145
}
138146

139147
macro_rules! get_socket {
@@ -220,7 +228,7 @@ impl ContactInfo {
220228
version: solana_version::Version::default(),
221229
addrs: Vec::<IpAddr>::default(),
222230
sockets: Vec::<SocketEntry>::default(),
223-
extensions: Vec::<Extension>::default(),
231+
extensions: Vec::default(),
224232
cache: EMPTY_SOCKET_ADDR_CACHE,
225233
}
226234
}
@@ -549,7 +557,7 @@ impl TryFrom<ContactInfoLite> for ContactInfo {
549557
version,
550558
addrs,
551559
sockets,
552-
extensions,
560+
extensions: tlv::parse(&extensions),
553561
cache: EMPTY_SOCKET_ADDR_CACHE,
554562
};
555563
// Populate node.cache.
@@ -1180,4 +1188,14 @@ mod tests {
11801188
assert_eq!(other.overrides(&node), Some(true));
11811189
}
11821190
}
1191+
1192+
#[test]
1193+
fn test_extensions() {
1194+
let mut ci = ContactInfo::new_localhost(&Pubkey::new_unique(), 42);
1195+
ci.extensions.push(Extension::SupportSenderSignatures(42));
1196+
let bytes = bincode::serialize(&ci).unwrap();
1197+
let cil: ContactInfoLite = bincode::deserialize(&bytes).unwrap();
1198+
let ci2 = ContactInfo::try_from(cil).unwrap();
1199+
assert_eq!(ci, ci2);
1200+
}
11831201
}

gossip/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub mod epoch_specs;
2222
pub mod gossip_error;
2323
pub mod gossip_service;
2424
#[macro_use]
25+
mod tlv;
26+
#[macro_use]
2527
mod legacy_contact_info;
2628
pub mod ping_pong;
2729
mod protocol;

gossip/src/tlv.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use {
2+
serde::{Deserialize, Serialize},
3+
solana_short_vec as short_vec,
4+
};
5+
6+
/// Type-Length-Value encoding wrapper for bincode
7+
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
8+
pub(crate) struct TlvRecord {
9+
// type
10+
pub(crate) typ: u8,
11+
// length and serialized bytes of the value
12+
#[serde(with = "short_vec")]
13+
pub(crate) bytes: Vec<u8>,
14+
}
15+
16+
/// Macro that provides a quick and easy way to define TLV compatible enums
17+
#[macro_export]
18+
macro_rules! define_tlv_enum {
19+
(
20+
$vis:vis enum $enum_name:ident {
21+
$($typ:literal => $variant:ident($inner:ty)),* $(,)?
22+
}
23+
) => {
24+
// define the enum itself
25+
#[derive(Debug, Clone, Eq, PartialEq)]
26+
$vis enum $enum_name {
27+
$(
28+
$variant($inner),
29+
)*
30+
}
31+
32+
// Serialize enum by first converting into TlvRecord, and then serializing that
33+
impl serde::Serialize for $enum_name {
34+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35+
where
36+
S: serde::Serializer,
37+
{
38+
let tlv_rec = TlvRecord::try_from(self).map_err(|e| serde::ser::Error::custom(e))?;
39+
tlv_rec.serialize(serializer)
40+
}
41+
}
42+
43+
// define conversion from TLV wire format
44+
impl TryFrom<&TlvRecord> for $enum_name {
45+
type Error = bincode::Error;
46+
fn try_from(value: &TlvRecord) -> Result<Self, Self::Error> {
47+
use serde::ser::Error;
48+
match value.typ {
49+
$(
50+
$typ => Ok(Self::$variant(bincode::deserialize::<$inner>(&value.bytes)?)),
51+
)*
52+
_ => Err(bincode::Error::custom("Invalid type")),
53+
}
54+
}
55+
}
56+
// define conversion into TLV wire format
57+
impl TryFrom<&$enum_name> for TlvRecord {
58+
type Error = bincode::Error;
59+
fn try_from(value: &$enum_name) -> Result<Self, Self::Error> {
60+
match value {
61+
$(
62+
$enum_name::$variant(inner) => Ok(TlvRecord {
63+
typ: $typ,
64+
bytes: bincode::serialize(inner)?,
65+
}),
66+
)*
67+
}
68+
}
69+
}
70+
};
71+
}
72+
73+
/// Parses a slice of serialized TLV records into a provided type. Unsupported
74+
/// TLV records are ignored.
75+
pub(crate) fn parse<'a, T: TryFrom<&'a TlvRecord>>(entries: &'a [TlvRecord]) -> Vec<T> {
76+
entries.iter().filter_map(|v| T::try_from(v).ok()).collect()
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use crate::{define_tlv_enum, tlv::TlvRecord};
82+
83+
define_tlv_enum! (pub(crate) enum ExtensionNew {
84+
1=>Test(u64),
85+
2=>LegacyString(String),
86+
3=>NewString(String),
87+
});
88+
89+
define_tlv_enum! ( pub(crate) enum ExtensionLegacy {
90+
1=>Test(u64),
91+
2=>LegacyString(String),
92+
});
93+
94+
/// Test that TLV encoded data is backwards-compatible,
95+
/// i.e. that new TLV data can be decoded by a new
96+
/// receiver where possible, and skipped otherwise
97+
#[test]
98+
fn test_tlv_backwards_compat() {
99+
let new_tlv_data = vec![
100+
ExtensionNew::Test(42),
101+
ExtensionNew::NewString(String::from("bla")),
102+
];
103+
104+
let new_bytes = bincode::serialize(&new_tlv_data).unwrap();
105+
let tlv_vec: Vec<TlvRecord> = bincode::deserialize(&new_bytes).unwrap();
106+
// check that both TLV are encoded correctly
107+
let new: Vec<ExtensionNew> = crate::tlv::parse(&tlv_vec);
108+
assert!(matches!(new[0], ExtensionNew::Test(42)));
109+
if let ExtensionNew::NewString(s) = &new[1] {
110+
assert_eq!(s, "bla");
111+
} else {
112+
panic!("Wrong deserialization")
113+
};
114+
// Make sure legacy recover works correctly
115+
let legacy: Vec<ExtensionLegacy> = crate::tlv::parse(&tlv_vec);
116+
assert!(matches!(legacy[0], ExtensionLegacy::Test(42)));
117+
assert_eq!(legacy.len(), 1, "Legacy parser should only recover ")
118+
}
119+
120+
/// Test that TLV encoded data is forwards-compatible,
121+
/// i.e. that legacy TLV data can be decoded by a new
122+
/// receiver
123+
#[test]
124+
fn test_tlv_forward_compat() {
125+
let legacy_tlv_data = vec![
126+
ExtensionLegacy::Test(42),
127+
ExtensionLegacy::LegacyString(String::from("foo")),
128+
];
129+
let legacy_bytes = bincode::serialize(&legacy_tlv_data).unwrap();
130+
131+
let tlv_vec: Vec<TlvRecord> = bincode::deserialize(&legacy_bytes).unwrap();
132+
// Just in case make sure that legacy data is serialized correctly
133+
let legacy: Vec<ExtensionLegacy> = crate::tlv::parse(&tlv_vec);
134+
assert!(matches!(legacy[0], ExtensionLegacy::Test(42)));
135+
if let ExtensionLegacy::LegacyString(s) = &legacy[1] {
136+
assert_eq!(s, "foo");
137+
} else {
138+
panic!("Wrong deserialization")
139+
};
140+
// Parse the same bytes using new parser
141+
let new: Vec<ExtensionNew> = crate::tlv::parse(&tlv_vec);
142+
assert!(matches!(new[0], ExtensionNew::Test(42)));
143+
if let ExtensionNew::LegacyString(s) = &new[1] {
144+
assert_eq!(s, "foo");
145+
} else {
146+
panic!("Wrong deserialization")
147+
};
148+
}
149+
}

0 commit comments

Comments
 (0)