From ac22051e5c5c000eafada9e4f6f26608ebc06dc6 Mon Sep 17 00:00:00 2001 From: Brooks Date: Thu, 2 Jan 2025 16:51:37 -0500 Subject: [PATCH] Snapshots use lattice-based accounts hash (#4113) --- accounts-db/src/accounts_db.rs | 22 ++++ accounts-db/src/accounts_hash.rs | 9 ++ core/src/accounts_hash_verifier.rs | 51 ++++++-- core/tests/snapshots.rs | 11 +- ledger-tool/src/args.rs | 6 + runtime/src/bank.rs | 44 ++++++- runtime/src/bank/accounts_lt_hash.rs | 102 +++++++++++++++ runtime/src/snapshot_bank_utils.rs | 180 ++++++++++++++++++--------- runtime/src/snapshot_hash.rs | 25 +++- runtime/src/snapshot_package.rs | 62 +++++++-- runtime/src/snapshot_utils.rs | 7 +- sdk/feature-set/src/lib.rs | 5 + validator/src/cli.rs | 6 + validator/src/main.rs | 2 + 14 files changed, 434 insertions(+), 98 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 45ccf21f64cae6..9d1ac8cc54be25 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -514,6 +514,7 @@ pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig { scan_filter_for_shrinking: ScanFilter::OnlyAbnormalWithVerify, enable_experimental_accumulator_hash: false, verify_experimental_accumulator_hash: false, + snapshots_use_experimental_accumulator_hash: false, num_clean_threads: None, num_foreground_threads: None, num_hash_threads: None, @@ -540,6 +541,7 @@ pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig scan_filter_for_shrinking: ScanFilter::OnlyAbnormalWithVerify, enable_experimental_accumulator_hash: false, verify_experimental_accumulator_hash: false, + snapshots_use_experimental_accumulator_hash: false, num_clean_threads: None, num_foreground_threads: None, num_hash_threads: None, @@ -668,6 +670,7 @@ pub struct AccountsDbConfig { pub scan_filter_for_shrinking: ScanFilter, pub enable_experimental_accumulator_hash: bool, pub verify_experimental_accumulator_hash: bool, + pub snapshots_use_experimental_accumulator_hash: bool, /// Number of threads for background cleaning operations (`thread_pool_clean') pub num_clean_threads: Option, /// Number of threads for foreground operations (`thread_pool`) @@ -1621,6 +1624,10 @@ pub struct AccountsDb { /// (For R&D only) pub verify_experimental_accumulator_hash: bool, + /// Flag to indicate if the experimental accounts lattice hash is used for snapshots. + /// (For R&D only; a feature-gate also exists to turn this on.) + pub snapshots_use_experimental_accumulator_hash: AtomicBool, + /// These are the ancient storages that could be valuable to /// shrink, sorted by amount of dead bytes. The elements /// are sorted from the largest dead bytes to the smallest. @@ -2039,6 +2046,9 @@ impl AccountsDb { .into(), verify_experimental_accumulator_hash: accounts_db_config .verify_experimental_accumulator_hash, + snapshots_use_experimental_accumulator_hash: accounts_db_config + .snapshots_use_experimental_accumulator_hash + .into(), thread_pool, thread_pool_clean, thread_pool_hash, @@ -2129,6 +2139,18 @@ impl AccountsDb { .store(is_enabled, Ordering::Release); } + /// Returns if snapshots use the experimental accounts lattice hash + pub fn snapshots_use_experimental_accumulator_hash(&self) -> bool { + self.snapshots_use_experimental_accumulator_hash + .load(Ordering::Acquire) + } + + /// Sets if snapshots use the experimental accounts lattice hash + pub fn set_snapshots_use_experimental_accumulator_hash(&self, is_enabled: bool) { + self.snapshots_use_experimental_accumulator_hash + .store(is_enabled, Ordering::Release); + } + /// While scanning cleaning candidates obtain slots that can be /// reclaimed for each pubkey. In addition, if the pubkey is /// removed from the index, insert in pubkeys_removed_from_accounts_index. diff --git a/accounts-db/src/accounts_hash.rs b/accounts-db/src/accounts_hash.rs index bf2b8e3a8ef9f7..ad5b26cb8fcb66 100644 --- a/accounts-db/src/accounts_hash.rs +++ b/accounts-db/src/accounts_hash.rs @@ -1279,6 +1279,15 @@ pub const ZERO_LAMPORT_ACCOUNT_LT_HASH: AccountLtHash = AccountLtHash(LtHash::id #[derive(Debug, Clone, Eq, PartialEq)] pub struct AccountsLtHash(pub LtHash); +/// Hash of accounts +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MerkleOrLatticeAccountsHash { + /// Merkle-based hash of accounts + Merkle(AccountsHashKind), + /// Lattice-based hash of accounts + Lattice, +} + /// Hash of accounts #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum AccountsHashKind { diff --git a/core/src/accounts_hash_verifier.rs b/core/src/accounts_hash_verifier.rs index bdf0a760fabdf2..06a3578edbd816 100644 --- a/core/src/accounts_hash_verifier.rs +++ b/core/src/accounts_hash_verifier.rs @@ -7,7 +7,7 @@ use { accounts_db::CalcAccountsHashKind, accounts_hash::{ AccountsHash, AccountsHashKind, CalcAccountsHashConfig, HashStats, - IncrementalAccountsHash, + IncrementalAccountsHash, MerkleOrLatticeAccountsHash, }, sorted_storages::SortedStorages, }, @@ -16,7 +16,8 @@ use { serde_snapshot::BankIncrementalSnapshotPersistence, snapshot_config::SnapshotConfig, snapshot_package::{ - self, AccountsPackage, AccountsPackageKind, SnapshotKind, SnapshotPackage, + self, AccountsHashAlgorithm, AccountsPackage, AccountsPackageKind, SnapshotKind, + SnapshotPackage, }, snapshot_utils, }, @@ -215,10 +216,10 @@ impl AccountsHashVerifier { pending_snapshot_packages: &Mutex, snapshot_config: &SnapshotConfig, ) -> IoResult<()> { - let (accounts_hash_kind, bank_incremental_snapshot_persistence) = + let (merkle_or_lattice_accounts_hash, bank_incremental_snapshot_persistence) = Self::calculate_and_verify_accounts_hash(&accounts_package, snapshot_config)?; - Self::save_epoch_accounts_hash(&accounts_package, accounts_hash_kind); + Self::save_epoch_accounts_hash(&accounts_package, &merkle_or_lattice_accounts_hash); Self::purge_old_accounts_hashes(&accounts_package, snapshot_config); @@ -226,7 +227,7 @@ impl AccountsHashVerifier { accounts_package, pending_snapshot_packages, snapshot_config, - accounts_hash_kind, + merkle_or_lattice_accounts_hash, bank_incremental_snapshot_persistence, ); @@ -237,7 +238,26 @@ impl AccountsHashVerifier { fn calculate_and_verify_accounts_hash( accounts_package: &AccountsPackage, snapshot_config: &SnapshotConfig, - ) -> IoResult<(AccountsHashKind, Option)> { + ) -> IoResult<( + MerkleOrLatticeAccountsHash, + Option, + )> { + match accounts_package.accounts_hash_algorithm { + AccountsHashAlgorithm::Merkle => { + debug!( + "calculate_and_verify_accounts_hash(): snapshots lt hash is disabled, \ + DO merkle-based accounts hash calculation", + ); + } + AccountsHashAlgorithm::Lattice => { + debug!( + "calculate_and_verify_accounts_hash(): snapshots lt hash is enabled, \ + SKIP merkle-based accounts hash calculation", + ); + return Ok((MerkleOrLatticeAccountsHash::Lattice, None)); + } + } + let accounts_hash_calculation_kind = match accounts_package.package_kind { AccountsPackageKind::AccountsHashVerifier => CalcAccountsHashKind::Full, AccountsPackageKind::EpochAccountsHash => CalcAccountsHashKind::Full, @@ -297,7 +317,10 @@ impl AccountsHashVerifier { } }; - Ok((accounts_hash_kind, bank_incremental_snapshot_persistence)) + Ok(( + MerkleOrLatticeAccountsHash::Merkle(accounts_hash_kind), + bank_incremental_snapshot_persistence, + )) } fn _calculate_full_accounts_hash( @@ -412,11 +435,13 @@ impl AccountsHashVerifier { fn save_epoch_accounts_hash( accounts_package: &AccountsPackage, - accounts_hash: AccountsHashKind, + merkle_or_lattice_accounts_hash: &MerkleOrLatticeAccountsHash, ) { if accounts_package.package_kind == AccountsPackageKind::EpochAccountsHash { - let AccountsHashKind::Full(accounts_hash) = accounts_hash else { - panic!("EAH requires a full accounts hash!"); + let MerkleOrLatticeAccountsHash::Merkle(AccountsHashKind::Full(accounts_hash)) = + merkle_or_lattice_accounts_hash + else { + panic!("EAH requires a full accounts hash, but was given {merkle_or_lattice_accounts_hash:?}"); }; info!( "saving epoch accounts hash, slot: {}, hash: {}", @@ -426,7 +451,7 @@ impl AccountsHashVerifier { .accounts .accounts_db .epoch_accounts_hash_manager - .set_valid(accounts_hash.into(), accounts_package.slot); + .set_valid((*accounts_hash).into(), accounts_package.slot); } } @@ -465,7 +490,7 @@ impl AccountsHashVerifier { accounts_package: AccountsPackage, pending_snapshot_packages: &Mutex, snapshot_config: &SnapshotConfig, - accounts_hash_kind: AccountsHashKind, + merkle_or_lattice_accounts_hash: MerkleOrLatticeAccountsHash, bank_incremental_snapshot_persistence: Option, ) { if !snapshot_config.should_generate_snapshots() @@ -479,7 +504,7 @@ impl AccountsHashVerifier { let snapshot_package = SnapshotPackage::new( accounts_package, - accounts_hash_kind, + merkle_or_lattice_accounts_hash, bank_incremental_snapshot_persistence, ); pending_snapshot_packages diff --git a/core/tests/snapshots.rs b/core/tests/snapshots.rs index eb9644f441654f..e02759959c06c0 100644 --- a/core/tests/snapshots.rs +++ b/core/tests/snapshots.rs @@ -628,17 +628,24 @@ enum VerifyAccountsKind { Merkle, Lattice, } +#[derive(Debug, Eq, PartialEq)] +enum VerifySnapshotHashKind { + Merkle, + Lattice, +} /// Spin up the background services fully then test taking & verifying snapshots #[test_matrix( V1_2_0, [Development, Devnet, Testnet, MainnetBeta], - [VerifyAccountsKind::Merkle, VerifyAccountsKind::Lattice] + [VerifyAccountsKind::Merkle, VerifyAccountsKind::Lattice], + [VerifySnapshotHashKind::Merkle, VerifySnapshotHashKind::Lattice] )] fn test_snapshots_with_background_services( snapshot_version: SnapshotVersion, cluster_type: ClusterType, verify_accounts_kind: VerifyAccountsKind, + verify_snapshot_hash_kind: VerifySnapshotHashKind, ) { solana_logger::setup(); @@ -825,6 +832,8 @@ fn test_snapshots_with_background_services( let (_tmp_dir, temporary_accounts_dir) = create_tmp_accounts_dir_for_tests(); let accounts_db_config = AccountsDbConfig { enable_experimental_accumulator_hash: verify_accounts_kind == VerifyAccountsKind::Lattice, + snapshots_use_experimental_accumulator_hash: verify_snapshot_hash_kind + == VerifySnapshotHashKind::Lattice, ..ACCOUNTS_DB_CONFIG_FOR_TESTING }; let (deserialized_bank, ..) = snapshot_bank_utils::bank_from_latest_snapshot_archives( diff --git a/ledger-tool/src/args.rs b/ledger-tool/src/args.rs index 1d0505968d822d..2169826719261f 100644 --- a/ledger-tool/src/args.rs +++ b/ledger-tool/src/args.rs @@ -134,6 +134,10 @@ pub fn accounts_db_args<'a, 'b>() -> Box<[Arg<'a, 'b>]> { .long("accounts-db-verify-experimental-accumulator-hash") .help("Verifies the experimental accumulator hash") .hidden(hidden_unless_forced()), + Arg::with_name("accounts_db_snapshots_use_experimental_accumulator_hash") + .long("accounts-db-snapshots-use-experimental-accumulator-hash") + .help("Snapshots use the experimental accumulator hash") + .hidden(hidden_unless_forced()), Arg::with_name("accounts_db_hash_threads") .long("accounts-db-hash-threads") .value_name("NUM_THREADS") @@ -387,6 +391,8 @@ pub fn get_accounts_db_config( .is_present("accounts_db_experimental_accumulator_hash"), verify_experimental_accumulator_hash: arg_matches .is_present("accounts_db_verify_experimental_accumulator_hash"), + snapshots_use_experimental_accumulator_hash: arg_matches + .is_present("accounts_db_snapshots_use_experimental_accumulator_hash"), num_hash_threads, ..AccountsDbConfig::default() } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6d393a2d666312..8bb1c1f4b72a6d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -77,7 +77,7 @@ use { }, accounts_hash::{ AccountHash, AccountsHash, AccountsLtHash, CalcAccountsHashConfig, HashStats, - IncrementalAccountsHash, + IncrementalAccountsHash, MerkleOrLatticeAccountsHash, }, accounts_index::{IndexKey, ScanConfig, ScanResult}, accounts_partition::{self, Partition, PartitionIndex}, @@ -5722,20 +5722,52 @@ impl Bank { /// /// # Panics /// - /// Panics if there is both-or-neither of an `AccountsHash` and an `IncrementalAccountsHash` - /// for this bank's slot. There may only be one or the other. + /// If the snapshots lt hash feature is not enabled, panics if there is both-or-neither of an + /// `AccountsHash` and an `IncrementalAccountsHash` for this bank's slot. There may only be + /// one or the other. pub fn get_snapshot_hash(&self) -> SnapshotHash { + if self.is_snapshots_lt_hash_enabled() { + self.get_lattice_snapshot_hash() + } else { + self.get_merkle_snapshot_hash() + } + } + + /// Returns the merkle-based `SnapshotHash` for this bank's slot + /// + /// This fn is used at startup to verify the bank was rebuilt correctly. + /// + /// # Panics + /// + /// If the snapshots lt hash feature is not enabled, panics if there is both-or-neither of an + /// `AccountsHash` and an `IncrementalAccountsHash` for this bank's slot. There may only be + /// one or the other. + pub fn get_merkle_snapshot_hash(&self) -> SnapshotHash { let accounts_hash = self.get_accounts_hash(); let incremental_accounts_hash = self.get_incremental_accounts_hash(); - - let accounts_hash = match (accounts_hash, incremental_accounts_hash) { + let accounts_hash_kind = match (accounts_hash, incremental_accounts_hash) { (Some(_), Some(_)) => panic!("Both full and incremental accounts hashes are present for slot {}; it is ambiguous which one to use for the snapshot hash!", self.slot()), (Some(accounts_hash), None) => accounts_hash.into(), (None, Some(incremental_accounts_hash)) => incremental_accounts_hash.into(), (None, None) => panic!("accounts hash is required to get snapshot hash"), }; let epoch_accounts_hash = self.get_epoch_accounts_hash_to_serialize(); - SnapshotHash::new(&accounts_hash, epoch_accounts_hash.as_ref()) + SnapshotHash::new( + &MerkleOrLatticeAccountsHash::Merkle(accounts_hash_kind), + epoch_accounts_hash.as_ref(), + None, + ) + } + + /// Returns the lattice-based `SnapshotHash` for this bank's slot + /// + /// This fn is used at startup to verify the bank was rebuilt correctly. + pub fn get_lattice_snapshot_hash(&self) -> SnapshotHash { + SnapshotHash::new( + &MerkleOrLatticeAccountsHash::Lattice, + self.get_epoch_accounts_hash_to_serialize().as_ref(), + Some(self.accounts_lt_hash.lock().unwrap().0.checksum()), + ) } pub fn load_account_into_read_cache(&self, key: &Pubkey) { diff --git a/runtime/src/bank/accounts_lt_hash.rs b/runtime/src/bank/accounts_lt_hash.rs index 4e1f3741458b0e..5d57ef86e46aec 100644 --- a/runtime/src/bank/accounts_lt_hash.rs +++ b/runtime/src/bank/accounts_lt_hash.rs @@ -29,6 +29,19 @@ impl Bank { .is_active(&feature_set::accounts_lt_hash::id()) } + /// Returns if snapshots use the accounts lt hash + pub fn is_snapshots_lt_hash_enabled(&self) -> bool { + self.is_accounts_lt_hash_enabled() + && (self + .rc + .accounts + .accounts_db + .snapshots_use_experimental_accumulator_hash() + || self + .feature_set + .is_active(&feature_set::snapshots_lt_hash::id())) + } + /// Updates the accounts lt hash /// /// When freezing a bank, we compute and update the accounts lt hash. @@ -1175,4 +1188,93 @@ mod tests { roundtrip_bank.wait_for_initial_accounts_hash_verification_completed_for_tests(); assert_eq!(roundtrip_bank, *bank); } + + /// Ensure that the snapshot hash is correct when snapshots_lt_hash is enabled + #[test_matrix( + [Features::None, Features::All], + [Cli::Off, Cli::On], + [Cli::Off, Cli::On] + )] + fn test_snapshots_lt_hash(features: Features, cli: Cli, verify_cli: Cli) { + let (genesis_config, mint_keypair) = genesis_config_with(features); + let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + bank.rc + .accounts + .accounts_db + .set_is_experimental_accumulator_hash_enabled(features == Features::None); + // ensure the accounts lt hash is enabled, otherwise the snapshot lt hash is disabled + assert!(bank.is_accounts_lt_hash_enabled()); + + bank.rc + .accounts + .accounts_db + .set_snapshots_use_experimental_accumulator_hash(match cli { + Cli::Off => false, + Cli::On => true, + }); + + let amount = cmp::max( + bank.get_minimum_balance_for_rent_exemption(0), + LAMPORTS_PER_SOL, + ); + + // create some banks with some modified accounts so that there are stored accounts + // (note: the number of banks is arbitrary) + for _ in 0..3 { + let slot = bank.slot() + 1; + bank = + new_bank_from_parent_with_bank_forks(&bank_forks, bank, &Pubkey::default(), slot); + bank.register_unique_recent_blockhash_for_test(); + bank.transfer(amount, &mint_keypair, &pubkey::new_rand()) + .unwrap(); + bank.fill_bank_with_ticks_for_tests(); + bank.squash(); + bank.force_flush_accounts_cache(); + } + + let snapshot_config = SnapshotConfig::default(); + let bank_snapshots_dir = TempDir::new().unwrap(); + let snapshot_archives_dir = TempDir::new().unwrap(); + let snapshot = snapshot_bank_utils::bank_to_full_snapshot_archive( + &bank_snapshots_dir, + &bank, + Some(snapshot_config.snapshot_version), + &snapshot_archives_dir, + &snapshot_archives_dir, + snapshot_config.archive_format, + ) + .unwrap(); + let (_accounts_tempdir, accounts_dir) = snapshot_utils::create_tmp_accounts_dir_for_tests(); + let accounts_db_config = AccountsDbConfig { + enable_experimental_accumulator_hash: features == Features::None, + snapshots_use_experimental_accumulator_hash: match verify_cli { + Cli::Off => false, + Cli::On => true, + }, + ..ACCOUNTS_DB_CONFIG_FOR_TESTING + }; + let (roundtrip_bank, _) = snapshot_bank_utils::bank_from_snapshot_archives( + &[accounts_dir], + &bank_snapshots_dir, + &snapshot, + None, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + None, + false, + false, + false, + false, + Some(accounts_db_config), + None, + Arc::default(), + ) + .unwrap(); + + // Wait for the startup verification to complete. If we don't panic, then we're good! + roundtrip_bank.wait_for_initial_accounts_hash_verification_completed_for_tests(); + assert_eq!(roundtrip_bank, *bank); + } } diff --git a/runtime/src/snapshot_bank_utils.rs b/runtime/src/snapshot_bank_utils.rs index e9fbb0adb5c078..346559d3f67e56 100644 --- a/runtime/src/snapshot_bank_utils.rs +++ b/runtime/src/snapshot_bank_utils.rs @@ -31,6 +31,7 @@ use { CalcAccountsHashDataSource, DuplicatesLtHash, }, accounts_file::StorageAccess, + accounts_hash::MerkleOrLatticeAccountsHash, accounts_update_notifier_interface::AccountsUpdateNotifier, utils::delete_contents_of_path, }, @@ -38,6 +39,7 @@ use { solana_measure::{measure::Measure, measure_time}, solana_sdk::{ clock::{Epoch, Slot}, + feature_set, genesis_config::GenesisConfig, pubkey::Pubkey, slot_history::{Check, SlotHistory}, @@ -212,17 +214,21 @@ pub fn bank_from_snapshot_archives( snapshot_archive_info.hash, )?; - let base = incremental_snapshot_archive_info.is_some().then(|| { - let base_slot = full_snapshot_archive_info.slot(); - let base_capitalization = bank - .rc - .accounts - .accounts_db - .get_accounts_hash(base_slot) - .expect("accounts hash must exist at full snapshot's slot") - .1; - (base_slot, base_capitalization) - }); + let base = if bank.is_snapshots_lt_hash_enabled() { + None + } else { + incremental_snapshot_archive_info.is_some().then(|| { + let base_slot = full_snapshot_archive_info.slot(); + let base_capitalization = bank + .rc + .accounts + .accounts_db + .get_accounts_hash(base_slot) + .expect("accounts hash must exist at full snapshot's slot") + .1; + (base_slot, base_capitalization) + }) + }; let mut measure_verify = Measure::start("verify"); if !bank.verify_snapshot_bank( @@ -447,24 +453,51 @@ pub fn bank_from_latest_snapshot_dir( Ok(bank) } -/// Check to make sure the deserialized bank's slot and hash matches the snapshot archive's slot -/// and hash +/// Verifies the snapshot's slot and hash matches the bank's fn verify_bank_against_expected_slot_hash( bank: &Bank, - expected_slot: Slot, - expected_hash: SnapshotHash, + snapshot_slot: Slot, + snapshot_hash: SnapshotHash, ) -> snapshot_utils::Result<()> { let bank_slot = bank.slot(); + if bank_slot != snapshot_slot { + return Err(SnapshotError::MismatchedSlot(bank_slot, snapshot_slot)); + } + let bank_hash = bank.get_snapshot_hash(); + if bank_hash == snapshot_hash { + return Ok(()); + } - if bank_slot != expected_slot || bank_hash != expected_hash { - return Err(SnapshotError::MismatchedSlotHash( - (bank_slot, bank_hash), - (expected_slot, expected_hash), - )); + // If the slots match but the hashes don't, there may be a mismatch between snapshot + // generation and snapshot load w.r.t. the snapshots_lt_hash cli args. + + if bank + .feature_set + .is_active(&feature_set::snapshots_lt_hash::id()) + { + // ...but, if the snapshots_lt_hash *feature* is active, then mismatches are errors + return Err(SnapshotError::MismatchedHash(bank_hash, snapshot_hash)); } - Ok(()) + // once here, we know the snapshots_lt_hash *feature* is *disabled*, so check the other kind of + // snapshot hash in case we match that one + let other_bank_hash = if bank.is_snapshots_lt_hash_enabled() { + // If our snapshots_lt_hash cli arg is ON, maybe the node that generated this snapshot had + // the cli arg OFF? Try getting the merkle-based snapshot hash to compare. + bank.get_merkle_snapshot_hash() + } else { + // If our snapshots_lt_hash cli arg is OFF, maybe the node that generated this snapshot had + // the cli arg ON? Try getting the lattice-based snapshot hash to compare. + bank.get_lattice_snapshot_hash() + }; + + if other_bank_hash == snapshot_hash { + Ok(()) + } else { + // yes, use the *original* bank_hash here, not other_bank_hash + Err(SnapshotError::MismatchedHash(bank_hash, snapshot_hash)) + } } fn bank_fields_from_snapshots( @@ -909,8 +942,18 @@ fn bank_to_full_snapshot_archive_with( bank.rehash(); // Bank may have been manually modified by the caller bank.force_flush_accounts_cache(); bank.clean_accounts(); - let calculated_accounts_hash = - bank.update_accounts_hash(CalcAccountsHashDataSource::Storages, false, false); + + let merkle_or_lattice_accounts_hash = if bank.is_snapshots_lt_hash_enabled() { + MerkleOrLatticeAccountsHash::Lattice + } else { + let calculated_accounts_hash = + bank.update_accounts_hash(CalcAccountsHashDataSource::Storages, false, false); + let accounts_hash = bank + .get_accounts_hash() + .expect("accounts hash is required for snapshot"); + assert_eq!(accounts_hash, calculated_accounts_hash); + MerkleOrLatticeAccountsHash::Merkle(accounts_hash.into()) + }; let snapshot_storages = bank.get_snapshot_storages(None); let status_cache_slot_deltas = bank.status_cache.read().unwrap().root_slot_deltas(); @@ -921,12 +964,8 @@ fn bank_to_full_snapshot_archive_with( status_cache_slot_deltas, None, ); - - let accounts_hash = bank - .get_accounts_hash() - .expect("accounts hash is required for snapshot"); - assert_eq!(accounts_hash, calculated_accounts_hash); - let snapshot_package = SnapshotPackage::new(accounts_package, accounts_hash.into(), None); + let snapshot_package = + SnapshotPackage::new(accounts_package, merkle_or_lattice_accounts_hash, None); let snapshot_config = SnapshotConfig { full_snapshot_archives_dir: full_snapshot_archives_dir.as_ref().to_path_buf(), @@ -972,8 +1011,41 @@ pub fn bank_to_incremental_snapshot_archive( bank.rehash(); // Bank may have been manually modified by the caller bank.force_flush_accounts_cache(); bank.clean_accounts(); - let calculated_incremental_accounts_hash = - bank.update_incremental_accounts_hash(full_snapshot_slot); + + let (merkle_or_lattice_accounts_hash, bank_incremental_snapshot_persistence) = + if bank.is_snapshots_lt_hash_enabled() { + (MerkleOrLatticeAccountsHash::Lattice, None) + } else { + let calculated_incremental_accounts_hash = + bank.update_incremental_accounts_hash(full_snapshot_slot); + let (full_accounts_hash, full_capitalization) = bank + .rc + .accounts + .accounts_db + .get_accounts_hash(full_snapshot_slot) + .expect("base accounts hash is required for incremental snapshot"); + let (incremental_accounts_hash, incremental_capitalization) = bank + .rc + .accounts + .accounts_db + .get_incremental_accounts_hash(bank.slot()) + .expect("incremental accounts hash is required for incremental snapshot"); + assert_eq!( + incremental_accounts_hash, + calculated_incremental_accounts_hash, + ); + let bank_incremental_snapshot_persistence = BankIncrementalSnapshotPersistence { + full_slot: full_snapshot_slot, + full_hash: full_accounts_hash.into(), + full_capitalization, + incremental_hash: incremental_accounts_hash.into(), + incremental_capitalization, + }; + ( + MerkleOrLatticeAccountsHash::Merkle(incremental_accounts_hash.into()), + Some(bank_incremental_snapshot_persistence), + ) + }; let snapshot_storages = bank.get_snapshot_storages(Some(full_snapshot_slot)); let status_cache_slot_deltas = bank.status_cache.read().unwrap().root_slot_deltas(); @@ -984,34 +1056,10 @@ pub fn bank_to_incremental_snapshot_archive( status_cache_slot_deltas, None, ); - - let (full_accounts_hash, full_capitalization) = bank - .rc - .accounts - .accounts_db - .get_accounts_hash(full_snapshot_slot) - .expect("base accounts hash is required for incremental snapshot"); - let (incremental_accounts_hash, incremental_capitalization) = bank - .rc - .accounts - .accounts_db - .get_incremental_accounts_hash(bank.slot()) - .expect("incremental accounts hash is required for incremental snapshot"); - assert_eq!( - incremental_accounts_hash, - calculated_incremental_accounts_hash, - ); - let bank_incremental_snapshot_persistence = BankIncrementalSnapshotPersistence { - full_slot: full_snapshot_slot, - full_hash: full_accounts_hash.into(), - full_capitalization, - incremental_hash: incremental_accounts_hash.into(), - incremental_capitalization, - }; let snapshot_package = SnapshotPackage::new( accounts_package, - incremental_accounts_hash.into(), - Some(bank_incremental_snapshot_persistence), + merkle_or_lattice_accounts_hash, + bank_incremental_snapshot_persistence, ); // Note: Since the snapshot_storages above are *only* the incremental storages, @@ -1064,6 +1112,7 @@ mod tests { sorted_storages::SortedStorages, }, solana_sdk::{ + feature_set, genesis_config::create_genesis_config, native_token::{sol_to_lamports, LAMPORTS_PER_SOL}, signature::{Keypair, Signer}, @@ -1963,12 +2012,27 @@ mod tests { let full_snapshot_archives_dir = tempfile::TempDir::new().unwrap(); let incremental_snapshot_archives_dir = tempfile::TempDir::new().unwrap(); - let genesis_config_info = genesis_utils::create_genesis_config_with_leader( + let mut genesis_config_info = genesis_utils::create_genesis_config_with_leader( 1_000_000 * LAMPORTS_PER_SOL, &Pubkey::new_unique(), 100 * LAMPORTS_PER_SOL, ); let mint = &genesis_config_info.mint_keypair; + // When the snapshots lt hash feature is enabled, the IAH is effectively *disabled*, + // which causes this test to fail. + // Disable the snapshots lt hash feature by removing its account from genesis. + genesis_config_info + .genesis_config + .accounts + .remove(&feature_set::snapshots_lt_hash::id()) + .unwrap(); + // Additionally, remove the accounts lt hash feature, since it would cause startup + // verification to use the accounts lt hash and not the IAH. + genesis_config_info + .genesis_config + .accounts + .remove(&feature_set::accounts_lt_hash::id()) + .unwrap(); let do_transfers = |bank: &Bank| { let key1 = Keypair::new(); // lamports from mint diff --git a/runtime/src/snapshot_hash.rs b/runtime/src/snapshot_hash.rs index 0f76895757d2fe..e6d90657be1fb4 100644 --- a/runtime/src/snapshot_hash.rs +++ b/runtime/src/snapshot_hash.rs @@ -1,6 +1,9 @@ //! Helper types and functions for handling and dealing with snapshot hashes. use { - solana_accounts_db::{accounts_hash::AccountsHashKind, epoch_accounts_hash::EpochAccountsHash}, + solana_accounts_db::{ + accounts_hash::MerkleOrLatticeAccountsHash, epoch_accounts_hash::EpochAccountsHash, + }, + solana_lattice_hash::lt_hash::Checksum as AccountsLtHashChecksum, solana_sdk::{ clock::Slot, hash::{Hash, Hasher}, @@ -32,22 +35,32 @@ pub struct IncrementalSnapshotHash(pub (Slot, SnapshotHash)); pub struct SnapshotHash(pub Hash); impl SnapshotHash { - /// Make a snapshot hash from an accounts hash and epoch accounts hash + /// Make a snapshot hash from accounts hashes #[must_use] pub fn new( - accounts_hash: &AccountsHashKind, + merkle_or_lattice_accounts_hash: &MerkleOrLatticeAccountsHash, epoch_accounts_hash: Option<&EpochAccountsHash>, + accounts_lt_hash_checksum: Option, ) -> Self { + let accounts_hash = match merkle_or_lattice_accounts_hash { + MerkleOrLatticeAccountsHash::Merkle(accounts_hash_kind) => { + *accounts_hash_kind.as_hash() + } + MerkleOrLatticeAccountsHash::Lattice => Hash::new_from_array( + accounts_lt_hash_checksum + .expect("lattice kind must have lt hash checksum") + .0, + ), + }; let snapshot_hash = match epoch_accounts_hash { - None => *accounts_hash.as_hash(), + None => accounts_hash, Some(epoch_accounts_hash) => { let mut hasher = Hasher::default(); - hasher.hash(accounts_hash.as_hash().as_ref()); + hasher.hash(accounts_hash.as_ref()); hasher.hash(epoch_accounts_hash.as_ref().as_ref()); hasher.result() } }; - Self(snapshot_hash) } } diff --git a/runtime/src/snapshot_package.rs b/runtime/src/snapshot_package.rs index 943c5e9be8c69c..27779f6fb995ed 100644 --- a/runtime/src/snapshot_package.rs +++ b/runtime/src/snapshot_package.rs @@ -9,7 +9,9 @@ use { account_storage::meta::StoredMetaWriteVersion, accounts::Accounts, accounts_db::AccountStorageEntry, - accounts_hash::{AccountsDeltaHash, AccountsHash, AccountsHashKind}, + accounts_hash::{ + AccountsDeltaHash, AccountsHash, AccountsHashKind, MerkleOrLatticeAccountsHash, + }, epoch_accounts_hash::EpochAccountsHash, }, solana_sdk::{ @@ -36,6 +38,7 @@ pub struct AccountsPackage { pub accounts: Arc, pub epoch_schedule: EpochSchedule, pub rent_collector: RentCollector, + pub accounts_hash_algorithm: AccountsHashAlgorithm, /// Supplemental information needed for snapshots pub snapshot_info: Option, @@ -90,11 +93,17 @@ impl AccountsPackage { } }; + let accounts_hash_algorithm = if bank.is_snapshots_lt_hash_enabled() { + AccountsHashAlgorithm::Lattice + } else { + AccountsHashAlgorithm::Merkle + }; Self::_new( package_kind, bank, snapshot_storages, accounts_hash_for_testing, + accounts_hash_algorithm, Some(snapshot_info), ) } @@ -113,6 +122,7 @@ impl AccountsPackage { bank, snapshot_storages, accounts_hash_for_testing, + AccountsHashAlgorithm::Merkle, None, ) } @@ -131,6 +141,7 @@ impl AccountsPackage { bank, snapshot_storages, accounts_hash_for_testing, + AccountsHashAlgorithm::Merkle, None, ) } @@ -140,6 +151,7 @@ impl AccountsPackage { bank: &Bank, snapshot_storages: Vec>, accounts_hash_for_testing: Option, + accounts_hash_algorithm: AccountsHashAlgorithm, snapshot_info: Option, ) -> Self { Self { @@ -152,6 +164,7 @@ impl AccountsPackage { accounts: bank.accounts(), epoch_schedule: bank.epoch_schedule().clone(), rent_collector: bank.rent_collector().clone(), + accounts_hash_algorithm, snapshot_info, enqueued: Instant::now(), } @@ -174,6 +187,7 @@ impl AccountsPackage { accounts: Arc::new(accounts), epoch_schedule: EpochSchedule::default(), rent_collector: RentCollector::default(), + accounts_hash_algorithm: AccountsHashAlgorithm::Merkle, snapshot_info: Some(SupplementalSnapshotInfo { status_cache_slot_deltas: Vec::default(), bank_fields_to_serialize: BankFieldsToSerialize::default_for_tests(), @@ -193,6 +207,7 @@ impl std::fmt::Debug for AccountsPackage { .field("kind", &self.package_kind) .field("slot", &self.slot) .field("block_height", &self.block_height) + .field("accounts_hash_algorithm", &self.accounts_hash_algorithm) .finish_non_exhaustive() } } @@ -241,7 +256,7 @@ pub struct SnapshotPackage { impl SnapshotPackage { pub fn new( accounts_package: AccountsPackage, - accounts_hash_kind: AccountsHashKind, + merkle_or_lattice_accounts_hash: MerkleOrLatticeAccountsHash, bank_incremental_snapshot_persistence: Option, ) -> Self { let AccountsPackageKind::Snapshot(kind) = accounts_package.package_kind else { @@ -255,15 +270,24 @@ impl SnapshotPackage { ); }; - let accounts_hash = match accounts_hash_kind { - AccountsHashKind::Full(accounts_hash) => accounts_hash, - AccountsHashKind::Incremental(_) => { - // The accounts hash is only needed when serializing a full snapshot. - // When serializing an incremental snapshot, there will not be a full accounts hash - // at `slot`. In that case, use the default, because it doesn't actually get used. - // The incremental snapshot will use the BankIncrementalSnapshotPersistence - // field, so ensure it is Some. - assert!(bank_incremental_snapshot_persistence.is_some()); + let accounts_hash = match merkle_or_lattice_accounts_hash { + MerkleOrLatticeAccountsHash::Merkle(accounts_hash_kind) => { + match accounts_hash_kind { + AccountsHashKind::Full(accounts_hash) => accounts_hash, + AccountsHashKind::Incremental(_) => { + // The accounts hash is only needed when serializing a full snapshot. + // When serializing an incremental snapshot, there will not be a full accounts hash + // at `slot`. In that case, use the default, because it doesn't actually get used. + // The incremental snapshot will use the BankIncrementalSnapshotPersistence + // field, so ensure it is Some. + assert!(bank_incremental_snapshot_persistence.is_some()); + AccountsHash(Hash::default()) + } + } + } + MerkleOrLatticeAccountsHash::Lattice => { + // This is the merkle-based accounts hash, which isn't used in the Lattice case, + // so any value is fine here. AccountsHash(Hash::default()) } }; @@ -273,8 +297,13 @@ impl SnapshotPackage { slot: accounts_package.slot, block_height: accounts_package.block_height, hash: SnapshotHash::new( - &accounts_hash_kind, + &merkle_or_lattice_accounts_hash, snapshot_info.epoch_accounts_hash.as_ref(), + snapshot_info + .bank_fields_to_serialize + .accounts_lt_hash + .as_ref() + .map(|accounts_lt_hash| accounts_lt_hash.0.checksum()), ), snapshot_storages: accounts_package.snapshot_storages, status_cache_slot_deltas: snapshot_info.status_cache_slot_deltas, @@ -340,3 +369,12 @@ impl SnapshotKind { matches!(self, SnapshotKind::IncrementalSnapshot(_)) } } + +/// Which algorithm should be used to calculate the accounts hash? +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AccountsHashAlgorithm { + /// Merkle-based accounts hash algorithm + Merkle, + /// Lattice-based accounts hash algorithm + Lattice, +} diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index bc59d3fc5f5ce8..95cf08ba7d3dfe 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -339,8 +339,11 @@ pub enum SnapshotError { #[error("no snapshot archives to load from '{0}'")] NoSnapshotArchives(PathBuf), - #[error("snapshot has mismatch: deserialized bank: {0:?}, snapshot archive info: {1:?}")] - MismatchedSlotHash((Slot, SnapshotHash), (Slot, SnapshotHash)), + #[error("snapshot slot mismatch: deserialized bank: {0}, snapshot archive: {1}")] + MismatchedSlot(Slot, Slot), + + #[error("snapshot hash mismatch: deserialized bank: {0:?}, snapshot archive: {1:?}")] + MismatchedHash(SnapshotHash, SnapshotHash), #[error("snapshot slot deltas are invalid: {0}")] VerifySlotDeltas(#[from] VerifySlotDeltasError), diff --git a/sdk/feature-set/src/lib.rs b/sdk/feature-set/src/lib.rs index 5239edcaeb95f7..e992f406ae43ff 100644 --- a/sdk/feature-set/src/lib.rs +++ b/sdk/feature-set/src/lib.rs @@ -900,6 +900,10 @@ pub mod accounts_lt_hash { solana_pubkey::declare_id!("LtHaSHHsUge7EWTPVrmpuexKz6uVHZXZL6cgJa7W7Zn"); } +pub mod snapshots_lt_hash { + solana_pubkey::declare_id!("LTsNAP8h1voEVVToMNBNqoiNQex4aqfUrbFhRH3mSQ2"); +} + pub mod migrate_stake_program_to_core_bpf { solana_pubkey::declare_id!("6M4oQ6eXneVhtLoiAr4yRYQY43eVLjrKbiDZDJc892yk"); } @@ -1135,6 +1139,7 @@ lazy_static! { (lift_cpi_caller_restriction::id(), "Lift the restriction in CPI that the caller must have the callee as an instruction account #2202"), (disable_account_loader_special_case::id(), "Disable account loader special case #3513"), (accounts_lt_hash::id(), "enables lattice-based accounts hash #3333"), + (snapshots_lt_hash::id(), "snapshots use lattice-based accounts hash #3598"), (enable_secp256r1_precompile::id(), "Enable secp256r1 precompile SIMD-0075"), (migrate_stake_program_to_core_bpf::id(), "Migrate Stake program to Core BPF SIMD-0196 #3655"), (deplete_cu_meter_on_vm_failure::id(), "Deplete compute meter for vm errors SIMD-0182 #3993"), diff --git a/validator/src/cli.rs b/validator/src/cli.rs index 96bdf2dd6598fd..fa8d439b095e3b 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -1455,6 +1455,12 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> { .help("Verifies the experimental accumulator hash") .hidden(hidden_unless_forced()), ) + .arg( + Arg::with_name("accounts_db_snapshots_use_experimental_accumulator_hash") + .long("accounts-db-snapshots-use-experimental-accumulator-hash") + .help("Snapshots use the experimental accumulator hash") + .hidden(hidden_unless_forced()), + ) .arg( Arg::with_name("accounts_index_scan_results_limit_mb") .long("accounts-index-scan-results-limit-mb") diff --git a/validator/src/main.rs b/validator/src/main.rs index 4a0be757008720..4af305a39da5c8 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1364,6 +1364,8 @@ pub fn main() { .is_present("accounts_db_experimental_accumulator_hash"), verify_experimental_accumulator_hash: matches .is_present("accounts_db_verify_experimental_accumulator_hash"), + snapshots_use_experimental_accumulator_hash: matches + .is_present("accounts_db_snapshots_use_experimental_accumulator_hash"), num_clean_threads: Some(accounts_db_clean_threads), num_foreground_threads: Some(accounts_db_foreground_threads), num_hash_threads: Some(accounts_db_hash_threads),