hopr_crypto_packet/
packet.rs

1use std::fmt::{Display, Formatter};
2
3use hopr_crypto_sphinx::prelude::*;
4use hopr_crypto_types::prelude::*;
5use hopr_internal_types::prelude::*;
6use hopr_path::{NonEmptyPath, TransportPath};
7use hopr_primitive_types::prelude::*;
8
9use crate::{
10    HoprPseudonym, HoprReplyOpener, HoprSphinxHeaderSpec, HoprSphinxSuite, HoprSurb, PAYLOAD_SIZE_INT,
11    errors::{
12        PacketError::{PacketConstructionError, PacketDecodingError},
13        Result,
14    },
15    por::{SurbReceiverInfo, derive_ack_key_share, generate_proof_of_relay, pre_verify},
16    types::{HoprPacketMessage, HoprSenderId, HoprSurbId},
17};
18
19/// Represents an outgoing packet that has been only partially instantiated.
20///
21/// It contains [`PartialPacket`], required Proof-of-Relay
22/// fields, and the [`Ticket`], but it does not contain the payload.
23///
24/// This can be used to pre-compute packets for certain destinations,
25/// and [convert](PartialHoprPacket::into_hopr_packet) them to full packets
26/// once the payload is known.
27#[derive(Clone)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct PartialHoprPacket {
30    partial_packet: PartialPacket<HoprSphinxSuite, HoprSphinxHeaderSpec>,
31    surbs: Vec<HoprSurb>,
32    openers: Vec<HoprReplyOpener>,
33    ticket: Ticket,
34    next_hop: OffchainPublicKey,
35    ack_challenge: HalfKeyChallenge,
36}
37
38impl PartialHoprPacket {
39    /// Instantiates a new partial HOPR packet.
40    ///
41    /// # Arguments
42    ///
43    /// * `pseudonym` our pseudonym as packet sender.
44    /// * `routing` routing to the destination.
45    /// * `chain_keypair` private key of the local node.
46    /// * `ticket` ticket builder for the first hop on the path.
47    /// * `mapper` of the public key identifiers.
48    /// * `domain_separator` channels contract domain separator.
49    pub fn new<M: KeyIdMapper<HoprSphinxSuite, HoprSphinxHeaderSpec>, P: NonEmptyPath<OffchainPublicKey>>(
50        pseudonym: &HoprPseudonym,
51        routing: PacketRouting<P>,
52        chain_keypair: &ChainKeypair,
53        ticket: TicketBuilder,
54        mapper: &M,
55        domain_separator: &Hash,
56    ) -> Result<Self> {
57        match routing {
58            PacketRouting::ForwardPath {
59                forward_path,
60                return_paths,
61            } => {
62                // Create shared secrets and PoR challenge chain
63                let shared_keys = HoprSphinxSuite::new_shared_keys(&forward_path)?;
64                let (por_strings, por_values) = generate_proof_of_relay(&shared_keys.secrets)?;
65                let receiver_data = HoprSenderId::new(pseudonym);
66
67                // Create SURBs if some return paths were specified
68                let (surbs, openers): (Vec<_>, Vec<_>) = return_paths
69                    .iter()
70                    .zip(receiver_data.into_sequence())
71                    .map(|(rp, data)| create_surb_for_path(rp, data, mapper))
72                    .collect::<Result<Vec<_>>>()?
73                    .into_iter()
74                    .unzip();
75
76                // Update the ticket with the challenge
77                let ticket = ticket
78                    .challenge(por_values.ticket_challenge())
79                    .build_signed(chain_keypair, domain_separator)?
80                    .leak();
81
82                Ok(Self {
83                    partial_packet: PartialPacket::<HoprSphinxSuite, HoprSphinxHeaderSpec>::new(
84                        MetaPacketRouting::ForwardPath {
85                            shared_keys,
86                            forward_path: &forward_path,
87                            receiver_data: &receiver_data,
88                            additional_data_relayer: &por_strings,
89                            no_ack: false,
90                        },
91                        mapper,
92                    )?,
93                    surbs,
94                    openers,
95                    ticket,
96                    next_hop: forward_path[0],
97                    ack_challenge: por_values.acknowledgement_challenge(),
98                })
99            }
100            PacketRouting::Surb(id, surb) => {
101                // Update the ticket with the challenge
102                let ticket = ticket
103                    .challenge(surb.additional_data_receiver.proof_of_relay_values().ticket_challenge())
104                    .build_signed(chain_keypair, domain_separator)?
105                    .leak();
106
107                Ok(Self {
108                    ticket,
109                    next_hop: mapper.map_id_to_public(&surb.first_relayer).ok_or_else(|| {
110                        PacketConstructionError(format!(
111                            "failed to map key id {} to public key",
112                            surb.first_relayer.to_hex()
113                        ))
114                    })?,
115                    ack_challenge: surb
116                        .additional_data_receiver
117                        .proof_of_relay_values()
118                        .acknowledgement_challenge(),
119                    partial_packet: PartialPacket::<HoprSphinxSuite, HoprSphinxHeaderSpec>::new(
120                        MetaPacketRouting::Surb(surb, &HoprSenderId::from_pseudonym_and_id(pseudonym, id)),
121                        mapper,
122                    )?,
123                    surbs: vec![],
124                    openers: vec![],
125                })
126            }
127            PacketRouting::NoAck(destination) => {
128                // Create shared secrets and PoR challenge chain
129                let shared_keys = HoprSphinxSuite::new_shared_keys(&[destination])?;
130                let (por_strings, por_values) = generate_proof_of_relay(&shared_keys.secrets)?;
131
132                // Update the ticket with the challenge
133                let ticket = ticket
134                    .challenge(por_values.ticket_challenge())
135                    .build_signed(chain_keypair, domain_separator)?
136                    .leak();
137
138                Ok(Self {
139                    partial_packet: PartialPacket::<HoprSphinxSuite, HoprSphinxHeaderSpec>::new(
140                        MetaPacketRouting::ForwardPath {
141                            shared_keys,
142                            forward_path: &[destination],
143                            receiver_data: &HoprSenderId::new(pseudonym),
144                            additional_data_relayer: &por_strings,
145                            no_ack: true, // Indicate this is a no acknowledgement probe packet
146                        },
147                        mapper,
148                    )?,
149                    ticket,
150                    next_hop: destination,
151                    ack_challenge: por_values.acknowledgement_challenge(),
152                    surbs: vec![],
153                    openers: vec![],
154                })
155            }
156        }
157    }
158
159    /// Turns this partial HOPR packet into a full [`Outgoing`](HoprPacket::Outgoing) [`HoprPacket`] by
160    /// attaching the given payload.
161    pub fn into_hopr_packet(self, msg: &[u8]) -> Result<(HoprPacket, Vec<HoprReplyOpener>)> {
162        let msg = HoprPacketMessage::from_parts(self.surbs, msg)?;
163        Ok((
164            HoprPacket::Outgoing(
165                HoprOutgoingPacket {
166                    packet: self.partial_packet.into_meta_packet(msg.into()),
167                    ticket: self.ticket,
168                    next_hop: self.next_hop,
169                    ack_challenge: self.ack_challenge,
170                }
171                .into(),
172            ),
173            self.openers,
174        ))
175    }
176}
177
178/// Represents a packet incoming to its final destination.
179#[derive(Clone)]
180pub struct HoprIncomingPacket {
181    /// Packet's authentication tag.
182    pub packet_tag: PacketTag,
183    /// Acknowledgement to be sent to the previous hop.
184    ///
185    /// In case an acknowledgement is not required, this field is `None`. This arises specifically
186    /// in case the message payload is used to send one or more acknowledgements in the payload.
187    pub ack_key: Option<HalfKey>,
188    /// Address of the previous hop.
189    pub previous_hop: OffchainPublicKey,
190    /// Decrypted packet payload.
191    pub plain_text: Box<[u8]>,
192    /// Pseudonym of the packet creator.
193    pub sender: HoprPseudonym,
194    /// List of [`SURBs`](SURB) to be used for replies sent to the packet creator.
195    pub surbs: Vec<(HoprSurbId, HoprSurb)>,
196}
197
198/// Represents a packet destined for another node.
199#[derive(Clone)]
200pub struct HoprOutgoingPacket {
201    /// Encrypted packet.
202    pub packet: MetaPacket<HoprSphinxSuite, HoprSphinxHeaderSpec, PAYLOAD_SIZE_INT>,
203    /// Ticket for this node.
204    pub ticket: Ticket,
205    /// Next hop this packet should be sent to.
206    pub next_hop: OffchainPublicKey,
207    /// Acknowledgement challenge solved once the next hop sends us an acknowledgement.
208    pub ack_challenge: HalfKeyChallenge,
209}
210
211/// Represents a [`HoprOutgoingPacket`] with additional forwarding information.
212#[derive(Clone)]
213pub struct HoprForwardedPacket {
214    /// Packet to be sent.
215    pub outgoing: HoprOutgoingPacket,
216    /// Authentication tag of the packet's header.
217    pub packet_tag: PacketTag,
218    /// Acknowledgement to be sent to the previous hop.
219    pub ack_key: HalfKey,
220    /// Sender of this packet.
221    pub previous_hop: OffchainPublicKey,
222    /// Key used to verify our challenge.
223    pub own_key: HalfKey,
224    /// Challenge for the next hop.
225    pub next_challenge: EthereumChallenge,
226    /// Our position in the path.
227    pub path_pos: u8,
228}
229
230/// Contains HOPR packet and its variants.
231///
232/// See [`HoprIncomingPacket`], [`HoprForwardedPacket`] and [`HoprOutgoingPacket`] for details.
233///
234/// The members are intentionally boxed to equalize the variant sizes.
235#[derive(Clone, strum::EnumTryAs, strum::EnumIs)]
236pub enum HoprPacket {
237    /// The packet is intended for us
238    Final(Box<HoprIncomingPacket>),
239    /// The packet must be forwarded
240    Forwarded(Box<HoprForwardedPacket>),
241    /// The packet that is being sent out by us
242    Outgoing(Box<HoprOutgoingPacket>),
243}
244
245impl Display for HoprPacket {
246    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
247        match &self {
248            Self::Final(_) => write!(f, "Final"),
249            Self::Forwarded(_) => write!(f, "Forwarded"),
250            Self::Outgoing(_) => write!(f, "Outgoing"),
251        }
252    }
253}
254
255/// Determines options on how HOPR packet can be routed to its destination.
256#[derive(Clone)]
257pub enum PacketRouting<P: NonEmptyPath<OffchainPublicKey> = TransportPath> {
258    /// The packet is routed directly via the given path.
259    /// Optionally, return paths for
260    /// attached SURBs can be specified.
261    ForwardPath { forward_path: P, return_paths: Vec<P> },
262    /// The packet is routed via an existing SURB that corresponds to a pseudonym.
263    Surb(HoprSurbId, HoprSurb),
264    /// No acknowledgement packet: a special type of 0-hop packet that is not going to be acknowledged but can carry a
265    /// payload.
266    NoAck(OffchainPublicKey),
267}
268
269fn create_surb_for_path<M: KeyIdMapper<HoprSphinxSuite, HoprSphinxHeaderSpec>, P: NonEmptyPath<OffchainPublicKey>>(
270    return_path: &P,
271    recv_data: HoprSenderId,
272    mapper: &M,
273) -> Result<(HoprSurb, HoprReplyOpener)> {
274    let shared_keys = HoprSphinxSuite::new_shared_keys(return_path)?;
275    let (por_strings, por_values) = generate_proof_of_relay(&shared_keys.secrets)?;
276
277    Ok(create_surb::<HoprSphinxSuite, HoprSphinxHeaderSpec>(
278        shared_keys,
279        &return_path
280            .iter()
281            .map(|k| {
282                mapper
283                    .map_key_to_id(k)
284                    .ok_or_else(|| PacketConstructionError(format!("failed to map key {} to id", k.to_hex())))
285            })
286            .collect::<Result<Vec<_>>>()?,
287        &por_strings,
288        recv_data,
289        SurbReceiverInfo::new(por_values, [0u8; 32]),
290    )
291    .map(|(s, r)| (s, (recv_data.surb_id(), r)))?)
292}
293
294impl HoprPacket {
295    /// The maximum number of SURBs that fit into a packet that contains no message.
296    pub const MAX_SURBS_IN_PACKET: usize = HoprPacket::PAYLOAD_SIZE / HoprSurb::SIZE;
297    /// Maximum message size when no SURBs are present in the packet.
298    ///
299    /// See [`HoprPacket::max_surbs_with_message`].
300    pub const PAYLOAD_SIZE: usize = PAYLOAD_SIZE_INT - HoprPacketMessage::HEADER_LEN;
301    /// The size of the packet including header, padded payload, ticket, and ack challenge.
302    pub const SIZE: usize =
303        MetaPacket::<HoprSphinxSuite, HoprSphinxHeaderSpec, PAYLOAD_SIZE_INT>::PACKET_LEN + Ticket::SIZE;
304
305    /// Constructs a new outgoing packet with the given path.
306    ///
307    /// # Arguments
308    /// * `msg` packet payload.
309    /// * `pseudonym` our pseudonym as packet sender.
310    /// * `routing` routing to the destination.
311    /// * `chain_keypair` private key of the local node.
312    /// * `ticket` ticket builder for the first hop on the path.
313    /// * `mapper` of the public key identifiers.
314    /// * `domain_separator` channels contract domain separator.
315    ///
316    /// **NOTE**
317    /// For the given pseudonym, the [`ReplyOpener`] order matters.
318    pub fn into_outgoing<M: KeyIdMapper<HoprSphinxSuite, HoprSphinxHeaderSpec>, P: NonEmptyPath<OffchainPublicKey>>(
319        msg: &[u8],
320        pseudonym: &HoprPseudonym,
321        routing: PacketRouting<P>,
322        chain_keypair: &ChainKeypair,
323        ticket: TicketBuilder,
324        mapper: &M,
325        domain_separator: &Hash,
326    ) -> Result<(Self, Vec<HoprReplyOpener>)> {
327        PartialHoprPacket::new(pseudonym, routing, chain_keypair, ticket, mapper, domain_separator)?
328            .into_hopr_packet(msg)
329    }
330
331    /// Calculates how many SURBs can be fitted into a packet that
332    /// also carries a message of the given length.
333    pub const fn max_surbs_with_message(msg_len: usize) -> usize {
334        HoprPacket::PAYLOAD_SIZE.saturating_sub(msg_len) / HoprSurb::SIZE
335    }
336
337    /// Calculates the maximum length of the message that can be carried by a packet
338    /// with the given number of SURBs.
339    pub const fn max_message_with_surbs(num_surbs: usize) -> usize {
340        HoprPacket::PAYLOAD_SIZE.saturating_sub(num_surbs * HoprSurb::SIZE)
341    }
342
343    /// Deserializes the packet and performs the forward-transformation, so the
344    /// packet can be further delivered (relayed to the next hop or read).
345    pub fn from_incoming<M, F>(
346        data: &[u8],
347        node_keypair: &OffchainKeypair,
348        previous_hop: OffchainPublicKey,
349        mapper: &M,
350        reply_openers: F,
351    ) -> Result<Self>
352    where
353        M: KeyIdMapper<HoprSphinxSuite, HoprSphinxHeaderSpec>,
354        F: FnMut(&HoprSenderId) -> Option<ReplyOpener>,
355    {
356        if data.len() == Self::SIZE {
357            let (pre_packet, pre_ticket) =
358                data.split_at(MetaPacket::<HoprSphinxSuite, HoprSphinxHeaderSpec, PAYLOAD_SIZE_INT>::PACKET_LEN);
359
360            let mp: MetaPacket<HoprSphinxSuite, HoprSphinxHeaderSpec, PAYLOAD_SIZE_INT> =
361                MetaPacket::try_from(pre_packet)?;
362
363            match mp.into_forwarded(node_keypair, mapper, reply_openers)? {
364                ForwardedMetaPacket::Relayed {
365                    packet,
366                    derived_secret,
367                    additional_info,
368                    packet_tag,
369                    next_node,
370                    path_pos,
371                    ..
372                } => {
373                    let ack_key = derive_ack_key_share(&derived_secret);
374
375                    let ticket = Ticket::try_from(pre_ticket)?;
376                    let verification_output = pre_verify(&derived_secret, &additional_info, &ticket.challenge)?;
377                    Ok(Self::Forwarded(
378                        HoprForwardedPacket {
379                            outgoing: HoprOutgoingPacket {
380                                packet,
381                                ticket,
382                                next_hop: next_node,
383                                ack_challenge: verification_output.ack_challenge,
384                            },
385                            packet_tag,
386                            ack_key,
387                            previous_hop,
388                            path_pos,
389                            own_key: verification_output.own_key,
390                            next_challenge: verification_output.next_ticket_challenge,
391                        }
392                        .into(),
393                    ))
394                }
395                ForwardedMetaPacket::Final {
396                    packet_tag,
397                    plain_text,
398                    derived_secret,
399                    receiver_data,
400                    no_ack,
401                } => {
402                    // The pre_ticket is not parsed nor verified on the final hop
403                    let (surbs, plain_text) = HoprPacketMessage::from(plain_text).try_into_parts()?;
404                    let should_acknowledge = !no_ack;
405                    Ok(Self::Final(
406                        HoprIncomingPacket {
407                            packet_tag,
408                            ack_key: (should_acknowledge).then_some(derive_ack_key_share(&derived_secret)),
409                            previous_hop,
410                            plain_text,
411                            surbs: receiver_data.into_sequence().map(|d| d.surb_id()).zip(surbs).collect(),
412                            sender: receiver_data.pseudonym(),
413                        }
414                        .into(),
415                    ))
416                }
417            }
418        } else {
419            Err(PacketDecodingError("packet has invalid size".into()))
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use anyhow::{Context, bail};
427    use bimap::BiHashMap;
428    use hex_literal::hex;
429    use hopr_crypto_random::Randomizable;
430    use hopr_path::TransportPath;
431    use parameterized::parameterized;
432
433    use super::*;
434
435    lazy_static::lazy_static! {
436        static ref PEERS: [(ChainKeypair, OffchainKeypair); 5] = [
437            (hex!("a7c486ceccf5ab53bd428888ab1543dc2667abd2d5e80aae918da8d4b503a426"), hex!("5eb212d4d6aa5948c4f71574d45dad43afef6d330edb873fca69d0e1b197e906")),
438            (hex!("9a82976f7182c05126313bead5617c623b93d11f9f9691c87b1a26f869d569ed"), hex!("e995db483ada5174666c46bafbf3628005aca449c94ebdc0c9239c3f65d61ae0")),
439            (hex!("ca4bdfd54a8467b5283a0216288fdca7091122479ccf3cfb147dfa59d13f3486"), hex!("9dec751c00f49e50fceff7114823f726a0425a68a8dc6af0e4287badfea8f4a4")),
440            (hex!("e306ebfb0d01d0da0952c9a567d758093a80622c6cb55052bf5f1a6ebd8d7b5c"), hex!("9a82976f7182c05126313bead5617c623b93d11f9f9691c87b1a26f869d569ed")),
441            (hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775"), hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e")),
442        ].map(|(p1,p2)| (ChainKeypair::from_secret(&p1).expect("lazy static keypair should be valid"), OffchainKeypair::from_secret(&p2).expect("lazy static keypair should be valid")));
443
444        static ref MAPPER: bimap::BiMap<KeyIdent, OffchainPublicKey> = PEERS
445            .iter()
446            .enumerate()
447            .map(|(i, (_, k))| (KeyIdent::from(i as u32), *k.public()))
448            .collect::<BiHashMap<_, _>>();
449    }
450
451    fn forward(
452        mut packet: HoprPacket,
453        chain_keypair: &ChainKeypair,
454        next_ticket: TicketBuilder,
455        domain_separator: &Hash,
456    ) -> HoprPacket {
457        if let HoprPacket::Forwarded(fwd) = &mut packet {
458            fwd.outgoing.ticket = next_ticket
459                .challenge(fwd.next_challenge)
460                .build_signed(chain_keypair, domain_separator)
461                .expect("ticket should create")
462                .leak();
463        }
464
465        packet
466    }
467
468    impl HoprPacket {
469        pub fn to_bytes(&self) -> Box<[u8]> {
470            let dummy_ticket = hex!("67f0ca18102feec505e5bfedcc25963e9c64a6f8a250adcad7d2830dd607585700000000000000000000000000000000000000000000000000000000000000003891bf6fd4a78e868fc7ad477c09b16fc70dd01ea67e18264d17e3d04f6d8576de2e6472b0072e510df6e9fa1dfcc2727cc7633edfeb9ec13860d9ead29bee71d68de3736c2f7a9f42de76ccd57a5f5847bc7349");
471            let (packet, ticket) = match self {
472                Self::Final(packet) => (packet.plain_text.clone(), dummy_ticket.as_ref().into()),
473                Self::Forwarded(fwd) => (
474                    Vec::from(fwd.outgoing.packet.as_ref()).into_boxed_slice(),
475                    fwd.outgoing.ticket.clone().into_boxed(),
476                ),
477                Self::Outgoing(out) => (
478                    Vec::from(out.packet.as_ref()).into_boxed_slice(),
479                    out.ticket.clone().into_boxed(),
480                ),
481            };
482
483            let mut ret = Vec::with_capacity(Self::SIZE);
484            ret.extend_from_slice(packet.as_ref());
485            ret.extend_from_slice(&ticket);
486            ret.into_boxed_slice()
487        }
488    }
489
490    fn mock_ticket(
491        next_peer_channel_key: &PublicKey,
492        path_len: usize,
493        private_key: &ChainKeypair,
494    ) -> anyhow::Result<TicketBuilder> {
495        assert!(path_len > 0);
496        let price_per_packet: U256 = 10000000000000000u128.into();
497
498        if path_len > 1 {
499            Ok(TicketBuilder::default()
500                .direction(&private_key.public().to_address(), &next_peer_channel_key.to_address())
501                .amount(price_per_packet.div_f64(1.0)? * U256::from(path_len as u64 - 1))
502                .index(1)
503                .index_offset(1)
504                .win_prob(WinningProbability::ALWAYS)
505                .channel_epoch(1)
506                .challenge(Default::default()))
507        } else {
508            Ok(TicketBuilder::zero_hop()
509                .direction(&private_key.public().to_address(), &next_peer_channel_key.to_address()))
510        }
511    }
512
513    fn create_packet(
514        forward_hops: usize,
515        pseudonym: HoprPseudonym,
516        return_hops: Vec<usize>,
517        msg: &[u8],
518    ) -> anyhow::Result<(HoprPacket, Vec<HoprReplyOpener>)> {
519        assert!((0..=3).contains(&forward_hops), "forward hops must be between 1 and 3");
520        assert!(
521            return_hops.iter().all(|h| (0..=3).contains(h)),
522            "return hops must be between 1 and 3"
523        );
524
525        let ticket = mock_ticket(&PEERS[1].0.public().0, forward_hops + 1, &PEERS[0].0)?;
526        let forward_path = TransportPath::new(PEERS[1..=forward_hops + 1].iter().map(|kp| *kp.1.public()))?;
527
528        let return_paths = return_hops
529            .into_iter()
530            .map(|h| TransportPath::new(PEERS[0..=h].iter().rev().map(|kp| *kp.1.public())))
531            .collect::<std::result::Result<Vec<_>, hopr_path::errors::PathError>>()?;
532
533        Ok(HoprPacket::into_outgoing(
534            msg,
535            &pseudonym,
536            PacketRouting::ForwardPath {
537                forward_path,
538                return_paths,
539            },
540            &PEERS[0].0,
541            ticket,
542            &*MAPPER,
543            &Hash::default(),
544        )?)
545    }
546
547    fn create_packet_from_surb(
548        sender_node: usize,
549        surb_id: HoprSurbId,
550        surb: HoprSurb,
551        hopr_pseudonym: &HoprPseudonym,
552        msg: &[u8],
553    ) -> anyhow::Result<HoprPacket> {
554        assert!((1..=4).contains(&sender_node), "sender_node must be between 1 and 4");
555
556        let ticket = mock_ticket(
557            &PEERS[sender_node - 1].0.public().0,
558            surb.additional_data_receiver.proof_of_relay_values().chain_length() as usize,
559            &PEERS[sender_node].0,
560        )?;
561
562        Ok(HoprPacket::into_outgoing(
563            msg,
564            hopr_pseudonym,
565            PacketRouting::<TransportPath>::Surb(surb_id, surb),
566            &PEERS[sender_node].0,
567            ticket,
568            &*MAPPER,
569            &Hash::default(),
570        )?
571        .0)
572    }
573
574    fn process_packet_at_node<F>(
575        path_len: usize,
576        node_pos: usize,
577        is_reply: bool,
578        packet: HoprPacket,
579        openers: F,
580    ) -> anyhow::Result<HoprPacket>
581    where
582        F: FnMut(&HoprSenderId) -> Option<ReplyOpener>,
583    {
584        assert!((0..=4).contains(&node_pos), "node position must be between 1 and 3");
585
586        let prev_hop = match (node_pos, is_reply) {
587            (1, false) => *PEERS[0].1.public(),
588            (_, false) => *PEERS[node_pos - 1].1.public(),
589            (3, true) => *PEERS[4].1.public(),
590            (_, true) => *PEERS[node_pos + 1].1.public(),
591        };
592
593        let packet = HoprPacket::from_incoming(&packet.to_bytes(), &PEERS[node_pos].1, prev_hop, &*MAPPER, openers)
594            .context(format!("deserialization failure at hop {node_pos}"))?;
595
596        match &packet {
597            HoprPacket::Final(_) => Ok(packet),
598            HoprPacket::Forwarded(_) => {
599                let next_hop = match (node_pos, is_reply) {
600                    (3, false) => PEERS[4].0.public().0.clone(),
601                    (_, false) => PEERS[node_pos + 1].0.public().0.clone(),
602                    (1, true) => PEERS[0].0.public().0.clone(),
603                    (_, true) => PEERS[node_pos - 1].0.public().0.clone(),
604                };
605
606                let next_ticket = mock_ticket(&next_hop, path_len, &PEERS[node_pos].0)?;
607                Ok(forward(
608                    packet.clone(),
609                    &PEERS[node_pos].0,
610                    next_ticket,
611                    &Hash::default(),
612                ))
613            }
614            HoprPacket::Outgoing(_) => bail!("invalid packet state"),
615        }
616    }
617
618    #[parameterized(hops = { 0,1,2,3 })]
619    fn test_packet_forward_message_no_surb(hops: usize) -> anyhow::Result<()> {
620        let msg = b"some testing forward message";
621        let pseudonym = SimplePseudonym::random();
622        let (mut packet, opener) = create_packet(hops, pseudonym, vec![], msg)?;
623
624        assert!(opener.is_empty());
625        match &packet {
626            HoprPacket::Outgoing { .. } => {}
627            _ => bail!("invalid packet initial state"),
628        }
629
630        let mut actual_plain_text = Box::default();
631        for hop in 1..=hops + 1 {
632            packet = process_packet_at_node(hops + 1, hop, false, packet, |_| None)
633                .context(format!("packet decoding failed at hop {hop}"))?;
634
635            match &packet {
636                HoprPacket::Final(packet) => {
637                    assert_eq!(hop - 1, hops, "final packet must be at the last hop");
638                    assert!(packet.ack_key.is_some(), "must not be a no-ack packet");
639                    actual_plain_text = packet.plain_text.clone();
640                }
641                HoprPacket::Forwarded(fwd) => {
642                    assert_eq!(PEERS[hop - 1].1.public(), &fwd.previous_hop, "invalid previous hop");
643                    assert_eq!(PEERS[hop + 1].1.public(), &fwd.outgoing.next_hop, "invalid next hop");
644                    assert_eq!(hops + 1 - hop, fwd.path_pos as usize, "invalid path position");
645                }
646                HoprPacket::Outgoing(_) => bail!("invalid packet state at hop {hop}"),
647            }
648        }
649
650        assert_eq!(actual_plain_text.as_ref(), msg, "invalid plaintext");
651        Ok(())
652    }
653
654    #[parameterized(forward_hops = { 0,1,2,3 }, return_hops = { 0, 1, 2, 3})]
655    fn test_packet_forward_message_with_surb(forward_hops: usize, return_hops: usize) -> anyhow::Result<()> {
656        let msg = b"some testing forward message";
657        let pseudonym = SimplePseudonym::random();
658        let (mut packet, openers) = create_packet(forward_hops, pseudonym, vec![return_hops], msg)?;
659
660        assert_eq!(1, openers.len(), "invalid number of openers");
661        match &packet {
662            HoprPacket::Outgoing { .. } => {}
663            _ => bail!("invalid packet initial state"),
664        }
665
666        let mut received_plain_text = Box::default();
667        let mut received_surbs = vec![];
668        for hop in 1..=forward_hops + 1 {
669            packet = process_packet_at_node(forward_hops + 1, hop, false, packet, |_| None)
670                .context(format!("packet decoding failed at hop {hop}"))?;
671
672            match &packet {
673                HoprPacket::Final(packet) => {
674                    assert_eq!(hop - 1, forward_hops, "final packet must be at the last hop");
675                    assert_eq!(pseudonym, packet.sender, "invalid sender");
676                    assert!(packet.ack_key.is_some(), "must not be a no-ack packet");
677                    received_plain_text = packet.plain_text.clone();
678                    received_surbs.extend(packet.surbs.clone());
679                }
680                HoprPacket::Forwarded(fwd) => {
681                    assert_eq!(PEERS[hop - 1].1.public(), &fwd.previous_hop, "invalid previous hop");
682                    assert_eq!(PEERS[hop + 1].1.public(), &fwd.outgoing.next_hop, "invalid next hop");
683                    assert_eq!(forward_hops + 1 - hop, fwd.path_pos as usize, "invalid path position");
684                }
685                HoprPacket::Outgoing(_) => bail!("invalid packet state at hop {hop}"),
686            }
687        }
688
689        assert_eq!(received_plain_text.as_ref(), msg, "invalid plaintext");
690        assert_eq!(1, received_surbs.len(), "invalid number of surbs");
691        assert_eq!(
692            return_hops as u8 + 1,
693            received_surbs[0]
694                .1
695                .additional_data_receiver
696                .proof_of_relay_values()
697                .chain_length(),
698            "surb has invalid por chain length"
699        );
700
701        Ok(())
702    }
703
704    #[parameterized(
705        forward_hops = { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 },
706        return_hops  = { 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 }
707    )]
708    fn test_packet_forward_and_reply_message(forward_hops: usize, return_hops: usize) -> anyhow::Result<()> {
709        let pseudonym = SimplePseudonym::random();
710
711        // Forward packet
712        let fwd_msg = b"some testing forward message";
713        let (mut fwd_packet, mut openers) = create_packet(forward_hops, pseudonym, vec![return_hops], fwd_msg)?;
714
715        assert_eq!(1, openers.len(), "invalid number of openers");
716        match &fwd_packet {
717            HoprPacket::Outgoing { .. } => {}
718            _ => bail!("invalid packet initial state"),
719        }
720
721        let mut received_fwd_plain_text = Box::default();
722        let mut received_surbs = vec![];
723        for hop in 1..=forward_hops + 1 {
724            fwd_packet = process_packet_at_node(forward_hops + 1, hop, false, fwd_packet, |_| None)
725                .context(format!("packet decoding failed at hop {hop}"))?;
726
727            match &fwd_packet {
728                HoprPacket::Final(incoming) => {
729                    assert_eq!(hop - 1, forward_hops, "final packet must be at the last hop");
730                    assert_eq!(pseudonym, incoming.sender, "invalid sender");
731                    assert!(incoming.ack_key.is_some(), "must not be a no-ack packet");
732                    received_fwd_plain_text = incoming.plain_text.clone();
733                    received_surbs.extend(incoming.surbs.clone());
734                }
735                HoprPacket::Forwarded(fwd) => {
736                    assert_eq!(PEERS[hop - 1].1.public(), &fwd.previous_hop, "invalid previous hop");
737                    assert_eq!(PEERS[hop + 1].1.public(), &fwd.outgoing.next_hop, "invalid next hop");
738                    assert_eq!(forward_hops + 1 - hop, fwd.path_pos as usize, "invalid path position");
739                }
740                HoprPacket::Outgoing { .. } => bail!("invalid packet state at hop {hop}"),
741            }
742        }
743
744        assert_eq!(received_fwd_plain_text.as_ref(), fwd_msg, "invalid plaintext");
745        assert_eq!(1, received_surbs.len(), "invalid number of surbs");
746        assert_eq!(
747            return_hops as u8 + 1,
748            received_surbs[0]
749                .1
750                .additional_data_receiver
751                .proof_of_relay_values()
752                .chain_length(),
753            "surb has invalid por chain length"
754        );
755
756        // The reply packet
757        let re_msg = b"some testing reply message";
758        let mut re_packet = create_packet_from_surb(
759            forward_hops + 1,
760            received_surbs[0].0,
761            received_surbs[0].1.clone(),
762            &pseudonym,
763            re_msg,
764        )?;
765
766        let mut openers_fn = |p: &HoprSenderId| {
767            assert_eq!(p.pseudonym(), pseudonym);
768            let opener = openers.pop();
769            assert!(opener.as_ref().is_none_or(|(id, _)| id == &p.surb_id()));
770            opener.map(|(_, opener)| opener)
771        };
772
773        match &re_packet {
774            HoprPacket::Outgoing { .. } => {}
775            _ => bail!("invalid packet initial state"),
776        }
777
778        let mut received_re_plain_text = Box::default();
779        for hop in (0..=return_hops).rev() {
780            re_packet = process_packet_at_node(return_hops + 1, hop, true, re_packet, &mut openers_fn)
781                .context(format!("packet decoding failed at hop {hop}"))?;
782
783            match &re_packet {
784                HoprPacket::Final(incoming) => {
785                    assert_eq!(hop, 0, "final packet must be at the last hop");
786                    assert_eq!(pseudonym, incoming.sender, "invalid sender");
787                    assert!(incoming.ack_key.is_some(), "must not be a no-ack packet");
788                    assert!(incoming.surbs.is_empty(), "must not receive surbs on reply");
789                    received_re_plain_text = incoming.plain_text.clone();
790                }
791                HoprPacket::Forwarded(fwd) => {
792                    assert_eq!(PEERS[hop + 1].1.public(), &fwd.previous_hop, "invalid previous hop");
793                    assert_eq!(PEERS[hop - 1].1.public(), &fwd.outgoing.next_hop, "invalid next hop");
794                    assert_eq!(hop, fwd.path_pos as usize, "invalid path position");
795                }
796                HoprPacket::Outgoing(_) => bail!("invalid packet state at hop {hop}"),
797            }
798        }
799
800        assert_eq!(received_re_plain_text.as_ref(), re_msg, "invalid plaintext");
801        Ok(())
802    }
803
804    #[parameterized(
805        forward_hops = { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3 },
806        return_hops  = { 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 }
807    )]
808    fn test_packet_surbs_only_and_reply_message(forward_hops: usize, return_hops: usize) -> anyhow::Result<()> {
809        let pseudonym = SimplePseudonym::random();
810
811        // Forward packet
812        let (mut fwd_packet, mut openers) = create_packet(forward_hops, pseudonym, vec![return_hops; 2], &[])?;
813
814        assert_eq!(2, openers.len(), "invalid number of openers");
815        match &fwd_packet {
816            HoprPacket::Outgoing { .. } => {}
817            _ => bail!("invalid packet initial state"),
818        }
819
820        let mut received_surbs = vec![];
821        for hop in 1..=forward_hops + 1 {
822            fwd_packet = process_packet_at_node(forward_hops + 1, hop, false, fwd_packet, |_| None)
823                .context(format!("packet decoding failed at hop {hop}"))?;
824
825            match &fwd_packet {
826                HoprPacket::Final(incoming) => {
827                    assert_eq!(hop - 1, forward_hops, "final packet must be at the last hop");
828                    assert!(
829                        incoming.plain_text.is_empty(),
830                        "must not receive plaintext on surbs only packet"
831                    );
832                    assert!(incoming.ack_key.is_some(), "must not be a no-ack packet");
833                    assert_eq!(2, incoming.surbs.len(), "invalid number of received surbs per packet");
834                    assert_eq!(pseudonym, incoming.sender, "invalid sender");
835                    received_surbs.extend(incoming.surbs.clone());
836                }
837                HoprPacket::Forwarded(fwd) => {
838                    assert_eq!(PEERS[hop - 1].1.public(), &fwd.previous_hop, "invalid previous hop");
839                    assert_eq!(PEERS[hop + 1].1.public(), &fwd.outgoing.next_hop, "invalid next hop");
840                    assert_eq!(forward_hops + 1 - hop, fwd.path_pos as usize, "invalid path position");
841                }
842                HoprPacket::Outgoing { .. } => bail!("invalid packet state at hop {hop}"),
843            }
844        }
845
846        assert_eq!(2, received_surbs.len(), "invalid number of surbs");
847        for recv_surb in &received_surbs {
848            assert_eq!(
849                return_hops as u8 + 1,
850                recv_surb
851                    .1
852                    .additional_data_receiver
853                    .proof_of_relay_values()
854                    .chain_length(),
855                "surb has invalid por chain length"
856            );
857        }
858
859        let mut openers_fn = |p: &HoprSenderId| {
860            assert_eq!(p.pseudonym(), pseudonym);
861            let (id, opener) = openers.remove(0);
862            assert_eq!(id, p.surb_id());
863            Some(opener)
864        };
865
866        // The reply packet
867        for (i, recv_surb) in received_surbs.into_iter().enumerate() {
868            let re_msg = format!("some testing reply message {i}");
869            let mut re_packet = create_packet_from_surb(
870                forward_hops + 1,
871                recv_surb.0,
872                recv_surb.1,
873                &pseudonym,
874                re_msg.as_bytes(),
875            )?;
876
877            match &re_packet {
878                HoprPacket::Outgoing { .. } => {}
879                _ => bail!("invalid packet initial state in reply {i}"),
880            }
881
882            let mut received_re_plain_text = Box::default();
883            for hop in (0..=return_hops).rev() {
884                re_packet = process_packet_at_node(return_hops + 1, hop, true, re_packet, &mut openers_fn)
885                    .context(format!("packet decoding failed at hop {hop} in reply {i}"))?;
886
887                match &re_packet {
888                    HoprPacket::Final(incoming) => {
889                        assert_eq!(hop, 0, "final packet must be at the last hop for reply {i}");
890                        assert!(incoming.ack_key.is_some(), "must not be a no-ack packet");
891                        assert!(
892                            incoming.surbs.is_empty(),
893                            "must not receive surbs on reply for reply {i}"
894                        );
895                        received_re_plain_text = incoming.plain_text.clone();
896                    }
897                    HoprPacket::Forwarded(fwd) => {
898                        assert_eq!(
899                            PEERS[hop + 1].1.public(),
900                            &fwd.previous_hop,
901                            "invalid previous hop in reply {i}"
902                        );
903                        assert_eq!(
904                            PEERS[hop - 1].1.public(),
905                            &fwd.outgoing.next_hop,
906                            "invalid next hop in reply {i}"
907                        );
908                        assert_eq!(hop, fwd.path_pos as usize, "invalid path position in reply {i}");
909                    }
910                    HoprPacket::Outgoing(_) => bail!("invalid packet state at hop {hop} in reply {i}"),
911                }
912            }
913
914            assert_eq!(
915                received_re_plain_text.as_ref(),
916                re_msg.as_bytes(),
917                "invalid plaintext in reply {i}"
918            );
919        }
920        Ok(())
921    }
922}