Skip to main content

solana_runtime/
snapshot_utils.rs

1#[cfg(feature = "dev-context-only-utils")]
2use solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs;
3use {
4    crate::{
5        bank::BankFieldsToDeserialize,
6        serde_snapshot::{
7            self, AccountsDbFields, ExtraFieldsToSerialize, SerdeObsoleteAccountsMap,
8            SerializableAccountStorageEntry, SnapshotAccountsDbFields, SnapshotBankFields,
9            SnapshotStreams, StorageListItem, StoragesList,
10        },
11        snapshot_package::BankSnapshotPackage,
12        snapshot_utils::snapshot_storage_rebuilder::{
13            SnapshotStorageRebuilder, get_slot_and_append_vec_id,
14        },
15    },
16    agave_fs::{
17        FileInfo, FileSize,
18        buffered_reader::large_file_buf_reader,
19        buffered_writer::{SizeLimitedWriter, large_file_buf_writer},
20        io_setup::IoSetupState,
21    },
22    agave_snapshots::{
23        ArchiveFormat, Result, SnapshotArchiveKind, SnapshotVersion, archive_snapshot,
24        error::{
25            AddBankSnapshotError, SnapshotError, SnapshotFastbootError, SnapshotNewFromDirError,
26        },
27        paths::{self as snapshot_paths, incremental_snapshot_archives_iter},
28        snapshot_archive_info::{
29            FullSnapshotArchiveInfo, IncrementalSnapshotArchiveInfo, SnapshotArchiveInfo,
30            SnapshotArchiveInfoGetter,
31        },
32        snapshot_config::SnapshotConfig,
33        snapshot_hash::SnapshotHash,
34        streaming_unarchive_snapshot,
35    },
36    crossbeam_channel::Receiver,
37    log::*,
38    regex::Regex,
39    semver::Version,
40    solana_accounts_db::{
41        account_storage::AccountStorageMap,
42        account_storage_entry::AccountStorageEntry,
43        accounts_db::{AccountsFileId, AtomicAccountsFileId},
44        utils::{
45            ACCOUNTS_RUN_DIR, ACCOUNTS_SNAPSHOT_DIR, move_and_async_delete_path,
46            move_and_async_delete_path_contents,
47        },
48    },
49    solana_clock::Slot,
50    solana_measure::{measure::Measure, measure_time, measure_us},
51    std::{
52        cmp::Ordering,
53        collections::{HashMap, HashSet},
54        fs,
55        io::{self, BufReader, Error as IoError, Read, Seek, Write},
56        mem,
57        num::NonZeroUsize,
58        path::{Path, PathBuf},
59        str::FromStr,
60        sync::{Arc, LazyLock},
61        thread,
62    },
63    tempfile::TempDir,
64    wincode::io::std_read::ReadAdapter,
65};
66
67pub mod snapshot_storage_rebuilder;
68
69/// Limit the size of the obsolete accounts file
70/// If it exceeds this limit, remove the file which will force restore from archives
71/// Limit is set assuming 24 bytes per entry, 5% of 10 billion accounts
72/// = 500 million entries * 24 bytes = 12 GB
73pub const MAX_OBSOLETE_ACCOUNTS_FILE_SIZE: u64 = 1024 * 1024 * 1024 * 12; // 12 GB
74/// Limit the size of the storages list file.
75/// Each `(slot, id)` entry encodes to 12 bytes; 100 MiB covers ~8.7 million entries, well past
76/// any realistic storage count.
77pub const MAX_STORAGES_LIST_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB
78pub const MAX_SNAPSHOT_DATA_FILE_SIZE: u64 = 32 * 1024 * 1024 * 1024; // 32 GiB
79const MAX_SNAPSHOT_VERSION_FILE_SIZE: u64 = 8; // byte
80/// Buffer size for reading auxiliary per-snapshot files (obsolete accounts, storages list).
81/// Sized to allow several concurrent reads at the default io-uring reader read size (1MiB).
82const AUX_SNAPSHOT_FILE_READ_BUF_SIZE: usize = 4 * 1024 * 1024;
83
84// Snapshot Fastboot Version History
85// Legacy - No fastboot version file, storages flushed file presence determines if snapshot is loadable
86// 1.0.0 - Initial version file. Backwards and forwards compatible with Legacy.
87// 2.0.0 - Obsolete Accounts File added, storages flushed file not written anymore
88//         Snapshots created with version 2.0.0 will not fastboot to older versions
89//         Snapshots created with versions <2.0.0 will fastboot to version 2.0.0
90// 3.0.0 - Storages List file added, replaces the per-storage hardlink dirs.
91//         3.0.0 validators can still fastboot from 2.0.0 snapshots: the legacy hardlinks are
92//         migrated back into the account run dirs at load time (see `migrate_legacy_hardlinks`),
93//         and the next teardown writes the new-format storages list.
94//         Note: 2.0.0 validators cannot fastboot from 3.0.0 snapshots because the per-storage
95//         hardlink dirs they rely on are no longer written; they must fall back to archive.
96const SNAPSHOT_FASTBOOT_VERSION: Version = Version::new(3, 0, 0);
97
98/// Information about a bank snapshot. Namely the slot of the bank, the path to the snapshot, and
99/// the kind of the snapshot.
100#[derive(PartialEq, Eq, Debug)]
101pub struct BankSnapshotInfo {
102    /// Slot of the bank
103    pub slot: Slot,
104    /// Path to the bank snapshot directory
105    pub snapshot_dir: PathBuf,
106    /// Snapshot version
107    pub snapshot_version: SnapshotVersion,
108    /// Fastboot version
109    pub fastboot_version: Option<Version>,
110}
111
112impl PartialOrd for BankSnapshotInfo {
113    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114        Some(self.cmp(other))
115    }
116}
117
118// Order BankSnapshotInfo by slot (ascending), which practically is sorting chronologically
119impl Ord for BankSnapshotInfo {
120    fn cmp(&self, other: &Self) -> Ordering {
121        self.slot.cmp(&other.slot)
122    }
123}
124
125impl BankSnapshotInfo {
126    pub fn new_from_dir(
127        bank_snapshots_dir: impl AsRef<Path>,
128        slot: Slot,
129    ) -> std::result::Result<BankSnapshotInfo, SnapshotNewFromDirError> {
130        // check this directory to see if there is a BankSnapshotPre and/or
131        // BankSnapshotPost file
132        let bank_snapshot_dir = snapshot_paths::get_bank_snapshot_dir(&bank_snapshots_dir, slot);
133
134        if !bank_snapshot_dir.is_dir() {
135            return Err(SnapshotNewFromDirError::InvalidBankSnapshotDir(
136                bank_snapshot_dir,
137            ));
138        }
139
140        // Among the files checks, the completion flag file check should be done first to avoid the later
141        // I/O errors.
142
143        // There is a time window from the slot directory being created, and the content being completely
144        // filled.  Check the version file as it is the last file written to avoid using a highest
145        // found slot directory with missing content
146        let version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
147        let version_file_info = FileInfo::new_from_path(&version_path)
148            .map_err(|err| SnapshotNewFromDirError::IncompleteDir(err, version_path))?;
149        let version_str = snapshot_version_from_file(version_file_info).map_err(|err| {
150            SnapshotNewFromDirError::IncompleteDir(err, bank_snapshot_dir.clone())
151        })?;
152
153        let snapshot_version = SnapshotVersion::from_str(version_str.as_str())
154            .or(Err(SnapshotNewFromDirError::InvalidVersion(version_str)))?;
155
156        let status_cache_file =
157            bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME);
158        if !status_cache_file.is_file() {
159            return Err(SnapshotNewFromDirError::MissingStatusCacheFile(
160                status_cache_file,
161            ));
162        }
163
164        let bank_snapshot_path =
165            bank_snapshot_dir.join(snapshot_paths::get_snapshot_file_name(slot));
166        if !bank_snapshot_path.is_file() {
167            return Err(SnapshotNewFromDirError::MissingSnapshotFile(
168                bank_snapshot_dir,
169            ));
170        };
171
172        let snapshot_fastboot_version_path =
173            bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_FASTBOOT_VERSION_FILENAME);
174
175        // If the version file is absent, fastboot_version will be None. This allows versions 3.1+
176        // to load snapshots created by versions <3.1. In version 3.2, the version file will become
177        // mandatory, and its absence can be treated as an error.
178        let fastboot_version = fs::read_to_string(&snapshot_fastboot_version_path)
179            .ok()
180            .map(|version_string| {
181                Version::from_str(version_string.trim())
182                    .map_err(|_| SnapshotNewFromDirError::InvalidFastbootVersion(version_string))
183            })
184            .transpose()?;
185
186        Ok(BankSnapshotInfo {
187            slot,
188            snapshot_dir: bank_snapshot_dir,
189            snapshot_version,
190            fastboot_version,
191        })
192    }
193
194    pub fn snapshot_path(&self) -> PathBuf {
195        self.snapshot_dir
196            .join(snapshot_paths::get_snapshot_file_name(self.slot))
197    }
198}
199
200/// When constructing a bank a snapshot, traditionally the snapshot was from a snapshot archive.  Now,
201/// the snapshot can be from a snapshot directory, or from a snapshot archive.  This is the flag to
202/// indicate which.
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum SnapshotFrom {
205    /// Build from the snapshot archive
206    Archive,
207    /// Build directly from the bank snapshot directory
208    Dir,
209}
210
211/// Helper type when rebuilding from snapshots.  Designed to handle when rebuilding from just a
212/// full snapshot, or from both a full snapshot and an incremental snapshot.
213#[derive(Debug)]
214pub struct SnapshotRootPaths {
215    pub full_snapshot_root_file_path: PathBuf,
216    pub incremental_snapshot_root_file_path: Option<PathBuf>,
217}
218
219/// Helper type to bundle up the results from `unarchive_snapshot()`
220#[derive(Debug)]
221pub struct UnarchivedSnapshot {
222    unpack_dir: TempDir,
223    pub storage: AccountStorageMap,
224    pub bank_fields: BankFieldsToDeserialize,
225    pub(crate) accounts_db_fields: AccountsDbFields<SerializableAccountStorageEntry>,
226    pub unpacked_snapshots_dir_and_version: UnpackedSnapshotsDirAndVersion,
227    pub measure_untar: Measure,
228}
229
230/// Helper type to bundle up the results from `verify_and_unarchive_snapshots()`.
231#[derive(Debug)]
232pub struct UnarchivedSnapshots {
233    pub full_storage: AccountStorageMap,
234    pub incremental_storage: Option<AccountStorageMap>,
235    pub bank_fields: SnapshotBankFields,
236    pub accounts_db_fields: SnapshotAccountsDbFields<SerializableAccountStorageEntry>,
237    pub full_unpacked_snapshots_dir_and_version: UnpackedSnapshotsDirAndVersion,
238    pub incremental_unpacked_snapshots_dir_and_version: Option<UnpackedSnapshotsDirAndVersion>,
239    pub full_measure_untar: Measure,
240    pub incremental_measure_untar: Option<Measure>,
241    pub next_append_vec_id: AtomicAccountsFileId,
242}
243
244/// Guard type that keeps the unpack directories of snapshots alive.
245/// Once dropped, the unpack directories are removed.
246#[expect(dead_code)]
247#[derive(Debug)]
248pub struct UnarchivedSnapshotsGuard {
249    full_unpack_dir: TempDir,
250    incremental_unpack_dir: Option<TempDir>,
251}
252/// Helper type for passing around the unpacked snapshots dir and the snapshot version together
253#[derive(Debug)]
254pub struct UnpackedSnapshotsDirAndVersion {
255    pub unpacked_snapshots_dir: PathBuf,
256    pub snapshot_version: SnapshotVersion,
257}
258
259/// Helper type for passing around account storage map and next append vec id
260/// for reconstructing accounts from a snapshot
261pub(crate) struct StorageAndNextAccountsFileId {
262    pub storage: AccountStorageMap,
263    pub next_append_vec_id: AtomicAccountsFileId,
264}
265
266/// Purges incomplete bank snapshots
267pub fn purge_incomplete_bank_snapshots(bank_snapshots_dir: impl AsRef<Path>) {
268    let Ok(read_dir_iter) = std::fs::read_dir(&bank_snapshots_dir) else {
269        // If we cannot read the bank snapshots dir, then there's nothing to do
270        return;
271    };
272
273    let is_incomplete = |dir: &PathBuf| !is_bank_snapshot_complete(dir);
274
275    let incomplete_dirs: Vec<_> = read_dir_iter
276        .filter_map(|entry| entry.ok())
277        .map(|entry| entry.path())
278        .filter(|path| path.is_dir())
279        .filter(is_incomplete)
280        .collect();
281
282    // attempt to purge all the incomplete directories; do not exit early
283    for incomplete_dir in incomplete_dirs {
284        let result = purge_bank_snapshot(&incomplete_dir);
285        match result {
286            Ok(_) => info!(
287                "Purged incomplete snapshot dir: {}",
288                incomplete_dir.display()
289            ),
290            Err(err) => warn!("Failed to purge incomplete snapshot dir: {err}"),
291        }
292    }
293}
294
295/// Is the bank snapshot complete?
296fn is_bank_snapshot_complete(bank_snapshot_dir: impl AsRef<Path>) -> bool {
297    let version_path = bank_snapshot_dir
298        .as_ref()
299        .join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
300
301    let Ok(version_file_info) = FileInfo::new_from_path(&version_path) else {
302        // failed to either open or query the file -- snapshot is incomplete
303        return false;
304    };
305
306    let Ok(version_str) = snapshot_version_from_file(version_file_info) else {
307        // failed to read from file -- snapshot is incomplete
308        return false;
309    };
310
311    let Ok(_snapshot_version) = SnapshotVersion::from_str(version_str.as_str()) else {
312        // invalid snapshot version -- snapshot is incomplete
313        return false;
314    };
315
316    // version file is good, so now check the serialized bank and status cache files
317    let Some(slot) = bank_snapshot_dir.as_ref().file_name() else {
318        return false;
319    };
320    let Some(slot) = slot.to_str() else {
321        return false;
322    };
323    for file_name in [slot, snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME] {
324        let file_path = bank_snapshot_dir.as_ref().join(file_name);
325        let Ok(file_info) = FileInfo::new_from_path(file_path) else {
326            // failed to either open or query the file -- snapshot is incomplete
327            return false;
328        };
329        if file_info.size == 0 {
330            // file is empty -- snapshot is incomplete
331            return false;
332        }
333    }
334
335    true
336}
337
338/// Writes files that indicate the bank snapshot is loadable by fastboot
339pub fn mark_bank_snapshot_as_loadable(bank_snapshot_dir: impl AsRef<Path>) -> io::Result<()> {
340    let snapshot_fastboot_version_path = bank_snapshot_dir
341        .as_ref()
342        .join(snapshot_paths::SNAPSHOT_FASTBOOT_VERSION_FILENAME);
343    fs::write(
344        &snapshot_fastboot_version_path,
345        SNAPSHOT_FASTBOOT_VERSION.to_string(),
346    )
347    .map_err(|err| {
348        IoError::other(format!(
349            "failed to write fastboot version file '{}': {err}",
350            snapshot_fastboot_version_path.display(),
351        ))
352    })?;
353    Ok(())
354}
355
356/// Is this bank snapshot loadable?
357fn is_bank_snapshot_loadable(
358    fastboot_version: Option<&Version>,
359) -> std::result::Result<bool, SnapshotFastbootError> {
360    if let Some(fastboot_version) = fastboot_version {
361        is_snapshot_fastboot_compatible(fastboot_version)
362    } else {
363        // No fastboot version file, so this is not a fastbootable
364        Ok(false)
365    }
366}
367
368/// Is the fastboot snapshot version compatible?
369fn is_snapshot_fastboot_compatible(
370    version: &Version,
371) -> std::result::Result<bool, SnapshotFastbootError> {
372    match version.major {
373        // Current format: storages list lives next to the bank snapshot file.
374        3 => Ok(true),
375        // Legacy format: per-storage hardlink dirs. `rebuild_storages_from_snapshot_dir`
376        // migrates them to the new format at load time.
377        2 => Ok(true),
378        v if v > SNAPSHOT_FASTBOOT_VERSION.major => {
379            Err(SnapshotFastbootError::IncompatibleVersion(version.clone()))
380        }
381        // Older format we no longer know how to load — fall back to archive.
382        _ => Ok(false),
383    }
384}
385
386/// Gets the highest, loadable, bank snapshot
387///
388/// The highest bank snapshot is the one with the highest slot.
389pub fn get_highest_loadable_bank_snapshot(
390    snapshot_config: &SnapshotConfig,
391) -> Option<BankSnapshotInfo> {
392    let highest_bank_snapshot = get_highest_bank_snapshot(&snapshot_config.bank_snapshots_dir)?;
393
394    let is_bank_snapshot_loadable =
395        is_bank_snapshot_loadable(highest_bank_snapshot.fastboot_version.as_ref());
396
397    match is_bank_snapshot_loadable {
398        Ok(true) => Some(highest_bank_snapshot),
399        Ok(false) => None,
400        Err(err) => {
401            warn!(
402                "Bank snapshot is not loadable '{}': {err}",
403                highest_bank_snapshot.snapshot_dir.display()
404            );
405            None
406        }
407    }
408}
409
410/// If the validator halts in the middle of `archive_snapshot_package()`, the temporary staging
411/// directory won't be cleaned up.  Call this function to clean them up.
412pub fn remove_tmp_snapshot_archives(snapshot_archives_dir: impl AsRef<Path>) {
413    if let Ok(entries) = std::fs::read_dir(snapshot_archives_dir) {
414        for entry in entries.flatten() {
415            if entry
416                .file_name()
417                .to_str()
418                .map(|file_name| file_name.starts_with(snapshot_paths::TMP_SNAPSHOT_ARCHIVE_PREFIX))
419                .unwrap_or(false)
420            {
421                let path = entry.path();
422                let result = if path.is_dir() {
423                    fs::remove_dir_all(&path)
424                } else {
425                    fs::remove_file(&path)
426                };
427                if let Err(err) = result {
428                    warn!(
429                        "Failed to remove temporary snapshot archive '{}': {err}",
430                        path.display(),
431                    );
432                }
433            }
434        }
435    }
436}
437
438/// Creates an archive based on the bank snapshot and snapshot storages
439pub fn archive_snapshot_package(
440    snapshot_archive_kind: SnapshotArchiveKind,
441    snapshot_slot: Slot,
442    snapshot_hash: SnapshotHash,
443    bank_snapshot_dir: impl AsRef<Path>,
444    mut snapshot_storages: Vec<Arc<AccountStorageEntry>>,
445    snapshot_config: &SnapshotConfig,
446    io_setup: &IoSetupState,
447) -> Result<SnapshotArchiveInfo> {
448    let snapshot_archive_path = match snapshot_archive_kind {
449        SnapshotArchiveKind::Full => snapshot_paths::build_full_snapshot_archive_path(
450            &snapshot_config.full_snapshot_archives_dir,
451            snapshot_slot,
452            &snapshot_hash,
453            snapshot_config.archive_format,
454        ),
455        SnapshotArchiveKind::Incremental(incremental_snapshot_base_slot) => {
456            // After the snapshot has been serialized, it is now safe (and required) to prune all
457            // the storages that are *not* to be archived for this incremental snapshot.
458            snapshot_storages.retain(|storage| storage.slot() > incremental_snapshot_base_slot);
459            snapshot_paths::build_incremental_snapshot_archive_path(
460                &snapshot_config.incremental_snapshot_archives_dir,
461                incremental_snapshot_base_slot,
462                snapshot_slot,
463                &snapshot_hash,
464                snapshot_config.archive_format,
465            )
466        }
467    };
468
469    let snapshot_archive_info = archive_snapshot(
470        snapshot_archive_kind,
471        snapshot_slot,
472        snapshot_hash,
473        snapshot_storages.as_slice(),
474        &bank_snapshot_dir,
475        snapshot_archive_path,
476        snapshot_config.archive_format,
477        io_setup,
478    )?;
479
480    Ok(snapshot_archive_info)
481}
482
483/// Serializes a snapshot into `bank_snapshots_dir`
484pub fn serialize_snapshot(
485    bank_snapshots_dir: impl AsRef<Path>,
486    snapshot_version: SnapshotVersion,
487    bank_snapshot_package: BankSnapshotPackage,
488    snapshot_storages: &[Arc<AccountStorageEntry>],
489    should_finalize: bool,
490    io_setup: &IoSetupState,
491) -> Result<BankSnapshotInfo> {
492    let BankSnapshotPackage {
493        mut bank_fields,
494        bank_hash_stats,
495        status_cache_slot_deltas,
496    } = bank_snapshot_package;
497    let status_cache_slot_deltas = status_cache_slot_deltas.as_slice();
498    let slot = bank_fields.slot;
499
500    // this lambda function is to facilitate converting between
501    // the AddBankSnapshotError and SnapshotError types
502    let do_serialize_snapshot = || {
503        let mut measure_everything = Measure::start("");
504        let bank_snapshot_dir = snapshot_paths::get_bank_snapshot_dir(&bank_snapshots_dir, slot);
505        if bank_snapshot_dir.exists() {
506            return Err(AddBankSnapshotError::SnapshotDirAlreadyExists(
507                bank_snapshot_dir,
508            ));
509        }
510        fs::create_dir_all(&bank_snapshot_dir).map_err(|err| {
511            AddBankSnapshotError::CreateSnapshotDir(err, bank_snapshot_dir.clone())
512        })?;
513
514        // the bank snapshot is stored as bank_snapshots_dir/slot/slot
515        let bank_snapshot_path =
516            bank_snapshot_dir.join(snapshot_paths::get_snapshot_file_name(slot));
517        info!(
518            "Creating bank snapshot for slot {slot} at '{}'",
519            bank_snapshot_path.display(),
520        );
521
522        let bank_snapshot_serializer = move |stream: &mut dyn Write| -> Result<()> {
523            let versioned_epoch_stakes = mem::take(&mut bank_fields.versioned_epoch_stakes);
524            let extra_fields = ExtraFieldsToSerialize {
525                lamports_per_signature: bank_fields.fee_rate_governor.lamports_per_signature,
526                unused_incremental_snapshot_persistence: None,
527                unused_epoch_accounts_hash: None,
528                versioned_epoch_stakes,
529                accounts_lt_hash: Some(bank_fields.accounts_lt_hash.clone().into()),
530                block_id: Some(bank_fields.block_id),
531            };
532            serde_snapshot::serialize_bank_snapshot_into(
533                stream,
534                bank_fields,
535                bank_hash_stats,
536                snapshot_storages,
537                extra_fields,
538            )?;
539            Ok(())
540        };
541        let (bank_snapshot_consumed_size, bank_serialize) = measure_time!(
542            serialize_snapshot_data_file(&bank_snapshot_path, io_setup, bank_snapshot_serializer)
543                .map_err(|err| AddBankSnapshotError::SerializeBank(Box::new(err)))?,
544            "bank serialize"
545        );
546
547        let status_cache_path =
548            bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME);
549        let (status_cache_consumed_size, status_cache_serialize_us) = measure_us!(
550            serde_snapshot::serialize_status_cache(
551                status_cache_slot_deltas,
552                &status_cache_path,
553                io_setup,
554            )
555            .map_err(|err| AddBankSnapshotError::SerializeStatusCache(Box::new(err)))?
556        );
557
558        let version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
559        let (_, write_version_file_us) = measure_us!(
560            fs::write(&version_path, snapshot_version.as_str().as_bytes(),)
561                .map_err(|err| AddBankSnapshotError::WriteSnapshotVersionFile(err, version_path))?
562        );
563
564        let (flush_storages_us, serialize_obsolete_accounts_us, write_storages_list_us) =
565            if should_finalize {
566                let flush_measure = Measure::start("");
567                for storage in snapshot_storages {
568                    storage.flush().map_err(|err| {
569                        AddBankSnapshotError::FlushStorage(err, storage.path().to_path_buf())
570                    })?;
571                    // We're about to mark this snapshot fastboot-loadable. Pin the storage
572                    // file so it outlives the validator-exit Drop chain.
573                    storage.disable_remove_on_drop();
574                }
575                let flush_us = flush_measure.end_as_us();
576
577                let (_, serialize_obsolete_accounts_us) = measure_us!({
578                    write_obsolete_accounts_to_snapshot(
579                        &bank_snapshot_dir,
580                        snapshot_storages,
581                        slot,
582                        io_setup,
583                    )
584                    .map_err(|err| AddBankSnapshotError::SerializeObsoleteAccounts(Box::new(err)))?
585                });
586
587                let (_, write_storages_list_us) = measure_us!(
588                    write_storages_list_to_snapshot(
589                        &bank_snapshot_dir,
590                        snapshot_storages,
591                        io_setup,
592                    )
593                    .map_err(|err| AddBankSnapshotError::WriteStoragesList(Box::new(err)))?
594                );
595
596                mark_bank_snapshot_as_loadable(&bank_snapshot_dir)
597                    .map_err(AddBankSnapshotError::MarkSnapshotLoadable)?;
598
599                (
600                    Some(flush_us),
601                    Some(serialize_obsolete_accounts_us),
602                    Some(write_storages_list_us),
603                )
604            } else {
605                (None, None, None)
606            };
607
608        measure_everything.stop();
609
610        // Monitor sizes because they're capped to MAX_SNAPSHOT_DATA_FILE_SIZE
611        datapoint_info!(
612            "snapshot_bank",
613            ("slot", slot, i64),
614            ("bank_size", bank_snapshot_consumed_size, i64),
615            ("status_cache_size", status_cache_consumed_size, i64),
616            ("flush_storages_us", flush_storages_us, Option<i64>),
617            ("serialize_obsolete_accounts_us", serialize_obsolete_accounts_us, Option<i64>),
618            ("write_storages_list_us", write_storages_list_us, Option<i64>),
619            ("bank_serialize_us", bank_serialize.as_us(), i64),
620            ("status_cache_serialize_us", status_cache_serialize_us, i64),
621            ("write_version_file_us", write_version_file_us, i64),
622            ("total_us", measure_everything.as_us(), i64),
623        );
624
625        info!(
626            "{} for slot {} at {}",
627            bank_serialize,
628            slot,
629            bank_snapshot_path.display(),
630        );
631
632        Ok(BankSnapshotInfo {
633            slot,
634            snapshot_dir: bank_snapshot_dir,
635            snapshot_version,
636            fastboot_version: None,
637        })
638    };
639
640    do_serialize_snapshot().map_err(|err| SnapshotError::AddBankSnapshot(err, slot))
641}
642
643/// Get the bank snapshots in a directory
644pub fn get_bank_snapshots(bank_snapshots_dir: impl AsRef<Path>) -> Vec<BankSnapshotInfo> {
645    let mut bank_snapshots = Vec::default();
646    match fs::read_dir(&bank_snapshots_dir) {
647        Err(err) => {
648            info!(
649                "Unable to read bank snapshots directory '{}': {err}",
650                bank_snapshots_dir.as_ref().display(),
651            );
652        }
653        Ok(paths) => paths
654            .filter_map(|entry| {
655                // check if this entry is a directory and only a Slot
656                // bank snapshots are bank_snapshots_dir/slot/slot
657                entry
658                    .ok()
659                    .filter(|entry| entry.path().is_dir())
660                    .and_then(|entry| {
661                        entry
662                            .path()
663                            .file_name()
664                            .and_then(|file_name| file_name.to_str())
665                            .and_then(|file_name| file_name.parse::<Slot>().ok())
666                    })
667            })
668            .for_each(
669                |slot| match BankSnapshotInfo::new_from_dir(&bank_snapshots_dir, slot) {
670                    Ok(snapshot_info) => bank_snapshots.push(snapshot_info),
671                    // Other threads may be modifying bank snapshots in parallel; only return
672                    // snapshots that are complete as deemed by BankSnapshotInfo::new_from_dir()
673                    Err(err) => debug!("Unable to read bank snapshot for slot {slot}: {err}"),
674                },
675            ),
676    }
677    bank_snapshots
678}
679
680/// Get the bank snapshot with the highest slot in a directory
681///
682/// This function gets the highest bank snapshot of any kind
683pub fn get_highest_bank_snapshot(bank_snapshots_dir: impl AsRef<Path>) -> Option<BankSnapshotInfo> {
684    do_get_highest_bank_snapshot(get_bank_snapshots(&bank_snapshots_dir))
685}
686
687fn do_get_highest_bank_snapshot(
688    mut bank_snapshots: Vec<BankSnapshotInfo>,
689) -> Option<BankSnapshotInfo> {
690    bank_snapshots.sort_unstable();
691    bank_snapshots.into_iter().next_back()
692}
693
694pub fn write_obsolete_accounts_to_snapshot(
695    bank_snapshot_dir: impl AsRef<Path>,
696    snapshot_storages: &[Arc<AccountStorageEntry>],
697    snapshot_slot: Slot,
698    io_setup: &IoSetupState,
699) -> Result<u64> {
700    let obsolete_accounts =
701        SerdeObsoleteAccountsMap::new_from_storages(snapshot_storages, snapshot_slot);
702    serialize_obsolete_accounts(
703        bank_snapshot_dir,
704        &obsolete_accounts,
705        MAX_OBSOLETE_ACCOUNTS_FILE_SIZE,
706        io_setup,
707    )
708}
709
710fn serialize_obsolete_accounts(
711    bank_snapshot_dir: impl AsRef<Path>,
712    obsolete_accounts_map: &SerdeObsoleteAccountsMap,
713    maximum_obsolete_accounts_file_size: u64,
714    io_setup: &IoSetupState,
715) -> Result<u64> {
716    let obsolete_accounts_path = bank_snapshot_dir
717        .as_ref()
718        .join(snapshot_paths::SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME);
719    let mut file_stream = SizeLimitedWriter::new(
720        large_file_buf_writer(&obsolete_accounts_path, io_setup)?,
721        maximum_obsolete_accounts_file_size,
722    );
723
724    serde_snapshot::serialize_into(&mut file_stream, obsolete_accounts_map).map_err(|err| {
725        IoError::other(format!(
726            "unable to serialize obsolete accounts to file '{}': {err}",
727            obsolete_accounts_path.display(),
728        ))
729    })?;
730
731    Ok(file_stream.bytes_written())
732}
733
734fn deserialize_obsolete_accounts(
735    bank_snapshot_dir: impl AsRef<Path>,
736    maximum_obsolete_accounts_file_size: u64,
737) -> Result<SerdeObsoleteAccountsMap> {
738    let obsolete_accounts_path = bank_snapshot_dir
739        .as_ref()
740        .join(snapshot_paths::SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME);
741    let obsolete_accounts_reader = ReadAdapter::new(large_file_buf_reader(
742        &obsolete_accounts_path,
743        AUX_SNAPSHOT_FILE_READ_BUF_SIZE,
744        &IoSetupState::default(),
745    )?);
746    // If the file is too large return error
747    let obsolete_accounts_file_metadata = fs::metadata(&obsolete_accounts_path)?;
748    if obsolete_accounts_file_metadata.len() > maximum_obsolete_accounts_file_size {
749        let error_message = format!(
750            "too large obsolete accounts file to deserialize: '{}' has {} bytes (max size is \
751             {maximum_obsolete_accounts_file_size} bytes)",
752            obsolete_accounts_path.display(),
753            obsolete_accounts_file_metadata.len(),
754        );
755        return Err(IoError::other(error_message).into());
756    }
757
758    Ok(serde_snapshot::deserialize_wincode_from(
759        obsolete_accounts_reader,
760    )?)
761}
762
763pub fn write_storages_list_to_snapshot(
764    bank_snapshot_dir: impl AsRef<Path>,
765    snapshot_storages: &[Arc<AccountStorageEntry>],
766    io_setup: &IoSetupState,
767) -> Result<FileSize> {
768    let storages_list = StoragesList::new_from_storages(snapshot_storages);
769    serialize_storages_list_to_snapshot(bank_snapshot_dir, storages_list, io_setup)
770}
771
772fn serialize_storages_list_to_snapshot(
773    bank_snapshot_dir: impl AsRef<Path>,
774    storages_list: StoragesList,
775    io_setup: &IoSetupState,
776) -> Result<FileSize> {
777    let storages_list_path = bank_snapshot_dir
778        .as_ref()
779        .join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME);
780    let mut file_stream = SizeLimitedWriter::new(
781        large_file_buf_writer(&storages_list_path, io_setup)?,
782        MAX_STORAGES_LIST_FILE_SIZE,
783    );
784    serde_snapshot::serialize_into(&mut file_stream, &storages_list).map_err(|err| {
785        IoError::other(format!(
786            "unable to serialize storages list to file '{}': {err}",
787            storages_list_path.display(),
788        ))
789    })?;
790    Ok(file_stream.bytes_written())
791}
792
793fn deserialize_storages_list(
794    storages_list_path: &Path,
795    maximum_storages_list_file_size: u64,
796) -> Result<StoragesList> {
797    let storages_list_reader = ReadAdapter::new(large_file_buf_reader(
798        storages_list_path,
799        AUX_SNAPSHOT_FILE_READ_BUF_SIZE,
800        &IoSetupState::default(),
801    )?);
802    // If the file is too large return error
803    let storages_list_file_metadata = fs::metadata(storages_list_path)?;
804    if storages_list_file_metadata.len() > maximum_storages_list_file_size {
805        let error_message = format!(
806            "too large storages list file to deserialize: '{}' has {} bytes (max size is \
807             {maximum_storages_list_file_size} bytes)",
808            storages_list_path.display(),
809            storages_list_file_metadata.len(),
810        );
811        return Err(IoError::other(error_message).into());
812    }
813
814    Ok(serde_snapshot::deserialize_wincode_from(
815        storages_list_reader,
816    )?)
817}
818
819pub fn serialize_snapshot_data_file<F>(
820    data_file_path: &Path,
821    io_setup: &IoSetupState,
822    serializer: F,
823) -> Result<u64>
824where
825    F: FnOnce(&mut dyn Write) -> Result<()>,
826{
827    serialize_snapshot_data_file_capped::<F>(
828        data_file_path,
829        MAX_SNAPSHOT_DATA_FILE_SIZE,
830        io_setup,
831        serializer,
832    )
833}
834
835pub fn deserialize_snapshot_data_file<T: Sized>(
836    data_file_path: &Path,
837    deserializer: impl FnOnce(&mut BufReader<std::fs::File>) -> Result<T>,
838) -> Result<T> {
839    let wrapped_deserializer = move |streams: &mut SnapshotStreams<std::fs::File>| -> Result<T> {
840        deserializer(streams.full_snapshot_stream)
841    };
842
843    let wrapped_data_file_path = SnapshotRootPaths {
844        full_snapshot_root_file_path: data_file_path.to_path_buf(),
845        incremental_snapshot_root_file_path: None,
846    };
847
848    deserialize_snapshot_data_files_capped(
849        &wrapped_data_file_path,
850        MAX_SNAPSHOT_DATA_FILE_SIZE,
851        wrapped_deserializer,
852    )
853}
854
855pub fn deserialize_snapshot_data_files<T: Sized>(
856    snapshot_root_paths: &SnapshotRootPaths,
857    deserializer: impl FnOnce(&mut SnapshotStreams<std::fs::File>) -> Result<T>,
858) -> Result<T> {
859    deserialize_snapshot_data_files_capped(
860        snapshot_root_paths,
861        MAX_SNAPSHOT_DATA_FILE_SIZE,
862        deserializer,
863    )
864}
865
866fn serialize_snapshot_data_file_capped<F>(
867    data_file_path: &Path,
868    maximum_file_size: u64,
869    io_setup: &IoSetupState,
870    serializer: F,
871) -> Result<u64>
872where
873    F: FnOnce(&mut dyn Write) -> Result<()>,
874{
875    let mut data_file_stream = SizeLimitedWriter::new(
876        large_file_buf_writer(data_file_path, io_setup)?,
877        maximum_file_size,
878    );
879    serializer(&mut data_file_stream).map_err(|err| {
880        IoError::other(format!(
881            "unable to serialize snapshot data to file '{}': {err}",
882            data_file_path.display(),
883        ))
884    })?;
885    data_file_stream.flush()?;
886    Ok(data_file_stream.bytes_written())
887}
888
889fn deserialize_snapshot_data_files_capped<T: Sized>(
890    snapshot_root_paths: &SnapshotRootPaths,
891    maximum_file_size: u64,
892    deserializer: impl FnOnce(&mut SnapshotStreams<std::fs::File>) -> Result<T>,
893) -> Result<T> {
894    let (full_snapshot_file_size, mut full_snapshot_data_file_stream) =
895        create_snapshot_data_file_stream(
896            &snapshot_root_paths.full_snapshot_root_file_path,
897            maximum_file_size,
898        )?;
899
900    let (incremental_snapshot_file_size, mut incremental_snapshot_data_file_stream) =
901        if let Some(ref incremental_snapshot_root_file_path) =
902            snapshot_root_paths.incremental_snapshot_root_file_path
903        {
904            Some(create_snapshot_data_file_stream(
905                incremental_snapshot_root_file_path,
906                maximum_file_size,
907            )?)
908        } else {
909            None
910        }
911        .unzip();
912
913    let mut snapshot_streams = SnapshotStreams {
914        full_snapshot_stream: &mut full_snapshot_data_file_stream,
915        incremental_snapshot_stream: incremental_snapshot_data_file_stream.as_mut(),
916    };
917    let ret = deserializer(&mut snapshot_streams)?;
918
919    check_deserialize_file_consumed(
920        full_snapshot_file_size,
921        &snapshot_root_paths.full_snapshot_root_file_path,
922        &mut full_snapshot_data_file_stream,
923    )?;
924
925    if let Some(ref incremental_snapshot_root_file_path) =
926        snapshot_root_paths.incremental_snapshot_root_file_path
927    {
928        check_deserialize_file_consumed(
929            incremental_snapshot_file_size.unwrap(),
930            incremental_snapshot_root_file_path,
931            incremental_snapshot_data_file_stream.as_mut().unwrap(),
932        )?;
933    }
934
935    Ok(ret)
936}
937
938/// Before running the deserializer function, perform common operations on the snapshot archive
939/// files, such as checking the file size and opening the file into a stream.
940fn create_snapshot_data_file_stream(
941    snapshot_root_file_path: impl AsRef<Path>,
942    maximum_file_size: u64,
943) -> Result<(u64, BufReader<std::fs::File>)> {
944    let snapshot_file_size = fs::metadata(&snapshot_root_file_path)?.len();
945
946    if snapshot_file_size > maximum_file_size {
947        let error_message = format!(
948            "too large snapshot data file to deserialize: '{}' has {} bytes (max size is {} bytes)",
949            snapshot_root_file_path.as_ref().display(),
950            snapshot_file_size,
951            maximum_file_size,
952        );
953        return Err(IoError::other(error_message).into());
954    }
955
956    let snapshot_data_file = fs::File::open(snapshot_root_file_path)?;
957    let snapshot_data_file_stream = BufReader::new(snapshot_data_file);
958
959    Ok((snapshot_file_size, snapshot_data_file_stream))
960}
961
962/// After running the deserializer function, perform common checks to ensure the snapshot archive
963/// files were consumed correctly.
964fn check_deserialize_file_consumed(
965    file_size: u64,
966    file_path: impl AsRef<Path>,
967    file_stream: &mut BufReader<std::fs::File>,
968) -> Result<()> {
969    let consumed_size = file_stream.stream_position()?;
970
971    if consumed_size != file_size {
972        let error_message = format!(
973            "invalid snapshot data file: '{}' has {} bytes, however consumed {} bytes to \
974             deserialize",
975            file_path.as_ref().display(),
976            file_size,
977            consumed_size,
978        );
979        return Err(IoError::other(error_message).into());
980    }
981
982    Ok(())
983}
984
985/// Unarchives the given full and incremental snapshot archives, as long as they are compatible.
986pub fn verify_and_unarchive_snapshots(
987    bank_snapshots_dir: impl AsRef<Path>,
988    full_snapshot_archive_info: &FullSnapshotArchiveInfo,
989    incremental_snapshot_archive_info: Option<&IncrementalSnapshotArchiveInfo>,
990    account_paths: &[PathBuf],
991    io_setup: &IoSetupState,
992) -> Result<(UnarchivedSnapshots, UnarchivedSnapshotsGuard)> {
993    check_are_snapshots_compatible(
994        full_snapshot_archive_info,
995        incremental_snapshot_archive_info,
996    )?;
997
998    let next_append_vec_id = Arc::new(AtomicAccountsFileId::new(0));
999    let UnarchivedSnapshot {
1000        unpack_dir: full_unpack_dir,
1001        storage: full_storage,
1002        bank_fields: full_bank_fields,
1003        accounts_db_fields: full_accounts_db_fields,
1004        unpacked_snapshots_dir_and_version: full_unpacked_snapshots_dir_and_version,
1005        measure_untar: full_measure_untar,
1006    } = unarchive_snapshot(
1007        &bank_snapshots_dir,
1008        snapshot_paths::TMP_SNAPSHOT_ARCHIVE_PREFIX,
1009        full_snapshot_archive_info.path(),
1010        "snapshot untar",
1011        account_paths,
1012        full_snapshot_archive_info.archive_format(),
1013        next_append_vec_id.clone(),
1014        io_setup,
1015    )?;
1016
1017    let (
1018        incremental_unpack_dir,
1019        incremental_storage,
1020        incremental_bank_fields,
1021        incremental_accounts_db_fields,
1022        incremental_unpacked_snapshots_dir_and_version,
1023        incremental_measure_untar,
1024    ) = if let Some(incremental_snapshot_archive_info) = incremental_snapshot_archive_info {
1025        let UnarchivedSnapshot {
1026            unpack_dir,
1027            storage,
1028            bank_fields,
1029            accounts_db_fields,
1030            unpacked_snapshots_dir_and_version,
1031            measure_untar,
1032        } = unarchive_snapshot(
1033            &bank_snapshots_dir,
1034            snapshot_paths::TMP_SNAPSHOT_ARCHIVE_PREFIX,
1035            incremental_snapshot_archive_info.path(),
1036            "incremental snapshot untar",
1037            account_paths,
1038            incremental_snapshot_archive_info.archive_format(),
1039            next_append_vec_id.clone(),
1040            io_setup,
1041        )?;
1042        (
1043            Some(unpack_dir),
1044            Some(storage),
1045            Some(bank_fields),
1046            Some(accounts_db_fields),
1047            Some(unpacked_snapshots_dir_and_version),
1048            Some(measure_untar),
1049        )
1050    } else {
1051        (None, None, None, None, None, None)
1052    };
1053
1054    let bank_fields = SnapshotBankFields::new(full_bank_fields, incremental_bank_fields);
1055    let accounts_db_fields =
1056        SnapshotAccountsDbFields::new(full_accounts_db_fields, incremental_accounts_db_fields);
1057    let next_append_vec_id = Arc::try_unwrap(next_append_vec_id).unwrap();
1058
1059    Ok((
1060        UnarchivedSnapshots {
1061            full_storage,
1062            incremental_storage,
1063            bank_fields,
1064            accounts_db_fields,
1065            full_unpacked_snapshots_dir_and_version,
1066            incremental_unpacked_snapshots_dir_and_version,
1067            full_measure_untar,
1068            incremental_measure_untar,
1069            next_append_vec_id,
1070        },
1071        UnarchivedSnapshotsGuard {
1072            full_unpack_dir,
1073            incremental_unpack_dir,
1074        },
1075    ))
1076}
1077
1078/// Used to determine if a filename is structured like a version file, bank file, or storage file
1079#[derive(PartialEq, Debug)]
1080enum SnapshotFileKind {
1081    Version,
1082    BankFields,
1083    Storage,
1084}
1085
1086/// Determines `SnapshotFileKind` for `filename` if any
1087fn get_snapshot_file_kind(filename: &str) -> Option<SnapshotFileKind> {
1088    static VERSION_FILE_REGEX: LazyLock<Regex> =
1089        LazyLock::new(|| Regex::new(r"^version$").unwrap());
1090    static BANK_FIELDS_FILE_REGEX: LazyLock<Regex> =
1091        LazyLock::new(|| Regex::new(r"^[0-9]+(\.pre)?$").unwrap());
1092
1093    if VERSION_FILE_REGEX.is_match(filename) {
1094        Some(SnapshotFileKind::Version)
1095    } else if BANK_FIELDS_FILE_REGEX.is_match(filename) {
1096        Some(SnapshotFileKind::BankFields)
1097    } else if get_slot_and_append_vec_id(filename).is_ok() {
1098        Some(SnapshotFileKind::Storage)
1099    } else {
1100        None
1101    }
1102}
1103
1104/// Waits for snapshot file
1105/// Due to parallel unpacking, we may receive some append_vec files before the snapshot file
1106/// This function will push append_vec files into a buffer until we receive the snapshot file
1107fn get_version_and_snapshot_files(
1108    file_receiver: &Receiver<FileInfo>,
1109) -> Result<(FileInfo, FileInfo, Vec<FileInfo>)> {
1110    let mut append_vec_files = Vec::with_capacity(1024);
1111    let mut snapshot_version = None;
1112    let mut snapshot_bank = None;
1113
1114    loop {
1115        if let Ok(file_info) = file_receiver.recv() {
1116            let filename = file_info.path.file_name().unwrap().to_str().unwrap();
1117            match get_snapshot_file_kind(filename) {
1118                Some(SnapshotFileKind::Version) => {
1119                    snapshot_version = Some(file_info);
1120
1121                    // break if we have both the snapshot file and the version file
1122                    if snapshot_bank.is_some() {
1123                        break;
1124                    }
1125                }
1126                Some(SnapshotFileKind::BankFields) => {
1127                    snapshot_bank = Some(file_info);
1128
1129                    // break if we have both the snapshot file and the version file
1130                    if snapshot_version.is_some() {
1131                        break;
1132                    }
1133                }
1134                Some(SnapshotFileKind::Storage) => {
1135                    append_vec_files.push(file_info);
1136                }
1137                None => {} // do nothing for other kinds of files
1138            }
1139        } else {
1140            return Err(SnapshotError::RebuildStorages(
1141                "did not receive snapshot file from unpacking threads".to_string(),
1142            ));
1143        }
1144    }
1145    let snapshot_version = snapshot_version.unwrap();
1146    let snapshot_bank = snapshot_bank.unwrap();
1147
1148    Ok((snapshot_version, snapshot_bank, append_vec_files))
1149}
1150
1151/// Fields and information parsed from the snapshot.
1152struct SnapshotFieldsBundle {
1153    snapshot_version: SnapshotVersion,
1154    bank_fields: BankFieldsToDeserialize,
1155    accounts_db_fields: AccountsDbFields<SerializableAccountStorageEntry>,
1156    append_vec_files: Vec<FileInfo>,
1157}
1158
1159/// Parses fields and information from the snapshot files provided by
1160/// `file_receiver`.
1161fn snapshot_fields_from_files(file_receiver: &Receiver<FileInfo>) -> Result<SnapshotFieldsBundle> {
1162    let (snapshot_version, snapshot_bank, append_vec_files) =
1163        get_version_and_snapshot_files(file_receiver)?;
1164    let snapshot_version_str = snapshot_version_from_file(snapshot_version)?;
1165    let snapshot_version = snapshot_version_str.parse().map_err(|err| {
1166        IoError::other(format!(
1167            "unsupported snapshot version '{snapshot_version_str}': {err}",
1168        ))
1169    })?;
1170
1171    let mut snapshot_stream = BufReader::new(snapshot_bank.file);
1172    let (bank_fields, accounts_db_fields) = match snapshot_version {
1173        SnapshotVersion::V1_2_0 => serde_snapshot::fields_from_stream(&mut snapshot_stream)?,
1174    };
1175
1176    Ok(SnapshotFieldsBundle {
1177        snapshot_version,
1178        bank_fields,
1179        accounts_db_fields,
1180        append_vec_files,
1181    })
1182}
1183
1184/// BankSnapshotInfo::new_from_dir() requires a few meta files to accept a snapshot dir
1185/// as a valid one.  A dir unpacked from an archive lacks these files.  Fill them here to
1186/// allow new_from_dir() checks to pass.  These checks are not needed for unpacked dirs,
1187/// but it is not clean to add another flag to new_from_dir() to skip them.
1188fn create_snapshot_meta_files_for_unarchived_snapshot(unpack_dir: impl AsRef<Path>) -> Result<()> {
1189    let snapshots_dir = unpack_dir.as_ref().join(snapshot_paths::BANK_SNAPSHOTS_DIR);
1190    if !snapshots_dir.is_dir() {
1191        return Err(SnapshotError::NoSnapshotSlotDir(snapshots_dir));
1192    }
1193
1194    // The unpacked dir has a single slot dir, which is the snapshot slot dir.
1195    let slot_dir = std::fs::read_dir(&snapshots_dir)
1196        .map_err(|_| SnapshotError::NoSnapshotSlotDir(snapshots_dir.clone()))?
1197        .find(|entry| entry.as_ref().unwrap().path().is_dir())
1198        .ok_or_else(|| SnapshotError::NoSnapshotSlotDir(snapshots_dir.clone()))?
1199        .map_err(|_| SnapshotError::NoSnapshotSlotDir(snapshots_dir.clone()))?
1200        .path();
1201
1202    let version_file = unpack_dir
1203        .as_ref()
1204        .join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
1205    fs::hard_link(
1206        version_file,
1207        slot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME),
1208    )?;
1209
1210    let status_cache_file = snapshots_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME);
1211    fs::hard_link(
1212        status_cache_file,
1213        slot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME),
1214    )?;
1215
1216    Ok(())
1217}
1218
1219/// Perform the common tasks when unarchiving a snapshot.  Handles creating the temporary
1220/// directories, untaring, reading the version file, and then returning those fields plus the
1221/// rebuilt storage
1222#[allow(clippy::too_many_arguments)]
1223fn unarchive_snapshot(
1224    bank_snapshots_dir: impl AsRef<Path>,
1225    unpacked_snapshots_dir_prefix: &'static str,
1226    snapshot_archive_path: impl AsRef<Path>,
1227    measure_name: &'static str,
1228    account_paths: &[PathBuf],
1229    archive_format: ArchiveFormat,
1230    next_append_vec_id: Arc<AtomicAccountsFileId>,
1231    io_setup: &IoSetupState,
1232) -> Result<UnarchivedSnapshot> {
1233    let unpack_dir = tempfile::Builder::new()
1234        .prefix(unpacked_snapshots_dir_prefix)
1235        .tempdir_in(bank_snapshots_dir)?;
1236    let unpacked_snapshots_dir = unpack_dir.path().join(snapshot_paths::BANK_SNAPSHOTS_DIR);
1237
1238    let (file_sender, file_receiver) = crossbeam_channel::unbounded();
1239    thread::scope(|scope| {
1240        let unarchive_handle = streaming_unarchive_snapshot(
1241            scope,
1242            file_sender,
1243            account_paths.to_vec(),
1244            unpack_dir.path().to_path_buf(),
1245            snapshot_archive_path.as_ref().to_path_buf(),
1246            archive_format,
1247            io_setup,
1248        );
1249
1250        let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then(
1251            |SnapshotFieldsBundle {
1252                 snapshot_version,
1253                 bank_fields,
1254                 accounts_db_fields,
1255                 append_vec_files,
1256                 ..
1257             }| {
1258                let (storage, measure_untar) = measure_time!(
1259                    SnapshotStorageRebuilder::rebuild_storages(
1260                        append_vec_files.into_iter().chain(file_receiver),
1261                        next_append_vec_id,
1262                        SnapshotFrom::Archive,
1263                        None,
1264                    )?,
1265                    measure_name
1266                );
1267                info!("{measure_untar}");
1268                create_snapshot_meta_files_for_unarchived_snapshot(&unpack_dir)?;
1269
1270                Ok(UnarchivedSnapshot {
1271                    unpack_dir,
1272                    storage,
1273                    bank_fields,
1274                    accounts_db_fields,
1275                    unpacked_snapshots_dir_and_version: UnpackedSnapshotsDirAndVersion {
1276                        unpacked_snapshots_dir,
1277                        snapshot_version,
1278                    },
1279                    measure_untar,
1280                })
1281            },
1282        );
1283        // Producer errors are usually the root cause (no files -> no reception).
1284        let unarchive_result = unarchive_handle.join().expect("must join unarchive thread");
1285        match (unarchive_result, snapshot_result) {
1286            // Rebuilder closed the receiver early; the producer's send failure is just the
1287            // downstream symptom — surface the rebuilder's error instead.
1288            (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap,
1289            (Err(err), _) => Err(err),
1290            (Ok(()), snap) => snap,
1291        }
1292    })
1293}
1294
1295/// Spawn thread that streams snapshot dir files across channel
1296///
1297/// Follow the flow of streaming_unarchive_snapshot(), but handle the from_dir case.
1298fn spawn_streaming_snapshot_dir_files(
1299    snapshot_file_path: PathBuf,
1300    snapshot_version_path: PathBuf,
1301    account_paths: &[PathBuf],
1302) -> (Receiver<FileInfo>, thread::JoinHandle<Result<()>>) {
1303    let (file_sender, file_receiver) = crossbeam_channel::unbounded();
1304    let account_paths = account_paths.to_vec();
1305
1306    let handle = thread::Builder::new()
1307        .name("solSnapDirFiles".to_string())
1308        .spawn(move || {
1309            let snapshot_bank_file_info = FileInfo::new_from_path(snapshot_file_path)?;
1310            file_sender.send(snapshot_bank_file_info)?;
1311            let snapshot_version_file_info = FileInfo::new_from_path(snapshot_version_path)?;
1312            file_sender.send(snapshot_version_file_info)?;
1313
1314            for account_path in account_paths {
1315                for dir_entry_result in fs::read_dir(account_path)? {
1316                    let dir_entry = dir_entry_result?;
1317                    let path = dir_entry.path();
1318                    let file_info = FileInfo::new_from_path(path)?;
1319                    file_sender.send(file_info)?;
1320                }
1321            }
1322            Ok::<_, SnapshotError>(())
1323        })
1324        .expect("should spawn thread");
1325
1326    (file_receiver, handle)
1327}
1328
1329/// Migrates a legacy (2.0.0) bank snapshot's hardlink-based storages into the new format.
1330///
1331/// Walks `<bank_snapshot_dir>/accounts_hardlinks/`, follows each symlink to its
1332/// `<account_path>/snapshot/<slot>/` target, and renames each storage file there back into
1333/// `<account_path>/run/`. Writes the derived storages list into the bank snapshot dir so the
1334/// load path can always read it from disk. Tears down the legacy directories (the
1335/// `accounts_hardlinks/` symlink dir and the whole `<account_path>/snapshot/` tree) on success
1336/// so subsequent restarts go through the normal new-format path.
1337fn migrate_legacy_hardlinks(bank_snapshot_dir: &Path, account_run_paths: &[PathBuf]) -> Result<()> {
1338    let accounts_hardlinks_dir =
1339        bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS);
1340    let mut items: Vec<StorageListItem> = Vec::new();
1341
1342    for entry in fs::read_dir(&accounts_hardlinks_dir).map_err(|err| {
1343        IoError::other(format!(
1344            "failed to read legacy accounts hardlinks dir '{}': {err}",
1345            accounts_hardlinks_dir.display(),
1346        ))
1347    })? {
1348        let symlink_path = entry?.path();
1349        let snapshot_slot_dir = fs::read_link(&symlink_path).map_err(|err| {
1350            IoError::other(format!(
1351                "failed to read symlink '{}': {err}",
1352                symlink_path.display(),
1353            ))
1354        })?;
1355        // snapshot_slot_dir = `<X>/snapshot/<slot>/`. The account run dir is its
1356        // grandparent + `run` (i.e. `<X>/run`).
1357        let run_dir = snapshot_slot_dir
1358            .parent()
1359            .and_then(Path::parent)
1360            .ok_or_else(|| {
1361                IoError::other(format!(
1362                    "invalid legacy hardlink target '{}'",
1363                    snapshot_slot_dir.display(),
1364                ))
1365            })?
1366            .join(ACCOUNTS_RUN_DIR);
1367        // The legacy snapshot was taken against the account paths in use at the time. If
1368        // those have changed (e.g. the operator reconfigured `account_paths` while upgrading
1369        // Agave), the run dir we just derived isn't one we're loading into — bail rather than
1370        // silently writing files into a location nobody's reading from.
1371        if !account_run_paths.contains(&run_dir) {
1372            return Err(IoError::other(format!(
1373                "legacy hardlink target '{}' points to run dir '{}' which is not in the current \
1374                 account paths ({:?}); the account paths configuration has changed since this \
1375                 snapshot was taken — load from a snapshot archive instead",
1376                snapshot_slot_dir.display(),
1377                run_dir.display(),
1378                account_run_paths,
1379            ))
1380            .into());
1381        }
1382
1383        for file_entry in fs::read_dir(&snapshot_slot_dir).map_err(|err| {
1384            IoError::other(format!(
1385                "failed to read legacy hardlink dir '{}': {err}",
1386                snapshot_slot_dir.display(),
1387            ))
1388        })? {
1389            let src = file_entry?.path();
1390            let Some(name) = src.file_name().and_then(|n| n.to_str()) else {
1391                continue;
1392            };
1393            let (slot, id) = get_slot_and_append_vec_id(name)?;
1394            let dest = run_dir.join(name);
1395            fs::rename(&src, &dest).map_err(|err| {
1396                IoError::other(format!(
1397                    "failed to migrate legacy storage from '{}' to '{}': {err}",
1398                    src.display(),
1399                    dest.display(),
1400                ))
1401            })?;
1402            items.push(StorageListItem {
1403                slot,
1404                id: id as AccountsFileId,
1405            });
1406        }
1407    }
1408
1409    // Persist the derived list now: migration is destructive (it removes the legacy hardlinks
1410    // below), and writing the storages list is what actually brings the bank snapshot to
1411    // fastboot version >=3 compatibility. Doing it here means the snapshot stays loadable even
1412    // if the validator never performs a proper teardown (e.g. crashes).
1413    serialize_storages_list_to_snapshot(
1414        bank_snapshot_dir,
1415        StoragesList::from_items(items),
1416        &IoSetupState::default(),
1417    )?;
1418
1419    // Tear down the legacy state so we don't repeat this migration: drop the bank snapshot's
1420    // `accounts_hardlinks/` symlink dir and wipe each `<account_path>/snapshot/` tree (catches
1421    // both the per-slot dirs we just emptied and any orphans from older purged snapshots).
1422    fs::remove_dir_all(&accounts_hardlinks_dir).map_err(|err| {
1423        IoError::other(format!(
1424            "failed to remove legacy accounts hardlinks dir '{}': {err}",
1425            accounts_hardlinks_dir.display(),
1426        ))
1427    })?;
1428    wipe_account_snapshot_dirs(account_run_paths);
1429
1430    // Bump the fastboot version so subsequent loads take the normal 3.0+ path instead of
1431    // re-running the migration (which would fail now that the hardlinks dir is gone).
1432    mark_bank_snapshot_as_loadable(bank_snapshot_dir)?;
1433
1434    Ok(())
1435}
1436
1437/// Removes storage files from `account_paths` whose `(slot, id)` pair isn't listed in the
1438/// storages list (i.e. they don't belong to the snapshot being loaded). Files whose names
1439/// don't parse as `<slot>.<id>` storage filenames are left alone.
1440fn prune_stale_storages(account_paths: &[PathBuf], storages_list: StoragesList) -> Result<()> {
1441    let expected_storages = storages_list.into_slot_file_id_set();
1442    for account_path in account_paths {
1443        let read_dir = fs::read_dir(account_path).map_err(|err| {
1444            IoError::other(format!(
1445                "failed to read account path '{}': {err}",
1446                account_path.display(),
1447            ))
1448        })?;
1449        for entry in read_dir {
1450            let path = entry?.path();
1451            let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
1452                continue;
1453            };
1454            let Ok((slot, id)) = get_slot_and_append_vec_id(name) else {
1455                // Not a storage file name — leave it alone.
1456                continue;
1457            };
1458            if !expected_storages.contains(&(slot, id as AccountsFileId)) {
1459                info!(
1460                    "Removing stale storage file '{}' not in storages list",
1461                    path.display(),
1462                );
1463                fs::remove_file(&path)?
1464            }
1465        }
1466    }
1467    Ok(())
1468}
1469
1470/// Performs the common tasks when deserializing a snapshot
1471///
1472/// Handles reading the snapshot file and version file,
1473/// then returning those fields plus the rebuilt storages.
1474pub(crate) fn rebuild_storages_from_snapshot_dir(
1475    snapshot_info: &BankSnapshotInfo,
1476    account_paths: &[PathBuf],
1477    next_append_vec_id: Arc<AtomicAccountsFileId>,
1478) -> Result<(
1479    AccountStorageMap,
1480    BankFieldsToDeserialize,
1481    AccountsDbFields<SerializableAccountStorageEntry>,
1482)> {
1483    let bank_snapshot_dir = &snapshot_info.snapshot_dir;
1484
1485    // With fastboot_version >= 2, obsolete accounts are tracked and stored in the snapshot
1486    // Even if obsolete accounts are not enabled, the snapshot may still contain obsolete accounts
1487    // as the feature may have been enabled in previous validator runs.
1488    let obsolete_accounts = snapshot_info
1489        .fastboot_version
1490        .as_ref()
1491        .is_some_and(|fastboot_version| fastboot_version.major >= 2)
1492        .then(|| deserialize_obsolete_accounts(bank_snapshot_dir, MAX_OBSOLETE_ACCOUNTS_FILE_SIZE))
1493        .transpose()
1494        .map_err(|err| {
1495            IoError::other(format!(
1496                "failed to read obsolete accounts file '{}': {err}",
1497                bank_snapshot_dir.display()
1498            ))
1499        })?;
1500
1501    // The bank snapshot lists the storage files belonging to it. Anything else in the account
1502    // paths is from a later (post-snapshot) slot and must be removed before we load — the
1503    // lt hash check at startup verifies the surviving storages.
1504    let storages_list_path =
1505        bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME);
1506    if !storages_list_path.exists() {
1507        // Legacy (2.0.0) bank snapshot: storages live as hardlinks under
1508        // `<account_path>/snapshot/<slot>/`, with symlinks in
1509        // `<bank_snapshot_dir>/accounts_hardlinks/` tying them to the bank snapshot. Move the
1510        // files back into `<account_path>/run/` and write out the storages list so the load
1511        // path below can read it like any other 3.0+ snapshot.
1512        migrate_legacy_hardlinks(bank_snapshot_dir, account_paths)?;
1513    }
1514    let storages_list =
1515        deserialize_storages_list(&storages_list_path, MAX_STORAGES_LIST_FILE_SIZE)?;
1516    prune_stale_storages(account_paths, storages_list)?;
1517
1518    let snapshot_file_path = snapshot_info.snapshot_path();
1519    let snapshot_version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
1520    let (file_receiver, stream_files_handle) = spawn_streaming_snapshot_dir_files(
1521        snapshot_file_path,
1522        snapshot_version_path,
1523        account_paths,
1524    );
1525
1526    let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then(
1527        |SnapshotFieldsBundle {
1528             bank_fields,
1529             accounts_db_fields,
1530             append_vec_files,
1531             ..
1532         }| {
1533            let storage = SnapshotStorageRebuilder::rebuild_storages(
1534                append_vec_files.into_iter().chain(file_receiver),
1535                next_append_vec_id,
1536                SnapshotFrom::Dir,
1537                obsolete_accounts,
1538            )?;
1539            Ok((storage, bank_fields, accounts_db_fields))
1540        },
1541    );
1542
1543    // Producer errors are usually the root cause (no files -> no reception).
1544    let stream_files_result = stream_files_handle.join().expect("must join dir thread");
1545    match (stream_files_result, snapshot_result) {
1546        // Rebuilder closed the receiver early; the producer's send failure is just the
1547        // downstream symptom — surface the rebuilder's error instead.
1548        (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap,
1549        (Err(err), _) => Err(err),
1550        (Ok(()), snap) => snap,
1551    }
1552}
1553
1554/// Reads the `snapshot_version` from a file. Before opening the file, its size
1555/// is compared to `MAX_SNAPSHOT_VERSION_FILE_SIZE`. If the size exceeds this
1556/// threshold, it is not opened and an error is returned.
1557fn snapshot_version_from_file(mut file_info: FileInfo) -> io::Result<String> {
1558    let file_size = file_info.size;
1559    if file_size > MAX_SNAPSHOT_VERSION_FILE_SIZE {
1560        let error_message = format!(
1561            "snapshot version file too large: '{}' has {} bytes (max size is {} bytes)",
1562            file_info.path.display(),
1563            file_size,
1564            MAX_SNAPSHOT_VERSION_FILE_SIZE,
1565        );
1566        return Err(IoError::other(error_message));
1567    }
1568
1569    // Read snapshot_version from file.
1570    let mut snapshot_version = String::new();
1571    file_info
1572        .file
1573        .read_to_string(&mut snapshot_version)
1574        .map_err(|err| {
1575            IoError::other(format!(
1576                "failed to read snapshot version from file '{}': {err}",
1577                file_info.path.display()
1578            ))
1579        })?;
1580
1581    Ok(snapshot_version.trim().to_string())
1582}
1583
1584/// Check if an incremental snapshot is compatible with a full snapshot.  This is done by checking
1585/// if the incremental snapshot's base slot is the same as the full snapshot's slot.
1586fn check_are_snapshots_compatible(
1587    full_snapshot_archive_info: &FullSnapshotArchiveInfo,
1588    incremental_snapshot_archive_info: Option<&IncrementalSnapshotArchiveInfo>,
1589) -> Result<()> {
1590    if incremental_snapshot_archive_info.is_none() {
1591        return Ok(());
1592    }
1593
1594    let incremental_snapshot_archive_info = incremental_snapshot_archive_info.unwrap();
1595
1596    (full_snapshot_archive_info.slot() == incremental_snapshot_archive_info.base_slot())
1597        .then_some(())
1598        .ok_or_else(|| {
1599            SnapshotError::MismatchedBaseSlot(
1600                full_snapshot_archive_info.slot(),
1601                incremental_snapshot_archive_info.base_slot(),
1602            )
1603        })
1604}
1605
1606pub fn purge_old_snapshot_archives(
1607    full_snapshot_archives_dir: impl AsRef<Path>,
1608    incremental_snapshot_archives_dir: impl AsRef<Path>,
1609    maximum_full_snapshot_archives_to_retain: NonZeroUsize,
1610    maximum_incremental_snapshot_archives_to_retain: NonZeroUsize,
1611) {
1612    info!(
1613        "Purging old full snapshot archives in {}, retaining up to {} full snapshots",
1614        full_snapshot_archives_dir.as_ref().display(),
1615        maximum_full_snapshot_archives_to_retain
1616    );
1617
1618    let mut full_snapshot_archives =
1619        snapshot_paths::full_snapshot_archives_iter(full_snapshot_archives_dir.as_ref())
1620            .collect::<Vec<_>>();
1621    full_snapshot_archives.sort_unstable();
1622    full_snapshot_archives.reverse();
1623
1624    let num_to_retain = full_snapshot_archives
1625        .len()
1626        .min(maximum_full_snapshot_archives_to_retain.get());
1627    trace!(
1628        "There are {} full snapshot archives, retaining {}",
1629        full_snapshot_archives.len(),
1630        num_to_retain,
1631    );
1632
1633    let (full_snapshot_archives_to_retain, full_snapshot_archives_to_remove) =
1634        if full_snapshot_archives.is_empty() {
1635            None
1636        } else {
1637            Some(full_snapshot_archives.split_at(num_to_retain))
1638        }
1639        .unwrap_or_default();
1640
1641    let retained_full_snapshot_slots = full_snapshot_archives_to_retain
1642        .iter()
1643        .map(|ai| ai.slot())
1644        .collect::<HashSet<_>>();
1645
1646    fn remove_archives<T: SnapshotArchiveInfoGetter>(archives: &[T]) {
1647        for path in archives.iter().map(|a| a.path()) {
1648            trace!("Removing snapshot archive: {}", path.display());
1649            let result = fs::remove_file(path);
1650            if let Err(err) = result {
1651                info!(
1652                    "Failed to remove snapshot archive '{}': {err}",
1653                    path.display()
1654                );
1655            }
1656        }
1657    }
1658    remove_archives(full_snapshot_archives_to_remove);
1659
1660    info!(
1661        "Purging old incremental snapshot archives in {}, retaining up to {} incremental snapshots",
1662        incremental_snapshot_archives_dir.as_ref().display(),
1663        maximum_incremental_snapshot_archives_to_retain
1664    );
1665    let mut incremental_snapshot_archives_by_base_slot = HashMap::<Slot, Vec<_>>::new();
1666    for incremental_snapshot_archive in
1667        incremental_snapshot_archives_iter(incremental_snapshot_archives_dir.as_ref())
1668    {
1669        incremental_snapshot_archives_by_base_slot
1670            .entry(incremental_snapshot_archive.base_slot())
1671            .or_default()
1672            .push(incremental_snapshot_archive)
1673    }
1674
1675    let highest_full_snapshot_slot = retained_full_snapshot_slots.iter().max().copied();
1676    for (base_slot, mut incremental_snapshot_archives) in incremental_snapshot_archives_by_base_slot
1677    {
1678        incremental_snapshot_archives.sort_unstable();
1679        let num_to_retain = if Some(base_slot) == highest_full_snapshot_slot {
1680            maximum_incremental_snapshot_archives_to_retain.get()
1681        } else {
1682            usize::from(retained_full_snapshot_slots.contains(&base_slot))
1683        };
1684        trace!(
1685            "There are {} incremental snapshot archives for base slot {}, removing {} of them",
1686            incremental_snapshot_archives.len(),
1687            base_slot,
1688            incremental_snapshot_archives
1689                .len()
1690                .saturating_sub(num_to_retain),
1691        );
1692
1693        incremental_snapshot_archives.truncate(
1694            incremental_snapshot_archives
1695                .len()
1696                .saturating_sub(num_to_retain),
1697        );
1698        remove_archives(&incremental_snapshot_archives);
1699    }
1700}
1701
1702pub fn verify_unpacked_snapshots_dir_and_version(
1703    unpacked_snapshots_dir_and_version: &UnpackedSnapshotsDirAndVersion,
1704) -> Result<(SnapshotVersion, BankSnapshotInfo)> {
1705    info!(
1706        "snapshot version: {}",
1707        unpacked_snapshots_dir_and_version.snapshot_version
1708    );
1709
1710    let snapshot_version = unpacked_snapshots_dir_and_version.snapshot_version;
1711    let mut bank_snapshots =
1712        get_bank_snapshots(&unpacked_snapshots_dir_and_version.unpacked_snapshots_dir);
1713    if bank_snapshots.len() > 1 {
1714        return Err(IoError::other(format!(
1715            "invalid snapshot format: only one snapshot allowed, but found {}",
1716            bank_snapshots.len(),
1717        ))
1718        .into());
1719    }
1720    let root_paths = bank_snapshots.pop().ok_or_else(|| {
1721        IoError::other(format!(
1722            "no snapshots found in snapshots directory '{}'",
1723            unpacked_snapshots_dir_and_version
1724                .unpacked_snapshots_dir
1725                .display(),
1726        ))
1727    })?;
1728    Ok((snapshot_version, root_paths))
1729}
1730
1731#[derive(Debug, Copy, Clone)]
1732/// allow tests to specify what happened to the serialized format
1733pub enum VerifyBank {
1734    /// the bank's serialized format is expected to be identical to what we are comparing against
1735    Deterministic,
1736    /// the serialized bank was 'reserialized' into a non-deterministic format
1737    /// so, deserialize both files and compare deserialized results
1738    NonDeterministic,
1739}
1740
1741/// For each account run dir, wipes the sibling `snapshot/` dir.
1742///
1743/// Validator account paths are laid out as a parent containing two siblings: `run/` (the live
1744/// storage files) and `snapshot/` (legacy per-slot hardlink dirs, pre-3.0). `account_run_paths`
1745/// here holds the `run/` paths, so for each entry we walk up to the parent and wipe the
1746/// `snapshot/` sibling. Nothing new is written under `snapshot/` anymore, so any content is
1747/// either legacy hardlink dirs (written by pre-3.0 validators during fastboot) or orphans
1748/// from purged bank snapshots. Used by the legacy-hardlink migration path and the
1749/// archive-load path to drop that leftover state.
1750pub fn wipe_account_snapshot_dirs(account_run_paths: &[PathBuf]) {
1751    for account_run_path in account_run_paths {
1752        if let Some(parent) = account_run_path.parent() {
1753            move_and_async_delete_path_contents(parent.join(ACCOUNTS_SNAPSHOT_DIR));
1754        }
1755    }
1756}
1757
1758/// Purges all bank snapshots
1759pub fn purge_all_bank_snapshots(bank_snapshots_dir: impl AsRef<Path>) {
1760    let bank_snapshots = get_bank_snapshots(&bank_snapshots_dir);
1761    purge_bank_snapshots(&bank_snapshots);
1762}
1763
1764/// Purges bank snapshots, retaining the newest `num_bank_snapshots_to_retain`
1765pub fn purge_old_bank_snapshots(
1766    bank_snapshots_dir: impl AsRef<Path>,
1767    num_bank_snapshots_to_retain: usize,
1768) {
1769    let mut bank_snapshots = get_bank_snapshots(&bank_snapshots_dir);
1770
1771    bank_snapshots.sort_unstable();
1772    purge_bank_snapshots(
1773        bank_snapshots
1774            .iter()
1775            .rev()
1776            .skip(num_bank_snapshots_to_retain),
1777    );
1778}
1779
1780/// At startup, purge old (i.e. unusable) bank snapshots
1781pub fn purge_old_bank_snapshots_at_startup(bank_snapshots_dir: impl AsRef<Path>) {
1782    purge_old_bank_snapshots(&bank_snapshots_dir, 1);
1783
1784    let highest_bank_snapshot = get_highest_bank_snapshot(&bank_snapshots_dir);
1785    if let Some(highest_bank_snapshot) = highest_bank_snapshot {
1786        debug!(
1787            "Retained bank snapshot for slot {}, and purged the rest.",
1788            highest_bank_snapshot.slot
1789        );
1790    }
1791}
1792
1793/// Purges bank snapshots that are older than `slot`
1794pub fn purge_bank_snapshots_older_than_slot(bank_snapshots_dir: impl AsRef<Path>, slot: Slot) {
1795    let mut bank_snapshots = get_bank_snapshots(&bank_snapshots_dir);
1796    bank_snapshots.retain(|bank_snapshot| bank_snapshot.slot < slot);
1797    purge_bank_snapshots(&bank_snapshots);
1798}
1799
1800/// Purges all `bank_snapshots`
1801///
1802/// Does not exit early if there is an error while purging a bank snapshot.
1803fn purge_bank_snapshots<'a>(bank_snapshots: impl IntoIterator<Item = &'a BankSnapshotInfo>) {
1804    for snapshot_dir in bank_snapshots.into_iter().map(|s| &s.snapshot_dir) {
1805        if purge_bank_snapshot(snapshot_dir).is_err() {
1806            warn!("Failed to purge bank snapshot: {}", snapshot_dir.display());
1807        }
1808    }
1809}
1810
1811/// Remove the bank snapshot at this path
1812pub fn purge_bank_snapshot(bank_snapshot_dir: impl AsRef<Path>) -> Result<()> {
1813    const FN_ERR: &str = "failed to purge bank snapshot";
1814    // Migration: snapshots written by pre-storages-list versions kept an `accounts_hardlinks/`
1815    // subdir of symlinks pointing at hardlink dirs under `<account_path>/snapshot/<slot>/`.
1816    // Follow them so the hardlink dirs don't outlive the owning bank snapshot when we purge at
1817    // runtime (startup-time cleanup catches any leftovers).
1818    let accounts_hardlinks_dir = bank_snapshot_dir
1819        .as_ref()
1820        .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS);
1821    if accounts_hardlinks_dir.is_dir() {
1822        let read_dir = fs::read_dir(&accounts_hardlinks_dir).map_err(|err| {
1823            IoError::other(format!(
1824                "{FN_ERR}: failed to read accounts hardlinks dir '{}': {err}",
1825                accounts_hardlinks_dir.display(),
1826            ))
1827        })?;
1828        for entry in read_dir {
1829            let accounts_hardlink_dir = entry?.path();
1830            let accounts_hardlink_dir = fs::read_link(&accounts_hardlink_dir).map_err(|err| {
1831                IoError::other(format!(
1832                    "{FN_ERR}: failed to read symlink '{}': {err}",
1833                    accounts_hardlink_dir.display(),
1834                ))
1835            })?;
1836            move_and_async_delete_path(&accounts_hardlink_dir);
1837        }
1838    }
1839    fs::remove_dir_all(&bank_snapshot_dir).map_err(|err| {
1840        IoError::other(format!(
1841            "{FN_ERR}: failed to remove dir '{}': {err}",
1842            bank_snapshot_dir.as_ref().display(),
1843        ))
1844    })?;
1845    Ok(())
1846}
1847
1848pub fn should_take_full_snapshot(
1849    block_height: Slot,
1850    full_snapshot_archive_interval_slots: Slot,
1851) -> bool {
1852    block_height.is_multiple_of(full_snapshot_archive_interval_slots)
1853}
1854
1855pub fn should_take_incremental_snapshot(
1856    block_height: Slot,
1857    incremental_snapshot_archive_interval_slots: Slot,
1858    latest_full_snapshot_slot: Option<Slot>,
1859) -> bool {
1860    block_height.is_multiple_of(incremental_snapshot_archive_interval_slots)
1861        && latest_full_snapshot_slot.is_some()
1862}
1863
1864/// Creates an "accounts path" directory for tests
1865///
1866/// This temporary directory will contain the "run" and "snapshot"
1867/// sub-directories required by a validator.
1868#[cfg(feature = "dev-context-only-utils")]
1869pub fn create_tmp_accounts_dir_for_tests() -> (TempDir, PathBuf) {
1870    let tmp_dir = tempfile::TempDir::new().unwrap();
1871    let account_dir = create_accounts_run_and_snapshot_dirs(&tmp_dir).unwrap().0;
1872    (tmp_dir, account_dir)
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877    use {
1878        super::*,
1879        agave_snapshots::{
1880            paths::{
1881                full_snapshot_archives_iter, get_highest_full_snapshot_archive_slot,
1882                get_highest_incremental_snapshot_archive_slot,
1883            },
1884            snapshot_config::{
1885                DEFAULT_MAX_FULL_SNAPSHOT_ARCHIVES_TO_RETAIN,
1886                DEFAULT_MAX_INCREMENTAL_SNAPSHOT_ARCHIVES_TO_RETAIN,
1887            },
1888        },
1889        assert_matches::assert_matches,
1890        bincode::{deserialize_from, serialize_into},
1891        solana_accounts_db::accounts_file::{AccountsFile, AccountsFileProvider},
1892        solana_hash::Hash,
1893        std::{convert::TryFrom, mem::size_of},
1894        tempfile::NamedTempFile,
1895        test_case::test_case,
1896    };
1897
1898    #[test]
1899    fn test_serialize_snapshot_data_file_under_limit() {
1900        let temp_dir = tempfile::TempDir::new().unwrap();
1901        let expected_consumed_size = size_of::<u32>() as u64;
1902        let consumed_size = serialize_snapshot_data_file_capped(
1903            &temp_dir.path().join("data-file"),
1904            expected_consumed_size,
1905            &IoSetupState::default(),
1906            |stream| {
1907                serialize_into(stream, &2323_u32)?;
1908                Ok(())
1909            },
1910        )
1911        .unwrap();
1912        assert_eq!(consumed_size, expected_consumed_size);
1913    }
1914
1915    #[test]
1916    fn test_serialize_snapshot_data_file_over_limit() {
1917        let temp_dir = tempfile::TempDir::new().unwrap();
1918        let expected_consumed_size = size_of::<u32>() as u64;
1919        let result = serialize_snapshot_data_file_capped(
1920            &temp_dir.path().join("data-file"),
1921            expected_consumed_size - 1,
1922            &IoSetupState::default(),
1923            |stream| {
1924                serialize_into(stream, &2323_u32)?;
1925                Ok(())
1926            },
1927        );
1928        assert_matches!(result, Err(SnapshotError::Io(ref message)) if message.to_string().contains("bytes would exceed limit of"));
1929    }
1930
1931    #[test]
1932    fn test_deserialize_snapshot_data_file_under_limit() {
1933        let expected_data = 2323_u32;
1934        let expected_consumed_size = size_of::<u32>() as u64;
1935
1936        let temp_dir = tempfile::TempDir::new().unwrap();
1937        serialize_snapshot_data_file_capped(
1938            &temp_dir.path().join("data-file"),
1939            expected_consumed_size,
1940            &IoSetupState::default(),
1941            |stream| {
1942                serialize_into(stream, &expected_data)?;
1943                Ok(())
1944            },
1945        )
1946        .unwrap();
1947
1948        let snapshot_root_paths = SnapshotRootPaths {
1949            full_snapshot_root_file_path: temp_dir.path().join("data-file"),
1950            incremental_snapshot_root_file_path: None,
1951        };
1952
1953        let actual_data = deserialize_snapshot_data_files_capped(
1954            &snapshot_root_paths,
1955            expected_consumed_size,
1956            |stream| {
1957                Ok(deserialize_from::<_, u32>(
1958                    &mut stream.full_snapshot_stream,
1959                )?)
1960            },
1961        )
1962        .unwrap();
1963        assert_eq!(actual_data, expected_data);
1964    }
1965
1966    #[test]
1967    fn test_deserialize_snapshot_data_file_over_limit() {
1968        let expected_data = 2323_u32;
1969        let expected_consumed_size = size_of::<u32>() as u64;
1970
1971        let temp_dir = tempfile::TempDir::new().unwrap();
1972        serialize_snapshot_data_file_capped(
1973            &temp_dir.path().join("data-file"),
1974            expected_consumed_size,
1975            &IoSetupState::default(),
1976            |stream| {
1977                serialize_into(stream, &expected_data)?;
1978                Ok(())
1979            },
1980        )
1981        .unwrap();
1982
1983        let snapshot_root_paths = SnapshotRootPaths {
1984            full_snapshot_root_file_path: temp_dir.path().join("data-file"),
1985            incremental_snapshot_root_file_path: None,
1986        };
1987
1988        let result = deserialize_snapshot_data_files_capped(
1989            &snapshot_root_paths,
1990            expected_consumed_size - 1,
1991            |stream| {
1992                Ok(deserialize_from::<_, u32>(
1993                    &mut stream.full_snapshot_stream,
1994                )?)
1995            },
1996        );
1997        assert_matches!(result, Err(SnapshotError::Io(ref message)) if message.to_string().starts_with("too large snapshot data file to deserialize"));
1998    }
1999
2000    #[test]
2001    fn test_deserialize_snapshot_data_file_extra_data() {
2002        let expected_data = 2323_u32;
2003        let expected_consumed_size = size_of::<u32>() as u64;
2004
2005        let temp_dir = tempfile::TempDir::new().unwrap();
2006        serialize_snapshot_data_file_capped(
2007            &temp_dir.path().join("data-file"),
2008            expected_consumed_size * 2,
2009            &IoSetupState::default(),
2010            |stream| {
2011                serialize_into(&mut *stream, &expected_data)?;
2012                serialize_into(&mut *stream, &expected_data)?;
2013                Ok(())
2014            },
2015        )
2016        .unwrap();
2017
2018        let snapshot_root_paths = SnapshotRootPaths {
2019            full_snapshot_root_file_path: temp_dir.path().join("data-file"),
2020            incremental_snapshot_root_file_path: None,
2021        };
2022
2023        let result = deserialize_snapshot_data_files_capped(
2024            &snapshot_root_paths,
2025            expected_consumed_size * 2,
2026            |stream| {
2027                Ok(deserialize_from::<_, u32>(
2028                    &mut stream.full_snapshot_stream,
2029                )?)
2030            },
2031        );
2032        assert_matches!(result, Err(SnapshotError::Io(ref message)) if message.to_string().starts_with("invalid snapshot data file"));
2033    }
2034
2035    #[test]
2036    fn test_snapshot_version_from_file_under_limit() {
2037        let file_content = SnapshotVersion::default().as_str();
2038        let mut file = NamedTempFile::new().unwrap();
2039        file.write_all(file_content.as_bytes()).unwrap();
2040        let file_info = FileInfo::new_from_path(file.path()).unwrap();
2041        let version_from_file = snapshot_version_from_file(file_info).unwrap();
2042        assert_eq!(version_from_file, file_content);
2043    }
2044
2045    #[test]
2046    fn test_snapshot_version_from_file_over_limit() {
2047        let over_limit_size = usize::try_from(MAX_SNAPSHOT_VERSION_FILE_SIZE + 1).unwrap();
2048        let file_content = vec![7u8; over_limit_size];
2049        let mut file = NamedTempFile::new().unwrap();
2050        file.write_all(&file_content).unwrap();
2051        let file_info = FileInfo::new_from_path(file.path()).unwrap();
2052        assert_matches!(
2053            snapshot_version_from_file(file_info),
2054            Err(ref message) if message.to_string().starts_with("snapshot version file too large")
2055        );
2056    }
2057
2058    #[test]
2059    fn test_check_are_snapshots_compatible() {
2060        let slot1: Slot = 1234;
2061        let slot2: Slot = 5678;
2062        let slot3: Slot = 999_999;
2063
2064        let full_snapshot_archive_info = FullSnapshotArchiveInfo::new_from_path(PathBuf::from(
2065            format!("/dir/snapshot-{}-{}.tar.zst", slot1, Hash::new_unique()),
2066        ))
2067        .unwrap();
2068
2069        assert!(check_are_snapshots_compatible(&full_snapshot_archive_info, None,).is_ok());
2070
2071        let incremental_snapshot_archive_info =
2072            IncrementalSnapshotArchiveInfo::new_from_path(PathBuf::from(format!(
2073                "/dir/incremental-snapshot-{}-{}-{}.tar.zst",
2074                slot1,
2075                slot2,
2076                Hash::new_unique()
2077            )))
2078            .unwrap();
2079
2080        assert!(
2081            check_are_snapshots_compatible(
2082                &full_snapshot_archive_info,
2083                Some(&incremental_snapshot_archive_info)
2084            )
2085            .is_ok()
2086        );
2087
2088        let incremental_snapshot_archive_info =
2089            IncrementalSnapshotArchiveInfo::new_from_path(PathBuf::from(format!(
2090                "/dir/incremental-snapshot-{}-{}-{}.tar.zst",
2091                slot2,
2092                slot3,
2093                Hash::new_unique()
2094            )))
2095            .unwrap();
2096
2097        assert!(
2098            check_are_snapshots_compatible(
2099                &full_snapshot_archive_info,
2100                Some(&incremental_snapshot_archive_info)
2101            )
2102            .is_err()
2103        );
2104    }
2105
2106    /// A test heler function that creates bank snapshot files
2107    fn common_create_bank_snapshot_files(
2108        bank_snapshots_dir: &Path,
2109        min_slot: Slot,
2110        max_slot: Slot,
2111    ) {
2112        for slot in min_slot..max_slot {
2113            let snapshot_dir = snapshot_paths::get_bank_snapshot_dir(bank_snapshots_dir, slot);
2114            fs::create_dir_all(&snapshot_dir).unwrap();
2115
2116            let snapshot_filename = snapshot_paths::get_snapshot_file_name(slot);
2117            let snapshot_path = snapshot_dir.join(snapshot_filename);
2118            fs::File::create(snapshot_path).unwrap();
2119
2120            let status_cache_file =
2121                snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME);
2122            fs::File::create(status_cache_file).unwrap();
2123
2124            let version_path = snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
2125            fs::write(version_path, SnapshotVersion::default().as_str().as_bytes()).unwrap();
2126        }
2127    }
2128
2129    #[test]
2130    fn test_get_bank_snapshots() {
2131        let temp_snapshots_dir = tempfile::TempDir::new().unwrap();
2132        let min_slot = 10;
2133        let max_slot = 20;
2134        common_create_bank_snapshot_files(temp_snapshots_dir.path(), min_slot, max_slot);
2135
2136        let bank_snapshots = get_bank_snapshots(temp_snapshots_dir.path());
2137        assert_eq!(bank_snapshots.len() as Slot, max_slot - min_slot);
2138    }
2139
2140    #[test]
2141    fn test_get_highest_bank_snapshot() {
2142        let temp_snapshots_dir = tempfile::TempDir::new().unwrap();
2143        let min_slot = 99;
2144        let max_slot = 123;
2145        common_create_bank_snapshot_files(temp_snapshots_dir.path(), min_slot, max_slot);
2146
2147        let highest_bank_snapshot = get_highest_bank_snapshot(temp_snapshots_dir.path());
2148        assert!(highest_bank_snapshot.is_some());
2149        assert_eq!(highest_bank_snapshot.unwrap().slot, max_slot - 1);
2150    }
2151
2152    /// A test helper function that creates full and incremental snapshot archive files.  Creates
2153    /// full snapshot files in the range (`min_full_snapshot_slot`, `max_full_snapshot_slot`], and
2154    /// incremental snapshot files in the range (`min_incremental_snapshot_slot`,
2155    /// `max_incremental_snapshot_slot`].  Additionally, "bad" files are created for both full and
2156    /// incremental snapshots to ensure the tests properly filter them out.
2157    fn common_create_snapshot_archive_files(
2158        full_snapshot_archives_dir: &Path,
2159        incremental_snapshot_archives_dir: &Path,
2160        min_full_snapshot_slot: Slot,
2161        max_full_snapshot_slot: Slot,
2162        min_incremental_snapshot_slot: Slot,
2163        max_incremental_snapshot_slot: Slot,
2164    ) {
2165        fs::create_dir_all(full_snapshot_archives_dir).unwrap();
2166        fs::create_dir_all(incremental_snapshot_archives_dir).unwrap();
2167        for full_snapshot_slot in min_full_snapshot_slot..max_full_snapshot_slot {
2168            for incremental_snapshot_slot in
2169                min_incremental_snapshot_slot..max_incremental_snapshot_slot
2170            {
2171                let snapshot_filename = format!(
2172                    "incremental-snapshot-{}-{}-{}.tar.zst",
2173                    full_snapshot_slot,
2174                    incremental_snapshot_slot,
2175                    Hash::default()
2176                );
2177                let snapshot_filepath = incremental_snapshot_archives_dir.join(snapshot_filename);
2178                fs::File::create(snapshot_filepath).unwrap();
2179            }
2180
2181            let snapshot_filename = format!(
2182                "snapshot-{}-{}.tar.zst",
2183                full_snapshot_slot,
2184                Hash::default()
2185            );
2186            let snapshot_filepath = full_snapshot_archives_dir.join(snapshot_filename);
2187            fs::File::create(snapshot_filepath).unwrap();
2188
2189            // Add in an incremental snapshot with a bad filename and high slot to ensure filename are filtered and sorted correctly
2190            let bad_filename = format!(
2191                "incremental-snapshot-{}-{}-bad!hash.tar.zst",
2192                full_snapshot_slot,
2193                max_incremental_snapshot_slot + 1,
2194            );
2195            let bad_filepath = incremental_snapshot_archives_dir.join(bad_filename);
2196            fs::File::create(bad_filepath).unwrap();
2197        }
2198
2199        // Add in a snapshot with a bad filename and high slot to ensure filename are filtered and
2200        // sorted correctly
2201        let bad_filename = format!("snapshot-{}-bad!hash.tar.zst", max_full_snapshot_slot + 1);
2202        let bad_filepath = full_snapshot_archives_dir.join(bad_filename);
2203        fs::File::create(bad_filepath).unwrap();
2204    }
2205
2206    #[test]
2207    fn test_get_full_snapshot_archives() {
2208        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2209        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2210        let min_slot = 123;
2211        let max_slot = 456;
2212        common_create_snapshot_archive_files(
2213            full_snapshot_archives_dir.path(),
2214            incremental_snapshot_archives_dir.path(),
2215            min_slot,
2216            max_slot,
2217            0,
2218            0,
2219        );
2220
2221        let snapshot_archives =
2222            full_snapshot_archives_iter(full_snapshot_archives_dir.path()).collect::<Vec<_>>();
2223        assert_eq!(snapshot_archives.len() as Slot, max_slot - min_slot);
2224    }
2225
2226    #[test]
2227    fn test_get_full_snapshot_archives_remote() {
2228        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2229        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2230        let min_slot = 123;
2231        let max_slot = 456;
2232        common_create_snapshot_archive_files(
2233            &full_snapshot_archives_dir
2234                .path()
2235                .join(snapshot_paths::SNAPSHOT_ARCHIVE_DOWNLOAD_DIR),
2236            &incremental_snapshot_archives_dir
2237                .path()
2238                .join(snapshot_paths::SNAPSHOT_ARCHIVE_DOWNLOAD_DIR),
2239            min_slot,
2240            max_slot,
2241            0,
2242            0,
2243        );
2244
2245        let snapshot_archives =
2246            full_snapshot_archives_iter(full_snapshot_archives_dir.path()).collect::<Vec<_>>();
2247        assert_eq!(snapshot_archives.len() as Slot, max_slot - min_slot);
2248        assert!(snapshot_archives.iter().all(|info| info.is_remote()));
2249    }
2250
2251    #[test]
2252    fn test_get_incremental_snapshot_archives() {
2253        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2254        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2255        let min_full_snapshot_slot = 12;
2256        let max_full_snapshot_slot = 23;
2257        let min_incremental_snapshot_slot = 34;
2258        let max_incremental_snapshot_slot = 45;
2259        common_create_snapshot_archive_files(
2260            full_snapshot_archives_dir.path(),
2261            incremental_snapshot_archives_dir.path(),
2262            min_full_snapshot_slot,
2263            max_full_snapshot_slot,
2264            min_incremental_snapshot_slot,
2265            max_incremental_snapshot_slot,
2266        );
2267
2268        let incremental_snapshot_archives =
2269            incremental_snapshot_archives_iter(incremental_snapshot_archives_dir.path())
2270                .collect::<Vec<_>>();
2271        assert_eq!(
2272            incremental_snapshot_archives.len() as Slot,
2273            (max_full_snapshot_slot - min_full_snapshot_slot)
2274                * (max_incremental_snapshot_slot - min_incremental_snapshot_slot)
2275        );
2276    }
2277
2278    #[test]
2279    fn test_get_incremental_snapshot_archives_remote() {
2280        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2281        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2282        let min_full_snapshot_slot = 12;
2283        let max_full_snapshot_slot = 23;
2284        let min_incremental_snapshot_slot = 34;
2285        let max_incremental_snapshot_slot = 45;
2286        common_create_snapshot_archive_files(
2287            &full_snapshot_archives_dir
2288                .path()
2289                .join(snapshot_paths::SNAPSHOT_ARCHIVE_DOWNLOAD_DIR),
2290            &incremental_snapshot_archives_dir
2291                .path()
2292                .join(snapshot_paths::SNAPSHOT_ARCHIVE_DOWNLOAD_DIR),
2293            min_full_snapshot_slot,
2294            max_full_snapshot_slot,
2295            min_incremental_snapshot_slot,
2296            max_incremental_snapshot_slot,
2297        );
2298
2299        let incremental_snapshot_archives =
2300            incremental_snapshot_archives_iter(incremental_snapshot_archives_dir.path())
2301                .collect::<Vec<_>>();
2302        assert_eq!(
2303            incremental_snapshot_archives.len() as Slot,
2304            (max_full_snapshot_slot - min_full_snapshot_slot)
2305                * (max_incremental_snapshot_slot - min_incremental_snapshot_slot)
2306        );
2307        assert!(
2308            incremental_snapshot_archives
2309                .iter()
2310                .all(|info| info.is_remote())
2311        );
2312    }
2313
2314    #[test]
2315    fn test_get_highest_full_snapshot_archive_slot() {
2316        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2317        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2318        let min_slot = 123;
2319        let max_slot = 456;
2320        common_create_snapshot_archive_files(
2321            full_snapshot_archives_dir.path(),
2322            incremental_snapshot_archives_dir.path(),
2323            min_slot,
2324            max_slot,
2325            0,
2326            0,
2327        );
2328
2329        assert_eq!(
2330            get_highest_full_snapshot_archive_slot(full_snapshot_archives_dir.path()),
2331            Some(max_slot - 1)
2332        );
2333    }
2334
2335    #[test]
2336    fn test_get_highest_incremental_snapshot_slot() {
2337        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2338        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2339        let min_full_snapshot_slot = 12;
2340        let max_full_snapshot_slot = 23;
2341        let min_incremental_snapshot_slot = 34;
2342        let max_incremental_snapshot_slot = 45;
2343        common_create_snapshot_archive_files(
2344            full_snapshot_archives_dir.path(),
2345            incremental_snapshot_archives_dir.path(),
2346            min_full_snapshot_slot,
2347            max_full_snapshot_slot,
2348            min_incremental_snapshot_slot,
2349            max_incremental_snapshot_slot,
2350        );
2351
2352        for full_snapshot_slot in min_full_snapshot_slot..max_full_snapshot_slot {
2353            assert_eq!(
2354                get_highest_incremental_snapshot_archive_slot(
2355                    incremental_snapshot_archives_dir.path(),
2356                    full_snapshot_slot
2357                ),
2358                Some(max_incremental_snapshot_slot - 1)
2359            );
2360        }
2361
2362        assert_eq!(
2363            get_highest_incremental_snapshot_archive_slot(
2364                incremental_snapshot_archives_dir.path(),
2365                max_full_snapshot_slot
2366            ),
2367            None
2368        );
2369    }
2370
2371    fn common_test_purge_old_snapshot_archives(
2372        snapshot_names: &[&String],
2373        maximum_full_snapshot_archives_to_retain: NonZeroUsize,
2374        maximum_incremental_snapshot_archives_to_retain: NonZeroUsize,
2375        expected_snapshots: &[&String],
2376    ) {
2377        let temp_snap_dir = tempfile::TempDir::new().unwrap();
2378
2379        for snap_name in snapshot_names {
2380            let snap_path = temp_snap_dir.path().join(snap_name);
2381            let mut _snap_file = fs::File::create(snap_path);
2382        }
2383        purge_old_snapshot_archives(
2384            temp_snap_dir.path(),
2385            temp_snap_dir.path(),
2386            maximum_full_snapshot_archives_to_retain,
2387            maximum_incremental_snapshot_archives_to_retain,
2388        );
2389
2390        let mut retained_snaps = HashSet::new();
2391        for entry in fs::read_dir(temp_snap_dir.path()).unwrap() {
2392            let entry_path_buf = entry.unwrap().path();
2393            let entry_path = entry_path_buf.as_path();
2394            let snapshot_name = entry_path
2395                .file_name()
2396                .unwrap()
2397                .to_str()
2398                .unwrap()
2399                .to_string();
2400            retained_snaps.insert(snapshot_name);
2401        }
2402
2403        for snap_name in expected_snapshots {
2404            assert!(
2405                retained_snaps.contains(snap_name.as_str()),
2406                "{snap_name} not found"
2407            );
2408        }
2409        assert_eq!(retained_snaps.len(), expected_snapshots.len());
2410    }
2411
2412    #[test]
2413    fn test_purge_old_full_snapshot_archives() {
2414        let snap1_name = format!("snapshot-1-{}.tar.zst", Hash::default());
2415        let snap2_name = format!("snapshot-3-{}.tar.zst", Hash::default());
2416        let snap3_name = format!("snapshot-50-{}.tar.zst", Hash::default());
2417        let snapshot_names = vec![&snap1_name, &snap2_name, &snap3_name];
2418
2419        // expecting only the newest to be retained
2420        let expected_snapshots = vec![&snap3_name];
2421        common_test_purge_old_snapshot_archives(
2422            &snapshot_names,
2423            NonZeroUsize::new(1).unwrap(),
2424            DEFAULT_MAX_INCREMENTAL_SNAPSHOT_ARCHIVES_TO_RETAIN,
2425            &expected_snapshots,
2426        );
2427
2428        // retaining 2, expecting the 2 newest to be retained
2429        let expected_snapshots = vec![&snap2_name, &snap3_name];
2430        common_test_purge_old_snapshot_archives(
2431            &snapshot_names,
2432            NonZeroUsize::new(2).unwrap(),
2433            DEFAULT_MAX_INCREMENTAL_SNAPSHOT_ARCHIVES_TO_RETAIN,
2434            &expected_snapshots,
2435        );
2436
2437        // retaining 3, all three should be retained
2438        let expected_snapshots = vec![&snap1_name, &snap2_name, &snap3_name];
2439        common_test_purge_old_snapshot_archives(
2440            &snapshot_names,
2441            NonZeroUsize::new(3).unwrap(),
2442            DEFAULT_MAX_INCREMENTAL_SNAPSHOT_ARCHIVES_TO_RETAIN,
2443            &expected_snapshots,
2444        );
2445    }
2446
2447    /// Mimic a running node's behavior w.r.t. purging old snapshot archives.  Take snapshots in a
2448    /// loop, and periodically purge old snapshot archives.  After purging, check to make sure the
2449    /// snapshot archives on disk are correct.
2450    #[test]
2451    fn test_purge_old_full_snapshot_archives_in_the_loop() {
2452        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2453        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2454        let maximum_snapshots_to_retain = NonZeroUsize::new(5).unwrap();
2455        let starting_slot: Slot = 42;
2456
2457        for slot in (starting_slot..).take(100) {
2458            let full_snapshot_archive_file_name =
2459                format!("snapshot-{}-{}.tar.zst", slot, Hash::default());
2460            let full_snapshot_archive_path = full_snapshot_archives_dir
2461                .as_ref()
2462                .join(full_snapshot_archive_file_name);
2463            fs::File::create(full_snapshot_archive_path).unwrap();
2464
2465            // don't purge-and-check until enough snapshot archives have been created
2466            if slot < starting_slot + maximum_snapshots_to_retain.get() as Slot {
2467                continue;
2468            }
2469
2470            // purge infrequently, so there will always be snapshot archives to purge
2471            if slot % (maximum_snapshots_to_retain.get() as Slot * 2) != 0 {
2472                continue;
2473            }
2474
2475            purge_old_snapshot_archives(
2476                &full_snapshot_archives_dir,
2477                &incremental_snapshot_archives_dir,
2478                maximum_snapshots_to_retain,
2479                NonZeroUsize::new(usize::MAX).unwrap(),
2480            );
2481            let mut full_snapshot_archives =
2482                full_snapshot_archives_iter(full_snapshot_archives_dir.path()).collect::<Vec<_>>();
2483            full_snapshot_archives.sort_unstable();
2484            assert_eq!(
2485                full_snapshot_archives.len(),
2486                maximum_snapshots_to_retain.get()
2487            );
2488            assert_eq!(full_snapshot_archives.last().unwrap().slot(), slot);
2489            for (i, full_snapshot_archive) in full_snapshot_archives.iter().rev().enumerate() {
2490                assert_eq!(full_snapshot_archive.slot(), slot - i as Slot);
2491            }
2492        }
2493    }
2494
2495    #[test]
2496    fn test_purge_old_incremental_snapshot_archives() {
2497        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2498        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2499        let starting_slot = 100_000;
2500
2501        let maximum_incremental_snapshot_archives_to_retain =
2502            DEFAULT_MAX_INCREMENTAL_SNAPSHOT_ARCHIVES_TO_RETAIN;
2503        let maximum_full_snapshot_archives_to_retain = DEFAULT_MAX_FULL_SNAPSHOT_ARCHIVES_TO_RETAIN;
2504
2505        let incremental_snapshot_interval = 100;
2506        let num_incremental_snapshots_per_full_snapshot =
2507            maximum_incremental_snapshot_archives_to_retain.get() * 2;
2508        let full_snapshot_interval =
2509            incremental_snapshot_interval * num_incremental_snapshots_per_full_snapshot;
2510
2511        let mut snapshot_filenames = vec![];
2512        (starting_slot..)
2513            .step_by(full_snapshot_interval)
2514            .take(
2515                maximum_full_snapshot_archives_to_retain
2516                    .checked_mul(NonZeroUsize::new(2).unwrap())
2517                    .unwrap()
2518                    .get(),
2519            )
2520            .for_each(|full_snapshot_slot| {
2521                let snapshot_filename = format!(
2522                    "snapshot-{}-{}.tar.zst",
2523                    full_snapshot_slot,
2524                    Hash::default()
2525                );
2526                let snapshot_path = full_snapshot_archives_dir.path().join(&snapshot_filename);
2527                fs::File::create(snapshot_path).unwrap();
2528                snapshot_filenames.push(snapshot_filename);
2529
2530                (full_snapshot_slot..)
2531                    .step_by(incremental_snapshot_interval)
2532                    .take(num_incremental_snapshots_per_full_snapshot)
2533                    .skip(1)
2534                    .for_each(|incremental_snapshot_slot| {
2535                        let snapshot_filename = format!(
2536                            "incremental-snapshot-{}-{}-{}.tar.zst",
2537                            full_snapshot_slot,
2538                            incremental_snapshot_slot,
2539                            Hash::default()
2540                        );
2541                        let snapshot_path = incremental_snapshot_archives_dir
2542                            .path()
2543                            .join(&snapshot_filename);
2544                        fs::File::create(snapshot_path).unwrap();
2545                        snapshot_filenames.push(snapshot_filename);
2546                    });
2547            });
2548
2549        purge_old_snapshot_archives(
2550            full_snapshot_archives_dir.path(),
2551            incremental_snapshot_archives_dir.path(),
2552            maximum_full_snapshot_archives_to_retain,
2553            maximum_incremental_snapshot_archives_to_retain,
2554        );
2555
2556        // Ensure correct number of full snapshot archives are purged/retained
2557        let mut remaining_full_snapshot_archives =
2558            full_snapshot_archives_iter(full_snapshot_archives_dir.path()).collect::<Vec<_>>();
2559        assert_eq!(
2560            remaining_full_snapshot_archives.len(),
2561            maximum_full_snapshot_archives_to_retain.get(),
2562        );
2563        remaining_full_snapshot_archives.sort_unstable();
2564        let latest_full_snapshot_archive_slot =
2565            remaining_full_snapshot_archives.last().unwrap().slot();
2566
2567        // Ensure correct number of incremental snapshot archives are purged/retained
2568        // For each additional full snapshot archive, one additional (the newest)
2569        // incremental snapshot archive is retained. This is accounted for by the
2570        // `+ maximum_full_snapshot_archives_to_retain.saturating_sub(1)`
2571        let mut remaining_incremental_snapshot_archives =
2572            incremental_snapshot_archives_iter(incremental_snapshot_archives_dir.path())
2573                .collect::<Vec<_>>();
2574        assert_eq!(
2575            remaining_incremental_snapshot_archives.len(),
2576            maximum_incremental_snapshot_archives_to_retain
2577                .get()
2578                .saturating_add(
2579                    maximum_full_snapshot_archives_to_retain
2580                        .get()
2581                        .saturating_sub(1)
2582                )
2583        );
2584        remaining_incremental_snapshot_archives.sort_unstable();
2585        remaining_incremental_snapshot_archives.reverse();
2586
2587        // Ensure there exists one incremental snapshot all but the latest full snapshot
2588        for i in (1..maximum_full_snapshot_archives_to_retain.get()).rev() {
2589            let incremental_snapshot_archive =
2590                remaining_incremental_snapshot_archives.pop().unwrap();
2591
2592            let expected_base_slot =
2593                latest_full_snapshot_archive_slot - (i * full_snapshot_interval) as u64;
2594            assert_eq!(incremental_snapshot_archive.base_slot(), expected_base_slot);
2595            let expected_slot = expected_base_slot
2596                + (full_snapshot_interval - incremental_snapshot_interval) as u64;
2597            assert_eq!(incremental_snapshot_archive.slot(), expected_slot);
2598        }
2599
2600        // Ensure all remaining incremental snapshots are only for the latest full snapshot
2601        for incremental_snapshot_archive in &remaining_incremental_snapshot_archives {
2602            assert_eq!(
2603                incremental_snapshot_archive.base_slot(),
2604                latest_full_snapshot_archive_slot
2605            );
2606        }
2607
2608        // Ensure the remaining incremental snapshots are at the right slot
2609        let expected_remaining_incremental_snapshot_archive_slots =
2610            (latest_full_snapshot_archive_slot..)
2611                .step_by(incremental_snapshot_interval)
2612                .take(num_incremental_snapshots_per_full_snapshot)
2613                .skip(
2614                    num_incremental_snapshots_per_full_snapshot
2615                        - maximum_incremental_snapshot_archives_to_retain.get(),
2616                )
2617                .collect::<HashSet<_>>();
2618
2619        let actual_remaining_incremental_snapshot_archive_slots =
2620            remaining_incremental_snapshot_archives
2621                .iter()
2622                .map(|snapshot| snapshot.slot())
2623                .collect::<HashSet<_>>();
2624        assert_eq!(
2625            actual_remaining_incremental_snapshot_archive_slots,
2626            expected_remaining_incremental_snapshot_archive_slots
2627        );
2628    }
2629
2630    #[test]
2631    fn test_purge_all_incremental_snapshot_archives_when_no_full_snapshot_archives() {
2632        let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2633        let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap();
2634
2635        for snapshot_filenames in [
2636            format!("incremental-snapshot-100-120-{}.tar.zst", Hash::default()),
2637            format!("incremental-snapshot-100-140-{}.tar.zst", Hash::default()),
2638            format!("incremental-snapshot-100-160-{}.tar.zst", Hash::default()),
2639            format!("incremental-snapshot-100-180-{}.tar.zst", Hash::default()),
2640            format!("incremental-snapshot-200-220-{}.tar.zst", Hash::default()),
2641            format!("incremental-snapshot-200-240-{}.tar.zst", Hash::default()),
2642            format!("incremental-snapshot-200-260-{}.tar.zst", Hash::default()),
2643            format!("incremental-snapshot-200-280-{}.tar.zst", Hash::default()),
2644        ] {
2645            let snapshot_path = incremental_snapshot_archives_dir
2646                .path()
2647                .join(snapshot_filenames);
2648            fs::File::create(snapshot_path).unwrap();
2649        }
2650
2651        purge_old_snapshot_archives(
2652            full_snapshot_archives_dir.path(),
2653            incremental_snapshot_archives_dir.path(),
2654            NonZeroUsize::new(usize::MAX).unwrap(),
2655            NonZeroUsize::new(usize::MAX).unwrap(),
2656        );
2657
2658        let remaining_incremental_snapshot_archives =
2659            incremental_snapshot_archives_iter(incremental_snapshot_archives_dir.path())
2660                .collect::<Vec<_>>();
2661        assert!(remaining_incremental_snapshot_archives.is_empty());
2662    }
2663
2664    #[test]
2665    fn test_get_snapshot_file_kind() {
2666        assert_eq!(None, get_snapshot_file_kind("file.txt"));
2667        assert_eq!(
2668            Some(SnapshotFileKind::Version),
2669            get_snapshot_file_kind(snapshot_paths::SNAPSHOT_VERSION_FILENAME)
2670        );
2671        assert_eq!(
2672            Some(SnapshotFileKind::BankFields),
2673            get_snapshot_file_kind("1234")
2674        );
2675        assert_eq!(
2676            Some(SnapshotFileKind::Storage),
2677            get_snapshot_file_kind("1000.999")
2678        );
2679    }
2680
2681    #[test_case(0)]
2682    #[test_case(1)]
2683    #[test_case(10)]
2684    fn test_serialize_deserialize_account_storage_entries(num_storages: u64) {
2685        let temp_dir = tempfile::tempdir().unwrap();
2686        let bank_snapshot_dir = temp_dir.path();
2687        let snapshot_slot = num_storages + 1 as Slot;
2688
2689        // Create AccountStorageEntries
2690        let mut snapshot_storages = Vec::new();
2691        for i in 0..num_storages {
2692            let storage = Arc::new(AccountStorageEntry::new(
2693                &PathBuf::new(),
2694                i,        // Incrementing slot
2695                i as u32, // Incrementing id
2696                1024,
2697                AccountsFileProvider::AppendVec,
2698            ));
2699            snapshot_storages.push(storage);
2700        }
2701
2702        // write obsolete accounts to snapshot
2703        write_obsolete_accounts_to_snapshot(
2704            bank_snapshot_dir,
2705            &snapshot_storages,
2706            snapshot_slot,
2707            &IoSetupState::default(),
2708        )
2709        .unwrap();
2710
2711        // Deserialize
2712        let mut deserialized_accounts =
2713            deserialize_obsolete_accounts(bank_snapshot_dir, MAX_OBSOLETE_ACCOUNTS_FILE_SIZE)
2714                .unwrap()
2715                .into_hashmap();
2716
2717        // Verify
2718        for storage in &snapshot_storages {
2719            let obsolete_accounts = deserialized_accounts.remove(&storage.slot()).unwrap();
2720            assert!(obsolete_accounts.into_tuple().2 == 0);
2721        }
2722    }
2723
2724    #[test]
2725    #[should_panic(expected = "bytes would exceed limit of 100")]
2726    fn test_serialize_obsolete_accounts_too_large_file() {
2727        let temp_dir = tempfile::tempdir().unwrap();
2728        let bank_snapshot_dir = temp_dir.path();
2729        let num_storages = 10;
2730        let snapshot_slot = num_storages + 1 as Slot;
2731
2732        // Create AccountStorageEntries
2733        let mut snapshot_storages = Vec::new();
2734        for i in 0..num_storages {
2735            let storage = Arc::new(AccountStorageEntry::new(
2736                &PathBuf::new(),
2737                i,        // Incrementing slot
2738                i as u32, // Incrementing id
2739                1024,
2740                AccountsFileProvider::AppendVec,
2741            ));
2742            snapshot_storages.push(storage);
2743        }
2744
2745        // write obsolete accounts to snapshot
2746        let obsolete_accounts =
2747            SerdeObsoleteAccountsMap::new_from_storages(&snapshot_storages, snapshot_slot);
2748
2749        // Limit the file size to something low for the test
2750        serialize_obsolete_accounts(
2751            bank_snapshot_dir,
2752            &obsolete_accounts,
2753            100,
2754            &IoSetupState::default(),
2755        )
2756        .unwrap();
2757    }
2758
2759    #[test]
2760    #[should_panic(expected = "too large obsolete accounts file to deserialize")]
2761    fn test_deserialize_obsolete_accounts_too_large_file() {
2762        let temp_dir = tempfile::tempdir().unwrap();
2763        let bank_snapshot_dir = temp_dir.path();
2764        let num_storages = 10;
2765        let snapshot_slot = num_storages + 1 as Slot;
2766
2767        // Create AccountStorageEntries
2768        let mut snapshot_storages = Vec::new();
2769        for i in 0..num_storages {
2770            let storage = Arc::new(AccountStorageEntry::new(
2771                &PathBuf::new(),
2772                i,        // Incrementing slot
2773                i as u32, // Incrementing id
2774                1024,
2775                AccountsFileProvider::AppendVec,
2776            ));
2777            snapshot_storages.push(storage);
2778        }
2779
2780        // Write obsolete accounts to snapshot
2781        write_obsolete_accounts_to_snapshot(
2782            bank_snapshot_dir,
2783            &snapshot_storages,
2784            snapshot_slot,
2785            &IoSetupState::default(),
2786        )
2787        .unwrap();
2788
2789        // Set a very low maximum file size for deserialization
2790        // This should panic
2791        deserialize_obsolete_accounts(bank_snapshot_dir, 100).unwrap();
2792    }
2793
2794    #[test]
2795    fn test_is_bank_snapshot_complete() {
2796        let temp_dir = TempDir::new().unwrap();
2797        let slot = 123;
2798        let bank_snapshot_dir = temp_dir.as_ref().join(slot.to_string());
2799        fs::create_dir(&bank_snapshot_dir).unwrap();
2800
2801        let version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME);
2802        let serialized_bank_path = bank_snapshot_dir.join(slot.to_string());
2803        let status_cache_path =
2804            bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME);
2805
2806        // scenario 1: no version file
2807        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2808
2809        // scenario 2: bad version file (too large)
2810        let too_large = format!(
2811            "{:v>width$}",
2812            "hi",
2813            width = (MAX_SNAPSHOT_VERSION_FILE_SIZE + 1) as usize,
2814        );
2815        fs::write(&version_path, too_large).unwrap();
2816        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2817
2818        // scenario 3: bad version
2819        fs::remove_file(&version_path).unwrap();
2820        let bad_version = String::from("v0.0.0");
2821        fs::write(&version_path, bad_version).unwrap();
2822        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2823
2824        // scenario 4: empty version
2825        fs::remove_file(&version_path).unwrap();
2826        fs::File::create_new(&version_path).unwrap();
2827        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2828
2829        // write a "good" version file so we can check the next file
2830        fs::remove_file(&version_path).unwrap();
2831        fs::write(&version_path, SnapshotVersion::default().as_str()).unwrap();
2832
2833        // scenario 5: no serialized bank file
2834        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2835
2836        // scenario 6: empty serialized bank
2837        fs::File::create_new(&serialized_bank_path).unwrap();
2838        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2839
2840        // write a "good" serialized bank file so we can check the next file
2841        fs::remove_file(&serialized_bank_path).unwrap();
2842        fs::write(&serialized_bank_path, "serialized bank").unwrap();
2843
2844        // scenario 7: no status cache file
2845        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2846
2847        // scenario 8: empty status cache
2848        fs::File::create_new(&status_cache_path).unwrap();
2849        assert!(!is_bank_snapshot_complete(&bank_snapshot_dir));
2850
2851        // write a "good" status cache file so we can check for all good
2852        fs::remove_file(&status_cache_path).unwrap();
2853        fs::write(&status_cache_path, "status cache").unwrap();
2854
2855        // scenario 9: all good
2856        assert!(is_bank_snapshot_complete(bank_snapshot_dir));
2857    }
2858
2859    #[test]
2860    fn test_prune_stale_storages() {
2861        let account_path = tempfile::TempDir::new().unwrap();
2862        // Files that belong to the snapshot.
2863        let keep_a = account_path.path().join(AccountsFile::file_name(100, 1));
2864        let keep_b = account_path.path().join(AccountsFile::file_name(200, 2));
2865        // A stale storage file that should be removed.
2866        let stale = account_path.path().join(AccountsFile::file_name(300, 3));
2867        // A non-storage filename — should be left alone.
2868        let untouched = account_path.path().join("something_else.txt");
2869        for path in [&keep_a, &keep_b, &stale, &untouched] {
2870            fs::write(path, b"x").unwrap();
2871        }
2872
2873        let storages_list = StoragesList::from_items(vec![
2874            StorageListItem { slot: 100, id: 1 },
2875            StorageListItem { slot: 200, id: 2 },
2876        ]);
2877        prune_stale_storages(
2878            std::slice::from_ref(&account_path.path().to_path_buf()),
2879            storages_list,
2880        )
2881        .unwrap();
2882
2883        assert!(keep_a.exists(), "expected storage file was deleted");
2884        assert!(keep_b.exists(), "expected storage file was deleted");
2885        assert!(!stale.exists(), "stale storage file was not removed");
2886        assert!(untouched.exists(), "non-storage file was wrongly removed");
2887    }
2888}