From 79bfc4454534089d3d9492e4186abc13cedad224 Mon Sep 17 00:00:00 2001 From: Yueh-Hsuan Chiang Date: Fri, 20 Jan 2023 04:46:49 -0800 Subject: [PATCH] Introduce AccountsDataStorage --- Cargo.lock | 1 + ledger-tool/src/main.rs | 103 +++ runtime/Cargo.toml | 1 + runtime/src/accounts_data_storage.rs | 796 ++++++++++++++++++ .../src/accounts_data_storage/data_block.rs | 151 ++++ runtime/src/accounts_data_storage/error.rs | 13 + runtime/src/accounts_data_storage/file.rs | 76 ++ runtime/src/accounts_data_storage/footer.rs | 228 +++++ .../src/accounts_data_storage/meta_entries.rs | 430 ++++++++++ runtime/src/accounts_data_storage/writer.rs | 487 +++++++++++ runtime/src/accounts_file.rs | 99 ++- runtime/src/ancient_append_vecs.rs | 3 + runtime/src/append_vec.rs | 12 +- runtime/src/append_vec/test_utils.rs | 14 + runtime/src/lib.rs | 1 + sdk/src/account.rs | 10 +- 16 files changed, 2414 insertions(+), 11 deletions(-) create mode 100644 runtime/src/accounts_data_storage.rs create mode 100644 runtime/src/accounts_data_storage/data_block.rs create mode 100644 runtime/src/accounts_data_storage/error.rs create mode 100644 runtime/src/accounts_data_storage/file.rs create mode 100644 runtime/src/accounts_data_storage/footer.rs create mode 100644 runtime/src/accounts_data_storage/meta_entries.rs create mode 100644 runtime/src/accounts_data_storage/writer.rs diff --git a/Cargo.lock b/Cargo.lock index 6fafcd4fa1f..cad3fd77f31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6437,6 +6437,7 @@ dependencies = [ "num-derive", "num-traits", "num_cpus", + "num_enum", "once_cell", "ouroboros", "rand 0.7.3", diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 4ad0269211b..a1c875e0dee 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -52,11 +52,13 @@ use { AbsRequestHandlers, AbsRequestSender, AccountsBackgroundService, PrunedBanksRequestHandler, SnapshotRequestHandler, }, + accounts_data_storage::{AccountMetaFlags, AccountsDataStorage}, accounts_db::{ AccountsDb, AccountsDbConfig, CalcAccountsHashDataSource, FillerAccountsConfig, }, accounts_index::{AccountsIndexConfig, IndexLimitMb, ScanConfig}, accounts_update_notifier_interface::AccountsUpdateNotifier, + append_vec::{AppendVec, StoredAccountMeta, StoredMeta}, bank::{Bank, RewardCalculationEvent, TotalAccountsStats}, bank_forks::BankForks, cost_model::CostModel, @@ -2369,6 +2371,24 @@ fn main() { If no file name is specified, it will print the metadata of all ledger files.") ) ) + .subcommand( + SubCommand::with_name("new_ads_file") + .about("Create a new accounts-data-storage file from an existing append_vec file.") + .arg( + Arg::with_name("append_vec") + .long("append-vec") + .takes_value(true) + .value_name("APPEND_VEC_FILE_NAME") + .help("The name of the append vec file.") + ) + .arg( + Arg::with_name("ads_file_name") + .long("ads-file-name") + .takes_value(true) + .value_name("ADS_FILE_NAME") + .help("The name of the output ads file.") + ) + ) .get_matches(); info!("{} {}", crate_name!(), solana_version::version!()); @@ -4330,6 +4350,89 @@ fn main() { eprintln!("{err}"); } } + ("new_ads_file", Some(arg_matches)) => { + let append_vec_path = value_t_or_exit!(arg_matches, "append_vec", String); + let ads_file_path = + PathBuf::from(value_t_or_exit!(arg_matches, "ads_file_name", String)); + let append_vec_len = std::fs::metadata(&append_vec_path).unwrap().len() as usize; + let mut append_vec = + AppendVec::new_from_file_unchecked(append_vec_path, append_vec_len) + .expect("should succeed"); + append_vec.set_no_remove_on_drop(); + let ads = AccountsDataStorage::new(&ads_file_path, true /* create */); + ads.write_from_append_vec(&append_vec).unwrap(); + + let footer = ads.read_footer_block().unwrap(); + info!("footer = {:?}", footer); + + // read append-vec + let mut num_accounts = 0; + let mut offset = 0; + let mut account_map: HashMap = HashMap::new(); + while let Some((account, next_offset)) = append_vec.get_account(offset) { + offset = next_offset; + num_accounts += 1; + account_map.insert(account.meta.pubkey, account); + } + assert_eq!(num_accounts, footer.account_meta_count as u64); + info!("# accounts from append_vec = {:?}", num_accounts); + + let mut metas = ads + .read_account_metas_block( + footer.account_metas_offset, + footer.account_meta_count, + ) + .unwrap(); + + for i in 0..footer.account_meta_count as usize { + let account_pubkey = ads + .read_account_pubkey(&footer, i.try_into().unwrap()) + .unwrap(); + let account_owner = ads + .read_owner(footer.owners_offset, metas[i].owner_local_id) + .unwrap(); + if account_pubkey == Pubkey::default() { + continue; + } + if account_owner == Pubkey::default() { + continue; + } + let av_account = &account_map[&account_pubkey]; + info!("verifing account {:?}", account_pubkey); + info!( + " lamport: append_vec({:?}) vs ads_file ({:?})", + av_account.account_meta.lamports, metas[i].lamports + ); + assert_eq!(av_account.account_meta.lamports, metas[i].lamports); + + let data_block = ads.read_account_data_block(&footer, &mut metas, i).unwrap(); + let account_data_from_storage = metas[i].get_account_data(&data_block); + + let stored_meta_from_storage = StoredMeta { + // we no longer store write_version anymore + write_version_obsolete: av_account.meta.write_version_obsolete, + // load the first pubkey in the storage + pubkey: account_pubkey, + data_len: (account_data_from_storage.len()) as u64, + }; + assert_eq!(&stored_meta_from_storage, av_account.meta); + info!(" StoredMeta: append_vec({:?})", av_account.meta); + info!(" ads_file ({:?})", stored_meta_from_storage); + + let account_from_storage = AccountSharedData { + lamports: metas[i].lamports, + data: Arc::new(account_data_from_storage.to_vec()), + owner: account_owner, + executable: metas[i].flags_get(AccountMetaFlags::EXECUTABLE), + rent_epoch: metas[i].rent_epoch(&data_block).unwrap_or(0), + }; + let av_shared_meta = av_account.clone_account(); + + info!(" AccountSharedData: append_vec({:?})", av_shared_meta); + info!(" ads_file ({:?})", account_from_storage); + assert_eq!(account_from_storage, av_shared_meta); + } + } ("", _) => { eprintln!("{}", matches.usage()); exit(1); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a1068d4277c..f20032ee06a 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -33,6 +33,7 @@ memmap2 = "0.5.8" num-derive = { version = "0.3" } num-traits = { version = "0.2" } num_cpus = "1.13.1" +num_enum = "0.5.7" once_cell = "1.13.0" ouroboros = "0.15.0" rand = "0.7.0" diff --git a/runtime/src/accounts_data_storage.rs b/runtime/src/accounts_data_storage.rs new file mode 100644 index 00000000000..641a3d91420 --- /dev/null +++ b/runtime/src/accounts_data_storage.rs @@ -0,0 +1,796 @@ +//! docs/src/proposals/append-vec-storage.md + +mod data_block; +mod error; +mod file; +mod footer; +mod meta_entries; +mod writer; + +pub use { + crate::{ + append_vec::{ + AccountMeta, AppendVec, StorableAccountsWithHashesAndWriteVersions, StoredAccountMeta, + StoredMeta, APPEND_VEC_MMAPPED_FILES_OPEN, + }, + storable_accounts::StorableAccounts, + }, + data_block::{AccountDataBlock, AccountDataBlockFormat, AccountDataBlockWriter}, + error::AccountsDataStorageError, + file::AccountsDataStorageFile, + footer::AccountsDataStorageFooter, + log::*, + meta_entries::{AccountMetaFlags, AccountMetaStorageEntry}, + solana_sdk::{account::ReadableAccount, hash::Hash, pubkey::Pubkey}, + std::{ + borrow::Borrow, + collections::HashMap, + fs::remove_file, + io::{Seek, SeekFrom}, + path::{Path, PathBuf}, + sync::atomic::Ordering, + }, + writer::AccountsDataStorageWriter, +}; + +pub const ACCOUNT_DATA_BLOCK_SIZE: usize = 4096; +pub const ACCOUNTS_DATA_STORAGE_FORMAT_VERSION: u64 = 1; + +pub type Result = std::result::Result; + +lazy_static! { + pub static ref HASH_DEFAULT: Hash = Hash::default(); +} + +#[derive(Debug)] +pub struct AccountsDataStorage { + storage: AccountsDataStorageFile, + path: PathBuf, + footer: Option, + metas: Vec, + stored_metas: Vec, + account_metas: Vec, + account_data_blocks: HashMap>, + // append_buffer: Mutex>, + remove_on_drop: bool, +} + +impl Drop for AccountsDataStorage { + fn drop(&mut self) { + if self.remove_on_drop { + APPEND_VEC_MMAPPED_FILES_OPEN.fetch_sub(1, Ordering::Relaxed); + if let Err(_e) = remove_file(&self.path) { + // promote this to panic soon. + // disabled due to many false positive warnings while running tests. + // blocked by rpc's upgrade to jsonrpc v17 + //error!("AppendVec failed to remove {:?}: {:?}", &self.path, e); + inc_new_counter_info!("append_vec_drop_fail", 1); + } + } + } +} + +impl AccountsDataStorage { + /// Create a new accounts-state-file + pub fn new(file_path: &Path, create: bool) -> Self { + if create { + let _ignored = remove_file(file_path); + Self { + storage: AccountsDataStorageFile::new(file_path, create), + path: file_path.to_path_buf(), + footer: None, + metas: Vec::new(), + stored_metas: Vec::new(), + account_metas: Vec::new(), + account_data_blocks: HashMap::new(), + // append_buffer: Mutex::new(Vec::new()), + remove_on_drop: true, + } + } else { + let mut ads = Self { + storage: AccountsDataStorageFile::new(file_path, create), + path: file_path.to_path_buf(), + footer: None, + metas: Vec::new(), + stored_metas: Vec::new(), + account_metas: Vec::new(), + account_data_blocks: HashMap::new(), + // append_buffer: Mutex::new(Vec::new()), + remove_on_drop: true, + }; + ads.init_from_file_footer().unwrap(); + ads + } + } + + fn init_from_file_footer(&mut self) -> Result<()> { + let footer = self.read_footer_block().unwrap(); + + let metas = self + .read_account_metas_block(footer.account_metas_offset, footer.account_meta_count) + .unwrap(); + + let addresses = self + .read_account_addresses_block(footer.account_pubkeys_offset, footer.account_meta_count) + .unwrap(); + + let owners = self + .read_owners_block(footer.owners_offset, footer.owner_count) + .unwrap(); + + let count = footer.account_meta_count as usize; + let mut stored_metas: Vec = Vec::with_capacity(count); + let mut account_metas: Vec = Vec::with_capacity(count); + self.footer = Some(self.read_footer_block().unwrap()); + for i in 0..count { + self.update_account_data_blocks(&metas, i); + + stored_metas.push(StoredMeta { + write_version_obsolete: metas[i] + .write_version(&self.account_data_blocks[&metas[i].block_offset]) + .unwrap_or(0), + pubkey: addresses[i], + data_len: self.get_account_data_from_meta(&metas, i).len() as u64, + }); + + account_metas.push(AccountMeta { + lamports: metas[i].lamports, + owner: owners[metas[i].owner_local_id as usize], + executable: metas[i].flags_get(AccountMetaFlags::EXECUTABLE), + rent_epoch: metas[i] + .rent_epoch(&self.account_data_blocks[&metas[i].block_offset]) + .unwrap_or(u64::MAX), + }); + } + assert_eq!(metas.len(), stored_metas.len()); + assert_eq!(metas.len(), account_metas.len()); + + self.metas = metas; + self.stored_metas = stored_metas; + self.account_metas = account_metas; + Ok(()) + } + + /////////////////////////////////////////////////////////////////////////////// + + pub fn set_no_remove_on_drop(&mut self) { + self.remove_on_drop = false; + } + + pub fn flush(&self) -> std::io::Result<()> { + Ok(()) + } + + pub fn reset(&self) {} + + pub fn remaining_bytes(&self) -> u64 { + self.capacity() - self.len() as u64 + } + + pub fn len(&self) -> usize { + self.file_size().unwrap_or(0).try_into().unwrap() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn capacity(&self) -> u64 { + std::u64::MAX + } + + pub fn get_account<'a>(&'a self, index: usize) -> Option<(StoredAccountMeta<'a>, usize)> { + if self.is_read_only() { + if let Some(footer) = &self.footer { + if index >= footer.account_meta_count.try_into().unwrap() { + return None; + } + } + let stored_meta: &'a StoredMeta = &self.stored_metas[index]; + let account_meta: &'a AccountMeta = &self.account_metas[index]; + let data: &'a [u8] = self.get_account_data(index); + let hash: &'a Hash = self.metas[index] + .account_hash(&self.account_data_blocks[&self.metas[index].block_offset]); + return Some(( + StoredAccountMeta { + meta: stored_meta, + account_meta, + data, + offset: index, + stored_size: 0, + hash, + }, + index + 1, + )); + } + None + } + + pub fn get_path(&self) -> PathBuf { + self.path.clone() + } + + pub fn accounts(&self, mut index: usize) -> Vec { + let mut accounts = vec![]; + while let Some((account, next)) = self.get_account(index) { + accounts.push(account); + index = next; + } + accounts + } + + pub fn append_accounts< + 'a, + 'b, + T: ReadableAccount + Sync, + U: StorableAccounts<'a, T>, + V: Borrow, + >( + &self, + accounts: &StorableAccountsWithHashesAndWriteVersions<'a, 'b, T, U, V>, + skip: usize, + ) -> Option> { + if self.is_read_only() { + return None; + } + + let writer = AccountsDataStorageWriter::new(&self.path); + writer.append_accounts(accounts, skip) + } + + pub fn is_ancient(&self) -> bool { + false + } + + pub fn file_size(&self) -> Result { + Ok(self.storage.file.metadata().unwrap().len()) + } + + /////////////////////////////////////////////////////////////////////////////// + + pub fn is_read_only(&self) -> bool { + self.footer.is_some() + } + + pub fn write_from_append_vec(&self, append_vec: &AppendVec) -> Result<()> { + let writer = AccountsDataStorageWriter::new(&self.path); + writer.write_from_append_vec(&append_vec) + } + + fn get_compressed_block_size( + &self, + footer: &AccountsDataStorageFooter, + metas: &Vec, + index: usize, + ) -> usize { + let mut block_size = footer.account_metas_offset - metas[index].block_offset; + + for i in index..metas.len() { + if metas[i].block_offset == metas[index].block_offset { + continue; + } + block_size = metas[i].block_offset - metas[index].block_offset; + break; + } + + block_size.try_into().unwrap() + } + + fn update_account_data_blocks(&mut self, metas: &Vec, index: usize) { + let block_offset = &metas[index].block_offset; + if !self.account_data_blocks.contains_key(&block_offset) { + let data_block = self + .read_account_data_block(self.footer.as_ref().unwrap(), &metas, index) + .unwrap(); + + self.account_data_blocks + .insert(metas[index].block_offset, data_block); + } + } + + fn get_account_data_from_meta( + &self, + metas: &Vec, + index: usize, + ) -> &[u8] { + let data = metas[index].get_account_data( + self.account_data_blocks + .get(&metas[index].block_offset) + .unwrap(), + ); + data + } + + fn get_account_data(&self, index: usize) -> &[u8] { + self.get_account_data_from_meta(&self.metas, index) + } + + pub fn read_account_data_block( + &self, + footer: &AccountsDataStorageFooter, + metas: &Vec, + index: usize, + ) -> Result> { + let compressed_block_size = self.get_compressed_block_size(footer, metas, index) as usize; + + self.storage.seek(metas[index].block_offset)?; + + let mut buffer: Vec = vec![0; compressed_block_size]; + self.storage.read_bytes(&mut buffer)?; + + Ok(AccountDataBlock::decode( + AccountDataBlockFormat::Lz4, + &buffer[..], + )?) + } + + pub fn read_account_metas_block( + &self, + offset: u64, + meta_count: u32, + ) -> Result> { + let mut metas: Vec = Vec::with_capacity(meta_count as usize); + (&self.storage.file).seek(SeekFrom::Start(offset))?; + + for _ in 0..meta_count { + metas.push(AccountMetaStorageEntry::new_from_file(&self.storage)?); + } + + Ok(metas) + } + + fn read_account_addresses_block(&self, offset: u64, count: u32) -> Result> { + self.read_pubkeys_block(offset, count) + } + + fn read_owners_block(&self, offset: u64, count: u32) -> Result> { + self.read_pubkeys_block(offset, count) + } + + fn read_pubkeys_block(&self, offset: u64, count: u32) -> Result> { + let mut addresses: Vec = Vec::with_capacity(count as usize); + self.storage.seek(offset)?; + for _ in 0..count { + let mut pubkey = Pubkey::default(); + self.storage.read_type(&mut pubkey)?; + addresses.push(pubkey); + } + + Ok(addresses) + } + + pub fn read_account_pubkey( + &self, + footer: &AccountsDataStorageFooter, + index: u64, + ) -> Result { + self.storage + .seek(footer.account_pubkeys_offset + index * std::mem::size_of::() as u64)?; + + let mut pubkey: Pubkey = Pubkey::default(); + self.storage.read_type(&mut pubkey)?; + Ok(pubkey) + } + + pub fn read_owner(&self, owner_offset: u64, owner_local_id: u32) -> Result { + self.storage + .seek(owner_offset + owner_local_id as u64 * std::mem::size_of::() as u64)?; + + let mut pubkey: Pubkey = Pubkey::default(); + self.storage.read_type(&mut pubkey)?; + Ok(pubkey) + } + + pub fn read_footer_block(&self) -> Result { + AccountsDataStorageFooter::new_from_footer_block(&self.storage) + } +} + +#[cfg(test)] +pub mod tests { + use { + crate::{ + accounts_data_storage::{ + data_block::AccountDataBlockFormat, + footer::FOOTER_SIZE, + meta_entries::{AccountMetaOptionalFields, ACCOUNT_META_ENTRY_SIZE_BYTES}, + writer::AccountsDataStorageWriter, + AccountMetaFlags, AccountMetaStorageEntry, AccountsDataStorage, + AccountsDataStorageFile, AccountsDataStorageFooter, + ACCOUNTS_DATA_STORAGE_FORMAT_VERSION, ACCOUNT_DATA_BLOCK_SIZE, + }, + append_vec::{ + test_utils::{create_test_account_from_len, get_append_vec_path, TempFile}, + AppendVec, StorableAccountsWithHashesAndWriteVersions, StoredAccountMeta, + StoredMeta, StoredMetaWriteVersion, + }, + }, + solana_sdk::{ + account::AccountSharedData, clock::Slot, hash::Hash, pubkey::Pubkey, + stake_history::Epoch, + }, + std::{collections::HashMap, mem, path::Path, sync::Arc}, // Mutex}}, + }; + + impl AccountsDataStorage { + fn new_for_test(file_path: &Path, create: bool) -> Self { + Self { + storage: AccountsDataStorageFile::new(file_path, create), + path: file_path.to_path_buf(), + footer: None, + metas: Vec::new(), + stored_metas: Vec::new(), + account_metas: Vec::new(), + account_data_blocks: HashMap::new(), + // append_buffer: Mutex::new(Vec::new()), + remove_on_drop: false, + } + } + } + + #[test] + fn test_account_metas_block() { + let path = get_append_vec_path("test_account_metas_block"); + + const ENTRY_COUNT: u64 = 128; + const TEST_LAMPORT_BASE: u64 = 48372; + const BLOCK_OFFSET_BASE: u64 = 3423; + const DATA_LENGTH: u16 = 976; + const TEST_RENT_EPOCH: Epoch = 327; + const TEST_WRITE_VERSION: StoredMetaWriteVersion = 543432; + let mut expected_metas: Vec = vec![]; + + { + let ads = AccountsDataStorageWriter::new(&path.path); + let mut footer = AccountsDataStorageFooter::new(); + let mut cursor = 0; + let meta_per_block = (ACCOUNT_DATA_BLOCK_SIZE as u16) / DATA_LENGTH; + for i in 0..ENTRY_COUNT { + expected_metas.push( + AccountMetaStorageEntry::new() + .with_lamports(i * TEST_LAMPORT_BASE) + .with_block_offset(i * BLOCK_OFFSET_BASE) + .with_owner_local_id(i as u32) + .with_uncompressed_data_size(DATA_LENGTH) + .with_intra_block_offset(((i as u16) % meta_per_block) * DATA_LENGTH) + .with_flags( + AccountMetaFlags::new() + .with_bit(AccountMetaFlags::EXECUTABLE, i % 2 == 0) + .to_value(), + ) + .with_optional_fields(&AccountMetaOptionalFields { + rent_epoch: if i % 2 == 1 { + Some(TEST_RENT_EPOCH) + } else { + None + }, + account_hash: if i % 2 == 0 { + Some(Hash::new_unique()) + } else { + None + }, + write_version_obsolete: if i % 2 == 1 { + Some(TEST_WRITE_VERSION) + } else { + None + }, + }), + ); + } + ads.write_account_metas_block(&mut cursor, &mut footer, &expected_metas) + .unwrap(); + } + + let ads = AccountsDataStorage::new_for_test(&path.path, false); + let metas: Vec = + ads.read_account_metas_block(0, ENTRY_COUNT as u32).unwrap(); + assert_eq!(expected_metas, metas); + for i in 0..ENTRY_COUNT as usize { + assert_eq!( + metas[i].flags_get(AccountMetaFlags::HAS_RENT_EPOCH), + i % 2 == 1 + ); + assert_eq!( + metas[i].flags_get(AccountMetaFlags::HAS_ACCOUNT_HASH), + i % 2 == 0 + ); + assert_eq!( + metas[i].flags_get(AccountMetaFlags::HAS_WRITE_VERSION), + i % 2 == 1 + ); + } + } + + #[test] + fn test_account_pubkeys_block() { + let path = get_append_vec_path("test_account_pubkeys_block"); + let mut expected_pubkeys: Vec = vec![]; + const ENTRY_COUNT: u32 = 1024; + + { + let ads = AccountsDataStorageWriter::new(&path.path); + let mut footer = AccountsDataStorageFooter::new(); + let mut cursor = 0; + for _ in 0..ENTRY_COUNT { + expected_pubkeys.push(Pubkey::new_unique()); + } + ads.write_account_pubkeys_block(&mut cursor, &mut footer, &expected_pubkeys) + .unwrap(); + } + + let ads = AccountsDataStorage::new_for_test(&path.path, false); + let pubkeys: Vec = ads.read_pubkeys_block(0, ENTRY_COUNT).unwrap(); + assert_eq!(expected_pubkeys, pubkeys); + } + + fn create_test_append_vec( + path: &str, + data_sizes: &[usize], + ) -> (HashMap, AppendVec) { + let av_path = get_append_vec_path(path); + let av = AppendVec::new(&av_path.path, true, 100 * 1024 * 1024); + let mut test_accounts: HashMap = HashMap::new(); + + for size in data_sizes { + let account = create_test_account_from_len(*size); + let index = av.append_account_test(&account).unwrap(); + assert_eq!(av.get_account_test(index).unwrap(), account); + test_accounts.insert(account.0.pubkey, account); + } + + (test_accounts, av) + } + + fn ads_writer_test_help(path_prefix: &str, account_data_sizes: &[usize]) { + write_from_append_vec_test_helper( + &(path_prefix.to_owned() + "_from_append_vec"), + account_data_sizes, + ); + append_accounts_test_helper( + &(path_prefix.to_owned() + "_append_accounts"), + account_data_sizes, + ); + } + + fn append_accounts_test_helper(path_prefix: &str, account_data_sizes: &[usize]) { + let account_count = account_data_sizes.len(); + let (test_accounts, _av) = + create_test_append_vec(&(path_prefix.to_owned() + "_av"), account_data_sizes); + + let slot_ignored = Slot::MAX; + let accounts: Vec<(Pubkey, AccountSharedData)> = test_accounts + .clone() + .into_iter() + .map(|(pubkey, acc)| (pubkey, acc.1)) + .collect(); + let mut accounts_ref: Vec<(&Pubkey, &AccountSharedData)> = Vec::new(); + + for (x, y) in &accounts { + accounts_ref.push((&x, &y)); + } + + let slice = &accounts_ref[..]; + let account_data = (slot_ignored, slice); + let mut write_versions = Vec::new(); + + for (_pubkey, acc) in &test_accounts { + write_versions.push(acc.0.write_version_obsolete); + } + + let mut hashes = Vec::new(); + let mut hashes_ref = Vec::new(); + let mut hashes_map = HashMap::new(); + + for _ in 0..write_versions.len() { + hashes.push(Hash::new_unique()); + } + for i in 0..write_versions.len() { + hashes_ref.push(&hashes[i]); + } + for i in 0..write_versions.len() { + hashes_map.insert(accounts[i].0, &hashes[i]); + } + + let storable_accounts = + StorableAccountsWithHashesAndWriteVersions::new_with_hashes_and_write_versions( + &account_data, + hashes_ref, + write_versions, + ); + + let ads_path = get_append_vec_path(&(path_prefix.to_owned() + "_ads")); + { + let ads = AccountsDataStorage::new_for_test(&ads_path.path, true); + ads.append_accounts(&storable_accounts, 0); + } + + verify_account_data_storage(account_count, &test_accounts, &ads_path); + verify_account_data_storage2(account_count, &test_accounts, &ads_path, &hashes_map); + } + + fn write_from_append_vec_test_helper(path_prefix: &str, account_data_sizes: &[usize]) { + let account_count = account_data_sizes.len(); + let (test_accounts, av) = + create_test_append_vec(&(path_prefix.to_owned() + "_av"), account_data_sizes); + + let ads_path = get_append_vec_path(&(path_prefix.to_owned() + "_ads")); + { + let ads = AccountsDataStorage::new_for_test(&ads_path.path, true); + ads.write_from_append_vec(&av).unwrap(); + } + + verify_account_data_storage(account_count, &test_accounts, &ads_path); + } + + fn verify_account_data_storage2( + account_count: usize, + test_accounts: &HashMap, + ads_path: &TempFile, + hashes_map: &HashMap, + ) { + let ads = AccountsDataStorage::new(&ads_path.path, false); + let footer = ads.read_footer_block().unwrap(); + + let expected_footer = AccountsDataStorageFooter { + account_meta_count: account_count as u32, + account_meta_entry_size: ACCOUNT_META_ENTRY_SIZE_BYTES, + account_data_block_size: ACCOUNT_DATA_BLOCK_SIZE as u64, + owner_count: account_count as u32, + owner_entry_size: mem::size_of::() as u32, + // This number should be the total compressed account data size. + account_metas_offset: footer.account_metas_offset, + account_pubkeys_offset: footer.account_pubkeys_offset, + owners_offset: footer.account_pubkeys_offset + + (account_count * mem::size_of::()) as u64, + // TODO(yhchiang): not yet implemented + data_block_format: AccountDataBlockFormat::Lz4, + // TODO(yhchiang): not yet implemented + hash: footer.hash, + // TODO(yhchiang): fix this + min_account_address: Hash::default(), + max_account_address: Hash::default(), + format_version: ACCOUNTS_DATA_STORAGE_FORMAT_VERSION, + footer_size: FOOTER_SIZE as u64, + }; + assert_eq!(footer, expected_footer); + + let mut index = 0; + let mut count_from_ads = 0; + + while let Some((account, next)) = ads.get_account(index) { + index = next; + count_from_ads += 1; + let expected_account = &test_accounts[&account.meta.pubkey]; + let expected_hash = &hashes_map[&account.meta.pubkey]; + verify_account(&account, expected_account); + assert_eq!(account.hash, *expected_hash); + } + assert_eq!(&count_from_ads, &account_count); + } + + fn verify_account( + account: &StoredAccountMeta, + expected_account: &(StoredMeta, AccountSharedData), + ) { + assert_eq!(*account.meta, expected_account.0); // StoredMeta + + assert_eq!(account.account_meta.lamports, expected_account.1.lamports); + assert_eq!(account.account_meta.owner, expected_account.1.owner); + assert_eq!( + account.account_meta.rent_epoch, + expected_account.1.rent_epoch + ); + assert_eq!( + account.account_meta.executable, + expected_account.1.executable + ); + + assert_eq!(*account.data, *expected_account.1.data); + } + + fn verify_account_data_storage( + account_count: usize, + test_accounts: &HashMap, + ads_path: &TempFile, + ) { + let ads = AccountsDataStorage::new_for_test(&ads_path.path, false); + let footer = ads.read_footer_block().unwrap(); + + let expected_footer = AccountsDataStorageFooter { + account_meta_count: account_count as u32, + account_meta_entry_size: ACCOUNT_META_ENTRY_SIZE_BYTES, + account_data_block_size: ACCOUNT_DATA_BLOCK_SIZE as u64, + owner_count: account_count as u32, + owner_entry_size: mem::size_of::() as u32, + // This number should be the total compressed account data size. + account_metas_offset: footer.account_metas_offset, + account_pubkeys_offset: footer.account_pubkeys_offset, + owners_offset: footer.account_pubkeys_offset + + (account_count * mem::size_of::()) as u64, + // TODO(yhchiang): not yet implemented + data_block_format: AccountDataBlockFormat::Lz4, + // TODO(yhchiang): not yet implemented + hash: footer.hash, + min_account_address: Hash::default(), + max_account_address: Hash::default(), + format_version: ACCOUNTS_DATA_STORAGE_FORMAT_VERSION, + footer_size: FOOTER_SIZE as u64, + }; + assert_eq!(footer, expected_footer); + + let mut metas = ads + .read_account_metas_block(footer.account_metas_offset, footer.account_meta_count) + .unwrap(); + assert_eq!(metas.len(), account_count); + + for i in 0..account_count { + let account_pubkey = ads + .read_account_pubkey(&footer, i.try_into().unwrap()) + .unwrap(); + let expected_account = &test_accounts[&account_pubkey]; + assert_eq!(expected_account.1.lamports, metas[i].lamports); + + let data_block = ads.read_account_data_block(&footer, &mut metas, i).unwrap(); + let account_data_from_storage = metas[i].get_account_data(&data_block); + + let account_from_storage = AccountSharedData { + lamports: metas[i].lamports, + data: Arc::new(account_data_from_storage.to_vec()), + owner: ads + .read_owner(footer.owners_offset, metas[i].owner_local_id) + .unwrap(), + executable: metas[i].flags_get(AccountMetaFlags::EXECUTABLE), + rent_epoch: metas[i].rent_epoch(&data_block).unwrap_or(0), + }; + assert_eq!(account_from_storage, expected_account.1); + + let stored_meta_from_storage = StoredMeta { + write_version_obsolete: metas[i].write_version(&data_block).unwrap_or(0), + pubkey: account_pubkey, + data_len: (account_data_from_storage.len()) as u64, + }; + assert_eq!(stored_meta_from_storage, expected_account.0); + } + } + + #[test] + fn test_write_from_append_vec_one_small() { + ads_writer_test_help("test_write_from_append_vec_one_small", &[255]); + } + + #[test] + fn test_write_from_append_vec_one_big() { + ads_writer_test_help("test_write_from_append_vec_one_big", &[25500]); + } + + #[test] + fn test_write_from_append_vec_one_10_mb() { + ads_writer_test_help("test_write_from_append_vec_one_10_mb", &[10 * 1024 * 1024]); + } + + #[test] + fn test_write_from_append_vec_multiple_blobs() { + ads_writer_test_help( + "test_write_from_append_vec_multiple_blobs", + &[5000, 6000, 7000, 8000, 5500, 10241023, 9999], + ); + } + + #[test] + fn test_write_from_append_vec_one_data_block() { + ads_writer_test_help( + "test_write_from_append_vec_one_data_block", + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ); + } + + #[test] + fn test_write_from_append_vec_mixed_block() { + ads_writer_test_help( + "test_write_from_append_vec_mixed_block", + &[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000, 2000, 3000, 4000, 9, 8, 7, 6, 5, 4, 3, 2, 1, + ], + ); + } +} diff --git a/runtime/src/accounts_data_storage/data_block.rs b/runtime/src/accounts_data_storage/data_block.rs new file mode 100644 index 00000000000..3e008c06a29 --- /dev/null +++ b/runtime/src/accounts_data_storage/data_block.rs @@ -0,0 +1,151 @@ +use { + serde::{Deserialize, Serialize}, + std::{ + io::{Cursor, Error, Read, Write}, + mem, + }, +}; + +#[repr(u64)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + Deserialize, + num_enum::IntoPrimitive, + Serialize, + num_enum::TryFromPrimitive, +)] +#[serde(into = "u64", try_from = "u64")] +pub enum AccountDataBlockFormat { + AlignedRaw = 0u64, + Lz4 = 1u64, +} + +enum AccountDataBlockEncoder { + Raw(Cursor>), + Lz4(lz4::Encoder>), +} + +pub struct AccountDataBlockWriter { + len: usize, + encoder: AccountDataBlockEncoder, +} + +impl AccountDataBlockWriter { + pub fn new(encoding: AccountDataBlockFormat) -> Self { + Self { + len: 0, + encoder: match encoding { + AccountDataBlockFormat::AlignedRaw => { + AccountDataBlockEncoder::Raw(Cursor::new(Vec::new())) + } + AccountDataBlockFormat::Lz4 => AccountDataBlockEncoder::Lz4( + lz4::EncoderBuilder::new() + .level(1) + .build(Vec::new()) + .unwrap(), + ), + }, + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn write_type(&mut self, value: &T) -> Result { + let len = mem::size_of::(); + unsafe { + let ptr = std::slice::from_raw_parts((value as *const T) as *const u8, len); + // self.len has been updated here + self.write(ptr, len)?; + } + Ok(len) + } + + pub fn write(&mut self, buf: &[u8], len: usize) -> Result<(), Error> { + let result = match &mut self.encoder { + AccountDataBlockEncoder::Raw(cursor) => cursor.write_all(&buf[0..len]), + AccountDataBlockEncoder::Lz4(lz4_encoder) => lz4_encoder.write_all(&buf[0..len]), + }; + if result.is_ok() { + self.len += len; + } + result + } + + pub fn finish(self) -> Result<(Vec, usize), Error> { + match self.encoder { + AccountDataBlockEncoder::Raw(cursor) => Ok((cursor.into_inner(), self.len)), + AccountDataBlockEncoder::Lz4(lz4_encoder) => { + let (compressed_block, result) = lz4_encoder.finish(); + result?; + Ok((compressed_block, self.len)) + } + } + } +} + +pub struct AccountDataBlock {} + +impl AccountDataBlock { + pub fn decode(encoding: AccountDataBlockFormat, input: &[u8]) -> Result, Error> { + match encoding { + AccountDataBlockFormat::Lz4 => { + let mut decoder = lz4::Decoder::new(input).unwrap(); + let mut output: Vec = vec![]; + decoder.read_to_end(&mut output)?; + Ok(output) + } + AccountDataBlockFormat::AlignedRaw => panic!("Not implemented"), + } + } +} + +/* + #[test] + fn test_compress_and_decompress() { + let path = get_append_vec_path("test_compress_and_decompress"); + + const DATA_SIZE: usize = 10 * 1024 * 1024; + let mut expected_data: Vec = Vec::with_capacity(DATA_SIZE); + let compressed_len_full; + let compressed_len_partial; + let mut byte = rand::random::(); + for i in 0..DATA_SIZE { + if i % 64 == 0 { + byte = rand::random::(); + } + expected_data.push(byte); + } + + { + let ads = AccountsDataStorage::new_for_test(&path.path, true); + compressed_len_full = ads + .compress_and_write(&expected_data, expected_data.len()) + .unwrap(); + compressed_len_partial = ads + .compress_and_write(&expected_data, expected_data.len() / 2) + .unwrap(); + } + // Make sure we do compress data + assert!(compressed_len_full > 0); + assert!(DATA_SIZE / 2 > compressed_len_full); + + let ads = AccountsDataStorage::new_for_test(&path.path, false); + + // full data + let mut data = ads.decompress_and_read(compressed_len_full).unwrap(); + assert_eq!(expected_data, data); + + // partial data + data = ads.decompress_and_read(compressed_len_partial).unwrap(); + assert_eq!(data.len(), expected_data.len() / 2); + assert_eq!(&expected_data[0..expected_data.len() / 2], data); + } + +*/ diff --git a/runtime/src/accounts_data_storage/error.rs b/runtime/src/accounts_data_storage/error.rs new file mode 100644 index 00000000000..aa0ddac5f0b --- /dev/null +++ b/runtime/src/accounts_data_storage/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AccountsDataStorageError { + Io(#[from] std::io::Error), + MagicNumberMismatch, +} + +impl std::fmt::Display for AccountsDataStorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "accounts_data_storage_error") + } +} diff --git a/runtime/src/accounts_data_storage/file.rs b/runtime/src/accounts_data_storage/file.rs new file mode 100644 index 00000000000..9bded74d04f --- /dev/null +++ b/runtime/src/accounts_data_storage/file.rs @@ -0,0 +1,76 @@ +use { + memmap2::MmapMut, + std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + mem, + path::Path, + }, +}; + +#[derive(Debug)] +pub struct AccountsDataStorageFile { + pub file: File, + pub map: MmapMut, +} + +impl AccountsDataStorageFile { + pub fn new(file_path: &Path, create: bool) -> Self { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(create) + .open(file_path) + .map_err(|e| { + panic!( + "Unable to {} data file {} in current dir({:?}): {:?}", + if create { "create" } else { "open" }, + file_path.display(), + std::env::current_dir(), + e + ); + }) + .unwrap(); + let map = unsafe { MmapMut::map_mut(&file).unwrap() }; + Self { file, map } + } + + pub fn write_type(&self, value: &T) -> Result { + unsafe { + let ptr = + std::slice::from_raw_parts((value as *const T) as *const u8, mem::size_of::()); + (&self.file).write_all(ptr)?; + } + Ok(std::mem::size_of::()) + } + + pub fn read_type(&self, value: &mut T) -> Result<(), std::io::Error> { + unsafe { + // TODO(yhchiang): this requires alignment + let ptr = + std::slice::from_raw_parts_mut((value as *mut T) as *mut u8, mem::size_of::()); + (&self.file).read_exact(ptr)?; + } + Ok(()) + } + + pub fn seek(&self, offset: u64) -> Result { + (&self.file).seek(SeekFrom::Start(offset)) + } + + pub fn seek_from_end(&self, offset: i64) -> Result { + (&self.file).seek(SeekFrom::End(offset)) + } + + pub fn write_bytes(&self, bytes: &[u8]) -> Result { + (&self.file).write_all(bytes)?; + + Ok(bytes.len()) + } + + pub fn read_bytes(&self, buffer: &mut [u8]) -> Result<(), std::io::Error> { + (&self.file).read_exact(buffer)?; + + Ok(()) + } +} diff --git a/runtime/src/accounts_data_storage/footer.rs b/runtime/src/accounts_data_storage/footer.rs new file mode 100644 index 00000000000..6c07d67f235 --- /dev/null +++ b/runtime/src/accounts_data_storage/footer.rs @@ -0,0 +1,228 @@ +use { + crate::accounts_data_storage::{ + data_block::AccountDataBlockFormat, error::AccountsDataStorageError, + file::AccountsDataStorageFile, + }, + serde::{Deserialize, Serialize}, + solana_sdk::hash::Hash, + std::mem, +}; + +type Result = std::result::Result; + +pub(crate) const FOOTER_SIZE: i64 = + (mem::size_of::() + mem::size_of::()) as i64; +pub(crate) const FOOTER_TAIL_SIZE: i64 = 24; + +pub(crate) const FOOTER_MAGIC_NUMBER: u64 = 0x501A2AB5; // SOLALABS -> SOLANA LABS + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct AccountsDataStorageMagicNumber { + pub magic: u64, +} + +impl AccountsDataStorageMagicNumber { + pub fn new() -> Self { + Self { magic: 0 } + } + fn default() -> Self { + Self { + magic: FOOTER_MAGIC_NUMBER, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct AccountsDataStorageFooter { + // regular accounts' stats + pub account_meta_count: u32, + pub account_meta_entry_size: u32, + pub account_data_block_size: u64, + + // owner's stats + pub owner_count: u32, + pub owner_entry_size: u32, + + // offsets + pub account_metas_offset: u64, + pub account_pubkeys_offset: u64, + pub owners_offset: u64, + + // misc + pub data_block_format: AccountDataBlockFormat, + pub hash: Hash, + + // account range + pub min_account_address: Hash, + pub max_account_address: Hash, + + // tailing information + pub footer_size: u64, + pub format_version: u64, + // This field is persisted in the storage but not in this struct. + // pub magic_number: u64, // FOOTER_MAGIC_NUMBER +} + +impl AccountsDataStorageFooter { + pub fn new() -> Self { + Self { ..Self::default() } + } +} + +impl Default for AccountsDataStorageFooter { + fn default() -> Self { + Self { + account_meta_count: 0, + account_meta_entry_size: 0, + account_data_block_size: 0, + owner_count: 0, + owner_entry_size: 0, + account_metas_offset: 0, + account_pubkeys_offset: 0, + owners_offset: 0, + data_block_format: AccountDataBlockFormat::Lz4, + hash: Hash::new_unique(), + min_account_address: Hash::default(), + max_account_address: Hash::default(), + footer_size: FOOTER_SIZE as u64, + format_version: 1, + } + } +} + +impl AccountsDataStorageFooter { + pub fn write_footer_block(&self, ads_file: &AccountsDataStorageFile) -> Result<()> { + ads_file.write_type(self)?; + ads_file.write_type(&AccountsDataStorageMagicNumber::default())?; + + Ok(()) + } + + pub fn new_from_footer_block(ads_file: &AccountsDataStorageFile) -> Result { + let mut footer_size: u64 = 0; + let mut footer_version: u64 = 0; + let mut magic_number = AccountsDataStorageMagicNumber::new(); + + ads_file.seek_from_end(-FOOTER_TAIL_SIZE)?; + ads_file.read_type(&mut footer_size)?; + ads_file.read_type(&mut footer_version)?; + ads_file.read_type(&mut magic_number)?; + + if magic_number != AccountsDataStorageMagicNumber::default() { + return Err(AccountsDataStorageError::MagicNumberMismatch); + } + + let mut footer = Self::new(); + ads_file.seek_from_end(-(footer_size as i64))?; + ads_file.read_type(&mut footer)?; + + Ok(footer) + } +} + +#[cfg(test)] +pub mod tests { + use { + crate::{ + accounts_data_storage::{ + data_block::AccountDataBlockFormat, + file::AccountsDataStorageFile, + footer::{AccountsDataStorageFooter, FOOTER_SIZE}, + }, + append_vec::test_utils::get_append_vec_path, + }, + memoffset::offset_of, + solana_sdk::hash::Hash, + std::mem, + }; + + #[test] + /// Make sure the in-memory size is what we expected. + fn test_footer_size() { + assert_eq!( + mem::size_of::() + mem::size_of::(), + FOOTER_SIZE as usize + ); + } + + #[test] + fn test_footer() { + let path = get_append_vec_path("test_file_footer"); + let expected_footer = AccountsDataStorageFooter { + account_meta_count: 300, + account_meta_entry_size: 24, + account_data_block_size: 4096, + owner_count: 250, + owner_entry_size: 32, + account_metas_offset: 1062400, + account_pubkeys_offset: 1069600, + owners_offset: 1081200, + data_block_format: AccountDataBlockFormat::Lz4, + hash: Hash::new_unique(), + min_account_address: Hash::default(), + max_account_address: Hash::default(), + footer_size: FOOTER_SIZE as u64, + format_version: 1, + }; + + { + let ads_file = AccountsDataStorageFile::new(&path.path, true); + expected_footer.write_footer_block(&ads_file).unwrap(); + } + + // Reopen the same storage, and expect the persisted footer is + // the same as what we have written. + { + let ads_file = AccountsDataStorageFile::new(&path.path, true); + let footer = AccountsDataStorageFooter::new_from_footer_block(&ads_file).unwrap(); + assert_eq!(expected_footer, footer); + } + } + + #[test] + fn test_footer_layout() { + assert_eq!( + offset_of!(AccountsDataStorageFooter, account_meta_count), + 0x00 + ); + assert_eq!( + offset_of!(AccountsDataStorageFooter, account_meta_entry_size), + 0x04 + ); + assert_eq!( + offset_of!(AccountsDataStorageFooter, account_data_block_size), + 0x08 + ); + assert_eq!(offset_of!(AccountsDataStorageFooter, owner_count), 0x10); + assert_eq!( + offset_of!(AccountsDataStorageFooter, owner_entry_size), + 0x14 + ); + assert_eq!( + offset_of!(AccountsDataStorageFooter, account_metas_offset), + 0x18 + ); + assert_eq!( + offset_of!(AccountsDataStorageFooter, account_pubkeys_offset), + 0x20 + ); + assert_eq!(offset_of!(AccountsDataStorageFooter, owners_offset), 0x28); + assert_eq!( + offset_of!(AccountsDataStorageFooter, data_block_format), + 0x30 + ); + assert_eq!(offset_of!(AccountsDataStorageFooter, hash), 0x38); + assert_eq!( + offset_of!(AccountsDataStorageFooter, min_account_address), + 0x58 + ); + assert_eq!( + offset_of!(AccountsDataStorageFooter, max_account_address), + 0x78 + ); + assert_eq!(offset_of!(AccountsDataStorageFooter, footer_size), 0x98); + assert_eq!(offset_of!(AccountsDataStorageFooter, format_version), 0xA0); + } +} diff --git a/runtime/src/accounts_data_storage/meta_entries.rs b/runtime/src/accounts_data_storage/meta_entries.rs new file mode 100644 index 00000000000..2471e0cd55b --- /dev/null +++ b/runtime/src/accounts_data_storage/meta_entries.rs @@ -0,0 +1,430 @@ +use { + crate::{ + accounts_data_storage::{ + error::AccountsDataStorageError, file::AccountsDataStorageFile, AccountDataBlockWriter, + }, + append_vec::StoredMetaWriteVersion, + }, + ::solana_sdk::{hash::Hash, stake_history::Epoch}, + serde::{Deserialize, Serialize}, + std::mem::size_of, +}; + +pub const ACCOUNT_META_ENTRY_SIZE_BYTES: u32 = 32; +pub const ACCOUNT_DATA_ENTIRE_BLOCK: u16 = std::u16::MAX; + +type Result = std::result::Result; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct AccountMetaFlags { + flags: u64, +} + +lazy_static! { + pub static ref DEFAULT_ACCOUNT_HASH: Hash = Hash::default(); +} + +impl AccountMetaFlags { + pub const EXECUTABLE: u64 = 1u64; + pub const HAS_RENT_EPOCH: u64 = 1u64 << 1; + pub const HAS_ACCOUNT_HASH: u64 = 1u64 << 2; + pub const HAS_WRITE_VERSION: u64 = 1u64 << 3; + pub const HAS_DATA_LENGTH: u64 = 1u64 << 4; + + pub fn new() -> Self { + Self { flags: 0 } + } + + pub fn new_from(value: u64) -> Self { + Self { flags: value } + } + + pub fn with_bit(mut self, bit_field: u64, value: bool) -> Self { + self.set(bit_field, value); + + self + } + + pub fn to_value(self) -> u64 { + self.flags + } + + pub fn set(&mut self, bit_field: u64, value: bool) { + if value == true { + self.flags |= bit_field; + } else { + self.flags &= !bit_field; + } + } + + pub fn get(flags: &u64, bit_field: u64) -> bool { + (flags & bit_field) > 0 + } + + pub fn get_value(&self) -> u64 { + self.flags + } + + pub fn get_value_mut(&mut self) -> &mut u64 { + &mut self.flags + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct AccountMetaStorageEntry { + pub lamports: u64, + pub block_offset: u64, + pub uncompressed_data_size: u16, + pub intra_block_offset: u16, + pub owner_local_id: u32, + pub flags: u64, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AccountMetaOptionalFields { + pub rent_epoch: Option, + pub account_hash: Option, + pub write_version_obsolete: Option, +} + +impl AccountMetaOptionalFields { + /// Returns the 16-bit value where each bit represesnts whether one + /// optional field has a Some value. + pub fn update_flags(&self, flags_value: &mut u64) { + let mut flags = AccountMetaFlags::new_from(*flags_value); + flags.set(AccountMetaFlags::HAS_RENT_EPOCH, self.rent_epoch.is_some()); + flags.set( + AccountMetaFlags::HAS_ACCOUNT_HASH, + self.account_hash.is_some(), + ); + flags.set( + AccountMetaFlags::HAS_WRITE_VERSION, + self.write_version_obsolete.is_some(), + ); + *flags_value = flags.to_value(); + } + + pub fn size(&self) -> usize { + let mut size_in_bytes = 0; + if self.rent_epoch.is_some() { + size_in_bytes += size_of::(); + } + if self.account_hash.is_some() { + size_in_bytes += size_of::(); + } + if self.write_version_obsolete.is_some() { + size_in_bytes += size_of::(); + } + + size_in_bytes + } + + pub fn write(&self, writer: &mut AccountDataBlockWriter) -> Result { + let mut length = 0; + if let Some(rent_epoch) = self.rent_epoch { + length += writer.write_type(&rent_epoch)?; + } + if let Some(hash) = self.account_hash { + length += writer.write_type(&hash)?; + } + if let Some(write_version) = self.write_version_obsolete { + length += writer.write_type(&write_version)?; + } + + Ok(length) + } +} + +impl AccountMetaStorageEntry { + pub fn new() -> Self { + Self { + ..AccountMetaStorageEntry::default() + } + } + + pub fn with_lamports(mut self, lamports: u64) -> Self { + self.lamports = lamports; + self + } + + pub fn with_block_offset(mut self, offset: u64) -> Self { + self.block_offset = offset; + self + } + + pub fn with_owner_local_id(mut self, local_id: u32) -> Self { + self.owner_local_id = local_id; + self + } + + pub fn with_uncompressed_data_size(mut self, data_size: u16) -> Self { + self.uncompressed_data_size = data_size; + self + } + + pub fn with_intra_block_offset(mut self, offset: u16) -> Self { + self.intra_block_offset = offset; + self + } + + pub fn with_flags(mut self, flags: u64) -> Self { + self.flags = flags; + self + } + + pub fn with_optional_fields(mut self, fields: &AccountMetaOptionalFields) -> Self { + fields.update_flags(&mut self.flags); + + self + } + + pub fn flags_get(&self, bit_field: u64) -> bool { + AccountMetaFlags::get(&self.flags, bit_field) + } + + pub fn account_data<'a>(&self, data_block: &'a [u8]) -> &'a [u8] { + &data_block[(self.intra_block_offset as usize) + ..(self.intra_block_offset + self.uncompressed_data_size) as usize] + } + + pub fn rent_epoch(&self, data_block: &[u8]) -> Option { + let offset = self.optional_fields_offset(data_block); + if self.flags_get(AccountMetaFlags::HAS_RENT_EPOCH) { + unsafe { + let unaligned = + std::ptr::addr_of!(data_block[offset..offset + std::mem::size_of::()]) + as *const Epoch; + return Some(std::ptr::read_unaligned(unaligned)); + } + } + None + } + + pub fn account_hash<'a>(&self, data_block: &'a [u8]) -> &'a Hash { + let mut offset = self.optional_fields_offset(data_block); + if self.flags_get(AccountMetaFlags::HAS_RENT_EPOCH) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_ACCOUNT_HASH) { + unsafe { + let raw_ptr = std::slice::from_raw_parts( + data_block[offset..offset + std::mem::size_of::()].as_ptr() as *const u8, + std::mem::size_of::(), + ); + let ptr: *const Hash = raw_ptr.as_ptr() as *const Hash; + return &*ptr; + } + } + return &DEFAULT_ACCOUNT_HASH; + } + + pub fn write_version(&self, data_block: &[u8]) -> Option { + let mut offset = self.optional_fields_offset(data_block); + if self.flags_get(AccountMetaFlags::HAS_RENT_EPOCH) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_ACCOUNT_HASH) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_WRITE_VERSION) { + unsafe { + let unaligned = std::ptr::addr_of!( + data_block[offset..offset + std::mem::size_of::()] + ) as *const StoredMetaWriteVersion; + return Some(std::ptr::read_unaligned(unaligned)); + } + } + None + } + + pub fn data_length(&self, data_block: &[u8]) -> Option { + let mut offset = self.optional_fields_offset(data_block); + if self.flags_get(AccountMetaFlags::HAS_RENT_EPOCH) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_ACCOUNT_HASH) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_WRITE_VERSION) { + offset += std::mem::size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_DATA_LENGTH) { + unsafe { + let unaligned = + std::ptr::addr_of!(data_block[offset..offset + std::mem::size_of::()]) + as *const u64; + return Some(std::ptr::read_unaligned(unaligned)); + } + } + None + } + + pub fn optional_fields_size(&self) -> usize { + let mut size_in_bytes = 0; + if self.flags_get(AccountMetaFlags::HAS_RENT_EPOCH) { + size_in_bytes += size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_ACCOUNT_HASH) { + size_in_bytes += size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_WRITE_VERSION) { + size_in_bytes += size_of::(); + } + if self.flags_get(AccountMetaFlags::HAS_DATA_LENGTH) { + size_in_bytes += size_of::(); + } + + size_in_bytes + } + + fn optional_fields_offset<'a>(&self, data_block: &'a [u8]) -> usize { + if self.is_blob_account() { + return data_block.len().saturating_sub(self.optional_fields_size()); + } + (self.intra_block_offset + self.uncompressed_data_size) as usize + } + + pub fn get_account_data<'a>(&self, data_block: &'a Vec) -> &'a [u8] { + &data_block[(self.intra_block_offset as usize)..self.optional_fields_offset(data_block)] + } + + pub fn is_blob_account(&self) -> bool { + self.uncompressed_data_size == ACCOUNT_DATA_ENTIRE_BLOCK && self.intra_block_offset == 0 + } + + pub fn write_account_meta_entry(&self, ads_file: &AccountsDataStorageFile) -> Result { + ads_file.write_type(self)?; + + Ok(std::mem::size_of::()) + } + + pub fn new_from_file(ads_file: &AccountsDataStorageFile) -> Result { + let mut entry = AccountMetaStorageEntry::new(); + ads_file.read_type(&mut entry)?; + + Ok(entry) + } +} + +impl Default for AccountMetaStorageEntry { + fn default() -> Self { + Self { + lamports: 0, + block_offset: 0, + owner_local_id: 0, + uncompressed_data_size: 0, + intra_block_offset: 0, + flags: AccountMetaFlags::new().to_value(), + } + } +} + +#[cfg(test)] +pub mod tests { + use { + crate::{ + accounts_data_storage::{ + file::AccountsDataStorageFile, + meta_entries::{ + AccountMetaFlags, AccountMetaOptionalFields, AccountMetaStorageEntry, + }, + }, + append_vec::{test_utils::get_append_vec_path, StoredMetaWriteVersion}, + }, + ::solana_sdk::{hash::Hash, stake_history::Epoch}, + memoffset::offset_of, + }; + + impl AccountMetaFlags { + pub(crate) fn get_test(&self, bit_field: u64) -> bool { + (self.flags & bit_field) > 0 + } + } + + #[test] + fn test_flags() { + let mut flags = AccountMetaFlags::new(); + assert_eq!(flags.get_test(AccountMetaFlags::EXECUTABLE), false); + assert_eq!(flags.get_test(AccountMetaFlags::HAS_RENT_EPOCH), false); + + flags.set(AccountMetaFlags::EXECUTABLE, true); + assert_eq!(flags.get_test(AccountMetaFlags::EXECUTABLE), true); + assert_eq!(flags.get_test(AccountMetaFlags::HAS_RENT_EPOCH), false); + + flags.set(AccountMetaFlags::HAS_RENT_EPOCH, true); + assert_eq!(flags.get_test(AccountMetaFlags::EXECUTABLE), true); + assert_eq!(flags.get_test(AccountMetaFlags::HAS_RENT_EPOCH), true); + + flags.set(AccountMetaFlags::EXECUTABLE, false); + assert_eq!(flags.get_test(AccountMetaFlags::EXECUTABLE), false); + assert_eq!(flags.get_test(AccountMetaFlags::HAS_RENT_EPOCH), true); + + flags.set(AccountMetaFlags::HAS_RENT_EPOCH, false); + assert_eq!(flags.get_test(AccountMetaFlags::EXECUTABLE), false); + assert_eq!(flags.get_test(AccountMetaFlags::HAS_RENT_EPOCH), false); + } + + #[test] + fn test_account_meta_entry() { + let path = get_append_vec_path("test_account_meta_entry"); + + const TEST_LAMPORT: u64 = 7; + const BLOCK_OFFSET: u64 = 56987; + const OWNER_LOCAL_ID: u32 = 54; + const UNCOMPRESSED_LENGTH: u16 = 0; + const LOCAL_OFFSET: u16 = 82; + const TEST_RENT_EPOCH: Epoch = 7; + const TEST_WRITE_VERSION: StoredMetaWriteVersion = 0; + + let optional_fields = AccountMetaOptionalFields { + rent_epoch: Some(TEST_RENT_EPOCH), + account_hash: Some(Hash::new_unique()), + write_version_obsolete: Some(TEST_WRITE_VERSION), + }; + + let expected_entry = AccountMetaStorageEntry::new() + .with_lamports(TEST_LAMPORT) + .with_block_offset(BLOCK_OFFSET) + .with_owner_local_id(OWNER_LOCAL_ID) + .with_uncompressed_data_size(UNCOMPRESSED_LENGTH) + .with_intra_block_offset(LOCAL_OFFSET) + .with_flags( + AccountMetaFlags::new() + .with_bit(AccountMetaFlags::EXECUTABLE, true) + .to_value(), + ) + .with_optional_fields(&optional_fields); + + { + let mut ads_file = AccountsDataStorageFile::new(&path.path, true); + expected_entry + .write_account_meta_entry(&mut ads_file) + .unwrap(); + } + + let mut ads_file = AccountsDataStorageFile::new(&path.path, true); + let entry = AccountMetaStorageEntry::new_from_file(&mut ads_file).unwrap(); + + assert_eq!(expected_entry, entry); + assert_eq!(entry.flags_get(AccountMetaFlags::EXECUTABLE), true); + assert_eq!(entry.flags_get(AccountMetaFlags::HAS_RENT_EPOCH), true); + } + + #[test] + fn test_meta_entry_layout() { + assert_eq!(offset_of!(AccountMetaStorageEntry, lamports), 0x00); + assert_eq!(offset_of!(AccountMetaStorageEntry, block_offset), 0x08); + assert_eq!( + offset_of!(AccountMetaStorageEntry, uncompressed_data_size), + 0x10 + ); + assert_eq!( + offset_of!(AccountMetaStorageEntry, intra_block_offset), + 0x12 + ); + assert_eq!(offset_of!(AccountMetaStorageEntry, owner_local_id), 0x14); + assert_eq!(offset_of!(AccountMetaStorageEntry, flags), 0x18); + } +} diff --git a/runtime/src/accounts_data_storage/writer.rs b/runtime/src/accounts_data_storage/writer.rs new file mode 100644 index 00000000000..e346bbb7be7 --- /dev/null +++ b/runtime/src/accounts_data_storage/writer.rs @@ -0,0 +1,487 @@ +//! docs/src/proposals/append-vec-storage.md + +use { + crate::{ + accounts_data_storage::{ + data_block::{AccountDataBlockFormat, AccountDataBlockWriter}, + error::AccountsDataStorageError, + file::AccountsDataStorageFile, + footer::AccountsDataStorageFooter, + meta_entries::{ + AccountMetaFlags, AccountMetaOptionalFields, AccountMetaStorageEntry, + ACCOUNT_DATA_ENTIRE_BLOCK, ACCOUNT_META_ENTRY_SIZE_BYTES, + }, + }, + append_vec::{ + AccountMeta, AppendVec, StorableAccountsWithHashesAndWriteVersions, StoredAccountMeta, + StoredMeta, + }, + storable_accounts::StorableAccounts, + }, + solana_sdk::{account::ReadableAccount, hash::Hash, pubkey::Pubkey}, + std::{borrow::Borrow, collections::HashMap, fs::remove_file, mem, path::Path}, +}; + +pub const ACCOUNT_DATA_BLOCK_SIZE: usize = 4096; +pub const ACCOUNTS_DATA_STORAGE_FORMAT_VERSION: u64 = 1; + +pub type Result = std::result::Result; + +lazy_static! { + pub static ref HASH_DEFAULT: Hash = Hash::default(); +} + +pub(crate) struct AccountOwnerTable { + pub owners_vec: Vec, + pub owners_map: HashMap, +} + +impl AccountOwnerTable { + pub fn new() -> Self { + Self { + owners_vec: vec![], + owners_map: HashMap::new(), + } + } + pub fn check_and_add(&mut self, pubkey: &Pubkey) -> u32 { + if let Some(index) = self.owners_map.get(pubkey) { + return index.clone(); + } + let index: u32 = self.owners_vec.len().try_into().unwrap(); + self.owners_vec.push(*pubkey); + self.owners_map.insert(*pubkey, index); + + index + } +} + +#[derive(Debug)] +pub struct AccountsDataStorageWriter { + storage: AccountsDataStorageFile, +} + +impl AccountsDataStorageWriter { + /// Create a new accounts-state-file + #[allow(dead_code)] + pub fn new(file_path: &Path) -> Self { + let _ignored = remove_file(file_path); + Self { + storage: AccountsDataStorageFile::new(file_path, true), + } + } + + pub fn append_accounts< + 'a, + 'b, + T: ReadableAccount + Sync, + U: StorableAccounts<'a, T>, + V: Borrow, + >( + &self, + accounts: &StorableAccountsWithHashesAndWriteVersions<'a, 'b, T, U, V>, + skip: usize, + ) -> Option> { + let mut footer = AccountsDataStorageFooter::new(); + footer.format_version = ACCOUNTS_DATA_STORAGE_FORMAT_VERSION; + let mut cursor = 0; + let mut account_metas: Vec = vec![]; + let mut account_pubkeys: Vec = vec![]; + let mut owners_table = AccountOwnerTable::new(); + let mut dummy_hash: Hash = Hash::new_unique(); + + let mut data_block_writer = self.new_data_block_writer(); + footer.account_data_block_size = ACCOUNT_DATA_BLOCK_SIZE as u64; + + let mut buffered_account_metas: Vec = vec![]; + let mut buffered_account_pubkeys: Vec = vec![]; + + let len = accounts.accounts.len(); + let mut account_storage_sizes: Vec = Vec::with_capacity(len); + + for i in skip..len { + let (account, pubkey, hash, write_version_obsolete) = accounts.get(i); + let account_meta = account + .map(|account| AccountMeta { + lamports: account.lamports(), + owner: *account.owner(), + rent_epoch: account.rent_epoch(), + executable: account.executable(), + }) + .unwrap_or_default(); + + let stored_meta = StoredMeta { + pubkey: *pubkey, + data_len: account + .map(|account| account.data().len()) + .unwrap_or_default() as u64, + write_version_obsolete, + }; + + let stored_account_meta = StoredAccountMeta { + meta: &stored_meta, + account_meta: &account_meta, + data: account.map(|account| account.data()).unwrap_or_default(), + offset: 0, + stored_size: 0, + hash: hash, + }; + + // TODO(yhchiang): this is an estimation + account_storage_sizes.push( + std::mem::size_of::() + + 2 * std::mem::size_of::() + + stored_meta.data_len as usize / 2, + ); + + data_block_writer = self + .write_stored_account_meta( + &stored_account_meta, + &mut cursor, + &mut footer, + &mut account_metas, + &mut account_pubkeys, + &mut owners_table, + data_block_writer, + &mut buffered_account_metas, + &mut buffered_account_pubkeys, + &mut dummy_hash, + ) + .unwrap(); + } + + // Persist the last block if any + if buffered_account_metas.len() > 0 { + self.flush_account_data_block( + &mut cursor, + &mut footer, + &mut account_metas, + &mut account_pubkeys, + &mut buffered_account_metas, + &mut buffered_account_pubkeys, + data_block_writer, + ) + .ok()?; + } + + assert_eq!(buffered_account_metas.len(), 0); + assert_eq!(buffered_account_pubkeys.len(), 0); + assert_eq!(footer.account_meta_count, account_metas.len() as u32); + + self.write_account_metas_block(&mut cursor, &mut footer, &account_metas) + .ok()?; + self.write_account_pubkeys_block(&mut cursor, &mut footer, &account_pubkeys) + .ok()?; + + self.write_owners_block(&mut cursor, &mut footer, &owners_table.owners_vec) + .ok()?; + + footer.write_footer_block(&self.storage).ok()?; + + Some(account_storage_sizes) + } + + fn new_data_block_writer(&self) -> AccountDataBlockWriter { + return AccountDataBlockWriter::new(AccountDataBlockFormat::Lz4); + } + + pub(crate) fn write_account_metas_block( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + account_metas: &Vec, + ) -> Result<()> { + let entry_size = ACCOUNT_META_ENTRY_SIZE_BYTES; + footer.account_metas_offset = *cursor; + footer.account_meta_entry_size = entry_size; + for account_meta in account_metas { + *cursor += account_meta.write_account_meta_entry(&self.storage)? as u64; + } + // make sure cursor advanced as what we expected + assert_eq!( + footer.account_metas_offset + (entry_size * account_metas.len() as u32) as u64, + *cursor + ); + + Ok(()) + } + + pub(crate) fn write_account_pubkeys_block( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + pubkeys: &Vec, + ) -> Result<()> { + footer.account_pubkeys_offset = *cursor; + + self.write_pubkeys_block(cursor, pubkeys) + } + + fn write_owners_block( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + pubkeys: &Vec, + ) -> Result<()> { + footer.owners_offset = *cursor; + footer.owner_count = pubkeys.len() as u32; + footer.owner_entry_size = mem::size_of::() as u32; + + self.write_pubkeys_block(cursor, pubkeys) + } + + fn write_pubkeys_block(&self, cursor: &mut u64, pubkeys: &Vec) -> Result<()> { + for pubkey in pubkeys { + *cursor += self.storage.write_type(pubkey)? as u64; + } + + Ok(()) + } + + fn flush_account_data_block( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + account_metas: &mut Vec, + account_pubkeys: &mut Vec, + input_metas: &mut Vec, + input_pubkeys: &mut Vec, + data_block_writer: AccountDataBlockWriter, + ) -> Result<()> { + // Persist the current block + let (encoded_data, _raw_data_size) = data_block_writer.finish()?; + self.storage.write_bytes(&encoded_data)?; + + assert_eq!(input_metas.len(), input_pubkeys.len()); + + for input_meta in &mut input_metas.into_iter() { + input_meta.block_offset = *cursor; + } + for input_meta in &mut input_metas.into_iter() { + assert_eq!(input_meta.block_offset, *cursor); + } + footer.account_meta_count += input_metas.len() as u32; + account_metas.append(input_metas); + account_pubkeys.append(input_pubkeys); + + *cursor += encoded_data.len() as u64; + assert_eq!(input_metas.len(), 0); + assert_eq!(input_pubkeys.len(), 0); + + Ok(()) + } + + fn write_stored_account_meta( + &self, + account: &StoredAccountMeta, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + account_metas: &mut Vec, + account_pubkeys: &mut Vec, + owners_table: &mut AccountOwnerTable, + mut data_block: AccountDataBlockWriter, + buffered_account_metas: &mut Vec, + buffered_account_pubkeys: &mut Vec, + _hash: &mut Hash, + ) -> Result { + if !account.sanitize() { + // Not Ok + } + + let optional_fields = AccountMetaOptionalFields { + rent_epoch: Some(account.account_meta.rent_epoch), + account_hash: Some(*account.hash), + write_version_obsolete: Some(account.meta.write_version_obsolete), + }; + + if account.data.len() > ACCOUNT_DATA_BLOCK_SIZE { + self.write_blob_account_data_block( + cursor, + footer, + account_metas, + account_pubkeys, + owners_table, + account, + )?; + return Ok(data_block); + } + + // If the current data cannot fit in the current block, then + // persist the current block. + if data_block.len() + account.data.len() + optional_fields.size() > ACCOUNT_DATA_BLOCK_SIZE + { + self.flush_account_data_block( + cursor, + footer, + account_metas, + account_pubkeys, + buffered_account_metas, + buffered_account_pubkeys, + data_block, + )?; + data_block = self.new_data_block_writer(); + } + + let owner_local_id = owners_table.check_and_add(&account.account_meta.owner); + let local_offset = data_block.len(); + + data_block.write(account.data, account.data.len())?; + optional_fields.write(&mut data_block)?; + + buffered_account_metas.push( + AccountMetaStorageEntry::new() + .with_lamports(account.account_meta.lamports) + .with_block_offset(*cursor) + .with_owner_local_id(owner_local_id) + .with_uncompressed_data_size(account.data.len() as u16) + .with_intra_block_offset(local_offset as u16) + .with_flags( + AccountMetaFlags::new() + .with_bit( + AccountMetaFlags::EXECUTABLE, + account.account_meta.executable, + ) + .to_value(), + ) + .with_optional_fields(&optional_fields), + ); + buffered_account_pubkeys.push(account.meta.pubkey); + + Ok(data_block) + } + + fn write_blob_account_data_block( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + account_metas: &mut Vec, + account_pubkeys: &mut Vec, + owners_table: &mut AccountOwnerTable, + account: &StoredAccountMeta, + ) -> Result<()> { + let owner_local_id = owners_table.check_and_add(&account.account_meta.owner); + let optional_fields = AccountMetaOptionalFields { + rent_epoch: Some(account.account_meta.rent_epoch), + account_hash: Some(*account.hash), + write_version_obsolete: Some(account.meta.write_version_obsolete), + }; + + let mut writer = AccountDataBlockWriter::new(AccountDataBlockFormat::Lz4); + writer.write(&account.data, account.data.len())?; + optional_fields.write(&mut writer)?; + + let (data, _uncompressed_len) = writer.finish().unwrap(); + let compressed_length = data.len(); + self.storage.write_bytes(&data)?; + + account_metas.push( + AccountMetaStorageEntry::new() + .with_lamports(account.account_meta.lamports) + .with_block_offset(*cursor) + .with_owner_local_id(owner_local_id) + .with_uncompressed_data_size(ACCOUNT_DATA_ENTIRE_BLOCK) + .with_intra_block_offset(0) + .with_flags( + AccountMetaFlags::new() + .with_bit( + AccountMetaFlags::EXECUTABLE, + account.account_meta.executable, + ) + .to_value(), + ) + .with_optional_fields(&optional_fields), + ); + account_pubkeys.push(account.meta.pubkey); + + *cursor += compressed_length as u64; + footer.account_meta_count += 1; + + Ok(()) + } + + //////////////////////////////////////////////////////////////////////////////// + + #[allow(dead_code)] + pub fn write_from_append_vec(&self, append_vec: &AppendVec) -> Result<()> { + let mut footer = AccountsDataStorageFooter::new(); + footer.format_version = ACCOUNTS_DATA_STORAGE_FORMAT_VERSION; + let mut cursor = 0; + let mut account_metas: Vec = vec![]; + let mut account_pubkeys: Vec = vec![]; + let mut owners_table = AccountOwnerTable::new(); + let mut hash: Hash = Hash::new_unique(); + + self.write_account_data_blocks( + &mut cursor, + &mut footer, + &mut account_metas, + &mut account_pubkeys, + &mut owners_table, + &mut hash, + &append_vec, + )?; + + self.write_account_metas_block(&mut cursor, &mut footer, &account_metas)?; + self.write_account_pubkeys_block(&mut cursor, &mut footer, &account_pubkeys)?; + + self.write_owners_block(&mut cursor, &mut footer, &owners_table.owners_vec)?; + + footer.write_footer_block(&self.storage)?; + + Ok(()) + } + + #[allow(dead_code)] + fn write_account_data_blocks( + &self, + cursor: &mut u64, + footer: &mut AccountsDataStorageFooter, + account_metas: &mut Vec, + account_pubkeys: &mut Vec, + owners_table: &mut AccountOwnerTable, + // TODO(yhchiang): update hash + _hash: &mut Hash, + append_vec: &AppendVec, + ) -> Result<()> { + let mut offset = 0; + footer.account_data_block_size = ACCOUNT_DATA_BLOCK_SIZE as u64; + + let mut buffered_account_metas: Vec = vec![]; + let mut buffered_account_pubkeys: Vec = vec![]; + let mut data_block_writer = self.new_data_block_writer(); + + while let Some((account, next_offset)) = append_vec.get_account(offset) { + offset = next_offset; + data_block_writer = self.write_stored_account_meta( + &account, + cursor, + footer, + account_metas, + account_pubkeys, + owners_table, + data_block_writer, + &mut buffered_account_metas, + &mut buffered_account_pubkeys, + _hash, + )?; + } + + // Persist the last block if any + if buffered_account_metas.len() > 0 { + self.flush_account_data_block( + cursor, + footer, + account_metas, + account_pubkeys, + &mut buffered_account_metas, + &mut buffered_account_pubkeys, + data_block_writer, + )?; + } + + assert_eq!(buffered_account_metas.len(), 0); + assert_eq!(buffered_account_pubkeys.len(), 0); + assert_eq!(footer.account_meta_count, account_metas.len() as u32); + + Ok(()) + } +} diff --git a/runtime/src/accounts_file.rs b/runtime/src/accounts_file.rs index 7f82f6ff049..3a2a6f32751 100644 --- a/runtime/src/accounts_file.rs +++ b/runtime/src/accounts_file.rs @@ -1,7 +1,15 @@ use { - crate::{append_vec::*, storable_accounts::StorableAccounts}, + crate::{ + accounts_data_storage::{AccountsDataStorage, AccountsDataStorageWriter}, + append_vec::*, + storable_accounts::StorableAccounts, + }, solana_sdk::{account::ReadableAccount, clock::Slot, hash::Hash, pubkey::Pubkey}, - std::{borrow::Borrow, io, path::PathBuf}, + std::{ + borrow::Borrow, + io, + path::{Path, PathBuf}, + }, }; #[derive(Debug)] @@ -9,51 +17,105 @@ use { /// under different formats. pub enum AccountsFile { AppendVec(AppendVec), + Cold(Option, PathBuf, bool), } impl AccountsFile { + pub fn new_cold_entry(file_path: &Path, create: bool) -> Self { + if create { + Self::Cold( + None, + file_path.to_path_buf(), + true, // remove_on_drop + ) + } else { + Self::Cold( + Some(AccountsDataStorage::new(file_path, create)), + file_path.to_path_buf(), + true, // remove_on_drop + ) + } + } + /// By default, all AccountsFile will remove its underlying file on /// drop. Calling this function to disable such behavior for this /// instance. pub fn set_no_remove_on_drop(&mut self) { match self { Self::AppendVec(av) => av.set_no_remove_on_drop(), + Self::Cold(ads, _, remove_on_drop) => { + if *remove_on_drop != true { + if let Some(reader) = ads.as_mut() { + reader.set_no_remove_on_drop(); + } + } + } } } pub fn flush(&self) -> io::Result<()> { match self { Self::AppendVec(av) => av.flush(), + Self::Cold(..) => Ok(()), } } pub fn reset(&self) { match self { Self::AppendVec(av) => av.reset(), + Self::Cold(..) => {} } } pub fn remaining_bytes(&self) -> u64 { match self { Self::AppendVec(av) => av.remaining_bytes(), + Self::Cold(ads, ..) => { + if let Some(_reader) = ads { + return 0; + } else { + return std::u64::MAX; + } + } } } pub fn len(&self) -> usize { match self { Self::AppendVec(av) => av.len(), + Self::Cold(ads, ..) => { + if let Some(reader) = ads { + return reader.len(); + } else { + return 0; + } + } } } pub fn is_empty(&self) -> bool { match self { Self::AppendVec(av) => av.is_empty(), + Self::Cold(ads, ..) => { + if let Some(_reader) = ads { + return false; + } else { + return true; + } + } } } pub fn capacity(&self) -> u64 { match self { Self::AppendVec(av) => av.capacity(), + Self::Cold(ads, ..) => { + if let Some(_reader) = ads { + return 0; + } else { + return std::u64::MAX; + } + } } } @@ -67,6 +129,13 @@ impl AccountsFile { pub fn get_account(&self, index: usize) -> Option<(StoredAccountMeta<'_>, usize)> { match self { Self::AppendVec(av) => av.get_account(index), + Self::Cold(ads, ..) => { + if let Some(reader) = ads { + return reader.get_account(index); + } else { + return None; + } + } } } @@ -77,6 +146,7 @@ impl AccountsFile { ) -> Result { match self { Self::AppendVec(av) => av.account_matches_owners(offset, owners), + Self::Cold(..) => todo!(), } } @@ -84,6 +154,7 @@ impl AccountsFile { pub fn get_path(&self) -> PathBuf { match self { Self::AppendVec(av) => av.get_path(), + Self::Cold(_, path, ..) => path.clone(), } } @@ -96,6 +167,13 @@ impl AccountsFile { pub fn accounts(&self, offset: usize) -> Vec { match self { Self::AppendVec(av) => av.accounts(offset), + Self::Cold(ads, ..) => { + if let Some(reader) = ads { + return reader.accounts(offset); + } else { + return Vec::new(); + } + } } } @@ -119,6 +197,22 @@ impl AccountsFile { ) -> Option> { match self { Self::AppendVec(av) => av.append_accounts(accounts, skip), + Self::Cold(ads, path, remove_on_drop) => { + if let Some(_) = ads { + return None; + } else { + let result; + { + let writer = AccountsDataStorageWriter::new(path); + result = writer.append_accounts(accounts, skip); + } + let mut file = AccountsDataStorage::new(path, false); + if !remove_on_drop { + file.set_no_remove_on_drop(); + } + return result; + } + } } } } @@ -157,6 +251,7 @@ pub mod tests { pub(crate) fn set_current_len_for_tests(&self, len: usize) { match self { Self::AppendVec(av) => av.set_current_len_for_tests(len), + Self::Cold(..) => todo!(), } } } diff --git a/runtime/src/ancient_append_vecs.rs b/runtime/src/ancient_append_vecs.rs index 5f6c8e54c04..443c7e2ed36 100644 --- a/runtime/src/ancient_append_vecs.rs +++ b/runtime/src/ancient_append_vecs.rs @@ -752,6 +752,9 @@ pub fn get_ancient_append_vec_capacity() -> u64 { pub fn is_ancient(storage: &AccountsFile) -> bool { match storage { AccountsFile::AppendVec(storage) => storage.capacity() >= get_ancient_append_vec_capacity(), + AccountsFile::Cold(..) => { + return false; + } } } diff --git a/runtime/src/append_vec.rs b/runtime/src/append_vec.rs index 4cf45c87b4e..e77d81de171 100644 --- a/runtime/src/append_vec.rs +++ b/runtime/src/append_vec.rs @@ -72,9 +72,9 @@ pub struct StorableAccountsWithHashesAndWriteVersions< /// accounts to store /// always has pubkey and account /// may also have hash and write_version per account - accounts: &'b U, + pub(crate) accounts: &'b U, /// if accounts does not have hash and write version, this has a hash and write version per account - hashes_and_write_versions: Option<(Vec, Vec)>, + pub(crate) hashes_and_write_versions: Option<(Vec, Vec)>, _phantom: PhantomData<&'a T>, } @@ -220,7 +220,7 @@ impl<'a> StoredAccountMeta<'a> { &self.meta.pubkey } - fn sanitize(&self) -> bool { + pub(crate) fn sanitize(&self) -> bool { self.sanitize_executable() && self.sanitize_lamports() } @@ -685,6 +685,7 @@ impl AppendVec { let mut rv = Vec::with_capacity(len); for i in skip..len { let (account, pubkey, hash, write_version_obsolete) = accounts.get(i); + println!("av hash = {}", hash); let account_meta = account .map(|account| AccountMeta { lamports: account.lamports(), @@ -754,7 +755,10 @@ pub mod tests { self.current_len.store(len, Ordering::Release); } - fn append_account_test(&self, data: &(StoredMeta, AccountSharedData)) -> Option { + pub(crate) fn append_account_test( + &self, + data: &(StoredMeta, AccountSharedData), + ) -> Option { let slot_ignored = Slot::MAX; let accounts = [(&data.0.pubkey, &data.1)]; let slice = &accounts[..]; diff --git a/runtime/src/append_vec/test_utils.rs b/runtime/src/append_vec/test_utils.rs index 252044e755d..04e72703bc8 100644 --- a/runtime/src/append_vec/test_utils.rs +++ b/runtime/src/append_vec/test_utils.rs @@ -45,3 +45,17 @@ pub fn create_test_account(sample: usize) -> (StoredMeta, AccountSharedData) { }; (stored_meta, account) } + +pub fn create_test_account_from_len(data_len: usize) -> (StoredMeta, AccountSharedData) { + let mut account = AccountSharedData::new(data_len as u64, 0, &Pubkey::new_unique()); + let data_byte: u8 = (data_len % 256) as u8; + account.set_data((0..data_len).map(|_| data_byte).collect()); + account.executable = data_len % 2 > 0; + account.rent_epoch = if data_len % 3 > 0 { data_len as u64 } else { 0 }; + let stored_meta = StoredMeta { + write_version_obsolete: data_len as u64, + pubkey: Pubkey::new_unique(), + data_len: data_len as u64, + }; + (stored_meta, account) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 0ca6807cde4..5aa830a2072 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,6 +11,7 @@ pub mod account_storage; pub mod accounts; pub mod accounts_background_service; pub mod accounts_cache; +pub mod accounts_data_storage; pub mod accounts_db; pub mod accounts_file; pub mod accounts_hash; diff --git a/sdk/src/account.rs b/sdk/src/account.rs index fca8f8b71f5..3e61018cca2 100644 --- a/sdk/src/account.rs +++ b/sdk/src/account.rs @@ -103,15 +103,15 @@ impl Serialize for AccountSharedData { #[serde(from = "Account")] pub struct AccountSharedData { /// lamports in the account - lamports: u64, + pub lamports: u64, /// data held in this account - data: Arc>, + pub data: Arc>, /// the program that owns this account. If executable, the program that loads this account. - owner: Pubkey, + pub owner: Pubkey, /// this account's data contains a loaded program (and is now read-only) - executable: bool, + pub executable: bool, /// the epoch at which this account will next owe rent - rent_epoch: Epoch, + pub rent_epoch: Epoch, } /// Compares two ReadableAccounts