hopr_crypto_sphinx/
routing.rs

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