use { crate::cluster_info_vote_listener::VoteTracker, solana_ledger::blockstore::Blockstore, solana_runtime::bank::Bank, solana_sdk::{clock::Slot, hash::Hash, timing::timestamp}, std::{collections::BTreeSet, time::Instant}, }; pub struct OptimisticConfirmationVerifier { snapshot_start_slot: Slot, unchecked_slots: BTreeSet<(Slot, Hash)>, last_optimistic_slot_ts: Instant, } impl OptimisticConfirmationVerifier { pub fn new(snapshot_start_slot: Slot) -> Self { Self { snapshot_start_slot, unchecked_slots: BTreeSet::default(), last_optimistic_slot_ts: Instant::now(), } } // Returns any optimistic slots that were not rooted pub fn verify_for_unrooted_optimistic_slots( &mut self, root_bank: &Bank, blockstore: &Blockstore, ) -> Vec<(Slot, Hash)> { let root = root_bank.slot(); let root_ancestors = &root_bank.ancestors; let slots_after_root = self .unchecked_slots .split_off(&((root + 1), Hash::default())); // `slots_before_root` now contains all slots <= root let slots_before_root = std::mem::replace(&mut self.unchecked_slots, slots_after_root); slots_before_root .into_iter() .filter(|(optimistic_slot, optimistic_hash)| { (*optimistic_slot == root && *optimistic_hash != root_bank.hash()) || (!root_ancestors.contains_key(optimistic_slot) && // In this second part of the `and`, we account for the possibility that // there was some other root `rootX` set in BankForks where: // // `root` > `rootX` > `optimistic_slot` // // in which case `root` may not contain the ancestor information for // slots < `rootX`, so we also have to check if `optimistic_slot` was rooted // through blockstore. !blockstore.is_root(*optimistic_slot)) }) .collect() } pub fn add_new_optimistic_confirmed_slots( &mut self, new_optimistic_slots: Vec<(Slot, Hash)>, blockstore: &Blockstore, ) { if new_optimistic_slots.is_empty() { return; } datapoint_info!( "optimistic_slot_elapsed", ( "average_elapsed_ms", self.last_optimistic_slot_ts.elapsed().as_millis() as i64, i64 ), ); // We don't have any information about ancestors before the snapshot root, // so ignore those slots for (new_optimistic_slot, hash) in new_optimistic_slots { if new_optimistic_slot > self.snapshot_start_slot { if let Err(e) = blockstore.insert_optimistic_slot( new_optimistic_slot, &hash, timestamp().try_into().unwrap(), ) { error!( "failed to record optimistic slot in blockstore: slot={}: {:?}", new_optimistic_slot, &e ); } datapoint_info!("optimistic_slot", ("slot", new_optimistic_slot, i64),); self.unchecked_slots.insert((new_optimistic_slot, hash)); } } self.last_optimistic_slot_ts = Instant::now(); } pub fn format_optimistic_confirmed_slot_violation_log(slot: Slot) -> String { format!("Optimistically confirmed slot {slot} was not rooted") } pub fn log_unrooted_optimistic_slots( root_bank: &Bank, vote_tracker: &VoteTracker, unrooted_optimistic_slots: &[(Slot, Hash)], ) { let root = root_bank.slot(); for (optimistic_slot, hash) in unrooted_optimistic_slots.iter() { let epoch = root_bank.epoch_schedule().get_epoch(*optimistic_slot); let epoch_stakes = root_bank.epoch_stakes(epoch); let total_epoch_stake = epoch_stakes.map(|e| e.total_stake()).unwrap_or(0); let voted_stake = { let slot_tracker = vote_tracker.get_slot_vote_tracker(*optimistic_slot); let r_slot_tracker = slot_tracker.as_ref().map(|s| s.read().unwrap()); let voted_stake = r_slot_tracker .as_ref() .and_then(|s| s.optimistic_votes_tracker(hash)) .map(|s| s.stake()) .unwrap_or(0); error!( "{}, hash: {}, epoch: {}, voted keys: {:?}, root: {}, root bank hash: {}, voted stake: {}, total epoch stake: {}, pct: {}", Self::format_optimistic_confirmed_slot_violation_log(*optimistic_slot), hash, epoch, r_slot_tracker .as_ref() .and_then(|s| s.optimistic_votes_tracker(hash)) .map(|s| s.voted()), root, root_bank.hash(), voted_stake, total_epoch_stake, voted_stake as f64 / total_epoch_stake as f64, ); voted_stake }; datapoint_warn!( "optimistic_slot_not_rooted", ("slot", *optimistic_slot, i64), ("epoch", epoch, i64), ("root", root, i64), ("voted_stake", voted_stake, i64), ("total_epoch_stake", total_epoch_stake, i64), ); } } } #[cfg(test)] mod test { use { super::*, crate::vote_simulator::VoteSimulator, solana_ledger::get_tmp_ledger_path_auto_delete, solana_runtime::bank::Bank, solana_sdk::pubkey::Pubkey, std::collections::HashMap, trees::tr, }; #[test] fn test_add_new_optimistic_confirmed_slots() { let snapshot_start_slot = 10; let bank_hash = Hash::default(); let mut optimistic_confirmation_verifier = OptimisticConfirmationVerifier::new(snapshot_start_slot); let blockstore_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(blockstore_path.path()).unwrap(); optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots( vec![(snapshot_start_slot - 1, bank_hash)], &blockstore, ); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 0); optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots( vec![(snapshot_start_slot, bank_hash)], &blockstore, ); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 0); optimistic_confirmation_verifier.add_new_optimistic_confirmed_slots( vec![(snapshot_start_slot + 1, bank_hash)], &blockstore, ); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 1); assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1); assert!(optimistic_confirmation_verifier .unchecked_slots .contains(&(snapshot_start_slot + 1, bank_hash))); } #[test] fn test_get_unrooted_optimistic_slots_same_slot_different_hash() { let snapshot_start_slot = 0; let mut optimistic_confirmation_verifier = OptimisticConfirmationVerifier::new(snapshot_start_slot); let bad_bank_hash = Hash::new(&[42u8; 32]); let blockstore_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(blockstore_path.path()).unwrap(); let optimistic_slots = vec![(1, bad_bank_hash), (3, Hash::default())]; optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 2); let vote_simulator = setup_forks(); let bank1 = vote_simulator.bank_forks.read().unwrap().get(1).unwrap(); assert_eq!( optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank1, &blockstore), vec![(1, bad_bank_hash)] ); assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1); assert!(optimistic_confirmation_verifier .unchecked_slots .contains(&(3, Hash::default()))); } #[test] fn test_get_unrooted_optimistic_slots() { let snapshot_start_slot = 0; let mut optimistic_confirmation_verifier = OptimisticConfirmationVerifier::new(snapshot_start_slot); let blockstore_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(blockstore_path.path()).unwrap(); let mut vote_simulator = setup_forks(); let optimistic_slots: Vec<_> = vec![1, 3, 5] .into_iter() .map(|s| { ( s, vote_simulator .bank_forks .read() .unwrap() .get(s) .unwrap() .hash(), ) }) .collect(); // If root is on same fork, nothing should be returned optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3); let bank5 = vote_simulator.bank_forks.read().unwrap().get(5).unwrap(); assert!(optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank5, &blockstore) .is_empty()); // 5 is >= than all the unchecked slots, so should clear everything assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty()); // If root is on same fork, nothing should be returned optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore); let bank3 = vote_simulator.bank_forks.read().unwrap().get(3).unwrap(); assert!(optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank3, &blockstore) .is_empty()); // 3 is bigger than only slot 1, so slot 5 should be left over assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1); assert!(optimistic_confirmation_verifier .unchecked_slots .contains(&optimistic_slots[2])); // If root is on different fork, the slots < root on different fork should // be returned optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore); let bank4 = vote_simulator.bank_forks.read().unwrap().get(4).unwrap(); assert_eq!( optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank4, &blockstore), vec![optimistic_slots[1]] ); // 4 is bigger than only slots 1 and 3, so slot 5 should be left over assert_eq!(optimistic_confirmation_verifier.unchecked_slots.len(), 1); assert!(optimistic_confirmation_verifier .unchecked_slots .contains(&optimistic_slots[2])); // Now set a root at slot 5, purging BankForks of slots < 5 vote_simulator.set_root(5); // Add a new bank 7 that descends from 6 let bank6 = vote_simulator.bank_forks.read().unwrap().get(6).unwrap(); vote_simulator .bank_forks .write() .unwrap() .insert(Bank::new_from_parent(&bank6, &Pubkey::default(), 7)); let bank7 = vote_simulator.bank_forks.read().unwrap().get(7).unwrap(); assert!(!bank7.ancestors.contains_key(&3)); // Should return slots 1, 3 as part of the rooted fork because there's no // ancestry information optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots.clone(), &blockstore); assert_eq!( optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank7, &blockstore), optimistic_slots[0..=1].to_vec() ); assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty()); // If we know set the root in blockstore, should return nothing blockstore.set_roots(vec![1, 3].iter()).unwrap(); optimistic_confirmation_verifier .add_new_optimistic_confirmed_slots(optimistic_slots, &blockstore); assert!(optimistic_confirmation_verifier .verify_for_unrooted_optimistic_slots(&bank7, &blockstore) .is_empty()); assert!(optimistic_confirmation_verifier.unchecked_slots.is_empty()); assert_eq!(blockstore.get_latest_optimistic_slots(10).unwrap().len(), 3); } fn setup_forks() -> VoteSimulator { /* Build fork structure: slot 0 | slot 1 / \ slot 2 | | slot 3 slot 4 | slot 5 | slot 6 */ let forks = tr(0) / (tr(1) / (tr(2) / (tr(4))) / (tr(3) / (tr(5) / (tr(6))))); let mut vote_simulator = VoteSimulator::new(1); vote_simulator.fill_bank_forks(forks, &HashMap::new(), true); vote_simulator } }