Skip to main content

solana_runtime/bank/
accounts_lt_hash.rs

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