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