993 lines
34 KiB
Rust
993 lines
34 KiB
Rust
use crate::{
|
|
fork_choice::ForkChoice, heaviest_subtree_fork_choice::HeaviestSubtreeForkChoice,
|
|
progress_map::ProgressMap,
|
|
};
|
|
use solana_sdk::{clock::Slot, hash::Hash};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
pub(crate) type DuplicateSlotsTracker = BTreeSet<Slot>;
|
|
pub(crate) type GossipDuplicateConfirmedSlots = BTreeMap<Slot, Hash>;
|
|
type SlotStateHandler = fn(Slot, &Hash, Option<&Hash>, bool, bool) -> Vec<ResultingStateChange>;
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub enum SlotStateUpdate {
|
|
Frozen,
|
|
DuplicateConfirmed,
|
|
Dead,
|
|
Duplicate,
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub enum ResultingStateChange {
|
|
// Hash of our current frozen version of the slot
|
|
MarkSlotDuplicate(Hash),
|
|
// Hash of the cluster confirmed slot that is not equivalent
|
|
// to our frozen version of the slot
|
|
RepairDuplicateConfirmedVersion(Hash),
|
|
// Hash of our current frozen version of the slot
|
|
DuplicateConfirmedSlotMatchesCluster(Hash),
|
|
}
|
|
|
|
impl SlotStateUpdate {
|
|
fn to_handler(&self) -> SlotStateHandler {
|
|
match self {
|
|
SlotStateUpdate::Dead => on_dead_slot,
|
|
SlotStateUpdate::Frozen => on_frozen_slot,
|
|
SlotStateUpdate::DuplicateConfirmed => on_cluster_update,
|
|
SlotStateUpdate::Duplicate => on_cluster_update,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn repair_correct_version(_slot: Slot, _hash: &Hash) {}
|
|
|
|
fn on_dead_slot(
|
|
slot: Slot,
|
|
bank_frozen_hash: &Hash,
|
|
cluster_duplicate_confirmed_hash: Option<&Hash>,
|
|
_is_slot_duplicate: bool,
|
|
is_dead: bool,
|
|
) -> Vec<ResultingStateChange> {
|
|
assert!(is_dead);
|
|
// Bank should not have been frozen if the slot was marked dead
|
|
assert_eq!(*bank_frozen_hash, Hash::default());
|
|
if let Some(cluster_duplicate_confirmed_hash) = cluster_duplicate_confirmed_hash {
|
|
// If the cluster duplicate_confirmed some version of this slot, then
|
|
// there's another version
|
|
warn!(
|
|
"Cluster duplicate_confirmed slot {} with hash {}, but we marked slot dead",
|
|
slot, cluster_duplicate_confirmed_hash
|
|
);
|
|
// No need to check `is_slot_duplicate` and modify fork choice as dead slots
|
|
// are never frozen, and thus never added to fork choice. The state change for
|
|
// `MarkSlotDuplicate` will try to modify fork choice, but won't find the slot
|
|
// in the fork choice tree, so is equivalent to a no-op
|
|
return vec![
|
|
ResultingStateChange::MarkSlotDuplicate(Hash::default()),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(
|
|
*cluster_duplicate_confirmed_hash,
|
|
),
|
|
];
|
|
}
|
|
|
|
vec![]
|
|
}
|
|
|
|
fn on_frozen_slot(
|
|
slot: Slot,
|
|
bank_frozen_hash: &Hash,
|
|
cluster_duplicate_confirmed_hash: Option<&Hash>,
|
|
is_slot_duplicate: bool,
|
|
is_dead: bool,
|
|
) -> Vec<ResultingStateChange> {
|
|
// If a slot is marked frozen, the bank hash should not be default,
|
|
// and the slot should not be dead
|
|
assert!(*bank_frozen_hash != Hash::default());
|
|
assert!(!is_dead);
|
|
|
|
if let Some(cluster_duplicate_confirmed_hash) = cluster_duplicate_confirmed_hash {
|
|
// If the cluster duplicate_confirmed some version of this slot, then
|
|
// confirm our version agrees with the cluster,
|
|
if cluster_duplicate_confirmed_hash != bank_frozen_hash {
|
|
// If the versions do not match, modify fork choice rule
|
|
// to exclude our version from being voted on and also
|
|
// repair correct version
|
|
warn!(
|
|
"Cluster duplicate_confirmed slot {} with hash {}, but we froze slot with hash {}",
|
|
slot, cluster_duplicate_confirmed_hash, bank_frozen_hash
|
|
);
|
|
return vec![
|
|
ResultingStateChange::MarkSlotDuplicate(*bank_frozen_hash),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(
|
|
*cluster_duplicate_confirmed_hash,
|
|
),
|
|
];
|
|
} else {
|
|
// If the versions match, then add the slot to the candidate
|
|
// set to account for the case where it was removed earlier
|
|
// by the `on_duplicate_slot()` handler
|
|
return vec![ResultingStateChange::DuplicateConfirmedSlotMatchesCluster(
|
|
*bank_frozen_hash,
|
|
)];
|
|
}
|
|
}
|
|
|
|
if is_slot_duplicate {
|
|
// If we detected a duplicate, but have not yet seen any version
|
|
// of the slot duplicate_confirmed (i.e. block above did not execute), then
|
|
// remove the slot from fork choice until we get confirmation.
|
|
|
|
// If we get here, we either detected duplicate from
|
|
// 1) WindowService
|
|
// 2) A gossip duplicate_confirmed version that didn't match our frozen
|
|
// version.
|
|
// In both cases, mark the progress map for this slot as duplicate
|
|
return vec![ResultingStateChange::MarkSlotDuplicate(*bank_frozen_hash)];
|
|
}
|
|
|
|
vec![]
|
|
}
|
|
|
|
// Called when we receive either:
|
|
// 1) A duplicate slot signal from WindowStage,
|
|
// 2) Confirmation of a slot by observing votes from replay or gossip.
|
|
//
|
|
// This signals external information about this slot, which affects
|
|
// this validator's understanding of the validity of this slot
|
|
fn on_cluster_update(
|
|
slot: Slot,
|
|
bank_frozen_hash: &Hash,
|
|
cluster_duplicate_confirmed_hash: Option<&Hash>,
|
|
is_slot_duplicate: bool,
|
|
is_dead: bool,
|
|
) -> Vec<ResultingStateChange> {
|
|
if is_dead {
|
|
on_dead_slot(
|
|
slot,
|
|
bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead,
|
|
)
|
|
} else if *bank_frozen_hash != Hash::default() {
|
|
// This case is mutually exclusive with is_dead case above because if a slot is dead,
|
|
// it cannot have been frozen, and thus cannot have a non-default bank hash.
|
|
on_frozen_slot(
|
|
slot,
|
|
bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead,
|
|
)
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
fn get_cluster_duplicate_confirmed_hash<'a>(
|
|
slot: Slot,
|
|
gossip_duplicate_confirmed_hash: Option<&'a Hash>,
|
|
local_frozen_hash: &'a Hash,
|
|
is_local_replay_duplicate_confirmed: bool,
|
|
) -> Option<&'a Hash> {
|
|
let local_duplicate_confirmed_hash = if is_local_replay_duplicate_confirmed {
|
|
// If local replay has duplicate_confirmed this slot, this slot must have
|
|
// descendants with votes for this slot, hence this slot must be
|
|
// frozen.
|
|
assert!(*local_frozen_hash != Hash::default());
|
|
Some(local_frozen_hash)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
match (
|
|
local_duplicate_confirmed_hash,
|
|
gossip_duplicate_confirmed_hash,
|
|
) {
|
|
(Some(local_duplicate_confirmed_hash), Some(gossip_duplicate_confirmed_hash)) => {
|
|
if local_duplicate_confirmed_hash != gossip_duplicate_confirmed_hash {
|
|
error!(
|
|
"For slot {}, the gossip duplicate confirmed hash {}, is not equal
|
|
to the confirmed hash we replayed: {}",
|
|
slot, gossip_duplicate_confirmed_hash, local_duplicate_confirmed_hash
|
|
);
|
|
}
|
|
Some(local_frozen_hash)
|
|
}
|
|
(Some(local_frozen_hash), None) => Some(local_frozen_hash),
|
|
_ => gossip_duplicate_confirmed_hash,
|
|
}
|
|
}
|
|
|
|
fn apply_state_changes(
|
|
slot: Slot,
|
|
fork_choice: &mut HeaviestSubtreeForkChoice,
|
|
state_changes: Vec<ResultingStateChange>,
|
|
) {
|
|
for state_change in state_changes {
|
|
match state_change {
|
|
ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash) => {
|
|
fork_choice.mark_fork_invalid_candidate(&(slot, bank_frozen_hash));
|
|
}
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(
|
|
cluster_duplicate_confirmed_hash,
|
|
) => {
|
|
// TODO: Should consider moving the updating of the duplicate slots in the
|
|
// progress map from ReplayStage::confirm_forks to here.
|
|
repair_correct_version(slot, &cluster_duplicate_confirmed_hash);
|
|
}
|
|
ResultingStateChange::DuplicateConfirmedSlotMatchesCluster(bank_frozen_hash) => {
|
|
fork_choice.mark_fork_valid_candidate(&(slot, bank_frozen_hash));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn check_slot_agrees_with_cluster(
|
|
slot: Slot,
|
|
root: Slot,
|
|
frozen_hash: Option<Hash>,
|
|
duplicate_slots_tracker: &mut DuplicateSlotsTracker,
|
|
gossip_duplicate_confirmed_slots: &GossipDuplicateConfirmedSlots,
|
|
progress: &ProgressMap,
|
|
fork_choice: &mut HeaviestSubtreeForkChoice,
|
|
slot_state_update: SlotStateUpdate,
|
|
) {
|
|
info!(
|
|
"check_slot_agrees_with_cluster()
|
|
slot: {},
|
|
root: {},
|
|
frozen_hash: {:?},
|
|
update: {:?}",
|
|
slot, root, frozen_hash, slot_state_update
|
|
);
|
|
|
|
if slot <= root {
|
|
return;
|
|
}
|
|
|
|
// Needs to happen before the frozen_hash.is_none() check below to account for duplicate
|
|
// signals arriving before the bank is constructed in replay.
|
|
if matches!(slot_state_update, SlotStateUpdate::Duplicate) {
|
|
// If this slot has already been processed before, return
|
|
if !duplicate_slots_tracker.insert(slot) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if frozen_hash.is_none() {
|
|
// If the bank doesn't even exist in BankForks yet,
|
|
// then there's nothing to do as replay of the slot
|
|
// hasn't even started
|
|
return;
|
|
}
|
|
|
|
let frozen_hash = frozen_hash.unwrap();
|
|
let gossip_duplicate_confirmed_hash = gossip_duplicate_confirmed_slots.get(&slot);
|
|
|
|
// If the bank hasn't been frozen yet, then we haven't duplicate confirmed a local version
|
|
// this slot through replay yet.
|
|
let is_local_replay_duplicate_confirmed = fork_choice
|
|
.is_duplicate_confirmed(&(slot, frozen_hash))
|
|
.unwrap_or(false);
|
|
let cluster_duplicate_confirmed_hash = get_cluster_duplicate_confirmed_hash(
|
|
slot,
|
|
gossip_duplicate_confirmed_hash,
|
|
&frozen_hash,
|
|
is_local_replay_duplicate_confirmed,
|
|
);
|
|
let is_slot_duplicate = duplicate_slots_tracker.contains(&slot);
|
|
let is_dead = progress.is_dead(slot).expect("If the frozen hash exists, then the slot must exist in bank forks and thus in progress map");
|
|
|
|
info!(
|
|
"check_slot_agrees_with_cluster() state
|
|
is_local_replay_duplicate_confirmed: {:?},
|
|
cluster_duplicate_confirmed_hash: {:?},
|
|
is_slot_duplicate: {:?},
|
|
is_dead: {:?}",
|
|
is_local_replay_duplicate_confirmed,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead,
|
|
);
|
|
|
|
let state_handler = slot_state_update.to_handler();
|
|
let state_changes = state_handler(
|
|
slot,
|
|
&frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead,
|
|
);
|
|
apply_state_changes(slot, fork_choice, state_changes);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::consensus::test::VoteSimulator;
|
|
use solana_runtime::bank_forks::BankForks;
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
sync::RwLock,
|
|
};
|
|
use trees::tr;
|
|
|
|
struct InitialState {
|
|
heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice,
|
|
progress: ProgressMap,
|
|
descendants: HashMap<Slot, HashSet<Slot>>,
|
|
bank_forks: RwLock<BankForks>,
|
|
}
|
|
|
|
fn setup() -> InitialState {
|
|
// Create simple fork 0 -> 1 -> 2 -> 3
|
|
let forks = tr(0) / (tr(1) / (tr(2) / tr(3)));
|
|
let mut vote_simulator = VoteSimulator::new(1);
|
|
vote_simulator.fill_bank_forks(forks, &HashMap::new());
|
|
|
|
let descendants = vote_simulator
|
|
.bank_forks
|
|
.read()
|
|
.unwrap()
|
|
.descendants()
|
|
.clone();
|
|
|
|
InitialState {
|
|
heaviest_subtree_fork_choice: vote_simulator.heaviest_subtree_fork_choice,
|
|
progress: vote_simulator.progress,
|
|
descendants,
|
|
bank_forks: vote_simulator.bank_forks,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_frozen_duplicate() {
|
|
// Common state
|
|
let slot = 0;
|
|
let cluster_duplicate_confirmed_hash = None;
|
|
let is_dead = false;
|
|
|
|
// Slot is not detected as duplicate yet
|
|
let mut is_slot_duplicate = false;
|
|
|
|
// Simulate freezing the bank, add a
|
|
// new non-default hash, should return
|
|
// no actionable state changes yet
|
|
let bank_frozen_hash = Hash::new_unique();
|
|
assert!(on_frozen_slot(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
)
|
|
.is_empty());
|
|
|
|
// Now mark the slot as duplicate, should
|
|
// trigger marking the slot as a duplicate
|
|
is_slot_duplicate = true;
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_frozen_duplicate_confirmed() {
|
|
// Common state
|
|
let slot = 0;
|
|
let is_slot_duplicate = false;
|
|
let is_dead = false;
|
|
|
|
// No cluster duplicate_confirmed hash yet
|
|
let mut cluster_duplicate_confirmed_hash = None;
|
|
|
|
// Simulate freezing the bank, add a
|
|
// new non-default hash, should return
|
|
// no actionable state changes
|
|
let bank_frozen_hash = Hash::new_unique();
|
|
assert!(on_frozen_slot(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
)
|
|
.is_empty());
|
|
|
|
// Now mark the same frozen slot hash as duplicate_confirmed by the cluster,
|
|
// should just confirm the slot
|
|
cluster_duplicate_confirmed_hash = Some(&bank_frozen_hash);
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![ResultingStateChange::DuplicateConfirmedSlotMatchesCluster(
|
|
bank_frozen_hash
|
|
),]
|
|
);
|
|
|
|
// If the cluster_duplicate_confirmed_hash does not match, then we
|
|
// should trigger marking the slot as a duplicate, and also
|
|
// try to repair correct version
|
|
let mismatched_hash = Hash::new_unique();
|
|
cluster_duplicate_confirmed_hash = Some(&mismatched_hash);
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![
|
|
ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(mismatched_hash),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_frozen_duplicate_confirmed() {
|
|
// Common state
|
|
let slot = 0;
|
|
let is_dead = false;
|
|
let is_slot_duplicate = true;
|
|
|
|
// Bank is not frozen yet
|
|
let mut cluster_duplicate_confirmed_hash = None;
|
|
let mut bank_frozen_hash = Hash::default();
|
|
|
|
// Mark the slot as duplicate. Because our version of the slot is not
|
|
// frozen yet, we don't know which version we have, so no action is
|
|
// taken.
|
|
assert!(on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
)
|
|
.is_empty());
|
|
|
|
// Freeze the bank, should now mark the slot as duplicate since we have
|
|
// not seen confirmation yet.
|
|
bank_frozen_hash = Hash::new_unique();
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash),]
|
|
);
|
|
|
|
// If the cluster_duplicate_confirmed_hash matches, we just confirm
|
|
// the slot
|
|
cluster_duplicate_confirmed_hash = Some(&bank_frozen_hash);
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![ResultingStateChange::DuplicateConfirmedSlotMatchesCluster(
|
|
bank_frozen_hash
|
|
),]
|
|
);
|
|
|
|
// If the cluster_duplicate_confirmed_hash does not match, then we
|
|
// should trigger marking the slot as a duplicate, and also
|
|
// try to repair correct version
|
|
let mismatched_hash = Hash::new_unique();
|
|
cluster_duplicate_confirmed_hash = Some(&mismatched_hash);
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![
|
|
ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(mismatched_hash),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_duplicate_confirmed() {
|
|
let slot = 0;
|
|
let correct_hash = Hash::new_unique();
|
|
let cluster_duplicate_confirmed_hash = Some(&correct_hash);
|
|
let is_dead = false;
|
|
// Bank is not frozen yet
|
|
let bank_frozen_hash = Hash::default();
|
|
|
|
// Because our version of the slot is not frozen yet, then even though
|
|
// the cluster has duplicate_confirmed a hash, we don't know which version we
|
|
// have, so no action is taken.
|
|
let is_slot_duplicate = true;
|
|
assert!(on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
)
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_dead() {
|
|
let slot = 0;
|
|
let cluster_duplicate_confirmed_hash = None;
|
|
let is_dead = true;
|
|
// Bank is not frozen yet
|
|
let bank_frozen_hash = Hash::default();
|
|
|
|
// Even though our version of the slot is dead, the cluster has not
|
|
// duplicate_confirmed a hash, we don't know which version we have, so no action
|
|
// is taken.
|
|
let is_slot_duplicate = true;
|
|
assert!(on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
)
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_confirmed_dead_duplicate() {
|
|
let slot = 0;
|
|
let correct_hash = Hash::new_unique();
|
|
// Cluster has duplicate_confirmed some version of the slot
|
|
let cluster_duplicate_confirmed_hash = Some(&correct_hash);
|
|
// Our version of the slot is dead
|
|
let is_dead = true;
|
|
let bank_frozen_hash = Hash::default();
|
|
|
|
// Even if the duplicate signal hasn't come in yet,
|
|
// we can deduce the slot is duplicate AND we have,
|
|
// the wrong version, so should mark the slot as duplicate,
|
|
// and repair the correct version
|
|
let mut is_slot_duplicate = false;
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![
|
|
ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(correct_hash),
|
|
]
|
|
);
|
|
|
|
// If the duplicate signal comes in, nothing should change
|
|
is_slot_duplicate = true;
|
|
assert_eq!(
|
|
on_cluster_update(
|
|
slot,
|
|
&bank_frozen_hash,
|
|
cluster_duplicate_confirmed_hash,
|
|
is_slot_duplicate,
|
|
is_dead
|
|
),
|
|
vec![
|
|
ResultingStateChange::MarkSlotDuplicate(bank_frozen_hash),
|
|
ResultingStateChange::RepairDuplicateConfirmedVersion(correct_hash),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_state_changes() {
|
|
// Common state
|
|
let InitialState {
|
|
mut heaviest_subtree_fork_choice,
|
|
descendants,
|
|
bank_forks,
|
|
..
|
|
} = setup();
|
|
|
|
// MarkSlotDuplicate should mark progress map and remove
|
|
// the slot from fork choice
|
|
let duplicate_slot = bank_forks.read().unwrap().root() + 1;
|
|
let duplicate_slot_hash = bank_forks
|
|
.read()
|
|
.unwrap()
|
|
.get(duplicate_slot)
|
|
.unwrap()
|
|
.hash();
|
|
apply_state_changes(
|
|
duplicate_slot,
|
|
&mut heaviest_subtree_fork_choice,
|
|
vec![ResultingStateChange::MarkSlotDuplicate(duplicate_slot_hash)],
|
|
);
|
|
assert!(!heaviest_subtree_fork_choice
|
|
.is_candidate(&(duplicate_slot, duplicate_slot_hash))
|
|
.unwrap());
|
|
for child_slot in descendants
|
|
.get(&duplicate_slot)
|
|
.unwrap()
|
|
.iter()
|
|
.chain(std::iter::once(&duplicate_slot))
|
|
{
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(
|
|
*child_slot,
|
|
bank_forks.read().unwrap().get(*child_slot).unwrap().hash()
|
|
))
|
|
.unwrap(),
|
|
duplicate_slot
|
|
);
|
|
}
|
|
|
|
// DuplicateConfirmedSlotMatchesCluster should re-enable fork choice
|
|
apply_state_changes(
|
|
duplicate_slot,
|
|
&mut heaviest_subtree_fork_choice,
|
|
vec![ResultingStateChange::DuplicateConfirmedSlotMatchesCluster(
|
|
duplicate_slot_hash,
|
|
)],
|
|
);
|
|
for child_slot in descendants
|
|
.get(&duplicate_slot)
|
|
.unwrap()
|
|
.iter()
|
|
.chain(std::iter::once(&duplicate_slot))
|
|
{
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(
|
|
*child_slot,
|
|
bank_forks.read().unwrap().get(*child_slot).unwrap().hash()
|
|
))
|
|
.is_none());
|
|
}
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_candidate(&(duplicate_slot, duplicate_slot_hash))
|
|
.unwrap());
|
|
}
|
|
|
|
fn run_test_state_duplicate_then_bank_frozen(initial_bank_hash: Option<Hash>) {
|
|
// Common state
|
|
let InitialState {
|
|
mut heaviest_subtree_fork_choice,
|
|
progress,
|
|
bank_forks,
|
|
..
|
|
} = setup();
|
|
|
|
// Setup a duplicate slot state transition with the initial bank state of the duplicate slot
|
|
// determined by `initial_bank_hash`, which can be:
|
|
// 1) A default hash (unfrozen bank),
|
|
// 2) None (a slot that hasn't even started replay yet).
|
|
let root = 0;
|
|
let mut duplicate_slots_tracker = DuplicateSlotsTracker::default();
|
|
let gossip_duplicate_confirmed_slots = GossipDuplicateConfirmedSlots::default();
|
|
let duplicate_slot = 2;
|
|
check_slot_agrees_with_cluster(
|
|
duplicate_slot,
|
|
root,
|
|
initial_bank_hash,
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::Duplicate,
|
|
);
|
|
assert!(duplicate_slots_tracker.contains(&duplicate_slot));
|
|
// Nothing should be applied yet to fork choice, since bank was not yet frozen
|
|
for slot in 2..=3 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.is_none());
|
|
}
|
|
|
|
// Now freeze the bank
|
|
let frozen_duplicate_slot_hash = bank_forks
|
|
.read()
|
|
.unwrap()
|
|
.get(duplicate_slot)
|
|
.unwrap()
|
|
.hash();
|
|
check_slot_agrees_with_cluster(
|
|
duplicate_slot,
|
|
root,
|
|
Some(frozen_duplicate_slot_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::Frozen,
|
|
);
|
|
|
|
// Progress map should have the correct updates, fork choice should mark duplicate
|
|
// as unvotable
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_unconfirmed_duplicate(&(duplicate_slot, frozen_duplicate_slot_hash))
|
|
.unwrap());
|
|
|
|
// The ancestor of the duplicate slot should be the best slot now
|
|
let (duplicate_ancestor, duplicate_parent_hash) = {
|
|
let r_bank_forks = bank_forks.read().unwrap();
|
|
let parent_bank = r_bank_forks.get(duplicate_slot).unwrap().parent().unwrap();
|
|
(parent_bank.slot(), parent_bank.hash())
|
|
};
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(duplicate_ancestor, duplicate_parent_hash)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_unfrozen_bank_duplicate_then_bank_frozen() {
|
|
run_test_state_duplicate_then_bank_frozen(Some(Hash::default()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_unreplayed_bank_duplicate_then_bank_frozen() {
|
|
run_test_state_duplicate_then_bank_frozen(None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_ancestor_confirmed_descendant_duplicate() {
|
|
// Common state
|
|
let InitialState {
|
|
mut heaviest_subtree_fork_choice,
|
|
progress,
|
|
bank_forks,
|
|
..
|
|
} = setup();
|
|
|
|
let slot3_hash = bank_forks.read().unwrap().get(3).unwrap().hash();
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
let root = 0;
|
|
let mut duplicate_slots_tracker = DuplicateSlotsTracker::default();
|
|
let mut gossip_duplicate_confirmed_slots = GossipDuplicateConfirmedSlots::default();
|
|
|
|
// Mark slot 2 as duplicate confirmed
|
|
let slot2_hash = bank_forks.read().unwrap().get(2).unwrap().hash();
|
|
gossip_duplicate_confirmed_slots.insert(2, slot2_hash);
|
|
check_slot_agrees_with_cluster(
|
|
2,
|
|
root,
|
|
Some(slot2_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::DuplicateConfirmed,
|
|
);
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(2, slot2_hash))
|
|
.unwrap());
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
for slot in 0..=2 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(slot, slot_hash))
|
|
.unwrap());
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.is_none());
|
|
}
|
|
|
|
// Mark 3 as duplicate, should not remove the duplicate confirmed slot 2 from
|
|
// fork choice
|
|
check_slot_agrees_with_cluster(
|
|
3,
|
|
root,
|
|
Some(slot3_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::Duplicate,
|
|
);
|
|
assert!(duplicate_slots_tracker.contains(&3));
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(2, slot2_hash)
|
|
);
|
|
for slot in 0..=3 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
if slot <= 2 {
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(slot, slot_hash))
|
|
.unwrap());
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.is_none());
|
|
} else {
|
|
assert!(!heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(slot, slot_hash))
|
|
.unwrap());
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.unwrap(),
|
|
3
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_ancestor_duplicate_descendant_confirmed() {
|
|
// Common state
|
|
let InitialState {
|
|
mut heaviest_subtree_fork_choice,
|
|
progress,
|
|
bank_forks,
|
|
..
|
|
} = setup();
|
|
|
|
let slot3_hash = bank_forks.read().unwrap().get(3).unwrap().hash();
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
let root = 0;
|
|
let mut duplicate_slots_tracker = DuplicateSlotsTracker::default();
|
|
let mut gossip_duplicate_confirmed_slots = GossipDuplicateConfirmedSlots::default();
|
|
|
|
// Mark 2 as duplicate
|
|
check_slot_agrees_with_cluster(
|
|
2,
|
|
root,
|
|
Some(bank_forks.read().unwrap().get(2).unwrap().hash()),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::Duplicate,
|
|
);
|
|
assert!(duplicate_slots_tracker.contains(&2));
|
|
for slot in 2..=3 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.unwrap(),
|
|
2
|
|
);
|
|
}
|
|
|
|
let slot1_hash = bank_forks.read().unwrap().get(1).unwrap().hash();
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(1, slot1_hash)
|
|
);
|
|
|
|
// Mark slot 3 as duplicate confirmed, should mark slot 2 as duplicate confirmed as well
|
|
gossip_duplicate_confirmed_slots.insert(3, slot3_hash);
|
|
check_slot_agrees_with_cluster(
|
|
3,
|
|
root,
|
|
Some(slot3_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::DuplicateConfirmed,
|
|
);
|
|
for slot in 0..=3 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(slot, slot_hash))
|
|
.unwrap());
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.is_none());
|
|
}
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_descendant_confirmed_ancestor_duplicate() {
|
|
// Common state
|
|
let InitialState {
|
|
mut heaviest_subtree_fork_choice,
|
|
progress,
|
|
bank_forks,
|
|
..
|
|
} = setup();
|
|
|
|
let slot3_hash = bank_forks.read().unwrap().get(3).unwrap().hash();
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
let root = 0;
|
|
let mut duplicate_slots_tracker = DuplicateSlotsTracker::default();
|
|
let mut gossip_duplicate_confirmed_slots = GossipDuplicateConfirmedSlots::default();
|
|
|
|
// Mark 3 as duplicate confirmed
|
|
gossip_duplicate_confirmed_slots.insert(3, slot3_hash);
|
|
check_slot_agrees_with_cluster(
|
|
3,
|
|
root,
|
|
Some(slot3_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::DuplicateConfirmed,
|
|
);
|
|
let verify_all_slots_duplicate_confirmed =
|
|
|bank_forks: &RwLock<BankForks>,
|
|
heaviest_subtree_fork_choice: &HeaviestSubtreeForkChoice| {
|
|
for slot in 0..=3 {
|
|
let slot_hash = bank_forks.read().unwrap().get(slot).unwrap().hash();
|
|
assert!(heaviest_subtree_fork_choice
|
|
.is_duplicate_confirmed(&(slot, slot_hash))
|
|
.unwrap());
|
|
assert!(heaviest_subtree_fork_choice
|
|
.latest_invalid_ancestor(&(slot, slot_hash))
|
|
.is_none());
|
|
}
|
|
};
|
|
verify_all_slots_duplicate_confirmed(&bank_forks, &heaviest_subtree_fork_choice);
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
|
|
// Mark ancestor 1 as duplicate, fork choice should be unaffected since
|
|
// slot 1 was duplicate confirmed by the confirmation on its
|
|
// descendant, 3.
|
|
let slot1_hash = bank_forks.read().unwrap().get(1).unwrap().hash();
|
|
check_slot_agrees_with_cluster(
|
|
1,
|
|
root,
|
|
Some(slot1_hash),
|
|
&mut duplicate_slots_tracker,
|
|
&gossip_duplicate_confirmed_slots,
|
|
&progress,
|
|
&mut heaviest_subtree_fork_choice,
|
|
SlotStateUpdate::Duplicate,
|
|
);
|
|
assert!(duplicate_slots_tracker.contains(&1));
|
|
verify_all_slots_duplicate_confirmed(&bank_forks, &heaviest_subtree_fork_choice);
|
|
assert_eq!(
|
|
heaviest_subtree_fork_choice.best_overall_slot(),
|
|
(3, slot3_hash)
|
|
);
|
|
}
|
|
}
|