Skip to main content

hopr_chain_connector/connector/
keys.rs

1use hopr_api::{
2    chain::{HoprKeyIdent, KeyIdMapping},
3    types::{crypto::prelude::OffchainPublicKey, primitive::prelude::Address},
4};
5
6use crate::{backend::Backend, connector::HoprBlockchainConnector, errors::ConnectorError};
7
8pub struct HoprKeyMapper<B> {
9    pub(crate) id_to_key: moka::sync::Cache<HoprKeyIdent, Option<OffchainPublicKey>, ahash::RandomState>,
10    pub(crate) key_to_id: moka::sync::Cache<OffchainPublicKey, Option<HoprKeyIdent>, ahash::RandomState>,
11    pub(crate) backend: std::sync::Arc<B>,
12}
13
14impl<B> Clone for HoprKeyMapper<B> {
15    fn clone(&self) -> Self {
16        Self {
17            id_to_key: self.id_to_key.clone(),
18            key_to_id: self.key_to_id.clone(),
19            backend: self.backend.clone(),
20        }
21    }
22}
23
24// These lookups run synchronously on Rayon threads (called from `HoprPacket::from_incoming`
25// inside `spawn_fifo_blocking`). The elapsed_ms timing in each init closure makes the rayon
26// execution time visible in structured logs.
27impl<B> KeyIdMapping<HoprKeyIdent, OffchainPublicKey> for HoprKeyMapper<B>
28where
29    B: Backend + Send + Sync + 'static,
30{
31    fn map_key_to_id(&self, key: &OffchainPublicKey) -> Option<HoprKeyIdent> {
32        self.key_to_id.get_with_by_ref(key, || {
33            let start = std::time::Instant::now();
34            tracing::warn!(%key, "cache miss on map_key_to_id");
35            let result = match self.backend.get_account_by_key(key) {
36                Ok(Some(account)) => Some(account.key_id),
37                Ok(None) => None,
38                Err(error) => {
39                    tracing::error!(%error, %key, "failed to get account by key");
40                    None
41                }
42            };
43            tracing::trace!(
44                %key,
45                elapsed_ms = start.elapsed().as_millis() as u64,
46                found = result.is_some(),
47                "map_key_to_id backend lookup"
48            );
49            result
50        })
51    }
52
53    fn map_id_to_public(&self, id: &HoprKeyIdent) -> Option<OffchainPublicKey> {
54        self.id_to_key.get_with_by_ref(id, || {
55            let start = std::time::Instant::now();
56            tracing::warn!(%id, "cache miss on map_id_to_public");
57            let result = match self.backend.get_account_by_id(id) {
58                Ok(Some(account)) => Some(account.public_key),
59                Ok(None) => None,
60                Err(error) => {
61                    tracing::error!(%error, %id, "failed to get account by id");
62                    None
63                }
64            };
65            tracing::trace!(
66                %id,
67                elapsed_ms = start.elapsed().as_millis() as u64,
68                found = result.is_some(),
69                "map_id_to_public backend lookup"
70            );
71            result
72        })
73    }
74}
75
76#[async_trait::async_trait]
77impl<B, C, P, R> hopr_api::chain::ChainKeyOperations for HoprBlockchainConnector<C, B, P, R>
78where
79    B: Backend + Send + Sync + 'static,
80    C: Send + Sync,
81    P: Send + Sync,
82    R: Send + Sync,
83{
84    type Error = ConnectorError;
85    type Mapper = HoprKeyMapper<B>;
86
87    async fn chain_key_to_packet_key(&self, chain: &Address) -> Result<Option<OffchainPublicKey>, Self::Error> {
88        self.check_connection_state()?;
89
90        let backend = self.backend.clone();
91        let chain_key = *chain;
92        Ok(self
93            .chain_to_packet
94            .try_get_with_by_ref(&chain_key, async move {
95                tracing::warn!(%chain_key, "cache miss on chain_key_to_packet_key");
96                match hopr_async_runtime::prelude::spawn_blocking(move || backend.get_account_by_address(&chain_key))
97                    .await
98                {
99                    Ok(Ok(value)) => Ok(value.map(|account| account.public_key)),
100                    Ok(Err(e)) => Err(ConnectorError::backend(e)),
101                    Err(e) => Err(ConnectorError::backend(e)),
102                }
103            })
104            .await?)
105    }
106
107    async fn packet_key_to_chain_key(&self, packet: &OffchainPublicKey) -> Result<Option<Address>, Self::Error> {
108        self.check_connection_state()?;
109
110        let backend = self.backend.clone();
111        let packet_key = *packet;
112        Ok(self
113            .packet_to_chain
114            .try_get_with_by_ref(&packet_key, async move {
115                tracing::warn!(
116                    peer_id = packet.to_peerid_str(),
117                    "cache miss on packet_key_to_chain_key"
118                );
119                match hopr_async_runtime::prelude::spawn_blocking(move || backend.get_account_by_key(&packet_key)).await
120                {
121                    Ok(Ok(value)) => Ok(value.map(|account| account.chain_addr)),
122                    Ok(Err(e)) => Err(ConnectorError::backend(e)),
123                    Err(e) => Err(ConnectorError::backend(e)),
124                }
125            })
126            .await?)
127    }
128
129    fn key_id_mapper_ref(&self) -> &Self::Mapper {
130        &self.mapper
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use hex_literal::hex;
137    use hopr_api::{
138        chain::ChainKeyOperations,
139        types::{crypto::prelude::*, internal::prelude::*, primitive::prelude::*},
140    };
141
142    use crate::{connector::tests::create_connector, testing::BlokliTestStateBuilder};
143
144    #[tokio::test]
145    async fn connector_should_map_keys_to_ids_and_back() -> anyhow::Result<()> {
146        let offchain_key = OffchainKeypair::from_secret(&hex!(
147            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
148        ))?;
149        let chain_addr: Address = [1u8; Address::SIZE].into();
150        let account = AccountEntry {
151            public_key: *offchain_key.public(),
152            chain_addr,
153            entry_type: AccountType::NotAnnounced,
154            safe_address: Some([2u8; Address::SIZE].into()),
155            key_id: 1.into(),
156        };
157
158        let blokli_client = BlokliTestStateBuilder::default()
159            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
160            .build_static_client();
161
162        let mut connector = create_connector(blokli_client)?;
163        connector.connect().await?;
164
165        assert_eq!(
166            Some(chain_addr),
167            connector.packet_key_to_chain_key(offchain_key.public()).await?
168        );
169        assert_eq!(
170            Some(*offchain_key.public()),
171            connector.chain_key_to_packet_key(&chain_addr).await?
172        );
173
174        let mapper = connector.key_id_mapper_ref();
175
176        assert_eq!(Some(account.key_id), mapper.map_key_to_id(offchain_key.public()));
177        assert_eq!(Some(*offchain_key.public()), mapper.map_id_to_public(&account.key_id));
178
179        Ok(())
180    }
181}