Skip to main content

calimero_context_primitives/
client.rs

1#![allow(clippy::multiple_inherent_impl, reason = "better readability")]
2
3use async_stream::try_stream;
4use borsh::BorshDeserialize;
5use calimero_context_config::client::{AnyTransport, Client as ExternalClient};
6use calimero_context_config::types::{
7    BlockHeight, InvitationFromMember, RevealPayloadData, SignedOpenInvitation, SignedRevealPayload,
8};
9use calimero_node_primitives::client::NodeClient;
10use calimero_primitives::alias::Alias;
11use calimero_primitives::application::ApplicationId;
12use calimero_primitives::common::DIGEST_SIZE;
13use calimero_primitives::context::{
14    Context, ContextConfigParams, ContextId, ContextInvitationPayload,
15};
16use calimero_primitives::hash::Hash;
17use calimero_primitives::identity::{PrivateKey, PublicKey};
18use calimero_store::{key, Store};
19use calimero_utils_actix::LazyRecipient;
20use eyre::{bail, ContextCompat, WrapErr};
21use futures_util::Stream;
22use rand::Rng;
23use sha2::{Digest, Sha256};
24use tokio::sync::oneshot;
25
26use crate::messages::{
27    ContextMessage, CreateContextRequest, CreateContextResponse, DeleteContextRequest,
28    DeleteContextResponse, ExecuteError, ExecuteRequest, ExecuteResponse, JoinContextRequest,
29    JoinContextResponse, MigrationParams, UpdateApplicationRequest,
30};
31use crate::ContextAtomic;
32
33pub mod crypto;
34pub mod external;
35mod sync;
36
37/// A client for interacting with the context management system.
38///
39/// This struct serves as the primary public API, providing methods to create,
40/// join, query, and manage contexts and their members. It orchestrates
41/// interactions between the datastore, background actors, and external networks.
42#[derive(Clone, Debug)]
43pub struct ContextClient {
44    /// A handle to the persistent key-value store for all context-related data.
45    datastore: Store,
46    /// A client for communicating with the underlying Calimero node.
47    node_client: NodeClient,
48    /// A client for interacting with external services, such as on-chain smart contracts.
49    external_client: ExternalClient<AnyTransport>,
50    /// A lazy-initialized sender handle to the `ContextManager` actor. This is used
51    /// to send asynchronous messages for processing.
52    context_manager: LazyRecipient<ContextMessage>,
53}
54
55impl ContextClient {
56    #[must_use]
57    pub const fn new(
58        datastore: Store,
59        node_client: NodeClient,
60        external_client: ExternalClient<AnyTransport>,
61        context_manager: LazyRecipient<ContextMessage>,
62    ) -> Self {
63        Self {
64            datastore,
65            node_client,
66            external_client,
67            context_manager,
68        }
69    }
70
71    /// Returns a handle to the datastore for direct access.
72    /// Used by node components that need to read stored data.
73    pub fn datastore_handle(&self) -> calimero_store::Handle<Store> {
74        self.datastore.handle()
75    }
76
77    /// Sends a request to create a new context.
78    ///
79    /// This operation is asynchronous and is handled by the `ContextManager` actor.
80    ///
81    /// # Arguments
82    ///
83    /// * `protocol` - The name of the protocol that will be used for the new context.
84    /// * `application_id` - The ID of the application that will run in the context.
85    /// * `identity_secret` - An optional private key to use for the initial identity. If not
86    ///   provided, a new identity will be generated.
87    /// * `init_params` - Raw byte parameters for initializing the application state.
88    /// * `seed` - An optional 32-byte seed for deterministic context ID and identity creation.
89    ///
90    /// # Returns
91    ///
92    /// A `Result` containing the `CreateContextResponse` from the actor upon completion.
93    pub async fn create_context(
94        &self,
95        protocol: String,
96        application_id: &ApplicationId,
97        identity_secret: Option<PrivateKey>,
98        init_params: Vec<u8>,
99        seed: Option<[u8; DIGEST_SIZE]>,
100    ) -> eyre::Result<CreateContextResponse> {
101        let (sender, receiver) = oneshot::channel();
102
103        self.context_manager
104            .send(ContextMessage::CreateContext {
105                request: CreateContextRequest {
106                    protocol,
107                    seed,
108                    application_id: *application_id,
109                    identity_secret,
110                    init_params,
111                },
112                outcome: sender,
113            })
114            .await
115            .expect("Mailbox not to be dropped");
116
117        receiver.await.expect("Mailbox not to be dropped")
118    }
119
120    /// Invites a new member to an existing context.
121    ///
122    /// This involves an external call to the on-chain contract to register the new member.
123    ///
124    /// # Arguments
125    ///
126    /// * `context_id` - The context to invite the member to.
127    /// * `inviter_id` - The public key of an existing member who is performing the invitation.
128    /// * `invitee_id` - The public key of the identity being invited.
129    ///
130    /// # Returns
131    ///
132    /// * A `Result` containing an `Option` with the shareable `ContextInvitationPayload`.
133    /// * Returns `Ok(None)` if the context configuration cannot be found locally.
134    pub async fn invite_member(
135        &self,
136        context_id: &ContextId,
137        inviter_id: &PublicKey,
138        invitee_id: &PublicKey,
139    ) -> eyre::Result<Option<ContextInvitationPayload>> {
140        let Some(external_config) = self.context_config(context_id)? else {
141            return Ok(None);
142        };
143
144        let external_client = self.external_client(context_id, &external_config)?;
145
146        external_client
147            .config()
148            .add_members(inviter_id, &[*invitee_id])
149            .await?;
150
151        let invitation_payload = ContextInvitationPayload::new(
152            *context_id,
153            *invitee_id,
154            external_config.protocol,
155            external_config.network_id,
156            external_config.contract_id,
157        )?;
158
159        Ok(Some(invitation_payload))
160    }
161
162    /// Creates and signs a one-time, expiring open invitation for a new member.
163    ///
164    /// This method allows an existing member of a context (the inviter) to generate a
165    /// shareable invitation. The method fetches the inviter's private key managed
166    /// by the local node, signs the invitation details, and returns the resulting
167    /// payload and signature.
168    ///
169    /// # Arguments
170    /// * `context_id` - The context to invite the new member to.
171    /// * `inviter_id` - The public key of the existing member creating the invitation.
172    ///                  This node must have the corresponding private key for this identity.
173    /// * `valid_for_blocks` - A number of blocks from the current block height for which the
174    ///   invitation is considered to be valid.
175    /// * `secret_salt` - A 32-byte random value to ensure the invitation is unique.
176    ///
177    /// # Returns
178    /// * A `Result` containing the `SignedOpenInvitation` if successful, or an error if
179    /// the inviter's private key is not found or signing fails.
180    /// * Returns `Ok(None)` if the context configuration cannot be found locally.
181    pub async fn invite_member_by_open_invitation(
182        &self,
183        context_id: &ContextId,
184        inviter_id: &PublicKey,
185        valid_for_blocks: BlockHeight,
186        _secret_salt: [u8; DIGEST_SIZE],
187    ) -> eyre::Result<Option<SignedOpenInvitation>> {
188        // TODO(identity): figure out the best place to generate salt.
189        // We temporarily ignore the passed `secret_salt` as we can't generate it in admin
190        // `invite_to_context_open_invitation::handler` as `Rng` is not thread-safe.
191        let mut rng = rand::thread_rng();
192        let salt: [u8; DIGEST_SIZE] = rng.gen::<[_; DIGEST_SIZE]>();
193        let secret_salt = salt;
194
195        let Some(external_config) = self.context_config(context_id)? else {
196            return Ok(None);
197        };
198
199        if external_config.protocol != "near" {
200            bail!("Failed to create an open invitaiton: only NEAR Protocol currently supports this feature");
201        }
202
203        //let external_client = self.external_client(context_id, &external_config)?;
204        // TODO: query the current block height from the NEAR client and use it to calculate the
205        // real expiration block height.
206        let current_block_height: BlockHeight = 999_999_999;
207        let expiration_block_height = current_block_height + valid_for_blocks;
208
209        // 1. Fetch the inviter's identity to get their private key for signing.
210        let inviter_identity = self
211            .get_identity(context_id, inviter_id)?
212            .with_context(|| format!("Inviter identity {inviter_id} not found"))?;
213        let inviter_private_key = inviter_identity.private_key()?;
214
215        let inviter_identity: [u8; DIGEST_SIZE] = **inviter_id;
216        let inviter_identity_context_type = inviter_identity.into();
217        let context_id = **context_id;
218
219        // 2. Construct the invitation payload.
220        let invitation = InvitationFromMember {
221            inviter_identity: inviter_identity_context_type,
222            context_id: context_id.into(),
223            expiration_height: expiration_block_height,
224            secret_salt,
225            protocol: external_config.protocol.to_string(),
226            network: external_config.network_id.to_string(),
227            contract_id: external_config.contract_id.to_string(),
228        };
229
230        // 3. Sign the invitation payload.
231        // The process is: borsh-serialize -> sha256-hash -> sign the hash.
232        let invitation_bytes =
233            borsh::to_vec(&invitation).context("Failed to serialize invitation")?;
234        let hash = Sha256::digest(&invitation_bytes);
235        let signature = inviter_private_key.sign(&hash).context("Signing failed")?;
236
237        // 4. Hex-encode the signature and return the complete package.
238        Ok(Some(SignedOpenInvitation {
239            invitation,
240            inviter_signature: hex::encode(signature.to_bytes()),
241        }))
242    }
243
244    /// Sends a request to join a context using an invitation payload.
245    ///
246    /// This is an asynchronous operation handled by the `ContextManager` actor. The actor
247    /// will parse the payload, validate the information, and configure the local node
248    /// to participate in the specified context.
249    ///
250    /// # Arguments
251    ///
252    /// * `invitation_payload` - The opaque `ContextInvitationPayload` received from an inviter.
253    ///
254    /// # Returns
255    ///
256    /// A `Result` containing the `JoinContextResponse` from the actor upon completion.
257    pub async fn join_context(
258        &self,
259        invitation_payload: ContextInvitationPayload,
260    ) -> eyre::Result<JoinContextResponse> {
261        let (sender, receiver) = oneshot::channel();
262
263        self.context_manager
264            .send(ContextMessage::JoinContext {
265                request: JoinContextRequest { invitation_payload },
266                outcome: sender,
267            })
268            .await
269            .expect("Mailbox not to be dropped");
270
271        receiver.await.expect("Mailbox not to be dropped")
272    }
273
274    // TODO(invitation): implement
275    /// Sends a request to join a context using the new commit-reveal open invitation flow.
276    ///
277    /// This is an asynchronous operation handled by the `ContextManager` actor. The actor
278    /// will parse the payload, validate the information, and configure the local node
279    /// to participate in the specified context.
280    ///
281    /// # Arguments
282    ///
283    /// * `invitation_payload` - The opaque `ContextInvitationPayload` received from an inviter.
284    ///
285    /// # Returns
286    ///
287    /// * A `Result` containing the `JoinContextResponse` from the actor upon completion.
288    /// * Returns `Ok(None)` if the context configuration cannot be found locally.
289    pub async fn join_context_by_open_invitation(
290        &self,
291        signed_invitation: SignedOpenInvitation,
292        new_member_public_key: &PublicKey,
293    ) -> eyre::Result<Option<JoinContextResponse>> {
294        let invitation = signed_invitation.invitation.clone();
295        // Convert `config::types::ContextId` to `crypto::ContextId`
296        let context_id = invitation.context_id.to_bytes().into();
297        println!("Try to join by open invitation the Context ID: {context_id}");
298
299        // At this step the identity should be at the zeroth context:
300        // it should exist on the node, but available to be assigned for a new context.
301        let new_member_identity = self
302            .get_identity(&ContextId::zero(), new_member_public_key)?
303            .with_context(|| format!("New member's identity {new_member_public_key} not found"))?;
304        let new_member_private_key = new_member_identity.private_key()?;
305
306        // Convert `crypto::ContextIdentity` to `calimero_contex_config::types::ContextId`
307        let new_member_identity_bytes: [u8; DIGEST_SIZE] = *new_member_identity.public_key;
308        let new_member_identity_context_type = new_member_identity_bytes.into();
309
310        let reveal_payload_data = RevealPayloadData {
311            signed_open_invitation: signed_invitation,
312            new_member_identity: new_member_identity_context_type,
313        };
314
315        let reveal_payload_data_bytes =
316            borsh::to_vec(&reveal_payload_data).context("Failed to serialize invitation")?;
317        let commitment_hash = hex::encode(Sha256::digest(&reveal_payload_data_bytes));
318        println!("New member commitment hash: {commitment_hash:?}");
319
320        // Create a config for the external client.
321        // We don't have a config for that context ID yet, as we are about to join it.
322        let mut external_config_params = None;
323        if !self.has_context(&context_id)? {
324            let mut external_config = ContextConfigParams {
325                protocol: invitation.protocol.into(),
326                network_id: invitation.network.into(),
327                contract_id: invitation.contract_id.into(),
328                proxy_contract: "".into(),
329                application_revision: 0,
330                members_revision: 0,
331            };
332
333            let external_client = self.external_client(&context_id, &external_config)?;
334            let config_client = external_client.config();
335            let proxy_contract = config_client.get_proxy_contract().await?;
336            external_config.proxy_contract = proxy_contract.into();
337
338            external_config_params = Some(external_config);
339        }
340
341        let external_config_params =
342            external_config_params.context("External config is None while it should be set")?;
343        let external_client = self.external_client(&context_id, &external_config_params)?;
344
345        external_client
346            .config()
347            .join_context_commit_invitation(
348                &new_member_identity.public_key,
349                commitment_hash,
350                reveal_payload_data
351                    .signed_open_invitation
352                    .invitation
353                    .expiration_height,
354            )
355            .await?;
356        println!("Successfully committed the invitation payload");
357
358        // The new member that is going to join the context by open invitation, signs the payload
359        // that will be committed and revealed later
360        let new_member_signature = {
361            let hash = Sha256::digest(&reveal_payload_data_bytes);
362            let signature = new_member_private_key
363                .sign(&hash)
364                .context("Signing reveal payload data failed")?;
365            hex::encode(signature.to_bytes())
366        };
367        println!("New member signature: {new_member_signature:?}");
368
369        let signed_payload = SignedRevealPayload {
370            data: reveal_payload_data,
371            invitee_signature: new_member_signature,
372        };
373
374        external_client
375            .config()
376            .join_context_reveal_invitation(&new_member_identity.public_key, signed_payload)
377            .await?;
378        println!("Successfully submitted the revealed invitation payload");
379
380        // Create the ContextInvitationPayload
381        let invitation_payload = ContextInvitationPayload::new(
382            context_id,
383            new_member_identity.public_key,
384            external_config_params.protocol,
385            external_config_params.network_id,
386            external_config_params.contract_id,
387        )?;
388
389        // Join the context in the node
390        let (sender, receiver) = oneshot::channel();
391
392        self.context_manager
393            .send(ContextMessage::JoinContext {
394                request: JoinContextRequest { invitation_payload },
395                outcome: sender,
396            })
397            .await
398            .expect("Mailbox not to be dropped");
399
400        let response = receiver.await.expect("Mailbox not to be dropped")?;
401        Ok(Some(response))
402    }
403
404    /// Checks if a context's metadata exists in the local datastore.
405    ///
406    /// # Arguments
407    ///
408    /// * `context_id` - The ID of the context to check for.
409    ///
410    /// # Returns
411    ///
412    /// A `Result` containing `true` if the context exists locally, `false` otherwise.
413    pub fn has_context(&self, context_id: &ContextId) -> eyre::Result<bool> {
414        let handle = self.datastore.handle();
415
416        let key = key::ContextMeta::new(*context_id);
417
418        Ok(handle.has(&key)?)
419    }
420
421    /// Retrieves a context metadata from the local datastore.
422    ///
423    /// # Arguments
424    ///
425    /// * `context_id` - The ID of the context to retrieve.
426    ///
427    /// # Returns
428    ///
429    /// A `Result` containing `Some(Context)` if the context is found, or `None` if it is not.
430    pub fn get_context(&self, context_id: &ContextId) -> eyre::Result<Option<Context>> {
431        let handle = self.datastore.handle();
432
433        let key = key::ContextMeta::new(*context_id);
434
435        let Some(meta) = handle.get(&key)? else {
436            return Ok(None);
437        };
438
439        let context = Context::with_dag_heads(
440            *context_id,
441            meta.application.application_id(),
442            meta.root_hash.into(),
443            meta.dag_heads.clone(),
444        );
445
446        tracing::debug!(
447            %context_id,
448            dag_heads_count = meta.dag_heads.len(),
449            "Loaded context from database"
450        );
451
452        Ok(Some(context))
453    }
454
455    /// Updates the DAG heads for a context after applying a delta.
456    ///
457    /// # Arguments
458    ///
459    /// * `context_id` - The ID of the context to update.
460    /// * `dag_heads` - The new DAG heads (typically the delta ID that was just applied).
461    ///
462    /// # Returns
463    ///
464    /// A `Result` indicating success or failure.
465    pub fn update_dag_heads(
466        &self,
467        context_id: &ContextId,
468        dag_heads: Vec<[u8; 32]>,
469    ) -> eyre::Result<()> {
470        let handle = self.datastore.handle();
471
472        let key = key::ContextMeta::new(*context_id);
473
474        let Some(mut meta) = handle.get(&key)? else {
475            eyre::bail!("Context not found: {}", context_id);
476        };
477
478        // Update dag_heads
479        meta.dag_heads = dag_heads.clone();
480
481        // Write back to database
482        self.datastore.clone().handle().put(&key, &meta)?;
483
484        tracing::debug!(
485            %context_id,
486            dag_heads_count = dag_heads.len(),
487            "Updated dag_heads in database"
488        );
489
490        Ok(())
491    }
492
493    /// Updates the ApplicationId for a context.
494    ///
495    /// # Arguments
496    ///
497    /// * `context_id` - The ID of the context to update.
498    /// * `application_id` - The new ApplicationId.
499    ///
500    /// # Returns
501    ///
502    /// A `Result` indicating success or failure.
503    pub fn update_context_application_id(
504        &self,
505        context_id: &ContextId,
506        application_id: ApplicationId,
507    ) -> eyre::Result<()> {
508        let handle = self.datastore.handle();
509
510        let key = key::ContextMeta::new(*context_id);
511
512        let Some(mut meta) = handle.get(&key)? else {
513            eyre::bail!("Context not found: {}", context_id);
514        };
515
516        // Update application_id
517        meta.application = key::ApplicationMeta::new(application_id);
518
519        // Write back to database
520        self.datastore.clone().handle().put(&key, &meta)?;
521
522        tracing::debug!(
523            %context_id,
524            %application_id,
525            "Updated application_id in database"
526        );
527
528        Ok(())
529    }
530
531    /// Computes the actual root hash from storage by reading the root Index entry.
532    ///
533    /// This reads the EntityIndex for Id::root() from RocksDB and extracts the
534    /// Merkle full_hash. This is the authoritative hash computed from the actual
535    /// state, not a claimed value.
536    ///
537    /// # Arguments
538    ///
539    /// * `context_id` - The ID of the context to compute the root hash for.
540    ///
541    /// # Returns
542    ///
543    /// The computed root hash, or `[0; 32]` if no root index exists (empty state).
544    pub fn compute_root_hash(&self, context_id: &ContextId) -> eyre::Result<[u8; 32]> {
545        // Compute the state_key for Key::Index(Id::root())
546        // Id::root() = Id::new(context_id) in WASM context
547        // Key::Index(id).to_bytes() = SHA256([0] || id.as_bytes())
548        let root_id: [u8; 32] = **context_id;
549        let mut key_bytes = [0u8; 33];
550        key_bytes[0] = 0; // Index discriminant
551        key_bytes[1..33].copy_from_slice(&root_id);
552        let state_key: [u8; 32] = Sha256::digest(key_bytes).into();
553
554        let handle = self.datastore.handle();
555        let db_key = key::ContextState::new(*context_id, state_key);
556
557        // Get data and convert to owned bytes to avoid lifetime issues
558        let data_opt = handle.get(&db_key)?;
559
560        match data_opt {
561            Some(data) => {
562                // Convert to owned Vec<u8> to avoid lifetime issues
563                let bytes: Vec<u8> = data.as_ref().to_vec();
564                drop(data); // Explicitly drop the borrowed data
565
566                self.parse_entity_index_root_hash(context_id, &bytes)
567            }
568            None => {
569                // No root index exists - empty state
570                tracing::debug!(
571                    %context_id,
572                    "No root index found, returning zero hash"
573                );
574                Ok([0; 32])
575            }
576        }
577    }
578
579    /// Parse EntityIndex bytes to extract the root hash.
580    fn parse_entity_index_root_hash(
581        &self,
582        context_id: &ContextId,
583        bytes: &[u8],
584    ) -> eyre::Result<[u8; 32]> {
585        // Deserialize EntityIndex and extract full_hash
586        // EntityIndex is borsh-serialized with full_hash at a known offset
587        // Structure: id(32) + parent_id(Option<32>) + children(Option<Vec>) + full_hash(32) + ...
588
589        if bytes.len() < 68 {
590            // Minimum size: id(32) + parent_id_tag(1) + children_tag(1) + full_hash(32) + own_hash(32) = 98
591            // But we check for 68 to be safe (id + tags + full_hash)
592            eyre::bail!(
593                "EntityIndex too small: {} bytes, expected at least 68",
594                bytes.len()
595            );
596        }
597
598        // Parse the EntityIndex structure manually for efficiency
599        // id: [u8; 32]
600        // parent_id: Option<Id> - 1 byte tag + optional 32 bytes
601        // children: Option<Vec<ChildInfo>> - 1 byte tag + optional length + data
602        // full_hash: [u8; 32]
603
604        let mut offset = 32; // Skip id
605
606        // Skip parent_id (Option<Id>)
607        let parent_tag = bytes[offset];
608        offset += 1;
609        if parent_tag == 1 {
610            offset += 32; // Skip the Id bytes
611        }
612
613        // Skip children (Option<Vec<ChildInfo>>)
614        let children_tag = bytes[offset];
615        offset += 1;
616        if children_tag == 1 {
617            // Children present - use full borsh deserialization for correctness
618            return self.compute_root_hash_via_borsh(context_id, bytes);
619        }
620
621        // Now at full_hash position
622        if offset + 32 > bytes.len() {
623            eyre::bail!(
624                "EntityIndex full_hash truncated at offset {}, len {}",
625                offset,
626                bytes.len()
627            );
628        }
629
630        let mut full_hash = [0u8; 32];
631        full_hash.copy_from_slice(&bytes[offset..offset + 32]);
632
633        tracing::debug!(
634            %context_id,
635            computed_root = ?Hash::from(full_hash),
636            "Computed root hash from storage"
637        );
638
639        Ok(full_hash)
640    }
641
642    /// Helper to compute root hash using full borsh deserialization.
643    fn compute_root_hash_via_borsh(
644        &self,
645        context_id: &ContextId,
646        bytes: &[u8],
647    ) -> eyre::Result<[u8; 32]> {
648        // Minimal EntityIndex structure for deserialization
649        #[derive(BorshDeserialize)]
650        struct EntityIndexMinimal {
651            _id: [u8; 32],
652            _parent_id: Option<[u8; 32]>,
653            _children: Option<Vec<ChildInfoMinimal>>,
654            full_hash: [u8; 32],
655            // Don't need rest
656        }
657
658        #[derive(BorshDeserialize)]
659        struct ChildInfoMinimal {
660            _id: [u8; 32],
661            _merkle_hash: [u8; 32],
662            _metadata: MetadataMinimal,
663        }
664
665        #[derive(BorshDeserialize)]
666        struct MetadataMinimal {
667            _created_at: u64,
668            _updated_at: UpdatedAtMinimal,
669            _storage_type: u8,
670        }
671
672        #[derive(BorshDeserialize)]
673        struct UpdatedAtMinimal(u64);
674
675        let index: EntityIndexMinimal = EntityIndexMinimal::try_from_slice(bytes)
676            .map_err(|e| eyre::eyre!("Failed to deserialize EntityIndex: {}", e))?;
677
678        tracing::debug!(
679            %context_id,
680            computed_root = ?Hash::from(index.full_hash),
681            "Computed root hash from storage (via borsh)"
682        );
683
684        Ok(index.full_hash)
685    }
686
687    /// Forces the root hash for a context to a specific value.
688    ///
689    /// **WARNING**: This bypasses verification and should only be used when
690    /// the hash has already been verified or during controlled operations.
691    /// Prefer `compute_root_hash` + `set_root_hash` for safety.
692    ///
693    /// # Arguments
694    ///
695    /// * `context_id` - The ID of the context to update.
696    /// * `root_hash` - The root hash to set.
697    ///
698    /// # Returns
699    ///
700    /// A `Result` indicating success or failure.
701    pub fn force_root_hash(&self, context_id: &ContextId, root_hash: Hash) -> eyre::Result<()> {
702        let handle = self.datastore.handle();
703
704        let key = key::ContextMeta::new(*context_id);
705
706        let Some(mut meta) = handle.get(&key)? else {
707            eyre::bail!("Context not found: {}", context_id);
708        };
709
710        tracing::debug!(
711            %context_id,
712            old_root = ?Hash::from(meta.root_hash),
713            new_root = ?root_hash,
714            "Setting root hash"
715        );
716
717        meta.root_hash = *root_hash;
718
719        self.datastore.clone().handle().put(&key, &meta)?;
720
721        Ok(())
722    }
723
724    /// Verifies that the stored root hash matches the actual state.
725    ///
726    /// Computes the root hash from storage and compares with the claimed hash.
727    /// Returns Ok(()) if they match, or an error describing the mismatch.
728    ///
729    /// # Arguments
730    ///
731    /// * `context_id` - The ID of the context to verify.
732    /// * `claimed_hash` - The hash to verify against.
733    ///
734    /// # Returns
735    ///
736    /// A `Result` indicating success if hashes match, or an error if they don't.
737    pub fn verify_root_hash(
738        &self,
739        context_id: &ContextId,
740        claimed_hash: [u8; 32],
741    ) -> eyre::Result<()> {
742        let computed = self.compute_root_hash(context_id)?;
743
744        if computed != claimed_hash {
745            eyre::bail!(
746                "Root hash verification failed for context {}: computed {} != claimed {}",
747                context_id,
748                hex::encode(computed),
749                hex::encode(claimed_hash)
750            );
751        }
752
753        tracing::debug!(
754            %context_id,
755            hash = ?Hash::from(computed),
756            "Root hash verified successfully"
757        );
758
759        Ok(())
760    }
761
762    /// Returns a stream of all context IDs stored locally.
763    ///
764    /// # Arguments
765    ///
766    /// * `start` - An optional `ContextId` from which to begin the stream. If `None`,
767    ///    the stream starts from the beginning.
768    ///
769    /// # Returns
770    ///
771    /// An implementation of `Stream` that yields `Result<ContextId>`.
772    pub fn get_context_ids(
773        &self,
774        start: Option<ContextId>,
775    ) -> impl Stream<Item = eyre::Result<ContextId>> {
776        let handle = self.datastore.handle();
777
778        try_stream! {
779            let mut iter = handle.iter::<key::ContextMeta>()?;
780
781            let start = start.and_then(|s| iter.seek(key::ContextMeta::new(s)).transpose());
782
783            for key in start.into_iter().chain(iter.keys()) {
784                yield key?.context_id();
785            }
786        }
787    }
788
789    /// Checks if a given public key is a member of a context in the local datastore.
790    ///
791    /// # Arguments
792    ///
793    /// * `context_id` - The context to check within.
794    /// * `public_key` - The public key of the potential member.
795    ///
796    /// # Returns
797    ///
798    /// A `Result` containing `true` if the identity is a known member, `false` otherwise.
799    pub fn has_member(
800        &self,
801        context_id: &ContextId,
802        public_key: &PublicKey,
803        // is_owned: Option<bool>,
804    ) -> eyre::Result<bool> {
805        let handle = self.datastore.handle();
806
807        let key = key::ContextIdentity::new(*context_id, *public_key);
808
809        Ok(handle.has(&key)?)
810    }
811
812    /// Retrieves and returns a stream of all members of a given context.
813    ///
814    /// # Arguments
815    ///
816    /// * `context_id` - The context to query for members.
817    /// * `owned` - If `Some(true)`, the stream returns only members for which this node holds
818    ///    the private key. If `Some(false)` or `None`, it returns all members.
819    ///
820    /// # Returns
821    ///
822    /// A stream of tuples `(PublicKey, bool)`, where the boolean indicates if the identity is owned.
823    pub fn get_context_members(
824        &self,
825        context_id: &ContextId,
826        owned: Option<bool>,
827    ) -> impl Stream<Item = eyre::Result<(PublicKey, bool)>> {
828        let handle = self.datastore.handle();
829        let context_id = *context_id;
830        let only_owned = owned.unwrap_or(false);
831
832        try_stream! {
833            let mut iter = handle.iter::<key::ContextIdentity>()?;
834
835            let first = iter
836                .seek(key::ContextIdentity::new(context_id, [0; DIGEST_SIZE].into()))
837                .transpose()
838                .map(|k| (k, iter.read()));
839
840            for (k, v) in first.into_iter().chain(iter.entries()) {
841                let (k, v) = (k?, v?);
842
843                if k.context_id() != context_id {
844                    break;
845                }
846
847                let is_owned = v.private_key.is_some();
848                if !only_owned || is_owned {
849                    yield (k.public_key(), is_owned);
850                }
851            }
852        }
853    }
854
855    /// Sends a request to execute a method within a context.
856    ///
857    /// This is the primary way to interact with the application running inside a context.
858    /// The request is handled asynchronously by the `ContextManager` actor.
859    ///
860    /// # Arguments
861    ///
862    /// * `context_id` - The ID of the context where the execution should occur.
863    /// * `executor` - The public key of the identity performing the execution. The executor
864    ///   must be a member of the context.
865    /// * `method` - The string name of the application method to call.
866    /// * `payload` - The input data (e.g., serialized JSON) for the method.
867    /// * `aliases` - A list of public key aliases to use for this specific execution.
868    /// * `atomic` - An optional handle for batching multiple executions into an atomic transaction.
869    ///
870    /// # Returns
871    ///
872    /// A `Result` containing the `ExecuteResponse` on success, or an `ExecuteError` on failure.
873    pub async fn execute(
874        &self,
875        context_id: &ContextId,
876        executor: &PublicKey,
877        method: String,
878        payload: Vec<u8>,
879        aliases: Vec<Alias<PublicKey>>,
880        atomic: Option<ContextAtomic>,
881    ) -> Result<ExecuteResponse, ExecuteError> {
882        let (sender, receiver) = oneshot::channel();
883
884        self.context_manager
885            .send(ContextMessage::Execute {
886                request: ExecuteRequest {
887                    context: *context_id,
888                    executor: *executor,
889                    method,
890                    payload,
891                    aliases,
892                    atomic,
893                },
894                outcome: sender,
895            })
896            .await
897            .expect("Mailbox not to be dropped");
898
899        receiver.await.expect("Mailbox not to be dropped")
900    }
901
902    /// Sends a request to update the application for a given context.
903    /// This is an asynchronous operation handled by the `ContextManager` actor.
904    ///
905    /// # Arguments
906    /// * `context_id` - The ID of the context where to update the application.
907    /// * `application_id` - The ID of the new application to switch to.
908    /// * `identity` - The public key of the member authorizing the update.
909    /// * `migrate_method` - Optional name of the migration function to execute.
910    ///
911    /// # Returns
912    ///
913    /// An empty `Result` indicating the outcome of the application update request.
914    pub async fn update_application(
915        &self,
916        context_id: &ContextId,
917        application_id: &ApplicationId,
918        identity: &PublicKey,
919        migrate_method: Option<String>,
920    ) -> eyre::Result<()> {
921        let (sender, receiver) = oneshot::channel();
922
923        let migration = migrate_method.map(|method| MigrationParams { method });
924
925        self.context_manager
926            .send(ContextMessage::UpdateApplication {
927                request: UpdateApplicationRequest {
928                    context_id: *context_id,
929                    application_id: *application_id,
930                    public_key: *identity,
931                    migration,
932                },
933                outcome: sender,
934            })
935            .await
936            .expect("Mailbox not to be dropped");
937
938        receiver.await.expect("Mailbox not to be dropped")
939    }
940
941    /// Sends a request to delete a context from the local node.
942    /// This is an asynchronous operation handled by the `ContextManager` actor. It will remove
943    /// all associated data for the context from the local datastore.
944    ///
945    /// # Arguments
946    /// * `context_id` - The ID of the context to delete.
947    ///
948    /// # Returns
949    ///
950    ///A `Result` containing the `DeleteContextResponse` from the actor.
951    pub async fn delete_context(
952        &self,
953        context_id: &ContextId,
954    ) -> eyre::Result<DeleteContextResponse> {
955        let (sender, receiver) = oneshot::channel();
956
957        self.context_manager
958            .send(ContextMessage::DeleteContext {
959                request: DeleteContextRequest {
960                    context_id: *context_id,
961                },
962                outcome: sender,
963            })
964            .await
965            .expect("Mailbox not to be dropped");
966
967        receiver.await.expect("Mailbox not to be dropped")
968    }
969}