Skip to content

Commit 7be24c2

Browse files
* TLV PoC for Gossip extensions --------- Co-authored-by: Greg Cusack <[email protected]>
1 parent b88655c commit 7be24c2

File tree

3 files changed

+194
-6
lines changed

3 files changed

+194
-6
lines changed

gossip/src/contact_info.rs

Lines changed: 23 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, TlvDecodeError, TlvRecord},
8+
},
49
assert_matches::{assert_matches, debug_assert_matches},
510
serde::{Deserialize, Deserializer, Serialize},
611
solana_pubkey::Pubkey,
@@ -105,8 +110,19 @@ struct SocketEntry {
105110
offset: u16, // Port offset with respect to the previous entry.
106111
}
107112

108-
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
109-
enum Extension {}
113+
define_tlv_enum!(
114+
/// TLV encoded Extensions in ContactInfo messages
115+
///
116+
/// On the wire each record is: [type: u8][len: varint][bytes]
117+
/// Extensions with unknown types are skipped by tlv::parse,
118+
/// so new types can be added without breaking legacy code,
119+
/// and support by all clients is not required.
120+
///
121+
/// Always add new TLV records to the end of this enum.
122+
/// Never reorder or reuse a type.
123+
/// Ensure new type collisions do not happen.
124+
pub(crate) enum Extension {}
125+
);
110126

111127
// As part of deserialization, self.addrs and self.sockets should be cross
112128
// verified and self.cache needs to be populated. This type serves as a
@@ -124,8 +140,9 @@ struct ContactInfoLite {
124140
addrs: Vec<IpAddr>,
125141
#[serde(with = "short_vec")]
126142
sockets: Vec<SocketEntry>,
143+
#[allow(dead_code)]
127144
#[serde(with = "short_vec")]
128-
extensions: Vec<Extension>,
145+
extensions: Vec<TlvRecord>,
129146
}
130147

131148
macro_rules! get_socket {
@@ -212,7 +229,7 @@ impl ContactInfo {
212229
version: solana_version::Version::default(),
213230
addrs: Vec::<IpAddr>::default(),
214231
sockets: Vec::<SocketEntry>::default(),
215-
extensions: Vec::<Extension>::default(),
232+
extensions: Vec::default(),
216233
cache: EMPTY_SOCKET_ADDR_CACHE,
217234
}
218235
}
@@ -541,7 +558,7 @@ impl TryFrom<ContactInfoLite> for ContactInfo {
541558
version,
542559
addrs,
543560
sockets,
544-
extensions,
561+
extensions: tlv::parse(&extensions),
545562
cache: EMPTY_SOCKET_ADDR_CACHE,
546563
};
547564
// Populate node.cache.

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

0 commit comments

Comments
 (0)