use {
crate::{
account_storage::meta::StoredAccountMeta,
accounts_file::MatchAccountOwnerError,
accounts_hash::AccountHash,
tiered_storage::{
byte_block,
file::TieredStorageFile,
footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter},
index::{AccountOffset, IndexBlockFormat, IndexOffset},
meta::{AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta},
mmap_utils::{get_pod, get_slice},
owners::{OwnerOffset, OwnersBlockFormat},
readable::TieredReadableAccount,
TieredStorageError, TieredStorageFormat, TieredStorageResult,
},
},
bytemuck::{Pod, Zeroable},
memmap2::{Mmap, MmapOptions},
modular_bitfield::prelude::*,
solana_sdk::{pubkey::Pubkey, stake_history::Epoch},
std::{fs::OpenOptions, 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_PADDING: u8 = 7;
const MAX_HOT_OWNER_OFFSET: OwnerOffset = OwnerOffset((1 << 29) - 1);
pub(crate) const HOT_ACCOUNT_ALIGNMENT: usize = 8;
const MAX_HOT_ACCOUNT_OFFSET: usize = u32::MAX as usize * 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 account_hash<'a>(&self, account_block: &'a [u8]) -> Option<&'a AccountHash> {
self.flags()
.has_account_hash()
.then(|| {
let offset = self.optional_fields_offset(account_block)
+ AccountMetaOptionalFields::account_hash_offset(self.flags());
byte_block::read_pod::<AccountHash>(account_block, offset)
})
.flatten()
}
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(Debug)]
pub struct HotStorageReader {
mmap: Mmap,
footer: TieredStorageFooter,
}
impl HotStorageReader {
pub fn new_from_path(path: impl AsRef<Path>) -> TieredStorageResult<Self> {
let file = OpenOptions::new().read(true).open(path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let footer = *TieredStorageFooter::new_from_mmap(&mmap)?;
Ok(Self { mmap, footer })
}
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)
}
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_account(
&self,
index_offset: IndexOffset,
) -> TieredStorageResult<Option<(StoredAccountMeta<'_>, usize)>> {
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((
StoredAccountMeta::Hot(TieredReadableAccount {
meta,
address,
owner,
index: index_offset.0 as usize,
account_block,
}),
index_offset.0.saturating_add(1) as usize,
)))
}
}
#[derive(Debug)]
pub struct HotStorageWriter {
storage: TieredStorageFile,
}
impl HotStorageWriter {
pub fn new(file_path: impl AsRef<Path>) -> TieredStorageResult<Self> {
Ok(Self {
storage: TieredStorageFile::new_writable(file_path)?,
})
}
}
#[cfg(test)]
pub mod tests {
use {
super::*,
crate::tiered_storage::{
byte_block::ByteBlockWriter,
file::TieredStorageFile,
footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter, FOOTER_SIZE},
hot::{HotAccountMeta, HotStorageReader},
index::{AccountIndexWriterEntry, IndexBlockFormat, IndexOffset},
meta::{AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta},
owners::OwnersBlockFormat,
},
assert_matches::assert_matches,
memoffset::offset_of,
rand::{seq::SliceRandom, Rng},
solana_sdk::{account::ReadableAccount, hash::Hash, pubkey::Pubkey, stake_history::Epoch},
tempfile::TempDir,
};
#[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),
account_hash: Some(AccountHash(Hash::new_unique())),
};
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),
account_hash: Some(AccountHash(Hash::new_unique())),
};
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!(meta.flags().has_account_hash());
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);
assert_eq!(
*(meta.account_hash(account_block).unwrap()),
optional_fields.account_hash.unwrap()
);
}
#[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 file = TieredStorageFile::new_writable(&path).unwrap();
expected_footer.write_footer_block(&file).unwrap();
}
{
let hot_storage = HotStorageReader::new_from_path(&path).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 file = TieredStorageFile::new_writable(&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(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).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 file = TieredStorageFile::new_writable(&path).unwrap();
footer.write_footer_block(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).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,
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 file = TieredStorageFile::new_writable(&path).unwrap();
let cursor = footer
.index_block_format
.write_index_block(&file, &index_writer_entries)
.unwrap();
footer.owners_block_offset = cursor as u64;
footer.write_footer_block(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).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 file = TieredStorageFile::new_writable(&path).unwrap();
footer
.owners_block_format
.write_owners_block(&file, &addresses)
.unwrap();
footer.write_footer_block(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).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 file = TieredStorageFile::new_writable(&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;
footer
.owners_block_format
.write_owners_block(&file, &owner_addresses)
.unwrap();
footer.write_footer_block(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).unwrap();
let mut owner_candidates: Vec<_> = owner_addresses.iter().collect();
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_owners: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_UNMATCHED_OWNERS)
.collect();
let unmatched_candidates: Vec<_> = unmatched_owners.iter().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]
);
}
}
fn padding_bytes(data_len: usize) -> u8 {
((HOT_ACCOUNT_ALIGNMENT - (data_len % HOT_ACCOUNT_ALIGNMENT)) % HOT_ACCOUNT_ALIGNMENT) as u8
}
#[test]
fn test_hot_storage_get_account() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test_hot_storage_get_account");
let mut rng = rand::thread_rng();
const NUM_OWNERS: usize = 10;
let owners: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_OWNERS)
.collect();
const NUM_ACCOUNTS: usize = 20;
let account_datas: Vec<_> = (0..NUM_ACCOUNTS)
.map(|i| vec![i as u8; rng.gen_range(0..4096)])
.collect();
let account_metas: Vec<_> = (0..NUM_ACCOUNTS)
.map(|i| {
HotAccountMeta::new()
.with_lamports(rng.gen_range(0..u64::MAX))
.with_owner_offset(OwnerOffset(rng.gen_range(0..NUM_OWNERS) as u32))
.with_account_data_padding(padding_bytes(account_datas[i].len()))
})
.collect();
let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
.take(NUM_ACCOUNTS)
.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 file = TieredStorageFile::new_writable(&path).unwrap();
let mut current_offset = 0;
let padding_buffer = [0u8; HOT_ACCOUNT_ALIGNMENT];
let index_writer_entries: Vec<_> = account_metas
.iter()
.zip(account_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,
offset: HotAccountOffset::new(prev_offset).unwrap(),
}
})
.collect();
footer.index_block_offset = current_offset as u64;
current_offset += footer
.index_block_format
.write_index_block(&file, &index_writer_entries)
.unwrap();
footer.owners_block_offset = current_offset as u64;
footer
.owners_block_format
.write_owners_block(&file, &owners)
.unwrap();
footer.write_footer_block(&file).unwrap();
}
let hot_storage = HotStorageReader::new_from_path(&path).unwrap();
for i in 0..NUM_ACCOUNTS {
let (stored_meta, next) = hot_storage
.get_account(IndexOffset(i as u32))
.unwrap()
.unwrap();
assert_eq!(stored_meta.lamports(), account_metas[i].lamports());
assert_eq!(stored_meta.data().len(), account_datas[i].len());
assert_eq!(stored_meta.data(), account_datas[i]);
assert_eq!(
*stored_meta.owner(),
owners[account_metas[i].owner_offset().0 as usize]
);
assert_eq!(*stored_meta.pubkey(), addresses[i]);
assert_eq!(i + 1, next);
}
assert_matches!(
hot_storage.get_account(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(_));
}
}