diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6529b3aea..e354ae575 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -209,6 +209,7 @@ struct VerifyAccountsHashConfig { mod address_lookup_table; mod builtin_programs; mod metrics; +mod serde_snapshot; mod sysvar_cache; #[cfg(test)] mod tests; diff --git a/runtime/src/bank/serde_snapshot.rs b/runtime/src/bank/serde_snapshot.rs new file mode 100644 index 000000000..78b877a85 --- /dev/null +++ b/runtime/src/bank/serde_snapshot.rs @@ -0,0 +1,644 @@ +#[cfg(test)] +mod tests { + use { + crate::{ + account_storage::{AccountStorageMap, AccountStorageReference}, + accounts_db::{ + get_temp_accounts_paths, AccountShrinkThreshold, AccountStorageEntry, AccountsDb, + AtomicAppendVecId, + }, + accounts_file::{AccountsFile, AccountsFileError}, + accounts_hash::{AccountsDeltaHash, AccountsHash}, + accounts_index::AccountSecondaryIndexes, + bank::{Bank, BankTestConfig, EpochRewardStatus, StartBlockHeightAndRewards}, + epoch_accounts_hash::{self, EpochAccountsHash}, + genesis_utils::{activate_all_features, activate_feature}, + runtime_config::RuntimeConfig, + serde_snapshot::{ + reserialize_bank_with_new_accounts_hash, BankIncrementalSnapshotPersistence, + SerdeAccountsHash, SerdeIncrementalAccountsHash, SerdeStyle, SnapshotStreams, + }, + snapshot_utils::{ + self, create_tmp_accounts_dir_for_tests, get_storages_to_serialize, ArchiveFormat, + StorageAndNextAppendVecId, BANK_SNAPSHOT_PRE_FILENAME_EXTENSION, + }, + stake_rewards::StakeReward, + status_cache::StatusCache, + }, + solana_sdk::{ + epoch_schedule::EpochSchedule, + feature_set, + genesis_config::create_genesis_config, + hash::Hash, + pubkey::Pubkey, + signature::{Keypair, Signer}, + }, + std::{ + io::{Cursor, Read, Write}, + num::NonZeroUsize, + ops::RangeFull, + path::Path, + sync::{Arc, RwLock}, + }, + tempfile::TempDir, + }; + + /// Simulates the unpacking & storage reconstruction done during snapshot unpacking + fn copy_append_vecs>( + accounts_db: &AccountsDb, + output_dir: P, + ) -> Result { + let storage_entries = accounts_db.get_snapshot_storages(RangeFull).0; + let storage: AccountStorageMap = AccountStorageMap::with_capacity(storage_entries.len()); + let mut next_append_vec_id = 0; + for storage_entry in storage_entries.into_iter() { + // Copy file to new directory + let storage_path = storage_entry.get_path(); + let file_name = + AccountsFile::file_name(storage_entry.slot(), storage_entry.append_vec_id()); + let output_path = output_dir.as_ref().join(file_name); + std::fs::copy(storage_path, &output_path)?; + + // Read new file into append-vec and build new entry + let (accounts_file, num_accounts) = + AccountsFile::new_from_file(output_path, storage_entry.accounts.len())?; + let new_storage_entry = AccountStorageEntry::new_existing( + storage_entry.slot(), + storage_entry.append_vec_id(), + accounts_file, + num_accounts, + ); + next_append_vec_id = next_append_vec_id.max(new_storage_entry.append_vec_id()); + storage.insert( + new_storage_entry.slot(), + AccountStorageReference { + id: new_storage_entry.append_vec_id(), + storage: Arc::new(new_storage_entry), + }, + ); + } + + Ok(StorageAndNextAppendVecId { + storage, + next_append_vec_id: AtomicAppendVecId::new(next_append_vec_id + 1), + }) + } + + fn test_bank_serialize_style( + serde_style: SerdeStyle, + reserialize_accounts_hash: bool, + update_accounts_hash: bool, + incremental_snapshot_persistence: bool, + initial_epoch_accounts_hash: bool, + ) { + solana_logger::setup(); + let (mut genesis_config, _) = create_genesis_config(500); + activate_feature(&mut genesis_config, feature_set::epoch_accounts_hash::id()); + genesis_config.epoch_schedule = EpochSchedule::custom(400, 400, false); + let bank0 = Arc::new(Bank::new_for_tests(&genesis_config)); + let eah_start_slot = epoch_accounts_hash::calculation_start(&bank0); + let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + bank0.squash(); + + // Create an account on a non-root fork + let key1 = Keypair::new(); + bank1.deposit(&key1.pubkey(), 5).unwrap(); + + // If setting an initial EAH, then the bank being snapshotted must be in the EAH calculation + // window. Otherwise `bank_to_stream()` below will *not* include the EAH in the bank snapshot, + // and the later-deserialized bank's EAH will not match the expected EAH. + let bank2_slot = if initial_epoch_accounts_hash { + eah_start_slot + } else { + 0 + } + 2; + let bank2 = Bank::new_from_parent(&bank0, &Pubkey::default(), bank2_slot); + + // Test new account + let key2 = Keypair::new(); + bank2.deposit(&key2.pubkey(), 10).unwrap(); + assert_eq!(bank2.get_balance(&key2.pubkey()), 10); + + let key3 = Keypair::new(); + bank2.deposit(&key3.pubkey(), 0).unwrap(); + + bank2.freeze(); + bank2.squash(); + bank2.force_flush_accounts_cache(); + bank2 + .accounts() + .accounts_db + .set_accounts_hash_for_tests(bank2.slot(), AccountsHash(Hash::new(&[0; 32]))); + + let snapshot_storages = bank2.get_snapshot_storages(None); + let mut buf = vec![]; + let mut writer = Cursor::new(&mut buf); + + let mut expected_epoch_accounts_hash = None; + + if initial_epoch_accounts_hash { + expected_epoch_accounts_hash = Some(Hash::new(&[7; 32])); + bank2 + .rc + .accounts + .accounts_db + .epoch_accounts_hash_manager + .set_valid( + EpochAccountsHash::new(expected_epoch_accounts_hash.unwrap()), + eah_start_slot, + ); + } + + crate::serde_snapshot::bank_to_stream( + serde_style, + &mut std::io::BufWriter::new(&mut writer), + &bank2, + &get_storages_to_serialize(&snapshot_storages), + ) + .unwrap(); + + if update_accounts_hash { + bank2 + .accounts() + .accounts_db + .set_accounts_hash_for_tests(bank2.slot(), AccountsHash(Hash::new(&[1; 32]))); + } + let accounts_hash = bank2.get_accounts_hash().unwrap(); + + let slot = bank2.slot(); + let incremental = + incremental_snapshot_persistence.then(|| BankIncrementalSnapshotPersistence { + full_slot: slot - 1, + full_hash: SerdeAccountsHash(Hash::new(&[1; 32])), + full_capitalization: 31, + incremental_hash: SerdeIncrementalAccountsHash(Hash::new(&[2; 32])), + incremental_capitalization: 32, + }); + + if reserialize_accounts_hash || incremental_snapshot_persistence { + let temp_dir = TempDir::new().unwrap(); + let slot_dir = snapshot_utils::get_bank_snapshot_dir(&temp_dir, slot); + let post_path = slot_dir.join(slot.to_string()); + let pre_path = post_path.with_extension(BANK_SNAPSHOT_PRE_FILENAME_EXTENSION); + std::fs::create_dir(&slot_dir).unwrap(); + { + let mut f = std::fs::File::create(pre_path).unwrap(); + f.write_all(&buf).unwrap(); + } + + assert!(reserialize_bank_with_new_accounts_hash( + slot_dir, + slot, + &accounts_hash, + incremental.as_ref(), + )); + let mut buf_reserialized; + { + let previous_len = buf.len(); + let expected = previous_len + + if incremental_snapshot_persistence { + // previously saved a none (size = sizeof_None), now added a Some + let sizeof_none = std::mem::size_of::(); + let sizeof_incremental_snapshot_persistence = + std::mem::size_of::>(); + sizeof_incremental_snapshot_persistence - sizeof_none + } else { + // no change + 0 + }; + + // +1: larger buffer than expected to make sure the file isn't larger than expected + buf_reserialized = vec![0; expected + 1]; + let mut f = std::fs::File::open(post_path).unwrap(); + let size = f.read(&mut buf_reserialized).unwrap(); + + assert_eq!( + size, + expected, + "(reserialize_accounts_hash, incremental_snapshot_persistence, update_accounts_hash, initial_epoch_accounts_hash): {:?}, previous_len: {previous_len}", + ( + reserialize_accounts_hash, + incremental_snapshot_persistence, + update_accounts_hash, + initial_epoch_accounts_hash, + ) + ); + buf_reserialized.truncate(size); + } + if update_accounts_hash { + // We cannot guarantee buffer contents are exactly the same if hash is the same. + // Things like hashsets/maps have randomness in their in-mem representations. + // This makes serialized bytes not deterministic. + // But, we can guarantee that the buffer is different if we change the hash! + assert_ne!(buf, buf_reserialized); + } + if update_accounts_hash || incremental_snapshot_persistence { + buf = buf_reserialized; + } + } + + let rdr = Cursor::new(&buf[..]); + let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); + + // Create a new set of directories for this bank's accounts + let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); + let mut status_cache = StatusCache::default(); + status_cache.add_root(2); + // Create a directory to simulate AppendVecs unpackaged from a snapshot tar + let copied_accounts = TempDir::new().unwrap(); + let storage_and_next_append_vec_id = + copy_append_vecs(&bank2.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); + let mut snapshot_streams = SnapshotStreams { + full_snapshot_stream: &mut reader, + incremental_snapshot_stream: None, + }; + let mut dbank = crate::serde_snapshot::bank_from_streams( + serde_style, + &mut snapshot_streams, + &dbank_paths, + storage_and_next_append_vec_id, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + AccountSecondaryIndexes::default(), + None, + AccountShrinkThreshold::default(), + false, + Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), + None, + Arc::default(), + ) + .unwrap(); + dbank.status_cache = Arc::new(RwLock::new(status_cache)); + assert_eq!(dbank.get_balance(&key1.pubkey()), 0); + assert_eq!(dbank.get_balance(&key2.pubkey()), 10); + assert_eq!(dbank.get_balance(&key3.pubkey()), 0); + if let Some(incremental_snapshot_persistence) = incremental.clone() { + assert_eq!(dbank.get_accounts_hash(), None,); + assert_eq!( + dbank.get_incremental_accounts_hash(), + Some(incremental_snapshot_persistence.incremental_hash.into()), + ); + } else { + assert_eq!(dbank.get_accounts_hash(), Some(accounts_hash)); + assert_eq!(dbank.get_incremental_accounts_hash(), None); + } + assert!(bank2 == dbank); + assert_eq!(dbank.incremental_snapshot_persistence, incremental); + assert_eq!(dbank.get_epoch_accounts_hash_to_serialize().map(|epoch_accounts_hash| *epoch_accounts_hash.as_ref()), expected_epoch_accounts_hash, + "(reserialize_accounts_hash, incremental_snapshot_persistence, update_accounts_hash, initial_epoch_accounts_hash): {:?}", + ( + reserialize_accounts_hash, + incremental_snapshot_persistence, + update_accounts_hash, + initial_epoch_accounts_hash, + ) + ); + } + + #[test] + fn test_bank_serialize_newer() { + for (reserialize_accounts_hash, update_accounts_hash) in + [(false, false), (true, false), (true, true)] + { + let parameters = if reserialize_accounts_hash { + [false, true].to_vec() + } else { + [false].to_vec() + }; + for incremental_snapshot_persistence in parameters.clone() { + for initial_epoch_accounts_hash in [false, true] { + test_bank_serialize_style( + SerdeStyle::Newer, + reserialize_accounts_hash, + update_accounts_hash, + incremental_snapshot_persistence, + initial_epoch_accounts_hash, + ) + } + } + } + } + + fn add_root_and_flush_write_cache(bank: &Bank) { + bank.rc.accounts.add_root(bank.slot()); + bank.flush_accounts_cache_slot_for_tests() + } + + #[test] + fn test_extra_fields_eof() { + solana_logger::setup(); + let sample_rewards = (0..2) + .map(|_| StakeReward::new_random()) + .collect::>(); + for epoch_reward_status_active in [None, Some(vec![]), Some(vec![sample_rewards])] { + let (genesis_config, _) = create_genesis_config(500); + + let bank0 = Arc::new(Bank::new_for_tests_with_config( + &genesis_config, + BankTestConfig::default(), + )); + bank0.squash(); + let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + + add_root_and_flush_write_cache(&bank0); + bank.rc + .accounts + .accounts_db + .set_accounts_delta_hash_for_tests( + bank.slot(), + AccountsDeltaHash(Hash::new_unique()), + ); + bank.rc + .accounts + .accounts_db + .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); + + // Set extra fields + bank.fee_rate_governor.lamports_per_signature = 7000; + + if let Some(rewards) = epoch_reward_status_active.as_ref() { + assert_eq!(bank.block_height(), 1); + bank.set_epoch_reward_status_active(rewards.clone()); + } + + // Serialize + let snapshot_storages = bank.get_snapshot_storages(None); + let mut buf = vec![]; + let mut writer = Cursor::new(&mut buf); + + crate::serde_snapshot::bank_to_stream( + SerdeStyle::Newer, + &mut std::io::BufWriter::new(&mut writer), + &bank, + &get_storages_to_serialize(&snapshot_storages), + ) + .unwrap(); + + // Deserialize + let rdr = Cursor::new(&buf[..]); + let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); + let mut snapshot_streams = SnapshotStreams { + full_snapshot_stream: &mut reader, + incremental_snapshot_stream: None, + }; + let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); + let copied_accounts = TempDir::new().unwrap(); + let storage_and_next_append_vec_id = + copy_append_vecs(&bank.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); + let dbank = crate::serde_snapshot::bank_from_streams( + SerdeStyle::Newer, + &mut snapshot_streams, + &dbank_paths, + storage_and_next_append_vec_id, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + AccountSecondaryIndexes::default(), + None, + AccountShrinkThreshold::default(), + false, + Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), + None, + Arc::default(), + ) + .unwrap(); + + assert_eq!( + bank.fee_rate_governor.lamports_per_signature, + dbank.fee_rate_governor.lamports_per_signature + ); + + // assert epoch_reward_status is the same as the set epoch reward status + let epoch_reward_status = bank + .get_epoch_reward_status_to_serialize() + .unwrap_or(&EpochRewardStatus::Inactive); + if let Some(rewards) = epoch_reward_status_active { + assert!(matches!(epoch_reward_status, EpochRewardStatus::Active(_))); + if let EpochRewardStatus::Active(StartBlockHeightAndRewards { + start_block_height, + ref stake_rewards_by_partition, + }) = epoch_reward_status + { + assert_eq!(*start_block_height, 1); + assert_eq!(&rewards[..], &stake_rewards_by_partition[..]); + } else { + unreachable!("Epoch reward status should NOT be inactive."); + } + } else { + assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); + } + } + } + + #[test] + fn test_extra_fields_full_snapshot_archive() { + solana_logger::setup(); + + let sample_rewards = (0..2) + .map(|_| StakeReward::new_random()) + .collect::>(); + for epoch_reward_status_active in [None, Some(vec![]), Some(vec![sample_rewards])] { + let (mut genesis_config, _) = create_genesis_config(500); + activate_all_features(&mut genesis_config); + + let bank0 = Arc::new(Bank::new_for_tests(&genesis_config)); + let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + while !bank.is_complete() { + bank.fill_bank_with_ticks_for_tests(); + } + + // Set extra field + bank.fee_rate_governor.lamports_per_signature = 7000; + + if let Some(rewards) = epoch_reward_status_active.as_ref() { + assert_eq!(bank.block_height(), 1); + bank.set_epoch_reward_status_active(rewards.clone()); + } + + let (_tmp_dir, accounts_dir) = create_tmp_accounts_dir_for_tests(); + let bank_snapshots_dir = TempDir::new().unwrap(); + let full_snapshot_archives_dir = TempDir::new().unwrap(); + let incremental_snapshot_archives_dir = TempDir::new().unwrap(); + + // Serialize + let snapshot_archive_info = snapshot_utils::bank_to_full_snapshot_archive( + &bank_snapshots_dir, + &bank, + None, + full_snapshot_archives_dir.path(), + incremental_snapshot_archives_dir.path(), + ArchiveFormat::TarBzip2, + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(1).unwrap(), + ) + .unwrap(); + + // Deserialize + let (dbank, _) = snapshot_utils::bank_from_snapshot_archives( + &[accounts_dir], + bank_snapshots_dir.path(), + &snapshot_archive_info, + None, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + AccountSecondaryIndexes::default(), + None, + AccountShrinkThreshold::default(), + false, + false, + false, + Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), + None, + Arc::default(), + ) + .unwrap(); + + assert_eq!( + bank.fee_rate_governor.lamports_per_signature, + dbank.fee_rate_governor.lamports_per_signature + ); + + // assert epoch_reward_status is the same as the set epoch reward status + let epoch_reward_status = bank + .get_epoch_reward_status_to_serialize() + .unwrap_or(&EpochRewardStatus::Inactive); + if let Some(rewards) = epoch_reward_status_active { + assert!(matches!(epoch_reward_status, EpochRewardStatus::Active(_))); + if let EpochRewardStatus::Active(StartBlockHeightAndRewards { + start_block_height, + ref stake_rewards_by_partition, + }) = epoch_reward_status + { + assert_eq!(*start_block_height, 1); + assert_eq!(&rewards[..], &stake_rewards_by_partition[..]); + } else { + unreachable!("Epoch reward status should NOT be inactive."); + } + } else { + assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); + } + } + } + + #[test] + fn test_blank_extra_fields() { + solana_logger::setup(); + let (genesis_config, _) = create_genesis_config(500); + + let bank0 = Arc::new(Bank::new_for_tests_with_config( + &genesis_config, + BankTestConfig::default(), + )); + bank0.squash(); + let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); + add_root_and_flush_write_cache(&bank0); + bank.rc + .accounts + .accounts_db + .set_accounts_delta_hash_for_tests(bank.slot(), AccountsDeltaHash(Hash::new_unique())); + bank.rc + .accounts + .accounts_db + .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); + + // Set extra fields + bank.fee_rate_governor.lamports_per_signature = 7000; + + // Serialize, but don't serialize the extra fields + let snapshot_storages = bank.get_snapshot_storages(None); + let mut buf = vec![]; + let mut writer = Cursor::new(&mut buf); + + crate::serde_snapshot::bank_to_stream_no_extra_fields( + SerdeStyle::Newer, + &mut std::io::BufWriter::new(&mut writer), + &bank, + &get_storages_to_serialize(&snapshot_storages), + ) + .unwrap(); + + // Deserialize + let rdr = Cursor::new(&buf[..]); + let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); + let mut snapshot_streams = SnapshotStreams { + full_snapshot_stream: &mut reader, + incremental_snapshot_stream: None, + }; + let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); + let copied_accounts = TempDir::new().unwrap(); + let storage_and_next_append_vec_id = + copy_append_vecs(&bank.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); + let dbank = crate::serde_snapshot::bank_from_streams( + SerdeStyle::Newer, + &mut snapshot_streams, + &dbank_paths, + storage_and_next_append_vec_id, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + AccountSecondaryIndexes::default(), + None, + AccountShrinkThreshold::default(), + false, + Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), + None, + Arc::default(), + ) + .unwrap(); + + // Defaults to 0 + assert_eq!(0, dbank.fee_rate_governor.lamports_per_signature); + + // epoch_reward status should default to `Inactive` + let epoch_reward_status = bank + .get_epoch_reward_status_to_serialize() + .unwrap_or(&EpochRewardStatus::Inactive); + assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); + } + + #[cfg(RUSTC_WITH_SPECIALIZATION)] + mod test_bank_serialize { + use {super::*, crate::serde_snapshot::serialize_test_bank_and_storage}; + + // This some what long test harness is required to freeze the ABI of + // Bank's serialization due to versioned nature + #[frozen_abi(digest = "A99zFXvqYm88n6EbtEFbroDbuFNnhw4K7AmqMh2wjJmh")] + #[derive(Serialize, AbiExample)] + pub struct BankAbiTestWrapperNewer { + #[serde(serialize_with = "wrapper_newer")] + bank: Bank, + } + + pub fn wrapper_newer(bank: &Bank, s: S) -> std::result::Result + where + S: serde::Serializer, + { + bank.rc + .accounts + .accounts_db + .set_accounts_delta_hash_for_tests( + bank.slot(), + AccountsDeltaHash(Hash::new_unique()), + ); + bank.rc + .accounts + .accounts_db + .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); + let snapshot_storages = bank.rc.accounts.accounts_db.get_snapshot_storages(..=0).0; + // ensure there is a single snapshot storage example for ABI digesting + assert_eq!(snapshot_storages.len(), 1); + + serialize_test_bank_and_storage::( + bank, + &get_storages_to_serialize(&snapshot_storages), + s, + ) + } + } +} diff --git a/runtime/src/serde_snapshot.rs b/runtime/src/serde_snapshot.rs index f543d3be4..aea49dc64 100644 --- a/runtime/src/serde_snapshot.rs +++ b/runtime/src/serde_snapshot.rs @@ -504,6 +504,23 @@ impl<'a, C: TypeContext<'a>> Serialize for SerializableBankAndStorage<'a, C> { } } +#[cfg(test)] +pub fn serialize_test_bank_and_storage( + bank: &Bank, + storage: &[Vec>], + s: S, +) -> std::result::Result +where + S: serde::Serializer, +{ + (SerializableBankAndStorage:: { + bank, + snapshot_storages: storage, + phantom: std::marker::PhantomData::default(), + }) + .serialize(s) +} + #[cfg(test)] struct SerializableBankAndStorageNoExtra<'a, C> { bank: &'a Bank, diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 7b0315845..01677f35a 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -9,33 +9,23 @@ use { get_temp_accounts_paths, test_utils::create_test_accounts, AccountShrinkThreshold, }, accounts_file::{AccountsFile, AccountsFileError}, - accounts_hash::{AccountsDeltaHash, AccountsHash}, - bank::{Bank, BankTestConfig, EpochRewardStatus, StartBlockHeightAndRewards}, - epoch_accounts_hash, - genesis_utils::{activate_all_features, activate_feature}, - snapshot_utils::{ - create_tmp_accounts_dir_for_tests, get_storages_to_serialize, ArchiveFormat, - }, - stake_rewards::StakeReward, - status_cache::StatusCache, + accounts_hash::AccountsHash, + snapshot_utils::get_storages_to_serialize, }, bincode::serialize_into, rand::{thread_rng, Rng}, solana_sdk::{ account::{AccountSharedData, ReadableAccount}, clock::Slot, - feature_set, - genesis_config::{create_genesis_config, ClusterType}, + genesis_config::ClusterType, hash::Hash, pubkey::Pubkey, - signature::{Keypair, Signer}, }, std::{ io::{BufReader, Cursor}, - num::NonZeroUsize, ops::RangeFull, path::Path, - sync::{Arc, RwLock}, + sync::Arc, }, tempfile::TempDir, }; @@ -228,219 +218,6 @@ fn test_accounts_serialize_style(serde_style: SerdeStyle) { assert_eq!(accounts_hash, daccounts_hash); } -fn test_bank_serialize_style( - serde_style: SerdeStyle, - reserialize_accounts_hash: bool, - update_accounts_hash: bool, - incremental_snapshot_persistence: bool, - initial_epoch_accounts_hash: bool, -) { - solana_logger::setup(); - let (mut genesis_config, _) = create_genesis_config(500); - activate_feature(&mut genesis_config, feature_set::epoch_accounts_hash::id()); - genesis_config.epoch_schedule = EpochSchedule::custom(400, 400, false); - let bank0 = Arc::new(Bank::new_for_tests(&genesis_config)); - let eah_start_slot = epoch_accounts_hash::calculation_start(&bank0); - let bank1 = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); - bank0.squash(); - - // Create an account on a non-root fork - let key1 = Keypair::new(); - bank1.deposit(&key1.pubkey(), 5).unwrap(); - - // If setting an initial EAH, then the bank being snapshotted must be in the EAH calculation - // window. Otherwise `bank_to_stream()` below will *not* include the EAH in the bank snapshot, - // and the later-deserialized bank's EAH will not match the expected EAH. - let bank2_slot = if initial_epoch_accounts_hash { - eah_start_slot - } else { - 0 - } + 2; - let bank2 = Bank::new_from_parent(&bank0, &Pubkey::default(), bank2_slot); - - // Test new account - let key2 = Keypair::new(); - bank2.deposit(&key2.pubkey(), 10).unwrap(); - assert_eq!(bank2.get_balance(&key2.pubkey()), 10); - - let key3 = Keypair::new(); - bank2.deposit(&key3.pubkey(), 0).unwrap(); - - bank2.freeze(); - bank2.squash(); - bank2.force_flush_accounts_cache(); - bank2 - .accounts() - .accounts_db - .set_accounts_hash_for_tests(bank2.slot(), AccountsHash(Hash::new(&[0; 32]))); - - let snapshot_storages = bank2.get_snapshot_storages(None); - let mut buf = vec![]; - let mut writer = Cursor::new(&mut buf); - - let mut expected_epoch_accounts_hash = None; - - if initial_epoch_accounts_hash { - expected_epoch_accounts_hash = Some(Hash::new(&[7; 32])); - bank2 - .rc - .accounts - .accounts_db - .epoch_accounts_hash_manager - .set_valid( - EpochAccountsHash::new(expected_epoch_accounts_hash.unwrap()), - eah_start_slot, - ); - } - - crate::serde_snapshot::bank_to_stream( - serde_style, - &mut std::io::BufWriter::new(&mut writer), - &bank2, - &get_storages_to_serialize(&snapshot_storages), - ) - .unwrap(); - - if update_accounts_hash { - bank2 - .accounts() - .accounts_db - .set_accounts_hash_for_tests(bank2.slot(), AccountsHash(Hash::new(&[1; 32]))); - } - let accounts_hash = bank2.get_accounts_hash().unwrap(); - - let slot = bank2.slot(); - let incremental = - incremental_snapshot_persistence.then(|| BankIncrementalSnapshotPersistence { - full_slot: slot - 1, - full_hash: SerdeAccountsHash(Hash::new(&[1; 32])), - full_capitalization: 31, - incremental_hash: SerdeIncrementalAccountsHash(Hash::new(&[2; 32])), - incremental_capitalization: 32, - }); - - if reserialize_accounts_hash || incremental_snapshot_persistence { - let temp_dir = TempDir::new().unwrap(); - let slot_dir = snapshot_utils::get_bank_snapshot_dir(&temp_dir, slot); - let post_path = slot_dir.join(slot.to_string()); - let pre_path = post_path.with_extension(BANK_SNAPSHOT_PRE_FILENAME_EXTENSION); - std::fs::create_dir(&slot_dir).unwrap(); - { - let mut f = std::fs::File::create(pre_path).unwrap(); - f.write_all(&buf).unwrap(); - } - - assert!(reserialize_bank_with_new_accounts_hash( - slot_dir, - slot, - &accounts_hash, - incremental.as_ref(), - )); - let mut buf_reserialized; - { - let previous_len = buf.len(); - let expected = previous_len - + if incremental_snapshot_persistence { - // previously saved a none (size = sizeof_None), now added a Some - let sizeof_none = std::mem::size_of::(); - let sizeof_incremental_snapshot_persistence = - std::mem::size_of::>(); - sizeof_incremental_snapshot_persistence - sizeof_none - } else { - // no change - 0 - }; - - // +1: larger buffer than expected to make sure the file isn't larger than expected - buf_reserialized = vec![0; expected + 1]; - let mut f = std::fs::File::open(post_path).unwrap(); - let size = f.read(&mut buf_reserialized).unwrap(); - - assert_eq!( - size, - expected, - "(reserialize_accounts_hash, incremental_snapshot_persistence, update_accounts_hash, initial_epoch_accounts_hash): {:?}, previous_len: {previous_len}", - ( - reserialize_accounts_hash, - incremental_snapshot_persistence, - update_accounts_hash, - initial_epoch_accounts_hash, - ) - ); - buf_reserialized.truncate(size); - } - if update_accounts_hash { - // We cannot guarantee buffer contents are exactly the same if hash is the same. - // Things like hashsets/maps have randomness in their in-mem representations. - // This makes serialized bytes not deterministic. - // But, we can guarantee that the buffer is different if we change the hash! - assert_ne!(buf, buf_reserialized); - } - if update_accounts_hash || incremental_snapshot_persistence { - buf = buf_reserialized; - } - } - - let rdr = Cursor::new(&buf[..]); - let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); - - // Create a new set of directories for this bank's accounts - let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); - let mut status_cache = StatusCache::default(); - status_cache.add_root(2); - // Create a directory to simulate AppendVecs unpackaged from a snapshot tar - let copied_accounts = TempDir::new().unwrap(); - let storage_and_next_append_vec_id = - copy_append_vecs(&bank2.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); - let mut snapshot_streams = SnapshotStreams { - full_snapshot_stream: &mut reader, - incremental_snapshot_stream: None, - }; - let mut dbank = crate::serde_snapshot::bank_from_streams( - serde_style, - &mut snapshot_streams, - &dbank_paths, - storage_and_next_append_vec_id, - &genesis_config, - &RuntimeConfig::default(), - None, - None, - AccountSecondaryIndexes::default(), - None, - AccountShrinkThreshold::default(), - false, - Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), - None, - Arc::default(), - ) - .unwrap(); - dbank.status_cache = Arc::new(RwLock::new(status_cache)); - assert_eq!(dbank.get_balance(&key1.pubkey()), 0); - assert_eq!(dbank.get_balance(&key2.pubkey()), 10); - assert_eq!(dbank.get_balance(&key3.pubkey()), 0); - if let Some(incremental_snapshot_persistence) = incremental.clone() { - assert_eq!(dbank.get_accounts_hash(), None,); - assert_eq!( - dbank.get_incremental_accounts_hash(), - Some(incremental_snapshot_persistence.incremental_hash.into()), - ); - } else { - assert_eq!(dbank.get_accounts_hash(), Some(accounts_hash)); - assert_eq!(dbank.get_incremental_accounts_hash(), None); - } - assert!(bank2 == dbank); - assert_eq!(dbank.incremental_snapshot_persistence, incremental); - assert_eq!(dbank.get_epoch_accounts_hash_to_serialize().map(|epoch_accounts_hash| *epoch_accounts_hash.as_ref()), expected_epoch_accounts_hash, - "(reserialize_accounts_hash, incremental_snapshot_persistence, update_accounts_hash, initial_epoch_accounts_hash): {:?}", - ( - reserialize_accounts_hash, - incremental_snapshot_persistence, - update_accounts_hash, - initial_epoch_accounts_hash, - ) - ); -} - pub(crate) fn reconstruct_accounts_db_via_serialization( accounts: &AccountsDb, slot: Slot, @@ -486,343 +263,3 @@ pub(crate) fn reconstruct_accounts_db_via_serialization( fn test_accounts_serialize_newer() { test_accounts_serialize_style(SerdeStyle::Newer) } - -#[test] -fn test_bank_serialize_newer() { - for (reserialize_accounts_hash, update_accounts_hash) in - [(false, false), (true, false), (true, true)] - { - let parameters = if reserialize_accounts_hash { - [false, true].to_vec() - } else { - [false].to_vec() - }; - for incremental_snapshot_persistence in parameters.clone() { - for initial_epoch_accounts_hash in [false, true] { - test_bank_serialize_style( - SerdeStyle::Newer, - reserialize_accounts_hash, - update_accounts_hash, - incremental_snapshot_persistence, - initial_epoch_accounts_hash, - ) - } - } - } -} - -fn add_root_and_flush_write_cache(bank: &Bank) { - bank.rc.accounts.add_root(bank.slot()); - bank.flush_accounts_cache_slot_for_tests() -} - -#[test] -fn test_extra_fields_eof() { - solana_logger::setup(); - let sample_rewards = (0..2) - .map(|_| StakeReward::new_random()) - .collect::>(); - for epoch_reward_status_active in [None, Some(vec![]), Some(vec![sample_rewards])] { - let (genesis_config, _) = create_genesis_config(500); - - let bank0 = Arc::new(Bank::new_for_tests_with_config( - &genesis_config, - BankTestConfig::default(), - )); - bank0.squash(); - let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); - - add_root_and_flush_write_cache(&bank0); - bank.rc - .accounts - .accounts_db - .set_accounts_delta_hash_for_tests(bank.slot(), AccountsDeltaHash(Hash::new_unique())); - bank.rc - .accounts - .accounts_db - .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); - - // Set extra fields - bank.fee_rate_governor.lamports_per_signature = 7000; - - if let Some(rewards) = epoch_reward_status_active.as_ref() { - assert_eq!(bank.block_height(), 1); - bank.set_epoch_reward_status_active(rewards.clone()); - } - - // Serialize - let snapshot_storages = bank.get_snapshot_storages(None); - let mut buf = vec![]; - let mut writer = Cursor::new(&mut buf); - - crate::serde_snapshot::bank_to_stream( - SerdeStyle::Newer, - &mut std::io::BufWriter::new(&mut writer), - &bank, - &get_storages_to_serialize(&snapshot_storages), - ) - .unwrap(); - - // Deserialize - let rdr = Cursor::new(&buf[..]); - let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); - let mut snapshot_streams = SnapshotStreams { - full_snapshot_stream: &mut reader, - incremental_snapshot_stream: None, - }; - let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); - let copied_accounts = TempDir::new().unwrap(); - let storage_and_next_append_vec_id = - copy_append_vecs(&bank.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); - let dbank = crate::serde_snapshot::bank_from_streams( - SerdeStyle::Newer, - &mut snapshot_streams, - &dbank_paths, - storage_and_next_append_vec_id, - &genesis_config, - &RuntimeConfig::default(), - None, - None, - AccountSecondaryIndexes::default(), - None, - AccountShrinkThreshold::default(), - false, - Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), - None, - Arc::default(), - ) - .unwrap(); - - assert_eq!( - bank.fee_rate_governor.lamports_per_signature, - dbank.fee_rate_governor.lamports_per_signature - ); - - // assert epoch_reward_status is the same as the set epoch reward status - let epoch_reward_status = bank - .get_epoch_reward_status_to_serialize() - .unwrap_or(&EpochRewardStatus::Inactive); - if let Some(rewards) = epoch_reward_status_active { - assert!(matches!(epoch_reward_status, EpochRewardStatus::Active(_))); - if let EpochRewardStatus::Active(StartBlockHeightAndRewards { - start_block_height, - ref stake_rewards_by_partition, - }) = epoch_reward_status - { - assert_eq!(*start_block_height, 1); - assert_eq!(&rewards[..], &stake_rewards_by_partition[..]); - } else { - unreachable!("Epoch reward status should NOT be inactive."); - } - } else { - assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); - } - } -} - -#[test] -fn test_extra_fields_full_snapshot_archive() { - solana_logger::setup(); - - let sample_rewards = (0..2) - .map(|_| StakeReward::new_random()) - .collect::>(); - for epoch_reward_status_active in [None, Some(vec![]), Some(vec![sample_rewards])] { - let (mut genesis_config, _) = create_genesis_config(500); - activate_all_features(&mut genesis_config); - - let bank0 = Arc::new(Bank::new_for_tests(&genesis_config)); - let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); - while !bank.is_complete() { - bank.fill_bank_with_ticks_for_tests(); - } - - // Set extra field - bank.fee_rate_governor.lamports_per_signature = 7000; - - if let Some(rewards) = epoch_reward_status_active.as_ref() { - assert_eq!(bank.block_height(), 1); - bank.set_epoch_reward_status_active(rewards.clone()); - } - - let (_tmp_dir, accounts_dir) = create_tmp_accounts_dir_for_tests(); - let bank_snapshots_dir = TempDir::new().unwrap(); - let full_snapshot_archives_dir = TempDir::new().unwrap(); - let incremental_snapshot_archives_dir = TempDir::new().unwrap(); - - // Serialize - let snapshot_archive_info = snapshot_utils::bank_to_full_snapshot_archive( - &bank_snapshots_dir, - &bank, - None, - full_snapshot_archives_dir.path(), - incremental_snapshot_archives_dir.path(), - ArchiveFormat::TarBzip2, - NonZeroUsize::new(1).unwrap(), - NonZeroUsize::new(1).unwrap(), - ) - .unwrap(); - - // Deserialize - let (dbank, _) = snapshot_utils::bank_from_snapshot_archives( - &[accounts_dir], - bank_snapshots_dir.path(), - &snapshot_archive_info, - None, - &genesis_config, - &RuntimeConfig::default(), - None, - None, - AccountSecondaryIndexes::default(), - None, - AccountShrinkThreshold::default(), - false, - false, - false, - Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), - None, - Arc::default(), - ) - .unwrap(); - - assert_eq!( - bank.fee_rate_governor.lamports_per_signature, - dbank.fee_rate_governor.lamports_per_signature - ); - - // assert epoch_reward_status is the same as the set epoch reward status - let epoch_reward_status = bank - .get_epoch_reward_status_to_serialize() - .unwrap_or(&EpochRewardStatus::Inactive); - if let Some(rewards) = epoch_reward_status_active { - assert!(matches!(epoch_reward_status, EpochRewardStatus::Active(_))); - if let EpochRewardStatus::Active(StartBlockHeightAndRewards { - start_block_height, - ref stake_rewards_by_partition, - }) = epoch_reward_status - { - assert_eq!(*start_block_height, 1); - assert_eq!(&rewards[..], &stake_rewards_by_partition[..]); - } else { - unreachable!("Epoch reward status should NOT be inactive."); - } - } else { - assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); - } - } -} - -#[test] -fn test_blank_extra_fields() { - solana_logger::setup(); - let (genesis_config, _) = create_genesis_config(500); - - let bank0 = Arc::new(Bank::new_for_tests_with_config( - &genesis_config, - BankTestConfig::default(), - )); - bank0.squash(); - let mut bank = Bank::new_from_parent(&bank0, &Pubkey::default(), 1); - add_root_and_flush_write_cache(&bank0); - bank.rc - .accounts - .accounts_db - .set_accounts_delta_hash_for_tests(bank.slot(), AccountsDeltaHash(Hash::new_unique())); - bank.rc - .accounts - .accounts_db - .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); - - // Set extra fields - bank.fee_rate_governor.lamports_per_signature = 7000; - - // Serialize, but don't serialize the extra fields - let snapshot_storages = bank.get_snapshot_storages(None); - let mut buf = vec![]; - let mut writer = Cursor::new(&mut buf); - - crate::serde_snapshot::bank_to_stream_no_extra_fields( - SerdeStyle::Newer, - &mut std::io::BufWriter::new(&mut writer), - &bank, - &get_storages_to_serialize(&snapshot_storages), - ) - .unwrap(); - - // Deserialize - let rdr = Cursor::new(&buf[..]); - let mut reader = std::io::BufReader::new(&buf[rdr.position() as usize..]); - let mut snapshot_streams = SnapshotStreams { - full_snapshot_stream: &mut reader, - incremental_snapshot_stream: None, - }; - let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); - let copied_accounts = TempDir::new().unwrap(); - let storage_and_next_append_vec_id = - copy_append_vecs(&bank.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); - let dbank = crate::serde_snapshot::bank_from_streams( - SerdeStyle::Newer, - &mut snapshot_streams, - &dbank_paths, - storage_and_next_append_vec_id, - &genesis_config, - &RuntimeConfig::default(), - None, - None, - AccountSecondaryIndexes::default(), - None, - AccountShrinkThreshold::default(), - false, - Some(crate::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING), - None, - Arc::default(), - ) - .unwrap(); - - // Defaults to 0 - assert_eq!(0, dbank.fee_rate_governor.lamports_per_signature); - - // epoch_reward status should default to `Inactive` - let epoch_reward_status = bank - .get_epoch_reward_status_to_serialize() - .unwrap_or(&EpochRewardStatus::Inactive); - assert!(matches!(epoch_reward_status, EpochRewardStatus::Inactive)); -} - -#[cfg(RUSTC_WITH_SPECIALIZATION)] -mod test_bank_serialize { - use super::*; - - // This some what long test harness is required to freeze the ABI of - // Bank's serialization due to versioned nature - #[frozen_abi(digest = "A99zFXvqYm88n6EbtEFbroDbuFNnhw4K7AmqMh2wjJmh")] - #[derive(Serialize, AbiExample)] - pub struct BankAbiTestWrapperNewer { - #[serde(serialize_with = "wrapper_newer")] - bank: Bank, - } - - pub fn wrapper_newer(bank: &Bank, s: S) -> std::result::Result - where - S: serde::Serializer, - { - bank.rc - .accounts - .accounts_db - .set_accounts_delta_hash_for_tests(bank.slot(), AccountsDeltaHash(Hash::new_unique())); - bank.rc - .accounts - .accounts_db - .set_accounts_hash_for_tests(bank.slot(), AccountsHash(Hash::new_unique())); - let snapshot_storages = bank.rc.accounts.accounts_db.get_snapshot_storages(..=0).0; - // ensure there is a single snapshot storage example for ABI digesting - assert_eq!(snapshot_storages.len(), 1); - - (SerializableBankAndStorage:: { - bank, - snapshot_storages: &get_storages_to_serialize(&snapshot_storages), - phantom: std::marker::PhantomData::default(), - }) - .serialize(s) - } -}