Skip to main content

hopr_crypto_packet/sphinx/
routing.rs

1use std::{
2    fmt::{Debug, Formatter},
3    marker::PhantomData,
4    num::NonZeroUsize,
5};
6
7use hopr_types::{
8    crypto::{
9        crypto_traits::{StreamCipher, StreamCipherSeek, UniversalHash},
10        prelude::*,
11        types::Pseudonym,
12    },
13    crypto_random::random_fill,
14    primitive::{prelude::*, typenum::Unsigned},
15};
16
17use super::{
18    derivation::{generate_key, generate_key_iv},
19    shared_keys::SharedSecret,
20};
21
22/// Current version of the header
23const SPHINX_HEADER_VERSION: u8 = 1;
24
25const HASH_KEY_PRG: &str = "HASH_KEY_PRG";
26
27const HASH_KEY_TAG: &str = "HASH_KEY_TAG";
28
29/// Contains the necessary size and type specifications for the Sphinx packet header.
30pub trait SphinxHeaderSpec {
31    /// Maximum number of hops.
32    const MAX_HOPS: NonZeroUsize;
33
34    /// Public key identifier type.
35    type KeyId: BytesRepresentable + Clone;
36
37    /// Pseudonym used to represent node for SURBs.
38    type Pseudonym: Pseudonym;
39
40    /// Type representing additional data for relayers.
41    type RelayerData: BytesRepresentable;
42
43    /// Type representing additional data delivered to the packet receiver.
44    ///
45    /// It is delivered on both forward and return paths.
46    type PacketReceiverData: BytesRepresentable;
47
48    /// Type representing additional data delivered with each SURB to the packet receiver.
49    ///
50    /// It is delivered only on the forward path.
51    type SurbReceiverData: BytesRepresentable;
52
53    /// Pseudo-Random Generator function used to encrypt and decrypt the Sphinx header.
54    type PRG: crypto_traits::StreamCipher + crypto_traits::StreamCipherSeek + crypto_traits::KeyIvInit;
55
56    /// One-time authenticator used for Sphinx header tag.
57    type UH: crypto_traits::UniversalHash + crypto_traits::KeyInit;
58
59    /// Size of the additional data for relayers.
60    const RELAYER_DATA_SIZE: usize = Self::RelayerData::SIZE;
61
62    /// Size of the additional data included in SURBs.
63    const SURB_RECEIVER_DATA_SIZE: usize = Self::SurbReceiverData::SIZE;
64
65    /// Size of the additional data for the packet receiver.
66    const RECEIVER_DATA_SIZE: usize = Self::PacketReceiverData::SIZE;
67
68    /// Size of the public key identifier
69    const KEY_ID_SIZE: NonZeroUsize = NonZeroUsize::new(Self::KeyId::SIZE).unwrap();
70
71    /// Size of the one-time authenticator tag.
72    const TAG_SIZE: usize = <Self::UH as crypto_traits::BlockSizeUser>::BlockSize::USIZE;
73
74    /// Length of the header routing information per hop.
75    ///
76    /// **The value shall not be overridden**.
77    const ROUTING_INFO_LEN: usize =
78        HeaderPrefix::SIZE + Self::KEY_ID_SIZE.get() + Self::TAG_SIZE + Self::RELAYER_DATA_SIZE;
79
80    /// Length of the whole Sphinx header.
81    ///
82    /// **The value shall not be overridden**.
83    const HEADER_LEN: usize =
84        HeaderPrefix::SIZE + Self::RECEIVER_DATA_SIZE + (Self::MAX_HOPS.get() - 1) * Self::ROUTING_INFO_LEN;
85
86    /// Extended header size used for computations.
87    ///
88    /// **The value shall not be overridden**.
89    const EXT_HEADER_LEN: usize =
90        HeaderPrefix::SIZE + Self::RECEIVER_DATA_SIZE + Self::MAX_HOPS.get() * Self::ROUTING_INFO_LEN;
91
92    fn generate_filler(secrets: &[SharedSecret]) -> hopr_types::crypto::errors::Result<Box<[u8]>> {
93        if secrets.len() < 2 {
94            return Ok(vec![].into_boxed_slice());
95        }
96
97        if secrets.len() > Self::MAX_HOPS.into() {
98            return Err(CryptoError::InvalidInputValue("secrets.len"));
99        }
100
101        let padding_len = (Self::MAX_HOPS.get() - secrets.len()) * Self::ROUTING_INFO_LEN;
102
103        let mut ret = vec![0u8; Self::HEADER_LEN - padding_len - Self::Pseudonym::SIZE - 1];
104        let mut length = Self::ROUTING_INFO_LEN;
105        let mut start = Self::HEADER_LEN;
106
107        for secret in secrets.iter().take(secrets.len() - 1) {
108            let mut prg = Self::new_prg(secret)?;
109            prg.seek(start);
110            prg.apply_keystream(&mut ret[0..length]);
111
112            length += Self::ROUTING_INFO_LEN;
113            start -= Self::ROUTING_INFO_LEN;
114        }
115
116        Ok(ret.into_boxed_slice())
117    }
118
119    /// Instantiates a new Pseudo-Random Generator.
120    fn new_prg(secret: &SecretKey) -> hopr_types::crypto::errors::Result<Self::PRG> {
121        generate_key_iv(secret, HASH_KEY_PRG, None)
122    }
123}
124
125/// Sphinx header byte prefix
126///
127/// ### Layout (MSB first)
128/// `Version (3 bits), No Ack flag (1 bit), Reply flag (1 bit), Path position (3 bits)`
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130struct HeaderPrefix(u8);
131
132impl HeaderPrefix {
133    pub const SIZE: usize = 1;
134
135    pub fn new(is_reply: bool, no_ack: bool, path_pos: u8) -> Result<Self, GeneralError> {
136        // Due to size restriction, we do not allow greater than 7 hop paths.
137        if path_pos > 7 {
138            return Err(GeneralError::ParseError("HeaderPrefixByte".into()));
139        }
140
141        let mut out = 0;
142        out |= (SPHINX_HEADER_VERSION & 0x07) << 5;
143        out |= (no_ack as u8) << 4;
144        out |= (is_reply as u8) << 3;
145        out |= path_pos & 0x07;
146        Ok(Self(out))
147    }
148
149    #[inline]
150    pub fn is_reply(&self) -> bool {
151        (self.0 & 0x08) != 0
152    }
153
154    #[inline]
155    pub fn is_no_ack(&self) -> bool {
156        (self.0 & 0x10) != 0
157    }
158
159    #[inline]
160    pub fn path_position(&self) -> u8 {
161        self.0 & 0x07
162    }
163
164    #[inline]
165    pub fn is_final_hop(&self) -> bool {
166        self.path_position() == 0
167    }
168}
169
170impl From<HeaderPrefix> for u8 {
171    fn from(value: HeaderPrefix) -> Self {
172        value.0
173    }
174}
175
176impl TryFrom<u8> for HeaderPrefix {
177    type Error = GeneralError;
178
179    fn try_from(value: u8) -> Result<Self, Self::Error> {
180        if (value & 0xe0) >> 5 == SPHINX_HEADER_VERSION {
181            Ok(Self(value))
182        } else {
183            Err(GeneralError::ParseError("invalid header version".into()))
184        }
185    }
186}
187
188/// Carries routing information for the mixnet packet.
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190pub struct RoutingInfo<H: SphinxHeaderSpec>(Box<[u8]>, PhantomData<H>);
191
192impl<H: SphinxHeaderSpec> Debug for RoutingInfo<H> {
193    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
194        write!(f, "{}", self.to_hex())
195    }
196}
197
198impl<H: SphinxHeaderSpec> Clone for RoutingInfo<H> {
199    fn clone(&self) -> Self {
200        Self(self.0.clone(), PhantomData)
201    }
202}
203
204impl<H: SphinxHeaderSpec> PartialEq for RoutingInfo<H> {
205    fn eq(&self, other: &Self) -> bool {
206        self.0 == other.0
207    }
208}
209
210impl<H: SphinxHeaderSpec> Eq for RoutingInfo<H> {}
211
212impl<H: SphinxHeaderSpec> Default for RoutingInfo<H> {
213    fn default() -> Self {
214        Self(vec![0u8; Self::SIZE].into_boxed_slice(), PhantomData)
215    }
216}
217
218impl<H: SphinxHeaderSpec> AsRef<[u8]> for RoutingInfo<H> {
219    fn as_ref(&self) -> &[u8] {
220        &self.0
221    }
222}
223
224impl<'a, H: SphinxHeaderSpec> TryFrom<&'a [u8]> for RoutingInfo<H> {
225    type Error = GeneralError;
226
227    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
228        if value.len() == Self::SIZE {
229            Ok(Self(value.into(), PhantomData))
230        } else {
231            Err(GeneralError::ParseError("RoutingInfo".into()))
232        }
233    }
234}
235
236impl<H: SphinxHeaderSpec> BytesRepresentable for RoutingInfo<H> {
237    const SIZE: usize = H::HEADER_LEN + H::TAG_SIZE;
238}
239
240impl<H: SphinxHeaderSpec> RoutingInfo<H> {
241    /// Creates the routing information of the mixnet packet.
242    ///
243    /// # Arguments
244    /// * `path` IDs of the nodes along the path (usually its public key or public key identifier).
245    /// * `secrets` shared secrets with the nodes along the path
246    /// * `additional_data_relayer` additional data for each relayer
247    /// * `receiver_data` data for the packet receiver (this usually contains also `H::Pseudonym`).
248    /// * `is_reply` flag indicating whether this is a reply packet
249    /// * `no_ack` special flag used for acknowledgement signaling to the recipient
250    pub fn new(
251        path: &[H::KeyId],
252        secrets: &[SharedSecret],
253        additional_data_relayer: &[H::RelayerData],
254        receiver_data: &H::PacketReceiverData,
255        is_reply: bool,
256        no_ack: bool,
257    ) -> hopr_types::crypto::errors::Result<Self> {
258        assert!(H::MAX_HOPS.get() <= 7, "maximum number of hops supported is 7");
259
260        if path.len() != secrets.len() {
261            return Err(CryptoError::InvalidParameterSize {
262                name: "path",
263                expected: secrets.len(),
264            });
265        }
266
267        if secrets.len() > H::MAX_HOPS.get() {
268            return Err(CryptoError::InvalidInputValue("secrets.len"));
269        }
270
271        let mut extended_header = vec![0u8; H::EXT_HEADER_LEN];
272        let mut ret = RoutingInfo::default();
273
274        for idx in 0..secrets.len() {
275            let inverted_idx = secrets.len() - idx - 1;
276            let prefix = HeaderPrefix::new(is_reply, no_ack, idx as u8)?;
277
278            let mut prg = H::new_prg(&secrets[inverted_idx])?;
279
280            if idx == 0 {
281                // Prefix byte
282                extended_header[0] = prefix.into();
283
284                // Last hop additional data
285                extended_header[HeaderPrefix::SIZE..HeaderPrefix::SIZE + H::PacketReceiverData::SIZE]
286                    .copy_from_slice(receiver_data.as_ref());
287
288                // Random padding for the rest of the extended header
289                let padding_len = (H::MAX_HOPS.get() - secrets.len()) * H::ROUTING_INFO_LEN;
290                if padding_len > 0 {
291                    random_fill(
292                        &mut extended_header[HeaderPrefix::SIZE + H::PacketReceiverData::SIZE
293                            ..HeaderPrefix::SIZE + H::PacketReceiverData::SIZE + padding_len],
294                    );
295                }
296
297                // Encrypt last hop data and padding
298                prg.apply_keystream(
299                    &mut extended_header[0..HeaderPrefix::SIZE + H::PacketReceiverData::SIZE + padding_len],
300                );
301
302                if secrets.len() > 1 {
303                    let filler = H::generate_filler(secrets)?;
304                    extended_header[HeaderPrefix::SIZE + H::PacketReceiverData::SIZE + padding_len
305                        ..HeaderPrefix::SIZE + H::PacketReceiverData::SIZE + padding_len + filler.len()]
306                        .copy_from_slice(&filler);
307                }
308            } else {
309                // Shift everything to the right to make space for the next hop's routing info
310                extended_header.copy_within(0..H::HEADER_LEN, H::ROUTING_INFO_LEN);
311
312                // Prefix byte must come first to ensure prefix RELAYER_END_PREFIX prefix safety
313                // of Ed25519 public keys.
314                extended_header[0] = prefix.into();
315
316                // Each public key identifier must have an equal length
317                let key_ident = path[inverted_idx + 1].as_ref();
318                if key_ident.len() != H::KEY_ID_SIZE.get() {
319                    return Err(CryptoError::InvalidParameterSize {
320                        name: "path[..]",
321                        expected: H::KEY_ID_SIZE.into(),
322                    });
323                }
324                // Copy the public key identifier
325                extended_header[HeaderPrefix::SIZE..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get()]
326                    .copy_from_slice(key_ident);
327
328                // Include the last computed authentication tag
329                extended_header[HeaderPrefix::SIZE + H::KEY_ID_SIZE.get()
330                    ..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE]
331                    .copy_from_slice(ret.mac());
332
333                // The additional relayer data is optional
334                if H::RELAYER_DATA_SIZE > 0
335                    && let Some(relayer_data) = additional_data_relayer.get(inverted_idx).map(|d| d.as_ref())
336                {
337                    if relayer_data.len() != H::RELAYER_DATA_SIZE {
338                        return Err(CryptoError::InvalidParameterSize {
339                            name: "additional_data_relayer[..]",
340                            expected: H::RELAYER_DATA_SIZE,
341                        });
342                    }
343
344                    extended_header[HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE
345                        ..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE + H::RELAYER_DATA_SIZE]
346                        .copy_from_slice(relayer_data);
347                }
348
349                // Encrypt the entire extended header
350                prg.apply_keystream(&mut extended_header[0..H::HEADER_LEN]);
351            }
352
353            let mut uh: H::UH = generate_key(&secrets[inverted_idx], HASH_KEY_TAG, None)
354                .map_err(|_| CryptoError::InvalidInputValue("mac_key"))?;
355            uh.update_padded(&extended_header[0..H::HEADER_LEN]);
356            ret.mac_mut().copy_from_slice(&uh.finalize());
357        }
358
359        ret.routing_mut().copy_from_slice(&extended_header[0..H::HEADER_LEN]);
360        Ok(ret)
361    }
362
363    fn mac(&self) -> &[u8] {
364        &self.0[H::HEADER_LEN..H::HEADER_LEN + H::TAG_SIZE]
365    }
366
367    fn routing_mut(&mut self) -> &mut [u8] {
368        &mut self.0[0..H::HEADER_LEN]
369    }
370
371    fn mac_mut(&mut self) -> &mut [u8] {
372        &mut self.0[H::HEADER_LEN..H::HEADER_LEN + H::TAG_SIZE]
373    }
374}
375
376/// Enum carry information about the packet based on whether it is destined for the current node (`FinalNode`)
377/// or if the packet is supposed to be only relayed (`RelayNode`).
378pub enum ForwardedHeader<H: SphinxHeaderSpec> {
379    /// The packet is supposed to be relayed
380    Relayed {
381        /// Transformed header
382        next_header: RoutingInfo<H>,
383        /// Position of the relay in the path
384        path_pos: u8,
385        /// Public key of the next node
386        next_node: H::KeyId,
387        /// Additional data for the relayer
388        additional_info: H::RelayerData,
389    },
390
391    /// The packet is at its final destination
392    Final {
393        /// Data from the sender to the packet receiver.
394        /// This usually contains also `H::Pseudonym`.
395        receiver_data: H::PacketReceiverData,
396        /// Indicates whether this message is a reply and a [`ReplyOpener`](super::surb::ReplyOpener)
397        /// should be used to further decrypt the message.
398        is_reply: bool,
399        /// Special flag used for acknowledgement signaling.
400        no_ack: bool,
401    },
402}
403
404/// Applies the forward transformation to the header.
405/// If the packet is destined for this node, it returns the additional data
406/// for the final destination ([`ForwardedHeader::Final`]).
407/// Otherwise, it returns the transformed header, the
408/// next authentication tag, the public key of the next node, and the additional data
409/// for the relayer ([`ForwardedHeader::Relayed`]).
410///
411/// # Arguments
412/// * `secret` - the shared secret with the creator of the packet
413/// * `header` - entire sphinx header to be forwarded
414pub fn forward_header<H: SphinxHeaderSpec>(
415    secret: &SecretKey,
416    header: &mut [u8],
417) -> hopr_types::crypto::errors::Result<ForwardedHeader<H>> {
418    if header.len() != RoutingInfo::<H>::SIZE {
419        return Err(CryptoError::InvalidParameterSize {
420            name: "header",
421            expected: H::HEADER_LEN,
422        });
423    }
424
425    // Compute and verify the authentication tag
426    let mut uh: H::UH =
427        generate_key(secret, HASH_KEY_TAG, None).map_err(|_| CryptoError::InvalidInputValue("mac_key"))?;
428    uh.update_padded(&header[0..H::HEADER_LEN]);
429    #[allow(deprecated)]
430    uh.verify(hopr_types::crypto::crypto_traits::Block::<H::UH>::from_slice(
431        &header[H::HEADER_LEN..H::HEADER_LEN + H::TAG_SIZE],
432    ))
433    .map_err(|_| CryptoError::TagMismatch)?;
434
435    // Decrypt the header using the key=stream
436    let mut prg = H::new_prg(secret)?;
437    prg.apply_keystream(&mut header[0..H::HEADER_LEN]);
438
439    let prefix = HeaderPrefix::try_from(header[0])?;
440
441    if !prefix.is_final_hop() {
442        // Try to deserialize the public key to validate it
443        let next_node = (&header[HeaderPrefix::SIZE..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get()])
444            .try_into()
445            .map_err(|_| CryptoError::InvalidInputValue("next_node"))?;
446
447        let mut next_header = RoutingInfo::<H>::default();
448
449        // Authentication tag
450        next_header.mac_mut().copy_from_slice(
451            &header[HeaderPrefix::SIZE + H::KEY_ID_SIZE.get()..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE],
452        );
453
454        // Optional additional relayer data
455        let additional_info = (&header[HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE
456            ..HeaderPrefix::SIZE + H::KEY_ID_SIZE.get() + H::TAG_SIZE + H::RELAYER_DATA_SIZE])
457            .try_into()
458            .map_err(|_| CryptoError::InvalidInputValue("additional_relayer_data"))?;
459
460        // Shift the entire header to the left to discard the data we just read
461        header.copy_within(H::ROUTING_INFO_LEN..H::HEADER_LEN, 0);
462
463        // Erase the read data from the header to apply the raw key-stream
464        header[H::HEADER_LEN - H::ROUTING_INFO_LEN..H::HEADER_LEN].fill(0);
465        prg.seek(H::HEADER_LEN);
466        prg.apply_keystream(&mut header[H::HEADER_LEN - H::ROUTING_INFO_LEN..H::HEADER_LEN]);
467
468        next_header.routing_mut().copy_from_slice(&header[0..H::HEADER_LEN]);
469
470        Ok(ForwardedHeader::Relayed {
471            next_header,
472            path_pos: prefix.path_position(),
473            next_node,
474            additional_info,
475        })
476    } else {
477        Ok(ForwardedHeader::Final {
478            receiver_data: (&header[HeaderPrefix::SIZE..HeaderPrefix::SIZE + H::PacketReceiverData::SIZE])
479                .try_into()
480                .map_err(|_| CryptoError::InvalidInputValue("receiver_data"))?,
481            is_reply: prefix.is_reply(),
482            no_ack: prefix.is_no_ack(),
483        })
484    }
485}
486
487#[cfg(test)]
488pub(crate) mod tests {
489    use hopr_types::{
490        crypto::{crypto_traits::BlockSizeUser, keypairs::OffchainKeypair},
491        crypto_random::Randomizable,
492    };
493    use parameterized::parameterized;
494
495    use super::{
496        super::{
497            shared_keys::{Alpha, GroupElement, SphinxSuite},
498            tests::*,
499        },
500        *,
501    };
502
503    #[test]
504    fn test_filler_generate_verify() -> anyhow::Result<()> {
505        let per_hop = 3 + OffchainPublicKey::SIZE + <Poly1305 as BlockSizeUser>::BlockSize::USIZE + 1;
506        let last_hop = SimplePseudonym::SIZE;
507        let max_hops = 4;
508
509        let secrets = (0..max_hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
510        let extended_header_len = per_hop * max_hops + last_hop + 1;
511        let header_len = per_hop * (max_hops - 1) + last_hop + 1;
512
513        let mut extended_header = vec![0u8; extended_header_len];
514
515        let filler = TestSpec::<OffchainPublicKey, 4, 3>::generate_filler(&secrets)?;
516
517        extended_header[1 + last_hop..1 + last_hop + filler.len()].copy_from_slice(&filler);
518        extended_header.copy_within(0..header_len, per_hop);
519
520        for i in 0..max_hops - 1 {
521            let idx = secrets.len() - i - 2;
522
523            let mut prg = generate_key_iv::<ChaCha20, _>(&secrets[idx], HASH_KEY_PRG, None)?;
524            prg.apply_keystream(&mut extended_header);
525
526            let mut erased = extended_header.clone();
527            erased[header_len..].iter_mut().for_each(|x| *x = 0);
528            assert_eq!(erased, extended_header, "xor blinding must erase last bits {i}");
529
530            extended_header.copy_within(0..header_len, per_hop);
531        }
532
533        Ok(())
534    }
535
536    #[test]
537    fn test_filler_edge_case() -> anyhow::Result<()> {
538        let hops = 1;
539
540        let secrets = (0..hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
541
542        let first_filler = TestSpec::<OffchainPublicKey, 1, 0>::generate_filler(&secrets)?;
543        assert_eq!(0, first_filler.len());
544
545        Ok(())
546    }
547
548    fn generic_test_generate_routing_info_and_forward<S>(keypairs: Vec<S::P>, reply: bool) -> anyhow::Result<()>
549    where
550        S: SphinxSuite,
551        for<'a> &'a Alpha<<S::G as GroupElement<S::E>>::AlphaLen>: From<&'a <S::P as Keypair>::Public>,
552    {
553        let pub_keys = keypairs.iter().map(|kp| kp.public().clone()).collect::<Vec<_>>();
554        let shares = S::new_shared_keys(&pub_keys)?;
555        let pseudonym = SimplePseudonym::random();
556        let no_ack_flag = true;
557
558        let mut rinfo = RoutingInfo::<TestSpec<<S::P as Keypair>::Public, 3, 0>>::new(
559            &pub_keys,
560            &shares.secrets,
561            &[],
562            &pseudonym,
563            reply,
564            no_ack_flag,
565        )?;
566
567        for (i, secret) in shares.secrets.iter().enumerate() {
568            let fwd = forward_header::<TestSpec<<S::P as Keypair>::Public, 3, 0>>(secret, &mut rinfo.0)?;
569
570            match fwd {
571                ForwardedHeader::Relayed {
572                    next_header,
573                    next_node,
574                    path_pos,
575                    ..
576                } => {
577                    rinfo = next_header;
578                    assert!(i < shares.secrets.len() - 1, "cannot be a relay node");
579                    assert_eq!(
580                        path_pos,
581                        (shares.secrets.len() - i - 1) as u8,
582                        "invalid path position {path_pos}"
583                    );
584                    assert_eq!(
585                        pub_keys[i + 1].as_ref(),
586                        next_node.as_ref(),
587                        "invalid public key of the next node"
588                    );
589                }
590                ForwardedHeader::Final {
591                    receiver_data,
592                    is_reply,
593                    no_ack,
594                } => {
595                    assert_eq!(shares.secrets.len() - 1, i, "cannot be a final node");
596                    assert_eq!(pseudonym, receiver_data, "invalid pseudonym");
597                    assert_eq!(is_reply, reply, "invalid reply flag");
598                    assert_eq!(no_ack, no_ack_flag, "invalid no_ack flag");
599                }
600            }
601        }
602
603        Ok(())
604    }
605
606    #[cfg(feature = "ed25519")]
607    #[parameterized(amount = { 3, 2, 1, 3, 2, 1 }, reply = { true, true, true, false, false, false })]
608    fn test_ed25519_generate_routing_info_and_forward(amount: usize, reply: bool) -> anyhow::Result<()> {
609        generic_test_generate_routing_info_and_forward::<crate::sphinx::ec_groups::Ed25519Suite>(
610            (0..amount).map(|_| OffchainKeypair::random()).collect(),
611            reply,
612        )
613    }
614
615    #[cfg(feature = "x25519")]
616    #[parameterized(amount = { 3, 2, 1, 3, 2, 1 }, reply = { true, true, true, false, false, false })]
617    fn test_x25519_generate_routing_info_and_forward(amount: usize, reply: bool) -> anyhow::Result<()> {
618        generic_test_generate_routing_info_and_forward::<crate::sphinx::ec_groups::X25519Suite>(
619            (0..amount).map(|_| OffchainKeypair::random()).collect(),
620            reply,
621        )
622    }
623
624    #[cfg(feature = "secp256k1")]
625    #[parameterized(amount = { 3, 2, 1, 3, 2, 1 }, reply = { true, true, true, false, false, false })]
626    fn test_secp256k1_generate_routing_info_and_forward(amount: usize, reply: bool) -> anyhow::Result<()> {
627        generic_test_generate_routing_info_and_forward::<crate::sphinx::ec_groups::Secp256k1Suite>(
628            (0..amount).map(|_| ChainKeypair::random()).collect(),
629            reply,
630        )
631    }
632}