hopr_transport_probe/
content.rs

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