use {
crate::{
account_info::AccountInfo,
account_storage::meta::StoredAccountMeta,
accounts_file::{MatchAccountOwnerError, StoredAccountsInfo},
append_vec::{IndexInfo, IndexInfoInner},
tiered_storage::{
byte_block,
file::{TieredReadableFile, TieredWritableFile},
footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter},
index::{AccountIndexWriterEntry, AccountOffset, IndexBlockFormat, IndexOffset},
meta::{
AccountAddressRange, AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta,
},
mmap_utils::{get_pod, get_slice},
owners::{OwnerOffset, OwnersBlockFormat, OwnersTable},
StorableAccounts, TieredStorageError, TieredStorageFormat, TieredStorageResult,
},
},
bytemuck_derive::{Pod, Zeroable},
memmap2::{Mmap, MmapOptions},
modular_bitfield::prelude::*,
solana_sdk::{
account::{AccountSharedData, ReadableAccount, WritableAccount},
pubkey::Pubkey,
rent_collector::RENT_EXEMPT_RENT_EPOCH,
stake_history::Epoch,
},
std::{io::Write, option::Option, path::Path},
};
pub const HOT_FORMAT: TieredStorageFormat = TieredStorageFormat {
meta_entry_size: std::mem::size_of::<HotAccountMeta>(),
account_meta_format: AccountMetaFormat::Hot,
owners_block_format: OwnersBlockFormat::AddressesOnly,
index_block_format: IndexBlockFormat::AddressesThenOffsets,
account_block_format: AccountBlockFormat::AlignedRaw,
};
fn new_hot_footer() -> TieredStorageFooter {
TieredStorageFooter {
account_meta_format: HOT_FORMAT.account_meta_format,
account_meta_entry_size: HOT_FORMAT.meta_entry_size as u32,
account_block_format: HOT_FORMAT.account_block_format,
index_block_format: HOT_FORMAT.index_block_format,
owners_block_format: HOT_FORMAT.owners_block_format,
..TieredStorageFooter::default()
}
}
const MAX_HOT_OWNER_OFFSET: OwnerOffset = OwnerOffset((1 << 29) - 1);
pub(crate) const HOT_ACCOUNT_ALIGNMENT: usize = 8;
pub(crate) const HOT_BLOCK_ALIGNMENT: usize = 8;
const MAX_HOT_ACCOUNT_OFFSET: usize = u32::MAX as usize * HOT_ACCOUNT_ALIGNMENT;
fn padding_bytes(data_len: usize) -> u8 {
((HOT_ACCOUNT_ALIGNMENT - (data_len % HOT_ACCOUNT_ALIGNMENT)) % HOT_ACCOUNT_ALIGNMENT) as u8
}
const MAX_HOT_PADDING: u8 = 7;
const PADDING_BUFFER: [u8; 8] = [0u8; HOT_ACCOUNT_ALIGNMENT];
#[bitfield(bits = 32)]
#[repr(C)]
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Pod, Zeroable)]
struct HotMetaPackedFields {
padding: B3,
owner_offset: B29,
}
const _: () = assert!(std::mem::size_of::<HotMetaPackedFields>() == 4);
#[repr(C)]
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Pod, Zeroable)]
pub struct HotAccountOffset(u32);
const _: () = assert!(std::mem::size_of::<HotAccountOffset>() == 4);
impl AccountOffset for HotAccountOffset {}
impl HotAccountOffset {
pub fn new(offset: usize) -> TieredStorageResult<Self> {
if offset > MAX_HOT_ACCOUNT_OFFSET {
return Err(TieredStorageError::OffsetOutOfBounds(
offset,
MAX_HOT_ACCOUNT_OFFSET,
));
}
if offset % HOT_ACCOUNT_ALIGNMENT != 0 {
return Err(TieredStorageError::OffsetAlignmentError(
offset,
HOT_ACCOUNT_ALIGNMENT,
));
}
Ok(HotAccountOffset((offset / HOT_ACCOUNT_ALIGNMENT) as u32))
}
fn offset(&self) -> usize {
self.0 as usize * HOT_ACCOUNT_ALIGNMENT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
#[repr(C)]
pub struct HotAccountMeta {
lamports: u64,
packed_fields: HotMetaPackedFields,
flags: AccountMetaFlags,
}
const _: () = assert!(std::mem::size_of::<HotAccountMeta>() == 8 + 4 + 4);
impl TieredAccountMeta for HotAccountMeta {
fn new() -> Self {
HotAccountMeta {
lamports: 0,
packed_fields: HotMetaPackedFields::default(),
flags: AccountMetaFlags::new(),
}
}
fn with_lamports(mut self, lamports: u64) -> Self {
self.lamports = lamports;
self
}
fn with_account_data_padding(mut self, padding: u8) -> Self {
if padding > MAX_HOT_PADDING {
panic!("padding exceeds MAX_HOT_PADDING");
}
self.packed_fields.set_padding(padding);
self
}
fn with_owner_offset(mut self, owner_offset: OwnerOffset) -> Self {
if owner_offset > MAX_HOT_OWNER_OFFSET {
panic!("owner_offset exceeds MAX_HOT_OWNER_OFFSET");
}
self.packed_fields.set_owner_offset(owner_offset.0);
self
}
fn with_account_data_size(self, _account_data_size: u64) -> Self {
self
}
fn with_flags(mut self, flags: &AccountMetaFlags) -> Self {
self.flags = *flags;
self
}
fn lamports(&self) -> u64 {
self.lamports
}
fn account_data_padding(&self) -> u8 {
self.packed_fields.padding()
}
fn owner_offset(&self) -> OwnerOffset {
OwnerOffset(self.packed_fields.owner_offset())
}
fn flags(&self) -> &AccountMetaFlags {
&self.flags
}
fn supports_shared_account_block() -> bool {
false
}
fn rent_epoch(&self, account_block: &[u8]) -> Option<Epoch> {
self.flags()
.has_rent_epoch()
.then(|| {
let offset = self.optional_fields_offset(account_block)
+ AccountMetaOptionalFields::rent_epoch_offset(self.flags());
byte_block::read_pod::<Epoch>(account_block, offset).copied()
})
.flatten()
}
fn final_rent_epoch(&self, account_block: &[u8]) -> Epoch {
self.rent_epoch(account_block)
.unwrap_or(if self.lamports() != 0 {
RENT_EXEMPT_RENT_EPOCH
} else {
Epoch::default()
})
}
fn optional_fields_offset(&self, account_block: &[u8]) -> usize {
account_block
.len()
.saturating_sub(AccountMetaOptionalFields::size_from_flags(&self.flags))
}
fn account_data_size(&self, account_block: &[u8]) -> usize {
self.optional_fields_offset(account_block)
.saturating_sub(self.account_data_padding() as usize)
}
fn account_data<'a>(&self, account_block: &'a [u8]) -> &'a [u8] {
&account_block[..self.account_data_size(account_block)]
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct HotAccount<'accounts_file, M: TieredAccountMeta> {
pub meta: &'accounts_file M,
pub address: &'accounts_file Pubkey,
pub owner: &'accounts_file Pubkey,
pub index: IndexOffset,
pub account_block: &'accounts_file [u8],
}
impl<'accounts_file, M: TieredAccountMeta> HotAccount<'accounts_file, M> {
pub fn address(&self) -> &'accounts_file Pubkey {
self.address
}
pub fn index(&self) -> IndexOffset {
self.index
}
pub fn data(&self) -> &'accounts_file [u8] {
self.meta.account_data(self.account_block)
}
pub fn stored_size(&self) -> usize {
stored_size(self.meta.account_data_size(self.account_block))
}
}
impl<'accounts_file, M: TieredAccountMeta> ReadableAccount for HotAccount<'accounts_file, M> {
fn lamports(&self) -> u64 {
self.meta.lamports()
}
fn owner(&self) -> &'accounts_file Pubkey {
self.owner
}
fn executable(&self) -> bool {
self.meta.flags().executable()
}
fn rent_epoch(&self) -> Epoch {
self.meta.final_rent_epoch(self.account_block)
}
fn data(&self) -> &'accounts_file [u8] {
self.data()
}
}
#[derive(Debug)]
pub struct HotStorageReader {
mmap: Mmap,
footer: TieredStorageFooter,
}
impl HotStorageReader {
pub fn new(file: TieredReadableFile) -> TieredStorageResult<Self> {
let mmap = unsafe { MmapOptions::new().map(&file.0)? };
let footer = *TieredStorageFooter::new_from_mmap(&mmap)?;
Ok(Self { mmap, footer })
}
pub fn len(&self) -> usize {
self.mmap.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn capacity(&self) -> u64 {
self.len() as u64
}
pub fn footer(&self) -> &TieredStorageFooter {
&self.footer
}
pub fn num_accounts(&self) -> usize {
self.footer.account_entry_count as usize
}
fn get_account_meta_from_offset(
&self,
account_offset: HotAccountOffset,
) -> TieredStorageResult<&HotAccountMeta> {
let offset = account_offset.offset();
assert!(
offset.saturating_add(std::mem::size_of::<HotAccountMeta>())
<= self.footer.index_block_offset as usize,
"reading HotAccountOffset ({}) would exceed accounts blocks offset boundary ({}).",
offset,
self.footer.index_block_offset,
);
let (meta, _) = get_pod::<HotAccountMeta>(&self.mmap, offset)?;
Ok(meta)
}
pub(super) fn get_account_offset(
&self,
index_offset: IndexOffset,
) -> TieredStorageResult<HotAccountOffset> {
self.footer
.index_block_format
.get_account_offset::<HotAccountOffset>(&self.mmap, &self.footer, index_offset)
}
fn get_account_address(&self, index: IndexOffset) -> TieredStorageResult<&Pubkey> {
self.footer
.index_block_format
.get_account_address(&self.mmap, &self.footer, index)
}
fn get_owner_address(&self, owner_offset: OwnerOffset) -> TieredStorageResult<&Pubkey> {
self.footer
.owners_block_format
.get_owner_address(&self.mmap, &self.footer, owner_offset)
}
pub fn account_matches_owners(
&self,
account_offset: HotAccountOffset,
owners: &[Pubkey],
) -> Result<usize, MatchAccountOwnerError> {
let account_meta = self
.get_account_meta_from_offset(account_offset)
.map_err(|_| MatchAccountOwnerError::UnableToLoad)?;
if account_meta.lamports() == 0 {
Err(MatchAccountOwnerError::NoMatch)
} else {
let account_owner = self
.get_owner_address(account_meta.owner_offset())
.map_err(|_| MatchAccountOwnerError::UnableToLoad)?;
owners
.iter()
.position(|candidate| account_owner == candidate)
.ok_or(MatchAccountOwnerError::NoMatch)
}
}
fn get_account_block_size(
&self,
account_offset: HotAccountOffset,
index_offset: IndexOffset,
) -> TieredStorageResult<usize> {
let account_meta_offset = account_offset.offset();
let account_block_ending_offset =
if index_offset.0.saturating_add(1) == self.footer.account_entry_count {
self.footer.index_block_offset as usize
} else {
self.get_account_offset(IndexOffset(index_offset.0.saturating_add(1)))?
.offset()
};
Ok(account_block_ending_offset
.saturating_sub(account_meta_offset)
.saturating_sub(std::mem::size_of::<HotAccountMeta>()))
}
fn get_account_block(
&self,
account_offset: HotAccountOffset,
index_offset: IndexOffset,
) -> TieredStorageResult<&[u8]> {
let (data, _) = get_slice(
&self.mmap,
account_offset.offset() + std::mem::size_of::<HotAccountMeta>(),
self.get_account_block_size(account_offset, index_offset)?,
)?;
Ok(data)
}
pub fn get_stored_account_meta_callback<Ret>(
&self,
index_offset: IndexOffset,
mut callback: impl for<'local> FnMut(StoredAccountMeta<'local>) -> Ret,
) -> TieredStorageResult<Option<Ret>> {
if index_offset.0 >= self.footer.account_entry_count {
return Ok(None);
}
let account_offset = self.get_account_offset(index_offset)?;
let meta = self.get_account_meta_from_offset(account_offset)?;
let address = self.get_account_address(index_offset)?;
let owner = self.get_owner_address(meta.owner_offset())?;
let account_block = self.get_account_block(account_offset, index_offset)?;
Ok(Some(callback(StoredAccountMeta::Hot(HotAccount {
meta,
address,
owner,
index: index_offset,
account_block,
}))))
}
pub fn get_account_shared_data(
&self,
index_offset: IndexOffset,
) -> TieredStorageResult<Option<AccountSharedData>> {
if index_offset.0 >= self.footer.account_entry_count {
return Ok(None);
}
let account_offset = self.get_account_offset(index_offset)?;
let meta = self.get_account_meta_from_offset(account_offset)?;
let account_block = self.get_account_block(account_offset, index_offset)?;
let lamports = meta.lamports();
let data = meta.account_data(account_block).to_vec();
let owner = *self.get_owner_address(meta.owner_offset())?;
let executable = meta.flags().executable();
let rent_epoch = meta.final_rent_epoch(account_block);
Ok(Some(AccountSharedData::create(
lamports, data, owner, executable, rent_epoch,
)))
}
pub fn scan_pubkeys(&self, mut callback: impl FnMut(&Pubkey)) -> TieredStorageResult<()> {
for i in 0..self.footer.account_entry_count {
let address = self.get_account_address(IndexOffset(i))?;
callback(address);
}
Ok(())
}
pub(crate) fn get_account_sizes(
&self,
sorted_offsets: &[usize],
) -> TieredStorageResult<Vec<usize>> {
let mut result = Vec::with_capacity(sorted_offsets.len());
for &offset in sorted_offsets {
let index_offset = IndexOffset(AccountInfo::get_reduced_offset(offset));
let account_offset = self.get_account_offset(index_offset)?;
let meta = self.get_account_meta_from_offset(account_offset)?;
let account_block = self.get_account_block(account_offset, index_offset)?;
let data_len = meta.account_data_size(account_block);
result.push(stored_size(data_len));
}
Ok(result)
}
pub(crate) fn scan_accounts(
&self,
mut callback: impl for<'local> FnMut(StoredAccountMeta<'local>),
) -> TieredStorageResult<()> {
for i in 0..self.footer.account_entry_count {
self.get_stored_account_meta_callback(IndexOffset(i), &mut callback)?;
}
Ok(())
}
pub(crate) fn scan_index(
&self,
mut callback: impl FnMut(IndexInfo),
) -> TieredStorageResult<()> {
for i in 0..self.footer.account_entry_count {
let index_offset = IndexOffset(i);
let account_offset = self.get_account_offset(index_offset)?;
let meta = self.get_account_meta_from_offset(account_offset)?;
let pubkey = self.get_account_address(index_offset)?;
let lamports = meta.lamports();
let account_block = self.get_account_block(account_offset, index_offset)?;
let data_len = meta.account_data_size(account_block);
callback(IndexInfo {
index_info: {
IndexInfoInner {
pubkey: *pubkey,
lamports,
offset: AccountInfo::reduced_offset_to_offset(i),
data_len: data_len as u64,
executable: meta.flags().executable(),
rent_epoch: meta.final_rent_epoch(account_block),
}
},
stored_size_aligned: stored_size(data_len),
});
}
Ok(())
}
pub fn data_for_archive(&self) -> &[u8] {
self.mmap.as_ref()
}
}
fn stored_size(data_len: usize) -> usize {
data_len + std::mem::size_of::<Pubkey>()
}
fn write_optional_fields(
file: &mut TieredWritableFile,
opt_fields: &AccountMetaOptionalFields,
) -> TieredStorageResult<usize> {
let mut size = 0;
if let Some(rent_epoch) = opt_fields.rent_epoch {
size += file.write_pod(&rent_epoch)?;
}
debug_assert_eq!(size, opt_fields.size());
Ok(size)
}
#[derive(Debug)]
pub struct HotStorageWriter {
storage: TieredWritableFile,
}
impl HotStorageWriter {
pub fn new(file_path: impl AsRef<Path>) -> TieredStorageResult<Self> {
Ok(Self {
storage: TieredWritableFile::new(file_path)?,
})
}
fn write_account(
&mut self,
lamports: u64,
owner_offset: OwnerOffset,
account_data: &[u8],
executable: bool,
rent_epoch: Option<Epoch>,
) -> TieredStorageResult<usize> {
let optional_fields = AccountMetaOptionalFields { rent_epoch };
let mut flags = AccountMetaFlags::new_from(&optional_fields);
flags.set_executable(executable);
let padding_len = padding_bytes(account_data.len());
let meta = HotAccountMeta::new()
.with_lamports(lamports)
.with_owner_offset(owner_offset)
.with_account_data_size(account_data.len() as u64)
.with_account_data_padding(padding_len)
.with_flags(&flags);
let mut stored_size = 0;
stored_size += self.storage.write_pod(&meta)?;
stored_size += self.storage.write_bytes(account_data)?;
stored_size += self
.storage
.write_bytes(&PADDING_BUFFER[0..(padding_len as usize)])?;
stored_size += write_optional_fields(&mut self.storage, &optional_fields)?;
Ok(stored_size)
}
pub fn write_accounts<'a>(
&mut self,
accounts: &impl StorableAccounts<'a>,
skip: usize,
) -> TieredStorageResult<StoredAccountsInfo> {
let mut footer = new_hot_footer();
let mut index = vec![];
let mut owners_table = OwnersTable::default();
let mut cursor = 0;
let mut address_range = AccountAddressRange::default();
let len = accounts.len();
let total_input_accounts = len.saturating_sub(skip);
let mut offsets = Vec::with_capacity(total_input_accounts);
for i in skip..len {
accounts.account_default_if_zero_lamport::<TieredStorageResult<()>>(i, |account| {
let index_entry = AccountIndexWriterEntry {
address: *account.pubkey(),
offset: HotAccountOffset::new(cursor)?,
};
address_range.update(account.pubkey());
let (lamports, owner, data, executable, rent_epoch) = {
(
account.lamports(),
account.owner(),
account.data(),
account.executable(),
(account.rent_epoch() != RENT_EXEMPT_RENT_EPOCH)
.then_some(account.rent_epoch()),
)
};
let owner_offset = owners_table.insert(owner);
cursor +=
self.write_account(lamports, owner_offset, data, executable, rent_epoch)?;
offsets.push(index.len());
index.push(index_entry);
Ok(())
})?;
}
footer.account_entry_count = total_input_accounts as u32;
assert!(cursor % HOT_BLOCK_ALIGNMENT == 0);
footer.index_block_offset = cursor as u64;
cursor += footer
.index_block_format
.write_index_block(&mut self.storage, &index)?;
if cursor % HOT_BLOCK_ALIGNMENT != 0 {
assert_eq!(cursor % HOT_BLOCK_ALIGNMENT, 4);
cursor += self.storage.write_pod(&0u32)?;
}
assert!(cursor % HOT_BLOCK_ALIGNMENT == 0);
footer.owners_block_offset = cursor as u64;
footer.owner_count = owners_table.len() as u32;
cursor += footer
.owners_block_format
.write_owners_block(&mut self.storage, &owners_table)?;
footer.min_account_address = address_range.min;
footer.max_account_address = address_range.max;
cursor += footer.write_footer_block(&mut self.storage)?;
Ok(StoredAccountsInfo {
offsets,
size: cursor,
})
}
pub fn flush(&mut self) -> TieredStorageResult<()> {
self.storage
.0
.flush()
.map_err(TieredStorageError::FlushHotWriter)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::tiered_storage::{
byte_block::ByteBlockWriter,
file::{TieredStorageMagicNumber, TieredWritableFile},
footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter, FOOTER_SIZE},
hot::{HotAccountMeta, HotStorageReader},
index::{AccountIndexWriterEntry, IndexBlockFormat, IndexOffset},
meta::{AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta},
owners::{OwnersBlockFormat, OwnersTable},
test_utils::{create_test_account, verify_test_account},
},
assert_matches::assert_matches,
memoffset::offset_of,
rand::{seq::SliceRandom, Rng},
solana_sdk::{
account::ReadableAccount, hash::Hash, pubkey::Pubkey, slot_history::Slot,
stake_history::Epoch,
},
std::path::PathBuf,
tempfile::TempDir,
};
struct WriteTestFileInfo {
metas: Vec<HotAccountMeta>,
addresses: Vec<Pubkey>,
owners: Vec<Pubkey>,
datas: Vec<Vec<u8>>,
file_path: PathBuf,
temp_dir: TempDir,
}
fn write_test_file(num_accounts: usize, num_owners: usize) -> WriteTestFileInfo {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test");
let mut rng = rand::thread_rng();
let owners: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(num_owners)
.collect();
let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(num_accounts)
.collect();
let datas: Vec<_> = (0..num_accounts)
.map(|i| vec![i as u8; rng.gen_range(0..4096)])
.collect();
let metas: Vec<_> = (0..num_accounts)
.map(|i| {
HotAccountMeta::new()
.with_lamports(rng.gen())
.with_owner_offset(OwnerOffset(rng.gen_range(0..num_owners) as u32))
.with_account_data_padding(padding_bytes(datas[i].len()))
})
.collect();
let mut footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
account_entry_count: num_accounts as u32,
owner_count: num_owners as u32,
..TieredStorageFooter::default()
};
{
let mut file = TieredWritableFile::new(&file_path).unwrap();
let mut current_offset = 0;
let padding_buffer = [0u8; HOT_ACCOUNT_ALIGNMENT];
let index_writer_entries: Vec<_> = metas
.iter()
.zip(datas.iter())
.zip(addresses.iter())
.map(|((meta, data), address)| {
let prev_offset = current_offset;
current_offset += file.write_pod(meta).unwrap();
current_offset += file.write_bytes(data).unwrap();
current_offset += file
.write_bytes(&padding_buffer[0..padding_bytes(data.len()) as usize])
.unwrap();
AccountIndexWriterEntry {
address: *address,
offset: HotAccountOffset::new(prev_offset).unwrap(),
}
})
.collect();
footer.index_block_offset = current_offset as u64;
current_offset += footer
.index_block_format
.write_index_block(&mut file, &index_writer_entries)
.unwrap();
footer.owners_block_offset = current_offset as u64;
let mut owners_table = OwnersTable::default();
owners.iter().for_each(|owner_address| {
owners_table.insert(owner_address);
});
footer
.owners_block_format
.write_owners_block(&mut file, &owners_table)
.unwrap();
footer.write_footer_block(&mut file).unwrap();
}
WriteTestFileInfo {
metas,
addresses,
owners,
datas,
file_path,
temp_dir,
}
}
#[test]
fn test_hot_account_meta_layout() {
assert_eq!(offset_of!(HotAccountMeta, lamports), 0x00);
assert_eq!(offset_of!(HotAccountMeta, packed_fields), 0x08);
assert_eq!(offset_of!(HotAccountMeta, flags), 0x0C);
assert_eq!(std::mem::size_of::<HotAccountMeta>(), 16);
}
#[test]
fn test_packed_fields() {
const TEST_PADDING: u8 = 7;
const TEST_OWNER_OFFSET: u32 = 0x1fff_ef98;
let mut packed_fields = HotMetaPackedFields::default();
packed_fields.set_padding(TEST_PADDING);
packed_fields.set_owner_offset(TEST_OWNER_OFFSET);
assert_eq!(packed_fields.padding(), TEST_PADDING);
assert_eq!(packed_fields.owner_offset(), TEST_OWNER_OFFSET);
}
#[test]
fn test_packed_fields_max_values() {
let mut packed_fields = HotMetaPackedFields::default();
packed_fields.set_padding(MAX_HOT_PADDING);
packed_fields.set_owner_offset(MAX_HOT_OWNER_OFFSET.0);
assert_eq!(packed_fields.padding(), MAX_HOT_PADDING);
assert_eq!(packed_fields.owner_offset(), MAX_HOT_OWNER_OFFSET.0);
}
#[test]
fn test_hot_meta_max_values() {
let meta = HotAccountMeta::new()
.with_account_data_padding(MAX_HOT_PADDING)
.with_owner_offset(MAX_HOT_OWNER_OFFSET);
assert_eq!(meta.account_data_padding(), MAX_HOT_PADDING);
assert_eq!(meta.owner_offset(), MAX_HOT_OWNER_OFFSET);
}
#[test]
fn test_max_hot_account_offset() {
assert_matches!(HotAccountOffset::new(0), Ok(_));
assert_matches!(HotAccountOffset::new(MAX_HOT_ACCOUNT_OFFSET), Ok(_));
}
#[test]
fn test_max_hot_account_offset_out_of_bounds() {
assert_matches!(
HotAccountOffset::new(MAX_HOT_ACCOUNT_OFFSET + HOT_ACCOUNT_ALIGNMENT),
Err(TieredStorageError::OffsetOutOfBounds(_, _))
);
}
#[test]
fn test_max_hot_account_offset_alignment_error() {
assert_matches!(
HotAccountOffset::new(HOT_ACCOUNT_ALIGNMENT - 1),
Err(TieredStorageError::OffsetAlignmentError(_, _))
);
}
#[test]
#[should_panic(expected = "padding exceeds MAX_HOT_PADDING")]
fn test_hot_meta_padding_exceeds_limit() {
HotAccountMeta::new().with_account_data_padding(MAX_HOT_PADDING + 1);
}
#[test]
#[should_panic(expected = "owner_offset exceeds MAX_HOT_OWNER_OFFSET")]
fn test_hot_meta_owner_offset_exceeds_limit() {
HotAccountMeta::new().with_owner_offset(OwnerOffset(MAX_HOT_OWNER_OFFSET.0 + 1));
}
#[test]
fn test_hot_account_meta() {
const TEST_LAMPORTS: u64 = 2314232137;
const TEST_PADDING: u8 = 5;
const TEST_OWNER_OFFSET: OwnerOffset = OwnerOffset(0x1fef_1234);
const TEST_RENT_EPOCH: Epoch = 7;
let optional_fields = AccountMetaOptionalFields {
rent_epoch: Some(TEST_RENT_EPOCH),
};
let flags = AccountMetaFlags::new_from(&optional_fields);
let meta = HotAccountMeta::new()
.with_lamports(TEST_LAMPORTS)
.with_account_data_padding(TEST_PADDING)
.with_owner_offset(TEST_OWNER_OFFSET)
.with_flags(&flags);
assert_eq!(meta.lamports(), TEST_LAMPORTS);
assert_eq!(meta.account_data_padding(), TEST_PADDING);
assert_eq!(meta.owner_offset(), TEST_OWNER_OFFSET);
assert_eq!(*meta.flags(), flags);
}
#[test]
fn test_hot_account_meta_full() {
let account_data = [11u8; 83];
let padding = [0u8; 5];
const TEST_LAMPORT: u64 = 2314232137;
const OWNER_OFFSET: u32 = 0x1fef_1234;
const TEST_RENT_EPOCH: Epoch = 7;
let optional_fields = AccountMetaOptionalFields {
rent_epoch: Some(TEST_RENT_EPOCH),
};
let flags = AccountMetaFlags::new_from(&optional_fields);
let expected_meta = HotAccountMeta::new()
.with_lamports(TEST_LAMPORT)
.with_account_data_padding(padding.len().try_into().unwrap())
.with_owner_offset(OwnerOffset(OWNER_OFFSET))
.with_flags(&flags);
let mut writer = ByteBlockWriter::new(AccountBlockFormat::AlignedRaw);
writer.write_pod(&expected_meta).unwrap();
unsafe {
writer.write_type(&account_data).unwrap();
writer.write_type(&padding).unwrap();
}
writer.write_optional_fields(&optional_fields).unwrap();
let buffer = writer.finish().unwrap();
let meta = byte_block::read_pod::<HotAccountMeta>(&buffer, 0).unwrap();
assert_eq!(expected_meta, *meta);
assert!(meta.flags().has_rent_epoch());
assert_eq!(meta.account_data_padding() as usize, padding.len());
let account_block = &buffer[std::mem::size_of::<HotAccountMeta>()..];
assert_eq!(
meta.optional_fields_offset(account_block),
account_block
.len()
.saturating_sub(AccountMetaOptionalFields::size_from_flags(&flags))
);
assert_eq!(account_data.len(), meta.account_data_size(account_block));
assert_eq!(account_data, meta.account_data(account_block));
assert_eq!(meta.rent_epoch(account_block), optional_fields.rent_epoch);
}
#[test]
fn test_hot_storage_footer() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_hot_storage_footer");
let expected_footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
owners_block_format: OwnersBlockFormat::AddressesOnly,
index_block_format: IndexBlockFormat::AddressesThenOffsets,
account_block_format: AccountBlockFormat::AlignedRaw,
account_entry_count: 300,
account_meta_entry_size: 16,
account_block_size: 4096,
owner_count: 250,
owner_entry_size: 32,
index_block_offset: 1069600,
owners_block_offset: 1081200,
hash: Hash::new_unique(),
min_account_address: Pubkey::default(),
max_account_address: Pubkey::new_unique(),
footer_size: FOOTER_SIZE as u64,
format_version: 1,
};
{
let mut file = TieredWritableFile::new(&path).unwrap();
expected_footer.write_footer_block(&mut file).unwrap();
}
{
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
assert_eq!(expected_footer, *hot_storage.footer());
}
}
#[test]
fn test_hot_storage_get_account_meta_from_offset() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_hot_storage_footer");
const NUM_ACCOUNTS: u32 = 10;
let mut rng = rand::thread_rng();
let hot_account_metas: Vec<_> = (0..NUM_ACCOUNTS)
.map(|_| {
HotAccountMeta::new()
.with_lamports(rng.gen_range(0..u64::MAX))
.with_owner_offset(OwnerOffset(rng.gen_range(0..NUM_ACCOUNTS)))
})
.collect();
let account_offsets: Vec<_>;
let mut footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
account_entry_count: NUM_ACCOUNTS,
..TieredStorageFooter::default()
};
{
let mut file = TieredWritableFile::new(&path).unwrap();
let mut current_offset = 0;
account_offsets = hot_account_metas
.iter()
.map(|meta| {
let prev_offset = current_offset;
current_offset += file.write_pod(meta).unwrap();
HotAccountOffset::new(prev_offset).unwrap()
})
.collect();
footer.index_block_offset = current_offset as u64;
footer.write_footer_block(&mut file).unwrap();
}
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
for (offset, expected_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
let meta = hot_storage.get_account_meta_from_offset(*offset).unwrap();
assert_eq!(meta, expected_meta);
}
assert_eq!(&footer, hot_storage.footer());
}
#[test]
#[should_panic(expected = "would exceed accounts blocks offset boundary")]
fn test_get_acount_meta_from_offset_out_of_bounds() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir
.path()
.join("test_get_acount_meta_from_offset_out_of_bounds");
let footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
index_block_offset: 160,
..TieredStorageFooter::default()
};
{
let mut file = TieredWritableFile::new(&path).unwrap();
footer.write_footer_block(&mut file).unwrap();
}
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
let offset = HotAccountOffset::new(footer.index_block_offset as usize).unwrap();
hot_storage.get_account_meta_from_offset(offset).unwrap();
}
#[test]
fn test_hot_storage_get_account_offset_and_address() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir
.path()
.join("test_hot_storage_get_account_offset_and_address");
const NUM_ACCOUNTS: u32 = 10;
let mut rng = rand::thread_rng();
let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_ACCOUNTS as usize)
.collect();
let index_writer_entries: Vec<_> = addresses
.iter()
.map(|address| AccountIndexWriterEntry {
address: *address,
offset: HotAccountOffset::new(
rng.gen_range(0..u32::MAX) as usize * HOT_ACCOUNT_ALIGNMENT,
)
.unwrap(),
})
.collect();
let mut footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
account_entry_count: NUM_ACCOUNTS,
index_block_offset: 0,
..TieredStorageFooter::default()
};
{
let mut file = TieredWritableFile::new(&path).unwrap();
let cursor = footer
.index_block_format
.write_index_block(&mut file, &index_writer_entries)
.unwrap();
footer.owners_block_offset = cursor as u64;
footer.write_footer_block(&mut file).unwrap();
}
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
for (i, index_writer_entry) in index_writer_entries.iter().enumerate() {
let account_offset = hot_storage
.get_account_offset(IndexOffset(i as u32))
.unwrap();
assert_eq!(account_offset, index_writer_entry.offset);
let account_address = hot_storage
.get_account_address(IndexOffset(i as u32))
.unwrap();
assert_eq!(account_address, &index_writer_entry.address);
}
}
#[test]
fn test_hot_storage_get_owner_address() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_hot_storage_get_owner_address");
const NUM_OWNERS: usize = 10;
let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_OWNERS)
.collect();
let footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
owners_block_offset: 0,
..TieredStorageFooter::default()
};
{
let mut file = TieredWritableFile::new(&path).unwrap();
let mut owners_table = OwnersTable::default();
addresses.iter().for_each(|owner_address| {
owners_table.insert(owner_address);
});
footer
.owners_block_format
.write_owners_block(&mut file, &owners_table)
.unwrap();
footer.write_footer_block(&mut file).unwrap();
}
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
for (i, address) in addresses.iter().enumerate() {
assert_eq!(
hot_storage
.get_owner_address(OwnerOffset(i as u32))
.unwrap(),
address,
);
}
}
#[test]
fn test_account_matches_owners() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_hot_storage_get_owner_address");
const NUM_OWNERS: u32 = 10;
let owner_addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_OWNERS as usize)
.collect();
const NUM_ACCOUNTS: u32 = 30;
let mut rng = rand::thread_rng();
let hot_account_metas: Vec<_> = std::iter::repeat_with({
|| {
HotAccountMeta::new()
.with_lamports(rng.gen_range(1..u64::MAX))
.with_owner_offset(OwnerOffset(rng.gen_range(0..NUM_OWNERS)))
}
})
.take(NUM_ACCOUNTS as usize)
.collect();
let mut footer = TieredStorageFooter {
account_meta_format: AccountMetaFormat::Hot,
account_entry_count: NUM_ACCOUNTS,
owner_count: NUM_OWNERS,
..TieredStorageFooter::default()
};
let account_offsets: Vec<_>;
{
let mut file = TieredWritableFile::new(&path).unwrap();
let mut current_offset = 0;
account_offsets = hot_account_metas
.iter()
.map(|meta| {
let prev_offset = current_offset;
current_offset += file.write_pod(meta).unwrap();
HotAccountOffset::new(prev_offset).unwrap()
})
.collect();
footer.index_block_offset = current_offset as u64;
footer.owners_block_offset = footer.index_block_offset;
let mut owners_table = OwnersTable::default();
owner_addresses.iter().for_each(|owner_address| {
owners_table.insert(owner_address);
});
footer
.owners_block_format
.write_owners_block(&mut file, &owners_table)
.unwrap();
footer.write_footer_block(&mut file).unwrap();
}
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
let mut owner_candidates = owner_addresses.clone();
owner_candidates.shuffle(&mut rng);
for (account_offset, account_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
let index = hot_storage
.account_matches_owners(*account_offset, &owner_candidates)
.unwrap();
assert_eq!(
owner_candidates[index],
owner_addresses[account_meta.owner_offset().0 as usize]
);
}
const NUM_UNMATCHED_OWNERS: usize = 20;
let unmatched_candidates: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_UNMATCHED_OWNERS)
.collect();
for account_offset in account_offsets.iter() {
assert_eq!(
hot_storage.account_matches_owners(*account_offset, &unmatched_candidates),
Err(MatchAccountOwnerError::NoMatch)
);
}
owner_candidates.extend(unmatched_candidates);
owner_candidates.shuffle(&mut rng);
for (account_offset, account_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
let index = hot_storage
.account_matches_owners(*account_offset, &owner_candidates)
.unwrap();
assert_eq!(
owner_candidates[index],
owner_addresses[account_meta.owner_offset().0 as usize]
);
}
}
#[test]
fn test_get_stored_account_meta() {
const NUM_ACCOUNTS: usize = 20;
const NUM_OWNERS: usize = 10;
let test_info = write_test_file(NUM_ACCOUNTS, NUM_OWNERS);
let file = TieredReadableFile::new(&test_info.file_path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
for i in 0..NUM_ACCOUNTS {
hot_storage
.get_stored_account_meta_callback(IndexOffset(i as u32), |stored_account_meta| {
assert_eq!(
stored_account_meta.lamports(),
test_info.metas[i].lamports()
);
assert_eq!(stored_account_meta.data().len(), test_info.datas[i].len());
assert_eq!(stored_account_meta.data(), test_info.datas[i]);
assert_eq!(
*stored_account_meta.owner(),
test_info.owners[test_info.metas[i].owner_offset().0 as usize]
);
assert_eq!(*stored_account_meta.pubkey(), test_info.addresses[i]);
})
.unwrap()
.unwrap();
}
assert_matches!(
hot_storage.get_stored_account_meta_callback(IndexOffset(NUM_ACCOUNTS as u32), |_| {
panic!("unexpected");
}),
Ok(None)
);
}
#[test]
fn test_get_account_shared_data() {
const NUM_ACCOUNTS: usize = 20;
const NUM_OWNERS: usize = 10;
let test_info = write_test_file(NUM_ACCOUNTS, NUM_OWNERS);
let file = TieredReadableFile::new(&test_info.file_path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
for i in 0..NUM_ACCOUNTS {
let index_offset = IndexOffset(i as u32);
let account = hot_storage
.get_account_shared_data(index_offset)
.unwrap()
.unwrap();
assert_eq!(account.lamports(), test_info.metas[i].lamports());
assert_eq!(account.data().len(), test_info.datas[i].len());
assert_eq!(account.data(), test_info.datas[i]);
assert_eq!(
*account.owner(),
test_info.owners[test_info.metas[i].owner_offset().0 as usize],
);
}
assert_matches!(
hot_storage.get_account_shared_data(IndexOffset(NUM_ACCOUNTS as u32)),
Ok(None)
);
}
#[test]
fn test_hot_storage_writer_twice_on_same_path() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir
.path()
.join("test_hot_storage_writer_twice_on_same_path");
assert_matches!(HotStorageWriter::new(&path), Ok(_));
assert_matches!(HotStorageWriter::new(&path), Err(_));
}
#[test]
fn test_write_account_and_index_blocks() {
let account_data_sizes = &[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000, 2000, 3000, 4000, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
];
let accounts: Vec<_> = account_data_sizes
.iter()
.map(|size| create_test_account(*size))
.collect();
let account_refs: Vec<_> = accounts
.iter()
.map(|account| (&account.0.pubkey, &account.1))
.collect();
let storable_accounts = (Slot::MAX, &account_refs[..]);
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_write_account_and_index_blocks");
let stored_accounts_info = {
let mut writer = HotStorageWriter::new(&path).unwrap();
let stored_accounts_info = writer.write_accounts(&storable_accounts, 0).unwrap();
writer.flush().unwrap();
stored_accounts_info
};
let file = TieredReadableFile::new(&path).unwrap();
let hot_storage = HotStorageReader::new(file).unwrap();
let num_accounts = account_data_sizes.len();
for i in 0..num_accounts {
hot_storage
.get_stored_account_meta_callback(IndexOffset(i as u32), |stored_account_meta| {
storable_accounts.account_default_if_zero_lamport(i, |account| {
verify_test_account(
&stored_account_meta,
&account.to_account_shared_data(),
account.pubkey(),
);
});
})
.unwrap()
.unwrap();
}
assert_matches!(
hot_storage.get_stored_account_meta_callback(IndexOffset(num_accounts as u32), |_| {
panic!("unexpected");
}),
Ok(None)
);
for offset in stored_accounts_info.offsets {
hot_storage
.get_stored_account_meta_callback(
IndexOffset(offset as u32),
|stored_account_meta| {
storable_accounts.account_default_if_zero_lamport(offset, |account| {
verify_test_account(
&stored_account_meta,
&account.to_account_shared_data(),
account.pubkey(),
);
});
},
)
.unwrap()
.unwrap();
}
let mut i = 0;
hot_storage
.scan_accounts(|stored_meta| {
storable_accounts.account_default_if_zero_lamport(i, |account| {
verify_test_account(
&stored_meta,
&account.to_account_shared_data(),
account.pubkey(),
);
});
i += 1;
})
.unwrap();
let footer = hot_storage.footer();
let expected_size = footer.owners_block_offset as usize
+ std::mem::size_of::<Pubkey>() * footer.owner_count as usize
+ std::mem::size_of::<TieredStorageFooter>()
+ std::mem::size_of::<TieredStorageMagicNumber>();
assert!(!hot_storage.is_empty());
assert_eq!(expected_size, hot_storage.len());
}
}