Skip to content

Commit

Permalink
add additional vote lockout stake threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
bw-solana committed Nov 16, 2023
1 parent b4c652e commit aea64cc
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 42 deletions.
121 changes: 81 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,86 @@ 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(
vote: Option<&Lockout>,
vote_state: &VoteState,
threshold_depth: usize,
threshold_size: f64,
slot: Slot,
voted_stakes: &HashMap<Slot, u64>,
total_stake: u64,
) -> ThresholdDecision {
let vote = match vote {
None => return ThresholdDecision::PassedThreshold,
Some(vote) => vote,
};
let fork_stake = match voted_stakes.get(&vote.slot()) {
None => {
// We haven't seen any votes on this fork yet, so no stake
return ThresholdDecision::FailedThreshold(threshold_depth as u64, 0);
}
Some(fork_stake) => fork_stake,
};

let lockout = *fork_stake as f64 / total_stake as f64;
trace!(
"fork_stake slot: {}, vote slot: {}, lockout: {} fork_stake: {} total_stake: {}",
slot,
vote.slot(),
lockout,
fork_stake,
total_stake
);
if vote.confirmation_count() as usize > threshold_depth {
for old_vote in &vote_state.votes {
if old_vote.slot() == vote.slot()
&& old_vote.confirmation_count() == vote.confirmation_count()
{
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![
(self.threshold_depth, self.threshold_size),
(VOTE_THRESHOLD_DEPTH_SHALLOW, SWITCH_FORK_THRESHOLD),
];

// 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 +2338,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 +2351,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 +2467,14 @@ 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]
Expand All @@ -2443,15 +2484,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 +2503,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 +2567,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 +2587,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 @@ -3292,7 +3293,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 @@ -3633,9 +3634,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 aea64cc

Please sign in to comment.