1use calimero_node_primitives::client::NodeClient;
8use calimero_primitives::application::ApplicationId;
9use calimero_primitives::blobs::BlobId;
10use calimero_primitives::common::DIGEST_SIZE;
11use calimero_primitives::context::{Context, ContextConfigParams, ContextId};
12use calimero_primitives::hash::Hash;
13use calimero_store::{key, types};
14use std::collections::BTreeSet;
15use tokio::sync::oneshot;
16use tokio::time::{sleep, Duration};
17use tracing::{debug, info, warn};
18use url::Url;
19
20use super::external::ExternalClient;
21use super::ContextClient;
22use crate::messages::{ContextMessage, SyncRequest};
23
24impl ContextClient {
25 const DEFAULT_PACKAGE: &str = "unknown";
27 const DEFAULT_VERSION: &str = "0.0.0";
28 const MAX_BLOB_RETRIES: u32 = 20;
29 const BLOB_RETRY_DELAY_MS: u64 = 1000;
30 const MEMBERS_PAGE_SIZE: usize = 100;
31
32 async fn try_install_from_url(
34 &self,
35 source: &Url,
36 metadata: &[u8],
37 ) -> eyre::Result<Option<ApplicationId>> {
38 match source.scheme() {
39 "http" | "https" => Ok(Some(
40 self.node_client
41 .install_application_from_url(source.clone(), metadata.to_vec(), None)
42 .await?,
43 )),
44 _ => Ok(None),
45 }
46 }
47
48 async fn install_regular_application(
50 &self,
51 blob_id: &BlobId,
52 size: u64,
53 source: &Url,
54 metadata: &[u8],
55 ) -> eyre::Result<ApplicationId> {
56 self.node_client.install_application(
57 blob_id,
58 size,
59 &source.clone().into(),
60 metadata.to_vec(),
61 Self::DEFAULT_PACKAGE,
62 Self::DEFAULT_VERSION,
63 None, false, )
66 }
67
68 async fn check_bundle_and_install(
70 &self,
71 blob_id: &BlobId,
72 blob_bytes: &[u8],
73 source: &Url,
74 size: u64,
75 metadata: &[u8],
76 ) -> eyre::Result<ApplicationId> {
77 let blob_bytes_clone = blob_bytes.to_vec();
78 let is_bundle =
79 tokio::task::spawn_blocking(move || NodeClient::is_bundle_blob(&blob_bytes_clone))
80 .await?;
81
82 if is_bundle {
83 debug!(
84 blob_id = %blob_id,
85 "Blob is a bundle, installing from bundle blob"
86 );
87 self.node_client
88 .install_application_from_bundle_blob(blob_id, &source.clone().into())
89 .await
90 } else {
91 debug!(
92 blob_id = %blob_id,
93 "Blob is not a bundle, using regular installation"
94 );
95 self.install_regular_application(blob_id, size, source, metadata)
96 .await
97 }
98 }
99
100 async fn install_from_existing_blob(
102 &self,
103 blob_id: &BlobId,
104 source: &Url,
105 size: u64,
106 metadata: &[u8],
107 ) -> eyre::Result<ApplicationId> {
108 debug!(
109 blob_id = %blob_id,
110 "Blob exists locally, checking if it's a bundle"
111 );
112
113 let Some(blob_bytes) = self.node_client.get_blob_bytes(blob_id, None).await? else {
115 debug!(
116 blob_id = %blob_id,
117 "Failed to read blob, falling back to regular installation"
118 );
119 return self
121 .install_regular_application(blob_id, size, source, metadata)
122 .await;
123 };
124
125 self.check_bundle_and_install(blob_id, &blob_bytes, source, size, metadata)
127 .await
128 }
129
130 async fn wait_for_blob_and_install(
132 &self,
133 blob_id: &BlobId,
134 source: &Url,
135 size: u64,
136 metadata: &[u8],
137 expected_app_id: ApplicationId,
138 ) -> eyre::Result<ApplicationId> {
139 debug!(
140 blob_id = %blob_id,
141 "Source indicates bundle (.mpk), waiting for blob to arrive via blob sharing"
142 );
143 for _ in 0..Self::MAX_BLOB_RETRIES {
147 if !self.node_client.has_blob(blob_id)? {
149 sleep(Duration::from_millis(Self::BLOB_RETRY_DELAY_MS)).await;
150 continue;
151 }
152
153 let Some(blob_bytes) = self.node_client.get_blob_bytes(blob_id, None).await? else {
155 sleep(Duration::from_millis(Self::BLOB_RETRY_DELAY_MS)).await;
156 continue;
157 };
158
159 debug!(
160 blob_id = %blob_id,
161 "Blob arrived, installing bundle"
162 );
163
164 return self
166 .check_bundle_and_install(blob_id, &blob_bytes, source, size, metadata)
167 .await;
168 }
169
170 warn!(
172 blob_id = %blob_id,
173 "Blob didn't arrive within retry window - bundle installation will be retried when blob arrives"
174 );
175 Ok(expected_app_id)
181 }
182
183 async fn install_when_blob_missing(
185 &self,
186 blob_id: &BlobId,
187 source: &Url,
188 size: u64,
189 metadata: &[u8],
190 expected_app_id: ApplicationId,
191 ) -> eyre::Result<ApplicationId> {
192 debug!(
193 blob_id = %blob_id,
194 "Blob doesn't exist locally, checking source for bundle detection"
195 );
196 let is_bundle_from_source = source.path().ends_with(".mpk");
199
200 if is_bundle_from_source {
201 self.wait_for_blob_and_install(blob_id, source, size, metadata, expected_app_id)
203 .await
204 } else {
205 debug!(
206 blob_id = %blob_id,
207 "Blob doesn't exist locally, using regular installation"
208 );
209 self.install_regular_application(blob_id, size, source, metadata)
213 .await
214 }
215 }
216
217 pub async fn sync_context_config(
218 &self,
219 context_id: ContextId,
220 config: Option<ContextConfigParams<'_>>,
221 ) -> eyre::Result<Context> {
222 let mut handle = self.datastore.handle();
223
224 let context = handle.get(&key::ContextMeta::new(context_id))?;
225
226 let (mut config, mut should_save_config) = config.map_or_else(
227 || {
228 let Some(config) = handle.get(&key::ContextConfig::new(context_id))? else {
229 eyre::bail!("context config not found")
230 };
231
232 let config = ContextConfigParams {
233 protocol: config.protocol.into_string().into(),
234 network_id: config.network.into_string().into(),
235 contract_id: config.contract.into_string().into(),
236 proxy_contract: config.proxy_contract.into_string().into(),
237 application_revision: config.application_revision,
238 members_revision: config.members_revision,
239 };
240
241 Ok((config, false))
242 },
243 |config| Ok((config, true)),
244 )?;
245
246 let remote_members_revision = {
248 let external_client = self.external_client(&context_id, &config)?;
249 external_client.config().members_revision().await?
250 };
251
252 if context.is_none() || remote_members_revision != config.members_revision {
254 tracing::info!(
255 %context_id,
256 local_members_revision = config.members_revision,
257 remote_members_revision,
258 "Members revision changed, synchronizing member list...",
259 );
260
261 should_save_config = true;
262 config.members_revision = remote_members_revision;
263
264 let external_client = self.external_client(&context_id, &config)?;
266 self.sync_members(context_id, &external_client).await?;
267 } else {
268 debug!(
269 %context_id,
270 local_members_revision = config.members_revision,
271 remote_members_revision,
272 "Members revision was not changed, skipping sync",
273 );
274 }
275
276 let application_revision = {
277 let external_client = self.external_client(&context_id, &config)?;
278 let config_client = external_client.config();
279 config_client.application_revision().await?
280 };
281
282 let mut application_id = None;
283
284 if context.is_none() || application_revision != config.application_revision {
285 should_save_config = true;
286 config.application_revision = application_revision;
287
288 let external_client = self.external_client(&context_id, &config)?;
289 let config_client = external_client.config();
290 let application = config_client.application().await?;
291
292 application_id = Some(application.id);
293
294 if !self.node_client.has_application(&application.id)? {
295 let source: Url = application.source.into();
296 let metadata = application.metadata.clone();
297 let blob_id = application.blob.bytecode;
298
299 let derived_application_id = {
300 if let Some(app_id) = self.try_install_from_url(&source, &metadata).await? {
302 app_id
303 } else {
304 if self.node_client.has_blob(&blob_id)? {
307 self.install_from_existing_blob(
308 &blob_id,
309 &source,
310 application.size,
311 &metadata,
312 )
313 .await?
314 } else {
315 self.install_when_blob_missing(
316 &blob_id,
317 &source,
318 application.size,
319 &metadata,
320 application.id,
321 )
322 .await?
323 }
324 }
325 };
326
327 if application.id != derived_application_id {
328 eyre::bail!(
329 "application mismatch: expected {}, got {}",
330 application.id,
331 derived_application_id
332 )
333 }
334 }
335 }
336
337 if should_save_config {
338 let config = config.clone();
341
342 handle.put(
343 &key::ContextConfig::new(context_id),
344 &types::ContextConfig::new(
345 config.protocol.into_owned().into_boxed_str(),
346 config.network_id.into_owned().into_boxed_str(),
347 config.contract_id.into_owned().into_boxed_str(),
348 config.proxy_contract.into_owned().into_boxed_str(),
349 config.application_revision,
350 config.members_revision,
351 ),
352 )?;
353 }
354
355 let (should_save, application_id, root_hash, dag_heads) = context.map_or_else(
356 || {
357 (
358 true,
359 application_id.expect("must've been defined if context doesn't exist"),
360 Hash::default(),
361 vec![],
362 )
363 },
364 |meta| {
365 (
366 application_id.is_some(),
367 application_id.unwrap_or_else(|| meta.application.application_id()),
368 meta.root_hash.into(),
369 meta.dag_heads.clone(),
370 )
371 },
372 );
373
374 if should_save {
375 handle.put(
376 &key::ContextMeta::new(context_id),
377 &types::ContextMeta::new(
378 key::ApplicationMeta::new(application_id),
379 *root_hash,
380 dag_heads.clone(),
381 ),
382 )?;
383
384 let (sender, receiver) = oneshot::channel();
385
386 self.context_manager
387 .send(ContextMessage::Sync {
388 request: SyncRequest {
389 context_id,
390 application_id,
391 },
392 outcome: sender,
393 })
394 .await
395 .expect("Mailbox not to be dropped");
396
397 receiver.await.expect("Mailbox not to be dropped");
398 }
399
400 let context = Context::with_dag_heads(context_id, application_id, root_hash, dag_heads);
401
402 Ok(context)
403 }
404
405 async fn sync_members(
412 &self,
413 context_id: ContextId,
414 external_client: &ExternalClient<'_>,
415 ) -> eyre::Result<()> {
416 let mut handle = self.datastore.handle();
417 let config_client = external_client.config();
418
419 let mut external_members = BTreeSet::new();
420
421 for (offset, length) in (0..).map(|i| {
423 (
424 Self::MEMBERS_PAGE_SIZE.saturating_mul(i),
425 Self::MEMBERS_PAGE_SIZE,
426 )
427 }) {
428 let members = config_client.members(offset, length).await?;
429 if members.is_empty() {
430 break;
431 }
432
433 for member in members {
434 external_members.insert(member);
435
436 let key = key::ContextIdentity::new(context_id, member);
438 if !handle.has(&key)? {
439 handle.put(
440 &key,
441 &types::ContextIdentity {
442 private_key: None,
443 sender_key: None,
444 },
445 )?;
446 }
447 }
448 }
449
450 let mut members_to_remove = Vec::new();
453
454 {
456 if let Ok(mut iter) = handle.iter::<key::ContextIdentity>() {
457 let start_key = key::ContextIdentity::new(context_id, [0u8; DIGEST_SIZE].into());
458
459 let first = iter.seek(start_key).ok().flatten();
461
462 for k in first.into_iter().chain(iter.keys().flatten()) {
464 if k.context_id() != context_id {
466 break;
467 }
468
469 if !external_members.contains(&k.public_key()) {
471 members_to_remove.push(*k.public_key());
472 }
473 }
474 }
475 }
476
477 for member in members_to_remove {
479 let member_public_key = member.into();
480 debug!(%context_id, %member_public_key, "Trying to prune member from local store (it was removed from the contract)");
481
482 let key = key::ContextIdentity::new(context_id, member_public_key);
483 handle.delete(&key)?;
484
485 info!(%context_id, %member_public_key, "Pruned member from local store (it was removed from the contract)");
486 }
487
488 Ok(())
489 }
490}