hopr_transport_probe/
content.rs

1use hopr_primitive_types::prelude::GeneralError;
2use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
3
4/// Serializable and deserializable enum for the probe message content
5#[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 message with random nonce
10    Ping([u8; Self::NONCE_SIZE]),
11    /// Pong message replying to a specific nonce
12    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    /// Returns the nonce of the message
20    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/// TODO: TBD as a separate task for network discovery
83#[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); // Modify the first byte to ensure it's different
272            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}