From 07f38838ae82333c026955ac78cd8d94f76a4b98 Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 12 Dec 2023 06:50:58 -0800 Subject: [PATCH] add additional vote lockout stake threshold (#34120) * add additional vote lockout stake threshold --- core/src/consensus.rs | 165 +++++++++++++++++++++++++++++---------- core/src/replay_stage.rs | 6 +- 2 files changed, 129 insertions(+), 42 deletions(-) diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 72a0c39bc35730..f23325f9beb72e 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -52,7 +52,7 @@ use { pub enum ThresholdDecision { #[default] PassedThreshold, - FailedThreshold(/* Observed stake */ u64), + FailedThreshold(/* vote depth */ u64, /* Observed stake */ u64), } impl ThresholdDecision { @@ -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; @@ -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, + 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 @@ -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] @@ -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()); } @@ -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] @@ -2443,7 +2528,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] @@ -2451,7 +2536,7 @@ pub mod test { 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] @@ -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] @@ -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 @@ -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()); } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 8d9cae1a137868..8d0a43813c1e30 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -114,6 +114,7 @@ pub enum HeaviestForkFailures { LockedOut(u64), FailedThreshold( Slot, + /* vote depth */ u64, /* Observed stake */ u64, /* Total stake */ u64, ), @@ -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 @@ -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, ));