Skip to main content

hopr_transport_probe/
content.rs

1use hopr_api::types::primitive::prelude::GeneralError;
2use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
3
4use crate::{
5    errors::ProbeError,
6    types::{NeighborProbe, PathTelemetry},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants, strum::Display)]
10#[strum_discriminants(vis(pub(crate)))]
11#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
12pub enum Message {
13    Telemetry(PathTelemetry),
14    Probe(NeighborProbe),
15}
16
17impl Message {
18    pub const VERSION: u8 = 1;
19
20    pub fn to_bytes(self) -> Box<[u8]> {
21        let mut out = Vec::<u8>::with_capacity(1 + NeighborProbe::SIZE.max(PathTelemetry::SIZE));
22        out.push(Self::VERSION);
23        out.push(MessageDiscriminants::from(&self) as u8);
24
25        match self {
26            Message::Telemetry(telemetry) => out.extend(telemetry.to_bytes()),
27            Message::Probe(probe) => out.extend(probe.to_bytes()),
28        }
29
30        out.into_boxed_slice()
31    }
32}
33
34impl<'a> TryFrom<&'a [u8]> for Message {
35    type Error = GeneralError;
36
37    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
38        if value.len() < 2 {
39            return Err(GeneralError::ParseError("Message.size".into()));
40        }
41
42        if value[0] != Self::VERSION {
43            return Err(GeneralError::ParseError("Message.version".into()));
44        }
45
46        match MessageDiscriminants::from_repr(value[1]).ok_or(GeneralError::ParseError("Message.disc".into()))? {
47            MessageDiscriminants::Telemetry => {
48                if value.len() == 2 + PathTelemetry::SIZE {
49                    Ok(Self::Telemetry(
50                        (&value[2..])
51                            .try_into()
52                            .map_err(|_| GeneralError::ParseError("Message.telemetry".into()))?,
53                    ))
54                } else {
55                    Err(GeneralError::ParseError("Message.telemetry.size".into()))?
56                }
57            }
58            MessageDiscriminants::Probe => {
59                if value.len() == 2 + NeighborProbe::SIZE {
60                    Ok(Self::Probe(
61                        (&value[2..])
62                            .try_into()
63                            .map_err(|_| GeneralError::ParseError("Message.probe".into()))?,
64                    ))
65                } else {
66                    Err(GeneralError::ParseError("Message.probe.size".into()))?
67                }
68            }
69        }
70    }
71}
72
73impl TryFrom<Message> for ApplicationData {
74    type Error = ProbeError;
75
76    fn try_from(message: Message) -> Result<Self, Self::Error> {
77        Ok(ApplicationData::new(ReservedTag::Ping, message.to_bytes().into_vec())?)
78    }
79}
80
81impl TryFrom<ApplicationData> for Message {
82    type Error = anyhow::Error;
83
84    fn try_from(value: ApplicationData) -> Result<Self, Self::Error> {
85        let reserved_probe_tag: Tag = ReservedTag::Ping.into();
86
87        if value.application_tag == reserved_probe_tag {
88            let message: Message = value.plain_text.as_ref().try_into()?;
89            Ok(message)
90        } else {
91            Err(anyhow::anyhow!("Non probing application tag found"))
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use anyhow::Context;
99    use hopr_api::types::primitive::traits::AsUnixTimestamp;
100    use hopr_utils::platform::time::native::current_time;
101    use more_asserts::assert_lt;
102
103    use super::*;
104
105    #[test]
106    fn probe_message_variant_probe_should_serialize_and_deserialize() -> anyhow::Result<()> {
107        let m1 = Message::Probe(NeighborProbe::random_nonce());
108        let m2 = Message::try_from(m1.to_bytes().as_ref())?;
109
110        assert_eq!(m1, m2);
111
112        Ok(())
113    }
114
115    #[test]
116    fn probe_message_variant_telemetry_should_serialize_and_deserialize() -> anyhow::Result<()> {
117        let m1 = Message::Telemetry(PathTelemetry {
118            id: hopr_api::types::crypto_random::random_bytes(),
119            path: hopr_api::types::crypto_random::random_bytes(),
120            timestamp: 1234567890,
121        });
122        let m2 = Message::try_from(m1.to_bytes().as_ref())?;
123
124        assert_eq!(m1, m2);
125        Ok(())
126    }
127
128    #[test]
129    fn random_generation_of_a_neighbor_probe_produces_a_ping() -> anyhow::Result<()> {
130        let ping = NeighborProbe::random_nonce();
131        anyhow::ensure!(matches!(ping, NeighborProbe::Ping(_)), "expected Ping variant");
132        Ok(())
133    }
134
135    #[test]
136    fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
137        let ping = NeighborProbe::random_nonce();
138        let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
139
140        assert!(ping.is_complement_to(pong));
141        Ok(())
142    }
143
144    #[test]
145    fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
146        let ping = NeighborProbe::random_nonce();
147        let pong = {
148            let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
149            other[0] = other[0].wrapping_add(1); // Modify the first byte to ensure it's different
150            NeighborProbe::Pong(other)
151        };
152
153        assert!(!ping.is_complement_to(pong));
154
155        Ok(())
156    }
157
158    #[test]
159    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
160        let ping = NeighborProbe::random_nonce();
161        let as_data: ApplicationData = Message::Probe(ping).try_into()?;
162
163        assert_lt!(
164            as_data.plain_text.len(),
165            ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
166        );
167
168        Ok(())
169    }
170
171    #[test]
172    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
173        let telemetry = PathTelemetry {
174            id: [1; 8],
175            path: [1; 5 * size_of::<u64>()],
176            timestamp: current_time().as_unix_timestamp().as_millis(),
177        };
178        let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
179
180        assert_lt!(
181            as_data.plain_text.len(),
182            ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
183        );
184
185        Ok(())
186    }
187
188    #[test]
189    fn message_from_empty_bytes_fails() -> anyhow::Result<()> {
190        let err = Message::try_from([].as_slice())
191            .err()
192            .context("expected error for empty bytes")?;
193        anyhow::ensure!(
194            matches!(&err, GeneralError::ParseError(s) if s.contains("size")),
195            "expected ParseError about size, got: {err}"
196        );
197        Ok(())
198    }
199
200    #[test]
201    fn message_from_truncated_header_fails() -> anyhow::Result<()> {
202        let err = Message::try_from([Message::VERSION].as_slice())
203            .err()
204            .context("expected error for truncated header")?;
205        anyhow::ensure!(
206            matches!(&err, GeneralError::ParseError(s) if s.contains("size")),
207            "expected ParseError about size, got: {err}"
208        );
209        Ok(())
210    }
211
212    #[test]
213    fn message_from_wrong_version_fails() -> anyhow::Result<()> {
214        // Version 0xFF instead of Message::VERSION (1)
215        let data = [0xFF, 0x00];
216        let err = Message::try_from(data.as_slice())
217            .err()
218            .context("expected error for wrong version")?;
219        anyhow::ensure!(
220            matches!(&err, GeneralError::ParseError(s) if s.contains("version")),
221            "expected ParseError about version, got: {err}"
222        );
223        Ok(())
224    }
225
226    #[test]
227    fn message_from_invalid_discriminant_fails() -> anyhow::Result<()> {
228        // Valid version but invalid discriminant
229        let data = [Message::VERSION, 0xFF];
230        let err = Message::try_from(data.as_slice())
231            .err()
232            .context("expected error for invalid discriminant")?;
233        anyhow::ensure!(
234            matches!(&err, GeneralError::ParseError(s) if s.contains("disc")),
235            "expected ParseError about discriminant, got: {err}"
236        );
237        Ok(())
238    }
239
240    #[test]
241    fn message_from_wrong_probe_size_fails() -> anyhow::Result<()> {
242        // Valid version + probe discriminant but truncated data
243        let data = [Message::VERSION, MessageDiscriminants::Probe as u8, 0x00];
244        let err = Message::try_from(data.as_slice())
245            .err()
246            .context("expected error for wrong probe size")?;
247        anyhow::ensure!(
248            matches!(&err, GeneralError::ParseError(s) if s.contains("probe.size")),
249            "expected ParseError about probe size, got: {err}"
250        );
251        Ok(())
252    }
253
254    #[test]
255    fn message_application_data_roundtrip() -> anyhow::Result<()> {
256        let probe = Message::Probe(NeighborProbe::random_nonce());
257        let app_data: ApplicationData = probe.try_into()?;
258        let restored: Message = app_data.try_into()?;
259
260        anyhow::ensure!(
261            matches!(restored, Message::Probe(NeighborProbe::Ping(_))),
262            "expected Probe(Ping) variant"
263        );
264
265        Ok(())
266    }
267
268    #[test]
269    fn message_application_data_wrong_tag_fails() -> anyhow::Result<()> {
270        let app_data = ApplicationData::new(Tag::MAX, b"not a probe")?;
271        let err = Message::try_from(app_data)
272            .err()
273            .context("expected error for wrong tag")?;
274        anyhow::ensure!(err.to_string().contains("tag"), "expected error about tag, got: {err}");
275        Ok(())
276    }
277}