hopr_transport_probe/
content.rs

1use hopr_primitive_types::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.is_empty() {
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 hopr_platform::time::native::current_time;
99    use hopr_primitive_types::traits::AsUnixTimestamp;
100    use more_asserts::assert_lt;
101
102    use super::*;
103
104    #[test]
105    fn probe_message_should_serialize_and_deserialize() -> anyhow::Result<()> {
106        let m1 = Message::Probe(NeighborProbe::random_nonce());
107        let m2 = Message::try_from(m1.to_bytes().as_ref())?;
108
109        assert_eq!(m1, m2);
110
111        let m1 = Message::Telemetry(PathTelemetry {
112            id: hopr_crypto_random::random_bytes(),
113            path: hopr_crypto_random::random_bytes(),
114            timestamp: 1234567890,
115        });
116        let m2 = Message::try_from(m1.to_bytes().as_ref())?;
117
118        assert_eq!(m1, m2);
119        Ok(())
120    }
121
122    #[test]
123    fn random_generation_of_a_neighbor_probe_produces_a_ping() {
124        let ping = NeighborProbe::random_nonce();
125        assert!(matches!(ping, NeighborProbe::Ping(_)));
126    }
127
128    #[test]
129    fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
130        let ping = NeighborProbe::random_nonce();
131        let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
132
133        assert!(ping.is_complement_to(pong));
134        Ok(())
135    }
136
137    #[test]
138    fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
139        let ping = NeighborProbe::random_nonce();
140        let pong = {
141            let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
142            other[0] = other[0].wrapping_add(1); // Modify the first byte to ensure it's different
143            NeighborProbe::Pong(other)
144        };
145
146        assert!(!ping.is_complement_to(pong));
147
148        Ok(())
149    }
150
151    #[test]
152    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
153        let ping = NeighborProbe::random_nonce();
154        let as_data: ApplicationData = Message::Probe(ping).try_into()?;
155
156        assert_lt!(
157            as_data.plain_text.len(),
158            ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
159        );
160
161        Ok(())
162    }
163
164    #[test]
165    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
166        let telemetry = PathTelemetry {
167            id: [1; 10],
168            path: [1; 10],
169            timestamp: current_time().as_unix_timestamp().as_millis(),
170        };
171        let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
172
173        assert_lt!(
174            as_data.plain_text.len(),
175            ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
176        );
177
178        Ok(())
179    }
180}