Skip to main content

solana_runtime/bank/
accounts_lt_hash.rs

1use {
2    super::Bank,
3    crossbeam_utils::CachePadded,
4    rayon::{
5        ThreadPool, ThreadPoolBuilder,
6        iter::{IntoParallelIterator, ParallelIterator},
7    },
8    solana_account::{AccountSharedData, ReadableAccount},
9    solana_accounts_db::{accounts_db::AccountsDb, storable_accounts::StorableAccounts},
10    solana_lattice_hash::lt_hash::LtHash,
11    solana_pubkey::Pubkey,
12    std::{
13        array, hint,
14        mem::size_of,
15        sync::{
16            Arc, LazyLock, Mutex,
17            atomic::{AtomicU64, AtomicUsize, Ordering},
18        },
19        time::Instant,
20    },
21};
22
23/// Number of threads for the async accounts hasher thread pool.
24const NUM_ACCOUNTS_HASHER_THREADS: usize = 4;
25
26// Maximum size, in bytes, for the seen-accounts freelist.
27const MAX_BYTES_SEEN_ACCOUNTS_FREELIST: usize = 10_000_000;
28
29impl Bank {
30    /// Enqueues the accounts lt hash updates for `accounts` to the accounts hasher thread pool.
31    ///
32    /// This fn is meant to be called by on-chain events, e.g. transaction processing.
33    /// This fn deduplicates from `accounts`, keeping only the latest version of each account.
34    /// It also loads the previous version of each account inline, because we assume the previous
35    /// version of each account is still in the accounts write cache, and thus fast to load.
36    ///
37    /// For non-transaction processing callers, consider `enqueue_off_chain_accounts_lt_hash_updates()`.
38    pub fn enqueue_on_chain_accounts_lt_hash_updates<'a>(
39        &self,
40        accounts: &impl StorableAccounts<'a>,
41    ) {
42        if accounts.is_empty() {
43            return;
44        }
45
46        let seen_accounts_freelist = seen_accounts_freelist();
47        let mut seen_accounts = seen_accounts_freelist.try_pop().unwrap_or_default();
48        let async_progress = &self.accounts_lt_hash_async_progress;
49        let thread_pool = accounts_hasher_thread_pool();
50
51        // process accounts in reverse because we must only count the latest version of each account
52        for index in (0..accounts.len()).rev() {
53            let address = accounts.pubkey(index);
54            if !seen_accounts.insert(*address) {
55                // we've already enqueued a newer update for the same account; skip this one
56                continue;
57            }
58            let prev_account = self
59                .rc
60                .accounts
61                .load_with_fixed_root_do_not_populate_read_cache(&self.ancestors, address)
62                .map(|(account, _slot)| account);
63            let curr_account = accounts.account(index, |account| {
64                (account.lamports() != 0).then(|| account.take_account())
65            });
66            if prev_account.is_none() && curr_account.is_none() {
67                // the account was ephemeral; skip it
68            } else {
69                // the account was modified; enqueue this update
70                async_progress.spawn(
71                    thread_pool,
72                    AccountsLtHashUpdate {
73                        address: *address,
74                        prev_account,
75                        curr_account,
76                    },
77                );
78            }
79        }
80
81        // reclaim the seen accounts hashset
82        seen_accounts_freelist.try_push(seen_accounts);
83    }
84
85    /// Enqueues the accounts lt hash updates for `accounts` to the accounts hasher thread pool.
86    ///
87    /// This fn is meant to be called by off-chain events, meaning we know/control `accounts`.
88    /// Contrasting with `enqueue_on_chain_accounts_lt_hash_updates()`, this fn:
89    /// - Does not deduplicate accounts, requiring the caller to ensure there are no duplicates.
90    /// - Does not assume loading the previous version of accounts is fast,
91    ///   e.g. when storing stake accounts as part of partitioned epoch rewards.
92    ///
93    /// If Some, `thread_pool_for_hashing_accounts` will be used
94    /// to load the previous version of accounts in parallel.
95    pub fn enqueue_off_chain_accounts_lt_hash_updates<'a>(
96        &self,
97        accounts: &impl StorableAccounts<'a>,
98        thread_pool_for_loading_accounts: Option<&ThreadPool>,
99    ) {
100        if cfg!(debug_assertions) {
101            // if debug assertions are on, we will check for duplicates
102            use ahash::HashSetExt as _;
103            let mut seen_accounts = ahash::HashSet::with_capacity(accounts.len());
104            let mut duplicate_pubkeys = ahash::HashSet::with_capacity(0); // assume no duplicates
105            for index in 0..accounts.len() {
106                let pubkey = accounts.pubkey(index);
107                if !seen_accounts.insert(pubkey) {
108                    // we've already seen this account, so add it to the duplicates list
109                    duplicate_pubkeys.insert(pubkey);
110                }
111            }
112            if !duplicate_pubkeys.is_empty() {
113                let mut duplicate_accounts = ahash::HashMap::<_, Vec<_>>::default();
114                for duplicate_pubkey in duplicate_pubkeys {
115                    for index in 0..accounts.len() {
116                        let pubkey = accounts.pubkey(index);
117                        if pubkey == duplicate_pubkey {
118                            duplicate_accounts
119                                .entry(pubkey)
120                                .or_default()
121                                .push(accounts.account(index, |account| account.take_account()));
122                        }
123                    }
124                }
125                panic!("duplicate accounts were enqueued for hashing: {duplicate_accounts:?}");
126            }
127        }
128
129        let async_progress = &self.accounts_lt_hash_async_progress;
130        let thread_pool_for_hashing_accounts = accounts_hasher_thread_pool();
131
132        // A closure that does the loading and enqueueing, so code is shared
133        // whether using the thread_pool_for_loading_accounts or not.
134        let load_then_enqueue = |index| {
135            let address = accounts.pubkey(index);
136            let prev_account = self
137                .rc
138                .accounts
139                .load_with_fixed_root_do_not_populate_read_cache(&self.ancestors, address)
140                .map(|(account, _slot)| account);
141            let curr_account = accounts.account(index, |account| {
142                (account.lamports() != 0).then(|| account.take_account())
143            });
144            if prev_account.is_none() && curr_account.is_none() {
145                // the account was ephemeral; skip it
146            } else {
147                // the account was modified; enqueue this update
148                async_progress.spawn(
149                    thread_pool_for_hashing_accounts,
150                    AccountsLtHashUpdate {
151                        address: *address,
152                        prev_account,
153                        curr_account,
154                    },
155                );
156            }
157        };
158
159        if let Some(thread_pool_for_loading_accounts) = thread_pool_for_loading_accounts {
160            // The previous version of accounts must be loaded before subsequent account
161            // modifications occur, so ThreadPool::spawn() canot be used here.
162            thread_pool_for_loading_accounts.install(|| {
163                (0..accounts.len())
164                    .into_par_iter()
165                    .for_each(load_then_enqueue);
166            });
167        } else {
168            (0..accounts.len()).for_each(load_then_enqueue);
169        }
170    }
171
172    /// Updates the accounts lt hash.
173    ///
174    /// When freezing a bank, we compute and update the accounts lt hash.
175    /// For each account modified in this bank, we:
176    /// - mix out its previous state, and
177    /// - mix in its current state
178    ///
179    /// This function waits for any in-flight jobs on the accounts hasher threads,
180    /// computes their combined delta lt hash, then mixes it into the bank.
181    pub fn finish_accounts_lt_hash_updates(&self) {
182        let timer = Instant::now();
183        let num_jobs_total = {
184            let mut accounts_lt_hash = self.accounts_lt_hash.lock().unwrap();
185            self.accounts_lt_hash_async_progress
186                .finish(&mut accounts_lt_hash.0)
187        };
188        let finish_time = timer.elapsed();
189
190        let seen_accounts_freelist_stats = seen_accounts_freelist().stats();
191        datapoint_info!(
192            "bank-accounts_lt_hash",
193            ("slot", self.slot(), i64),
194            ("num_jobs", num_jobs_total, i64),
195            ("finish_us", finish_time.as_micros(), i64),
196            (
197                "seen_accounts_freelist_num_containers",
198                seen_accounts_freelist_stats.num_containers,
199                i64
200            ),
201            (
202                "seen_accounts_freelist_capacity_elems",
203                seen_accounts_freelist_stats.capacity_elems,
204                i64
205            ),
206            (
207                "seen_accounts_freelist_capacity_bytes",
208                seen_accounts_freelist_stats.capacity_bytes,
209                i64
210            ),
211        );
212    }
213}
214
215/// Struct for tracking progress of the asynchronous accounts lt hashing for a Bank.
216pub struct AccountsLtHashAsyncProgress {
217    // Note: use [Mutex<CachePadded<LtHash>>] and *not* [CachePadded<Mutex<LtHash>>].
218    // - In both ways each mutex is on its own separate cache line.
219    // - In both ways the size used for each element, including padding, is the same.
220    // - Only this way ensures that each LtHash is placed for aligned SIMD/AVX access.
221    //
222    // Here's the layout of [Mutex<CachePadded<LtHash>>; 2]
223    //
224    //  │element 0                         │element 1
225    //  │                                  │
226    //  ▼───────┬─────────┬────────────────▼───────┬─────────┬────────────────┐
227    //  │ Mutex │ padding │     LtHash     │ Mutex │ padding │     LtHash     │
228    //  ├───────┼─────────┼────────────────┼───────┼─────────┼────────────────┤
229    //  │       │         │                │       │         │                │
230    //  │0      │6        │128 <-- aligned │2176   │2182     │2304            │4352
231    //
232    //
233    // And here's the layout of [CachePadded<Mutex<LtHash>>; 2]
234    //
235    //  │element 0                         │element 1
236    //  │                                  │
237    //  ▼───────┬────────────────┬─────────▼───────┬────────────────┬─────────┐
238    //  │ Mutex │     LtHash     │ padding │ Mutex │     LtHash     │ padding │
239    //  ├───────┼────────────────┼─────────┼───────┼────────────────┼─────────┤
240    //  │       │                │         │       │                │         │
241    //  │0      │6 <-- unaligned │2054     │2176   │2182            │4230     │4352
242    //
243    accumulators: Arc<[Mutex<CachePadded<LtHash>>; NUM_ACCOUNTS_HASHER_THREADS]>,
244    num_jobs_pending: Arc<AtomicUsize>,
245    num_jobs_total: AtomicU64,
246}
247
248impl AccountsLtHashAsyncProgress {
249    /// Creates a new AccountsLtHashAsyncProgress variable, which is suitable for a new Bank.
250    pub fn new() -> Self {
251        Self {
252            accumulators: Arc::new(array::from_fn(|_| {
253                Mutex::new(CachePadded::new(LtHash::identity()))
254            })),
255            num_jobs_pending: Arc::new(AtomicUsize::new(0)),
256            num_jobs_total: AtomicU64::new(0),
257        }
258    }
259
260    /// Enqueues `update` into `thread_pool` for asynchronous processing.
261    fn spawn(&self, thread_pool: &'static ThreadPool, update: AccountsLtHashUpdate) {
262        self.num_jobs_pending.fetch_add(1, Ordering::Relaxed);
263        self.num_jobs_total.fetch_add(1, Ordering::Relaxed);
264        thread_pool.spawn({
265            let accumulators = Arc::clone(&self.accumulators);
266            let num_jobs_pending = Arc::clone(&self.num_jobs_pending);
267            move || {
268                // SAFETY: We always call from the same/correct Rayon thread pool.
269                let worker_index = thread_pool.current_thread_index().unwrap();
270
271                // SAFETY: There are num_threads accumulators, and each
272                // thread's index shall always be in range 0..num_threads.
273                debug_assert!(worker_index < accumulators.len());
274                let accumulator = unsafe { accumulators.get_unchecked(worker_index) };
275
276                Self::process(&mut accumulator.lock().unwrap(), update);
277
278                // Decrementing the number of pending jobs MUST happen *after*
279                // accumulating the result.  This ensures `finish()` cannot
280                // observe zero pending jobs until all workers are done.
281                num_jobs_pending.fetch_sub(1, Ordering::Relaxed);
282            }
283        });
284    }
285
286    /// Waits for all pending jobs to complete, then mixes the results into `lt_hash`.
287    ///
288    /// Returns the number of asynchronous jobs completed.
289    ///
290    /// Note: Since an LtHash is large, `lt_hash` is passed as an in-out parameter.
291    /// This it to avoid Rust compiler bug that fails to perform return value optimization.
292    fn finish(&self, lt_hash: &mut LtHash) -> u64 {
293        while self.num_jobs_pending.load(Ordering::Relaxed) > 0 {
294            // Spin, do not yield! This is called by Bank::freeze() and we want to be fast.
295            hint::spin_loop();
296        }
297
298        for thread_accumulator in self.accumulators.iter() {
299            lt_hash.mix_in(&thread_accumulator.lock().unwrap());
300        }
301        self.num_jobs_total.load(Ordering::Relaxed)
302    }
303
304    /// Processes `update` and mixes the result into `accum_lt_hash`.
305    ///
306    /// Note: Since an LtHash is large, `accum_lt_hash` is passed as an in-out parameter.
307    /// This it to avoid Rust compiler bug that fails to perform return value optimization.
308    fn process(accum_lt_hash: &mut LtHash, update: AccountsLtHashUpdate) {
309        let AccountsLtHashUpdate {
310            address,
311            prev_account,
312            curr_account,
313        } = update;
314        if let Some(prev_account) = prev_account {
315            let prev_lt_hash = AccountsDb::lt_hash_account(&prev_account, &address);
316            accum_lt_hash.mix_out(&prev_lt_hash.0);
317        }
318        if let Some(curr_account) = curr_account {
319            let curr_lt_hash = AccountsDb::lt_hash_account(&curr_account, &address);
320            accum_lt_hash.mix_in(&curr_lt_hash.0);
321        }
322    }
323}
324
325/// A single accounts lt hash update to process.
326#[derive(Debug)]
327struct AccountsLtHashUpdate {
328    address: Pubkey,
329    prev_account: Option<AccountSharedData>,
330    curr_account: Option<AccountSharedData>,
331}
332
333/// Get the freelist of hashsets to use for seen accounts.
334fn seen_accounts_freelist() -> &'static HashSetFreelist<Pubkey> {
335    // Derived empirically while observing an unstaked node on mnb.
336    // Should end up being the same number as replay threads.
337    const MAX_CONTAINERS: usize = 50;
338    static FREELIST: LazyLock<HashSetFreelist<Pubkey>> = LazyLock::new(|| {
339        HashSetFreelist::new(MAX_CONTAINERS, Some(MAX_BYTES_SEEN_ACCOUNTS_FREELIST))
340    });
341    &FREELIST
342}
343
344/// Freelist of containers, to avoid repeat allocations/deallocations.
345#[derive(Debug)]
346struct HashSetFreelist<T> {
347    /// the maximum number of containers this freelist will hold
348    max_containers: usize,
349
350    /// the maximum capacity, in elements, this freelist will hold
351    max_capacity: Option<usize>,
352
353    inner: Mutex<HashSetFreelistInner<T>>,
354}
355
356impl<T> HashSetFreelist<T> {
357    /// Creates a new, empty, freelist.
358    ///
359    /// max_containers:
360    /// * The maximum number of containers this freelist can hold.
361    ///
362    /// max_bytes:
363    /// * The maximum number of bytes this freelist can hold.
364    /// * This value corresponds to the total capacity across all the containers in the freelist.
365    /// * If `None`, there is no maximum.
366    fn new(max_containers: usize, max_bytes: Option<usize>) -> Self {
367        let max_capacity = max_bytes.map(|max_bytes| max_bytes / size_of::<T>());
368        Self {
369            max_containers,
370            max_capacity,
371            inner: Mutex::new(HashSetFreelistInner {
372                list: Vec::with_capacity(max_containers),
373                total_capacity: 0,
374            }),
375        }
376    }
377
378    /// Pushes `container` on to the freelist (IFF its capacity is greater than zero).
379    fn try_push(&self, mut container: ahash::HashSet<T>) {
380        // If the capacity is zero, then the container never allocated.
381        // In that case, don't waste time putting it back into the freelist,
382        // since there's nothing of value to reuse.
383        //
384        // Else, check if pushing the container would exceed the max capacity of the freelist.
385        // If so, also do not put it back into the freelist.
386        let capacity = container.capacity();
387        if capacity == 0 {
388            return;
389        }
390
391        // container must be empty to be reused, so do it here outside of the lock
392        container.clear();
393
394        let mut inner = self.inner.lock().unwrap();
395
396        if inner.list.len() >= self.max_containers {
397            // the num containers would exceed the max, do not push
398            return;
399        }
400
401        let Some(new_total_capacity) = inner.total_capacity.checked_add(capacity) else {
402            // the new total capacity would overflow, do not push
403            return;
404        };
405
406        let max_capacity = self.max_capacity.unwrap_or(usize::MAX);
407        if new_total_capacity > max_capacity {
408            // the new total capacity would exceed the max, do not push
409            return;
410        }
411
412        inner.list.push(container);
413        inner.total_capacity = new_total_capacity;
414    }
415
416    /// Pops a container off the freelist and returns it.
417    ///
418    /// The returned container will always be empty.
419    fn try_pop(&self) -> Option<ahash::HashSet<T>> {
420        let mut inner = self.inner.lock().unwrap();
421        let container = inner.list.pop()?;
422        assert!(container.is_empty());
423        inner.total_capacity -= container.capacity();
424        Some(container)
425    }
426
427    /// Returns a snapshot of the freelist's stats.
428    fn stats(&self) -> FreelistStats {
429        let inner = self.inner.lock().unwrap();
430        let num_containers = inner.list.len();
431        let capacity_elems = inner.total_capacity;
432        drop(inner);
433        FreelistStats {
434            num_containers,
435            capacity_elems,
436            capacity_bytes: capacity_elems.saturating_mul(size_of::<T>()),
437        }
438    }
439}
440
441/// The mutable state of a [`HashSetFreelist`], guarded by a single lock.
442#[derive(Debug)]
443struct HashSetFreelistInner<T> {
444    /// the containers available for reuse
445    list: Vec<ahash::HashSet<T>>,
446    /// the capacity, in elements, across all the containers in the freelist
447    total_capacity: usize,
448}
449
450/// A snapshot of a freelist's stats.
451#[derive(Debug, Eq, PartialEq)]
452struct FreelistStats {
453    /// the number of containers held by the freelist
454    num_containers: usize,
455    /// the capacity, in elements, across all the containers in the freelist
456    capacity_elems: usize,
457    /// the capacity, in bytes, across all the containers in the freelist
458    capacity_bytes: usize,
459}
460
461/// Returns the thread pool for asynchronous accounts hashing.
462///
463/// Note, the thread pool will be created on first call.
464fn accounts_hasher_thread_pool() -> &'static ThreadPool {
465    static THREAD_POOL: LazyLock<ThreadPool> = LazyLock::new(|| {
466        ThreadPoolBuilder::new()
467            .num_threads(NUM_ACCOUNTS_HASHER_THREADS)
468            .stack_size(8 * 1024 * 1024)
469            .thread_name(|i| format!("solAcctsHashr{i:02}"))
470            .build()
471            .expect("new accounts hasher rayon threadpool")
472    });
473    &THREAD_POOL
474}
475
476#[cfg(test)]
477mod tests {
478    use {
479        super::*,
480        crate::{
481            genesis_utils::create_genesis_config_with_leader_ex, runtime_config::RuntimeConfig,
482            snapshot_bank_utils, snapshot_utils,
483        },
484        agave_feature_set::FeatureSet,
485        agave_snapshots::snapshot_config::SnapshotConfig,
486        ahash::HashSetExt as _,
487        solana_accounts_db::{
488            accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig},
489            accounts_index::{ACCOUNTS_INDEX_CONFIG_FOR_TESTING, AccountsIndexConfig, IndexLimit},
490        },
491        solana_cluster_type::ClusterType,
492        solana_fee_calculator::FeeRateGovernor,
493        solana_genesis_config::{self, GenesisConfig},
494        solana_hash::Hash,
495        solana_keypair::Keypair,
496        solana_leader_schedule::SlotLeader,
497        solana_native_token::LAMPORTS_PER_SOL,
498        solana_pubkey::{self as pubkey, Pubkey},
499        solana_rent::Rent,
500        solana_signer::Signer as _,
501        std::{
502            cmp, iter,
503            str::FromStr as _,
504            sync::{Arc, Barrier},
505            thread,
506        },
507        tempfile::TempDir,
508        test_case::{test_case, test_matrix},
509    };
510
511    /// What features should be enabled?
512    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
513    enum Features {
514        /// Do not enable any features
515        None,
516        /// Enable all features
517        All,
518    }
519
520    /// Creates a genesis config with `features` enabled
521    fn genesis_config_with(features: Features) -> (GenesisConfig, Keypair) {
522        let mint_keypair = Keypair::new();
523        let mint_lamports = 123_456_789 * LAMPORTS_PER_SOL;
524        let validator_lamports = 100 * LAMPORTS_PER_SOL;
525        let validator_stake_lamports = 10 * LAMPORTS_PER_SOL;
526        let validator_pubkey = Pubkey::new_unique();
527        let vote_account_pubkey = Pubkey::new_unique();
528        let stake_account_pubkey = Pubkey::new_unique();
529        let feature_set = match features {
530            Features::None => FeatureSet::default(),
531            Features::All => FeatureSet::all_enabled(),
532        };
533
534        let config = create_genesis_config_with_leader_ex(
535            mint_lamports,
536            &mint_keypair.pubkey(),
537            &validator_pubkey,
538            &vote_account_pubkey,
539            &stake_account_pubkey,
540            None,
541            validator_stake_lamports,
542            validator_lamports,
543            FeeRateGovernor::default(),
544            Rent::default(),
545            ClusterType::Development,
546            &feature_set,
547            vec![],
548        );
549
550        (config, mint_keypair)
551    }
552
553    #[test]
554    fn test_update_accounts_lt_hash() {
555        // Write to address 1, 2, and 5 in first bank, so that in second bank we have
556        // updates to these three accounts.  Make address 2 go to zero (dead).  Make address 1 and 3 stay
557        // alive.  Make address 5 unchanged.  Ensure the updates are expected.
558        //
559        // 1: alive -> alive
560        // 2: alive -> dead
561        // 3: dead -> alive
562        // 4. dead -> dead
563        // 5. alive -> alive *unchanged*
564
565        let keypair1 = Keypair::new();
566        let keypair2 = Keypair::new();
567        let keypair3 = Keypair::new();
568        let keypair4 = Keypair::new();
569        let keypair5 = Keypair::new();
570
571        let (mut genesis_config, mint_keypair) =
572            solana_genesis_config::create_genesis_config(123_456_789 * LAMPORTS_PER_SOL);
573        // This test requires zero fees so that we can easily transfer an account's entire balance.
574        genesis_config.fee_rate_governor = FeeRateGovernor::new(0, 0);
575        let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
576
577        let amount = cmp::max(
578            bank.get_minimum_balance_for_rent_exemption(0),
579            LAMPORTS_PER_SOL,
580        );
581
582        // send lamports to accounts 1, 2, and 5 so they are alive,
583        // and so we'll have a delta in the next bank
584        bank.register_unique_recent_blockhash_for_test();
585        bank.transfer(amount, &mint_keypair, &keypair1.pubkey())
586            .unwrap();
587        bank.transfer(amount, &mint_keypair, &keypair2.pubkey())
588            .unwrap();
589        bank.transfer(amount, &mint_keypair, &keypair5.pubkey())
590            .unwrap();
591
592        // manually freeze the bank to trigger updating the accounts lt hash
593        bank.freeze();
594        let prev_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone();
595
596        // save the initial values of the accounts to use for asserts later
597        let prev_mint = bank.get_account_with_fixed_root(&mint_keypair.pubkey());
598        let prev_account1 = bank.get_account_with_fixed_root(&keypair1.pubkey());
599        let prev_account2 = bank.get_account_with_fixed_root(&keypair2.pubkey());
600        let prev_account3 = bank.get_account_with_fixed_root(&keypair3.pubkey());
601        let prev_account4 = bank.get_account_with_fixed_root(&keypair4.pubkey());
602        let prev_account5 = bank.get_account_with_fixed_root(&keypair5.pubkey());
603
604        assert!(prev_mint.is_some());
605        assert!(prev_account1.is_some());
606        assert!(prev_account2.is_some());
607        assert!(prev_account3.is_none());
608        assert!(prev_account4.is_none());
609        assert!(prev_account5.is_some());
610
611        // These sysvars are also updated, but outside of transaction processing.  This means they
612        // will not be in the accounts lt hash cache, but *will* be in the list of modified
613        // accounts.  They must be included in the accounts lt hash.
614        let sysvars = [
615            Pubkey::from_str("SysvarS1otHashes111111111111111111111111111").unwrap(),
616            Pubkey::from_str("SysvarC1ock11111111111111111111111111111111").unwrap(),
617            Pubkey::from_str("SysvarRecentB1ockHashes11111111111111111111").unwrap(),
618            Pubkey::from_str("SysvarS1otHistory11111111111111111111111111").unwrap(),
619        ];
620        let prev_sysvar_accounts: Vec<_> = sysvars
621            .iter()
622            .map(|address| bank.get_account_with_fixed_root(address))
623            .collect();
624
625        let bank = {
626            let slot = bank.slot() + 1;
627            Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), slot)
628        };
629
630        // send from account 2 to account 1; account 1 stays alive, account 2 ends up dead
631        bank.register_unique_recent_blockhash_for_test();
632        bank.transfer(amount, &keypair2, &keypair1.pubkey())
633            .unwrap();
634
635        // send lamports to account 4, then turn around and send them to account 3
636        // account 3 will be alive, and account 4 will end dead
637        bank.register_unique_recent_blockhash_for_test();
638        bank.transfer(amount, &mint_keypair, &keypair4.pubkey())
639            .unwrap();
640        bank.register_unique_recent_blockhash_for_test();
641        bank.transfer(amount, &keypair4, &keypair3.pubkey())
642            .unwrap();
643
644        // store account 5 into this new bank, unchanged
645        bank.store_account(&keypair5.pubkey(), prev_account5.as_ref().unwrap());
646
647        // freeze the bank to trigger updating the accounts lt hash
648        bank.freeze();
649
650        let post_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone();
651        let post_mint = bank.get_account_with_fixed_root(&mint_keypair.pubkey());
652        let post_account1 = bank.get_account_with_fixed_root(&keypair1.pubkey());
653        let post_account2 = bank.get_account_with_fixed_root(&keypair2.pubkey());
654        let post_account3 = bank.get_account_with_fixed_root(&keypair3.pubkey());
655        let post_account4 = bank.get_account_with_fixed_root(&keypair4.pubkey());
656        let post_account5 = bank.get_account_with_fixed_root(&keypair5.pubkey());
657
658        assert!(post_mint.is_some());
659        assert!(post_account1.is_some());
660        assert!(post_account2.is_none());
661        assert!(post_account3.is_some());
662        assert!(post_account4.is_none());
663        assert!(post_account5.is_some());
664
665        let post_sysvar_accounts: Vec<_> = sysvars
666            .iter()
667            .map(|address| bank.get_account_with_fixed_root(address))
668            .collect();
669
670        let mut expected_accounts_lt_hash = prev_accounts_lt_hash;
671        let mut updater =
672            |address: &Pubkey, prev: Option<AccountSharedData>, post: Option<AccountSharedData>| {
673                // if there was an alive account, mix out
674                if let Some(prev) = prev {
675                    let prev_lt_hash = AccountsDb::lt_hash_account(&prev, address);
676                    expected_accounts_lt_hash.0.mix_out(&prev_lt_hash.0);
677                }
678
679                // mix in the new one
680                let post = post.unwrap_or_default();
681                let post_lt_hash = AccountsDb::lt_hash_account(&post, address);
682                expected_accounts_lt_hash.0.mix_in(&post_lt_hash.0);
683            };
684        updater(&mint_keypair.pubkey(), prev_mint, post_mint);
685        updater(&keypair1.pubkey(), prev_account1, post_account1);
686        updater(&keypair2.pubkey(), prev_account2, post_account2);
687        updater(&keypair3.pubkey(), prev_account3, post_account3);
688        updater(&keypair4.pubkey(), prev_account4, post_account4);
689        updater(&keypair5.pubkey(), prev_account5, post_account5);
690        for (i, sysvar) in sysvars.iter().enumerate() {
691            updater(
692                sysvar,
693                prev_sysvar_accounts[i].clone(),
694                post_sysvar_accounts[i].clone(),
695            );
696        }
697
698        // now make sure the accounts lt hashes match
699        let expected = expected_accounts_lt_hash.0.checksum();
700        let actual = post_accounts_lt_hash.0.checksum();
701        assert_eq!(
702            expected, actual,
703            "accounts_lt_hash, expected: {expected}, actual: {actual}",
704        );
705    }
706
707    /// Ensure that the accounts lt hash is correct for slot 0
708    ///
709    /// This test does a simple transfer in slot 0 so that a primordial account is modified.
710    ///
711    /// Slot 0 is special because primordial accounts have no previous accounts lt hash entry.
712    #[test_case(Features::None; "no features")]
713    #[test_case(Features::All; "all features")]
714    fn test_slot0_accounts_lt_hash(features: Features) {
715        let (genesis_config, mint_keypair) = genesis_config_with(features);
716        let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
717
718        // ensure this bank is for slot 0, otherwise this test doesn't actually do anything...
719        assert_eq!(bank.slot(), 0);
720
721        // process a transaction that modifies a primordial account
722        bank.transfer(LAMPORTS_PER_SOL, &mint_keypair, &Pubkey::new_unique())
723            .unwrap();
724
725        // manually freeze the bank to trigger updating the accounts lt hash
726        bank.freeze();
727        let actual_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone();
728
729        // ensure the actual accounts lt hash matches the value calculated from the index
730        let calculated_accounts_lt_hash = bank
731            .rc
732            .accounts
733            .accounts_db
734            .calculate_accounts_lt_hash_at_startup_from_index(&bank.ancestors);
735        assert_eq!(actual_accounts_lt_hash, calculated_accounts_lt_hash);
736    }
737
738    #[test_case(Features::None; "no features")]
739    #[test_case(Features::All; "all features")]
740    fn test_calculate_accounts_lt_hash_at_startup_from_index(features: Features) {
741        let (genesis_config, mint_keypair) = genesis_config_with(features);
742        let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
743
744        let amount = cmp::max(
745            bank.get_minimum_balance_for_rent_exemption(0),
746            LAMPORTS_PER_SOL,
747        );
748
749        // create some banks with some modified accounts so that there are stored accounts
750        // (note: the number of banks and transfers are arbitrary)
751        for _ in 0..7 {
752            let slot = bank.slot() + 1;
753            bank = Bank::new_from_parent_with_bank_forks(
754                &bank_forks,
755                bank,
756                SlotLeader::default(),
757                slot,
758            );
759            for _ in 0..13 {
760                bank.register_unique_recent_blockhash_for_test();
761                // note: use a random pubkey here to ensure accounts
762                // are spread across all the index bins
763                bank.transfer(amount, &mint_keypair, &pubkey::new_rand())
764                    .unwrap();
765            }
766            bank.freeze();
767        }
768        let expected_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone();
769
770        // root the bank and flush the accounts write cache to disk
771        // (this more accurately simulates startup, where accounts are in storages on disk)
772        bank.squash();
773        bank.force_flush_accounts_cache();
774
775        // call the fn that calculates the accounts lt hash at startup, then ensure it matches
776        let calculated_accounts_lt_hash = bank
777            .rc
778            .accounts
779            .accounts_db
780            .calculate_accounts_lt_hash_at_startup_from_index(&bank.ancestors);
781        assert_eq!(expected_accounts_lt_hash, calculated_accounts_lt_hash);
782    }
783
784    #[test_matrix(
785        [Features::None, Features::All],
786        [IndexLimit::Minimal, IndexLimit::InMemOnly]
787    )]
788    fn test_verify_accounts_lt_hash_at_startup(
789        features: Features,
790        accounts_index_limit: IndexLimit,
791    ) {
792        let (mut genesis_config, mint_keypair) = genesis_config_with(features);
793        // This test requires zero fees so that we can easily transfer an account's entire balance.
794        genesis_config.fee_rate_governor = FeeRateGovernor::new(0, 0);
795        let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
796
797        let amount = cmp::max(
798            bank.get_minimum_balance_for_rent_exemption(0),
799            LAMPORTS_PER_SOL,
800        );
801
802        // Write to this pubkey multiple times, so there are guaranteed duplicates in the storages.
803        let duplicate_pubkey = pubkey::new_rand();
804
805        // create some banks with some modified accounts so that there are stored accounts
806        // (note: the number of banks and transfers are arbitrary)
807        for _ in 0..9 {
808            let slot = bank.slot() + 1;
809            let leader = *bank.leader();
810            bank = Bank::new_from_parent_with_bank_forks(&bank_forks, bank, leader, slot);
811            for _ in 0..3 {
812                bank.register_unique_recent_blockhash_for_test();
813                bank.transfer(amount, &mint_keypair, &pubkey::new_rand())
814                    .unwrap();
815                bank.register_unique_recent_blockhash_for_test();
816                bank.transfer(amount, &mint_keypair, &duplicate_pubkey)
817                    .unwrap();
818            }
819
820            // flush the write cache to disk to ensure there are duplicates across the storages
821            bank.fill_bank_with_ticks_for_tests();
822            bank.squash();
823            bank.force_flush_accounts_cache();
824        }
825
826        // Create a few more storages to exercise the zero lamport duplicates handling during
827        // generate_index(), which is used for the lattice-based accounts verification.
828        // There needs to be accounts that only have a single duplicate (i.e. there are only two
829        // versions of the accounts), and toggle between non-zero and zero lamports.
830        // One account will go zero -> non-zero, and the other will go non-zero -> zero.
831        let num_accounts = 2;
832        let accounts: Vec<_> = iter::repeat_with(Keypair::new).take(num_accounts).collect();
833        for i in 0..num_accounts {
834            let slot = bank.slot() + 1;
835            let leader = *bank.leader();
836            bank = Bank::new_from_parent_with_bank_forks(&bank_forks, bank, leader, slot);
837            bank.register_unique_recent_blockhash_for_test();
838
839            // transfer into the accounts so they start with a non-zero balance
840            for account in &accounts {
841                bank.transfer(amount, &mint_keypair, &account.pubkey())
842                    .unwrap();
843                assert_ne!(bank.get_balance(&account.pubkey()), 0);
844            }
845
846            // then transfer *out* all the lamports from one of 'em
847            bank.transfer(
848                bank.get_balance(&accounts[i].pubkey()),
849                &accounts[i],
850                &pubkey::new_rand(),
851            )
852            .unwrap();
853            assert_eq!(bank.get_balance(&accounts[i].pubkey()), 0);
854
855            // flush the write cache to disk to ensure the storages match the accounts written here
856            bank.fill_bank_with_ticks_for_tests();
857            bank.squash();
858            bank.force_flush_accounts_cache();
859        }
860        bank.set_block_id(Some(Hash::default()));
861
862        // verification happens at startup, so mimic the behavior by loading from a snapshot
863        let bank_snapshots_dir = TempDir::new().unwrap();
864        let snapshot_archives_dir = TempDir::new().unwrap();
865        let snapshot_config = SnapshotConfig {
866            full_snapshot_archives_dir: snapshot_archives_dir.path().to_path_buf(),
867            incremental_snapshot_archives_dir: snapshot_archives_dir.path().to_path_buf(),
868            bank_snapshots_dir: bank_snapshots_dir.path().to_path_buf(),
869            ..SnapshotConfig::default()
870        };
871        let snapshot =
872            snapshot_bank_utils::bank_to_full_snapshot_archive(&snapshot_config, &bank).unwrap();
873        let (_accounts_tempdir, accounts_dir) = snapshot_utils::create_tmp_accounts_dir_for_tests();
874        let accounts_index_config = AccountsIndexConfig {
875            index_limit: accounts_index_limit,
876            ..ACCOUNTS_INDEX_CONFIG_FOR_TESTING
877        };
878        let accounts_db_config = AccountsDbConfig {
879            index: Some(accounts_index_config),
880            ..ACCOUNTS_DB_CONFIG_FOR_TESTING
881        };
882        let roundtrip_bank = snapshot_bank_utils::bank_from_snapshot_archives(
883            &[accounts_dir],
884            &snapshot,
885            None,
886            &snapshot_config,
887            &genesis_config,
888            &RuntimeConfig::default(),
889            None,
890            None, // leader_for_tests
891            None,
892            false,
893            false,
894            false,
895            accounts_db_config,
896            None,
897            Arc::default(),
898        )
899        .unwrap();
900
901        // Correctly calculating the accounts lt hash in Bank::new_from_snapshot() depends on the
902        // bank being frozen.  This is so we don't call `update_accounts_lt_hash()` twice on the
903        // same bank!
904        assert!(roundtrip_bank.is_frozen());
905
906        assert_eq!(roundtrip_bank, *bank);
907    }
908
909    /// Ensure that the snapshot hash is correct
910    #[test_case(Features::None; "no features")]
911    #[test_case(Features::All; "all features")]
912    fn test_snapshots(features: Features) {
913        let (genesis_config, mint_keypair) = genesis_config_with(features);
914        let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
915
916        let amount = cmp::max(
917            bank.get_minimum_balance_for_rent_exemption(0),
918            LAMPORTS_PER_SOL,
919        );
920
921        // create some banks with some modified accounts so that there are stored accounts
922        // (note: the number of banks is arbitrary)
923        for _ in 0..3 {
924            let slot = bank.slot() + 1;
925            let leader = *bank.leader();
926            bank = Bank::new_from_parent_with_bank_forks(&bank_forks, bank, leader, slot);
927            bank.register_unique_recent_blockhash_for_test();
928            bank.transfer(amount, &mint_keypair, &pubkey::new_rand())
929                .unwrap();
930            bank.fill_bank_with_ticks_for_tests();
931            bank.squash();
932            bank.force_flush_accounts_cache();
933        }
934        bank.set_block_id(Some(Hash::default()));
935
936        let bank_snapshots_dir = TempDir::new().unwrap();
937        let snapshot_archives_dir = TempDir::new().unwrap();
938        let snapshot_config = SnapshotConfig {
939            full_snapshot_archives_dir: snapshot_archives_dir.path().to_path_buf(),
940            incremental_snapshot_archives_dir: snapshot_archives_dir.path().to_path_buf(),
941            bank_snapshots_dir: bank_snapshots_dir.path().to_path_buf(),
942            ..SnapshotConfig::default()
943        };
944        let snapshot =
945            snapshot_bank_utils::bank_to_full_snapshot_archive(&snapshot_config, &bank).unwrap();
946        let (_accounts_tempdir, accounts_dir) = snapshot_utils::create_tmp_accounts_dir_for_tests();
947        let roundtrip_bank = snapshot_bank_utils::bank_from_snapshot_archives(
948            &[accounts_dir],
949            &snapshot,
950            None,
951            &snapshot_config,
952            &genesis_config,
953            &RuntimeConfig::default(),
954            None,
955            None, // leader_for_tests
956            None,
957            false,
958            false,
959            false,
960            ACCOUNTS_DB_CONFIG_FOR_TESTING,
961            None,
962            Arc::default(),
963        )
964        .unwrap();
965
966        assert_eq!(roundtrip_bank, *bank);
967    }
968
969    /// Ensure enqueue_off_chain_accounts_lt_hash_updates() catches duplicates in debug mode.
970    #[should_panic(expected = "duplicate accounts were enqueued for hashing")]
971    #[test_case(Features::None; "no features")]
972    #[test_case(Features::All; "all features")]
973    fn test_enqueue_off_chain_accounts_lt_hash_updates_catches_duplicates(features: Features) {
974        use rand::seq::SliceRandom as _;
975        let (genesis_config, _) = genesis_config_with(features);
976        let bank = Bank::new_for_tests(&genesis_config);
977
978        let pubkey1 = pubkey::new_rand();
979        let pubkey2 = pubkey::new_rand();
980        let pubkey3 = pubkey::new_rand();
981
982        let mut accounts = [
983            // one version of pubkey1
984            (&pubkey1, &AccountSharedData::new(11, 0, &Pubkey::default())),
985            // two versions of pubkey2
986            (&pubkey2, &AccountSharedData::new(21, 0, &Pubkey::default())),
987            (&pubkey2, &AccountSharedData::new(22, 0, &Pubkey::default())),
988            // three versions of pubkey3
989            (&pubkey3, &AccountSharedData::new(31, 0, &Pubkey::default())),
990            (&pubkey3, &AccountSharedData::new(32, 0, &Pubkey::default())),
991            (&pubkey3, &AccountSharedData::new(33, 0, &Pubkey::default())),
992        ];
993        accounts.shuffle(&mut rand::rng());
994
995        bank.store_accounts((bank.slot(), accounts.as_slice()), None);
996    }
997
998    /// Ensure freelist respects max size.
999    #[test]
1000    fn test_freelist_max_capacity() {
1001        type Container = ahash::HashSet<u64>;
1002
1003        // This test uses a hashbrown container, which has some special power-of-two sizing plus
1004        // a buffer.  So create the container first, and use that to derive the max capacity.
1005        let container = Container::with_capacity(77);
1006
1007        let max_capacity = container.capacity();
1008        let max_bytes = max_capacity * size_of::<u64>();
1009        let mut freelist = HashSetFreelist::new(10, Some(max_bytes));
1010
1011        // pushing a container that is too big will not actually push
1012        freelist.try_push(Container::with_capacity(max_capacity + 1));
1013        let stats0 = freelist.stats();
1014        assert_eq!(stats0.num_containers, 0);
1015        assert_eq!(stats0.capacity_elems, 0);
1016        assert_eq!(stats0.capacity_bytes, 0);
1017
1018        // pushing a container that is not too big will actually push
1019        freelist.try_push(container);
1020        let stats1 = freelist.stats();
1021        assert_eq!(stats1.num_containers, 1);
1022        assert_eq!(stats1.capacity_elems, max_capacity);
1023        assert_eq!(stats1.capacity_bytes, max_bytes);
1024
1025        // pushing a container that would exceed capacity will not push
1026        freelist.try_push(Container::with_capacity(1));
1027        assert_eq!(freelist.stats(), stats1);
1028
1029        // ...but, if we remove the limit, push should work again
1030        freelist.max_capacity = None;
1031        let container = Container::with_capacity(1);
1032        let container_capacity = container.capacity();
1033        freelist.try_push(container);
1034        let stats2 = freelist.stats();
1035        assert_eq!(stats2.num_containers, 2);
1036        assert_eq!(stats2.capacity_elems, max_capacity + container_capacity);
1037        assert_eq!(
1038            stats2.capacity_bytes,
1039            max_bytes + container_capacity * size_of::<u64>(),
1040        );
1041    }
1042
1043    /// Ensure concurrent pushes do not exceed the freelist's max capacity.
1044    #[test]
1045    fn test_freelist_concurrent_push() {
1046        // This test uses a hashbrown container, which has some special power-of-two sizing plus
1047        // a buffer.  So create the container first, and use that to derive the max capacity.
1048        let container = ahash::HashSet::<u64>::with_capacity(77);
1049
1050        let num_threads = 16;
1051        let barrier = Arc::new(Barrier::new(num_threads));
1052        let max_capacity = container.capacity();
1053        let max_bytes = max_capacity * size_of::<u64>();
1054        let freelist = Arc::new(HashSetFreelist::new(num_threads, Some(max_bytes)));
1055
1056        let threads: Vec<_> = iter::repeat_with(|| {
1057            let container = container.clone();
1058            let freelist = Arc::clone(&freelist);
1059            let barrier = Arc::clone(&barrier);
1060            thread::spawn(move || {
1061                barrier.wait();
1062                freelist.try_push(container);
1063            })
1064        })
1065        .take(num_threads)
1066        .collect();
1067
1068        for thread in threads {
1069            thread.join().unwrap();
1070        }
1071
1072        let stats = freelist.stats();
1073        assert_eq!(stats.num_containers, 1);
1074        assert_eq!(stats.capacity_elems, max_capacity);
1075        assert_eq!(stats.capacity_bytes, max_bytes);
1076    }
1077}