Skip to content

Commit 1046bf8

Browse files
chore: Force-sync all indices in sync_anchor_indices (#3659)
# Motivation Indices computed based on pre-existing data cannot be synched for existing anchors simply by reading and then writing the anchor memory. Instead, the previous state of each index to be force-updated should be empty. In particular, this enables correctly populating the new `lookup_anchor_with_passkey_pubkey_hash_memory` index. --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 156b0d9 commit 1046bf8

File tree

5 files changed

+293
-65
lines changed

5 files changed

+293
-65
lines changed

src/canister_tests/src/api/internet_identity.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,3 +493,20 @@ pub fn get_attributes(
493493

494494
/// A "compatibility" module for the previous version of II to handle API changes.
495495
pub mod compat {}
496+
497+
pub fn count_passkeys(env: &PocketIc, canister_id: CanisterId) -> Result<u64, RejectResponse> {
498+
query_candid(env, canister_id, "count_passkeys", ()).map(|(x,)| x)
499+
}
500+
501+
pub fn list_anchor_migration_current_batch_id(
502+
env: &PocketIc,
503+
canister_id: CanisterId,
504+
) -> Result<u64, RejectResponse> {
505+
query_candid(
506+
env,
507+
canister_id,
508+
"list_anchor_migration_current_batch_id",
509+
(),
510+
)
511+
.map(|(x,)| x)
512+
}

src/internet_identity/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,9 @@ fn init(maybe_arg: Option<InternetIdentityInit>) {
645645
#[post_upgrade]
646646
fn post_upgrade(maybe_arg: Option<InternetIdentityInit>) {
647647
state::init_from_stable_memory();
648+
// Run synchronous one-time migrations (e.g. clearing incompatible indices)
649+
// before any other code can access the storage.
650+
state::storage_borrow_mut(|storage| storage.run_post_upgrade_migrations());
648651
// load the persistent state after initializing storage as it manages the respective stable cell
649652
state::load_persistent_state();
650653

src/internet_identity/src/migrations/sync_anchor_indices.rs

Lines changed: 135 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ impl<M: Memory + Clone> Storage<M> {
6060
// Save this value for future invocations.
6161
ANCHOR_MIGRATION_LAST_ANCHOR_ID.replace(Some(last_anchor_id));
6262

63+
// Note: clearing of `lookup_anchor_with_passkey_pubkey_hash_memory`
64+
// (key type changed from [u8; 32] to Principal) is now done
65+
// synchronously in `run_post_upgrade_migrations`.
66+
6367
last_anchor_id
6468
};
6569

@@ -79,11 +83,12 @@ impl<M: Memory + Clone> Storage<M> {
7983
// anchors within this entire batch failed to migrate.
8084
let mut batch_did_not_fail_completely = false;
8185

82-
// This is where the index migration happens. For each anchor in the batch, read it
83-
// from storage and write it back, which updates the indices.
86+
// This is where the index migration happens. For each anchor in the batch,
87+
// force-sync its indices with empty previous data so that all current entries
88+
// get added to the indices (even if the StorableAnchor already existed).
8489
for anchor_number in begin..=end {
85-
let anchor = match self.read(anchor_number) {
86-
Ok(anchor) => anchor,
90+
match self.force_sync_all_indices(anchor_number) {
91+
Ok(_) => {}
8792
Err(StorageError::AnchorNotFound { .. }) => {
8893
ic_cdk::println!("Marking {} as <DUMMY ANCHOR>", anchor_number);
8994

@@ -97,19 +102,18 @@ impl<M: Memory + Clone> Storage<M> {
97102
// data in the future. Safe to unwrap while the name is shorter than
98103
// `MAX_NAME_LENGTH` bytes.
99104
anchor.set_name(Some("<DUMMY ANCHOR>".to_string())).unwrap();
100-
anchor
101-
}
102-
Err(err) => {
103-
let err = format!("r#{}:{:?}", anchor_number, err);
104-
errors.push(err);
105-
continue;
106-
}
107-
};
108105

109-
match self.write(anchor) {
110-
Ok(_) => (),
106+
match self.write(anchor) {
107+
Ok(_) => {}
108+
Err(err) => {
109+
let err = format!("w#{}:{:?}", anchor_number, err);
110+
errors.push(err);
111+
continue;
112+
}
113+
}
114+
}
111115
Err(err) => {
112-
let err = format!("w#{}:{:?}", anchor_number, err);
116+
let err = format!("idx#{}:{:?}", anchor_number, err);
113117
errors.push(err);
114118
continue;
115119
}
@@ -225,35 +229,41 @@ mod sync_anchor_indices_tests {
225229
storage.write(a2.clone()).unwrap();
226230
storage.write(a3.clone()).unwrap();
227231

232+
// Clear the passkey pubkey hash index to simulate the state before the
233+
// migration (StorableAnchors exist but the index is empty).
234+
storage
235+
.lookup_anchor_with_passkey_pubkey_hash_memory
236+
.clear_new();
237+
228238
const BATCH_SIZE: u64 = 3;
229239

230240
reset_migration_state();
231241

232242
// Run migration
233243
storage.sync_anchor_indices(0, BATCH_SIZE);
234244

235-
// Check that recovery phrase principals are indexed for anchors 1 and 3
236-
let principal_1 = Principal::self_authenticating(pubkey(1));
237-
let principal_2 = Principal::self_authenticating(pubkey(2));
238-
let principal_3 = Principal::self_authenticating(pubkey(3));
245+
// Check that passkey pubkey principals are indexed for anchors 2 and 3
246+
let passkey_principal_2 = Principal::self_authenticating(pubkey(2));
247+
let passkey_principal_4 = Principal::self_authenticating(pubkey(4));
248+
let passkey_principal_1 = Principal::self_authenticating(pubkey(1));
239249

240250
assert_eq!(
241251
storage
242-
.lookup_anchor_with_recovery_phrase_principal_memory
243-
.get(&principal_1),
244-
Some(1)
252+
.lookup_anchor_with_passkey_pubkey_hash_memory
253+
.get(&passkey_principal_2),
254+
Some(2)
245255
);
246256
assert_eq!(
247257
storage
248-
.lookup_anchor_with_recovery_phrase_principal_memory
249-
.get(&principal_3),
258+
.lookup_anchor_with_passkey_pubkey_hash_memory
259+
.get(&passkey_principal_4),
250260
Some(3)
251261
);
252-
// Anchor 2 has no recovery device, so nothing indexed for its pubkey
262+
// Anchor 1 only has a recovery phrase, no passkey
253263
assert_eq!(
254264
storage
255-
.lookup_anchor_with_recovery_phrase_principal_memory
256-
.get(&principal_2),
265+
.lookup_anchor_with_passkey_pubkey_hash_memory
266+
.get(&passkey_principal_1),
257267
None
258268
);
259269

@@ -278,8 +288,10 @@ mod sync_anchor_indices_tests {
278288
storage.write(a2.clone()).unwrap();
279289
storage.write(a3.clone()).unwrap();
280290

291+
// Clear the passkey pubkey hash index to simulate the state before the
292+
// migration (StorableAnchors exist but the index is empty).
281293
storage
282-
.lookup_anchor_with_recovery_phrase_principal_memory
294+
.lookup_anchor_with_passkey_pubkey_hash_memory
283295
.clear_new();
284296

285297
// Not all anchors will fit into the first batch
@@ -290,29 +302,21 @@ mod sync_anchor_indices_tests {
290302
// Run migration (1)
291303
storage.sync_anchor_indices(0, BATCH_SIZE);
292304

293-
// Check that recovery phrase principals are indexed for anchors 1 and 3
294-
let principal_1 = Principal::self_authenticating(pubkey(1));
295-
let principal_2 = Principal::self_authenticating(pubkey(2));
296-
let principal_3 = Principal::self_authenticating(pubkey(3));
305+
// Check passkey pubkey principal index: anchors 1 and 2 are in the first batch
306+
let passkey_principal_2 = Principal::self_authenticating(pubkey(2));
307+
let passkey_principal_4 = Principal::self_authenticating(pubkey(4));
297308

298309
assert_eq!(
299310
storage
300-
.lookup_anchor_with_recovery_phrase_principal_memory
301-
.get(&principal_1),
302-
Some(1)
311+
.lookup_anchor_with_passkey_pubkey_hash_memory
312+
.get(&passkey_principal_2),
313+
Some(2)
303314
);
304-
// Anchor 2 has no recovery device, so nothing indexed for its pubkey
315+
// Anchor 3's passkey not migrated yet
305316
assert_eq!(
306317
storage
307-
.lookup_anchor_with_recovery_phrase_principal_memory
308-
.get(&principal_2),
309-
None
310-
);
311-
// Anchor 3 not migrated yet
312-
assert_eq!(
313-
storage
314-
.lookup_anchor_with_recovery_phrase_principal_memory
315-
.get(&principal_3),
318+
.lookup_anchor_with_passkey_pubkey_hash_memory
319+
.get(&passkey_principal_4),
316320
None
317321
);
318322

@@ -322,8 +326,8 @@ mod sync_anchor_indices_tests {
322326
// Now anchor 3 should be migrated
323327
assert_eq!(
324328
storage
325-
.lookup_anchor_with_recovery_phrase_principal_memory
326-
.get(&principal_3),
329+
.lookup_anchor_with_passkey_pubkey_hash_memory
330+
.get(&passkey_principal_4),
327331
Some(3)
328332
);
329333

@@ -347,4 +351,88 @@ mod sync_anchor_indices_tests {
347351

348352
assert_migration_completed(vec![]);
349353
}
354+
355+
/// Regression test: the previous migration used `read()` + `write()`, which relies on
356+
/// diff-based index syncing. When a `StorableAnchor` already existed in `stable_anchor_memory`
357+
/// (written by a prior `write()` call), re-writing the same data produced an empty diff,
358+
/// so no entries were added to `lookup_anchor_with_passkey_pubkey_hash_memory`.
359+
/// This was the root cause of the index being nearly empty after a seemingly successful migration.
360+
#[test]
361+
fn regression_populates_passkey_index_when_storable_anchor_already_exists() {
362+
let mut storage = Storage::new((1, 9), DefaultMemoryImpl::default());
363+
364+
// Create anchors with passkey devices and write them to storage.
365+
// This populates `stable_anchor_memory` (the StorableAnchor entries).
366+
let mut a1 = storage.allocate_anchor(111).unwrap();
367+
let mut a2 = storage.allocate_anchor(222).unwrap();
368+
369+
a1.add_device(other_device(pubkey(10))).unwrap();
370+
a2.add_device(other_device(pubkey(20))).unwrap();
371+
a2.add_device(recovery_phrase(pubkey(30))).unwrap();
372+
373+
storage.write(a1).unwrap();
374+
storage.write(a2).unwrap();
375+
376+
// At this point, `stable_anchor_memory` has StorableAnchor entries for both anchors
377+
// AND the indices are populated (because `write()` synced them).
378+
// Verify the indices are populated as a sanity check.
379+
let passkey_principal_10 = Principal::self_authenticating(pubkey(10));
380+
let passkey_principal_20 = Principal::self_authenticating(pubkey(20));
381+
assert_eq!(
382+
storage
383+
.lookup_anchor_with_passkey_pubkey_hash_memory
384+
.get(&passkey_principal_10),
385+
Some(1)
386+
);
387+
assert_eq!(
388+
storage
389+
.lookup_anchor_with_passkey_pubkey_hash_memory
390+
.get(&passkey_principal_20),
391+
Some(2)
392+
);
393+
394+
// Now clear ONLY the passkey pubkey hash index (not stable_anchor_memory) to
395+
// simulate the real production scenario: StorableAnchors already exist, but the
396+
// passkey pubkey hash index was introduced later and is empty.
397+
storage
398+
.lookup_anchor_with_passkey_pubkey_hash_memory
399+
.clear_new();
400+
401+
// Confirm indices are now empty.
402+
assert_eq!(
403+
storage
404+
.lookup_anchor_with_passkey_pubkey_hash_memory
405+
.get(&passkey_principal_10),
406+
None
407+
);
408+
assert_eq!(
409+
storage.lookup_anchor_with_passkey_pubkey_hash_memory.len(),
410+
0
411+
);
412+
413+
// Run the migration. The bug was that with the old read+write approach, the migration
414+
// would see previous == current (both from stable_anchor_memory) and produce an empty
415+
// diff, leaving the index unpopulated.
416+
const BATCH_SIZE: u64 = 10;
417+
reset_migration_state();
418+
storage.sync_anchor_indices(0, BATCH_SIZE);
419+
420+
// After the fix (force_sync_all_indices), the indices must be fully populated.
421+
assert_eq!(
422+
storage
423+
.lookup_anchor_with_passkey_pubkey_hash_memory
424+
.get(&passkey_principal_10),
425+
Some(1),
426+
"passkey pubkey principal index should be populated for anchor 1 even though StorableAnchor already existed"
427+
);
428+
assert_eq!(
429+
storage
430+
.lookup_anchor_with_passkey_pubkey_hash_memory
431+
.get(&passkey_principal_20),
432+
Some(2),
433+
"passkey pubkey principal index should be populated for anchor 2 even though StorableAnchor already existed"
434+
);
435+
436+
assert_migration_completed(vec![]);
437+
}
350438
}

0 commit comments

Comments
 (0)