solana_accounts_db/tiered_storage/
hot.rs

1//! The account meta and related structs for hot accounts.
2
3use {
4    crate::{
5        account_info::AccountInfo,
6        account_storage::meta::StoredAccountMeta,
7        accounts_file::{MatchAccountOwnerError, StoredAccountsInfo},
8        append_vec::{IndexInfo, IndexInfoInner},
9        tiered_storage::{
10            byte_block,
11            file::{TieredReadableFile, TieredWritableFile},
12            footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter},
13            index::{AccountIndexWriterEntry, AccountOffset, IndexBlockFormat, IndexOffset},
14            meta::{
15                AccountAddressRange, AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta,
16            },
17            mmap_utils::{get_pod, get_slice},
18            owners::{OwnerOffset, OwnersBlockFormat, OwnersTable},
19            StorableAccounts, TieredStorageError, TieredStorageFormat, TieredStorageResult,
20        },
21    },
22    bytemuck_derive::{Pod, Zeroable},
23    memmap2::{Mmap, MmapOptions},
24    modular_bitfield::prelude::*,
25    solana_pubkey::Pubkey,
26    solana_sdk::{
27        account::{AccountSharedData, ReadableAccount, WritableAccount},
28        rent_collector::RENT_EXEMPT_RENT_EPOCH,
29        stake_history::Epoch,
30    },
31    std::{io::Write, option::Option, path::Path},
32};
33
34pub const HOT_FORMAT: TieredStorageFormat = TieredStorageFormat {
35    meta_entry_size: std::mem::size_of::<HotAccountMeta>(),
36    account_meta_format: AccountMetaFormat::Hot,
37    owners_block_format: OwnersBlockFormat::AddressesOnly,
38    index_block_format: IndexBlockFormat::AddressesThenOffsets,
39    account_block_format: AccountBlockFormat::AlignedRaw,
40};
41
42/// A helper function that creates a new default footer for hot
43/// accounts storage.
44fn new_hot_footer() -> TieredStorageFooter {
45    TieredStorageFooter {
46        account_meta_format: HOT_FORMAT.account_meta_format,
47        account_meta_entry_size: HOT_FORMAT.meta_entry_size as u32,
48        account_block_format: HOT_FORMAT.account_block_format,
49        index_block_format: HOT_FORMAT.index_block_format,
50        owners_block_format: HOT_FORMAT.owners_block_format,
51        ..TieredStorageFooter::default()
52    }
53}
54
55/// The maximum allowed value for the owner index of a hot account.
56const MAX_HOT_OWNER_OFFSET: OwnerOffset = OwnerOffset((1 << 29) - 1);
57
58/// The byte alignment for hot accounts.  This alignment serves duo purposes.
59/// First, it allows hot accounts to be directly accessed when the underlying
60/// file is mmapped.  In addition, as all hot accounts are aligned, it allows
61/// each hot accounts file to handle more accounts with the same number of
62/// bytes in HotAccountOffset.
63pub(crate) const HOT_ACCOUNT_ALIGNMENT: usize = 8;
64
65/// The alignment for the blocks inside a hot accounts file.  A hot accounts
66/// file consists of accounts block, index block, owners block, and footer.
67/// This requirement allows the offset of each block properly aligned so
68/// that they can be readable under mmap.
69pub(crate) const HOT_BLOCK_ALIGNMENT: usize = 8;
70
71/// The maximum supported offset for hot accounts storage.
72const MAX_HOT_ACCOUNT_OFFSET: usize = u32::MAX as usize * HOT_ACCOUNT_ALIGNMENT;
73
74// returns the required number of padding
75fn padding_bytes(data_len: usize) -> u8 {
76    ((HOT_ACCOUNT_ALIGNMENT - (data_len % HOT_ACCOUNT_ALIGNMENT)) % HOT_ACCOUNT_ALIGNMENT) as u8
77}
78
79/// The maximum number of padding bytes used in a hot account entry.
80const MAX_HOT_PADDING: u8 = 7;
81
82/// The buffer that is used for padding.
83const PADDING_BUFFER: [u8; 8] = [0u8; HOT_ACCOUNT_ALIGNMENT];
84
85#[bitfield(bits = 32)]
86#[repr(C)]
87#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Pod, Zeroable)]
88struct HotMetaPackedFields {
89    /// A hot account entry consists of the following elements:
90    ///
91    /// * HotAccountMeta
92    /// * [u8] account data
93    /// * 0-7 bytes padding
94    /// * optional fields
95    ///
96    /// The following field records the number of padding bytes used
97    /// in its hot account entry.
98    padding: B3,
99    /// The index to the owner of a hot account inside an AccountsFile.
100    owner_offset: B29,
101}
102
103// Ensure there are no implicit padding bytes
104const _: () = assert!(std::mem::size_of::<HotMetaPackedFields>() == 4);
105
106/// The offset to access a hot account.
107#[repr(C)]
108#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Pod, Zeroable)]
109pub struct HotAccountOffset(u32);
110
111// Ensure there are no implicit padding bytes
112const _: () = assert!(std::mem::size_of::<HotAccountOffset>() == 4);
113
114impl AccountOffset for HotAccountOffset {}
115
116impl HotAccountOffset {
117    /// Creates a new AccountOffset instance
118    pub fn new(offset: usize) -> TieredStorageResult<Self> {
119        if offset > MAX_HOT_ACCOUNT_OFFSET {
120            return Err(TieredStorageError::OffsetOutOfBounds(
121                offset,
122                MAX_HOT_ACCOUNT_OFFSET,
123            ));
124        }
125
126        // Hot accounts are aligned based on HOT_ACCOUNT_ALIGNMENT.
127        if offset % HOT_ACCOUNT_ALIGNMENT != 0 {
128            return Err(TieredStorageError::OffsetAlignmentError(
129                offset,
130                HOT_ACCOUNT_ALIGNMENT,
131            ));
132        }
133
134        Ok(HotAccountOffset((offset / HOT_ACCOUNT_ALIGNMENT) as u32))
135    }
136
137    /// Returns the offset to the account.
138    fn offset(&self) -> usize {
139        self.0 as usize * HOT_ACCOUNT_ALIGNMENT
140    }
141}
142
143/// The storage and in-memory representation of the metadata entry for a
144/// hot account.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
146#[repr(C)]
147pub struct HotAccountMeta {
148    /// The balance of this account.
149    lamports: u64,
150    /// Stores important fields in a packed struct.
151    packed_fields: HotMetaPackedFields,
152    /// Stores boolean flags and existence of each optional field.
153    flags: AccountMetaFlags,
154}
155
156// Ensure there are no implicit padding bytes
157const _: () = assert!(std::mem::size_of::<HotAccountMeta>() == 8 + 4 + 4);
158
159impl TieredAccountMeta for HotAccountMeta {
160    /// Construct a HotAccountMeta instance.
161    fn new() -> Self {
162        HotAccountMeta {
163            lamports: 0,
164            packed_fields: HotMetaPackedFields::default(),
165            flags: AccountMetaFlags::new(),
166        }
167    }
168
169    /// A builder function that initializes lamports.
170    fn with_lamports(mut self, lamports: u64) -> Self {
171        self.lamports = lamports;
172        self
173    }
174
175    /// A builder function that initializes the number of padding bytes
176    /// for the account data associated with the current meta.
177    fn with_account_data_padding(mut self, padding: u8) -> Self {
178        if padding > MAX_HOT_PADDING {
179            panic!("padding exceeds MAX_HOT_PADDING");
180        }
181        self.packed_fields.set_padding(padding);
182        self
183    }
184
185    /// A builder function that initializes the owner's index.
186    fn with_owner_offset(mut self, owner_offset: OwnerOffset) -> Self {
187        if owner_offset > MAX_HOT_OWNER_OFFSET {
188            panic!("owner_offset exceeds MAX_HOT_OWNER_OFFSET");
189        }
190        self.packed_fields.set_owner_offset(owner_offset.0);
191        self
192    }
193
194    /// A builder function that initializes the account data size.
195    fn with_account_data_size(self, _account_data_size: u64) -> Self {
196        // Hot meta does not store its data size as it derives its data length
197        // by comparing the offsets of two consecutive account meta entries.
198        self
199    }
200
201    /// A builder function that initializes the AccountMetaFlags of the current
202    /// meta.
203    fn with_flags(mut self, flags: &AccountMetaFlags) -> Self {
204        self.flags = *flags;
205        self
206    }
207
208    /// Returns the balance of the lamports associated with the account.
209    fn lamports(&self) -> u64 {
210        self.lamports
211    }
212
213    /// Returns the number of padding bytes for the associated account data
214    fn account_data_padding(&self) -> u8 {
215        self.packed_fields.padding()
216    }
217
218    /// Returns the index to the accounts' owner in the current AccountsFile.
219    fn owner_offset(&self) -> OwnerOffset {
220        OwnerOffset(self.packed_fields.owner_offset())
221    }
222
223    /// Returns the AccountMetaFlags of the current meta.
224    fn flags(&self) -> &AccountMetaFlags {
225        &self.flags
226    }
227
228    /// Always returns false as HotAccountMeta does not support multiple
229    /// meta entries sharing the same account block.
230    fn supports_shared_account_block() -> bool {
231        false
232    }
233
234    /// Returns the epoch that this account will next owe rent by parsing
235    /// the specified account block.  None will be returned if this account
236    /// does not persist this optional field.
237    fn rent_epoch(&self, account_block: &[u8]) -> Option<Epoch> {
238        self.flags()
239            .has_rent_epoch()
240            .then(|| {
241                let offset = self.optional_fields_offset(account_block)
242                    + AccountMetaOptionalFields::rent_epoch_offset(self.flags());
243                byte_block::read_pod::<Epoch>(account_block, offset).copied()
244            })
245            .flatten()
246    }
247
248    /// Returns the epoch that this account will next owe rent by parsing
249    /// the specified account block.  RENT_EXEMPT_RENT_EPOCH will be returned
250    /// if the account is rent-exempt.
251    ///
252    /// For a zero-lamport account, Epoch::default() will be returned to
253    /// default states of an AccountSharedData.
254    fn final_rent_epoch(&self, account_block: &[u8]) -> Epoch {
255        self.rent_epoch(account_block)
256            .unwrap_or(if self.lamports() != 0 {
257                RENT_EXEMPT_RENT_EPOCH
258            } else {
259                // While there is no valid-values for any fields of a zero
260                // lamport account, here we return Epoch::default() to
261                // match the default states of AccountSharedData.  Otherwise,
262                // a hash mismatch will occur.
263                Epoch::default()
264            })
265    }
266
267    /// Returns the offset of the optional fields based on the specified account
268    /// block.
269    fn optional_fields_offset(&self, account_block: &[u8]) -> usize {
270        account_block
271            .len()
272            .saturating_sub(AccountMetaOptionalFields::size_from_flags(&self.flags))
273    }
274
275    /// Returns the length of the data associated to this account based on the
276    /// specified account block.
277    fn account_data_size(&self, account_block: &[u8]) -> usize {
278        self.optional_fields_offset(account_block)
279            .saturating_sub(self.account_data_padding() as usize)
280    }
281
282    /// Returns the data associated to this account based on the specified
283    /// account block.
284    fn account_data<'a>(&self, account_block: &'a [u8]) -> &'a [u8] {
285        &account_block[..self.account_data_size(account_block)]
286    }
287}
288
289/// The struct that offers read APIs for accessing a hot account.
290#[derive(PartialEq, Eq, Debug)]
291pub struct HotAccount<'accounts_file, M: TieredAccountMeta> {
292    /// TieredAccountMeta
293    pub meta: &'accounts_file M,
294    /// The address of the account
295    pub address: &'accounts_file Pubkey,
296    /// The address of the account owner
297    pub owner: &'accounts_file Pubkey,
298    /// The index for accessing the account inside its belonging AccountsFile
299    pub index: IndexOffset,
300    /// The account block that contains this account.  Note that this account
301    /// block may be shared with other accounts.
302    pub account_block: &'accounts_file [u8],
303}
304
305impl<'accounts_file, M: TieredAccountMeta> HotAccount<'accounts_file, M> {
306    /// Returns the address of this account.
307    pub fn address(&self) -> &'accounts_file Pubkey {
308        self.address
309    }
310
311    /// Returns the index to this account in its AccountsFile.
312    pub fn index(&self) -> IndexOffset {
313        self.index
314    }
315
316    /// Returns the data associated to this account.
317    pub fn data(&self) -> &'accounts_file [u8] {
318        self.meta.account_data(self.account_block)
319    }
320
321    /// Returns the approximate stored size of this account.
322    pub fn stored_size(&self) -> usize {
323        stored_size(self.meta.account_data_size(self.account_block))
324    }
325}
326
327impl<'accounts_file, M: TieredAccountMeta> ReadableAccount for HotAccount<'accounts_file, M> {
328    /// Returns the balance of the lamports of this account.
329    fn lamports(&self) -> u64 {
330        self.meta.lamports()
331    }
332
333    /// Returns the address of the owner of this account.
334    fn owner(&self) -> &'accounts_file Pubkey {
335        self.owner
336    }
337
338    /// Returns true if the data associated to this account is executable.
339    fn executable(&self) -> bool {
340        self.meta.flags().executable()
341    }
342
343    /// Returns the epoch that this account will next owe rent by parsing
344    /// the specified account block.  RENT_EXEMPT_RENT_EPOCH will be returned
345    /// if the account is rent-exempt.
346    ///
347    /// For a zero-lamport account, Epoch::default() will be returned to
348    /// default states of an AccountSharedData.
349    fn rent_epoch(&self) -> Epoch {
350        self.meta.final_rent_epoch(self.account_block)
351    }
352
353    /// Returns the data associated to this account.
354    fn data(&self) -> &'accounts_file [u8] {
355        self.data()
356    }
357}
358
359/// The reader to a hot accounts file.
360#[derive(Debug)]
361pub struct HotStorageReader {
362    mmap: Mmap,
363    footer: TieredStorageFooter,
364}
365
366impl HotStorageReader {
367    pub fn new(file: TieredReadableFile) -> TieredStorageResult<Self> {
368        let mmap = unsafe { MmapOptions::new().map(&file.0)? };
369        // Here we are copying the footer, as accessing any data in a
370        // TieredStorage instance requires accessing its Footer.
371        // This can help improve cache locality and reduce the overhead
372        // of indirection associated with memory-mapped accesses.
373        let footer = *TieredStorageFooter::new_from_mmap(&mmap)?;
374
375        Ok(Self { mmap, footer })
376    }
377
378    /// Returns the size of the underlying storage.
379    pub fn len(&self) -> usize {
380        self.mmap.len()
381    }
382
383    /// Returns whether the underlying storage is empty.
384    pub fn is_empty(&self) -> bool {
385        self.len() == 0
386    }
387
388    pub fn capacity(&self) -> u64 {
389        self.len() as u64
390    }
391
392    /// Returns the footer of the underlying tiered-storage accounts file.
393    pub fn footer(&self) -> &TieredStorageFooter {
394        &self.footer
395    }
396
397    /// Returns the number of files inside the underlying tiered-storage
398    /// accounts file.
399    pub fn num_accounts(&self) -> usize {
400        self.footer.account_entry_count as usize
401    }
402
403    /// Returns the account meta located at the specified offset.
404    fn get_account_meta_from_offset(
405        &self,
406        account_offset: HotAccountOffset,
407    ) -> TieredStorageResult<&HotAccountMeta> {
408        let offset = account_offset.offset();
409
410        assert!(
411            offset.saturating_add(std::mem::size_of::<HotAccountMeta>())
412                <= self.footer.index_block_offset as usize,
413            "reading HotAccountOffset ({}) would exceed accounts blocks offset boundary ({}).",
414            offset,
415            self.footer.index_block_offset,
416        );
417        let (meta, _) = get_pod::<HotAccountMeta>(&self.mmap, offset)?;
418        Ok(meta)
419    }
420
421    /// Returns the offset to the account given the specified index.
422    pub(super) fn get_account_offset(
423        &self,
424        index_offset: IndexOffset,
425    ) -> TieredStorageResult<HotAccountOffset> {
426        self.footer
427            .index_block_format
428            .get_account_offset::<HotAccountOffset>(&self.mmap, &self.footer, index_offset)
429    }
430
431    /// Returns the address of the account associated with the specified index.
432    fn get_account_address(&self, index: IndexOffset) -> TieredStorageResult<&Pubkey> {
433        self.footer
434            .index_block_format
435            .get_account_address(&self.mmap, &self.footer, index)
436    }
437
438    /// Returns the address of the account owner given the specified
439    /// owner_offset.
440    fn get_owner_address(&self, owner_offset: OwnerOffset) -> TieredStorageResult<&Pubkey> {
441        self.footer
442            .owners_block_format
443            .get_owner_address(&self.mmap, &self.footer, owner_offset)
444    }
445
446    /// Returns Ok(index_of_matching_owner) if the account owner at
447    /// `account_offset` is one of the pubkeys in `owners`.
448    ///
449    /// Returns Err(MatchAccountOwnerError::NoMatch) if the account has 0
450    /// lamports or the owner is not one of the pubkeys in `owners`.
451    ///
452    /// Returns Err(MatchAccountOwnerError::UnableToLoad) if there is any internal
453    /// error that causes the data unable to load, including `account_offset`
454    /// causes a data overrun.
455    pub fn account_matches_owners(
456        &self,
457        account_offset: HotAccountOffset,
458        owners: &[Pubkey],
459    ) -> Result<usize, MatchAccountOwnerError> {
460        let account_meta = self
461            .get_account_meta_from_offset(account_offset)
462            .map_err(|_| MatchAccountOwnerError::UnableToLoad)?;
463
464        if account_meta.lamports() == 0 {
465            Err(MatchAccountOwnerError::NoMatch)
466        } else {
467            let account_owner = self
468                .get_owner_address(account_meta.owner_offset())
469                .map_err(|_| MatchAccountOwnerError::UnableToLoad)?;
470
471            owners
472                .iter()
473                .position(|candidate| account_owner == candidate)
474                .ok_or(MatchAccountOwnerError::NoMatch)
475        }
476    }
477
478    /// Returns the size of the account block based on its account offset
479    /// and index offset.
480    ///
481    /// The account block size information is omitted in the hot accounts file
482    /// as it can be derived by comparing the offset of the next hot account
483    /// meta in the index block.
484    fn get_account_block_size(
485        &self,
486        account_offset: HotAccountOffset,
487        index_offset: IndexOffset,
488    ) -> TieredStorageResult<usize> {
489        // the offset that points to the hot account meta.
490        let account_meta_offset = account_offset.offset();
491
492        // Obtain the ending offset of the account block.  If the current
493        // account is the last account, then the ending offset is the
494        // index_block_offset.
495        let account_block_ending_offset =
496            if index_offset.0.saturating_add(1) == self.footer.account_entry_count {
497                self.footer.index_block_offset as usize
498            } else {
499                self.get_account_offset(IndexOffset(index_offset.0.saturating_add(1)))?
500                    .offset()
501            };
502
503        // With the ending offset, minus the starting offset (i.e.,
504        // the account meta offset) and the HotAccountMeta size, the reminder
505        // is the account block size (account data + optional fields).
506        Ok(account_block_ending_offset
507            .saturating_sub(account_meta_offset)
508            .saturating_sub(std::mem::size_of::<HotAccountMeta>()))
509    }
510
511    /// Returns the account block that contains the account associated with
512    /// the specified index given the offset to the account meta and its index.
513    fn get_account_block(
514        &self,
515        account_offset: HotAccountOffset,
516        index_offset: IndexOffset,
517    ) -> TieredStorageResult<&[u8]> {
518        let (data, _) = get_slice(
519            &self.mmap,
520            account_offset.offset() + std::mem::size_of::<HotAccountMeta>(),
521            self.get_account_block_size(account_offset, index_offset)?,
522        )?;
523
524        Ok(data)
525    }
526
527    /// calls `callback` with the account located at the specified index offset.
528    pub fn get_stored_account_meta_callback<Ret>(
529        &self,
530        index_offset: IndexOffset,
531        mut callback: impl for<'local> FnMut(StoredAccountMeta<'local>) -> Ret,
532    ) -> TieredStorageResult<Option<Ret>> {
533        if index_offset.0 >= self.footer.account_entry_count {
534            return Ok(None);
535        }
536
537        let account_offset = self.get_account_offset(index_offset)?;
538
539        let meta = self.get_account_meta_from_offset(account_offset)?;
540        let address = self.get_account_address(index_offset)?;
541        let owner = self.get_owner_address(meta.owner_offset())?;
542        let account_block = self.get_account_block(account_offset, index_offset)?;
543
544        Ok(Some(callback(StoredAccountMeta::Hot(HotAccount {
545            meta,
546            address,
547            owner,
548            index: index_offset,
549            account_block,
550        }))))
551    }
552
553    /// Returns the account located at the specified index offset.
554    pub fn get_account_shared_data(
555        &self,
556        index_offset: IndexOffset,
557    ) -> TieredStorageResult<Option<AccountSharedData>> {
558        if index_offset.0 >= self.footer.account_entry_count {
559            return Ok(None);
560        }
561
562        let account_offset = self.get_account_offset(index_offset)?;
563
564        let meta = self.get_account_meta_from_offset(account_offset)?;
565        let account_block = self.get_account_block(account_offset, index_offset)?;
566
567        let lamports = meta.lamports();
568        let data = meta.account_data(account_block).to_vec();
569        let owner = *self.get_owner_address(meta.owner_offset())?;
570        let executable = meta.flags().executable();
571        let rent_epoch = meta.final_rent_epoch(account_block);
572        Ok(Some(AccountSharedData::create(
573            lamports, data, owner, executable, rent_epoch,
574        )))
575    }
576
577    /// iterate over all pubkeys
578    pub fn scan_pubkeys(&self, mut callback: impl FnMut(&Pubkey)) -> TieredStorageResult<()> {
579        for i in 0..self.footer.account_entry_count {
580            let address = self.get_account_address(IndexOffset(i))?;
581            callback(address);
582        }
583        Ok(())
584    }
585
586    /// for each offset in `sorted_offsets`, return the account size
587    pub(crate) fn get_account_sizes(
588        &self,
589        sorted_offsets: &[usize],
590    ) -> TieredStorageResult<Vec<usize>> {
591        let mut result = Vec::with_capacity(sorted_offsets.len());
592        for &offset in sorted_offsets {
593            let index_offset = IndexOffset(AccountInfo::get_reduced_offset(offset));
594            let account_offset = self.get_account_offset(index_offset)?;
595            let meta = self.get_account_meta_from_offset(account_offset)?;
596            let account_block = self.get_account_block(account_offset, index_offset)?;
597            let data_len = meta.account_data_size(account_block);
598            result.push(stored_size(data_len));
599        }
600        Ok(result)
601    }
602
603    /// Iterate over all accounts and call `callback` with each account.
604    pub(crate) fn scan_accounts(
605        &self,
606        mut callback: impl for<'local> FnMut(StoredAccountMeta<'local>),
607    ) -> TieredStorageResult<()> {
608        for i in 0..self.footer.account_entry_count {
609            self.get_stored_account_meta_callback(IndexOffset(i), &mut callback)?;
610        }
611        Ok(())
612    }
613
614    /// iterate over all entries to put in index
615    pub(crate) fn scan_index(
616        &self,
617        mut callback: impl FnMut(IndexInfo),
618    ) -> TieredStorageResult<()> {
619        for i in 0..self.footer.account_entry_count {
620            let index_offset = IndexOffset(i);
621            let account_offset = self.get_account_offset(index_offset)?;
622
623            let meta = self.get_account_meta_from_offset(account_offset)?;
624            let pubkey = self.get_account_address(index_offset)?;
625            let lamports = meta.lamports();
626            let account_block = self.get_account_block(account_offset, index_offset)?;
627            let data_len = meta.account_data_size(account_block);
628            callback(IndexInfo {
629                index_info: {
630                    IndexInfoInner {
631                        pubkey: *pubkey,
632                        lamports,
633                        offset: AccountInfo::reduced_offset_to_offset(i),
634                        data_len: data_len as u64,
635                        executable: meta.flags().executable(),
636                        rent_epoch: meta.final_rent_epoch(account_block),
637                    }
638                },
639                stored_size_aligned: stored_size(data_len),
640            });
641        }
642        Ok(())
643    }
644
645    /// Returns a slice suitable for use when archiving hot storages
646    pub fn data_for_archive(&self) -> &[u8] {
647        self.mmap.as_ref()
648    }
649}
650
651/// return an approximation of the cost to store an account.
652/// Some fields like owner are shared across multiple accounts.
653fn stored_size(data_len: usize) -> usize {
654    data_len + std::mem::size_of::<Pubkey>()
655}
656
657fn write_optional_fields(
658    file: &mut TieredWritableFile,
659    opt_fields: &AccountMetaOptionalFields,
660) -> TieredStorageResult<usize> {
661    let mut size = 0;
662    if let Some(rent_epoch) = opt_fields.rent_epoch {
663        size += file.write_pod(&rent_epoch)?;
664    }
665
666    debug_assert_eq!(size, opt_fields.size());
667
668    Ok(size)
669}
670
671/// The writer that creates a hot accounts file.
672#[derive(Debug)]
673pub struct HotStorageWriter {
674    storage: TieredWritableFile,
675}
676
677impl HotStorageWriter {
678    /// Create a new HotStorageWriter with the specified path.
679    pub fn new(file_path: impl AsRef<Path>) -> TieredStorageResult<Self> {
680        Ok(Self {
681            storage: TieredWritableFile::new(file_path)?,
682        })
683    }
684
685    /// Persists an account with the specified information and returns
686    /// the stored size of the account.
687    fn write_account(
688        &mut self,
689        lamports: u64,
690        owner_offset: OwnerOffset,
691        account_data: &[u8],
692        executable: bool,
693        rent_epoch: Option<Epoch>,
694    ) -> TieredStorageResult<usize> {
695        let optional_fields = AccountMetaOptionalFields { rent_epoch };
696
697        let mut flags = AccountMetaFlags::new_from(&optional_fields);
698        flags.set_executable(executable);
699
700        let padding_len = padding_bytes(account_data.len());
701        let meta = HotAccountMeta::new()
702            .with_lamports(lamports)
703            .with_owner_offset(owner_offset)
704            .with_account_data_size(account_data.len() as u64)
705            .with_account_data_padding(padding_len)
706            .with_flags(&flags);
707
708        let mut stored_size = 0;
709
710        stored_size += self.storage.write_pod(&meta)?;
711        stored_size += self.storage.write_bytes(account_data)?;
712        stored_size += self
713            .storage
714            .write_bytes(&PADDING_BUFFER[0..(padding_len as usize)])?;
715        stored_size += write_optional_fields(&mut self.storage, &optional_fields)?;
716
717        Ok(stored_size)
718    }
719
720    /// Persists `accounts` into the underlying hot accounts file associated
721    /// with this HotStorageWriter.  The first `skip` number of accounts are
722    /// *not* persisted.
723    pub fn write_accounts<'a>(
724        &mut self,
725        accounts: &impl StorableAccounts<'a>,
726        skip: usize,
727    ) -> TieredStorageResult<StoredAccountsInfo> {
728        let mut footer = new_hot_footer();
729        let mut index = vec![];
730        let mut owners_table = OwnersTable::default();
731        let mut cursor = 0;
732        let mut address_range = AccountAddressRange::default();
733
734        let len = accounts.len();
735        let total_input_accounts = len.saturating_sub(skip);
736        let mut offsets = Vec::with_capacity(total_input_accounts);
737
738        // writing accounts blocks
739        for i in skip..len {
740            accounts.account_default_if_zero_lamport::<TieredStorageResult<()>>(i, |account| {
741                let index_entry = AccountIndexWriterEntry {
742                    address: *account.pubkey(),
743                    offset: HotAccountOffset::new(cursor)?,
744                };
745                address_range.update(account.pubkey());
746
747                // Obtain necessary fields from the account, or default fields
748                // for a zero-lamport account in the None case.
749                let (lamports, owner, data, executable, rent_epoch) = {
750                    (
751                        account.lamports(),
752                        account.owner(),
753                        account.data(),
754                        account.executable(),
755                        // only persist rent_epoch for those rent-paying accounts
756                        (account.rent_epoch() != RENT_EXEMPT_RENT_EPOCH)
757                            .then_some(account.rent_epoch()),
758                    )
759                };
760                let owner_offset = owners_table.insert(owner);
761                cursor +=
762                    self.write_account(lamports, owner_offset, data, executable, rent_epoch)?;
763
764                // Here we pass the IndexOffset as the get_account() API
765                // takes IndexOffset.  Given the account address is also
766                // maintained outside the TieredStorage, a potential optimization
767                // is to store AccountOffset instead, which can further save
768                // one jump from the index block to the accounts block.
769                offsets.push(index.len());
770                index.push(index_entry);
771                Ok(())
772            })?;
773        }
774        footer.account_entry_count = total_input_accounts as u32;
775
776        // writing index block
777        // expect the offset of each block aligned.
778        assert!(cursor % HOT_BLOCK_ALIGNMENT == 0);
779        footer.index_block_offset = cursor as u64;
780        cursor += footer
781            .index_block_format
782            .write_index_block(&mut self.storage, &index)?;
783        if cursor % HOT_BLOCK_ALIGNMENT != 0 {
784            // In case it is not yet aligned, it is due to the fact that
785            // the index block has an odd number of entries.  In such case,
786            // we expect the amount off is equal to 4.
787            assert_eq!(cursor % HOT_BLOCK_ALIGNMENT, 4);
788            cursor += self.storage.write_pod(&0u32)?;
789        }
790
791        // writing owners block
792        assert!(cursor % HOT_BLOCK_ALIGNMENT == 0);
793        footer.owners_block_offset = cursor as u64;
794        footer.owner_count = owners_table.len() as u32;
795        cursor += footer
796            .owners_block_format
797            .write_owners_block(&mut self.storage, &owners_table)?;
798
799        // writing footer
800        footer.min_account_address = address_range.min;
801        footer.max_account_address = address_range.max;
802        cursor += footer.write_footer_block(&mut self.storage)?;
803
804        Ok(StoredAccountsInfo {
805            offsets,
806            size: cursor,
807        })
808    }
809
810    /// Flushes any buffered data to the file
811    pub fn flush(&mut self) -> TieredStorageResult<()> {
812        self.storage
813            .0
814            .flush()
815            .map_err(TieredStorageError::FlushHotWriter)
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use {
822        super::*,
823        crate::tiered_storage::{
824            byte_block::ByteBlockWriter,
825            file::{TieredStorageMagicNumber, TieredWritableFile},
826            footer::{AccountBlockFormat, AccountMetaFormat, TieredStorageFooter, FOOTER_SIZE},
827            hot::{HotAccountMeta, HotStorageReader},
828            index::{AccountIndexWriterEntry, IndexBlockFormat, IndexOffset},
829            meta::{AccountMetaFlags, AccountMetaOptionalFields, TieredAccountMeta},
830            owners::{OwnersBlockFormat, OwnersTable},
831            test_utils::{create_test_account, verify_test_account},
832        },
833        assert_matches::assert_matches,
834        memoffset::offset_of,
835        rand::{seq::SliceRandom, Rng},
836        solana_pubkey::Pubkey,
837        solana_sdk::{
838            account::ReadableAccount, hash::Hash, slot_history::Slot, stake_history::Epoch,
839        },
840        std::path::PathBuf,
841        tempfile::TempDir,
842    };
843
844    /// info created to write a hot storage file for tests
845    struct WriteTestFileInfo {
846        /// metadata for the accounts
847        metas: Vec<HotAccountMeta>,
848        /// addresses for the accounts
849        addresses: Vec<Pubkey>,
850        /// owners for the accounts
851        owners: Vec<Pubkey>,
852        /// data for the accounts
853        datas: Vec<Vec<u8>>,
854        /// path to the hot storage file that was written
855        file_path: PathBuf,
856        /// temp directory where the hot storage file was written
857        temp_dir: TempDir,
858    }
859
860    /// Writes a hot storage file for tests
861    fn write_test_file(num_accounts: usize, num_owners: usize) -> WriteTestFileInfo {
862        // Generate a new temp path that is guaranteed to NOT already have a file.
863        let temp_dir = TempDir::new().unwrap();
864        let file_path = temp_dir.path().join("test");
865
866        let mut rng = rand::thread_rng();
867
868        // create owners
869        let owners: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
870            .take(num_owners)
871            .collect();
872
873        // create account addresses
874        let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
875            .take(num_accounts)
876            .collect();
877
878        // create account data
879        let datas: Vec<_> = (0..num_accounts)
880            .map(|i| vec![i as u8; rng.gen_range(0..4096)])
881            .collect();
882
883        // create account metas that link to its data and owner
884        let metas: Vec<_> = (0..num_accounts)
885            .map(|i| {
886                HotAccountMeta::new()
887                    .with_lamports(rng.gen())
888                    .with_owner_offset(OwnerOffset(rng.gen_range(0..num_owners) as u32))
889                    .with_account_data_padding(padding_bytes(datas[i].len()))
890            })
891            .collect();
892
893        let mut footer = TieredStorageFooter {
894            account_meta_format: AccountMetaFormat::Hot,
895            account_entry_count: num_accounts as u32,
896            owner_count: num_owners as u32,
897            ..TieredStorageFooter::default()
898        };
899
900        // write the hot storage file
901        {
902            let mut file = TieredWritableFile::new(&file_path).unwrap();
903            let mut current_offset = 0;
904
905            // write accounts blocks
906            let padding_buffer = [0u8; HOT_ACCOUNT_ALIGNMENT];
907            let index_writer_entries: Vec<_> = metas
908                .iter()
909                .zip(datas.iter())
910                .zip(addresses.iter())
911                .map(|((meta, data), address)| {
912                    let prev_offset = current_offset;
913                    current_offset += file.write_pod(meta).unwrap();
914                    current_offset += file.write_bytes(data).unwrap();
915                    current_offset += file
916                        .write_bytes(&padding_buffer[0..padding_bytes(data.len()) as usize])
917                        .unwrap();
918                    AccountIndexWriterEntry {
919                        address: *address,
920                        offset: HotAccountOffset::new(prev_offset).unwrap(),
921                    }
922                })
923                .collect();
924
925            // write index blocks
926            footer.index_block_offset = current_offset as u64;
927            current_offset += footer
928                .index_block_format
929                .write_index_block(&mut file, &index_writer_entries)
930                .unwrap();
931
932            // write owners block
933            footer.owners_block_offset = current_offset as u64;
934            let mut owners_table = OwnersTable::default();
935            owners.iter().for_each(|owner_address| {
936                owners_table.insert(owner_address);
937            });
938            footer
939                .owners_block_format
940                .write_owners_block(&mut file, &owners_table)
941                .unwrap();
942
943            footer.write_footer_block(&mut file).unwrap();
944        }
945
946        WriteTestFileInfo {
947            metas,
948            addresses,
949            owners,
950            datas,
951            file_path,
952            temp_dir,
953        }
954    }
955
956    #[test]
957    fn test_hot_account_meta_layout() {
958        assert_eq!(offset_of!(HotAccountMeta, lamports), 0x00);
959        assert_eq!(offset_of!(HotAccountMeta, packed_fields), 0x08);
960        assert_eq!(offset_of!(HotAccountMeta, flags), 0x0C);
961        assert_eq!(std::mem::size_of::<HotAccountMeta>(), 16);
962    }
963
964    #[test]
965    fn test_packed_fields() {
966        const TEST_PADDING: u8 = 7;
967        const TEST_OWNER_OFFSET: u32 = 0x1fff_ef98;
968        let mut packed_fields = HotMetaPackedFields::default();
969        packed_fields.set_padding(TEST_PADDING);
970        packed_fields.set_owner_offset(TEST_OWNER_OFFSET);
971        assert_eq!(packed_fields.padding(), TEST_PADDING);
972        assert_eq!(packed_fields.owner_offset(), TEST_OWNER_OFFSET);
973    }
974
975    #[test]
976    fn test_packed_fields_max_values() {
977        let mut packed_fields = HotMetaPackedFields::default();
978        packed_fields.set_padding(MAX_HOT_PADDING);
979        packed_fields.set_owner_offset(MAX_HOT_OWNER_OFFSET.0);
980        assert_eq!(packed_fields.padding(), MAX_HOT_PADDING);
981        assert_eq!(packed_fields.owner_offset(), MAX_HOT_OWNER_OFFSET.0);
982    }
983
984    #[test]
985    fn test_hot_meta_max_values() {
986        let meta = HotAccountMeta::new()
987            .with_account_data_padding(MAX_HOT_PADDING)
988            .with_owner_offset(MAX_HOT_OWNER_OFFSET);
989
990        assert_eq!(meta.account_data_padding(), MAX_HOT_PADDING);
991        assert_eq!(meta.owner_offset(), MAX_HOT_OWNER_OFFSET);
992    }
993
994    #[test]
995    fn test_max_hot_account_offset() {
996        assert_matches!(HotAccountOffset::new(0), Ok(_));
997        assert_matches!(HotAccountOffset::new(MAX_HOT_ACCOUNT_OFFSET), Ok(_));
998    }
999
1000    #[test]
1001    fn test_max_hot_account_offset_out_of_bounds() {
1002        assert_matches!(
1003            HotAccountOffset::new(MAX_HOT_ACCOUNT_OFFSET + HOT_ACCOUNT_ALIGNMENT),
1004            Err(TieredStorageError::OffsetOutOfBounds(_, _))
1005        );
1006    }
1007
1008    #[test]
1009    fn test_max_hot_account_offset_alignment_error() {
1010        assert_matches!(
1011            HotAccountOffset::new(HOT_ACCOUNT_ALIGNMENT - 1),
1012            Err(TieredStorageError::OffsetAlignmentError(_, _))
1013        );
1014    }
1015
1016    #[test]
1017    #[should_panic(expected = "padding exceeds MAX_HOT_PADDING")]
1018    fn test_hot_meta_padding_exceeds_limit() {
1019        HotAccountMeta::new().with_account_data_padding(MAX_HOT_PADDING + 1);
1020    }
1021
1022    #[test]
1023    #[should_panic(expected = "owner_offset exceeds MAX_HOT_OWNER_OFFSET")]
1024    fn test_hot_meta_owner_offset_exceeds_limit() {
1025        HotAccountMeta::new().with_owner_offset(OwnerOffset(MAX_HOT_OWNER_OFFSET.0 + 1));
1026    }
1027
1028    #[test]
1029    fn test_hot_account_meta() {
1030        const TEST_LAMPORTS: u64 = 2314232137;
1031        const TEST_PADDING: u8 = 5;
1032        const TEST_OWNER_OFFSET: OwnerOffset = OwnerOffset(0x1fef_1234);
1033        const TEST_RENT_EPOCH: Epoch = 7;
1034
1035        let optional_fields = AccountMetaOptionalFields {
1036            rent_epoch: Some(TEST_RENT_EPOCH),
1037        };
1038
1039        let flags = AccountMetaFlags::new_from(&optional_fields);
1040        let meta = HotAccountMeta::new()
1041            .with_lamports(TEST_LAMPORTS)
1042            .with_account_data_padding(TEST_PADDING)
1043            .with_owner_offset(TEST_OWNER_OFFSET)
1044            .with_flags(&flags);
1045
1046        assert_eq!(meta.lamports(), TEST_LAMPORTS);
1047        assert_eq!(meta.account_data_padding(), TEST_PADDING);
1048        assert_eq!(meta.owner_offset(), TEST_OWNER_OFFSET);
1049        assert_eq!(*meta.flags(), flags);
1050    }
1051
1052    #[test]
1053    fn test_hot_account_meta_full() {
1054        let account_data = [11u8; 83];
1055        let padding = [0u8; 5];
1056
1057        const TEST_LAMPORT: u64 = 2314232137;
1058        const OWNER_OFFSET: u32 = 0x1fef_1234;
1059        const TEST_RENT_EPOCH: Epoch = 7;
1060
1061        let optional_fields = AccountMetaOptionalFields {
1062            rent_epoch: Some(TEST_RENT_EPOCH),
1063        };
1064
1065        let flags = AccountMetaFlags::new_from(&optional_fields);
1066        let expected_meta = HotAccountMeta::new()
1067            .with_lamports(TEST_LAMPORT)
1068            .with_account_data_padding(padding.len().try_into().unwrap())
1069            .with_owner_offset(OwnerOffset(OWNER_OFFSET))
1070            .with_flags(&flags);
1071
1072        let mut writer = ByteBlockWriter::new(AccountBlockFormat::AlignedRaw);
1073        writer.write_pod(&expected_meta).unwrap();
1074        // SAFETY: These values are POD, so they are safe to write.
1075        unsafe {
1076            writer.write_type(&account_data).unwrap();
1077            writer.write_type(&padding).unwrap();
1078        }
1079        writer.write_optional_fields(&optional_fields).unwrap();
1080        let buffer = writer.finish().unwrap();
1081
1082        let meta = byte_block::read_pod::<HotAccountMeta>(&buffer, 0).unwrap();
1083        assert_eq!(expected_meta, *meta);
1084        assert!(meta.flags().has_rent_epoch());
1085        assert_eq!(meta.account_data_padding() as usize, padding.len());
1086
1087        let account_block = &buffer[std::mem::size_of::<HotAccountMeta>()..];
1088        assert_eq!(
1089            meta.optional_fields_offset(account_block),
1090            account_block
1091                .len()
1092                .saturating_sub(AccountMetaOptionalFields::size_from_flags(&flags))
1093        );
1094        assert_eq!(account_data.len(), meta.account_data_size(account_block));
1095        assert_eq!(account_data, meta.account_data(account_block));
1096        assert_eq!(meta.rent_epoch(account_block), optional_fields.rent_epoch);
1097    }
1098
1099    #[test]
1100    fn test_hot_storage_footer() {
1101        // Generate a new temp path that is guaranteed to NOT already have a file.
1102        let temp_dir = TempDir::new().unwrap();
1103        let path = temp_dir.path().join("test_hot_storage_footer");
1104        let expected_footer = TieredStorageFooter {
1105            account_meta_format: AccountMetaFormat::Hot,
1106            owners_block_format: OwnersBlockFormat::AddressesOnly,
1107            index_block_format: IndexBlockFormat::AddressesThenOffsets,
1108            account_block_format: AccountBlockFormat::AlignedRaw,
1109            account_entry_count: 300,
1110            account_meta_entry_size: 16,
1111            account_block_size: 4096,
1112            owner_count: 250,
1113            owner_entry_size: 32,
1114            index_block_offset: 1069600,
1115            owners_block_offset: 1081200,
1116            hash: Hash::new_unique(),
1117            min_account_address: Pubkey::default(),
1118            max_account_address: Pubkey::new_unique(),
1119            footer_size: FOOTER_SIZE as u64,
1120            format_version: 1,
1121        };
1122
1123        {
1124            let mut file = TieredWritableFile::new(&path).unwrap();
1125            expected_footer.write_footer_block(&mut file).unwrap();
1126        }
1127
1128        // Reopen the same storage, and expect the persisted footer is
1129        // the same as what we have written.
1130        {
1131            let file = TieredReadableFile::new(&path).unwrap();
1132            let hot_storage = HotStorageReader::new(file).unwrap();
1133            assert_eq!(expected_footer, *hot_storage.footer());
1134        }
1135    }
1136
1137    #[test]
1138    fn test_hot_storage_get_account_meta_from_offset() {
1139        // Generate a new temp path that is guaranteed to NOT already have a file.
1140        let temp_dir = TempDir::new().unwrap();
1141        let path = temp_dir.path().join("test_hot_storage_footer");
1142
1143        const NUM_ACCOUNTS: u32 = 10;
1144        let mut rng = rand::thread_rng();
1145
1146        let hot_account_metas: Vec<_> = (0..NUM_ACCOUNTS)
1147            .map(|_| {
1148                HotAccountMeta::new()
1149                    .with_lamports(rng.gen_range(0..u64::MAX))
1150                    .with_owner_offset(OwnerOffset(rng.gen_range(0..NUM_ACCOUNTS)))
1151            })
1152            .collect();
1153
1154        let account_offsets: Vec<_>;
1155        let mut footer = TieredStorageFooter {
1156            account_meta_format: AccountMetaFormat::Hot,
1157            account_entry_count: NUM_ACCOUNTS,
1158            ..TieredStorageFooter::default()
1159        };
1160        {
1161            let mut file = TieredWritableFile::new(&path).unwrap();
1162            let mut current_offset = 0;
1163
1164            account_offsets = hot_account_metas
1165                .iter()
1166                .map(|meta| {
1167                    let prev_offset = current_offset;
1168                    current_offset += file.write_pod(meta).unwrap();
1169                    HotAccountOffset::new(prev_offset).unwrap()
1170                })
1171                .collect();
1172            // while the test only focuses on account metas, writing a footer
1173            // here is necessary to make it a valid tiered-storage file.
1174            footer.index_block_offset = current_offset as u64;
1175            footer.write_footer_block(&mut file).unwrap();
1176        }
1177
1178        let file = TieredReadableFile::new(&path).unwrap();
1179        let hot_storage = HotStorageReader::new(file).unwrap();
1180
1181        for (offset, expected_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
1182            let meta = hot_storage.get_account_meta_from_offset(*offset).unwrap();
1183            assert_eq!(meta, expected_meta);
1184        }
1185
1186        assert_eq!(&footer, hot_storage.footer());
1187    }
1188
1189    #[test]
1190    #[should_panic(expected = "would exceed accounts blocks offset boundary")]
1191    fn test_get_acount_meta_from_offset_out_of_bounds() {
1192        // Generate a new temp path that is guaranteed to NOT already have a file.
1193        let temp_dir = TempDir::new().unwrap();
1194        let path = temp_dir
1195            .path()
1196            .join("test_get_acount_meta_from_offset_out_of_bounds");
1197
1198        let footer = TieredStorageFooter {
1199            account_meta_format: AccountMetaFormat::Hot,
1200            index_block_offset: 160,
1201            ..TieredStorageFooter::default()
1202        };
1203
1204        {
1205            let mut file = TieredWritableFile::new(&path).unwrap();
1206            footer.write_footer_block(&mut file).unwrap();
1207        }
1208
1209        let file = TieredReadableFile::new(&path).unwrap();
1210        let hot_storage = HotStorageReader::new(file).unwrap();
1211        let offset = HotAccountOffset::new(footer.index_block_offset as usize).unwrap();
1212        // Read from index_block_offset, which offset doesn't belong to
1213        // account blocks.  Expect assert failure here
1214        hot_storage.get_account_meta_from_offset(offset).unwrap();
1215    }
1216
1217    #[test]
1218    fn test_hot_storage_get_account_offset_and_address() {
1219        // Generate a new temp path that is guaranteed to NOT already have a file.
1220        let temp_dir = TempDir::new().unwrap();
1221        let path = temp_dir
1222            .path()
1223            .join("test_hot_storage_get_account_offset_and_address");
1224        const NUM_ACCOUNTS: u32 = 10;
1225        let mut rng = rand::thread_rng();
1226
1227        let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
1228            .take(NUM_ACCOUNTS as usize)
1229            .collect();
1230
1231        let index_writer_entries: Vec<_> = addresses
1232            .iter()
1233            .map(|address| AccountIndexWriterEntry {
1234                address: *address,
1235                offset: HotAccountOffset::new(
1236                    rng.gen_range(0..u32::MAX) as usize * HOT_ACCOUNT_ALIGNMENT,
1237                )
1238                .unwrap(),
1239            })
1240            .collect();
1241
1242        let mut footer = TieredStorageFooter {
1243            account_meta_format: AccountMetaFormat::Hot,
1244            account_entry_count: NUM_ACCOUNTS,
1245            // Set index_block_offset to 0 as we didn't write any account
1246            // meta/data in this test
1247            index_block_offset: 0,
1248            ..TieredStorageFooter::default()
1249        };
1250        {
1251            let mut file = TieredWritableFile::new(&path).unwrap();
1252
1253            let cursor = footer
1254                .index_block_format
1255                .write_index_block(&mut file, &index_writer_entries)
1256                .unwrap();
1257            footer.owners_block_offset = cursor as u64;
1258            footer.write_footer_block(&mut file).unwrap();
1259        }
1260
1261        let file = TieredReadableFile::new(&path).unwrap();
1262        let hot_storage = HotStorageReader::new(file).unwrap();
1263        for (i, index_writer_entry) in index_writer_entries.iter().enumerate() {
1264            let account_offset = hot_storage
1265                .get_account_offset(IndexOffset(i as u32))
1266                .unwrap();
1267            assert_eq!(account_offset, index_writer_entry.offset);
1268
1269            let account_address = hot_storage
1270                .get_account_address(IndexOffset(i as u32))
1271                .unwrap();
1272            assert_eq!(account_address, &index_writer_entry.address);
1273        }
1274    }
1275
1276    #[test]
1277    fn test_hot_storage_get_owner_address() {
1278        // Generate a new temp path that is guaranteed to NOT already have a file.
1279        let temp_dir = TempDir::new().unwrap();
1280        let path = temp_dir.path().join("test_hot_storage_get_owner_address");
1281        const NUM_OWNERS: usize = 10;
1282
1283        let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
1284            .take(NUM_OWNERS)
1285            .collect();
1286
1287        let footer = TieredStorageFooter {
1288            account_meta_format: AccountMetaFormat::Hot,
1289            // meta/data nor index block in this test
1290            owners_block_offset: 0,
1291            ..TieredStorageFooter::default()
1292        };
1293
1294        {
1295            let mut file = TieredWritableFile::new(&path).unwrap();
1296
1297            let mut owners_table = OwnersTable::default();
1298            addresses.iter().for_each(|owner_address| {
1299                owners_table.insert(owner_address);
1300            });
1301            footer
1302                .owners_block_format
1303                .write_owners_block(&mut file, &owners_table)
1304                .unwrap();
1305
1306            // while the test only focuses on account metas, writing a footer
1307            // here is necessary to make it a valid tiered-storage file.
1308            footer.write_footer_block(&mut file).unwrap();
1309        }
1310
1311        let file = TieredReadableFile::new(&path).unwrap();
1312        let hot_storage = HotStorageReader::new(file).unwrap();
1313        for (i, address) in addresses.iter().enumerate() {
1314            assert_eq!(
1315                hot_storage
1316                    .get_owner_address(OwnerOffset(i as u32))
1317                    .unwrap(),
1318                address,
1319            );
1320        }
1321    }
1322
1323    #[test]
1324    fn test_account_matches_owners() {
1325        // Generate a new temp path that is guaranteed to NOT already have a file.
1326        let temp_dir = TempDir::new().unwrap();
1327        let path = temp_dir.path().join("test_hot_storage_get_owner_address");
1328        const NUM_OWNERS: u32 = 10;
1329
1330        let owner_addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
1331            .take(NUM_OWNERS as usize)
1332            .collect();
1333
1334        const NUM_ACCOUNTS: u32 = 30;
1335        let mut rng = rand::thread_rng();
1336
1337        let hot_account_metas: Vec<_> = std::iter::repeat_with({
1338            || {
1339                HotAccountMeta::new()
1340                    .with_lamports(rng.gen_range(1..u64::MAX))
1341                    .with_owner_offset(OwnerOffset(rng.gen_range(0..NUM_OWNERS)))
1342            }
1343        })
1344        .take(NUM_ACCOUNTS as usize)
1345        .collect();
1346        let mut footer = TieredStorageFooter {
1347            account_meta_format: AccountMetaFormat::Hot,
1348            account_entry_count: NUM_ACCOUNTS,
1349            owner_count: NUM_OWNERS,
1350            ..TieredStorageFooter::default()
1351        };
1352        let account_offsets: Vec<_>;
1353
1354        {
1355            let mut file = TieredWritableFile::new(&path).unwrap();
1356            let mut current_offset = 0;
1357
1358            account_offsets = hot_account_metas
1359                .iter()
1360                .map(|meta| {
1361                    let prev_offset = current_offset;
1362                    current_offset += file.write_pod(meta).unwrap();
1363                    HotAccountOffset::new(prev_offset).unwrap()
1364                })
1365                .collect();
1366            footer.index_block_offset = current_offset as u64;
1367            // Typically, the owners block is stored after index block, but
1368            // since we don't write index block in this test, so we have
1369            // the owners_block_offset set to the end of the accounts blocks.
1370            footer.owners_block_offset = footer.index_block_offset;
1371
1372            let mut owners_table = OwnersTable::default();
1373            owner_addresses.iter().for_each(|owner_address| {
1374                owners_table.insert(owner_address);
1375            });
1376            footer
1377                .owners_block_format
1378                .write_owners_block(&mut file, &owners_table)
1379                .unwrap();
1380
1381            // while the test only focuses on account metas, writing a footer
1382            // here is necessary to make it a valid tiered-storage file.
1383            footer.write_footer_block(&mut file).unwrap();
1384        }
1385
1386        let file = TieredReadableFile::new(&path).unwrap();
1387        let hot_storage = HotStorageReader::new(file).unwrap();
1388
1389        // First, verify whether we can find the expected owners.
1390        let mut owner_candidates = owner_addresses.clone();
1391        owner_candidates.shuffle(&mut rng);
1392
1393        for (account_offset, account_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
1394            let index = hot_storage
1395                .account_matches_owners(*account_offset, &owner_candidates)
1396                .unwrap();
1397            assert_eq!(
1398                owner_candidates[index],
1399                owner_addresses[account_meta.owner_offset().0 as usize]
1400            );
1401        }
1402
1403        // Second, verify the MatchAccountOwnerError::NoMatch case
1404        const NUM_UNMATCHED_OWNERS: usize = 20;
1405        let unmatched_candidates: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
1406            .take(NUM_UNMATCHED_OWNERS)
1407            .collect();
1408
1409        for account_offset in account_offsets.iter() {
1410            assert_eq!(
1411                hot_storage.account_matches_owners(*account_offset, &unmatched_candidates),
1412                Err(MatchAccountOwnerError::NoMatch)
1413            );
1414        }
1415
1416        // Thirdly, we mixed two candidates and make sure we still find the
1417        // matched owner.
1418        owner_candidates.extend(unmatched_candidates);
1419        owner_candidates.shuffle(&mut rng);
1420
1421        for (account_offset, account_meta) in account_offsets.iter().zip(hot_account_metas.iter()) {
1422            let index = hot_storage
1423                .account_matches_owners(*account_offset, &owner_candidates)
1424                .unwrap();
1425            assert_eq!(
1426                owner_candidates[index],
1427                owner_addresses[account_meta.owner_offset().0 as usize]
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn test_get_stored_account_meta() {
1434        const NUM_ACCOUNTS: usize = 20;
1435        const NUM_OWNERS: usize = 10;
1436        let test_info = write_test_file(NUM_ACCOUNTS, NUM_OWNERS);
1437
1438        let file = TieredReadableFile::new(&test_info.file_path).unwrap();
1439        let hot_storage = HotStorageReader::new(file).unwrap();
1440
1441        for i in 0..NUM_ACCOUNTS {
1442            hot_storage
1443                .get_stored_account_meta_callback(IndexOffset(i as u32), |stored_account_meta| {
1444                    assert_eq!(
1445                        stored_account_meta.lamports(),
1446                        test_info.metas[i].lamports()
1447                    );
1448                    assert_eq!(stored_account_meta.data().len(), test_info.datas[i].len());
1449                    assert_eq!(stored_account_meta.data(), test_info.datas[i]);
1450                    assert_eq!(
1451                        *stored_account_meta.owner(),
1452                        test_info.owners[test_info.metas[i].owner_offset().0 as usize]
1453                    );
1454                    assert_eq!(*stored_account_meta.pubkey(), test_info.addresses[i]);
1455                })
1456                .unwrap()
1457                .unwrap();
1458        }
1459        // Make sure it returns None on NUM_ACCOUNTS to allow termination on
1460        // while loop in actual accounts-db read case.
1461        assert_matches!(
1462            hot_storage.get_stored_account_meta_callback(IndexOffset(NUM_ACCOUNTS as u32), |_| {
1463                panic!("unexpected");
1464            }),
1465            Ok(None)
1466        );
1467    }
1468
1469    #[test]
1470    fn test_get_account_shared_data() {
1471        const NUM_ACCOUNTS: usize = 20;
1472        const NUM_OWNERS: usize = 10;
1473        let test_info = write_test_file(NUM_ACCOUNTS, NUM_OWNERS);
1474
1475        let file = TieredReadableFile::new(&test_info.file_path).unwrap();
1476        let hot_storage = HotStorageReader::new(file).unwrap();
1477
1478        for i in 0..NUM_ACCOUNTS {
1479            let index_offset = IndexOffset(i as u32);
1480            let account = hot_storage
1481                .get_account_shared_data(index_offset)
1482                .unwrap()
1483                .unwrap();
1484
1485            assert_eq!(account.lamports(), test_info.metas[i].lamports());
1486            assert_eq!(account.data().len(), test_info.datas[i].len());
1487            assert_eq!(account.data(), test_info.datas[i]);
1488            assert_eq!(
1489                *account.owner(),
1490                test_info.owners[test_info.metas[i].owner_offset().0 as usize],
1491            );
1492        }
1493        // Make sure it returns None on NUM_ACCOUNTS to allow termination on
1494        // while loop in actual accounts-db read case.
1495        assert_matches!(
1496            hot_storage.get_account_shared_data(IndexOffset(NUM_ACCOUNTS as u32)),
1497            Ok(None)
1498        );
1499    }
1500
1501    #[test]
1502    fn test_hot_storage_writer_twice_on_same_path() {
1503        let temp_dir = TempDir::new().unwrap();
1504        let path = temp_dir
1505            .path()
1506            .join("test_hot_storage_writer_twice_on_same_path");
1507
1508        // Expect the first returns Ok
1509        assert_matches!(HotStorageWriter::new(&path), Ok(_));
1510        // Expect the second call on the same path returns Err, as the
1511        // HotStorageWriter only writes once.
1512        assert_matches!(HotStorageWriter::new(&path), Err(_));
1513    }
1514
1515    #[test]
1516    fn test_write_account_and_index_blocks() {
1517        let account_data_sizes = &[
1518            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000, 2000, 3000, 4000, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
1519        ];
1520
1521        let accounts: Vec<_> = account_data_sizes
1522            .iter()
1523            .map(|size| create_test_account(*size))
1524            .collect();
1525
1526        let account_refs: Vec<_> = accounts
1527            .iter()
1528            .map(|account| (&account.0.pubkey, &account.1))
1529            .collect();
1530
1531        // Slot information is not used here
1532        let storable_accounts = (Slot::MAX, &account_refs[..]);
1533
1534        let temp_dir = TempDir::new().unwrap();
1535        let path = temp_dir.path().join("test_write_account_and_index_blocks");
1536        let stored_accounts_info = {
1537            let mut writer = HotStorageWriter::new(&path).unwrap();
1538            let stored_accounts_info = writer.write_accounts(&storable_accounts, 0).unwrap();
1539            writer.flush().unwrap();
1540            stored_accounts_info
1541        };
1542
1543        let file = TieredReadableFile::new(&path).unwrap();
1544        let hot_storage = HotStorageReader::new(file).unwrap();
1545
1546        let num_accounts = account_data_sizes.len();
1547        for i in 0..num_accounts {
1548            hot_storage
1549                .get_stored_account_meta_callback(IndexOffset(i as u32), |stored_account_meta| {
1550                    storable_accounts.account_default_if_zero_lamport(i, |account| {
1551                        verify_test_account(
1552                            &stored_account_meta,
1553                            &account.to_account_shared_data(),
1554                            account.pubkey(),
1555                        );
1556                    });
1557                })
1558                .unwrap()
1559                .unwrap();
1560        }
1561        // Make sure it returns None on NUM_ACCOUNTS to allow termination on
1562        // while loop in actual accounts-db read case.
1563        assert_matches!(
1564            hot_storage.get_stored_account_meta_callback(IndexOffset(num_accounts as u32), |_| {
1565                panic!("unexpected");
1566            }),
1567            Ok(None)
1568        );
1569
1570        for offset in stored_accounts_info.offsets {
1571            hot_storage
1572                .get_stored_account_meta_callback(
1573                    IndexOffset(offset as u32),
1574                    |stored_account_meta| {
1575                        storable_accounts.account_default_if_zero_lamport(offset, |account| {
1576                            verify_test_account(
1577                                &stored_account_meta,
1578                                &account.to_account_shared_data(),
1579                                account.pubkey(),
1580                            );
1581                        });
1582                    },
1583                )
1584                .unwrap()
1585                .unwrap();
1586        }
1587
1588        // verify everything
1589        let mut i = 0;
1590        hot_storage
1591            .scan_accounts(|stored_meta| {
1592                storable_accounts.account_default_if_zero_lamport(i, |account| {
1593                    verify_test_account(
1594                        &stored_meta,
1595                        &account.to_account_shared_data(),
1596                        account.pubkey(),
1597                    );
1598                });
1599                i += 1;
1600            })
1601            .unwrap();
1602
1603        let footer = hot_storage.footer();
1604
1605        let expected_size = footer.owners_block_offset as usize
1606            + std::mem::size_of::<Pubkey>() * footer.owner_count as usize
1607            + std::mem::size_of::<TieredStorageFooter>()
1608            + std::mem::size_of::<TieredStorageMagicNumber>();
1609
1610        assert!(!hot_storage.is_empty());
1611        assert_eq!(expected_size, hot_storage.len());
1612    }
1613}