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