Skip to main content

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: \
77                         slot={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, solana_leader_schedule::SlotLeader,
142        solana_ledger::get_tmp_ledger_path_auto_delete, solana_runtime::bank::Bank,
143        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!(
171            optimistic_confirmation_verifier
172                .unchecked_slots
173                .contains(&(snapshot_start_slot + 1, bank_hash))
174        );
175    }
176
177    #[test]
178    fn test_get_unrooted_optimistic_slots_same_slot_different_hash() {
179        let snapshot_start_slot = 0;
180        let mut optimistic_confirmation_verifier =
181            OptimisticConfirmationVerifier::new(snapshot_start_slot);
182        let bad_bank_hash = Hash::new_from_array([42u8; 32]);
183        let blockstore_path = get_tmp_ledger_path_auto_delete!();
184        let blockstore = Blockstore::open(blockstore_path.path()).unwrap();
185        let optimistic_slots = vec![(1, bad_bank_hash), (3, Hash::default())];
186        optimistic_confirmation_verifier
187            .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore);
188        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 2);
189        let vote_simulator = setup_forks();
190        let bank1 = vote_simulator.bank_forks.read().unwrap().get(1).unwrap();
191        assert_eq!(
192            optimistic_confirmation_verifier
193                .verify_for_unrooted_optimistic_slots(&bank1, &blockstore),
194            vec![(1, bad_bank_hash)]
195        );
196        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
197        assert!(
198            optimistic_confirmation_verifier
199                .unchecked_slots
200                .contains(&(3, Hash::default()))
201        );
202    }
203
204    #[test]
205    fn test_get_unrooted_optimistic_slots() {
206        let snapshot_start_slot = 0;
207        let mut optimistic_confirmation_verifier =
208            OptimisticConfirmationVerifier::new(snapshot_start_slot);
209        let blockstore_path = get_tmp_ledger_path_auto_delete!();
210        let blockstore = Blockstore::open(blockstore_path.path()).unwrap();
211        let mut vote_simulator = setup_forks();
212        let optimistic_slots: Vec<_> = vec![1, 3, 5]
213            .into_iter()
214            .map(|s| {
215                (
216                    s,
217                    vote_simulator
218                        .bank_forks
219                        .read()
220                        .unwrap()
221                        .get(s)
222                        .unwrap()
223                        .hash(),
224                )
225            })
226            .collect();
227
228        // If root is on same fork, nothing should be returned
229        optimistic_confirmation_verifier
230            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
231        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3);
232        let bank5 = vote_simulator.bank_forks.read().unwrap().get(5).unwrap();
233        assert!(
234            optimistic_confirmation_verifier
235                .verify_for_unrooted_optimistic_slots(&bank5, &blockstore)
236                .is_empty()
237        );
238        // 5 is >= than all the unchecked slots, so should clear everything
239        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
240
241        // If root is on same fork, nothing should be returned
242        optimistic_confirmation_verifier
243            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
244        let bank3 = vote_simulator.bank_forks.read().unwrap().get(3).unwrap();
245        assert!(
246            optimistic_confirmation_verifier
247                .verify_for_unrooted_optimistic_slots(&bank3, &blockstore)
248                .is_empty()
249        );
250        // 3 is bigger than only slot 1, so slot 5 should be left over
251        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
252        assert!(
253            optimistic_confirmation_verifier
254                .unchecked_slots
255                .contains(&optimistic_slots[2])
256        );
257
258        // If root is on different fork, the slots < root on different fork should
259        // be returned
260        optimistic_confirmation_verifier
261            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
262        let bank4 = vote_simulator.bank_forks.read().unwrap().get(4).unwrap();
263        assert_eq!(
264            optimistic_confirmation_verifier
265                .verify_for_unrooted_optimistic_slots(&bank4, &blockstore),
266            vec![optimistic_slots[1]]
267        );
268        // 4 is bigger than only slots 1 and 3, so slot 5 should be left over
269        assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1);
270        assert!(
271            optimistic_confirmation_verifier
272                .unchecked_slots
273                .contains(&optimistic_slots[2])
274        );
275
276        // Now set a root at slot 5, purging BankForks of slots < 5
277        vote_simulator.set_root(5);
278
279        // Add a new bank 7 that descends from 6
280        let bank6 = vote_simulator.bank_forks.read().unwrap().get(6).unwrap();
281        let bank7_new = Bank::new_from_parent(bank6, SlotLeader::default(), 7);
282        vote_simulator.bank_forks.write().unwrap().insert(bank7_new);
283        let bank7 = vote_simulator.bank_forks.read().unwrap().get(7).unwrap();
284        assert!(!bank7.ancestors.contains_key(&3));
285
286        // Should return slots 1, 3 as part of the rooted fork because there's no
287        // ancestry information
288        optimistic_confirmation_verifier
289            .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore);
290        assert_eq!(
291            optimistic_confirmation_verifier
292                .verify_for_unrooted_optimistic_slots(&bank7, &blockstore),
293            optimistic_slots[0..=1].to_vec()
294        );
295        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
296
297        // If we know set the root in blockstore, should return nothing
298        blockstore.set_roots([1, 3].iter()).unwrap();
299        optimistic_confirmation_verifier
300            .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore);
301        assert!(
302            optimistic_confirmation_verifier
303                .verify_for_unrooted_optimistic_slots(&bank7, &blockstore)
304                .is_empty()
305        );
306        assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty());
307        assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3);
308    }
309
310    fn setup_forks() -> VoteSimulator {
311        /*
312            Build fork structure:
313                 slot 0
314                   |
315                 slot 1
316                 /    \
317            slot 2    |
318               |    slot 3
319            slot 4    |
320                    slot 5
321                      |
322                    slot 6
323        */
324        let forks = tr(0) / (tr(1) / (tr(2) / (tr(4))) / (tr(3) / (tr(5) / (tr(6)))));
325
326        let mut vote_simulator = VoteSimulator::new(1);
327        vote_simulator.fill_bank_forks(forks, &HashMap::new(), true);
328        vote_simulator
329    }
330}