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