solana_core/
optimistic_confirmation_verifier.rs

1use {
2    crate::cluster_info_vote_listener::VoteTracker,
3    solana_clock::Slot,
4    solana_hash::Hash,
5    solana_ledger::blockstore::Blockstore,
6    solana_runtime::bank::Bank,
7    solana_time_utils::timestamp,
8    std::{collections::BTreeSet, time::Instant},
9};
10
11pub struct OptimisticConfirmationVerifier {
12    snapshot_start_slot: Slot,
13    unchecked_slots: BTreeSet<(Slot, Hash)>,
14    last_optimistic_slot_ts: Instant,
15}
16
17impl OptimisticConfirmationVerifier {
18    pub fn new(snapshot_start_slot: Slot) -> Self {
19        Self {
20            snapshot_start_slot,
21            unchecked_slots: BTreeSet::default(),
22            last_optimistic_slot_ts: Instant::now(),
23        }
24    }
25
26    // Returns any optimistic slots that were not rooted
27    pub fn verify_for_unrooted_optimistic_slots(
28        &mut self,
29        root_bank: &Bank,
30        blockstore: &Blockstore,
31    ) -> Vec<(Slot, Hash)> {
32        let root = root_bank.slot();
33        let root_ancestors = &root_bank.ancestors;
34        let slots_after_root = self
35            .unchecked_slots
36            .split_off(&((root + 1), Hash::default()));
37        // `slots_before_root` now contains all slots <= root
38        let slots_before_root = std::mem::replace(&mut self.unchecked_slots, slots_after_root);
39        slots_before_root
40            .into_iter()
41            .filter(|(optimistic_slot, optimistic_hash)| {
42                (*optimistic_slot == root && *optimistic_hash != root_bank.hash())
43                    || (!root_ancestors.contains_key(optimistic_slot) &&
44                    // In this second part of the `and`, we account for the possibility that
45                    // there was some other root `rootX` set in BankForks where:
46                    //
47                    // `root` > `rootX` > `optimistic_slot`
48                    //
49                    // in which case `root` may  not contain the ancestor information for
50                    // slots < `rootX`, so we also have to check if `optimistic_slot` was rooted
51                    // through blockstore.
52                    !blockstore.is_root(*optimistic_slot))
53            })
54            .collect()
55    }
56
57    pub fn add_new_optimistic_confirmed_slots(
58        &mut self,
59        new_optimistic_slots: Vec<(Slot, Hash)>,
60        blockstore: &Blockstore,
61    ) {
62        if new_optimistic_slots.is_empty() {
63            return;
64        }
65
66        // We don't have any information about ancestors before the snapshot root,
67        // so ignore those slots
68        for (new_optimistic_slot, hash) in new_optimistic_slots {
69            if new_optimistic_slot > self.snapshot_start_slot {
70                if let Err(e) = blockstore.insert_optimistic_slot(
71                    new_optimistic_slot,
72                    &hash,
73                    timestamp().try_into().unwrap(),
74                ) {
75                    error!(
76                        "failed to record optimistic slot in blockstore: slot={}: {:?}",
77                        new_optimistic_slot, &e
78                    );
79                }
80                datapoint_info!("optimistic_slot", ("slot", new_optimistic_slot, i64),);
81                self.unchecked_slots.insert((new_optimistic_slot, hash));
82            }
83        }
84
85        self.last_optimistic_slot_ts = Instant::now();
86    }
87
88    pub fn format_optimistic_confirmed_slot_violation_log(slot: Slot) -> String {
89        format!("Optimistically confirmed slot {slot} was not rooted")
90    }
91
92    pub fn log_unrooted_optimistic_slots(
93        root_bank: &Bank,
94        vote_tracker: &VoteTracker,
95        unrooted_optimistic_slots: &[(Slot, Hash)],
96    ) {
97        let root = root_bank.slot();
98        for (optimistic_slot, hash) in unrooted_optimistic_slots.iter() {
99            let epoch = root_bank.epoch_schedule().get_epoch(*optimistic_slot);
100            let epoch_stakes = root_bank.epoch_stakes(epoch);
101            let total_epoch_stake = epoch_stakes.map(|e| e.total_stake()).unwrap_or(0);
102            let voted_stake = {
103                let slot_tracker = vote_tracker.get_slot_vote_tracker(*optimistic_slot);
104                let r_slot_tracker = slot_tracker.as_ref().map(|s| s.read().unwrap());
105                let voted_stake = r_slot_tracker
106                    .as_ref()
107                    .and_then(|s| s.optimistic_votes_tracker(hash))
108                    .map(|s| s.stake())
109                    .unwrap_or(0);
110
111                error!(
112                    "{}, hash: {hash}, epoch: {epoch}, voted keys: {:?}, root: {root}, root bank \
113                     hash: {}, voted stake: {voted_stake}, total epoch stake: \
114                     {total_epoch_stake}, pct: {}",
115                    Self::format_optimistic_confirmed_slot_violation_log(*optimistic_slot),
116                    r_slot_tracker
117                        .as_ref()
118                        .and_then(|s| s.optimistic_votes_tracker(hash))
119                        .map(|s| s.voted()),
120                    root_bank.hash(),
121                    voted_stake as f64 / total_epoch_stake as f64,
122                );
123                voted_stake
124            };
125
126            datapoint_warn!(
127                "optimistic_slot_not_rooted",
128                ("slot", *optimistic_slot, i64),
129                ("epoch", epoch, i64),
130                ("root", root, i64),
131                ("voted_stake", voted_stake, i64),
132                ("total_epoch_stake", total_epoch_stake, i64),
133            );
134        }
135    }
136}
137
138#[cfg(test)]
139mod test {
140    use {
141        super::*, crate::vote_simulator::VoteSimulator,
142        solana_ledger::get_tmp_ledger_path_auto_delete, solana_pubkey::Pubkey,
143        solana_runtime::bank::Bank, std::collections::HashMap, trees::tr,
144    };
145
146    #[test]
147    fn test_add_new_optimistic_confirmed_slots() {
148        let snapshot_start_slot = 10;
149        let bank_hash = Hash::default();
150        let mut optimistic_confirmation_verifier =
151            OptimisticConfirmationVerifier::new(snapshot_start_slot);
152        let blockstore_path = get_tmp_ledger_path_auto_delete!();
153        let blockstore = Blockstore::open(blockstore_path.path()).unwrap();
154        optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots(
155            vec![(snapshot_start_slot - 1, bank_hash)],
156            &blockstore,
157        );
158        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 0);
159        optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots(
160            vec![(snapshot_start_slot, bank_hash)],
161            &blockstore,
162        );
163        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 0);
164        optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots(
165            vec![(snapshot_start_slot + 1, bank_hash)],
166            &blockstore,
167        );
168        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 1);
169        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
170        assert!(optimistic_confirmation_verifier
171            .unchecked_slots
172            .contains(&(snapshot_start_slot + 1, bank_hash)));
173    }
174
175    #[test]
176    fn test_get_unrooted_optimistic_slots_same_slot_different_hash() {
177        let snapshot_start_slot = 0;
178        let mut optimistic_confirmation_verifier =
179            OptimisticConfirmationVerifier::new(snapshot_start_slot);
180        let bad_bank_hash = Hash::new_from_array([42u8; 32]);
181        let blockstore_path = get_tmp_ledger_path_auto_delete!();
182        let blockstore = Blockstore::open(blockstore_path.path()).unwrap();
183        let optimistic_slots = vec![(1, bad_bank_hash), (3, Hash::default())];
184        optimistic_confirmation_verifier
185            .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore);
186        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 2);
187        let vote_simulator = setup_forks();
188        let bank1 = vote_simulator.bank_forks.read().unwrap().get(1).unwrap();
189        assert_eq!(
190            optimistic_confirmation_verifier
191                .verify_for_unrooted_optimistic_slots(&bank1, &blockstore),
192            vec![(1, bad_bank_hash)]
193        );
194        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
195        assert!(optimistic_confirmation_verifier
196            .unchecked_slots
197            .contains(&(3, Hash::default())));
198    }
199
200    #[test]
201    fn test_get_unrooted_optimistic_slots() {
202        let snapshot_start_slot = 0;
203        let mut optimistic_confirmation_verifier =
204            OptimisticConfirmationVerifier::new(snapshot_start_slot);
205        let blockstore_path = get_tmp_ledger_path_auto_delete!();
206        let blockstore = Blockstore::open(blockstore_path.path()).unwrap();
207        let mut vote_simulator = setup_forks();
208        let optimistic_slots: Vec<_> = vec![1, 3, 5]
209            .into_iter()
210            .map(|s| {
211                (
212                    s,
213                    vote_simulator
214                        .bank_forks
215                        .read()
216                        .unwrap()
217                        .get(s)
218                        .unwrap()
219                        .hash(),
220                )
221            })
222            .collect();
223
224        // If root is on same fork, nothing should be returned
225        optimistic_confirmation_verifier
226            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
227        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3);
228        let bank5 = vote_simulator.bank_forks.read().unwrap().get(5).unwrap();
229        assert!(optimistic_confirmation_verifier
230            .verify_for_unrooted_optimistic_slots(&bank5, &blockstore)
231            .is_empty());
232        // 5 is >= than all the unchecked slots, so should clear everything
233        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
234
235        // If root is on same fork, nothing should be returned
236        optimistic_confirmation_verifier
237            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
238        let bank3 = vote_simulator.bank_forks.read().unwrap().get(3).unwrap();
239        assert!(optimistic_confirmation_verifier
240            .verify_for_unrooted_optimistic_slots(&bank3, &blockstore)
241            .is_empty());
242        // 3 is bigger than only slot 1, so slot 5 should be left over
243        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
244        assert!(optimistic_confirmation_verifier
245            .unchecked_slots
246            .contains(&optimistic_slots[2]));
247
248        // If root is on different fork, the slots < root on different fork should
249        // be returned
250        optimistic_confirmation_verifier
251            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
252        let bank4 = vote_simulator.bank_forks.read().unwrap().get(4).unwrap();
253        assert_eq!(
254            optimistic_confirmation_verifier
255                .verify_for_unrooted_optimistic_slots(&bank4, &blockstore),
256            vec![optimistic_slots[1]]
257        );
258        // 4 is bigger than only slots 1 and 3, so slot 5 should be left over
259        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
260        assert!(optimistic_confirmation_verifier
261            .unchecked_slots
262            .contains(&optimistic_slots[2]));
263
264        // Now set a root at slot 5, purging BankForks of slots < 5
265        vote_simulator.set_root(5);
266
267        // Add a new bank 7 that descends from 6
268        let bank6 = vote_simulator.bank_forks.read().unwrap().get(6).unwrap();
269        vote_simulator
270            .bank_forks
271            .write()
272            .unwrap()
273            .insert(Bank::new_from_parent(bank6, &Pubkey::default(), 7));
274        let bank7 = vote_simulator.bank_forks.read().unwrap().get(7).unwrap();
275        assert!(!bank7.ancestors.contains_key(&3));
276
277        // Should return slots 1, 3 as part of the rooted fork because there's no
278        // ancestry information
279        optimistic_confirmation_verifier
280            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
281        assert_eq!(
282            optimistic_confirmation_verifier
283                .verify_for_unrooted_optimistic_slots(&bank7, &blockstore),
284            optimistic_slots[0..=1].to_vec()
285        );
286        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
287
288        // If we know set the root in blockstore, should return nothing
289        blockstore.set_roots([1, 3].iter()).unwrap();
290        optimistic_confirmation_verifier
291            .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore);
292        assert!(optimistic_confirmation_verifier
293            .verify_for_unrooted_optimistic_slots(&bank7, &blockstore)
294            .is_empty());
295        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
296        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3);
297    }
298
299    fn setup_forks() -> VoteSimulator {
300        /*
301            Build fork structure:
302                 slot 0
303                   |
304                 slot 1
305                 /    \
306            slot 2    |
307               |    slot 3
308            slot 4    |
309                    slot 5
310                      |
311                    slot 6
312        */
313        let forks = tr(0) / (tr(1) / (tr(2) / (tr(4))) / (tr(3) / (tr(5) / (tr(6)))));
314
315        let mut vote_simulator = VoteSimulator::new(1);
316        vote_simulator.fill_bank_forks(forks, &HashMap::new(), true);
317        vote_simulator
318    }
319}