solana_runtime/
storable_accounts.rs

1//! trait for abstracting underlying storage of pubkey and account pairs to be written
2use {
3    crate::{accounts_db::IncludeSlotInHash, append_vec::StoredAccountMeta},
4    solana_sdk::{account::ReadableAccount, clock::Slot, hash::Hash, pubkey::Pubkey},
5};
6
7/// abstract access to pubkey, account, slot, target_slot of either:
8/// a. (slot, &[&Pubkey, &ReadableAccount])
9/// b. (slot, &[&Pubkey, &ReadableAccount, Slot]) (we will use this later)
10/// This trait avoids having to allocate redundant data when there is a duplicated slot parameter.
11/// All legacy callers do not have a unique slot per account to store.
12pub trait StorableAccounts<'a, T: ReadableAccount + Sync>: Sync {
13    /// pubkey at 'index'
14    fn pubkey(&self, index: usize) -> &Pubkey;
15    /// account at 'index'
16    fn account(&self, index: usize) -> &T;
17    /// None if account is zero lamports
18    fn account_default_if_zero_lamport(&self, index: usize) -> Option<&T> {
19        let account = self.account(index);
20        (account.lamports() != 0).then_some(account)
21    }
22    // current slot for account at 'index'
23    fn slot(&self, index: usize) -> Slot;
24    /// slot that all accounts are to be written to
25    fn target_slot(&self) -> Slot;
26    /// true if no accounts to write
27    fn is_empty(&self) -> bool {
28        self.len() == 0
29    }
30    /// # accounts to write
31    fn len(&self) -> usize;
32    /// are there accounts from multiple slots
33    /// only used for an assert
34    fn contains_multiple_slots(&self) -> bool {
35        false
36    }
37    /// true iff hashing these accounts should include the slot
38    fn include_slot_in_hash(&self) -> IncludeSlotInHash;
39
40    /// true iff the impl can provide hash and write_version
41    /// Otherwise, hash and write_version have to be provided separately to store functions.
42    fn has_hash_and_write_version(&self) -> bool {
43        false
44    }
45
46    /// return hash for account at 'index'
47    /// Should only be called if 'has_hash_and_write_version' = true
48    fn hash(&self, _index: usize) -> &Hash {
49        // this should never be called if has_hash_and_write_version returns false
50        unimplemented!();
51    }
52
53    /// return write_version for account at 'index'
54    /// Should only be called if 'has_hash_and_write_version' = true
55    fn write_version(&self, _index: usize) -> u64 {
56        // this should never be called if has_hash_and_write_version returns false
57        unimplemented!();
58    }
59}
60
61/// accounts that are moving from 'old_slot' to 'target_slot'
62/// since all accounts are from the same old slot, we don't need to create a slice with per-account slot
63/// but, we need slot(_) to return 'old_slot' for all accounts
64/// Created a struct instead of a tuple to make the code easier to read.
65pub struct StorableAccountsMovingSlots<'a, T: ReadableAccount + Sync> {
66    pub accounts: &'a [(&'a Pubkey, &'a T)],
67    /// accounts will be written to this slot
68    pub target_slot: Slot,
69    /// slot where accounts are currently stored
70    pub old_slot: Slot,
71    /// This is temporarily here until feature activation.
72    pub include_slot_in_hash: IncludeSlotInHash,
73}
74
75impl<'a, T: ReadableAccount + Sync> StorableAccounts<'a, T> for StorableAccountsMovingSlots<'a, T> {
76    fn pubkey(&self, index: usize) -> &Pubkey {
77        self.accounts[index].0
78    }
79    fn account(&self, index: usize) -> &T {
80        self.accounts[index].1
81    }
82    fn slot(&self, _index: usize) -> Slot {
83        // per-index slot is not unique per slot, but it is different than 'target_slot'
84        self.old_slot
85    }
86    fn target_slot(&self) -> Slot {
87        self.target_slot
88    }
89    fn len(&self) -> usize {
90        self.accounts.len()
91    }
92    fn include_slot_in_hash(&self) -> IncludeSlotInHash {
93        self.include_slot_in_hash
94    }
95}
96
97/// The last parameter exists until this feature is activated:
98///  ignore slot when calculating an account hash #28420
99impl<'a, T: ReadableAccount + Sync> StorableAccounts<'a, T>
100    for (Slot, &'a [(&'a Pubkey, &'a T)], IncludeSlotInHash)
101{
102    fn pubkey(&self, index: usize) -> &Pubkey {
103        self.1[index].0
104    }
105    fn account(&self, index: usize) -> &T {
106        self.1[index].1
107    }
108    fn slot(&self, _index: usize) -> Slot {
109        // per-index slot is not unique per slot when per-account slot is not included in the source data
110        self.target_slot()
111    }
112    fn target_slot(&self) -> Slot {
113        self.0
114    }
115    fn len(&self) -> usize {
116        self.1.len()
117    }
118    fn include_slot_in_hash(&self) -> IncludeSlotInHash {
119        self.2
120    }
121}
122
123/// The last parameter exists until this feature is activated:
124///  ignore slot when calculating an account hash #28420
125impl<'a> StorableAccounts<'a, StoredAccountMeta<'a>>
126    for (Slot, &'a [&'a StoredAccountMeta<'a>], IncludeSlotInHash)
127{
128    fn pubkey(&self, index: usize) -> &Pubkey {
129        self.account(index).pubkey()
130    }
131    fn account(&self, index: usize) -> &StoredAccountMeta<'a> {
132        self.1[index]
133    }
134    fn slot(&self, _index: usize) -> Slot {
135        // per-index slot is not unique per slot when per-account slot is not included in the source data
136        self.0
137    }
138    fn target_slot(&self) -> Slot {
139        self.0
140    }
141    fn len(&self) -> usize {
142        self.1.len()
143    }
144    fn include_slot_in_hash(&self) -> IncludeSlotInHash {
145        self.2
146    }
147    fn has_hash_and_write_version(&self) -> bool {
148        true
149    }
150    fn hash(&self, index: usize) -> &Hash {
151        self.account(index).hash
152    }
153    fn write_version(&self, index: usize) -> u64 {
154        self.account(index).meta.write_version_obsolete
155    }
156}
157
158/// holds slices of accounts being moved FROM a common source slot to 'target_slot'
159pub struct StorableAccountsBySlot<'a> {
160    target_slot: Slot,
161    /// each element is (source slot, accounts moving FROM source slot)
162    slots_and_accounts: &'a [(Slot, &'a [&'a StoredAccountMeta<'a>])],
163    include_slot_in_hash: IncludeSlotInHash,
164
165    /// This is calculated based off slots_and_accounts.
166    /// cumulative offset of all account slices prior to this one
167    /// starting_offsets[0] is the starting offset of slots_and_accounts[1]
168    /// The starting offset of slots_and_accounts[0] is always 0
169    starting_offsets: Vec<usize>,
170    /// true if there is more than 1 slot represented in slots_and_accounts
171    contains_multiple_slots: bool,
172    /// total len of all accounts, across all slots_and_accounts
173    len: usize,
174}
175
176impl<'a> StorableAccountsBySlot<'a> {
177    #[allow(dead_code)]
178    /// each element of slots_and_accounts is (source slot, accounts moving FROM source slot)
179    pub(crate) fn new(
180        target_slot: Slot,
181        slots_and_accounts: &'a [(Slot, &'a [&'a StoredAccountMeta<'a>])],
182        include_slot_in_hash: IncludeSlotInHash,
183    ) -> Self {
184        let mut cumulative_len = 0usize;
185        let mut starting_offsets = Vec::with_capacity(slots_and_accounts.len());
186        let first_slot = slots_and_accounts
187            .first()
188            .map(|(slot, _)| *slot)
189            .unwrap_or_default();
190        let mut contains_multiple_slots = false;
191        for (slot, accounts) in slots_and_accounts {
192            cumulative_len = cumulative_len.saturating_add(accounts.len());
193            starting_offsets.push(cumulative_len);
194            contains_multiple_slots |= &first_slot != slot;
195        }
196        Self {
197            target_slot,
198            slots_and_accounts,
199            starting_offsets,
200            include_slot_in_hash,
201            contains_multiple_slots,
202            len: cumulative_len,
203        }
204    }
205    /// given an overall index for all accounts in self:
206    /// return (slots_and_accounts index, index within those accounts)
207    fn find_internal_index(&self, index: usize) -> (usize, usize) {
208        // search offsets for the accounts slice that contains 'index'.
209        // This could be a binary search.
210        for (offset_index, next_offset) in self.starting_offsets.iter().enumerate() {
211            if next_offset > &index {
212                // offset of prior entry
213                let prior_offset = if offset_index > 0 {
214                    self.starting_offsets[offset_index.saturating_sub(1)]
215                } else {
216                    0
217                };
218                return (offset_index, index - prior_offset);
219            }
220        }
221        panic!("failed");
222    }
223}
224
225/// The last parameter exists until this feature is activated:
226///  ignore slot when calculating an account hash #28420
227impl<'a> StorableAccounts<'a, StoredAccountMeta<'a>> for StorableAccountsBySlot<'a> {
228    fn pubkey(&self, index: usize) -> &Pubkey {
229        self.account(index).pubkey()
230    }
231    fn account(&self, index: usize) -> &StoredAccountMeta<'a> {
232        let indexes = self.find_internal_index(index);
233        self.slots_and_accounts[indexes.0].1[indexes.1]
234    }
235    fn slot(&self, index: usize) -> Slot {
236        let indexes = self.find_internal_index(index);
237        self.slots_and_accounts[indexes.0].0
238    }
239    fn target_slot(&self) -> Slot {
240        self.target_slot
241    }
242    fn len(&self) -> usize {
243        self.len
244    }
245    fn contains_multiple_slots(&self) -> bool {
246        self.contains_multiple_slots
247    }
248    fn include_slot_in_hash(&self) -> IncludeSlotInHash {
249        self.include_slot_in_hash
250    }
251    fn has_hash_and_write_version(&self) -> bool {
252        true
253    }
254    fn hash(&self, index: usize) -> &Hash {
255        self.account(index).hash
256    }
257    fn write_version(&self, index: usize) -> u64 {
258        self.account(index).meta.write_version_obsolete
259    }
260}
261
262/// this tuple contains a single different source slot that applies to all accounts
263/// accounts are StoredAccountMeta
264impl<'a> StorableAccounts<'a, StoredAccountMeta<'a>>
265    for (
266        Slot,
267        &'a [&'a StoredAccountMeta<'a>],
268        IncludeSlotInHash,
269        Slot,
270    )
271{
272    fn pubkey(&self, index: usize) -> &Pubkey {
273        self.account(index).pubkey()
274    }
275    fn account(&self, index: usize) -> &StoredAccountMeta<'a> {
276        self.1[index]
277    }
278    fn slot(&self, _index: usize) -> Slot {
279        // same other slot for all accounts
280        self.3
281    }
282    fn target_slot(&self) -> Slot {
283        self.0
284    }
285    fn len(&self) -> usize {
286        self.1.len()
287    }
288    fn include_slot_in_hash(&self) -> IncludeSlotInHash {
289        self.2
290    }
291    fn has_hash_and_write_version(&self) -> bool {
292        true
293    }
294    fn hash(&self, index: usize) -> &Hash {
295        self.account(index).hash
296    }
297    fn write_version(&self, index: usize) -> u64 {
298        self.account(index).meta.write_version_obsolete
299    }
300}
301
302#[cfg(test)]
303pub mod tests {
304    use {
305        super::*,
306        crate::{
307            accounts_db::INCLUDE_SLOT_IN_HASH_TESTS,
308            append_vec::{AccountMeta, StoredAccountMeta, StoredMeta},
309        },
310        solana_sdk::{
311            account::{accounts_equal, AccountSharedData, WritableAccount},
312            hash::Hash,
313        },
314    };
315
316    fn compare<
317        'a,
318        T: ReadableAccount + Sync + PartialEq + std::fmt::Debug,
319        U: ReadableAccount + Sync + PartialEq + std::fmt::Debug,
320    >(
321        a: &impl StorableAccounts<'a, T>,
322        b: &impl StorableAccounts<'a, U>,
323    ) {
324        assert_eq!(a.target_slot(), b.target_slot());
325        assert_eq!(a.len(), b.len());
326        assert_eq!(a.is_empty(), b.is_empty());
327        (0..a.len()).for_each(|i| {
328            assert_eq!(a.pubkey(i), b.pubkey(i));
329            assert!(accounts_equal(a.account(i), b.account(i)));
330        })
331    }
332
333    #[test]
334    fn test_contains_multiple_slots() {
335        let pk = Pubkey::from([1; 32]);
336        let slot = 0;
337        let lamports = 1;
338        let owner = Pubkey::default();
339        let executable = false;
340        let rent_epoch = 0;
341        let meta = StoredMeta {
342            write_version_obsolete: 5,
343            pubkey: pk,
344            data_len: 7,
345        };
346        let account_meta = AccountMeta {
347            lamports,
348            owner,
349            executable,
350            rent_epoch,
351        };
352        let data = Vec::default();
353        let offset = 99;
354        let stored_size = 101;
355        let hash = Hash::new_unique();
356        let stored_account = StoredAccountMeta {
357            meta: &meta,
358            account_meta: &account_meta,
359            data: &data,
360            offset,
361            stored_size,
362            hash: &hash,
363        };
364
365        let test3 = (
366            slot,
367            &vec![&stored_account, &stored_account][..],
368            INCLUDE_SLOT_IN_HASH_TESTS,
369            slot,
370        );
371        assert!(!test3.contains_multiple_slots());
372    }
373
374    #[test]
375    fn test_storable_accounts() {
376        let max_slots = 3_u64;
377        for target_slot in 0..max_slots {
378            for entries in 0..2 {
379                for starting_slot in 0..max_slots {
380                    let data = Vec::default();
381                    let hash = Hash::new_unique();
382                    let mut raw = Vec::new();
383                    let mut raw2 = Vec::new();
384                    for entry in 0..entries {
385                        let pk = Pubkey::from([entry; 32]);
386                        let account = AccountSharedData::create(
387                            (entry as u64) * starting_slot,
388                            Vec::default(),
389                            Pubkey::default(),
390                            false,
391                            0,
392                        );
393
394                        raw.push((
395                            pk,
396                            account.clone(),
397                            starting_slot % max_slots,
398                            StoredMeta {
399                                write_version_obsolete: 0, // just something
400                                pubkey: pk,
401                                data_len: u64::MAX, // just something
402                            },
403                            AccountMeta {
404                                lamports: account.lamports(),
405                                owner: *account.owner(),
406                                executable: account.executable(),
407                                rent_epoch: account.rent_epoch(),
408                            },
409                        ));
410                    }
411                    for entry in 0..entries {
412                        let offset = 99;
413                        let stored_size = 101;
414                        raw2.push(StoredAccountMeta {
415                            meta: &raw[entry as usize].3,
416                            account_meta: &raw[entry as usize].4,
417                            data: &data,
418                            offset,
419                            stored_size,
420                            hash: &hash,
421                        });
422                    }
423
424                    let mut two = Vec::new();
425                    let mut three = Vec::new();
426                    raw.iter().zip(raw2.iter()).for_each(|(raw, raw2)| {
427                        two.push((&raw.0, &raw.1)); // 2 item tuple
428                        three.push(raw2);
429                    });
430                    let test2 = (target_slot, &two[..], INCLUDE_SLOT_IN_HASH_TESTS);
431
432                    let source_slot = starting_slot % max_slots;
433                    let test3 = (
434                        target_slot,
435                        &three[..],
436                        INCLUDE_SLOT_IN_HASH_TESTS,
437                        source_slot,
438                    );
439                    let old_slot = starting_slot;
440                    let test_moving_slots = StorableAccountsMovingSlots {
441                        accounts: &two[..],
442                        target_slot,
443                        old_slot,
444                        include_slot_in_hash: INCLUDE_SLOT_IN_HASH_TESTS,
445                    };
446                    let for_slice = [(old_slot, &three[..])];
447                    let test_moving_slots2 = StorableAccountsBySlot::new(
448                        target_slot,
449                        &for_slice,
450                        INCLUDE_SLOT_IN_HASH_TESTS,
451                    );
452                    compare(&test2, &test3);
453                    compare(&test2, &test_moving_slots);
454                    compare(&test2, &test_moving_slots2);
455                    for (i, raw) in raw.iter().enumerate() {
456                        assert_eq!(raw.0, *test3.pubkey(i));
457                        assert!(accounts_equal(&raw.1, test3.account(i)));
458                        assert_eq!(raw.2, test3.slot(i));
459                        assert_eq!(target_slot, test2.slot(i));
460                        assert_eq!(old_slot, test_moving_slots.slot(i));
461                        assert_eq!(old_slot, test_moving_slots2.slot(i));
462                    }
463                    assert_eq!(target_slot, test3.target_slot());
464                    assert_eq!(target_slot, test_moving_slots2.target_slot());
465                    assert!(!test2.contains_multiple_slots());
466                    assert!(!test_moving_slots.contains_multiple_slots());
467                    assert_eq!(test3.contains_multiple_slots(), entries > 1);
468                }
469            }
470        }
471    }
472
473    #[test]
474    fn test_storable_accounts_by_slot() {
475        solana_logger::setup();
476        // slots 0..4
477        // each one containing a subset of the overall # of entries (0..4)
478        for entries in 0..6 {
479            let data = Vec::default();
480            let hashes = (0..entries).map(|_| Hash::new_unique()).collect::<Vec<_>>();
481            let mut raw = Vec::new();
482            let mut raw2 = Vec::new();
483            for entry in 0..entries {
484                let pk = Pubkey::from([entry; 32]);
485                let account = AccountSharedData::create(
486                    entry as u64,
487                    Vec::default(),
488                    Pubkey::default(),
489                    false,
490                    0,
491                );
492                raw.push((
493                    pk,
494                    account.clone(),
495                    StoredMeta {
496                        write_version_obsolete: 500 + (entry * 3) as u64, // just something
497                        pubkey: pk,
498                        data_len: (entry * 2) as u64, // just something
499                    },
500                    AccountMeta {
501                        lamports: account.lamports(),
502                        owner: *account.owner(),
503                        executable: account.executable(),
504                        rent_epoch: account.rent_epoch(),
505                    },
506                ));
507            }
508            for entry in 0..entries {
509                let offset = 99;
510                let stored_size = 101;
511                raw2.push(StoredAccountMeta {
512                    meta: &raw[entry as usize].2,
513                    account_meta: &raw[entry as usize].3,
514                    data: &data,
515                    offset,
516                    stored_size,
517                    hash: &hashes[entry as usize],
518                });
519            }
520            let raw2_refs = raw2.iter().collect::<Vec<_>>();
521
522            // enumerate through permutations of # entries (ie. accounts) in each slot. Each one is 0..=entries.
523            for entries0 in 0..=entries {
524                let remaining1 = entries.saturating_sub(entries0);
525                for entries1 in 0..=remaining1 {
526                    let remaining2 = entries.saturating_sub(entries0 + entries1);
527                    for entries2 in 0..=remaining2 {
528                        let remaining3 = entries.saturating_sub(entries0 + entries1 + entries2);
529                        let entries_by_level = vec![entries0, entries1, entries2, remaining3];
530                        let mut overall_index = 0;
531                        let mut expected_slots = Vec::default();
532                        let slots_and_accounts = entries_by_level
533                            .iter()
534                            .enumerate()
535                            .filter_map(|(slot, count)| {
536                                let slot = slot as Slot;
537                                let count = *count as usize;
538                                (overall_index < raw2.len()).then(|| {
539                                    let range = overall_index..(overall_index + count);
540                                    let result = &raw2_refs[range.clone()];
541                                    range.for_each(|_| expected_slots.push(slot));
542                                    overall_index += count;
543                                    (slot, result)
544                                })
545                            })
546                            .collect::<Vec<_>>();
547                        let storable = StorableAccountsBySlot::new(
548                            99,
549                            &slots_and_accounts[..],
550                            INCLUDE_SLOT_IN_HASH_TESTS,
551                        );
552                        assert!(storable.has_hash_and_write_version());
553                        assert_eq!(99, storable.target_slot());
554                        assert_eq!(entries0 != entries, storable.contains_multiple_slots());
555                        (0..entries).for_each(|index| {
556                            let index = index as usize;
557                            assert_eq!(storable.account(index), &raw2[index]);
558                            assert_eq!(storable.pubkey(index), raw2[index].pubkey());
559                            assert_eq!(storable.hash(index), raw2[index].hash);
560                            assert_eq!(storable.slot(index), expected_slots[index]);
561                            assert_eq!(
562                                storable.write_version(index),
563                                raw2[index].meta.write_version_obsolete
564                            );
565                        })
566                    }
567                }
568            }
569        }
570    }
571}