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.
This commit is contained in:
steviez 2023-07-05 10:18:15 -04:00 committed by GitHub
parent 52fe7eb1cf
commit 5feebd2dc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 131 additions and 34 deletions

View File

@ -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<OptimisticSlotInfo> {
// 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<Utc> = 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<Utc> = 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);
}
}

View File

@ -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<Option<(Hash, UnixTimestamp)>> {
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,