Skip to main content

calimero_context_primitives/client/
crypto.rs

1use calimero_primitives::context::ContextId;
2use calimero_primitives::identity::{PrivateKey, PublicKey};
3use calimero_store::{key, types};
4use eyre::bail;
5
6use super::ContextClient;
7
8/// Represents a user's identity within a specific context.
9///
10/// An identity is defined by a public key. If the node manages this identity,
11/// it will also hold the corresponding private key(s).
12#[derive(Debug)]
13pub struct ContextIdentity {
14    /// The primary public key for this identity, used for identification and signing.
15    pub public_key: PublicKey,
16    /// The optional private key corresponding to `public_key`. If `Some`, this node
17    /// "owns" or "manages" this identity and can sign transactions on its behalf.
18    /// If `None`, this node only knows about the identity but cannot act as it.
19    pub private_key: Option<PrivateKey>,
20    /// An optional, secondary private key used for a specific purpose, such as a
21    /// dedicated key for sending messages or transactions to reduce exposure of the primary key.
22    pub sender_key: Option<PrivateKey>,
23}
24
25impl ContextIdentity {
26    /// Returns a reference to the private key if it exists.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the identity is not managed by this node (i.e., `private_key` is `None`).
31    pub fn private_key(&self) -> eyre::Result<&PrivateKey> {
32        let Some(private_key) = &self.private_key else {
33            bail!(
34                "the identity '{}' is not managed by this node",
35                self.public_key
36            );
37        };
38
39        Ok(private_key)
40    }
41}
42
43impl ContextClient {
44    /// Creates a new cryptographic identity (key pair) and stores it in the datastore.
45    /// The private key is randomly generated.
46    /// The new identity doesn't have any `sender_key`. If needed, the `sender_key` could be set via
47    /// `update_identity()` method later.
48    ///
49    /// # Note
50    ///
51    /// This identity is not initially tied to a specific context (it is stored under a
52    /// zeroed-out `ContextId`). It can be seen as a "global" identity within the node
53    /// that can later be associated with one or more contexts.
54    ///
55    /// # Returns
56    ///
57    /// A `Result` containing the `PublicKey` of the newly created identity.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if there is an issue writing the new identity to the datastore.
62    pub fn new_identity(&self) -> eyre::Result<PublicKey> {
63        let mut handle = self.datastore.handle();
64
65        let private_key = PrivateKey::random(&mut rand::thread_rng());
66        let public_key = private_key.public_key();
67
68        handle.put(
69            &key::ContextIdentity::new(ContextId::zero(), public_key),
70            &types::ContextIdentity {
71                private_key: Some(*private_key),
72                sender_key: None,
73            },
74        )?;
75
76        Ok(public_key)
77    }
78
79    /// Retrieves an identity from the datastore for a given context.
80    ///
81    /// # Arguments
82    ///
83    /// * `context_id` - The context in which the identity is being retrieved.
84    /// * `public_key` - The public key of the identity to fetch.
85    ///
86    /// # Returns
87    ///
88    /// An `Option` containing the `ContextIdentity` if found, otherwise `None`.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if there is an issue reading from the datastore.
93    pub fn get_identity(
94        &self,
95        context_id: &ContextId,
96        public_key: &PublicKey,
97    ) -> eyre::Result<Option<ContextIdentity>> {
98        let handle = self.datastore.handle();
99
100        let key = key::ContextIdentity::new(*context_id, *public_key);
101
102        let Some(identity) = handle.get(&key)? else {
103            return Ok(None);
104        };
105
106        let identity = ContextIdentity {
107            public_key: *public_key,
108            private_key: identity.private_key.map(PrivateKey::from),
109            sender_key: identity.sender_key.map(PrivateKey::from),
110        };
111
112        Ok(Some(identity))
113    }
114
115    /// Updates an existing identity in the datastore.
116    ///
117    /// This is typically used to add or change the `sender_key` or `private_key`
118    /// for an identity that the node already knows about.
119    ///
120    /// # Arguments
121    ///
122    /// * `context_id` - The context of the identity to update.
123    /// * `new_identity` - The `ContextIdentity` object containing the updated fields.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the identity does not exist or if there is a datastore issue.
128    pub fn update_identity(
129        &self,
130        context_id: &ContextId,
131        new_identity: &ContextIdentity,
132    ) -> eyre::Result<()> {
133        let mut handle = self.datastore.handle();
134
135        let key = key::ContextIdentity::new(*context_id, new_identity.public_key);
136
137        let Some(mut identity) = handle.get(&key)? else {
138            bail!(
139                "the identity '{}' is not managed on this node for context '{}'",
140                new_identity.public_key,
141                context_id
142            );
143        };
144
145        identity.sender_key = new_identity.sender_key.as_deref().copied();
146        // TODO: what we are updating the private key for? if we got here, the datastore already
147        // has the `identity.private_key` set.
148        identity.private_key = new_identity.private_key.as_deref().copied();
149
150        handle.put(&key, &identity)?;
151
152        Ok(())
153    }
154
155    /// Deletes an identity from the datastore for a given context.
156    ///
157    /// # Arguments
158    ///
159    /// * `context_id` - The context from which to delete the identity.
160    /// * `public_key` - The public key of the identity to delete.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if there is an issue writing to the datastore.
165    pub fn delete_identity(
166        &self,
167        context_id: &ContextId,
168        public_key: &PublicKey,
169    ) -> eyre::Result<()> {
170        let mut handle = self.datastore.handle();
171
172        let key = key::ContextIdentity::new(*context_id, *public_key);
173
174        handle.delete(&key)?;
175
176        Ok(())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::client::ContextClient;
184    use calimero_blobstore::{config::BlobStoreConfig, BlobManager, FileSystem};
185    use calimero_context_config::client::{
186        config::{ClientConfig, ClientRelayerSigner, ClientSigner, LocalConfig},
187        Client as ExternalClient,
188    };
189    use calimero_network_primitives::client::NetworkClient;
190    use calimero_node_primitives::{client::NodeClient, messages::NodeMessage};
191    use calimero_primitives::common::DIGEST_SIZE;
192    use calimero_store::{db::InMemoryDB, key, types, Store};
193    use calimero_utils_actix::LazyRecipient;
194    use std::collections::BTreeMap;
195    use std::sync::Arc;
196    use tokio;
197    use tokio::sync::{broadcast, mpsc};
198
199    /// Correctly initializes all dependencies using the public `from_config` constructor.
200    async fn setup_context_client() -> ContextClient {
201        // 1. Create the InMemoryDB directly.
202        let db = InMemoryDB::owned();
203        let store = Store::new(Arc::new(db));
204
205        // 2. BlobManager setup.
206        let tmp_dir = tempfile::tempdir().unwrap();
207        let blob_store_config =
208            BlobStoreConfig::new(tmp_dir.path().to_path_buf().try_into().unwrap());
209        let file_system = FileSystem::new(&blob_store_config).await.unwrap();
210        let blob_manager = BlobManager::new(store.clone(), file_system);
211
212        // 3. Mock/dummy network and actor dependencies.
213        let network_client = NetworkClient::new(LazyRecipient::new());
214        let (event_sender, _) = broadcast::channel(16);
215        let (ctx_sync_tx, _) = mpsc::channel(16);
216        let node_manager = LazyRecipient::<NodeMessage>::new();
217
218        // 4. Construct the real NodeClient.
219        let node_client = NodeClient::new(
220            store.clone(),
221            blob_manager,
222            network_client,
223            node_manager,
224            event_sender,
225            ctx_sync_tx,
226            String::new(), // Not used in tests
227        );
228
229        // 5. Create a minimal, valid ClientConfig.
230        let client_config = ClientConfig {
231            params: BTreeMap::new(),
232            signer: ClientSigner {
233                relayer: ClientRelayerSigner {
234                    url: "http://127.0.0.1:3030".parse().unwrap(),
235                },
236                local: LocalConfig {
237                    protocols: BTreeMap::new(),
238                },
239            },
240        };
241
242        // 6. Construct the ExternalClient using the intended public API.
243        // This is much cleaner and more robust than manual construction.
244        let external_client = ExternalClient::from_config(&client_config);
245
246        // 7. Construct the final ContextClient.
247        let context_manager = LazyRecipient::new();
248        ContextClient::new(store, node_client, external_client, context_manager)
249    }
250
251    #[tokio::test]
252    async fn test_new_and_get_identity() {
253        let client = setup_context_client().await;
254        let public_key = client.new_identity().expect("Should create identity");
255        let context_id = ContextId::zero();
256        let identity = client
257            .get_identity(&context_id, &public_key)
258            .unwrap()
259            .expect("Identity should be found in the datastore");
260
261        assert_eq!(identity.public_key, public_key);
262        assert!(identity.private_key.is_some(), "Identity should be owned");
263        assert!(identity.sender_key.is_none());
264    }
265
266    #[tokio::test]
267    async fn test_update_and_delete_identity() {
268        let client = setup_context_client().await;
269        let context_id = ContextId::from([1; DIGEST_SIZE]);
270        let public_key = client.new_identity().unwrap();
271
272        let private_key_bytes: [u8; DIGEST_SIZE] = [1; DIGEST_SIZE];
273        let mut handle = client.datastore.handle();
274        let key = key::ContextIdentity::new(context_id, public_key);
275        let id_data = types::ContextIdentity {
276            private_key: Some(private_key_bytes.into()),
277            sender_key: None,
278        };
279        handle.put(&key, &id_data).unwrap();
280
281        let sender_private_key = PrivateKey::from([2; DIGEST_SIZE]);
282        let mut identity_to_update = client
283            .get_identity(&context_id, &public_key)
284            .unwrap()
285            .unwrap();
286        identity_to_update.sender_key = Some(sender_private_key);
287
288        client
289            .update_identity(&context_id, &identity_to_update)
290            .unwrap();
291
292        let updated_identity = client
293            .get_identity(&context_id, &public_key)
294            .unwrap()
295            .unwrap();
296        assert!(
297            updated_identity.sender_key.is_some(),
298            "Sender key should have been updated"
299        );
300
301        client.delete_identity(&context_id, &public_key).unwrap();
302
303        let final_identity = client.get_identity(&context_id, &public_key).unwrap();
304        assert!(
305            final_identity.is_none(),
306            "Identity should be None after deletion"
307        );
308    }
309}