solana_runtime/
snapshot_minimizer.rs

1//! Used to create minimal snapshots - separated here to keep accounts_db simpler
2
3use {
4    crate::{bank::Bank, static_ids},
5    dashmap::DashSet,
6    log::info,
7    rayon::{
8        iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
9        prelude::ParallelSlice,
10    },
11    solana_accounts_db::{
12        accounts_db::{
13            stats::PurgeStats, AccountStorageEntry, AccountsDb, GetUniqueAccountsResult,
14        },
15        accounts_partition,
16        storable_accounts::StorableAccountsBySlot,
17    },
18    solana_measure::measure_time,
19    solana_sdk::{
20        account::ReadableAccount,
21        account_utils::StateMut,
22        bpf_loader_upgradeable::{self, UpgradeableLoaderState},
23        clock::Slot,
24        pubkey::Pubkey,
25        reserved_account_keys::ReservedAccountKeys,
26    },
27    std::{
28        collections::HashSet,
29        sync::{
30            atomic::{AtomicUsize, Ordering},
31            Arc, Mutex,
32        },
33    },
34};
35
36/// Used to modify bank and accounts_db to create a minimized snapshot
37pub struct SnapshotMinimizer<'a> {
38    bank: &'a Bank,
39    starting_slot: Slot,
40    ending_slot: Slot,
41    minimized_account_set: DashSet<Pubkey>,
42}
43
44impl<'a> SnapshotMinimizer<'a> {
45    /// Removes all accounts not necessary for replaying slots in the range [starting_slot, ending_slot].
46    /// `transaction_account_set` should contain accounts used in transactions in the slot range [starting_slot, ending_slot].
47    /// This function will accumulate other accounts (rent collection, builtins, etc) necessary to replay transactions.
48    ///
49    /// This function will modify accounts_db by removing accounts not needed to replay [starting_slot, ending_slot],
50    /// and update the bank's capitalization.
51    pub fn minimize(
52        bank: &'a Bank,
53        starting_slot: Slot,
54        ending_slot: Slot,
55        transaction_account_set: DashSet<Pubkey>,
56    ) {
57        let minimizer = SnapshotMinimizer {
58            bank,
59            starting_slot,
60            ending_slot,
61            minimized_account_set: transaction_account_set,
62        };
63
64        minimizer.add_accounts(Self::get_active_bank_features, "active bank features");
65        minimizer.add_accounts(Self::get_inactive_bank_features, "inactive bank features");
66        minimizer.add_accounts(Self::get_static_runtime_accounts, "static runtime accounts");
67        minimizer.add_accounts(Self::get_reserved_accounts, "reserved accounts");
68
69        minimizer.add_accounts(
70            Self::get_rent_collection_accounts,
71            "rent collection accounts",
72        );
73        minimizer.add_accounts(Self::get_vote_accounts, "vote accounts");
74        minimizer.add_accounts(Self::get_stake_accounts, "stake accounts");
75        minimizer.add_accounts(Self::get_owner_accounts, "owner accounts");
76        minimizer.add_accounts(Self::get_programdata_accounts, "programdata accounts");
77
78        minimizer.minimize_accounts_db();
79
80        // Update accounts_cache and capitalization
81        minimizer.bank.force_flush_accounts_cache();
82        minimizer.bank.set_capitalization();
83    }
84
85    /// Helper function to measure time and number of accounts added
86    fn add_accounts<F>(&self, add_accounts_fn: F, name: &'static str)
87    where
88        F: Fn(&SnapshotMinimizer<'a>),
89    {
90        let initial_accounts_len = self.minimized_account_set.len();
91        let (_, measure) = measure_time!(add_accounts_fn(self), name);
92        let total_accounts_len = self.minimized_account_set.len();
93        let added_accounts = total_accounts_len - initial_accounts_len;
94
95        info!(
96            "Added {added_accounts} {name} for total of {total_accounts_len} accounts. get {measure}"
97        );
98    }
99
100    /// Used to get active bank feature accounts in `minimize`.
101    fn get_active_bank_features(&self) {
102        self.bank.feature_set.active.iter().for_each(|(pubkey, _)| {
103            self.minimized_account_set.insert(*pubkey);
104        });
105    }
106
107    /// Used to get inactive bank feature accounts in `minimize`
108    fn get_inactive_bank_features(&self) {
109        self.bank.feature_set.inactive.iter().for_each(|pubkey| {
110            self.minimized_account_set.insert(*pubkey);
111        });
112    }
113
114    /// Used to get static runtime accounts in `minimize`
115    fn get_static_runtime_accounts(&self) {
116        static_ids::STATIC_IDS.iter().for_each(|pubkey| {
117            self.minimized_account_set.insert(*pubkey);
118        });
119    }
120
121    /// Used to get reserved accounts in `minimize`
122    fn get_reserved_accounts(&self) {
123        ReservedAccountKeys::all_keys_iter().for_each(|pubkey| {
124            self.minimized_account_set.insert(*pubkey);
125        })
126    }
127
128    /// Used to get rent collection accounts in `minimize`
129    /// Add all pubkeys we would collect rent from or rewrite to `minimized_account_set`.
130    /// related to Bank::rent_collection_partitions
131    fn get_rent_collection_accounts(&self) {
132        let partitions = if !self.bank.use_fixed_collection_cycle() {
133            self.bank
134                .variable_cycle_partitions_between_slots(self.starting_slot, self.ending_slot)
135        } else {
136            self.bank
137                .fixed_cycle_partitions_between_slots(self.starting_slot, self.ending_slot)
138        };
139
140        partitions.into_iter().for_each(|partition| {
141            let subrange = accounts_partition::pubkey_range_from_partition(partition);
142            // This may be overkill since we just need the pubkeys and don't need to actually load the accounts.
143            // Leaving it for now as this is only used by ledger-tool. If used in runtime, we will need to instead use
144            // some of the guts of `load_to_collect_rent_eagerly`.
145            self.bank
146                .accounts()
147                .load_to_collect_rent_eagerly(&self.bank.ancestors, subrange)
148                .into_par_iter()
149                .for_each(|(pubkey, ..)| {
150                    self.minimized_account_set.insert(pubkey);
151                })
152        });
153    }
154
155    /// Used to get vote and node pubkeys in `minimize`
156    /// Add all pubkeys from vote accounts and nodes to `minimized_account_set`
157    fn get_vote_accounts(&self) {
158        self.bank
159            .vote_accounts()
160            .par_iter()
161            .for_each(|(pubkey, (_stake, vote_account))| {
162                self.minimized_account_set.insert(*pubkey);
163                self.minimized_account_set
164                    .insert(*vote_account.node_pubkey());
165            });
166    }
167
168    /// Used to get stake accounts in `minimize`
169    /// Add all pubkeys from stake accounts to `minimized_account_set`
170    fn get_stake_accounts(&self) {
171        self.bank.get_stake_accounts(&self.minimized_account_set);
172    }
173
174    /// Used to get owner accounts in `minimize`
175    /// For each account in `minimized_account_set` adds the owner account's pubkey to `minimized_account_set`.
176    fn get_owner_accounts(&self) {
177        let owner_accounts: HashSet<_> = self
178            .minimized_account_set
179            .par_iter()
180            .filter_map(|pubkey| self.bank.get_account(&pubkey))
181            .map(|account| *account.owner())
182            .collect();
183        owner_accounts.into_par_iter().for_each(|pubkey| {
184            self.minimized_account_set.insert(pubkey);
185        });
186    }
187
188    /// Used to get program data accounts in `minimize`
189    /// For each upgradable bpf program, adds the programdata account pubkey to `minimized_account_set`
190    fn get_programdata_accounts(&self) {
191        let programdata_accounts: HashSet<_> = self
192            .minimized_account_set
193            .par_iter()
194            .filter_map(|pubkey| self.bank.get_account(&pubkey))
195            .filter(|account| account.executable())
196            .filter(|account| bpf_loader_upgradeable::check_id(account.owner()))
197            .filter_map(|account| {
198                if let Ok(UpgradeableLoaderState::Program {
199                    programdata_address,
200                }) = account.state()
201                {
202                    Some(programdata_address)
203                } else {
204                    None
205                }
206            })
207            .collect();
208        programdata_accounts.into_par_iter().for_each(|pubkey| {
209            self.minimized_account_set.insert(pubkey);
210        });
211    }
212
213    /// Remove accounts not in `minimized_accoun_set` from accounts_db
214    fn minimize_accounts_db(&self) {
215        let (minimized_slot_set, minimized_slot_set_measure) =
216            measure_time!(self.get_minimized_slot_set(), "generate minimized slot set");
217        info!("{minimized_slot_set_measure}");
218
219        let ((dead_slots, dead_storages), process_snapshot_storages_measure) = measure_time!(
220            self.process_snapshot_storages(minimized_slot_set),
221            "process snapshot storages"
222        );
223        info!("{process_snapshot_storages_measure}");
224
225        // Avoid excessive logging
226        self.accounts_db()
227            .log_dead_slots
228            .store(false, Ordering::Relaxed);
229
230        let (_, purge_dead_slots_measure) =
231            measure_time!(self.purge_dead_slots(dead_slots), "purge dead slots");
232        info!("{purge_dead_slots_measure}");
233
234        let (_, drop_storages_measure) = measure_time!(drop(dead_storages), "drop storages");
235        info!("{drop_storages_measure}");
236
237        // Turn logging back on after minimization
238        self.accounts_db()
239            .log_dead_slots
240            .store(true, Ordering::Relaxed);
241    }
242
243    /// Determines minimum set of slots that accounts in `minimized_account_set` are in
244    fn get_minimized_slot_set(&self) -> DashSet<Slot> {
245        let minimized_slot_set = DashSet::new();
246        self.minimized_account_set.par_iter().for_each(|pubkey| {
247            self.accounts_db()
248                .accounts_index
249                .get_and_then(&pubkey, |entry| {
250                    if let Some(entry) = entry {
251                        let max_slot = entry
252                            .slot_list
253                            .read()
254                            .unwrap()
255                            .iter()
256                            .map(|(slot, _)| *slot)
257                            .max();
258                        if let Some(max_slot) = max_slot {
259                            minimized_slot_set.insert(max_slot);
260                        }
261                    }
262                    (false, ())
263                });
264        });
265        minimized_slot_set
266    }
267
268    /// Process all snapshot storages to during `minimize`
269    fn process_snapshot_storages(
270        &self,
271        minimized_slot_set: DashSet<Slot>,
272    ) -> (Vec<Slot>, Vec<Arc<AccountStorageEntry>>) {
273        let snapshot_storages = self.accounts_db().get_storages(..=self.starting_slot).0;
274
275        let dead_slots = Mutex::new(Vec::new());
276        let dead_storages = Mutex::new(Vec::new());
277
278        snapshot_storages.into_par_iter().for_each(|storage| {
279            let slot = storage.slot();
280            if slot != self.starting_slot {
281                if minimized_slot_set.contains(&slot) {
282                    self.filter_storage(&storage, &dead_storages);
283                } else {
284                    dead_slots.lock().unwrap().push(slot);
285                }
286            }
287        });
288
289        let dead_slots = dead_slots.into_inner().unwrap();
290        let dead_storages = dead_storages.into_inner().unwrap();
291        (dead_slots, dead_storages)
292    }
293
294    /// Creates new storage replacing `storages` that contains only accounts in `minimized_account_set`.
295    fn filter_storage(
296        &self,
297        storage: &Arc<AccountStorageEntry>,
298        dead_storages: &Mutex<Vec<Arc<AccountStorageEntry>>>,
299    ) {
300        let slot = storage.slot();
301        let GetUniqueAccountsResult {
302            stored_accounts, ..
303        } = self.accounts_db().get_unique_accounts_from_storage(storage);
304
305        let keep_accounts_collect = Mutex::new(Vec::with_capacity(stored_accounts.len()));
306        let purge_pubkeys_collect = Mutex::new(Vec::with_capacity(stored_accounts.len()));
307        let total_bytes_collect = AtomicUsize::new(0);
308        const CHUNK_SIZE: usize = 50;
309        stored_accounts.par_chunks(CHUNK_SIZE).for_each(|chunk| {
310            let mut chunk_bytes = 0;
311            let mut keep_accounts = Vec::with_capacity(CHUNK_SIZE);
312            let mut purge_pubkeys = Vec::with_capacity(CHUNK_SIZE);
313            chunk.iter().for_each(|account| {
314                if self.minimized_account_set.contains(account.pubkey()) {
315                    chunk_bytes += account.stored_size();
316                    keep_accounts.push(account);
317                } else if self.accounts_db().accounts_index.contains(account.pubkey()) {
318                    purge_pubkeys.push(account.pubkey());
319                }
320            });
321
322            keep_accounts_collect
323                .lock()
324                .unwrap()
325                .append(&mut keep_accounts);
326            purge_pubkeys_collect
327                .lock()
328                .unwrap()
329                .append(&mut purge_pubkeys);
330            total_bytes_collect.fetch_add(chunk_bytes, Ordering::Relaxed);
331        });
332
333        let keep_accounts = keep_accounts_collect.into_inner().unwrap();
334        let remove_pubkeys = purge_pubkeys_collect.into_inner().unwrap();
335        let total_bytes = total_bytes_collect.load(Ordering::Relaxed);
336
337        let purge_pubkeys: Vec<_> = remove_pubkeys
338            .into_iter()
339            .map(|pubkey| (*pubkey, slot))
340            .collect();
341        let _ = self.accounts_db().purge_keys_exact(purge_pubkeys.iter());
342
343        let mut shrink_in_progress = None;
344        if total_bytes > 0 {
345            shrink_in_progress = Some(
346                self.accounts_db()
347                    .get_store_for_shrink(slot, total_bytes as u64),
348            );
349            let new_storage = shrink_in_progress.as_ref().unwrap().new_storage();
350
351            let accounts = [(slot, &keep_accounts[..])];
352            let storable_accounts =
353                StorableAccountsBySlot::new(slot, &accounts, self.accounts_db());
354
355            self.accounts_db()
356                .store_accounts_frozen(storable_accounts, new_storage);
357
358            new_storage.flush().unwrap();
359        }
360
361        let mut dead_storages_this_time = self.accounts_db().mark_dirty_dead_stores(
362            slot,
363            true, // add_dirty_stores
364            shrink_in_progress,
365            false,
366        );
367        dead_storages
368            .lock()
369            .unwrap()
370            .append(&mut dead_storages_this_time);
371    }
372
373    /// Purge dead slots from storage and cache
374    fn purge_dead_slots(&self, dead_slots: Vec<Slot>) {
375        let stats = PurgeStats::default();
376        self.accounts_db()
377            .purge_slots_from_cache_and_store(dead_slots.iter(), &stats, false);
378    }
379
380    /// Convenience function for getting accounts_db
381    fn accounts_db(&self) -> &AccountsDb {
382        &self.bank.rc.accounts.accounts_db
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use {
389        crate::{
390            bank::Bank, genesis_utils::create_genesis_config_with_leader,
391            snapshot_minimizer::SnapshotMinimizer,
392        },
393        dashmap::DashSet,
394        solana_sdk::{
395            account::{AccountSharedData, ReadableAccount, WritableAccount},
396            bpf_loader_upgradeable::{self, UpgradeableLoaderState},
397            genesis_config::{create_genesis_config, GenesisConfig},
398            pubkey::Pubkey,
399            signer::Signer,
400            stake,
401        },
402        std::sync::Arc,
403    };
404
405    #[test]
406    fn test_get_rent_collection_accounts() {
407        solana_logger::setup();
408
409        let genesis_config = GenesisConfig::default();
410        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
411
412        // Slots correspond to subrange: A52Kf8KJNVhs1y61uhkzkSF82TXCLxZekqmFwiFXLnHu..=ChWNbfHUHLvFY3uhXj6kQhJ7a9iZB4ykh34WRGS5w9NE
413        // Initially, there are no existing keys in this range
414        {
415            let minimizer = SnapshotMinimizer {
416                bank: &bank,
417                starting_slot: 100_000,
418                ending_slot: 110_000,
419                minimized_account_set: DashSet::new(),
420            };
421            minimizer.get_rent_collection_accounts();
422            assert!(
423                minimizer.minimized_account_set.is_empty(),
424                "rent collection accounts should be empty: len={}",
425                minimizer.minimized_account_set.len()
426            );
427        }
428
429        // Add a key in the subrange
430        let pubkey: Pubkey = "ChWNbfHUHLvFY3uhXj6kQhJ7a9iZB4ykh34WRGS5w9ND"
431            .parse()
432            .unwrap();
433        bank.store_account(&pubkey, &AccountSharedData::new(1, 0, &Pubkey::default()));
434
435        {
436            let minimizer = SnapshotMinimizer {
437                bank: &bank,
438                starting_slot: 100_000,
439                ending_slot: 110_000,
440                minimized_account_set: DashSet::new(),
441            };
442            minimizer.get_rent_collection_accounts();
443            assert_eq!(
444                1,
445                minimizer.minimized_account_set.len(),
446                "rent collection accounts should have len=1: len={}",
447                minimizer.minimized_account_set.len()
448            );
449            assert!(minimizer.minimized_account_set.contains(&pubkey));
450        }
451
452        // Slots correspond to subrange: ChXFtoKuDvQum4HvtgiqGWrgUYbtP1ZzGFGMnT8FuGaB..=FKzRYCFeCC8e48jP9kSW4xM77quv1BPrdEMktpceXWSa
453        // The previous key is not contained in this range, so is not added
454        {
455            let minimizer = SnapshotMinimizer {
456                bank: &bank,
457                starting_slot: 110_001,
458                ending_slot: 120_000,
459                minimized_account_set: DashSet::new(),
460            };
461            assert!(
462                minimizer.minimized_account_set.is_empty(),
463                "rent collection accounts should be empty: len={}",
464                minimizer.minimized_account_set.len()
465            );
466        }
467    }
468
469    #[test]
470    fn test_minimization_get_vote_accounts() {
471        solana_logger::setup();
472
473        let bootstrap_validator_pubkey = solana_pubkey::new_rand();
474        let bootstrap_validator_stake_lamports = 30;
475        let genesis_config_info = create_genesis_config_with_leader(
476            10,
477            &bootstrap_validator_pubkey,
478            bootstrap_validator_stake_lamports,
479        );
480
481        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
482
483        let minimizer = SnapshotMinimizer {
484            bank: &bank,
485            starting_slot: 0,
486            ending_slot: 0,
487            minimized_account_set: DashSet::new(),
488        };
489        minimizer.get_vote_accounts();
490
491        assert!(minimizer
492            .minimized_account_set
493            .contains(&genesis_config_info.voting_keypair.pubkey()));
494        assert!(minimizer
495            .minimized_account_set
496            .contains(&genesis_config_info.validator_pubkey));
497    }
498
499    #[test]
500    fn test_minimization_get_stake_accounts() {
501        solana_logger::setup();
502
503        let bootstrap_validator_pubkey = solana_pubkey::new_rand();
504        let bootstrap_validator_stake_lamports = 30;
505        let genesis_config_info = create_genesis_config_with_leader(
506            10,
507            &bootstrap_validator_pubkey,
508            bootstrap_validator_stake_lamports,
509        );
510
511        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
512        let minimizer = SnapshotMinimizer {
513            bank: &bank,
514            starting_slot: 0,
515            ending_slot: 0,
516            minimized_account_set: DashSet::new(),
517        };
518        minimizer.get_stake_accounts();
519
520        let mut expected_stake_accounts: Vec<_> = genesis_config_info
521            .genesis_config
522            .accounts
523            .iter()
524            .filter_map(|(pubkey, account)| {
525                stake::program::check_id(account.owner()).then_some(*pubkey)
526            })
527            .collect();
528        expected_stake_accounts.push(bootstrap_validator_pubkey);
529
530        assert_eq!(
531            minimizer.minimized_account_set.len(),
532            expected_stake_accounts.len()
533        );
534        for stake_pubkey in expected_stake_accounts {
535            assert!(minimizer.minimized_account_set.contains(&stake_pubkey));
536        }
537    }
538
539    #[test]
540    fn test_minimization_get_owner_accounts() {
541        solana_logger::setup();
542
543        let (genesis_config, _) = create_genesis_config(1_000_000);
544        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
545
546        let pubkey = solana_pubkey::new_rand();
547        let owner_pubkey = solana_pubkey::new_rand();
548        bank.store_account(&pubkey, &AccountSharedData::new(1, 0, &owner_pubkey));
549
550        let owner_accounts = DashSet::new();
551        owner_accounts.insert(pubkey);
552        let minimizer = SnapshotMinimizer {
553            bank: &bank,
554            starting_slot: 0,
555            ending_slot: 0,
556            minimized_account_set: owner_accounts,
557        };
558
559        minimizer.get_owner_accounts();
560        assert!(minimizer.minimized_account_set.contains(&pubkey));
561        assert!(minimizer.minimized_account_set.contains(&owner_pubkey));
562    }
563
564    #[test]
565    fn test_minimization_add_programdata_accounts() {
566        solana_logger::setup();
567
568        let (genesis_config, _) = create_genesis_config(1_000_000);
569        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
570
571        let non_program_id = solana_pubkey::new_rand();
572        let program_id = solana_pubkey::new_rand();
573        let programdata_address = solana_pubkey::new_rand();
574
575        let program = UpgradeableLoaderState::Program {
576            programdata_address,
577        };
578
579        let non_program_acount = AccountSharedData::new(1, 0, &non_program_id);
580        let mut program_account =
581            AccountSharedData::new_data(40, &program, &bpf_loader_upgradeable::id()).unwrap();
582        program_account.set_executable(true);
583
584        bank.store_account(&non_program_id, &non_program_acount);
585        bank.store_account(&program_id, &program_account);
586
587        // Non-program account does not add any additional keys
588        let programdata_accounts = DashSet::new();
589        programdata_accounts.insert(non_program_id);
590        let minimizer = SnapshotMinimizer {
591            bank: &bank,
592            starting_slot: 0,
593            ending_slot: 0,
594            minimized_account_set: programdata_accounts,
595        };
596        minimizer.get_programdata_accounts();
597        assert_eq!(minimizer.minimized_account_set.len(), 1);
598        assert!(minimizer.minimized_account_set.contains(&non_program_id));
599
600        // Programdata account adds the programdata address to the set
601        minimizer.minimized_account_set.insert(program_id);
602        minimizer.get_programdata_accounts();
603        assert_eq!(minimizer.minimized_account_set.len(), 3);
604        assert!(minimizer.minimized_account_set.contains(&non_program_id));
605        assert!(minimizer.minimized_account_set.contains(&program_id));
606        assert!(minimizer
607            .minimized_account_set
608            .contains(&programdata_address));
609    }
610
611    #[test]
612    fn test_minimize_accounts_db() {
613        solana_logger::setup();
614
615        let (genesis_config, _) = create_genesis_config(1_000_000);
616        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
617        let accounts = &bank.accounts().accounts_db;
618
619        let num_slots = 5;
620        let num_accounts_per_slot = 300;
621
622        let mut current_slot = 0;
623        let minimized_account_set = DashSet::new();
624        for _ in 0..num_slots {
625            let pubkeys: Vec<_> = (0..num_accounts_per_slot)
626                .map(|_| solana_pubkey::new_rand())
627                .collect();
628
629            let some_lamport = 223;
630            let no_data = 0;
631            let owner = *AccountSharedData::default().owner();
632            let account = AccountSharedData::new(some_lamport, no_data, &owner);
633
634            current_slot += 1;
635
636            for (index, pubkey) in pubkeys.iter().enumerate() {
637                accounts.store_for_tests(current_slot, &[(pubkey, &account)]);
638
639                if current_slot % 2 == 0 && index % 100 == 0 {
640                    minimized_account_set.insert(*pubkey);
641                }
642            }
643            accounts.calculate_accounts_delta_hash(current_slot);
644            accounts.add_root_and_flush_write_cache(current_slot);
645        }
646
647        assert_eq!(minimized_account_set.len(), 6);
648        let minimizer = SnapshotMinimizer {
649            bank: &bank,
650            starting_slot: current_slot,
651            ending_slot: current_slot,
652            minimized_account_set,
653        };
654        minimizer.minimize_accounts_db();
655
656        let snapshot_storages = accounts.get_storages(..=current_slot).0;
657        assert_eq!(snapshot_storages.len(), 3);
658
659        let mut account_count = 0;
660        snapshot_storages.into_iter().for_each(|storage| {
661            storage.accounts.scan_pubkeys(|_| {
662                account_count += 1;
663            });
664        });
665
666        assert_eq!(
667            account_count,
668            minimizer.minimized_account_set.len() + num_accounts_per_slot
669        ); // snapshot slot is untouched, so still has all 300 accounts
670    }
671}