Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

add additional vote lockout stake threshold #34120

Merged
merged 2 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 125 additions & 40 deletions core/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ use {
pub enum ThresholdDecision {
#[default]
PassedThreshold,
FailedThreshold(/* Observed stake */ u64),
FailedThreshold(/* vote depth */ u64, /* Observed stake */ u64),
}

impl ThresholdDecision {
Expand Down Expand Up @@ -141,6 +141,7 @@ impl SwitchForkDecision {
}
}

const VOTE_THRESHOLD_DEPTH_SHALLOW: usize = 4;
pub const VOTE_THRESHOLD_DEPTH: usize = 8;
pub const SWITCH_FORK_THRESHOLD: f64 = 0.38;

Expand Down Expand Up @@ -1042,46 +1043,88 @@ impl Tower {
self.last_switch_threshold_check.is_none()
}

/// Performs threshold check for `slot`
///
/// If it passes the check returns None, otherwise returns Some(fork_stake)
pub fn check_vote_stake_threshold(
/// Checks a single vote threshold for `slot`
fn check_vote_stake_threshold(
threshold_vote: Option<&Lockout>,
vote_state_before_applying_vote: &VoteState,
threshold_depth: usize,
threshold_size: f64,
slot: Slot,
voted_stakes: &HashMap<Slot, u64>,
total_stake: u64,
) -> ThresholdDecision {
let Some(threshold_vote) = threshold_vote else {
// Tower isn't that deep.
return ThresholdDecision::PassedThreshold;
};
let Some(fork_stake) = voted_stakes.get(&threshold_vote.slot()) else {
// We haven't seen any votes on this fork yet, so no stake
return ThresholdDecision::FailedThreshold(threshold_depth as u64, 0);
};

let lockout = *fork_stake as f64 / total_stake as f64;
trace!(
"fork_stake slot: {}, threshold_vote slot: {}, lockout: {} fork_stake: {} total_stake: {}",
slot,
threshold_vote.slot(),
lockout,
fork_stake,
total_stake
);
if threshold_vote.confirmation_count() as usize > threshold_depth {
for old_vote in &vote_state_before_applying_vote.votes {
if old_vote.slot() == threshold_vote.slot()
&& old_vote.confirmation_count() == threshold_vote.confirmation_count()
{
// If you bounce back to voting on the main fork after not
// voting for a while, your latest vote N on the main fork
// might pop off a lot of the stake of votes in the tower.
// This stake would have rolled up to earlier votes in the
// tower, so skip the stake check.
return ThresholdDecision::PassedThreshold;
}
}
}
if lockout > threshold_size {
return ThresholdDecision::PassedThreshold;
}
ThresholdDecision::FailedThreshold(threshold_depth as u64, *fork_stake)
}

/// Performs vote threshold checks for `slot`
pub fn check_vote_stake_thresholds(
&self,
slot: Slot,
voted_stakes: &VotedStakes,
total_stake: Stake,
) -> ThresholdDecision {
// Generate the vote state assuming this vote is included.
let mut vote_state = self.vote_state.clone();
process_slot_vote_unchecked(&mut vote_state, slot);
let lockout = vote_state.nth_recent_lockout(self.threshold_depth);
if let Some(lockout) = lockout {
if let Some(fork_stake) = voted_stakes.get(&lockout.slot()) {
let lockout_stake = *fork_stake as f64 / total_stake as f64;
trace!(
"fork_stake slot: {}, vote slot: {}, lockout: {} fork_stake: {} total_stake: {}",
slot, lockout.slot(), lockout_stake, fork_stake, total_stake
);
if lockout.confirmation_count() as usize > self.threshold_depth {
for old_vote in &self.vote_state.votes {
if old_vote.slot() == lockout.slot()
&& old_vote.confirmation_count() == lockout.confirmation_count()
{
return ThresholdDecision::PassedThreshold;
}
}
}

if lockout_stake > self.threshold_size {
return ThresholdDecision::PassedThreshold;
}
ThresholdDecision::FailedThreshold(*fork_stake)
} else {
// We haven't seen any votes on this fork yet, so no stake
ThresholdDecision::FailedThreshold(0)
// Assemble all the vote thresholds and depths to check.
let vote_thresholds_and_depths = vec![
(VOTE_THRESHOLD_DEPTH_SHALLOW, SWITCH_FORK_THRESHOLD),
(self.threshold_depth, self.threshold_size),
];

// Check one by one. If any threshold fails, return failure.
for (threshold_depth, threshold_size) in vote_thresholds_and_depths {
if let ThresholdDecision::FailedThreshold(vote_depth, stake) =
Self::check_vote_stake_threshold(
vote_state.nth_recent_lockout(threshold_depth),
&self.vote_state,
threshold_depth,
threshold_size,
slot,
voted_stakes,
total_stake,
)
{
return ThresholdDecision::FailedThreshold(vote_depth, stake);
}
} else {
ThresholdDecision::PassedThreshold
}
ThresholdDecision::PassedThreshold
}

/// Update lockouts for all the ancestors
Expand Down Expand Up @@ -2297,7 +2340,7 @@ pub mod test {
fn test_check_vote_threshold_without_votes() {
let tower = Tower::new_for_tests(1, 0.67);
let stakes = vec![(0, 1)].into_iter().collect();
assert!(tower.check_vote_stake_threshold(0, &stakes, 2).passed());
assert!(tower.check_vote_stake_thresholds(0, &stakes, 2).passed());
}

#[test]
Expand All @@ -2310,7 +2353,7 @@ pub mod test {
tower.record_vote(i, Hash::default());
}
assert!(!tower
.check_vote_stake_threshold(MAX_LOCKOUT_HISTORY as u64 + 1, &stakes, 2,)
.check_vote_stake_thresholds(MAX_LOCKOUT_HISTORY as u64 + 1, &stakes, 2)
.passed());
}

Expand Down Expand Up @@ -2426,14 +2469,56 @@ pub mod test {
let mut tower = Tower::new_for_tests(1, 0.67);
let stakes = vec![(0, 1)].into_iter().collect();
tower.record_vote(0, Hash::default());
assert!(!tower.check_vote_stake_threshold(1, &stakes, 2).passed());
assert!(!tower.check_vote_stake_thresholds(1, &stakes, 2).passed());
}
#[test]
fn test_check_vote_threshold_above_threshold() {
let mut tower = Tower::new_for_tests(1, 0.67);
let stakes = vec![(0, 2)].into_iter().collect();
tower.record_vote(0, Hash::default());
assert!(tower.check_vote_stake_threshold(1, &stakes, 2).passed());
assert!(tower.check_vote_stake_thresholds(1, &stakes, 2).passed());
}

#[test]
fn test_check_vote_thresholds_above_thresholds() {
let mut tower = Tower::new_for_tests(VOTE_THRESHOLD_DEPTH, 0.67);
let stakes = vec![(0, 3), (VOTE_THRESHOLD_DEPTH_SHALLOW as u64, 2)]
.into_iter()
.collect();
for slot in 0..VOTE_THRESHOLD_DEPTH {
tower.record_vote(slot as Slot, Hash::default());
}
assert!(tower
.check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 4)
.passed());
}

#[test]
fn test_check_vote_threshold_deep_below_threshold() {
let mut tower = Tower::new_for_tests(VOTE_THRESHOLD_DEPTH, 0.67);
let stakes = vec![(0, 6), (VOTE_THRESHOLD_DEPTH_SHALLOW as u64, 4)]
.into_iter()
.collect();
for slot in 0..VOTE_THRESHOLD_DEPTH {
tower.record_vote(slot as Slot, Hash::default());
}
assert!(!tower
.check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 10)
.passed());
}

#[test]
fn test_check_vote_threshold_shallow_below_threshold() {
let mut tower = Tower::new_for_tests(VOTE_THRESHOLD_DEPTH, 0.67);
let stakes = vec![(0, 7), (VOTE_THRESHOLD_DEPTH_SHALLOW as u64, 1)]
.into_iter()
.collect();
for slot in 0..VOTE_THRESHOLD_DEPTH {
tower.record_vote(slot as Slot, Hash::default());
}
assert!(!tower
.check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 10)
.passed());
}

#[test]
Expand All @@ -2443,15 +2528,15 @@ pub mod test {
tower.record_vote(0, Hash::default());
tower.record_vote(1, Hash::default());
tower.record_vote(2, Hash::default());
assert!(tower.check_vote_stake_threshold(6, &stakes, 2).passed());
assert!(tower.check_vote_stake_thresholds(6, &stakes, 2).passed());
}

#[test]
fn test_check_vote_threshold_above_threshold_no_stake() {
let mut tower = Tower::new_for_tests(1, 0.67);
let stakes = HashMap::new();
tower.record_vote(0, Hash::default());
assert!(!tower.check_vote_stake_threshold(1, &stakes, 2).passed());
assert!(!tower.check_vote_stake_thresholds(1, &stakes, 2).passed());
}

#[test]
Expand All @@ -2462,7 +2547,7 @@ pub mod test {
tower.record_vote(0, Hash::default());
tower.record_vote(1, Hash::default());
tower.record_vote(2, Hash::default());
assert!(tower.check_vote_stake_threshold(6, &stakes, 2,).passed());
assert!(tower.check_vote_stake_thresholds(6, &stakes, 2).passed());
}

#[test]
Expand Down Expand Up @@ -2526,7 +2611,7 @@ pub mod test {
&mut LatestValidatorVotesForFrozenBanks::default(),
);
assert!(tower
.check_vote_stake_threshold(vote_to_evaluate, &voted_stakes, total_stake,)
.check_vote_stake_thresholds(vote_to_evaluate, &voted_stakes, total_stake)
.passed());

// CASE 2: Now we want to evaluate a vote for slot VOTE_THRESHOLD_DEPTH + 1. This slot
Expand All @@ -2546,7 +2631,7 @@ pub mod test {
&mut LatestValidatorVotesForFrozenBanks::default(),
);
assert!(!tower
.check_vote_stake_threshold(vote_to_evaluate, &voted_stakes, total_stake,)
.check_vote_stake_thresholds(vote_to_evaluate, &voted_stakes, total_stake)
.passed());
}

