solana_accounts_db/tiered_storage/
index.rs

1use {
2    crate::tiered_storage::{
3        file::TieredWritableFile, footer::TieredStorageFooter, mmap_utils::get_pod,
4        TieredStorageResult,
5    },
6    bytemuck::{Pod, Zeroable},
7    memmap2::Mmap,
8    solana_pubkey::Pubkey,
9};
10
11/// The in-memory struct for the writing index block.
12#[derive(Debug)]
13pub struct AccountIndexWriterEntry<Offset: AccountOffset> {
14    /// The account address.
15    pub address: Pubkey,
16    /// The offset to the account.
17    pub offset: Offset,
18}
19
20/// The offset to an account.
21pub trait AccountOffset: Clone + Copy + Pod + Zeroable {}
22
23/// The offset to an account/address entry in the accounts index block.
24/// This can be used to obtain the AccountOffset and address by looking through
25/// the accounts index block.
26#[repr(C)]
27#[derive(Clone, Copy, Debug, Eq, PartialEq, bytemuck_derive::Pod, bytemuck_derive::Zeroable)]
28pub struct IndexOffset(pub u32);
29
30// Ensure there are no implicit padding bytes
31const _: () = assert!(std::mem::size_of::<IndexOffset>() == 4);
32
33/// The index format of a tiered accounts file.
34#[repr(u16)]
35#[derive(
36    Clone,
37    Copy,
38    Debug,
39    Default,
40    Eq,
41    Hash,
42    PartialEq,
43    num_enum::IntoPrimitive,
44    num_enum::TryFromPrimitive,
45)]
46pub enum IndexBlockFormat {
47    /// This format optimizes the storage size by storing only account addresses
48    /// and block offsets.  It skips storing the size of account data by storing
49    /// account block entries and index block entries in the same order.
50    #[default]
51    AddressesThenOffsets = 0,
52}
53
54// Ensure there are no implicit padding bytes
55const _: () = assert!(std::mem::size_of::<IndexBlockFormat>() == 2);
56
57impl IndexBlockFormat {
58    /// Persists the specified index_entries to the specified file and returns
59    /// the total number of bytes written.
60    pub fn write_index_block(
61        &self,
62        file: &mut TieredWritableFile,
63        index_entries: &[AccountIndexWriterEntry<impl AccountOffset>],
64    ) -> TieredStorageResult<usize> {
65        match self {
66            Self::AddressesThenOffsets => {
67                let mut bytes_written = 0;
68                for index_entry in index_entries {
69                    bytes_written += file.write_pod(&index_entry.address)?;
70                }
71                for index_entry in index_entries {
72                    bytes_written += file.write_pod(&index_entry.offset)?;
73                }
74                Ok(bytes_written)
75            }
76        }
77    }
78
79    /// Returns the address of the account given the specified index.
80    pub fn get_account_address<'a>(
81        &self,
82        mmap: &'a Mmap,
83        footer: &TieredStorageFooter,
84        index_offset: IndexOffset,
85    ) -> TieredStorageResult<&'a Pubkey> {
86        let offset = match self {
87            Self::AddressesThenOffsets => {
88                debug_assert!(index_offset.0 < footer.account_entry_count);
89                footer.index_block_offset as usize
90                    + std::mem::size_of::<Pubkey>() * (index_offset.0 as usize)
91            }
92        };
93
94        debug_assert!(
95            offset.saturating_add(std::mem::size_of::<Pubkey>())
96                <= footer.owners_block_offset as usize,
97            "reading IndexOffset ({}) would exceed index block boundary ({}).",
98            offset,
99            footer.owners_block_offset,
100        );
101
102        let (address, _) = get_pod::<Pubkey>(mmap, offset)?;
103        Ok(address)
104    }
105
106    /// Returns the offset to the account given the specified index.
107    pub fn get_account_offset<Offset: AccountOffset>(
108        &self,
109        mmap: &Mmap,
110        footer: &TieredStorageFooter,
111        index_offset: IndexOffset,
112    ) -> TieredStorageResult<Offset> {
113        let offset = match self {
114            Self::AddressesThenOffsets => {
115                debug_assert!(index_offset.0 < footer.account_entry_count);
116                footer.index_block_offset as usize
117                    + std::mem::size_of::<Pubkey>() * footer.account_entry_count as usize
118                    + std::mem::size_of::<Offset>() * index_offset.0 as usize
119            }
120        };
121
122        debug_assert!(
123            offset.saturating_add(std::mem::size_of::<Offset>())
124                <= footer.owners_block_offset as usize,
125            "reading IndexOffset ({}) would exceed index block boundary ({}).",
126            offset,
127            footer.owners_block_offset,
128        );
129
130        let (account_offset, _) = get_pod::<Offset>(mmap, offset)?;
131
132        Ok(*account_offset)
133    }
134
135    /// Returns the size of one index entry.
136    pub fn entry_size<Offset: AccountOffset>(&self) -> usize {
137        match self {
138            Self::AddressesThenOffsets => {
139                std::mem::size_of::<Pubkey>() + std::mem::size_of::<Offset>()
140            }
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use {
148        super::*,
149        crate::tiered_storage::{
150            file::TieredWritableFile,
151            hot::{HotAccountOffset, HOT_ACCOUNT_ALIGNMENT},
152        },
153        memmap2::MmapOptions,
154        rand::Rng,
155        std::fs::OpenOptions,
156        tempfile::TempDir,
157    };
158
159    #[test]
160    fn test_address_and_offset_indexer() {
161        const ENTRY_COUNT: usize = 100;
162        let mut footer = TieredStorageFooter {
163            account_entry_count: ENTRY_COUNT as u32,
164            ..TieredStorageFooter::default()
165        };
166        let temp_dir = TempDir::new().unwrap();
167        let path = temp_dir.path().join("test_address_and_offset_indexer");
168        let addresses: Vec<_> = std::iter::repeat_with(Pubkey::new_unique)
169            .take(ENTRY_COUNT)
170            .collect();
171        let mut rng = rand::thread_rng();
172        let index_entries: Vec<_> = addresses
173            .iter()
174            .map(|address| AccountIndexWriterEntry {
175                address: *address,
176                offset: HotAccountOffset::new(
177                    rng.gen_range(0..u32::MAX) as usize * HOT_ACCOUNT_ALIGNMENT,
178                )
179                .unwrap(),
180            })
181            .collect();
182
183        {
184            let mut file = TieredWritableFile::new(&path).unwrap();
185            let indexer = IndexBlockFormat::AddressesThenOffsets;
186            let cursor = indexer
187                .write_index_block(&mut file, &index_entries)
188                .unwrap();
189            footer.owners_block_offset = cursor as u64;
190        }
191
192        let indexer = IndexBlockFormat::AddressesThenOffsets;
193        let file = OpenOptions::new()
194            .read(true)
195            .create(false)
196            .open(&path)
197            .unwrap();
198        let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
199        for (i, index_entry) in index_entries.iter().enumerate() {
200            let account_offset = indexer
201                .get_account_offset::<HotAccountOffset>(&mmap, &footer, IndexOffset(i as u32))
202                .unwrap();
203            assert_eq!(index_entry.offset, account_offset);
204            let address = indexer
205                .get_account_address(&mmap, &footer, IndexOffset(i as u32))
206                .unwrap();
207            assert_eq!(index_entry.address, *address);
208        }
209    }
210
211    #[test]
212    #[should_panic(expected = "index_offset.0 < footer.account_entry_count")]
213    fn test_get_account_address_out_of_bounds() {
214        let temp_dir = TempDir::new().unwrap();
215        let path = temp_dir
216            .path()
217            .join("test_get_account_address_out_of_bounds");
218
219        let footer = TieredStorageFooter {
220            account_entry_count: 100,
221            index_block_format: IndexBlockFormat::AddressesThenOffsets,
222            ..TieredStorageFooter::default()
223        };
224
225        {
226            // we only write a footer here as the test should hit an assert
227            // failure before it actually reads the file.
228            let mut file = TieredWritableFile::new(&path).unwrap();
229            footer.write_footer_block(&mut file).unwrap();
230        }
231
232        let file = OpenOptions::new()
233            .read(true)
234            .create(false)
235            .open(&path)
236            .unwrap();
237        let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
238        footer
239            .index_block_format
240            .get_account_address(&mmap, &footer, IndexOffset(footer.account_entry_count))
241            .unwrap();
242    }
243
244    #[test]
245    #[should_panic(expected = "would exceed index block boundary")]
246    fn test_get_account_address_exceeds_index_block_boundary() {
247        let temp_dir = TempDir::new().unwrap();
248        let path = temp_dir
249            .path()
250            .join("test_get_account_address_exceeds_index_block_boundary");
251
252        let footer = TieredStorageFooter {
253            account_entry_count: 100,
254            index_block_format: IndexBlockFormat::AddressesThenOffsets,
255            index_block_offset: 1024,
256            // only holds one index entry
257            owners_block_offset: 1024 + std::mem::size_of::<HotAccountOffset>() as u64,
258            ..TieredStorageFooter::default()
259        };
260
261        {
262            // we only write a footer here as the test should hit an assert
263            // failure before it actually reads the file.
264            let mut file = TieredWritableFile::new(&path).unwrap();
265            footer.write_footer_block(&mut file).unwrap();
266        }
267
268        let file = OpenOptions::new()
269            .read(true)
270            .create(false)
271            .open(&path)
272            .unwrap();
273        let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
274        // IndexOffset does not exceed the account_entry_count but exceeds
275        // the index block boundary.
276        footer
277            .index_block_format
278            .get_account_address(&mmap, &footer, IndexOffset(2))
279            .unwrap();
280    }
281
282    #[test]
283    #[should_panic(expected = "index_offset.0 < footer.account_entry_count")]
284    fn test_get_account_offset_out_of_bounds() {
285        let temp_dir = TempDir::new().unwrap();
286        let path = temp_dir
287            .path()
288            .join("test_get_account_offset_out_of_bounds");
289
290        let footer = TieredStorageFooter {
291            account_entry_count: 100,
292            index_block_format: IndexBlockFormat::AddressesThenOffsets,
293            ..TieredStorageFooter::default()
294        };
295
296        {
297            // we only write a footer here as the test should hit an assert
298            // failure before we actually read the file.
299            let mut file = TieredWritableFile::new(&path).unwrap();
300            footer.write_footer_block(&mut file).unwrap();
301        }
302
303        let file = OpenOptions::new()
304            .read(true)
305            .create(false)
306            .open(&path)
307            .unwrap();
308        let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
309        footer
310            .index_block_format
311            .get_account_offset::<HotAccountOffset>(
312                &mmap,
313                &footer,
314                IndexOffset(footer.account_entry_count),
315            )
316            .unwrap();
317    }
318
319    #[test]
320    #[should_panic(expected = "would exceed index block boundary")]
321    fn test_get_account_offset_exceeds_index_block_boundary() {
322        let temp_dir = TempDir::new().unwrap();
323        let path = temp_dir
324            .path()
325            .join("test_get_account_offset_exceeds_index_block_boundary");
326
327        let footer = TieredStorageFooter {
328            account_entry_count: 100,
329            index_block_format: IndexBlockFormat::AddressesThenOffsets,
330            index_block_offset: 1024,
331            // only holds one index entry
332            owners_block_offset: 1024 + std::mem::size_of::<HotAccountOffset>() as u64,
333            ..TieredStorageFooter::default()
334        };
335
336        {
337            // we only write a footer here as the test should hit an assert
338            // failure before we actually read the file.
339            let mut file = TieredWritableFile::new(&path).unwrap();
340            footer.write_footer_block(&mut file).unwrap();
341        }
342
343        let file = OpenOptions::new()
344            .read(true)
345            .create(false)
346            .open(&path)
347            .unwrap();
348        let mmap = unsafe { MmapOptions::new().map(&file).unwrap() };
349        // IndexOffset does not exceed the account_entry_count but exceeds
350        // the index block boundary.
351        footer
352            .index_block_format
353            .get_account_offset::<HotAccountOffset>(&mmap, &footer, IndexOffset(2))
354            .unwrap();
355    }
356}