Skip to main content

hopr_crypto_packet/
expand.rs

1use hopr_types::crypto::{
2    errors::Result,
3    types::{ExpandedOffchainPublicKey, OffchainPublicKey},
4};
5
6use crate::{HoprSphinxSuite, sphinx::prelude::SphinxSuite};
7
8/// Supplies the expanded form of a packet key to the Sphinx sender path.
9///
10/// Packet keys are stored, routed and compared in their compact form, but deriving the shared
11/// secrets for an outgoing packet needs the decompressed point for every hop. That is the only
12/// place in the protocol where the expanded form is required, so it is reached through this trait
13/// rather than by widening the types that carry a path.
14///
15/// Expansion is CPU-intensive and a sender keeps using the same handful of relays, so
16/// implementations are free to memoize; see `CachingKeyExpander` in `hopr-protocol-hopr`.
17pub trait KeyExpander: Sync {
18    /// Returns the expanded form of `key`, failing if it is not a point on the curve.
19    fn expand(&self, key: &OffchainPublicKey) -> Result<ExpandedOffchainPublicKey>;
20
21    /// Expands an entire path, in order.
22    fn expand_path(&self, keys: &[OffchainPublicKey]) -> Result<Vec<ExpandedOffchainPublicKey>> {
23        keys.iter().map(|key| self.expand(key)).collect()
24    }
25}
26
27/// A [`KeyExpander`] that decompresses on every call.
28///
29/// Correct everywhere, but only appropriate where a key is expanded once: tests, benchmarks, and
30/// paths that are not reused. A node sending packets should memoize instead.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct DirectKeyExpander;
33
34impl KeyExpander for DirectKeyExpander {
35    fn expand(&self, key: &OffchainPublicKey) -> Result<ExpandedOffchainPublicKey> {
36        HoprSphinxSuite::expand_public(key)
37    }
38}
39
40impl<T: KeyExpander + ?Sized> KeyExpander for &T {
41    fn expand(&self, key: &OffchainPublicKey) -> Result<ExpandedOffchainPublicKey> {
42        (**self).expand(key)
43    }
44
45    // Forwarded so that an override of `expand_path` (batching, one lock per path) is not lost through a reference.
46    fn expand_path(&self, keys: &[OffchainPublicKey]) -> Result<Vec<ExpandedOffchainPublicKey>> {
47        (**self).expand_path(keys)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use std::sync::atomic::{AtomicUsize, Ordering};
54
55    use anyhow::Context;
56    use hopr_types::crypto::keypairs::{Keypair, OffchainKeypair};
57
58    use super::*;
59
60    /// Counts how each method is reached.
61    #[derive(Default)]
62    struct CountingExpander {
63        expand_calls: AtomicUsize,
64        expand_path_calls: AtomicUsize,
65    }
66
67    impl KeyExpander for CountingExpander {
68        fn expand(&self, key: &OffchainPublicKey) -> Result<ExpandedOffchainPublicKey> {
69            self.expand_calls.fetch_add(1, Ordering::Relaxed);
70            DirectKeyExpander.expand(key)
71        }
72
73        fn expand_path(&self, keys: &[OffchainPublicKey]) -> Result<Vec<ExpandedOffchainPublicKey>> {
74            self.expand_path_calls.fetch_add(1, Ordering::Relaxed);
75            keys.iter().map(|key| DirectKeyExpander.expand(key)).collect()
76        }
77    }
78
79    /// Takes the expander by value, so that `&CountingExpander` selects the blanket `&T` impl.
80    fn expand_with<K: KeyExpander>(expander: K, keys: &[OffchainPublicKey]) -> Result<Vec<ExpandedOffchainPublicKey>> {
81        expander.expand_path(keys)
82    }
83
84    fn path(len: usize) -> Vec<OffchainPublicKey> {
85        (0..len).map(|_| *OffchainKeypair::random().public()).collect()
86    }
87
88    #[test]
89    fn expanding_a_path_through_a_reference_reaches_the_override() -> anyhow::Result<()> {
90        let expander = CountingExpander::default();
91        let keys = path(3);
92
93        let expanded = expand_with(&expander, &keys).context("expanding the path through a reference")?;
94
95        assert_eq!(expanded.len(), 3);
96        assert_eq!(expander.expand_path_calls.load(Ordering::Relaxed), 1);
97        assert_eq!(
98            expander.expand_calls.load(Ordering::Relaxed),
99            0,
100            "the override must not fall back to per-key calls"
101        );
102        Ok(())
103    }
104
105    #[test]
106    fn the_default_expand_path_expands_every_key_in_order() -> anyhow::Result<()> {
107        let keys = path(4);
108
109        let expanded = DirectKeyExpander.expand_path(&keys).context("expanding the path")?;
110
111        for (key, expanded) in keys.iter().zip(&expanded) {
112            assert_eq!(&OffchainPublicKey::from(expanded), key);
113        }
114        Ok(())
115    }
116}