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
274            .accounts_db()
275            .get_snapshot_storages(..=self.starting_slot)
276            .0;
277
278        let dead_slots = Mutex::new(Vec::new());
279        let dead_storages = Mutex::new(Vec::new());
280
281        snapshot_storages.into_par_iter().for_each(|storage| {
282            let slot = storage.slot();
283            if slot != self.starting_slot {
284                if minimized_slot_set.contains(&slot) {
285                    self.filter_storage(&storage, &dead_storages);
286                } else {
287                    dead_slots.lock().unwrap().push(slot);
288                }
289            }
290        });
291
292        let dead_slots = dead_slots.into_inner().unwrap();
293        let dead_storages = dead_storages.into_inner().unwrap();
294        (dead_slots, dead_storages)
295    }
296
297    /// Creates new storage replacing `storages` that contains only accounts in `minimized_account_set`.
298    fn filter_storage(
299        &self,
300        storage: &Arc<AccountStorageEntry>,
301        dead_storages: &Mutex<Vec<Arc<AccountStorageEntry>>>,
302    ) {
303        let slot = storage.slot();
304        let GetUniqueAccountsResult {
305            stored_accounts, ..
306        } = self.accounts_db().get_unique_accounts_from_storage(storage);
307
308        let keep_accounts_collect = Mutex::new(Vec::with_capacity(stored_accounts.len()));
309        let purge_pubkeys_collect = Mutex::new(Vec::with_capacity(stored_accounts.len()));
310        let total_bytes_collect = AtomicUsize::new(0);
311        const CHUNK_SIZE: usize = 50;
312        stored_accounts.par_chunks(CHUNK_SIZE).for_each(|chunk| {
313            let mut chunk_bytes = 0;
314            let mut keep_accounts = Vec::with_capacity(CHUNK_SIZE);
315            let mut purge_pubkeys = Vec::with_capacity(CHUNK_SIZE);
316            chunk.iter().for_each(|account| {
317                if self.minimized_account_set.contains(account.pubkey()) {
318                    chunk_bytes += account.stored_size();
319                    keep_accounts.push(account);
320                } else if self.accounts_db().accounts_index.contains(account.pubkey()) {
321                    purge_pubkeys.push(account.pubkey());
322                }
323            });
324
325            keep_accounts_collect
326                .lock()
327                .unwrap()
328                .append(&mut keep_accounts);
329            purge_pubkeys_collect
330                .lock()
331                .unwrap()
332                .append(&mut purge_pubkeys);
333            total_bytes_collect.fetch_add(chunk_bytes, Ordering::Relaxed);
334        });
335
336        let keep_accounts = keep_accounts_collect.into_inner().unwrap();
337        let remove_pubkeys = purge_pubkeys_collect.into_inner().unwrap();
338        let total_bytes = total_bytes_collect.load(Ordering::Relaxed);
339
340        let purge_pubkeys: Vec<_> = remove_pubkeys
341            .into_iter()
342            .map(|pubkey| (*pubkey, slot))
343            .collect();
344        let _ = self.accounts_db().purge_keys_exact(purge_pubkeys.iter());
345
346        let mut shrink_in_progress = None;
347        if total_bytes > 0 {
348            shrink_in_progress = Some(
349                self.accounts_db()
350                    .get_store_for_shrink(slot, total_bytes as u64),
351            );
352            let new_storage = shrink_in_progress.as_ref().unwrap().new_storage();
353
354            let accounts = [(slot, &keep_accounts[..])];
355            let storable_accounts =
356                StorableAccountsBySlot::new(slot, &accounts, self.accounts_db());
357
358            self.accounts_db()
359                .store_accounts_frozen(storable_accounts, new_storage);
360
361            new_storage.flush().unwrap();
362        }
363
364        let mut dead_storages_this_time = self.accounts_db().mark_dirty_dead_stores(
365            slot,
366            true, // add_dirty_stores
367            shrink_in_progress,
368            false,
369        );
370        dead_storages
371            .lock()
372            .unwrap()
373            .append(&mut dead_storages_this_time);
374    }
375
376    /// Purge dead slots from storage and cache
377    fn purge_dead_slots(&self, dead_slots: Vec<Slot>) {
378        let stats = PurgeStats::default();
379        self.accounts_db()
380            .purge_slots_from_cache_and_store(dead_slots.iter(), &stats, false);
381    }
382
383    /// Convenience function for getting accounts_db
384    fn accounts_db(&self) -> &AccountsDb {
385        &self.bank.rc.accounts.accounts_db
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use {
392        crate::{
393            bank::Bank, genesis_utils::create_genesis_config_with_leader,
394            snapshot_minimizer::SnapshotMinimizer,
395        },
396        dashmap::DashSet,
397        solana_sdk::{
398            account::{AccountSharedData, ReadableAccount, WritableAccount},
399            bpf_loader_upgradeable::{self, UpgradeableLoaderState},
400            genesis_config::{create_genesis_config, GenesisConfig},
401            pubkey::Pubkey,
402            signer::Signer,
403            stake,
404        },
405        std::sync::Arc,
406    };
407
408    #[test]
409    fn test_get_rent_collection_accounts() {
410        solana_logger::setup();
411
412        let genesis_config = GenesisConfig::default();
413        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
414
415        // Slots correspond to subrange: A52Kf8KJNVhs1y61uhkzkSF82TXCLxZekqmFwiFXLnHu..=ChWNbfHUHLvFY3uhXj6kQhJ7a9iZB4ykh34WRGS5w9NE
416        // Initially, there are no existing keys in this range
417        {
418            let minimizer = SnapshotMinimizer {
419                bank: &bank,
420                starting_slot: 100_000,
421                ending_slot: 110_000,
422                minimized_account_set: DashSet::new(),
423            };
424            minimizer.get_rent_collection_accounts();
425            assert!(
426                minimizer.minimized_account_set.is_empty(),
427                "rent collection accounts should be empty: len={}",
428                minimizer.minimized_account_set.len()
429            );
430        }
431
432        // Add a key in the subrange
433        let pubkey: Pubkey = "ChWNbfHUHLvFY3uhXj6kQhJ7a9iZB4ykh34WRGS5w9ND"
434            .parse()
435            .unwrap();
436        bank.store_account(&pubkey, &AccountSharedData::new(1, 0, &Pubkey::default()));
437
438        {
439            let minimizer = SnapshotMinimizer {
440                bank: &bank,
441                starting_slot: 100_000,
442                ending_slot: 110_000,
443                minimized_account_set: DashSet::new(),
444            };
445            minimizer.get_rent_collection_accounts();
446            assert_eq!(
447                1,
448                minimizer.minimized_account_set.len(),
449                "rent collection accounts should have len=1: len={}",
450                minimizer.minimized_account_set.len()
451            );
452            assert!(minimizer.minimized_account_set.contains(&pubkey));
453        }
454
455        // Slots correspond to subrange: ChXFtoKuDvQum4HvtgiqGWrgUYbtP1ZzGFGMnT8FuGaB..=FKzRYCFeCC8e48jP9kSW4xM77quv1BPrdEMktpceXWSa
456        // The previous key is not contained in this range, so is not added
457        {
458            let minimizer = SnapshotMinimizer {
459                bank: &bank,
460                starting_slot: 110_001,
461                ending_slot: 120_000,
462                minimized_account_set: DashSet::new(),
463            };
464            assert!(
465                minimizer.minimized_account_set.is_empty(),
466                "rent collection accounts should be empty: len={}",
467                minimizer.minimized_account_set.len()
468            );
469        }
470    }
471
472    #[test]
473    fn test_minimization_get_vote_accounts() {
474        solana_logger::setup();
475
476        let bootstrap_validator_pubkey = solana_sdk::pubkey::new_rand();
477        let bootstrap_validator_stake_lamports = 30;
478        let genesis_config_info = create_genesis_config_with_leader(
479            10,
480            &bootstrap_validator_pubkey,
481            bootstrap_validator_stake_lamports,
482        );
483
484        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
485
486        let minimizer = SnapshotMinimizer {
487            bank: &bank,
488            starting_slot: 0,
489            ending_slot: 0,
490            minimized_account_set: DashSet::new(),
491        };
492        minimizer.get_vote_accounts();
493
494        assert!(minimizer
495            .minimized_account_set
496            .contains(&genesis_config_info.voting_keypair.pubkey()));
497        assert!(minimizer
498            .minimized_account_set
499            .contains(&genesis_config_info.validator_pubkey));
500    }
501
502    #[test]
503    fn test_minimization_get_stake_accounts() {
504        solana_logger::setup();
505
506        let bootstrap_validator_pubkey = solana_sdk::pubkey::new_rand();
507        let bootstrap_validator_stake_lamports = 30;
508        let genesis_config_info = create_genesis_config_with_leader(
509            10,
510            &bootstrap_validator_pubkey,
511            bootstrap_validator_stake_lamports,
512        );
513
514        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
515        let minimizer = SnapshotMinimizer {
516            bank: &bank,
517            starting_slot: 0,
518            ending_slot: 0,
519            minimized_account_set: DashSet::new(),
520        };
521        minimizer.get_stake_accounts();
522
523        let mut expected_stake_accounts: Vec<_> = genesis_config_info
524            .genesis_config
525            .accounts
526            .iter()
527            .filter_map(|(pubkey, account)| {
528                stake::program::check_id(account.owner()).then_some(*pubkey)
529            })
530            .collect();
531        expected_stake_accounts.push(bootstrap_validator_pubkey);
532
533        assert_eq!(
534            minimizer.minimized_account_set.len(),
535            expected_stake_accounts.len()
536        );
537        for stake_pubkey in expected_stake_accounts {
538            assert!(minimizer.minimized_account_set.contains(&stake_pubkey));
539        }
540    }
541
542    #[test]
543    fn test_minimization_get_owner_accounts() {
544        solana_logger::setup();
545
546        let (genesis_config, _) = create_genesis_config(1_000_000);
547        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
548
549        let pubkey = solana_sdk::pubkey::new_rand();
550        let owner_pubkey = solana_sdk::pubkey::new_rand();
551        bank.store_account(&pubkey, &AccountSharedData::new(1, 0, &owner_pubkey));
552
553        let owner_accounts = DashSet::new();
554        owner_accounts.insert(pubkey);
555        let minimizer = SnapshotMinimizer {
556            bank: &bank,
557            starting_slot: 0,
558            ending_slot: 0,
559            minimized_account_set: owner_accounts,
560        };
561
562        minimizer.get_owner_accounts();
563        assert!(minimizer.minimized_account_set.contains(&pubkey));
564        assert!(minimizer.minimized_account_set.contains(&owner_pubkey));
565    }
566
567    #[test]
568    fn test_minimization_add_programdata_accounts() {
569        solana_logger::setup();
570
571        let (genesis_config, _) = create_genesis_config(1_000_000);
572        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
573
574        let non_program_id = solana_sdk::pubkey::new_rand();
575        let program_id = solana_sdk::pubkey::new_rand();
576        let programdata_address = solana_sdk::pubkey::new_rand();
577
578        let program = UpgradeableLoaderState::Program {
579            programdata_address,
580        };
581
582        let non_program_acount = AccountSharedData::new(1, 0, &non_program_id);
583        let mut program_account =
584            AccountSharedData::new_data(40, &program, &bpf_loader_upgradeable::id()).unwrap();
585        program_account.set_executable(true);
586
587        bank.store_account(&non_program_id, &non_program_acount);
588        bank.store_account(&program_id, &program_account);
589
590        // Non-program account does not add any additional keys
591        let programdata_accounts = DashSet::new();
592        programdata_accounts.insert(non_program_id);
593        let minimizer = SnapshotMinimizer {
594            bank: &bank,
595            starting_slot: 0,
596            ending_slot: 0,
597            minimized_account_set: programdata_accounts,
598        };
599        minimizer.get_programdata_accounts();
600        assert_eq!(minimizer.minimized_account_set.len(), 1);
601        assert!(minimizer.minimized_account_set.contains(&non_program_id));
602
603        // Programdata account adds the programdata address to the set
604        minimizer.minimized_account_set.insert(program_id);
605        minimizer.get_programdata_accounts();
606        assert_eq!(minimizer.minimized_account_set.len(), 3);
607        assert!(minimizer.minimized_account_set.contains(&non_program_id));
608        assert!(minimizer.minimized_account_set.contains(&program_id));
609        assert!(minimizer
610            .minimized_account_set
611            .contains(&programdata_address));
612    }
613
614    #[test]
615    fn test_minimize_accounts_db() {
616        solana_logger::setup();
617
618        let (genesis_config, _) = create_genesis_config(1_000_000);
619        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
620        let accounts = &bank.accounts().accounts_db;
621
622        let num_slots = 5;
623        let num_accounts_per_slot = 300;
624
625        let mut current_slot = 0;
626        let minimized_account_set = DashSet::new();
627        for _ in 0..num_slots {
628            let pubkeys: Vec<_> = (0..num_accounts_per_slot)
629                .map(|_| solana_sdk::pubkey::new_rand())
630                .collect();
631
632            let some_lamport = 223;
633            let no_data = 0;
634            let owner = *AccountSharedData::default().owner();
635            let account = AccountSharedData::new(some_lamport, no_data, &owner);
636
637            current_slot += 1;
638
639            for (index, pubkey) in pubkeys.iter().enumerate() {
640                accounts.store_for_tests(current_slot, &[(pubkey, &account)]);
641
642                if current_slot % 2 == 0 && index % 100 == 0 {
643                    minimized_account_set.insert(*pubkey);
644                }
645            }
646            accounts.calculate_accounts_delta_hash(current_slot);
647            accounts.add_root_and_flush_write_cache(current_slot);
648        }
649
650        assert_eq!(minimized_account_set.len(), 6);
651        let minimizer = SnapshotMinimizer {
652            bank: &bank,
653            starting_slot: current_slot,
654            ending_slot: current_slot,
655            minimized_account_set,
656        };
657        minimizer.minimize_accounts_db();
658
659        let snapshot_storages = accounts.get_snapshot_storages(..=current_slot).0;
660        assert_eq!(snapshot_storages.len(), 3);
661
662        let mut account_count = 0;
663        snapshot_storages.into_iter().for_each(|storage| {
664            storage.accounts.scan_pubkeys(|_| {
665                account_count += 1;
666            });
667        });
668
669        assert_eq!(
670            account_count,
671            minimizer.minimized_account_set.len() + num_accounts_per_slot
672        ); // snapshot slot is untouched, so still has all 300 accounts
673    }
674}