@@ -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