Skip to main content

hopr_crypto_packet/
packet.rs

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