Skip to main content

hopr_crypto_packet/
packet.rs

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