hopr_chain_connector/connector/
keys.rs1use 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
24impl<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 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}