hopr_transport_probe/
content.rs1use hopr_primitive_types::prelude::GeneralError;
2use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants)]
6#[strum_discriminants(vis(pub(crate)))]
7#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
8pub enum NeighborProbe {
9 Ping([u8; Self::NONCE_SIZE]),
11 Pong([u8; Self::NONCE_SIZE]),
13}
14
15impl NeighborProbe {
16 pub const NONCE_SIZE: usize = 32;
17 pub const SIZE: usize = 1 + Self::NONCE_SIZE;
18
19 pub fn random_nonce() -> Self {
21 Self::Ping(hopr_crypto_random::random_bytes::<{ Self::NONCE_SIZE }>())
22 }
23
24 pub fn is_complement_to(&self, other: Self) -> bool {
25 match (self, &other) {
26 (Self::Ping(nonce1), Self::Pong(nonce2)) => nonce1 == nonce2,
27 (Self::Pong(nonce1), Self::Ping(nonce2)) => nonce1 == nonce2,
28 _ => false,
29 }
30 }
31
32 pub fn to_bytes(self) -> Box<[u8]> {
33 let mut out = Vec::with_capacity(1 + Self::NONCE_SIZE);
34 out.push(NeighborProbeDiscriminants::from(&self) as u8);
35 out.extend_from_slice(self.as_ref());
36 out.into_boxed_slice()
37 }
38}
39
40impl<'a> TryFrom<&'a [u8]> for NeighborProbe {
41 type Error = GeneralError;
42
43 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
44 if value.len() == 1 + Self::NONCE_SIZE {
45 match NeighborProbeDiscriminants::from_repr(value[0])
46 .ok_or(GeneralError::ParseError("NeighborProbe.disc".into()))?
47 {
48 NeighborProbeDiscriminants::Ping => {
49 Ok(Self::Ping((&value[1..]).try_into().map_err(|_| {
50 GeneralError::ParseError("NeighborProbe.ping_nonce".into())
51 })?))
52 }
53 NeighborProbeDiscriminants::Pong => {
54 Ok(Self::Pong((&value[1..]).try_into().map_err(|_| {
55 GeneralError::ParseError("NeighborProbe.pong_nonce".into())
56 })?))
57 }
58 }
59 } else {
60 Err(GeneralError::ParseError("NeighborProbe.size".into()))
61 }
62 }
63}
64
65impl std::fmt::Display for NeighborProbe {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 NeighborProbe::Ping(nonce) => write!(f, "Ping({})", hex::encode(nonce)),
69 NeighborProbe::Pong(nonce) => write!(f, "Pong({})", hex::encode(nonce)),
70 }
71 }
72}
73
74impl AsRef<[u8]> for NeighborProbe {
75 fn as_ref(&self) -> &[u8] {
76 match self {
77 NeighborProbe::Ping(nonce) | NeighborProbe::Pong(nonce) => nonce,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub struct PathTelemetry {
85 pub id: [u8; Self::ID_SIZE],
86 pub path: [u8; Self::PATH_SIZE],
87 pub timestamp: u128,
88}
89
90impl PathTelemetry {
91 pub const ID_SIZE: usize = 10;
92 pub const PATH_SIZE: usize = 10;
93 pub const SIZE: usize = Self::ID_SIZE + Self::PATH_SIZE + size_of::<u128>();
94
95 pub fn to_bytes(self) -> Box<[u8]> {
96 let mut out = Vec::with_capacity(Self::SIZE);
97 out.extend_from_slice(&self.id);
98 out.extend_from_slice(&self.path);
99 out.extend_from_slice(&self.timestamp.to_be_bytes());
100 out.into_boxed_slice()
101 }
102}
103
104impl std::fmt::Display for PathTelemetry {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 write!(
107 f,
108 "PathTelemetry(id: {}, path: {}, timestamp: {})",
109 hex::encode(self.id),
110 hex::encode(self.path),
111 self.timestamp
112 )
113 }
114}
115
116impl<'a> TryFrom<&'a [u8]> for PathTelemetry {
117 type Error = GeneralError;
118
119 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
120 if value.len() == Self::SIZE {
121 Ok(Self {
122 id: (&value[0..10])
123 .try_into()
124 .map_err(|_| GeneralError::ParseError("PathTelemetry.id".into()))?,
125 path: (&value[10..20])
126 .try_into()
127 .map_err(|_| GeneralError::ParseError("PathTelemetry.path".into()))?,
128 timestamp: u128::from_be_bytes(
129 (&value[20..36])
130 .try_into()
131 .map_err(|_| GeneralError::ParseError("PathTelemetry.timestamp".into()))?,
132 ),
133 })
134 } else {
135 Err(GeneralError::ParseError("PathTelemetry".into()))
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants, strum::Display)]
141#[strum_discriminants(vis(pub(crate)))]
142#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
143pub enum Message {
144 Telemetry(PathTelemetry),
145 Probe(NeighborProbe),
146}
147
148impl Message {
149 pub const VERSION: u8 = 1;
150
151 pub fn to_bytes(self) -> Box<[u8]> {
152 let mut out = Vec::<u8>::with_capacity(1 + NeighborProbe::SIZE.max(PathTelemetry::SIZE));
153 out.push(Self::VERSION);
154 out.push(MessageDiscriminants::from(&self) as u8);
155
156 match self {
157 Message::Telemetry(telemetry) => out.extend(telemetry.to_bytes()),
158 Message::Probe(probe) => out.extend(probe.to_bytes()),
159 }
160
161 out.into_boxed_slice()
162 }
163}
164
165impl<'a> TryFrom<&'a [u8]> for Message {
166 type Error = GeneralError;
167
168 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
169 if value.is_empty() {
170 return Err(GeneralError::ParseError("Message.size".into()));
171 }
172
173 if value[0] != Self::VERSION {
174 return Err(GeneralError::ParseError("Message.version".into()));
175 }
176
177 match MessageDiscriminants::from_repr(value[1]).ok_or(GeneralError::ParseError("Message.disc".into()))? {
178 MessageDiscriminants::Telemetry => {
179 if value.len() == 2 + PathTelemetry::SIZE {
180 Ok(Self::Telemetry(
181 (&value[2..])
182 .try_into()
183 .map_err(|_| GeneralError::ParseError("Message.telemetry".into()))?,
184 ))
185 } else {
186 Err(GeneralError::ParseError("Message.telemetry.size".into()))?
187 }
188 }
189 MessageDiscriminants::Probe => {
190 if value.len() == 2 + NeighborProbe::SIZE {
191 Ok(Self::Probe(
192 (&value[2..])
193 .try_into()
194 .map_err(|_| GeneralError::ParseError("Message.probe".into()))?,
195 ))
196 } else {
197 Err(GeneralError::ParseError("Message.probe.size".into()))?
198 }
199 }
200 }
201 }
202}
203
204impl From<Message> for ApplicationData {
205 fn from(message: Message) -> Self {
206 ApplicationData::new_from_owned(ReservedTag::Ping, message.to_bytes())
207 }
208}
209
210impl TryFrom<ApplicationData> for Message {
211 type Error = anyhow::Error;
212
213 fn try_from(value: ApplicationData) -> Result<Self, Self::Error> {
214 let reserved_probe_tag: Tag = ReservedTag::Ping.into();
215
216 if value.application_tag == reserved_probe_tag {
217 let message: Message = value.plain_text.as_ref().try_into()?;
218 Ok(message)
219 } else {
220 Err(anyhow::anyhow!("Non probing application tag found"))
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use hopr_platform::time::native::current_time;
228 use hopr_primitive_types::traits::AsUnixTimestamp;
229 use more_asserts::assert_lt;
230
231 use super::*;
232
233 #[test]
234 fn probe_message_should_serialize_and_deserialize() -> anyhow::Result<()> {
235 let m1 = Message::Probe(NeighborProbe::random_nonce());
236 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
237
238 assert_eq!(m1, m2);
239
240 let m1 = Message::Telemetry(PathTelemetry {
241 id: hopr_crypto_random::random_bytes(),
242 path: hopr_crypto_random::random_bytes(),
243 timestamp: 1234567890,
244 });
245 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
246
247 assert_eq!(m1, m2);
248 Ok(())
249 }
250
251 #[test]
252 fn random_generation_of_a_neighbor_probe_produces_a_ping() {
253 let ping = NeighborProbe::random_nonce();
254 assert!(matches!(ping, NeighborProbe::Ping(_)));
255 }
256
257 #[test]
258 fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
259 let ping = NeighborProbe::random_nonce();
260 let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
261
262 assert!(ping.is_complement_to(pong));
263 Ok(())
264 }
265
266 #[test]
267 fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
268 let ping = NeighborProbe::random_nonce();
269 let pong = {
270 let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
271 other[0] = other[0].wrapping_add(1); NeighborProbe::Pong(other)
273 };
274
275 assert!(!ping.is_complement_to(pong));
276
277 Ok(())
278 }
279
280 #[test]
281 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
282 let ping = NeighborProbe::random_nonce();
283 let as_data: ApplicationData = Message::Probe(ping).into();
284
285 assert_lt!(
286 as_data.plain_text.len(),
287 ApplicationData::PAYLOAD_SIZE - hopr_db_api::protocol::HoprSurb::SIZE
288 );
289
290 Ok(())
291 }
292
293 #[test]
294 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
295 let telemetry = PathTelemetry {
296 id: [1; 10],
297 path: [1; 10],
298 timestamp: current_time().as_unix_timestamp().as_millis(),
299 };
300 let as_data: ApplicationData = Message::Telemetry(telemetry).into();
301
302 assert_lt!(
303 as_data.plain_text.len(),
304 ApplicationData::PAYLOAD_SIZE - hopr_db_api::protocol::HoprSurb::SIZE
305 );
306
307 Ok(())
308 }
309}