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