Expand Down
6 changes: 4 additions & 2 deletions core/src/replay_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub enum HeaviestForkFailures {
LockedOut(u64),
FailedThreshold(
Slot,
/* vote depth */ u64,
/* Observed stake */ u64,
/* Total stake */ u64,
),
Expand Down Expand Up @@ -3304,7 +3305,7 @@ impl ReplayStage {
.expect("All frozen banks must exist in the Progress map");

stats.vote_threshold =
tower.check_vote_stake_threshold(slot, &stats.voted_stakes, stats.total_stake);
tower.check_vote_stake_thresholds(slot, &stats.voted_stakes, stats.total_stake);
stats.is_locked_out = tower.is_locked_out(
slot,
ancestors
Expand Down Expand Up @@ -3645,9 +3646,10 @@ impl ReplayStage {
if is_locked_out {
failure_reasons.push(HeaviestForkFailures::LockedOut(candidate_vote_bank.slot()));
}
if let ThresholdDecision::FailedThreshold(fork_stake) = vote_threshold {
if let ThresholdDecision::FailedThreshold(vote_depth, fork_stake) = vote_threshold {
failure_reasons.push(HeaviestForkFailures::FailedThreshold(
candidate_vote_bank.slot(),
vote_depth,
fork_stake,
total_threshold_stake,
));
Expand Down