1use std::cell::RefCell;
2use std::rc::Rc;
3
4use actix::{ActorResponse, ActorTryFutureExt, Handler, Message, WrapFuture};
5use borsh::BorshDeserialize;
6use calimero_context_primitives::client::ContextClient;
7use calimero_context_primitives::messages::{MigrationParams, UpdateApplicationRequest};
8use calimero_node_primitives::client::NodeClient;
9use calimero_prelude::ROOT_STORAGE_ENTRY_ID;
10use calimero_primitives::application::{Application, ApplicationId};
11use calimero_primitives::context::{Context, ContextId};
12use calimero_primitives::events::{
13 ContextEvent, ContextEventPayload, ExecutionEvent, NodeEvent, StateMutationPayload,
14};
15use calimero_primitives::hash::Hash;
16use calimero_primitives::identity::PublicKey;
17use calimero_runtime::logic::Event as RuntimeEvent;
18use calimero_storage::address::Id;
19use calimero_storage::delta::clear_pending_delta;
20use calimero_storage::entities::Metadata;
21use calimero_storage::env::{with_runtime_env, RuntimeEnv};
22use calimero_storage::error::StorageError;
23use calimero_storage::index::{EntityIndex, Index};
24use calimero_storage::store::{Key, MainStorage};
25use calimero_storage::Interface;
26use calimero_store::{key, types};
27use calimero_utils_actix::global_runtime;
28use eyre::bail;
29use tracing::{debug, error, info};
30
31use crate::handlers::execute::storage::ContextStorage;
32use crate::handlers::utils::StoreContextHost;
33use crate::ContextManager;
34
35impl Handler<UpdateApplicationRequest> for ContextManager {
36 type Result = ActorResponse<Self, <UpdateApplicationRequest as Message>::Result>;
37
38 fn handle(
39 &mut self,
40 UpdateApplicationRequest {
41 context_id,
42 application_id,
43 public_key,
44 migration,
45 }: UpdateApplicationRequest,
46 _ctx: &mut Self::Context,
47 ) -> Self::Result {
48 info!(
49 %context_id,
50 %application_id,
51 has_migration = migration.is_some(),
52 "Handling UpdateApplicationRequest"
53 );
54
55 let context_meta = self.contexts.get(&context_id).map(|c| c.meta.clone());
56
57 if migration.is_none() {
61 if let Some(ref context) = context_meta {
62 if application_id == context.application_id {
63 debug!(%context_id, "Application already set and no migration requested, skipping update");
64 return ActorResponse::reply(Ok(()));
65 }
66 }
67 }
68
69 let application = self.applications.get(&application_id).cloned();
70
71 if let Some(ref migration_params) = migration {
73 debug!(
74 %context_id,
75 %application_id,
76 method = %migration_params.method,
77 "Migration requested, loading new module"
78 );
79
80 if self.applications.remove(&application_id).is_some() {
85 debug!(
86 %context_id,
87 %application_id,
88 "Invalidated stale cached application before migration module load"
89 );
90 }
91
92 let datastore = self.datastore.clone();
94 let node_client = self.node_client.clone();
95 let context_client = self.context_client.clone();
96 let migration_params = migration_params.clone();
97
98 let module_task = self.get_module(application_id);
100
101 let task = module_task.and_then(move |module, act, _ctx| {
102 let datastore = datastore.clone();
103 let node_client = node_client.clone();
104 let context_client = context_client.clone();
105 let context_meta = act.contexts.get(&context_id).map(|c| c.meta.clone());
106 let application = act.applications.get(&application_id).cloned();
107
108 async move {
109 update_application_with_migration(
110 datastore,
111 node_client,
112 context_client,
113 context_id,
114 context_meta,
115 application_id,
116 application,
117 public_key,
118 Some(migration_params),
119 module,
120 )
121 .await
122 }
123 .into_actor(act)
124 });
125
126 return ActorResponse::r#async(task.map_ok(
127 move |(_application, updated_context), act, _ctx| {
128 if act.applications.remove(&application_id).is_some() {
131 debug!(%context_id, %application_id, "Invalidated cached application module after migration");
132 }
133
134 if let Some(cached) = act.contexts.get_mut(&context_id) {
135 debug!(
136 %context_id,
137 old_root = ?cached.meta.root_hash,
138 new_root = ?updated_context.root_hash,
139 "Updating cached context after migration"
140 );
141 cached.meta.application_id = application_id;
142 cached.meta.root_hash = updated_context.root_hash;
143 cached.meta.dag_heads = updated_context.dag_heads;
144 }
145 },
146 ));
147 }
148
149 let task = update_application_id(
151 self.datastore.clone(),
152 self.node_client.clone(),
153 self.context_client.clone(),
154 context_id,
155 context_meta,
156 application_id,
157 application,
158 public_key,
159 );
160
161 ActorResponse::r#async(task.into_actor(self).map_ok(move |application, act, _ctx| {
162 let _ignored = act
163 .applications
164 .entry(application_id)
165 .or_insert(application);
166
167 if let Some(context) = act.contexts.get_mut(&context_id) {
168 context.meta.application_id = application_id;
169 }
170 }))
171 }
172}
173
174fn resolve_context_and_application(
178 context_client: &ContextClient,
179 node_client: &NodeClient,
180 context_id: ContextId,
181 context: Option<Context>,
182 application_id: ApplicationId,
183 application: Option<Application>,
184) -> eyre::Result<(Context, Application)> {
185 let context = match context {
186 Some(context) => context,
187 None => {
188 let Some(context) = context_client.get_context(&context_id)? else {
189 bail!("context '{}' does not exist", context_id);
190 };
191
192 context
193 }
194 };
195
196 let application = match application {
197 Some(application) => application,
198 None => {
199 let Some(application) = node_client.get_application(&application_id)? else {
200 bail!("application with id '{}' not found", application_id);
201 };
202
203 application
204 }
205 };
206
207 Ok((context, application))
208}
209
210async fn finalize_application_update(
217 datastore: &calimero_store::Store,
218 node_client: &NodeClient,
219 context_client: &ContextClient,
220 context: &mut Context,
221 application: &Application,
222 public_key: PublicKey,
223) -> eyre::Result<()> {
224 let context_id = context.id;
225
226 let Some(config_client) = context_client.context_config(&context_id)? else {
227 bail!(
228 "missing context config parameters for context '{}'",
229 context_id
230 );
231 };
232
233 let external_client = context_client.external_client(&context_id, &config_client)?;
234
235 external_client
236 .config()
237 .update_application(&public_key, application)
238 .await?;
239
240 let mut handle = datastore.handle();
241
242 handle.put(
243 &key::ContextMeta::new(context.id),
244 &types::ContextMeta::new(
245 key::ApplicationMeta::new(application.id),
246 *context.root_hash,
247 context.dag_heads.clone(),
248 ),
249 )?;
250
251 node_client.sync(Some(&context_id), None).await?;
252
253 Ok(())
254}
255
256pub async fn update_application_id(
257 datastore: calimero_store::Store,
258 node_client: NodeClient,
259 context_client: ContextClient,
260 context_id: ContextId,
261 context: Option<Context>,
262 application_id: ApplicationId,
263 application: Option<Application>,
264 public_key: PublicKey,
265) -> eyre::Result<Application> {
266 let (mut context, application) = resolve_context_and_application(
267 &context_client,
268 &node_client,
269 context_id,
270 context,
271 application_id,
272 application,
273 )?;
274
275 verify_appkey_continuity(&datastore, &context, &application_id)?;
277
278 finalize_application_update(
279 &datastore,
280 &node_client,
281 &context_client,
282 &mut context,
283 &application,
284 public_key,
285 )
286 .await?;
287
288 Ok(application)
289}
290
291fn verify_appkey_continuity(
301 datastore: &calimero_store::Store,
302 context: &Context,
303 new_application_id: &ApplicationId,
304) -> eyre::Result<()> {
305 let handle = datastore.handle();
306
307 let old_app_key = key::ApplicationMeta::new(context.application_id);
309 let Some(old_app_meta) = handle.get(&old_app_key)? else {
310 debug!(
312 context_id = %context.id,
313 "No existing application found, skipping AppKey continuity check"
314 );
315 return Ok(());
316 };
317
318 let new_app_key = key::ApplicationMeta::new(*new_application_id);
320 let Some(new_app_meta) = handle.get(&new_app_key)? else {
321 bail!(
322 "new application with id '{}' not found in database",
323 new_application_id
324 );
325 };
326
327 let old_signer_id = old_app_meta.signer_id.as_ref();
331 let new_signer_id = new_app_meta.signer_id.as_ref();
332
333 if !old_signer_id.is_empty() && !new_signer_id.is_empty() && old_signer_id != new_signer_id {
335 error!(
336 context_id = %context.id,
337 old_signer_id = %old_signer_id,
338 new_signer_id = %new_signer_id,
339 "AppKey continuity violation: signerId mismatch"
340 );
341 bail!(
342 "AppKey continuity violation: signerId mismatch. \
343 Cannot update from signerId '{}' to '{}'. \
344 The same signing key must be used for application updates.",
345 old_signer_id,
346 new_signer_id
347 );
348 }
349
350 if !old_signer_id.is_empty() && new_signer_id.is_empty() {
353 error!(
354 context_id = %context.id,
355 old_signer_id = %old_signer_id,
356 "Security downgrade rejected: Cannot update from signed application to unsigned (legacy) application"
357 );
358 bail!(
359 "Security downgrade rejected: Cannot update from signed application (signerId: '{}') \
360 to unsigned (legacy) application. \
361 Signed-to-unsigned downgrades are disallowed to prevent security vulnerabilities. \
362 If you need to use a legacy unsigned application, you must create a new context.",
363 old_signer_id
364 );
365 }
366
367 if old_signer_id.is_empty() && !new_signer_id.is_empty() {
369 info!(
370 context_id = %context.id,
371 new_signer_id = %new_signer_id,
372 "Upgrading from unsigned (legacy) to signed application - security improvement"
373 );
374 }
375
376 debug!(
377 context_id = %context.id,
378 signer_id = %if old_signer_id.is_empty() { "<unsigned>" } else { old_signer_id },
379 "AppKey continuity check passed"
380 );
381
382 Ok(())
383}
384
385async fn update_application_with_migration(
394 datastore: calimero_store::Store,
395 node_client: NodeClient,
396 context_client: ContextClient,
397 context_id: ContextId,
398 context: Option<Context>,
399 application_id: ApplicationId,
400 application: Option<Application>,
401 public_key: PublicKey,
402 migration: Option<MigrationParams>,
403 module: calimero_runtime::Module,
404) -> eyre::Result<(Application, Context)> {
405 let (mut context, application) = resolve_context_and_application(
406 &context_client,
407 &node_client,
408 context_id,
409 context,
410 application_id,
411 application,
412 )?;
413
414 verify_appkey_continuity(&datastore, &context, &application_id)?;
416
417 if let Some(migration_params) = migration {
419 info!(
420 %context_id,
421 %application_id,
422 method = %migration_params.method,
423 "Executing migration"
424 );
425
426 let (new_state_bytes, migration_events, migration_logs) = execute_migration(
428 &datastore,
429 node_client.clone(),
430 &context,
431 module,
432 &migration_params,
433 public_key,
434 )
435 .await?;
436
437 for log_line in &migration_logs {
439 info!(%context_id, migration_log = %log_line, "Migration log");
440 }
441
442 let full_hash = write_migration_state(&datastore, &context, &new_state_bytes, public_key)?;
445
446 let new_root_hash = Hash::from(full_hash);
449 context.root_hash = new_root_hash;
450
451 context.dag_heads = vec![*context.root_hash.as_bytes()];
455 debug!(
456 %context_id,
457 new_root_hash = %new_root_hash,
458 "Updated dag_heads to new root after migration"
459 );
460
461 if !migration_events.is_empty() {
463 let events_vec: Vec<ExecutionEvent> = migration_events
464 .into_iter()
465 .map(|e| ExecutionEvent {
466 kind: e.kind,
467 data: e.data,
468 handler: e.handler,
469 })
470 .collect();
471 let _ = node_client.send_event(NodeEvent::Context(ContextEvent {
472 context_id,
473 payload: ContextEventPayload::StateMutation(
474 StateMutationPayload::with_root_and_events(new_root_hash, events_vec),
475 ),
476 }));
477 }
478
479 info!(
480 %context_id,
481 new_root_hash = %new_root_hash,
482 state_size = new_state_bytes.len(),
483 "Migration completed successfully"
484 );
485 }
486
487 finalize_application_update(
488 &datastore,
489 &node_client,
490 &context_client,
491 &mut context,
492 &application,
493 public_key,
494 )
495 .await?;
496
497 Ok((application, context))
498}
499
500async fn execute_migration(
505 datastore: &calimero_store::Store,
506 node_client: NodeClient,
507 context: &Context,
508 module: calimero_runtime::Module,
509 migration_params: &MigrationParams,
510 executor_identity: PublicKey,
511) -> eyre::Result<(Vec<u8>, Vec<RuntimeEvent>, Vec<String>)> {
512 let context_id = context.id;
513 let method = migration_params.method.clone();
514
515 debug!(
516 %context_id,
517 method = %method,
518 "Preparing to execute migration function"
519 );
520
521 let mut storage = ContextStorage::from(datastore.clone(), context_id);
523
524 let context_host = StoreContextHost {
526 store: datastore.clone(),
527 context_id,
528 };
529
530 let outcome = global_runtime()
534 .spawn_blocking(move || {
535 module.run(
536 context_id,
537 executor_identity,
538 &method,
539 &[],
541 &mut storage,
542 None,
543 Some(node_client),
544 Some(Box::new(context_host)),
545 )
546 })
547 .await
548 .map_err(|e| eyre::eyre!("Migration task failed: {}", e))??;
549
550 let returns = outcome
556 .returns
557 .map_err(|e| eyre::eyre!("Migration execution failed: {:?}", e))?;
558
559 let Some(new_state_bytes) = returns else {
560 bail!("Migration function did not return any data. Ensure the migration function returns the new state.");
561 };
562
563 debug!(
564 %context_id,
565 bytes_len = new_state_bytes.len(),
566 events_count = outcome.events.len(),
567 logs_count = outcome.logs.len(),
568 "Migration function returned new state"
569 );
570
571 Ok((new_state_bytes, outcome.events, outcome.logs))
572}
573
574struct StorageCallbacks {
579 read: Rc<dyn Fn(&Key) -> Option<Vec<u8>>>,
580 write: Rc<dyn Fn(Key, &[u8]) -> bool>,
581 remove: Rc<dyn Fn(&Key) -> bool>,
582}
583
584fn create_storage_callbacks(
589 datastore: &calimero_store::Store,
590 context_id: ContextId,
591) -> StorageCallbacks {
592 let read: Rc<dyn Fn(&Key) -> Option<Vec<u8>>> = {
593 let handle = datastore.handle();
594 let ctx_id = context_id;
595 Rc::new(move |key: &Key| {
596 let storage_key = key.to_bytes();
597 let state_key = key::ContextState::new(ctx_id, storage_key);
598 match handle.get(&state_key) {
599 Ok(Some(state)) => Some(state.value.into_boxed().into_vec()),
600 Ok(None) => None,
601 Err(e) => {
602 error!(
603 %ctx_id,
604 storage_key = ?storage_key,
605 error = ?e,
606 "Storage read failed during migration state write"
607 );
608 None
609 }
610 }
611 })
612 };
613
614 let write: Rc<dyn Fn(Key, &[u8]) -> bool> = {
615 let handle_cell: Rc<RefCell<_>> = Rc::new(RefCell::new(datastore.handle()));
616 let ctx_id = context_id;
617 Rc::new(move |key: Key, value: &[u8]| {
618 let storage_key = key.to_bytes();
619 let state_key = key::ContextState::new(ctx_id, storage_key);
620 let slice: calimero_store::slice::Slice<'_> = value.to_vec().into();
621 let state_value = types::ContextState::from(slice);
622 handle_cell
623 .borrow_mut()
624 .put(&state_key, &state_value)
625 .is_ok()
626 })
627 };
628
629 let remove: Rc<dyn Fn(&Key) -> bool> = {
630 let handle_cell: Rc<RefCell<_>> = Rc::new(RefCell::new(datastore.handle()));
631 let ctx_id = context_id;
632 Rc::new(move |key: &Key| {
633 let storage_key = key.to_bytes();
634 let state_key = key::ContextState::new(ctx_id, storage_key);
635 handle_cell.borrow_mut().delete(&state_key).is_ok()
636 })
637 };
638
639 StorageCallbacks {
640 read,
641 write,
642 remove,
643 }
644}
645
646fn compute_deterministic_metadata(
653 datastore: &calimero_store::Store,
654 context_id: ContextId,
655) -> Metadata {
656 let root_entry_id = Id::new(ROOT_STORAGE_ENTRY_ID);
657 let index_key = Key::Index(root_entry_id);
658 let storage_key = index_key.to_bytes();
659 let state_key = key::ContextState::new(context_id, storage_key);
660
661 let timestamp = match datastore.handle().get(&state_key) {
662 Ok(Some(state_data)) => {
663 match EntityIndex::try_from_slice(&state_data.value.into_boxed().into_vec()) {
664 Ok(existing_index) => {
665 let existing_updated = existing_index.metadata.updated_at();
668 let existing_created = existing_index.metadata.created_at();
669 existing_updated.max(existing_created).saturating_add(1)
670 }
671 Err(e) => {
672 error!(
673 %context_id,
674 error = ?e,
675 "Failed to deserialize existing index for deterministic timestamp, using fallback"
676 );
677 u64::MAX / 2
678 }
679 }
680 }
681 Ok(None) => {
682 u64::MAX / 2
685 }
686 Err(e) => {
687 error!(
688 %context_id,
689 error = ?e,
690 "Failed to read existing index for deterministic timestamp, using fallback"
691 );
692 u64::MAX / 2
693 }
694 };
695
696 Metadata::new(timestamp, timestamp)
697}
698
699fn build_entry_bytes(new_state_bytes: &[u8]) -> Vec<u8> {
706 let mut entry_bytes = Vec::with_capacity(new_state_bytes.len() + ROOT_STORAGE_ENTRY_ID.len());
707 entry_bytes.extend_from_slice(new_state_bytes);
708 entry_bytes.extend_from_slice(&ROOT_STORAGE_ENTRY_ID);
709 entry_bytes
710}
711
712fn write_migration_state(
720 datastore: &calimero_store::Store,
721 context: &Context,
722 new_state_bytes: &[u8],
723 executor_identity: PublicKey,
724) -> eyre::Result<[u8; 32]> {
725 let context_id = context.id;
726
727 debug!(
728 %context_id,
729 state_size = new_state_bytes.len(),
730 "Writing migrated state via storage layer"
731 );
732
733 let callbacks = create_storage_callbacks(datastore, context_id);
734 let metadata = compute_deterministic_metadata(datastore, context_id);
735 let entry_bytes = build_entry_bytes(new_state_bytes);
736
737 let context_id_bytes: [u8; 32] = *context_id.as_ref();
738 let executor_id_bytes: [u8; 32] = *executor_identity.as_ref();
739 let runtime_env = RuntimeEnv::new(
740 callbacks.read,
741 callbacks.write,
742 callbacks.remove,
743 context_id_bytes,
744 executor_id_bytes,
745 );
746
747 let root_entry_id = Id::new(ROOT_STORAGE_ENTRY_ID);
748 let result = with_runtime_env(runtime_env, || -> Result<_, StorageError> {
749 let write_result = (|| -> Result<_, StorageError> {
750 let save_result =
753 Interface::<MainStorage>::save_raw(root_entry_id, entry_bytes, metadata)?;
754
755 if save_result.is_none() {
756 return Ok(None);
757 }
758
759 let root_hash = Index::<MainStorage>::get_hashes_for(Id::root())?
762 .map(|(full_hash, _)| full_hash)
763 .unwrap_or([0; 32]);
764
765 Ok(Some(root_hash))
766 })();
767
768 clear_pending_delta();
773
774 write_result
775 });
776
777 match result {
778 Ok(Some(root_hash)) => {
779 debug!(
780 %context_id,
781 root_hash = ?root_hash,
782 "Migrated state written successfully with Index update"
783 );
784 Ok(root_hash)
785 }
786 Ok(None) => {
787 error!(
788 %context_id,
789 "Migration state write was unexpectedly skipped - timestamp conflict"
790 );
791 bail!(
792 "Migration state write was unexpectedly skipped - timestamp conflict. \
793 This indicates a concurrent update conflict that prevented the migration \
794 state from being written. The migration must be retried."
795 )
796 }
797 Err(e) => {
798 error!(
799 %context_id,
800 error = ?e,
801 "Failed to write migrated state"
802 );
803 bail!("Failed to write migrated state: {:?}", e)
804 }
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use std::sync::Arc;
811
812 use calimero_primitives::application::ApplicationId;
813 use calimero_primitives::context::{Context, ContextId};
814 use calimero_primitives::hash::Hash;
815 use calimero_store::db::InMemoryDB;
816 use calimero_store::{key, types, Store};
817
818 use super::verify_appkey_continuity;
819
820 fn create_test_store() -> Store {
822 let db = InMemoryDB::owned();
823 Store::new(Arc::new(db))
824 }
825
826 fn create_app_meta(signer_id: &str) -> types::ApplicationMeta {
828 types::ApplicationMeta::new(
829 key::BlobMeta::new([1u8; 32].into()),
830 1024,
831 "file://test.wasm".into(),
832 vec![].into(),
833 key::BlobMeta::new([2u8; 32].into()),
834 "com.test.app".into(),
835 "1.0.0".into(),
836 signer_id.into(),
837 )
838 }
839
840 fn create_test_context(context_id: ContextId, application_id: ApplicationId) -> Context {
842 Context::new(context_id, application_id, Hash::from([0u8; 32]))
843 }
844
845 #[test]
848 fn test_appkey_continuity_passes_with_matching_signer_ids() {
849 let store = create_test_store();
851 let signer_id = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
852
853 let old_app_id = ApplicationId::from([10u8; 32]);
855 let new_app_id = ApplicationId::from([20u8; 32]);
856
857 let old_app_meta = create_app_meta(signer_id);
859 let new_app_meta = create_app_meta(signer_id);
860
861 let mut handle = store.handle();
863 handle
864 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
865 .expect("Failed to store old app meta");
866 handle
867 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
868 .expect("Failed to store new app meta");
869
870 let context_id = ContextId::from([1u8; 32]);
872 let context = create_test_context(context_id, old_app_id);
873
874 let result = verify_appkey_continuity(&store, &context, &new_app_id);
876 assert!(
877 result.is_ok(),
878 "AppKey continuity check should pass with matching signerIds: {:?}",
879 result.err()
880 );
881 }
882
883 #[test]
884 fn test_appkey_continuity_passes_for_new_context_without_old_app() {
885 let store = create_test_store();
887 let new_signer_id = "did:key:z6MkNewSignerKey123456789";
888
889 let old_app_id = ApplicationId::from([10u8; 32]); let new_app_id = ApplicationId::from([20u8; 32]);
892
893 let new_app_meta = create_app_meta(new_signer_id);
895 let mut handle = store.handle();
896 handle
897 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
898 .expect("Failed to store new app meta");
899
900 let context_id = ContextId::from([1u8; 32]);
902 let context = create_test_context(context_id, old_app_id);
903
904 let result = verify_appkey_continuity(&store, &context, &new_app_id);
906 assert!(
907 result.is_ok(),
908 "AppKey continuity check should pass for new context: {:?}",
909 result.err()
910 );
911 }
912
913 #[test]
914 fn test_appkey_continuity_passes_with_empty_signer_ids_legacy_apps() {
915 let store = create_test_store();
917
918 let old_app_id = ApplicationId::from([10u8; 32]);
920 let new_app_id = ApplicationId::from([20u8; 32]);
921
922 let old_app_meta = create_app_meta("");
924 let new_app_meta = create_app_meta("");
925
926 let mut handle = store.handle();
928 handle
929 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
930 .expect("Failed to store old app meta");
931 handle
932 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
933 .expect("Failed to store new app meta");
934
935 let context_id = ContextId::from([1u8; 32]);
937 let context = create_test_context(context_id, old_app_id);
938
939 let result = verify_appkey_continuity(&store, &context, &new_app_id);
941 assert!(
942 result.is_ok(),
943 "AppKey continuity check should pass for legacy apps: {:?}",
944 result.err()
945 );
946 }
947
948 #[test]
949 fn test_appkey_continuity_passes_when_upgrading_from_unsigned_to_signed() {
950 let store = create_test_store();
952 let new_signer_id = "did:key:z6MkNewSignerKey123456789";
953
954 let old_app_id = ApplicationId::from([10u8; 32]);
956 let new_app_id = ApplicationId::from([20u8; 32]);
957
958 let old_app_meta = create_app_meta("");
960 let new_app_meta = create_app_meta(new_signer_id);
961
962 let mut handle = store.handle();
964 handle
965 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
966 .expect("Failed to store old app meta");
967 handle
968 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
969 .expect("Failed to store new app meta");
970
971 let context_id = ContextId::from([1u8; 32]);
973 let context = create_test_context(context_id, old_app_id);
974
975 let result = verify_appkey_continuity(&store, &context, &new_app_id);
977 assert!(
978 result.is_ok(),
979 "AppKey continuity check should pass when upgrading from unsigned to signed: {:?}",
980 result.err()
981 );
982 }
983
984 #[test]
985 fn test_appkey_continuity_rejects_downgrade_from_signed_to_unsigned() {
986 let store = create_test_store();
989 let old_signer_id = "did:key:z6MkOldSignerKey123456789";
990
991 let old_app_id = ApplicationId::from([10u8; 32]);
993 let new_app_id = ApplicationId::from([20u8; 32]);
994
995 let old_app_meta = create_app_meta(old_signer_id);
997 let new_app_meta = create_app_meta(""); let mut handle = store.handle();
1001 handle
1002 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1003 .expect("Failed to store old app meta");
1004 handle
1005 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1006 .expect("Failed to store new app meta");
1007
1008 let context_id = ContextId::from([1u8; 32]);
1010 let context = create_test_context(context_id, old_app_id);
1011
1012 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1014 assert!(
1015 result.is_err(),
1016 "AppKey continuity check should reject downgrade from signed to unsigned: {:?}",
1017 result
1018 );
1019
1020 let error_message = result.unwrap_err().to_string();
1022 assert!(
1023 error_message.contains("Security downgrade rejected"),
1024 "Error should mention security downgrade rejection: {}",
1025 error_message
1026 );
1027 assert!(
1028 error_message.contains("signed application"),
1029 "Error should mention signed application: {}",
1030 error_message
1031 );
1032 }
1033
1034 #[test]
1035 fn test_appkey_continuity_passes_when_updating_same_application() {
1036 let store = create_test_store();
1038 let signer_id = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
1039
1040 let old_app_id = ApplicationId::from([10u8; 32]);
1043 let new_app_id = ApplicationId::from([11u8; 32]); let old_app_meta = create_app_meta(signer_id);
1047 let new_app_meta = create_app_meta(signer_id);
1048
1049 let mut handle = store.handle();
1050 handle
1051 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1052 .expect("Failed to store old app meta");
1053 handle
1054 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1055 .expect("Failed to store new app meta");
1056
1057 let context_id = ContextId::from([1u8; 32]);
1058 let context = create_test_context(context_id, old_app_id);
1059
1060 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1062 assert!(
1063 result.is_ok(),
1064 "Standard application upgrade should pass: {:?}",
1065 result.err()
1066 );
1067 }
1068
1069 #[test]
1072 fn test_appkey_continuity_fails_with_mismatched_signer_ids() {
1073 let store = create_test_store();
1075 let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1076 let new_signer_id = "did:key:z6MkNewSignerKey987654321";
1077
1078 let old_app_id = ApplicationId::from([10u8; 32]);
1080 let new_app_id = ApplicationId::from([20u8; 32]);
1081
1082 let old_app_meta = create_app_meta(old_signer_id);
1084 let new_app_meta = create_app_meta(new_signer_id);
1085
1086 let mut handle = store.handle();
1088 handle
1089 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1090 .expect("Failed to store old app meta");
1091 handle
1092 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1093 .expect("Failed to store new app meta");
1094
1095 let context_id = ContextId::from([1u8; 32]);
1097 let context = create_test_context(context_id, old_app_id);
1098
1099 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1101 assert!(
1102 result.is_err(),
1103 "AppKey continuity check should fail with mismatched signerIds"
1104 );
1105
1106 let error_message = result.unwrap_err().to_string();
1108 assert!(
1109 error_message.contains("AppKey continuity violation"),
1110 "Error should mention AppKey continuity violation: {}",
1111 error_message
1112 );
1113 assert!(
1114 error_message.contains("signerId mismatch"),
1115 "Error should mention signerId mismatch: {}",
1116 error_message
1117 );
1118 }
1119
1120 #[test]
1121 fn test_appkey_continuity_fails_when_new_app_not_found() {
1122 let store = create_test_store();
1124 let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1125
1126 let old_app_id = ApplicationId::from([10u8; 32]);
1128 let new_app_id = ApplicationId::from([20u8; 32]); let old_app_meta = create_app_meta(old_signer_id);
1132 let mut handle = store.handle();
1133 handle
1134 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1135 .expect("Failed to store old app meta");
1136
1137 let context_id = ContextId::from([1u8; 32]);
1139 let context = create_test_context(context_id, old_app_id);
1140
1141 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1143 assert!(
1144 result.is_err(),
1145 "AppKey continuity check should fail when new app not found"
1146 );
1147
1148 let error_message = result.unwrap_err().to_string();
1150 assert!(
1151 error_message.contains("not found"),
1152 "Error should mention app not found: {}",
1153 error_message
1154 );
1155 }
1156
1157 #[test]
1158 fn test_appkey_continuity_prevents_hijacking_attempt() {
1159 let store = create_test_store();
1161 let legitimate_signer = "did:key:z6MkLegitimatePublisher1234567890";
1162 let attacker_signer = "did:key:z6MkAttackerTryingToHijack999999";
1163
1164 let old_app_id = ApplicationId::from([10u8; 32]);
1166 let attacker_app_id = ApplicationId::from([99u8; 32]);
1167
1168 let legitimate_app_meta = create_app_meta(legitimate_signer);
1170 let attacker_app_meta = create_app_meta(attacker_signer);
1171
1172 let mut handle = store.handle();
1174 handle
1175 .put(&key::ApplicationMeta::new(old_app_id), &legitimate_app_meta)
1176 .expect("Failed to store legitimate app meta");
1177 handle
1178 .put(
1179 &key::ApplicationMeta::new(attacker_app_id),
1180 &attacker_app_meta,
1181 )
1182 .expect("Failed to store attacker app meta");
1183
1184 let context_id = ContextId::from([1u8; 32]);
1186 let context = create_test_context(context_id, old_app_id);
1187
1188 let result = verify_appkey_continuity(&store, &context, &attacker_app_id);
1190 assert!(
1191 result.is_err(),
1192 "AppKey continuity check should prevent hijacking attempt"
1193 );
1194
1195 let error_message = result.unwrap_err().to_string();
1196 assert!(
1197 error_message.contains("signerId mismatch"),
1198 "Error should indicate signerId mismatch: {}",
1199 error_message
1200 );
1201 }
1202
1203 #[test]
1204 fn test_appkey_continuity_is_case_sensitive() {
1205 let store = create_test_store();
1208 let legitimate_signer = "did:key:z6MkABCDEFGH123456789";
1209 let case_modified_signer = "did:key:z6Mkabcdefgh123456789"; let old_app_id = ApplicationId::from([10u8; 32]);
1212 let new_app_id = ApplicationId::from([20u8; 32]);
1213
1214 let old_app_meta = create_app_meta(legitimate_signer);
1215 let new_app_meta = create_app_meta(case_modified_signer);
1216
1217 let mut handle = store.handle();
1218 handle
1219 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1220 .expect("Failed to store old app meta");
1221 handle
1222 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1223 .expect("Failed to store new app meta");
1224
1225 let context_id = ContextId::from([1u8; 32]);
1226 let context = create_test_context(context_id, old_app_id);
1227
1228 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1230 assert!(
1231 result.is_err(),
1232 "SignerId comparison must be case-sensitive"
1233 );
1234 }
1235
1236 #[test]
1237 fn test_appkey_continuity_fails_with_similar_looking_signer_ids() {
1238 let store = create_test_store();
1241 let legitimate_signer = "did:key:z6MkPublisher0123456789";
1242 let similar_signer = "did:key:z6MkPublisherO123456789"; let old_app_id = ApplicationId::from([10u8; 32]);
1245 let new_app_id = ApplicationId::from([20u8; 32]);
1246
1247 let old_app_meta = create_app_meta(legitimate_signer);
1248 let new_app_meta = create_app_meta(similar_signer);
1249
1250 let mut handle = store.handle();
1251 handle
1252 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1253 .expect("Failed to store old app meta");
1254 handle
1255 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1256 .expect("Failed to store new app meta");
1257
1258 let context_id = ContextId::from([1u8; 32]);
1259 let context = create_test_context(context_id, old_app_id);
1260
1261 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1263 assert!(
1264 result.is_err(),
1265 "Similar-looking signerIds must still be rejected"
1266 );
1267 }
1268
1269 #[test]
1270 fn test_appkey_continuity_fails_with_whitespace_differences() {
1271 let store = create_test_store();
1273 let legitimate_signer = "did:key:z6MkPublisher123";
1274 let whitespace_signer = "did:key:z6MkPublisher123 "; let old_app_id = ApplicationId::from([10u8; 32]);
1277 let new_app_id = ApplicationId::from([20u8; 32]);
1278
1279 let old_app_meta = create_app_meta(legitimate_signer);
1280 let new_app_meta = create_app_meta(whitespace_signer);
1281
1282 let mut handle = store.handle();
1283 handle
1284 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1285 .expect("Failed to store old app meta");
1286 handle
1287 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1288 .expect("Failed to store new app meta");
1289
1290 let context_id = ContextId::from([1u8; 32]);
1291 let context = create_test_context(context_id, old_app_id);
1292
1293 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1295 assert!(
1296 result.is_err(),
1297 "SignerIds with whitespace differences must be rejected"
1298 );
1299 }
1300
1301 #[test]
1302 fn test_appkey_continuity_rejects_prefix_attack() {
1303 let store = create_test_store();
1305 let legitimate_signer = "did:key:z6MkPublisher123456789";
1306 let prefix_signer = "did:key:z6MkPublisher123"; let old_app_id = ApplicationId::from([10u8; 32]);
1309 let new_app_id = ApplicationId::from([20u8; 32]);
1310
1311 let old_app_meta = create_app_meta(legitimate_signer);
1312 let new_app_meta = create_app_meta(prefix_signer);
1313
1314 let mut handle = store.handle();
1315 handle
1316 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1317 .expect("Failed to store old app meta");
1318 handle
1319 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1320 .expect("Failed to store new app meta");
1321
1322 let context_id = ContextId::from([1u8; 32]);
1323 let context = create_test_context(context_id, old_app_id);
1324
1325 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1327 assert!(result.is_err(), "Prefix signerId attack must be rejected");
1328 }
1329
1330 #[test]
1333 fn test_no_state_written_when_appkey_continuity_fails() {
1334 let store = create_test_store();
1336 let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1337 let new_signer_id = "did:key:z6MkNewSignerKey987654321";
1338
1339 let old_app_id = ApplicationId::from([10u8; 32]);
1341 let new_app_id = ApplicationId::from([20u8; 32]);
1342 let context_id = ContextId::from([1u8; 32]);
1343
1344 let old_app_meta = create_app_meta(old_signer_id);
1346 let new_app_meta = create_app_meta(new_signer_id);
1347
1348 let initial_state = b"initial_state_data";
1350 let root_key = calimero_storage::constants::root_storage_key();
1351 let state_key = key::ContextState::new(context_id, root_key);
1352 let state_value =
1353 types::ContextState::from(calimero_store::slice::Slice::from(initial_state.to_vec()));
1354
1355 {
1357 let mut handle = store.handle();
1358 handle
1359 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1360 .expect("Failed to store old app meta");
1361 handle
1362 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1363 .expect("Failed to store new app meta");
1364 handle
1365 .put(&state_key, &state_value)
1366 .expect("Failed to store initial state");
1367 }
1368
1369 let context = create_test_context(context_id, old_app_id);
1371
1372 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1374 assert!(result.is_err(), "AppKey continuity check should fail");
1375
1376 let handle = store.handle();
1378 let stored_state: Option<types::ContextState> =
1379 handle.get(&state_key).expect("Failed to read state");
1380
1381 assert!(
1382 stored_state.is_some(),
1383 "Original state should still exist after failed update"
1384 );
1385
1386 let stored_bytes: &[u8] = stored_state.as_ref().unwrap().as_ref();
1387 assert_eq!(
1388 stored_bytes, initial_state,
1389 "State should be unchanged after failed AppKey continuity check"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_context_meta_unchanged_when_update_fails() {
1395 let store = create_test_store();
1397 let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1398 let new_signer_id = "did:key:z6MkDifferentSignerKey000000";
1399
1400 let old_app_id = ApplicationId::from([10u8; 32]);
1402 let new_app_id = ApplicationId::from([20u8; 32]);
1403 let context_id = ContextId::from([1u8; 32]);
1404
1405 let old_app_meta = create_app_meta(old_signer_id);
1407 let new_app_meta = create_app_meta(new_signer_id);
1408
1409 let original_root_hash = Hash::from([42u8; 32]);
1411 let context_meta = types::ContextMeta::new(
1412 key::ApplicationMeta::new(old_app_id),
1413 *original_root_hash,
1414 vec![],
1415 );
1416
1417 {
1419 let mut handle = store.handle();
1420 handle
1421 .put(&key::ApplicationMeta::new(old_app_id), &old_app_meta)
1422 .expect("Failed to store old app meta");
1423 handle
1424 .put(&key::ApplicationMeta::new(new_app_id), &new_app_meta)
1425 .expect("Failed to store new app meta");
1426 handle
1427 .put(&key::ContextMeta::new(context_id), &context_meta)
1428 .expect("Failed to store context meta");
1429 }
1430
1431 let context = create_test_context(context_id, old_app_id);
1433
1434 let result = verify_appkey_continuity(&store, &context, &new_app_id);
1436 assert!(result.is_err(), "AppKey continuity check should fail");
1437
1438 let handle = store.handle();
1440 let stored_meta: Option<types::ContextMeta> = handle
1441 .get(&key::ContextMeta::new(context_id))
1442 .expect("Failed to read context meta");
1443
1444 assert!(
1445 stored_meta.is_some(),
1446 "Context metadata should still exist after failed update"
1447 );
1448
1449 let meta = stored_meta.unwrap();
1450 assert_eq!(
1451 meta.root_hash, *original_root_hash,
1452 "Context root_hash should be unchanged after failed update"
1453 );
1454 }
1455
1456 #[test]
1457 fn test_multiple_failed_updates_preserve_original_state() {
1458 let store = create_test_store();
1460 let legitimate_signer = "did:key:z6MkLegitimatePublisher1234567890";
1461
1462 let attacker_signers = [
1464 "did:key:z6MkAttacker1111111111111111111111",
1465 "did:key:z6MkAttacker2222222222222222222222",
1466 "did:key:z6MkAttacker3333333333333333333333",
1467 ];
1468
1469 let old_app_id = ApplicationId::from([10u8; 32]);
1471 let context_id = ContextId::from([1u8; 32]);
1472
1473 let legitimate_app_meta = create_app_meta(legitimate_signer);
1475 {
1476 let mut handle = store.handle();
1477 handle
1478 .put(&key::ApplicationMeta::new(old_app_id), &legitimate_app_meta)
1479 .expect("Failed to store legitimate app meta");
1480 }
1481
1482 let original_state = b"precious_application_state";
1484 let root_key = calimero_storage::constants::root_storage_key();
1485 let state_key = key::ContextState::new(context_id, root_key);
1486 {
1487 let mut handle = store.handle();
1488 handle
1489 .put(
1490 &state_key,
1491 &types::ContextState::from(calimero_store::slice::Slice::from(
1492 original_state.to_vec(),
1493 )),
1494 )
1495 .expect("Failed to store initial state");
1496 }
1497
1498 let context = create_test_context(context_id, old_app_id);
1500
1501 for (i, attacker_signer) in attacker_signers.iter().enumerate() {
1503 let attacker_app_id = ApplicationId::from([(100 + i as u8); 32]);
1504 let attacker_app_meta = create_app_meta(attacker_signer);
1505
1506 {
1508 let mut handle = store.handle();
1509 handle
1510 .put(
1511 &key::ApplicationMeta::new(attacker_app_id),
1512 &attacker_app_meta,
1513 )
1514 .expect("Failed to store attacker app meta");
1515 }
1516
1517 let result = verify_appkey_continuity(&store, &context, &attacker_app_id);
1519 assert!(
1520 result.is_err(),
1521 "Attempt {} should fail AppKey continuity check",
1522 i + 1
1523 );
1524 }
1525
1526 let handle = store.handle();
1528 let stored_state: Option<types::ContextState> =
1529 handle.get(&state_key).expect("Failed to read state");
1530
1531 assert!(
1532 stored_state.is_some(),
1533 "State should still exist after multiple failed attacks"
1534 );
1535
1536 let stored_bytes: &[u8] = stored_state.as_ref().unwrap().as_ref();
1537 assert_eq!(
1538 stored_bytes, original_state,
1539 "State should be completely unchanged after multiple failed hijacking attempts"
1540 );
1541 }
1542}