Skip to content

Commit

Permalink
add additional vote lockout stake threshold (#34120)
Browse files Browse the repository at this point in the history
* add additional vote lockout stake threshold
  • Loading branch information
bw-solana authored Dec 12, 2023
1 parent 39a3566 commit 07f3883
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 42 deletions.
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 @@ -3305,7 +3306,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 @@ -3646,9 +3647,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

0 comments on commit 07f3883

Please sign in to comment.