From 5feebd2dc879274f74a4f4e5a8dd608cd2296acc Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 5 Jul 2023 10:18:15 -0400 Subject: [PATCH] ledger-tool: Manually walk optimistic slots ancestors (#32362) If a slot is marked as optimistically confirmed, it is probable but not guaranteed that its' ancestors will also be marked as optimistically confirmed in the Blockstore. Given the importance of examining optimistically confirmed slots around cluster restarts, manually walk an AncestorIterator to avoid the chance of a slot improperly being ignored in cluster restart scenarios. --- ledger-tool/src/main.rs | 156 ++++++++++++++++++++++++++++++--------- ledger/src/blockstore.rs | 9 +++ 2 files changed, 131 insertions(+), 34 deletions(-) diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index b1caabda1a..d61e124457 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -64,7 +64,7 @@ use { solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, account_utils::StateMut, - clock::{Epoch, Slot}, + clock::{Epoch, Slot, UnixTimestamp}, feature::{self, Feature}, feature_set::{self, FeatureSet}, genesis_config::ClusterType, @@ -954,6 +954,72 @@ fn compute_slot_cost(blockstore: &Blockstore, slot: Slot) -> Result<(), String> Ok(()) } +/// Returns true if the supplied slot contains any nonvote transactions +fn slot_contains_nonvote_tx(blockstore: &Blockstore, slot: Slot) -> bool { + let (entries, _, _) = blockstore + .get_slot_entries_with_shred_info(slot, 0, false) + .expect("Failed to get slot entries"); + let contains_nonvote = entries + .iter() + .flat_map(|entry| entry.transactions.iter()) + .flat_map(get_program_ids) + .any(|program_id| *program_id != solana_vote_program::id()); + contains_nonvote +} + +type OptimisticSlotInfo = (Slot, Option<(Hash, UnixTimestamp)>, bool); + +/// Return the latest `num_slots` optimistically confirmed slots, including +/// ancestors of optimistically confirmed slots that may not have been marked +/// as optimistically confirmed themselves. +fn get_latest_optimistic_slots( + blockstore: &Blockstore, + num_slots: usize, + exclude_vote_only_slots: bool, +) -> Vec { + // Consider a chain X -> Y -> Z where X and Z have been optimistically + // confirmed. Given that Y is an ancestor of Z, Y can implicitly be + // considered as optimistically confirmed. However, there isn't an explicit + // guarantee that Y will be marked as optimistically confirmed in the + // blockstore. + // + // Because retrieving optimistically confirmed slots is an important part + // of cluster restarts, exercise caution in this function and manually walk + // the ancestors of the latest optimistically confirmed slot instead of + // solely relying on the contents of the optimistically confirmed column. + let Some(latest_slot) = blockstore + .get_latest_optimistic_slots(1) + .expect("get_latest_optimistic_slots() failed") + .pop() else { + eprintln!("Blockstore does not contain any optimistically confirmed slots"); + return vec![]; + }; + let latest_slot = latest_slot.0; + + let slot_iter = AncestorIterator::new_inclusive(latest_slot, blockstore).map(|slot| { + let contains_nonvote_tx = slot_contains_nonvote_tx(blockstore, slot); + let hash_and_timestamp_opt = blockstore + .get_optimistic_slot(slot) + .expect("get_optimistic_slot() failed"); + if hash_and_timestamp_opt.is_none() { + warn!( + "Slot {slot} is an ancestor of latest optimistically confirmed slot \ + {latest_slot}, but was not marked as optimistically confirmed in blockstore." + ); + } + (slot, hash_and_timestamp_opt, contains_nonvote_tx) + }); + + if exclude_vote_only_slots { + slot_iter + .filter(|(_, _, contains_nonvote)| *contains_nonvote) + .take(num_slots) + .collect() + } else { + slot_iter.take(num_slots).collect() + } +} + /// Finds the accounts needed to replay slots `snapshot_slot` to `ending_slot`. /// Removes all other accounts from accounts_db, and updates the accounts hash /// and capitalization. This is used by the --minimize option in create-snapshot @@ -3902,44 +3968,26 @@ fn main() { ); let num_slots = value_t_or_exit!(arg_matches, "num_slots", usize); let exclude_vote_only_slots = arg_matches.is_present("exclude_vote_only_slots"); - - let slots_iter = blockstore - .reversed_optimistic_slots_iterator() - .expect("Failed to get reversed optimistic slots iterator") - .map(|(slot, hash, timestamp)| { - let (entries, _, _) = blockstore - .get_slot_entries_with_shred_info(slot, 0, false) - .expect("Failed to get slot entries"); - let contains_nonvote = entries - .iter() - .flat_map(|entry| entry.transactions.iter()) - .flat_map(get_program_ids) - .any(|program_id| *program_id != solana_vote_program::id()); - (slot, hash, timestamp, contains_nonvote) - }); - - let slots: Vec<_> = if exclude_vote_only_slots { - slots_iter - .filter(|(_, _, _, contains_nonvote)| *contains_nonvote) - .take(num_slots) - .collect() - } else { - slots_iter.take(num_slots).collect() - }; + let slots = + get_latest_optimistic_slots(&blockstore, num_slots, exclude_vote_only_slots); println!( "{:>20} {:>44} {:>32} {:>13}", "Slot", "Hash", "Timestamp", "Vote Only?" ); - for (slot, hash, timestamp, contains_nonvote) in slots.iter() { - let time_str = { - let secs: u64 = (timestamp / 1_000) as u64; - let nanos: u32 = ((timestamp % 1_000) * 1_000_000) as u32; - let t = UNIX_EPOCH + Duration::new(secs, nanos); - let datetime: DateTime = t.into(); - datetime.to_rfc3339() - }; - let hash_str = format!("{hash}"); + for (slot, hash_and_timestamp_opt, contains_nonvote) in slots.iter() { + let (time_str, hash_str) = + if let Some((hash, timestamp)) = hash_and_timestamp_opt { + let secs: u64 = (timestamp / 1_000) as u64; + let nanos: u32 = ((timestamp % 1_000) * 1_000_000) as u32; + let t = UNIX_EPOCH + Duration::new(secs, nanos); + let datetime: DateTime = t.into(); + + (datetime.to_rfc3339(), format!("{hash}")) + } else { + let unknown = "Unknown"; + (String::from(unknown), String::from(unknown)) + }; println!( "{:>20} {:>44} {:>32} {:>13}", slot, &hash_str, &time_str, !contains_nonvote @@ -4113,3 +4161,43 @@ fn main() { info!("{}", measure_total_execution_time); } } + +#[cfg(test)] +pub mod tests { + use { + super::*, + solana_ledger::{blockstore::make_many_slot_entries, get_tmp_ledger_path_auto_delete}, + }; + + #[test] + fn test_latest_optimistic_ancestors() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + // Insert 5 slots into blockstore + let start_slot = 0; + let num_slots = 5; + let entries_per_shred = 5; + let (shreds, _) = make_many_slot_entries(start_slot, num_slots, entries_per_shred); + blockstore.insert_shreds(shreds, None, false).unwrap(); + + // Mark even shreds as optimistically confirmed + (0..num_slots).step_by(2).for_each(|slot| { + blockstore + .insert_optimistic_slot(slot, &Hash::default(), UnixTimestamp::default()) + .unwrap(); + }); + + let exclude_vote_only_slots = false; + let optimistic_slots: Vec<_> = + get_latest_optimistic_slots(&blockstore, num_slots as usize, exclude_vote_only_slots) + .iter() + .map(|(slot, _, _)| *slot) + .collect(); + + // Should see all slots here since they're all chained, despite only evens getting marked + // get_latest_optimistic_slots() returns slots in descending order so use .rev() + let expected: Vec<_> = (start_slot..num_slots).rev().collect(); + assert_eq!(optimistic_slots, expected); + } +} diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index eccfd1fc07..dfa2e58e68 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -3103,6 +3103,15 @@ impl Blockstore { self.optimistic_slots_cf.put(slot, &slot_data) } + /// Returns information about a single optimistically confirmed slot + pub fn get_optimistic_slot(&self, slot: Slot) -> Result> { + Ok(self + .optimistic_slots_cf + .get(slot)? + .map(|meta| (meta.hash(), meta.timestamp()))) + } + + /// Returns information about the `num` latest optimistically confirmed slot pub fn get_latest_optimistic_slots( &self, num: usize,