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