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    fn chain_key_to_packet_key(&self, chain: &Address) -> Result<Option<OffchainPublicKey>, Self::Error> {
88        self.check_connection_state()?;
89
90        Ok(self.chain_to_packet.try_get_with_by_ref(chain, || {
91            tracing::warn!(%chain, "cache miss on chain_key_to_packet_key");
92            self.backend
93                .get_account_by_address(chain)
94                .map(|a| a.map(|ac| ac.public_key))
95                .map_err(ConnectorError::backend)
96        })?)
97    }
98
99    fn packet_key_to_chain_key(&self, packet: &OffchainPublicKey) -> Result<Option<Address>, Self::Error> {
100        self.check_connection_state()?;
101
102        Ok(self.packet_to_chain.try_get_with_by_ref(packet, || {
103            tracing::warn!(
104                peer_id = packet.to_peerid_str(),
105                "cache miss on packet_key_to_chain_key"
106            );
107            self.backend
108                .get_account_by_key(packet)
109                .map(|a| a.map(|ac| ac.chain_addr))
110                .map_err(ConnectorError::backend)
111        })?)
112    }
113
114    fn key_id_mapper_ref(&self) -> &Self::Mapper {
115        &self.mapper
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use hex_literal::hex;
122    use hopr_api::{
123        chain::{ChainKeyOperations, HoprKeyIdent, KeyIdMapping},
124        types::{crypto::prelude::*, internal::prelude::*, primitive::prelude::*},
125    };
126
127    use crate::{
128        backend::Backend, connector::tests::create_connector, errors::ConnectorError, testing::BlokliTestStateBuilder,
129    };
130
131    #[tokio::test]
132    async fn connector_should_map_keys_to_ids_and_back() -> anyhow::Result<()> {
133        let offchain_key = OffchainKeypair::from_secret(&hex!(
134            "60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d"
135        ))?;
136        let chain_addr: Address = [1u8; Address::SIZE].into();
137        let account = AccountEntry {
138            public_key: *offchain_key.public(),
139            chain_addr,
140            entry_type: AccountType::NotAnnounced,
141            safe_address: Some([2u8; Address::SIZE].into()),
142            key_id: 1.into(),
143        };
144
145        let blokli_client = BlokliTestStateBuilder::default()
146            .with_accounts([(account.clone(), HoprBalance::new_base(100), XDaiBalance::new_base(1))])
147            .build_static_client();
148
149        let mut connector = create_connector(blokli_client)?;
150        connector.connect().await?;
151
152        assert_eq!(
153            Some(chain_addr),
154            connector.packet_key_to_chain_key(offchain_key.public())?
155        );
156        assert_eq!(
157            Some(*offchain_key.public()),
158            connector.chain_key_to_packet_key(&chain_addr)?
159        );
160
161        let mapper = connector.key_id_mapper_ref();
162
163        assert_eq!(Some(account.key_id), mapper.map_key_to_id(offchain_key.public()));
164        assert_eq!(Some(*offchain_key.public()), mapper.map_id_to_public(&account.key_id));
165
166        Ok(())
167    }
168
169    struct MockErrorBackend;
170
171    impl Backend for MockErrorBackend {
172        type Error = ConnectorError;
173
174        fn insert_account(&self, _entry: AccountEntry) -> Result<Option<AccountEntry>, Self::Error> {
175            Err(ConnectorError::InvalidState("mock error"))
176        }
177
178        fn insert_channel(&self, _channel: ChannelEntry) -> Result<Option<ChannelEntry>, Self::Error> {
179            Err(ConnectorError::InvalidState("mock error"))
180        }
181
182        fn get_account_by_id(&self, _id: &HoprKeyIdent) -> Result<Option<AccountEntry>, Self::Error> {
183            Err(ConnectorError::InvalidState("mock error"))
184        }
185
186        fn get_account_by_key(&self, _key: &OffchainPublicKey) -> Result<Option<AccountEntry>, Self::Error> {
187            Err(ConnectorError::InvalidState("mock error"))
188        }
189
190        fn get_account_by_address(&self, _chain_key: &Address) -> Result<Option<AccountEntry>, Self::Error> {
191            Err(ConnectorError::InvalidState("mock error"))
192        }
193
194        fn get_channel_by_id(&self, _id: &ChannelId) -> Result<Option<ChannelEntry>, Self::Error> {
195            Err(ConnectorError::InvalidState("mock error"))
196        }
197    }
198
199    #[test]
200    fn mapper_should_handle_backend_errors() {
201        let mapper = super::HoprKeyMapper {
202            id_to_key: moka::sync::Cache::builder()
203                .max_capacity(10)
204                .build_with_hasher(ahash::RandomState::default()),
205            key_to_id: moka::sync::Cache::builder()
206                .max_capacity(10)
207                .build_with_hasher(ahash::RandomState::default()),
208            backend: std::sync::Arc::new(MockErrorBackend),
209        };
210
211        let key = *OffchainKeypair::random().public();
212        let id = HoprKeyIdent::from(1);
213
214        assert_eq!(None, mapper.map_key_to_id(&key));
215        assert_eq!(None, mapper.map_id_to_public(&id));
216    }
217
218    #[test]
219    fn mapper_should_handle_missing_accounts() {
220        use crate::InMemoryBackend;
221        let mapper = super::HoprKeyMapper {
222            id_to_key: moka::sync::Cache::builder()
223                .max_capacity(10)
224                .build_with_hasher(ahash::RandomState::default()),
225            key_to_id: moka::sync::Cache::builder()
226                .max_capacity(10)
227                .build_with_hasher(ahash::RandomState::default()),
228            backend: std::sync::Arc::new(InMemoryBackend::default()),
229        };
230
231        let key = *OffchainKeypair::random().public();
232        let id = HoprKeyIdent::from(1);
233
234        assert_eq!(None, mapper.map_key_to_id(&key));
235        assert_eq!(None, mapper.map_id_to_public(&id));
236    }
237
238    #[tokio::test]
239    async fn connector_should_check_connection_state() -> anyhow::Result<()> {
240        let blokli_client = BlokliTestStateBuilder::default().build_static_client();
241        let connector = create_connector(blokli_client)?;
242
243        let key = *OffchainKeypair::random().public();
244        let addr = Address::from([0u8; 20]);
245
246        assert!(connector.packet_key_to_chain_key(&key).is_err());
247        assert!(connector.chain_key_to_packet_key(&addr).is_err());
248
249        Ok(())
250    }
251
252    #[tokio::test]
253    async fn connector_should_handle_backend_errors() -> anyhow::Result<()> {
254        let blokli_client = BlokliTestStateBuilder::default().build_static_client();
255        let connector = create_connector(blokli_client)?;
256
257        // Instead of manual struct initialization, use what we have and inject the error backend
258        let mapper = super::HoprKeyMapper {
259            id_to_key: moka::sync::Cache::builder()
260                .max_capacity(10)
261                .build_with_hasher(ahash::RandomState::default()),
262            key_to_id: moka::sync::Cache::builder()
263                .max_capacity(10)
264                .build_with_hasher(ahash::RandomState::default()),
265            backend: std::sync::Arc::new(MockErrorBackend),
266        };
267
268        let mut connector = crate::connector::HoprBlockchainConnector::new(
269            connector.chain_key.clone(),
270            connector.cfg,
271            (*connector.client).clone(),
272            MockErrorBackend,
273            connector.payload_generator,
274        );
275        connector.mapper = mapper;
276        connector.connect().await?;
277
278        let key = *OffchainKeypair::random().public();
279        let addr = Address::from([0u8; 20]);
280
281        assert!(connector.packet_key_to_chain_key(&key).is_err());
282        assert!(connector.chain_key_to_packet_key(&addr).is_err());
283
284        Ok(())
285    }
286}