From 4ecbf6a9c61b98396326bff2bf288c411c72fe07 Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 16 Nov 2023 19:10:08 +0000 Subject: [PATCH 1/2] add additional vote lockout stake threshold --- core/src/consensus.rs | 192 +++++++++++++++++++++++++++++++-------- core/src/replay_stage.rs | 24 ++++- sdk/src/feature_set.rs | 5 + 3 files changed, 176 insertions(+), 45 deletions(-) diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 72a0c39bc35730..1c897ddb4140f6 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -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, @@ -44,6 +45,7 @@ use { Bound::{Included, Unbounded}, Deref, }, + sync::Arc, }, thiserror::Error, }; @@ -52,7 +54,7 @@ use { pub enum ThresholdDecision { #[default] PassedThreshold, - FailedThreshold(/* Observed stake */ u64), + FailedThreshold(/* vote depth */ u64, /* Observed stake */ u64), } impl ThresholdDecision { @@ -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; @@ -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, + 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>, ) -> 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 @@ -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] @@ -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()); } @@ -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] @@ -2443,7 +2549,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] @@ -2451,7 +2559,9 @@ 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, None) + .passed()); } #[test] @@ -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] @@ -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 @@ -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()); } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index fbf97d13e6e670..de8b5a62c06bff 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -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, @@ -114,6 +115,7 @@ pub enum HeaviestForkFailures { LockedOut(u64), FailedThreshold( Slot, + /* vote depth */ u64, /* Observed stake */ u64, /* Total stake */ u64, ), @@ -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, + ); } } } @@ -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 } @@ -3298,13 +3306,18 @@ impl ReplayStage { tower: &Tower, slot: Slot, ancestors: &HashMap>, + feature_set: &Arc, ) { 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 @@ -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, )); diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 806c3e0139575f..0f15c15fe56d4c 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -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 = [ @@ -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() From 3b9c8e7b2f3598c7b38b1bfabc291917cacc5d0b Mon Sep 17 00:00:00 2001 From: Brennan Date: Wed, 29 Nov 2023 14:41:45 +0000 Subject: [PATCH 2/2] remove feature gate --- core/src/consensus.rs | 59 +++++++++++----------------------------- core/src/replay_stage.rs | 20 +++----------- sdk/src/feature_set.rs | 5 ---- 3 files changed, 20 insertions(+), 64 deletions(-) diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 1c897ddb4140f6..f23325f9beb72e 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -22,7 +22,6 @@ 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, @@ -45,7 +44,6 @@ use { Bound::{Included, Unbounded}, Deref, }, - sync::Arc, }, thiserror::Error, }; @@ -1099,19 +1097,16 @@ impl Tower { slot: Slot, voted_stakes: &VotedStakes, total_stake: Stake, - feature_set: Option<&Arc>, ) -> 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); // 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)); - } + 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 { @@ -2345,9 +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_thresholds(0, &stakes, 2, None) - .passed()); + assert!(tower.check_vote_stake_thresholds(0, &stakes, 2).passed()); } #[test] @@ -2360,7 +2353,7 @@ pub mod test { tower.record_vote(i, Hash::default()); } assert!(!tower - .check_vote_stake_thresholds(MAX_LOCKOUT_HISTORY as u64 + 1, &stakes, 2, None) + .check_vote_stake_thresholds(MAX_LOCKOUT_HISTORY as u64 + 1, &stakes, 2) .passed()); } @@ -2476,18 +2469,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_thresholds(1, &stakes, 2, None) - .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_thresholds(1, &stakes, 2, None) - .passed()); + assert!(tower.check_vote_stake_thresholds(1, &stakes, 2).passed()); } #[test] @@ -2500,7 +2489,7 @@ pub mod test { tower.record_vote(slot as Slot, Hash::default()); } assert!(tower - .check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 4, None) + .check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 4) .passed()); } @@ -2514,12 +2503,7 @@ pub mod test { tower.record_vote(slot as Slot, Hash::default()); } assert!(!tower - .check_vote_stake_thresholds( - VOTE_THRESHOLD_DEPTH.try_into().unwrap(), - &stakes, - 10, - None - ) + .check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 10) .passed()); } @@ -2533,12 +2517,7 @@ pub mod test { tower.record_vote(slot as Slot, Hash::default()); } assert!(!tower - .check_vote_stake_thresholds( - VOTE_THRESHOLD_DEPTH.try_into().unwrap(), - &stakes, - 10, - None - ) + .check_vote_stake_thresholds(VOTE_THRESHOLD_DEPTH.try_into().unwrap(), &stakes, 10) .passed()); } @@ -2549,9 +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_thresholds(6, &stakes, 2, None) - .passed()); + assert!(tower.check_vote_stake_thresholds(6, &stakes, 2).passed()); } #[test] @@ -2559,9 +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_thresholds(1, &stakes, 2, None) - .passed()); + assert!(!tower.check_vote_stake_thresholds(1, &stakes, 2).passed()); } #[test] @@ -2572,9 +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_thresholds(6, &stakes, 2, None) - .passed()); + assert!(tower.check_vote_stake_thresholds(6, &stakes, 2).passed()); } #[test] @@ -2638,7 +2611,7 @@ pub mod test { &mut LatestValidatorVotesForFrozenBanks::default(), ); assert!(tower - .check_vote_stake_thresholds(vote_to_evaluate, &voted_stakes, total_stake, None) + .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 @@ -2658,7 +2631,7 @@ pub mod test { &mut LatestValidatorVotesForFrozenBanks::default(), ); assert!(!tower - .check_vote_stake_thresholds(vote_to_evaluate, &voted_stakes, total_stake, None) + .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 de8b5a62c06bff..cd3bdbc280dd24 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -67,7 +67,6 @@ use { }, solana_sdk::{ clock::{BankId, Slot, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS}, - feature_set::FeatureSet, genesis_config::ClusterType, hash::Hash, pubkey::Pubkey, @@ -3228,13 +3227,7 @@ 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, - &bank.feature_set, - ); + Self::cache_tower_stats(progress, tower, slot, ancestors); } } } @@ -3296,7 +3289,7 @@ impl ReplayStage { cluster_slots, ); - Self::cache_tower_stats(progress, tower, bank_slot, ancestors, &bank.feature_set); + Self::cache_tower_stats(progress, tower, bank_slot, ancestors); } new_stats } @@ -3306,18 +3299,13 @@ impl ReplayStage { tower: &Tower, slot: Slot, ancestors: &HashMap>, - feature_set: &Arc, ) { 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_thresholds( - slot, - &stats.voted_stakes, - stats.total_stake, - Some(feature_set), - ); + stats.vote_threshold = + tower.check_vote_stake_thresholds(slot, &stats.voted_stakes, stats.total_stake); stats.is_locked_out = tower.is_locked_out( slot, ancestors diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 0f15c15fe56d4c..806c3e0139575f 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -732,10 +732,6 @@ 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 = [ @@ -914,7 +910,6 @@ 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()