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 30, 2023
1 parent 71c1782 commit 4ecbf6a
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 45 deletions.
192 changes: 152 additions & 40 deletions core/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use {
solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::VOTE_THRESHOLD_SIZE},
solana_sdk::{
clock::{Slot, UnixTimestamp},
feature_set::FeatureSet,
hash::Hash,
instruction::Instruction,
pubkey::Pubkey,
Expand All @@ -44,6 +45,7 @@ use {
Bound::{Included, Unbounded},
Deref,
},
sync::Arc,
},
thiserror::Error,
};
Expand All @@ -52,7 +54,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 +143,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 +1045,91 @@ 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,
feature_set: Option<&Arc<FeatureSet>>,
) -> 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 mut vote_thresholds_and_depths = vec![(self.threshold_depth, self.threshold_size)];
if feature_set.as_ref().map_or(true, |fs| {
fs.is_active(&solana_sdk::feature_set::additional_vote_stake_threshold::id())
}) {
vote_thresholds_and_depths.push((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 +2345,9 @@ 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, None)
.passed());
}

#[test]
Expand All @@ -2310,7 +2360,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, None)
.passed());
}

Expand Down Expand Up @@ -2426,14 +2476,70 @@ 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, None)
.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, None)
.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, None)
.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,
None
)
.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,
None
)
.passed());
}

#[test]
Expand All @@ -2443,15 +2549,19 @@ 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, None)
.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, None)
.passed());
}

#[test]
Expand All @@ -2462,7 +2572,9 @@ 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, None)
.passed());
}

#[test]
Expand Down Expand Up @@ -2526,7 +2638,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, None)
.passed());

// CASE 2: Now we want to evaluate a vote for slot VOTE_THRESHOLD_DEPTH + 1. This slot
Expand All @@ -2546,7 +2658,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, None)
.passed());
}

Expand Down
24 changes: 19 additions & 5 deletions core/src/replay_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ use {
},
solana_sdk::{
clock::{BankId, Slot, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS},
feature_set::FeatureSet,
genesis_config::ClusterType,
hash::Hash,
pubkey::Pubkey,
Expand Down Expand Up @@ -114,6 +115,7 @@ pub enum HeaviestForkFailures {
LockedOut(u64),
FailedThreshold(
Slot,
/* vote depth */ u64,
/* Observed stake */ u64,
/* Total stake */ u64,
),
Expand Down Expand Up @@ -3226,7 +3228,13 @@ impl ReplayStage {
// Since we are updating our tower we need to update associated caches for previously computed
// slots as well.
for slot in frozen_banks.iter().map(|b| b.slot()) {
Self::cache_tower_stats(progress, tower, slot, ancestors);
Self::cache_tower_stats(
progress,
tower,
slot,
ancestors,
&bank.feature_set,
);
}
}
}
Expand Down Expand Up @@ -3288,7 +3296,7 @@ impl ReplayStage {
cluster_slots,
);

Self::cache_tower_stats(progress, tower, bank_slot, ancestors);
Self::cache_tower_stats(progress, tower, bank_slot, ancestors, &bank.feature_set);
}
new_stats
}
Expand All @@ -3298,13 +3306,18 @@ impl ReplayStage {
tower: &Tower,
slot: Slot,
ancestors: &HashMap<u64, HashSet<u64>>,
feature_set: &Arc<FeatureSet>,
) {
let stats = progress
.get_fork_stats_mut(slot)
.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);
stats.vote_threshold = tower.check_vote_stake_thresholds(
slot,
&stats.voted_stakes,
stats.total_stake,
Some(feature_set),
);
stats.is_locked_out = tower.is_locked_out(
slot,
ancestors
Expand Down Expand Up @@ -3645,9 +3658,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
5 changes: 5 additions & 0 deletions sdk/src/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,10 @@ pub mod enable_zk_transfer_with_fee {
solana_sdk::declare_id!("zkNLP7EQALfC1TYeB3biDU7akDckj8iPkvh9y2Mt2K3");
}

pub mod additional_vote_stake_threshold {
solana_sdk::declare_id!("CvgUdmzBnu4kwtL1nurkhK7iXPAdY6QnTKS41H3Jz574");
}

lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
Expand Down Expand Up @@ -910,6 +914,7 @@ lazy_static! {
(validate_fee_collector_account::id(), "validate fee collector account #33888"),
(disable_rent_fees_collection::id(), "Disable rent fees collection #33945"),
(enable_zk_transfer_with_fee::id(), "enable Zk Token proof program transfer with fee"),
(additional_vote_stake_threshold::id(), "Add an additional vote stake threshold"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()
Expand Down

0 comments on commit 4ecbf6a

Please sign in to comment.