hopr_internal_types/
announcement.rs

1use std::fmt::{Display, Formatter};
2
3use hopr_crypto_types::prelude::*;
4use hopr_primitive_types::prelude::*;
5use multiaddr::Multiaddr;
6use tracing::error;
7
8/// Holds the signed binding of the chain key and the packet key.
9///
10/// The signature is done via the offchain key to bind it with the on-chain key. The structure
11/// then makes it on-chain, making it effectively cross-signed with both keys (offchain and onchain).
12/// This is used to attest on-chain that node owns the corresponding packet key and links it with
13/// the chain key.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct KeyBinding {
16    pub chain_key: Address,
17    pub packet_key: OffchainPublicKey,
18    pub signature: OffchainSignature,
19}
20
21impl KeyBinding {
22    const SIGNING_SIZE: usize = 16 + Address::SIZE + OffchainPublicKey::SIZE;
23
24    fn prepare_for_signing(chain_key: &Address, packet_key: &OffchainPublicKey) -> [u8; Self::SIGNING_SIZE] {
25        let mut to_sign = [0u8; Self::SIGNING_SIZE];
26        to_sign[0..16].copy_from_slice(b"HOPR_KEY_BINDING");
27        to_sign[16..36].copy_from_slice(chain_key.as_ref());
28        to_sign[36..].copy_from_slice(packet_key.as_ref());
29        to_sign
30    }
31
32    /// Create and sign new key binding of the given chain key and packet key.
33    pub fn new(chain_key: Address, packet_key: &OffchainKeypair) -> Self {
34        let to_sign = Self::prepare_for_signing(&chain_key, packet_key.public());
35        Self {
36            chain_key,
37            packet_key: *packet_key.public(),
38            signature: OffchainSignature::sign_message(&to_sign, packet_key),
39        }
40    }
41}
42
43impl KeyBinding {
44    /// Re-construct binding from the chain key and packet key, while also verifying the given signature of the binding.
45    /// Fails if the signature is not valid for the given entries.
46    pub fn from_parts(
47        chain_key: Address,
48        packet_key: OffchainPublicKey,
49        signature: OffchainSignature,
50    ) -> crate::errors::Result<Self> {
51        let to_verify = Self::prepare_for_signing(&chain_key, &packet_key);
52        signature
53            .verify_message(&to_verify, &packet_key)
54            .then_some(Self {
55                chain_key,
56                packet_key,
57                signature,
58            })
59            .ok_or(CryptoError::SignatureVerification.into())
60    }
61}
62
63impl Display for KeyBinding {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        write!(f, "keybinding {} <-> {}", self.chain_key, self.packet_key)
66    }
67}
68
69/// Decapsulates the multiaddress (= strips the /p2p/<peer_id> suffix).
70/// If it is already decapsulated, the function is an identity.
71pub fn decapsulate_multiaddress(multiaddr: Multiaddr) -> Multiaddr {
72    multiaddr
73        .into_iter()
74        .take_while(|p| !matches!(p, multiaddr::Protocol::P2p(_)))
75        .collect()
76}
77
78/// Structure containing data used for an on-chain announcement.
79/// That is an optional decapsulated multiaddress (with the `/p2p/{peer_id}` suffix removed) and
80/// mandatory keybinding [`KeyBinding`].
81///
82/// NOTE: This currently supports only announcing of a single multiaddress
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct AnnouncementData {
85    multiaddress: Option<Multiaddr>,
86    key_binding: KeyBinding,
87}
88
89impl AnnouncementData {
90    /// Constructs structure from optional `multiaddress` and also `KeyBinding`.
91    ///
92    /// The `multiaddress` must not be empty if present. It should be the external address of the node.
93    /// It may contain a trailing PeerId (encapsulated multiaddr) or come without. If the
94    /// peerId is present, it must match with the keybinding.
95    pub fn new(key_binding: KeyBinding, multiaddress: Option<Multiaddr>) -> Result<Self, GeneralError> {
96        if let Some(multiaddress) = multiaddress {
97            if multiaddress.is_empty() {
98                error!("Received empty multiaddr");
99                return Err(GeneralError::InvalidInput);
100            }
101            // Encapsulate first (if already encapsulated, the operation verifies that peer id matches the given one)
102            match multiaddress.with_p2p(key_binding.packet_key.into()) {
103                Ok(mut multiaddress) => {
104                    // Now decapsulate again, because we store decapsulated multiaddress only (without the
105                    // /p2p/<peer_id> suffix)
106                    multiaddress.pop();
107                    Ok(Self {
108                        multiaddress: Some(multiaddress),
109                        key_binding,
110                    })
111                }
112                Err(multiaddress) => Err(GeneralError::NonSpecificError(format!(
113                    "{multiaddress} does not match the keybinding {} peer id",
114                    key_binding.packet_key.to_peerid_str()
115                ))),
116            }
117        } else {
118            Ok(Self {
119                multiaddress: None,
120                key_binding,
121            })
122        }
123    }
124
125    /// Returns the optional multiaddress associated with this announcement.
126    ///
127    /// Note that the returned multiaddress is *always* decapsulated (= without the /p2p/<peer_id> suffix)
128    pub fn multiaddress(&self) -> Option<&Multiaddr> {
129        self.multiaddress.as_ref()
130    }
131
132    /// Returns the key binding with this announcement.
133    pub fn key_binding(&self) -> &KeyBinding {
134        &self.key_binding
135    }
136}
137
138impl Display for AnnouncementData {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        if let Some(multiaddr) = &self.multiaddress {
141            write!(f, "announcement of {multiaddr} with {}", self.key_binding)
142        } else {
143            write!(f, "announcement of key binding only {}", self.key_binding)
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use hex_literal::hex;
151    use hopr_crypto_types::keypairs::{Keypair, OffchainKeypair};
152    use hopr_primitive_types::primitives::Address;
153    use multiaddr::Multiaddr;
154
155    use crate::{
156        announcement::{AnnouncementData, KeyBinding},
157        prelude::decapsulate_multiaddress,
158    };
159
160    lazy_static::lazy_static! {
161        static ref KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d")).expect("lazy static keypair should be constructible");
162        static ref CHAIN_ADDR: Address = Address::try_from(hex!("78392d47e3522219e2802e7d6c45ee84b5d5c185").as_ref()).expect("lazy static address should be constructible");
163        static ref SECOND_KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299")).expect("lazy static keypair should be constructible");
164    }
165
166    #[test]
167    fn test_key_binding() -> anyhow::Result<()> {
168        let kb_1 = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
169        let kb_2 = KeyBinding::from_parts(kb_1.chain_key, kb_1.packet_key, kb_1.signature.clone())?;
170
171        assert_eq!(kb_1, kb_2, "must be equal");
172
173        Ok(())
174    }
175
176    #[test]
177    fn test_announcement() -> anyhow::Result<()> {
178        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
179        let peer_id = KEY_PAIR.public().to_peerid_str();
180
181        for (ma_str, decapsulated_ma_str) in vec![
182            (
183                format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}"),
184                "/ip4/127.0.0.1/tcp/10000".to_string(),
185            ),
186            (
187                format!("/ip6/::1/tcp/10000/p2p/{peer_id}"),
188                "/ip6/::1/tcp/10000".to_string(),
189            ),
190            (
191                format!("/dns4/hoprnet.org/tcp/10000/p2p/{peer_id}"),
192                "/dns4/hoprnet.org/tcp/10000".to_string(),
193            ),
194            (
195                format!("/dns6/hoprnet.org/tcp/10000/p2p/{peer_id}"),
196                "/dns6/hoprnet.org/tcp/10000".to_string(),
197            ),
198            (
199                format!("/ip4/127.0.0.1/udp/10000/quic/p2p/{peer_id}"),
200                "/ip4/127.0.0.1/udp/10000/quic".to_string(),
201            ),
202        ] {
203            let maddr: Multiaddr = ma_str.parse()?;
204
205            let ad = AnnouncementData::new(key_binding.clone(), Some(maddr))?;
206            assert_eq!(Some(decapsulated_ma_str), ad.multiaddress().map(|m| m.to_string()));
207            assert_eq!(&key_binding, ad.key_binding());
208        }
209
210        Ok(())
211    }
212
213    #[test]
214    fn test_announcement_no_keybinding() -> anyhow::Result<()> {
215        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
216        let ad = AnnouncementData::new(key_binding, None)?;
217
218        assert_eq!(None, ad.multiaddress());
219
220        Ok(())
221    }
222
223    #[test]
224    fn test_announcement_decapsulated_ma() -> anyhow::Result<()> {
225        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
226        let maddr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".to_string().parse()?;
227
228        let ad = AnnouncementData::new(key_binding.clone(), Some(maddr))?;
229        assert_eq!(
230            Some("/ip4/127.0.0.1/tcp/10000".to_string()),
231            ad.multiaddress().map(|m| m.to_string())
232        );
233        assert_eq!(&key_binding, ad.key_binding());
234
235        Ok(())
236    }
237
238    #[test]
239    fn test_announcement_wrong_peerid() -> anyhow::Result<()> {
240        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
241        let peer_id = SECOND_KEY_PAIR.public().to_peerid_str();
242        let maddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}").parse()?;
243
244        assert!(AnnouncementData::new(key_binding, Some(maddr)).is_err());
245
246        Ok(())
247    }
248
249    #[test]
250    fn test_decapsulate_multiaddr() -> anyhow::Result<()> {
251        let maddr_1: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse()?;
252        let maddr_2 = maddr_1
253            .clone()
254            .with_p2p(OffchainKeypair::random().public().into())
255            .map_err(|e| anyhow::anyhow!(e.to_string()))?;
256
257        assert_eq!(maddr_1, decapsulate_multiaddress(maddr_2), "multiaddresses must match");
258        assert_eq!(
259            maddr_1,
260            decapsulate_multiaddress(maddr_1.clone()),
261            "decapsulation must be idempotent"
262        );
263
264        Ok(())
265    }
266}