Skip to main content

calimero_context/handlers/
update_application.rs

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        // Skip update only when the application ID is unchanged AND no migration is requested.
58        // When migration IS requested, the WASM binary may have been replaced under the same
59        // application ID (same signing key), so we must proceed to run the migration function.
60        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 migration is requested, we need to load the module first
72        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            // Invalidate the cached application entry BEFORE loading the module.
81            // The WASM binary may have been replaced under the same application ID
82            // (same signing key, new version), so we must force a fresh fetch from
83            // the node's blob storage to get the updated bytecode.
84            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            // Clone values needed for migration
93            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            // Load the (fresh) module
99            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                    // Invalidate cached module so the next execution loads the new WASM from the node.
129                    // Otherwise we would keep using the pre-migration (v1) module for execute calls.
130                    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        // No migration - use existing update path
150        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
174/// Resolves context and application from optional values or fetches them if missing.
175///
176/// Returns the resolved context and application, or an error if they don't exist.
177fn 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
210/// Finalizes the application update by updating external config, datastore, and syncing.
211///
212/// This function performs the common post-update steps:
213/// 1. Updates the external config client
214/// 2. Writes context metadata to datastore
215/// 3. Triggers node sync
216async 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 (signerId match)
276    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
291/// Verifies AppKey continuity by checking that the signerId matches between
292/// the currently installed application and the new application.
293///
294///  An update MUST be accepted only if:
295/// - candidate.signerId == installed.signerId, OR
296/// - candidate.signerId is permitted by key lineage (future extension), OR
297/// - context governance explicitly authorizes a signer switch
298///
299/// This check MUST occur BEFORE any migration logic executes.
300fn 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    // Get current application's metadata
308    let old_app_key = key::ApplicationMeta::new(context.application_id);
309    let Some(old_app_meta) = handle.get(&old_app_key)? else {
310        // If no old application exists (new context), allow the update
311        debug!(
312            context_id = %context.id,
313            "No existing application found, skipping AppKey continuity check"
314        );
315        return Ok(());
316    };
317
318    // Get new application's metadata
319    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    // Check signerId continuity
328    // Note: Empty signer_id is used as a sentinel for legacy non-bundle applications.
329    // We allow updates from/to legacy applications with empty signer_id for backwards compatibility.
330    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 both have non-empty signer_ids, they must match
334    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    // Security: Disallow signed-to-unsigned downgrades
351    // Allow unsigned-to-signed upgrades
352    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    // Warn if upgrading from unsigned to signed (allowed, but log for audit)
368    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
385/// Update application with migration execution.
386///
387/// This function implements the full migration flow:
388/// 1. Validates AppKey continuity (signerId match)
389/// 2. Loads the NEW application WASM module
390/// 3. Executes the migration function
391/// 4. Writes returned state bytes to root storage key
392/// 5. Updates context metadata and triggers sync
393async 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 (signerId match)
415    verify_appkey_continuity(&datastore, &context, &application_id)?;
416
417    // Execute migration if requested
418    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        // Execute migration function via module.run()
427        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        // Log migration logs
438        for log_line in &migration_logs {
439            info!(%context_id, migration_log = %log_line, "Migration log");
440        }
441
442        // Write returned state bytes to root storage key
443        // This uses the storage layer to properly update both Entry and Index
444        let full_hash = write_migration_state(&datastore, &context, &new_state_bytes, public_key)?;
445
446        // Update root_hash after migration: full_hash is already the Merkle tree hash from
447        // the storage layer; wrap the bytes directly (same as create_context/execute).
448        let new_root_hash = Hash::from(full_hash);
449        context.root_hash = new_root_hash;
450
451        // Align DAG heads with the new state. Migration does not create a causal delta,
452        // so use root_hash as dag_head fallback (same as execute flow when init() creates
453        // state without actions). This keeps sync protocol consistent and avoids divergence.
454        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        // Emit migration events to WebSocket clients
462        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
500/// Execute the migration function in the new WASM module.
501///
502/// The migration function reads old state via `read_raw()` and returns new state bytes.
503/// Also returns events and logs produced by the migration so the caller can emit them and log them.
504async 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    // Create storage for the migration execution
522    let mut storage = ContextStorage::from(datastore.clone(), context_id);
523
524    // Create host context for membership queries
525    let context_host = StoreContextHost {
526        store: datastore.clone(),
527        context_id,
528    };
529
530    // Execute the migration function in a blocking task
531    // Migration functions take no parameters - context is accessed via host functions
532    // Use the update requestor's identity as executor for proper audit trail and authorization
533    let outcome = global_runtime()
534        .spawn_blocking(move || {
535            module.run(
536                context_id,
537                executor_identity,
538                &method,
539                // Empty input - migration functions read old state via read_raw()
540                &[],
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    // Extract the return value from the outcome.
551    // `outcome.returns` is `Result<Option<Vec<u8>>, FunctionCallError>` where the
552    // Ok/Err discrimination is already handled by the `value_return` host function.
553    // The inner `Vec<u8>` is the raw borsh-serialized new state bytes — NOT a
554    // borsh-serialized `Result<Vec<u8>, Vec<u8>>`.
555    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
574/// Storage callback closures used by the `calimero-storage` runtime environment.
575///
576/// These closures bridge the `calimero-storage` [`Key`]-based interface to the
577/// underlying `calimero-store` [`key::ContextState`]-based KV store.
578struct 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
584/// Create storage callback closures that route `calimero-storage` operations to the datastore.
585///
586/// Each callback translates a storage-layer [`Key`] into a context-scoped
587/// [`key::ContextState`] and forwards the operation to the store handle.
588fn 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
646/// Compute a deterministic [`Metadata`] timestamp from the existing root index.
647///
648/// Reads the current root entry's [`EntityIndex`] from the store and picks a
649/// timestamp strictly greater than any existing `created_at`/`updated_at` value.
650/// If the index cannot be read or deserialized, a large deterministic fallback
651/// (`u64::MAX / 2`) is used so every node converges on the same value.
652fn 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                    // Use max(existing_updated_at + 1, existing_created_at + 1) to ensure
666                    // the new timestamp is strictly greater than any existing timestamp
667                    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            // No existing metadata — use a large deterministic value so the
683            // migration timestamp is newer than any possible prior state.
684            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
699/// Build the full entry byte vector expected by the storage layer.
700///
701/// The storage layer persists root state entries as `Entry<T> = borsh(T) ++ borsh(Element.id)`.
702/// The migration function returns only `borsh(T)` (user data), so we re-append the 32-byte
703/// [`ROOT_STORAGE_ENTRY_ID`] suffix so the data round-trips through the normal fetch path
704/// (`Root::fetch` → `Collection::get` → `find_by_id::<Entry<T>>`).
705fn 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
712/// Write migrated state bytes to the root storage key, properly updating both Entry and Index.
713///
714/// This function uses the `calimero-storage` layer to ensure the Merkle tree Index is
715/// updated along with the Entry data. This maintains consistency for the sync protocol.
716///
717/// Returns the Merkle tree root's `full_hash` (from `Id::root()`), matching the hash
718/// computation used by the normal execution flow in `system.rs`.
719fn 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            // Write the entry — this updates the entry's Index and propagates hashes
751            // up to the Merkle tree root via recalculate_ancestor_hashes_for.
752            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            // Read the Merkle tree root hash — same as the normal execution flow
760            // save_raw returns the *entry node's* full_hash, not the tree root's.
761            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        // `save_raw` pushes sync actions into thread-local DELTA_CONTEXT. This
769        // migration path does not emit a delta artifact, so explicitly discard
770        // pending actions to avoid contaminating subsequent operations on the
771        // same runtime thread.
772        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    /// Creates a test store with in-memory database.
821    fn create_test_store() -> Store {
822        let db = InMemoryDB::owned();
823        Store::new(Arc::new(db))
824    }
825
826    /// Creates a test ApplicationMeta with the given signer_id.
827    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    /// Creates a test Context with the given application_id.
841    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 migration succeeds with valid signerId
846
847    #[test]
848    fn test_appkey_continuity_passes_with_matching_signer_ids() {
849        // Setup: Create store and two applications with the same signerId
850        let store = create_test_store();
851        let signer_id = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
852
853        // Create old and new application IDs
854        let old_app_id = ApplicationId::from([10u8; 32]);
855        let new_app_id = ApplicationId::from([20u8; 32]);
856
857        // Create application metadata with the same signerId for both
858        let old_app_meta = create_app_meta(signer_id);
859        let new_app_meta = create_app_meta(signer_id);
860
861        // Store both applications in the database
862        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        // Create a context that uses the old application
871        let context_id = ContextId::from([1u8; 32]);
872        let context = create_test_context(context_id, old_app_id);
873
874        // Verify AppKey continuity passes
875        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        // Setup: Create store with only the new application
886        let store = create_test_store();
887        let new_signer_id = "did:key:z6MkNewSignerKey123456789";
888
889        // Create only the new application ID
890        let old_app_id = ApplicationId::from([10u8; 32]); // This won't exist in the store
891        let new_app_id = ApplicationId::from([20u8; 32]);
892
893        // Store only the new application
894        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        // Create a context that references a non-existent old application
901        let context_id = ContextId::from([1u8; 32]);
902        let context = create_test_context(context_id, old_app_id);
903
904        // Verify AppKey continuity passes (new context case)
905        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        // Setup: Test backward compatibility with legacy unsigned applications
916        let store = create_test_store();
917
918        // Create old and new application IDs
919        let old_app_id = ApplicationId::from([10u8; 32]);
920        let new_app_id = ApplicationId::from([20u8; 32]);
921
922        // Create application metadata with empty signerIds (legacy applications)
923        let old_app_meta = create_app_meta("");
924        let new_app_meta = create_app_meta("");
925
926        // Store both applications in the database
927        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        // Create a context that uses the old application
936        let context_id = ContextId::from([1u8; 32]);
937        let context = create_test_context(context_id, old_app_id);
938
939        // Verify AppKey continuity passes (legacy to legacy is allowed)
940        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        // Setup: Test upgrading from unsigned (legacy) to signed application
951        let store = create_test_store();
952        let new_signer_id = "did:key:z6MkNewSignerKey123456789";
953
954        // Create old and new application IDs
955        let old_app_id = ApplicationId::from([10u8; 32]);
956        let new_app_id = ApplicationId::from([20u8; 32]);
957
958        // Create old app with empty signerId (legacy) and new app with signerId
959        let old_app_meta = create_app_meta("");
960        let new_app_meta = create_app_meta(new_signer_id);
961
962        // Store both applications in the database
963        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        // Create a context that uses the old application
972        let context_id = ContextId::from([1u8; 32]);
973        let context = create_test_context(context_id, old_app_id);
974
975        // Verify AppKey continuity passes (unsigned to signed is allowed with warning)
976        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        // Setup: Test downgrading from signed to unsigned (legacy) application
987        // Security: This is explicitly rejected to prevent security vulnerabilities
988        let store = create_test_store();
989        let old_signer_id = "did:key:z6MkOldSignerKey123456789";
990
991        // Create old and new application IDs
992        let old_app_id = ApplicationId::from([10u8; 32]);
993        let new_app_id = ApplicationId::from([20u8; 32]);
994
995        // Create old app with signerId and new app without signerId (legacy)
996        let old_app_meta = create_app_meta(old_signer_id);
997        let new_app_meta = create_app_meta(""); // Empty signerId (legacy)
998
999        // Store both applications in the database
1000        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        // Create a context that uses the old application
1009        let context_id = ContextId::from([1u8; 32]);
1010        let context = create_test_context(context_id, old_app_id);
1011
1012        // Verify AppKey continuity rejects signed-to-unsigned downgrade
1013        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        // Verify the error message contains the expected content
1021        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        // Setup: Test updating to a newer version of the same application (same signerId)
1037        let store = create_test_store();
1038        let signer_id = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
1039
1040        // In practice, same application with different version would have different ApplicationId
1041        // but same signerId - this is the standard upgrade path
1042        let old_app_id = ApplicationId::from([10u8; 32]);
1043        let new_app_id = ApplicationId::from([11u8; 32]); // Different version = different hash
1044
1045        // Both versions have the same signerId (same publisher)
1046        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        // Standard upgrade should pass
1061        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 migration rejected with signerId mismatch
1070
1071    #[test]
1072    fn test_appkey_continuity_fails_with_mismatched_signer_ids() {
1073        // Setup: Create store and two applications with different signerIds
1074        let store = create_test_store();
1075        let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1076        let new_signer_id = "did:key:z6MkNewSignerKey987654321";
1077
1078        // Create old and new application IDs
1079        let old_app_id = ApplicationId::from([10u8; 32]);
1080        let new_app_id = ApplicationId::from([20u8; 32]);
1081
1082        // Create application metadata with different signerIds
1083        let old_app_meta = create_app_meta(old_signer_id);
1084        let new_app_meta = create_app_meta(new_signer_id);
1085
1086        // Store both applications in the database
1087        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        // Create a context that uses the old application
1096        let context_id = ContextId::from([1u8; 32]);
1097        let context = create_test_context(context_id, old_app_id);
1098
1099        // Verify AppKey continuity fails
1100        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        // Verify the error message contains the expected content
1107        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        // Setup: Create store with only the old application
1123        let store = create_test_store();
1124        let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1125
1126        // Create application IDs
1127        let old_app_id = ApplicationId::from([10u8; 32]);
1128        let new_app_id = ApplicationId::from([20u8; 32]); // This won't exist in the store
1129
1130        // Store only the old application
1131        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        // Create a context that uses the old application
1138        let context_id = ContextId::from([1u8; 32]);
1139        let context = create_test_context(context_id, old_app_id);
1140
1141        // Verify AppKey continuity fails because new app doesn't exist
1142        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        // Verify the error message mentions the new app not being found
1149        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        // Setup: Simulate an attacker trying to hijack an app with a different signerId
1160        let store = create_test_store();
1161        let legitimate_signer = "did:key:z6MkLegitimatePublisher1234567890";
1162        let attacker_signer = "did:key:z6MkAttackerTryingToHijack999999";
1163
1164        // Create application IDs
1165        let old_app_id = ApplicationId::from([10u8; 32]);
1166        let attacker_app_id = ApplicationId::from([99u8; 32]);
1167
1168        // Create legitimate app and attacker's app with different signerIds
1169        let legitimate_app_meta = create_app_meta(legitimate_signer);
1170        let attacker_app_meta = create_app_meta(attacker_signer);
1171
1172        // Store both applications
1173        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        // Create a context using the legitimate application
1185        let context_id = ContextId::from([1u8; 32]);
1186        let context = create_test_context(context_id, old_app_id);
1187
1188        // Attacker tries to update to their malicious application
1189        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        // Security test: Verify that signerId comparison is case-sensitive
1206        // An attacker should not be able to bypass check by changing case
1207        let store = create_test_store();
1208        let legitimate_signer = "did:key:z6MkABCDEFGH123456789";
1209        let case_modified_signer = "did:key:z6Mkabcdefgh123456789"; // Same but lowercase
1210
1211        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        // Case-modified signerId should be rejected
1229        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        // Security test: Verify that similar-looking signerIds are still rejected
1239        // Attackers might try using visually similar characters
1240        let store = create_test_store();
1241        let legitimate_signer = "did:key:z6MkPublisher0123456789";
1242        let similar_signer = "did:key:z6MkPublisherO123456789"; // 'O' instead of '0'
1243
1244        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        // Similar-looking signerId should still be rejected
1262        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        // Security test: Verify that whitespace variations are rejected
1272        let store = create_test_store();
1273        let legitimate_signer = "did:key:z6MkPublisher123";
1274        let whitespace_signer = "did:key:z6MkPublisher123 "; // Trailing space
1275
1276        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        // SignerId with whitespace difference should be rejected
1294        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        // Security test: Attacker tries to use signerId that is a prefix/suffix
1304        let store = create_test_store();
1305        let legitimate_signer = "did:key:z6MkPublisher123456789";
1306        let prefix_signer = "did:key:z6MkPublisher123"; // Prefix of legitimate
1307
1308        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        // Prefix signerId should be rejected
1326        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 rollback on migration failure
1331
1332    #[test]
1333    fn test_no_state_written_when_appkey_continuity_fails() {
1334        // Setup: Verify that no state changes occur when AppKey continuity check fails
1335        let store = create_test_store();
1336        let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1337        let new_signer_id = "did:key:z6MkNewSignerKey987654321";
1338
1339        // Create application IDs
1340        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        // Create application metadata with different signerIds
1345        let old_app_meta = create_app_meta(old_signer_id);
1346        let new_app_meta = create_app_meta(new_signer_id);
1347
1348        // Store initial context state
1349        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        // Store both applications and initial state
1356        {
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        // Create context
1370        let context = create_test_context(context_id, old_app_id);
1371
1372        // Attempt update that should fail AppKey continuity check
1373        let result = verify_appkey_continuity(&store, &context, &new_app_id);
1374        assert!(result.is_err(), "AppKey continuity check should fail");
1375
1376        // Verify the original state is unchanged (no partial writes occurred)
1377        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        // Setup: Verify context metadata is not modified when update fails
1396        let store = create_test_store();
1397        let old_signer_id = "did:key:z6MkOldSignerKey123456789";
1398        let new_signer_id = "did:key:z6MkDifferentSignerKey000000";
1399
1400        // Create application IDs
1401        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        // Create application metadata
1406        let old_app_meta = create_app_meta(old_signer_id);
1407        let new_app_meta = create_app_meta(new_signer_id);
1408
1409        // Store initial context metadata
1410        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        // Store applications and context metadata
1418        {
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        // Create context
1432        let context = create_test_context(context_id, old_app_id);
1433
1434        // Attempt update that should fail
1435        let result = verify_appkey_continuity(&store, &context, &new_app_id);
1436        assert!(result.is_err(), "AppKey continuity check should fail");
1437
1438        // Verify context metadata is unchanged
1439        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        // Setup: Verify that multiple failed update attempts don't corrupt state
1459        let store = create_test_store();
1460        let legitimate_signer = "did:key:z6MkLegitimatePublisher1234567890";
1461
1462        // Create multiple attacker signerIds
1463        let attacker_signers = [
1464            "did:key:z6MkAttacker1111111111111111111111",
1465            "did:key:z6MkAttacker2222222222222222222222",
1466            "did:key:z6MkAttacker3333333333333333333333",
1467        ];
1468
1469        // Create application IDs
1470        let old_app_id = ApplicationId::from([10u8; 32]);
1471        let context_id = ContextId::from([1u8; 32]);
1472
1473        // Create and store legitimate application
1474        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        // Store initial state
1483        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        // Create context
1499        let context = create_test_context(context_id, old_app_id);
1500
1501        // Simulate multiple failed hijacking attempts
1502        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            // Store attacker's app
1507            {
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            // Attempt update - should fail
1518            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        // Verify original state is completely intact after all failed attempts
1527        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}