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