solana_runtime/
bank_hash_cache.rs

1//! A wrapper around bank forks that maintains a lightweight cache of bank hashes.
2//!
3//! Notified by bank forks when a slot is dumped due to duplicate block handling, allowing for the
4//! cache to be invalidated. This ensures that the cache is always in sync with bank forks as
5//! long as the local lock is held during querying.
6//!
7//! This can be useful to avoid read-locking the bank forks when querying bank hashes, as we only
8//! contend for the local lock during slot dumping due to duplicate blocks which should be extremely rare.
9
10use {
11    crate::{bank::Bank, bank_forks::BankForks, root_bank_cache::RootBankCache},
12    solana_clock::Slot,
13    solana_hash::Hash,
14    std::{
15        collections::BTreeMap,
16        sync::{Arc, Mutex, MutexGuard, RwLock},
17    },
18};
19
20// Used to notify bank hash cache that slots have been dumped by replay
21pub type DumpedSlotSubscription = Arc<Mutex<bool>>;
22
23pub struct BankHashCache {
24    hashes: BTreeMap<Slot, Hash>,
25    bank_forks: Arc<RwLock<BankForks>>,
26    root_bank_cache: RootBankCache,
27    last_root: Slot,
28    dumped_slot_subscription: DumpedSlotSubscription,
29}
30
31impl BankHashCache {
32    pub fn new(bank_forks: Arc<RwLock<BankForks>>) -> Self {
33        let root_bank_cache = RootBankCache::new(bank_forks.clone());
34        let dumped_slot_subscription = DumpedSlotSubscription::default();
35        bank_forks
36            .write()
37            .unwrap()
38            .register_dumped_slot_subscriber(dumped_slot_subscription.clone());
39        Self {
40            hashes: BTreeMap::default(),
41            bank_forks,
42            root_bank_cache,
43            last_root: 0,
44            dumped_slot_subscription,
45        }
46    }
47
48    pub fn dumped_slot_subscription(&self) -> DumpedSlotSubscription {
49        self.dumped_slot_subscription.clone()
50    }
51
52    /// Should only be used after `slots_dumped` is acquired from `dumped_slot_subscription` to
53    /// guarantee synchronicity with `self.bank_forks`. Multiple calls to `hash` will only be
54    /// consistent with each other if `slots_dumped` was not released in between, as otherwise a dump
55    /// could have occured inbetween.
56    pub fn hash(&mut self, slot: Slot, slots_dumped: &mut MutexGuard<bool>) -> Option<Hash> {
57        if **slots_dumped {
58            // We could be smarter and keep a fork cache to only clear affected slots from the cache,
59            // but dumping slots should be extremely rare so it is simpler to invalidate the entire cache.
60            self.hashes.clear();
61            **slots_dumped = false;
62        }
63
64        if let Some(hash) = self.hashes.get(&slot) {
65            return Some(*hash);
66        }
67
68        let Some(hash) = self.bank_forks.read().unwrap().bank_hash(slot) else {
69            // Bank not yet received, bail
70            return None;
71        };
72
73        if hash == Hash::default() {
74            // If we have not frozen the bank then bail
75            return None;
76        }
77
78        // Cache the slot for future lookup
79        let prev_hash = self.hashes.insert(slot, hash);
80        debug_assert!(
81            prev_hash.is_none(),
82            "Programmer error, this indicates we have dumped and replayed \
83             a block however the cache was not invalidated"
84        );
85        Some(hash)
86    }
87
88    pub fn root(&mut self) -> Slot {
89        self.get_root_bank_and_prune_cache().slot()
90    }
91
92    /// Returns the root bank and also prunes cache of any slots < root
93    pub fn get_root_bank_and_prune_cache(&mut self) -> Arc<Bank> {
94        let root_bank = self.root_bank_cache.root_bank();
95        if root_bank.slot() != self.last_root {
96            self.last_root = root_bank.slot();
97            self.hashes = self.hashes.split_off(&self.last_root);
98        }
99        root_bank
100    }
101}