diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index fde831ea5f..8413d18e81 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1,6 +1,7 @@ use crate::{ cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, + stake::is_stake_program_v2_enabled, }; use chrono::{Local, TimeZone}; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -1349,6 +1350,8 @@ pub fn process_show_stakes( let stake_history = StakeHistory::from_account(&stake_history_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize stake history".to_string()) })?; + // At v1.6, this check can be removed and simply passed as `true` + let stake_program_v2_enabled = is_stake_program_v2_enabled(rpc_client); let mut stake_accounts: Vec = vec![]; for (stake_pubkey, stake_account) in all_stake_accounts { @@ -1364,6 +1367,7 @@ pub fn process_show_stakes( use_lamports_unit, &stake_history, &clock, + stake_program_v2_enabled, ), }); } @@ -1382,6 +1386,7 @@ pub fn process_show_stakes( use_lamports_unit, &stake_history, &clock, + stake_program_v2_enabled, ), }); } diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 469514ba4d..093ea3e36d 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -31,6 +31,7 @@ use solana_client::{ rpc_request::{self, DELINQUENT_VALIDATOR_SLOT_DISTANCE}, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; +use solana_runtime::{feature::Feature, feature_set}; use solana_sdk::{ account_utils::StateMut, clock::{Clock, Epoch, Slot, UnixTimestamp, SECONDS_PER_DAY}, @@ -1501,6 +1502,7 @@ pub fn build_stake_state( use_lamports_unit: bool, stake_history: &StakeHistory, clock: &Clock, + stake_program_v2_enabled: bool, ) -> CliStakeState { match stake_state { StakeState::Stake( @@ -1512,9 +1514,12 @@ pub fn build_stake_state( stake, ) => { let current_epoch = clock.epoch; - let (active_stake, activating_stake, deactivating_stake) = stake - .delegation - .stake_activating_and_deactivating(current_epoch, Some(stake_history)); + let (active_stake, activating_stake, deactivating_stake) = + stake.delegation.stake_activating_and_deactivating( + current_epoch, + Some(stake_history), + stake_program_v2_enabled, + ); let lockup = if lockup.is_in_force(clock, None) { Some(lockup.into()) } else { @@ -1711,6 +1716,7 @@ pub fn process_show_stake_account( use_lamports_unit, &stake_history, &clock, + is_stake_program_v2_enabled(rpc_client), // At v1.6, this check can be removed and simply passed as `true` ); if state.stake_type == CliStakeType::Stake { @@ -1881,6 +1887,15 @@ pub fn process_delegate_stake( } } +pub fn is_stake_program_v2_enabled(rpc_client: &RpcClient) -> bool { + rpc_client + .get_account(&feature_set::stake_program_v2::id()) + .ok() + .and_then(|account| Feature::from_account(&account)) + .and_then(|feature| feature.activated_at) + .is_some() +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 0da8cb880c..519b5ed4f2 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1046,8 +1046,11 @@ impl JsonRpcRequestProcessor { let stake_history = StakeHistory::from_account(&stake_history_account).ok_or_else(Error::internal_error)?; - let (active, activating, deactivating) = - delegation.stake_activating_and_deactivating(epoch, Some(&stake_history)); + let (active, activating, deactivating) = delegation.stake_activating_and_deactivating( + epoch, + Some(&stake_history), + bank.stake_program_v2_enabled(), + ); let stake_activation_state = if deactivating > 0 { StakeActivationState::Deactivating } else if activating > 0 { diff --git a/ledger/src/staking_utils.rs b/ledger/src/staking_utils.rs index b2a90aa466..7086ccea9a 100644 --- a/ledger/src/staking_utils.rs +++ b/ledger/src/staking_utils.rs @@ -236,7 +236,10 @@ pub(crate) mod tests { let result: Vec<_> = epoch_stakes_and_lockouts(&bank, first_leader_schedule_epoch); assert_eq!( result, - vec![(leader_stake.stake(first_leader_schedule_epoch, None), None)] + vec![( + leader_stake.stake(first_leader_schedule_epoch, None, true), + None + )] ); // epoch stakes and lockouts are saved off for the future epoch, should @@ -246,8 +249,14 @@ pub(crate) mod tests { let stake_history = StakeHistory::from_account(&bank.get_account(&stake_history::id()).unwrap()).unwrap(); let mut expected = vec![ - (leader_stake.stake(bank.epoch(), Some(&stake_history)), None), - (other_stake.stake(bank.epoch(), Some(&stake_history)), None), + ( + leader_stake.stake(bank.epoch(), Some(&stake_history), true), + None, + ), + ( + other_stake.stake(bank.epoch(), Some(&stake_history), true), + None, + ), ]; expected.sort(); diff --git a/programs/stake/src/legacy_stake_state.rs b/programs/stake/src/legacy_stake_state.rs index 2a6b7a3596..b73c677419 100644 --- a/programs/stake/src/legacy_stake_state.rs +++ b/programs/stake/src/legacy_stake_state.rs @@ -99,7 +99,7 @@ pub struct Stake { impl Stake { pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { - self.delegation.stake(epoch, history) + self.delegation.stake(epoch, history, false) } pub fn redeem_rewards( @@ -144,7 +144,7 @@ impl Stake { for (epoch, final_epoch_credits, initial_epoch_credits) in new_vote_state.epoch_credits().iter().copied() { - let stake = u128::from(self.delegation.stake(epoch, stake_history)); + let stake = u128::from(self.delegation.stake(epoch, stake_history, false)); // figure out how much this stake has seen that // for which the vote account has a record @@ -595,7 +595,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> { .check(&signers, StakeAuthorize::Withdrawer)?; // if we have a deactivation epoch and we're in cooldown let staked = if clock.epoch >= stake.delegation.deactivation_epoch { - stake.delegation.stake(clock.epoch, Some(stake_history)) + stake + .delegation + .stake(clock.epoch, Some(stake_history), false) } else { // Assume full stake if the stake account hasn't been // de-activated, because in the future the exposed stake @@ -692,11 +694,12 @@ pub fn calculate_points( } } -// utility function, used by runtime::Stakes, tests +// utility function, used by runtime::Stakes and tests pub fn new_stake_history_entry<'a, I>( epoch: Epoch, stakes: I, history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> StakeHistoryEntry where I: Iterator, @@ -707,7 +710,10 @@ where (a.0 + b.0, a.1 + b.1, a.2 + b.2) } let (effective, activating, deactivating) = stakes.fold((0, 0, 0), |sum, stake| { - add(sum, stake.stake_activating_and_deactivating(epoch, history)) + add( + sum, + stake.stake_activating_and_deactivating(epoch, history, fix_stake_deactivate), + ) }); StakeHistoryEntry { @@ -1040,6 +1046,7 @@ mod tests { epoch, delegations.iter().chain(bootstrap_delegation.iter()), Some(&stake_history), + false, ); stake_history.add(epoch, entry); } @@ -1062,25 +1069,34 @@ mod tests { let mut stake_history = StakeHistory::default(); // assert that this stake follows step function if there's no history assert_eq!( - stake.stake_activating_and_deactivating(stake.activation_epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating( + stake.activation_epoch, + Some(&stake_history), + false + ), (0, stake.stake, 0) ); for epoch in stake.activation_epoch + 1..stake.deactivation_epoch { assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating(epoch, Some(&stake_history), false), (stake.stake, 0, 0) ); } // assert that this stake is full deactivating assert_eq!( - stake.stake_activating_and_deactivating(stake.deactivation_epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating( + stake.deactivation_epoch, + Some(&stake_history), + false + ), (stake.stake, 0, stake.stake) ); // assert that this stake is fully deactivated if there's no history assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 1, - Some(&stake_history) + Some(&stake_history), + false, ), (0, 0, 0) ); @@ -1095,7 +1111,7 @@ mod tests { ); // assert that this stake is broken, because above setup is broken assert_eq!( - stake.stake_activating_and_deactivating(1, Some(&stake_history)), + stake.stake_activating_and_deactivating(1, Some(&stake_history), false), (0, stake.stake, 0) ); @@ -1110,7 +1126,7 @@ mod tests { ); // assert that this stake is broken, because above setup is broken assert_eq!( - stake.stake_activating_and_deactivating(2, Some(&stake_history)), + stake.stake_activating_and_deactivating(2, Some(&stake_history), false), (increment, stake.stake - increment, 0) ); @@ -1129,7 +1145,8 @@ mod tests { assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 1, - Some(&stake_history) + Some(&stake_history), + false, ), (stake.stake, 0, stake.stake) // says "I'm still waiting for deactivation" ); @@ -1147,7 +1164,8 @@ mod tests { assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 2, - Some(&stake_history) + Some(&stake_history), + false, ), (stake.stake - increment, 0, stake.stake - increment) // hung, should be lower ); @@ -1212,7 +1230,7 @@ mod tests { (0, history.deactivating) }; assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating(epoch, Some(&stake_history), false), (expected_stake, expected_activating, expected_deactivating) ); } @@ -1239,7 +1257,7 @@ mod tests { for epoch in 0..epochs { let stake = delegations .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history))) + .map(|delegation| delegation.stake(epoch, Some(&stake_history), false)) .sum::(); max_stake = max_stake.max(stake); min_stake = min_stake.min(stake); @@ -1298,7 +1316,7 @@ mod tests { let mut prev_total_effective_stake = delegations .iter() - .map(|delegation| delegation.stake(0, Some(&stake_history))) + .map(|delegation| delegation.stake(0, Some(&stake_history), false)) .sum::(); // uncomment and add ! for fun with graphing @@ -1306,7 +1324,7 @@ mod tests { for epoch in 1..epochs { let total_effective_stake = delegations .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history))) + .map(|delegation| delegation.stake(epoch, Some(&stake_history), false)) .sum::(); let delta = if total_effective_stake > prev_total_effective_stake { diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 96bd5c79a4..53c8bdac9a 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -212,8 +212,14 @@ impl Delegation { self.activation_epoch == std::u64::MAX } - pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { - self.stake_activating_and_deactivating(epoch, history).0 + pub fn stake( + &self, + epoch: Epoch, + history: Option<&StakeHistory>, + fix_stake_deactivate: bool, + ) -> u64 { + self.stake_activating_and_deactivating(epoch, history, fix_stake_deactivate) + .0 } #[allow(clippy::comparison_chain)] @@ -221,11 +227,13 @@ impl Delegation { &self, target_epoch: Epoch, history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> (u64, u64, u64) { let delegated_stake = self.stake; // first, calculate an effective and activating stake - let (effective_stake, activating_stake) = self.stake_and_activating(target_epoch, history); + let (effective_stake, activating_stake) = + self.stake_and_activating(target_epoch, history, fix_stake_deactivate); // then de-activate some portion if necessary if target_epoch < self.deactivation_epoch { @@ -301,12 +309,17 @@ impl Delegation { &self, target_epoch: Epoch, history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> (u64, u64) { let delegated_stake = self.stake; if self.is_bootstrap() { // fully effective immediately (delegated_stake, 0) + } else if fix_stake_deactivate && self.activation_epoch == self.deactivation_epoch { + // activated but instantly deactivated; no stake at all regardless of target_epoch + // this must be after the bootstrap check and before all-is-activating check + (0, 0) } else if target_epoch == self.activation_epoch { // all is activating (0, delegated_stake) @@ -440,8 +453,13 @@ pub struct PointValue { } impl Stake { - pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { - self.delegation.stake(epoch, history) + pub fn stake( + &self, + epoch: Epoch, + history: Option<&StakeHistory>, + fix_stake_deactivate: bool, + ) -> u64 { + self.delegation.stake(epoch, history, fix_stake_deactivate) } pub fn redeem_rewards( @@ -450,12 +468,14 @@ impl Stake { vote_state: &VoteState, stake_history: Option<&StakeHistory>, inflation_point_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> Option<(u64, u64)> { self.calculate_rewards( point_value, vote_state, stake_history, inflation_point_calc_tracer, + fix_stake_deactivate, ) .map(|(stakers_reward, voters_reward, credits_observed)| { self.credits_observed = credits_observed; @@ -469,9 +489,15 @@ impl Stake { vote_state: &VoteState, stake_history: Option<&StakeHistory>, inflation_point_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> u128 { - self.calculate_points_and_credits(vote_state, stake_history, inflation_point_calc_tracer) - .0 + self.calculate_points_and_credits( + vote_state, + stake_history, + inflation_point_calc_tracer, + fix_stake_deactivate, + ) + .0 } /// for a given stake and vote_state, calculate how many @@ -482,6 +508,7 @@ impl Stake { new_vote_state: &VoteState, stake_history: Option<&StakeHistory>, inflation_point_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> (u128, u64) { // if there is no newer credits since observed, return no point if new_vote_state.credits() <= self.credits_observed { @@ -494,7 +521,11 @@ impl Stake { for (epoch, final_epoch_credits, initial_epoch_credits) in new_vote_state.epoch_credits().iter().copied() { - let stake = u128::from(self.delegation.stake(epoch, stake_history)); + let stake = u128::from(self.delegation.stake( + epoch, + stake_history, + fix_stake_deactivate, + )); // figure out how much this stake has seen that // for which the vote account has a record @@ -541,11 +572,13 @@ impl Stake { vote_state: &VoteState, stake_history: Option<&StakeHistory>, inflation_point_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> Option<(u64, u64, u64)> { let (points, credits_observed) = self.calculate_points_and_credits( vote_state, stake_history, inflation_point_calc_tracer, + fix_stake_deactivate, ); if points == 0 || point_value.points == 0 { @@ -596,7 +629,7 @@ impl Stake { // can't redelegate if stake is active. either the stake // is freshly activated or has fully de-activated. redelegation // implies re-activation - if self.stake(clock.epoch, Some(stake_history)) != 0 { + if self.stake(clock.epoch, Some(stake_history), true) != 0 { return Err(StakeError::TooSoonToRedelegate); } self.delegation.stake = stake_lamports; @@ -950,7 +983,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { let meta = match self.state()? { StakeState::Stake(meta, stake) => { // stake must be fully de-activated - if stake.stake(clock.epoch, Some(stake_history)) != 0 { + if stake.stake(clock.epoch, Some(stake_history), true) != 0 { return Err(StakeError::MergeActivatedStake.into()); } meta @@ -964,7 +997,7 @@ impl<'a> StakeAccount for KeyedAccount<'a> { let source_meta = match source_stake.state()? { StakeState::Stake(meta, stake) => { // stake must be fully de-activated - if stake.stake(clock.epoch, Some(stake_history)) != 0 { + if stake.stake(clock.epoch, Some(stake_history), true) != 0 { return Err(StakeError::MergeActivatedStake.into()); } meta @@ -1006,7 +1039,9 @@ impl<'a> StakeAccount for KeyedAccount<'a> { .check(&signers, StakeAuthorize::Withdrawer)?; // if we have a deactivation epoch and we're in cooldown let staked = if clock.epoch >= stake.delegation.deactivation_epoch { - stake.delegation.stake(clock.epoch, Some(stake_history)) + stake + .delegation + .stake(clock.epoch, Some(stake_history), true) } else { // Assume full stake if the stake account hasn't been // de-activated, because in the future the exposed stake @@ -1066,6 +1101,7 @@ pub fn redeem_rewards( point_value: &PointValue, stake_history: Option<&StakeHistory>, inflation_point_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> Result<(u64, u64), InstructionError> { if let StakeState::Stake(meta, mut stake) = stake_account.state()? { let vote_state: VoteState = @@ -1084,6 +1120,7 @@ pub fn redeem_rewards( &vote_state, stake_history, inflation_point_calc_tracer, + fix_stake_deactivate, ) { stake_account.lamports += stakers_reward; vote_account.lamports += voters_reward; @@ -1104,12 +1141,18 @@ pub fn calculate_points( stake_account: &Account, vote_account: &Account, stake_history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> Result { if let StakeState::Stake(_meta, stake) = stake_account.state()? { let vote_state: VoteState = StateMut::::state(vote_account)?.convert_to_current(); - Ok(stake.calculate_points(&vote_state, stake_history, &mut null_tracer())) + Ok(stake.calculate_points( + &vote_state, + stake_history, + &mut null_tracer(), + fix_stake_deactivate, + )) } else { Err(InstructionError::InvalidAccountData) } @@ -1133,6 +1176,7 @@ pub fn new_stake_history_entry<'a, I>( epoch: Epoch, stakes: I, history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> StakeHistoryEntry where I: Iterator, @@ -1143,7 +1187,10 @@ where (a.0 + b.0, a.1 + b.1, a.2 + b.2) } let (effective, activating, deactivating) = stakes.fold((0, 0, 0), |sum, stake| { - add(sum, stake.stake_activating_and_deactivating(epoch, history)) + add( + sum, + stake.stake_activating_and_deactivating(epoch, history, fix_stake_deactivate), + ) }); StakeHistoryEntry { @@ -1485,6 +1532,7 @@ mod tests { epoch, delegations.iter().chain(bootstrap_delegation.iter()), Some(&stake_history), + true, ); stake_history.add(epoch, entry); } @@ -1507,25 +1555,34 @@ mod tests { let mut stake_history = StakeHistory::default(); // assert that this stake follows step function if there's no history assert_eq!( - stake.stake_activating_and_deactivating(stake.activation_epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating( + stake.activation_epoch, + Some(&stake_history), + true + ), (0, stake.stake, 0) ); for epoch in stake.activation_epoch + 1..stake.deactivation_epoch { assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating(epoch, Some(&stake_history), true), (stake.stake, 0, 0) ); } // assert that this stake is full deactivating assert_eq!( - stake.stake_activating_and_deactivating(stake.deactivation_epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating( + stake.deactivation_epoch, + Some(&stake_history), + true + ), (stake.stake, 0, stake.stake) ); // assert that this stake is fully deactivated if there's no history assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 1, - Some(&stake_history) + Some(&stake_history), + true, ), (0, 0, 0) ); @@ -1540,7 +1597,7 @@ mod tests { ); // assert that this stake is broken, because above setup is broken assert_eq!( - stake.stake_activating_and_deactivating(1, Some(&stake_history)), + stake.stake_activating_and_deactivating(1, Some(&stake_history), true), (0, stake.stake, 0) ); @@ -1555,7 +1612,7 @@ mod tests { ); // assert that this stake is broken, because above setup is broken assert_eq!( - stake.stake_activating_and_deactivating(2, Some(&stake_history)), + stake.stake_activating_and_deactivating(2, Some(&stake_history), true), (increment, stake.stake - increment, 0) ); @@ -1574,7 +1631,8 @@ mod tests { assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 1, - Some(&stake_history) + Some(&stake_history), + true, ), (stake.stake, 0, stake.stake) // says "I'm still waiting for deactivation" ); @@ -1592,12 +1650,158 @@ mod tests { assert_eq!( stake.stake_activating_and_deactivating( stake.deactivation_epoch + 2, - Some(&stake_history) + Some(&stake_history), + true, ), (stake.stake - increment, 0, stake.stake - increment) // hung, should be lower ); } + mod same_epoch_activation_then_deactivation { + use super::*; + + enum OldDeactivationBehavior { + Stuck, + Slow, + } + + fn do_test( + old_behavior: OldDeactivationBehavior, + fix_stake_deactivate: bool, + expected_stakes: &[(u64, u64, u64)], + ) { + let cluster_stake = 1_000; + let activating_stake = 10_000; + let some_stake = 700; + let some_epoch = 0; + + let stake = Delegation { + stake: some_stake, + activation_epoch: some_epoch, + deactivation_epoch: some_epoch, + ..Delegation::default() + }; + + let mut stake_history = StakeHistory::default(); + let cluster_deactivation_at_stake_modified_epoch = match old_behavior { + OldDeactivationBehavior::Stuck => 0, + OldDeactivationBehavior::Slow => 1000, + }; + + let stake_history_entries = vec![ + ( + cluster_stake, + activating_stake, + cluster_deactivation_at_stake_modified_epoch, + ), + (cluster_stake, activating_stake, 1000), + (cluster_stake, activating_stake, 1000), + (cluster_stake, activating_stake, 100), + (cluster_stake, activating_stake, 100), + (cluster_stake, activating_stake, 100), + (cluster_stake, activating_stake, 100), + ]; + + for (epoch, (effective, activating, deactivating)) in + stake_history_entries.into_iter().enumerate() + { + stake_history.add( + epoch as Epoch, + StakeHistoryEntry { + effective, + activating, + deactivating, + }, + ); + } + + assert_eq!( + expected_stakes, + &(0..expected_stakes.len()) + .map(|epoch| stake.stake_activating_and_deactivating( + epoch as u64, + Some(&stake_history), + fix_stake_deactivate + )) + .collect::>()[..] + ); + } + + #[test] + fn test_old_behavior_slow() { + do_test( + OldDeactivationBehavior::Slow, + false, + &[ + (0, 0, 0), + (13, 0, 13), + (10, 0, 10), + (8, 0, 8), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + ], + ); + } + + #[test] + fn test_old_behavior_stuck() { + do_test( + OldDeactivationBehavior::Stuck, + false, + &[ + (0, 0, 0), + (17, 0, 17), + (17, 0, 17), + (17, 0, 17), + (17, 0, 17), + (17, 0, 17), + (17, 0, 17), + ], + ); + } + + #[test] + fn test_new_behavior_previously_slow() { + // any stake accounts activated and deactivated at the same epoch + // shouldn't been activated (then deactivated) at all! + + do_test( + OldDeactivationBehavior::Slow, + true, + &[ + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + ], + ); + } + + #[test] + fn test_new_behavior_previously_stuck() { + // any stake accounts activated and deactivated at the same epoch + // shouldn't been activated (then deactivated) at all! + + do_test( + OldDeactivationBehavior::Stuck, + true, + &[ + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + (0, 0, 0), + ], + ); + } + } + #[test] fn test_stop_activating_after_deactivation() { solana_logger::setup(); @@ -1657,7 +1861,7 @@ mod tests { (0, history.deactivating) }; assert_eq!( - stake.stake_activating_and_deactivating(epoch, Some(&stake_history)), + stake.stake_activating_and_deactivating(epoch, Some(&stake_history), true), (expected_stake, expected_activating, expected_deactivating) ); } @@ -1684,7 +1888,7 @@ mod tests { for epoch in 0..epochs { let stake = delegations .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history))) + .map(|delegation| delegation.stake(epoch, Some(&stake_history), true)) .sum::(); max_stake = max_stake.max(stake); min_stake = min_stake.min(stake); @@ -1743,7 +1947,7 @@ mod tests { let mut prev_total_effective_stake = delegations .iter() - .map(|delegation| delegation.stake(0, Some(&stake_history))) + .map(|delegation| delegation.stake(0, Some(&stake_history), true)) .sum::(); // uncomment and add ! for fun with graphing @@ -1751,7 +1955,7 @@ mod tests { for epoch in 1..epochs { let total_effective_stake = delegations .iter() - .map(|delegation| delegation.stake(epoch, Some(&stake_history))) + .map(|delegation| delegation.stake(epoch, Some(&stake_history), true)) .sum::(); let delta = if total_effective_stake > prev_total_effective_stake { @@ -2547,6 +2751,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2565,6 +2770,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2600,6 +2806,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2613,7 +2820,7 @@ mod tests { // no overflow on points assert_eq!( u128::from(stake.delegation.stake) * epoch_slots, - stake.calculate_points(&vote_state, None, &mut null_tracer()) + stake.calculate_points(&vote_state, None, &mut null_tracer(), true) ); } @@ -2641,6 +2848,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2659,6 +2867,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2674,6 +2883,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2692,6 +2902,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2708,6 +2919,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2730,6 +2942,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); @@ -2746,6 +2959,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); vote_state.commission = 99; @@ -2759,6 +2973,7 @@ mod tests { &vote_state, None, &mut null_tracer(), + true, ) ); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index a2792737c6..a2a0a87fa7 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -843,7 +843,7 @@ impl Bank { capitalization: AtomicU64::new(parent.capitalization()), inflation: parent.inflation.clone(), transaction_count: AtomicU64::new(parent.transaction_count()), - stakes: RwLock::new(parent.stakes.read().unwrap().clone_with_epoch(epoch)), + stakes: RwLock::new(parent.stakes.read().unwrap().clone()), epoch_stakes: parent.epoch_stakes.clone(), parent_hash: parent.hash(), parent_slot: parent.slot(), @@ -874,8 +874,6 @@ impl Bank { ("block_height", new.block_height, i64) ); - let leader_schedule_epoch = epoch_schedule.get_leader_schedule_epoch(slot); - new.update_epoch_stakes(leader_schedule_epoch); new.ancestors.insert(new.slot(), 0); new.parents().iter().enumerate().for_each(|(i, p)| { new.ancestors.insert(p.slot(), i + 1); @@ -885,7 +883,15 @@ impl Bank { if parent.epoch() < new.epoch() { new.apply_feature_activations(false); } + let cloned = new + .stakes + .read() + .unwrap() + .clone_with_epoch(epoch, new.stake_program_v2_enabled()); + *new.stakes.write().unwrap() = cloned; + let leader_schedule_epoch = epoch_schedule.get_leader_schedule_epoch(slot); + new.update_epoch_stakes(leader_schedule_epoch); new.update_slot_hashes(); new.update_rewards(parent.epoch(), reward_calc_tracer); new.update_stake_history(Some(parent.epoch())); @@ -1259,8 +1265,11 @@ impl Bank { let old_vote_balance_and_staked = self.stakes.read().unwrap().vote_balance_and_staked(); - let validator_point_value = - self.pay_validator_rewards(validator_rewards, reward_calc_tracer); + let validator_point_value = self.pay_validator_rewards( + validator_rewards, + reward_calc_tracer, + self.stake_program_v2_enabled(), + ); if !self .feature_set @@ -1373,6 +1382,7 @@ impl Bank { &mut self, rewards: u64, reward_calc_tracer: &mut Option, + fix_stake_deactivate: bool, ) -> f64 { let stake_history = self.stakes.read().unwrap().history().clone(); @@ -1386,8 +1396,13 @@ impl Bank { .map(move |(_stake_pubkey, stake_account)| (stake_account, vote_account)) }) .map(|(stake_account, vote_account)| { - stake_state::calculate_points(&stake_account, &vote_account, Some(&stake_history)) - .unwrap_or(0) + stake_state::calculate_points( + &stake_account, + &vote_account, + Some(&stake_history), + fix_stake_deactivate, + ) + .unwrap_or(0) }) .sum(); @@ -1418,6 +1433,7 @@ impl Bank { &point_value, Some(&stake_history), &mut reward_calc_tracer.as_mut(), + fix_stake_deactivate, ); if let Ok((stakers_reward, _voters_reward)) = redeemed { self.store_account(&stake_pubkey, &stake_account); @@ -3295,7 +3311,10 @@ impl Bank { self.rc.accounts.store_slow(self.slot(), pubkey, account); if Stakes::is_stake(account) { - self.stakes.write().unwrap().store(pubkey, account); + self.stakes + .write() + .unwrap() + .store(pubkey, account, self.stake_program_v2_enabled()); } } @@ -3721,9 +3740,11 @@ impl Bank { .filter(|(_key, account)| (Stakes::is_stake(account))) { if Stakes::is_stake(account) { - if let Some(old_vote_account) = - self.stakes.write().unwrap().store(pubkey, account) - { + if let Some(old_vote_account) = self.stakes.write().unwrap().store( + pubkey, + account, + self.stake_program_v2_enabled(), + ) { overwritten_vote_accounts.push(OverwrittenVoteAccount { account: old_vote_account, transaction_index, @@ -3935,6 +3956,11 @@ impl Bank { self.feature_set.cumulative_rent_related_fixes_enabled() } + pub fn stake_program_v2_enabled(&self) -> bool { + self.feature_set + .is_active(&feature_set::stake_program_v2::id()) + } + // This is called from snapshot restore AND for each epoch boundary // The entire code path herein must be idempotent fn apply_feature_activations(&mut self, init_finish_or_warp: bool) { @@ -5906,7 +5932,8 @@ mod tests { .map(move |(_stake_pubkey, stake_account)| (stake_account, vote_account)) }) .map(|(stake_account, vote_account)| { - stake_state::calculate_points(&stake_account, &vote_account, None).unwrap_or(0) + stake_state::calculate_points(&stake_account, &vote_account, None, true) + .unwrap_or(0) }) .sum(); @@ -7323,7 +7350,7 @@ mod tests { // epoch_stakes are a snapshot at the leader_schedule_slot_offset boundary // in the prior epoch (0 in this case) assert_eq!( - leader_stake.stake(0, None), + leader_stake.stake(0, None, true), vote_accounts.unwrap().get(&leader_vote_account).unwrap().0 ); @@ -7339,7 +7366,7 @@ mod tests { assert!(child.epoch_vote_accounts(epoch).is_some()); assert_eq!( - leader_stake.stake(child.epoch(), None), + leader_stake.stake(child.epoch(), None, true), child .epoch_vote_accounts(epoch) .unwrap() @@ -7357,7 +7384,7 @@ mod tests { ); assert!(child.epoch_vote_accounts(epoch).is_some()); assert_eq!( - leader_stake.stake(child.epoch(), None), + leader_stake.stake(child.epoch(), None, true), child .epoch_vote_accounts(epoch) .unwrap() diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index d7028a1db1..073547ef33 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -29,7 +29,7 @@ impl Stakes { pub fn history(&self) -> &StakeHistory { &self.stake_history } - pub fn clone_with_epoch(&self, next_epoch: Epoch) -> Self { + pub fn clone_with_epoch(&self, next_epoch: Epoch, fix_stake_deactivate: bool) -> Self { let prev_epoch = self.epoch; if prev_epoch == next_epoch { self.clone() @@ -44,6 +44,7 @@ impl Stakes { .iter() .map(|(_pubkey, stake_delegation)| stake_delegation), Some(&self.stake_history), + fix_stake_deactivate, ), ); @@ -59,6 +60,7 @@ impl Stakes { pubkey, next_epoch, Some(&stake_history_upto_prev_epoch), + fix_stake_deactivate, ), account.clone(), ), @@ -82,12 +84,13 @@ impl Stakes { voter_pubkey: &Pubkey, epoch: Epoch, stake_history: Option<&StakeHistory>, + fix_stake_deactivate: bool, ) -> u64 { self.stake_delegations .iter() .map(|(_, stake_delegation)| { if &stake_delegation.voter_pubkey == voter_pubkey { - stake_delegation.stake(epoch, stake_history) + stake_delegation.stake(epoch, stake_history, fix_stake_deactivate) } else { 0 } @@ -113,12 +116,24 @@ impl Stakes { && account.data.len() >= std::mem::size_of::() } - pub fn store(&mut self, pubkey: &Pubkey, account: &Account) -> Option { + pub fn store( + &mut self, + pubkey: &Pubkey, + account: &Account, + fix_stake_deactivate: bool, + ) -> Option { if solana_vote_program::check_id(&account.owner) { let old = self.vote_accounts.remove(pubkey); if account.lamports != 0 { let stake = old.as_ref().map_or_else( - || self.calculate_stake(pubkey, self.epoch, Some(&self.stake_history)), + || { + self.calculate_stake( + pubkey, + self.epoch, + Some(&self.stake_history), + fix_stake_deactivate, + ) + }, |v| v.0, ); @@ -130,7 +145,7 @@ impl Stakes { let old_stake = self.stake_delegations.get(pubkey).map(|delegation| { ( delegation.voter_pubkey, - delegation.stake(self.epoch, Some(&self.stake_history)), + delegation.stake(self.epoch, Some(&self.stake_history), fix_stake_deactivate), ) }); @@ -140,7 +155,11 @@ impl Stakes { ( delegation.voter_pubkey, if account.lamports != 0 { - delegation.stake(self.epoch, Some(&self.stake_history)) + delegation.stake( + self.epoch, + Some(&self.stake_history), + fix_stake_deactivate, + ) } else { 0 }, @@ -264,44 +283,44 @@ pub mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, mut stake_account)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); let stake = StakeState::stake_from(&stake_account).unwrap(); { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(i, None) + stake.stake(i, None, true) ); } stake_account.lamports = 42; - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&stake_pubkey, &stake_account, true); { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(i, None) + stake.stake(i, None, true) ); // stays old stake, because only 10 is activated } // activate more let (_stake_pubkey, mut stake_account) = create_stake_account(42, &vote_pubkey); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&stake_pubkey, &stake_account, true); let stake = StakeState::stake_from(&stake_account).unwrap(); { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(i, None) + stake.stake(i, None, true) ); // now stake of 42 is activated } stake_account.lamports = 0; - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&stake_pubkey, &stake_account, true); { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); @@ -319,14 +338,14 @@ pub mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); let ((vote11_pubkey, vote11_account), (stake11_pubkey, stake11_account)) = create_staked_node_accounts(20); - stakes.store(&vote11_pubkey, &vote11_account); - stakes.store(&stake11_pubkey, &stake11_account); + stakes.store(&vote11_pubkey, &vote11_account, true); + stakes.store(&stake11_pubkey, &stake11_account, true); let vote11_node_pubkey = VoteState::from(&vote11_account).unwrap().node_pubkey; @@ -341,8 +360,8 @@ pub mod tests { let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); { let vote_accounts = stakes.vote_accounts(); @@ -351,14 +370,14 @@ pub mod tests { } vote_account.lamports = 0; - stakes.store(&vote_pubkey, &vote_account); + stakes.store(&vote_pubkey, &vote_account, true); { let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_none()); } vote_account.lamports = 1; - stakes.store(&vote_pubkey, &vote_account); + stakes.store(&vote_pubkey, &vote_account, true); { let vote_accounts = stakes.vote_accounts(); @@ -378,11 +397,11 @@ pub mod tests { let ((vote_pubkey2, vote_account2), (_stake_pubkey2, stake_account2)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&vote_pubkey2, &vote_account2); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&vote_pubkey2, &vote_account2, true); // delegates to vote_pubkey - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&stake_pubkey, &stake_account, true); let stake = StakeState::stake_from(&stake_account).unwrap(); @@ -391,14 +410,14 @@ pub mod tests { assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch, Some(&stakes.stake_history)) + stake.stake(stakes.epoch, Some(&stakes.stake_history), true) ); assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!(vote_accounts.get(&vote_pubkey2).unwrap().0, 0); } // delegates to vote_pubkey2 - stakes.store(&stake_pubkey, &stake_account2); + stakes.store(&stake_pubkey, &stake_account2, true); { let vote_accounts = stakes.vote_accounts(); @@ -407,7 +426,7 @@ pub mod tests { assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!( vote_accounts.get(&vote_pubkey2).unwrap().0, - stake.stake(stakes.epoch, Some(&stakes.stake_history)) + stake.stake(stakes.epoch, Some(&stakes.stake_history), true) ); } } @@ -421,11 +440,11 @@ pub mod tests { let (stake_pubkey2, stake_account2) = create_stake_account(10, &vote_pubkey); - stakes.store(&vote_pubkey, &vote_account); + stakes.store(&vote_pubkey, &vote_account, true); // delegates to vote_pubkey - stakes.store(&stake_pubkey, &stake_account); - stakes.store(&stake_pubkey2, &stake_account2); + stakes.store(&stake_pubkey, &stake_account, true); + stakes.store(&stake_pubkey2, &stake_account2, true); { let vote_accounts = stakes.vote_accounts(); @@ -440,23 +459,23 @@ pub mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); let stake = StakeState::stake_from(&stake_account).unwrap(); { let vote_accounts = stakes.vote_accounts(); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch, Some(&stakes.stake_history)) + stake.stake(stakes.epoch, Some(&stakes.stake_history), true) ); } - let stakes = stakes.clone_with_epoch(3); + let stakes = stakes.clone_with_epoch(3, true); { let vote_accounts = stakes.vote_accounts(); assert_eq!( vote_accounts.get(&vote_pubkey).unwrap().0, - stake.stake(stakes.epoch, Some(&stakes.stake_history)) + stake.stake(stakes.epoch, Some(&stakes.stake_history), true) ); } } @@ -469,8 +488,8 @@ pub mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); { let vote_accounts = stakes.vote_accounts(); @@ -482,6 +501,7 @@ pub mod tests { stakes.store( &stake_pubkey, &Account::new(1, 0, &solana_stake_program::id()), + true, ); { let vote_accounts = stakes.vote_accounts(); @@ -511,14 +531,14 @@ pub mod tests { let genesis_epoch = 0; let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_warming_staked_node_accounts(10, genesis_epoch); - stakes.store(&vote_pubkey, &vote_account); - stakes.store(&stake_pubkey, &stake_account); + stakes.store(&vote_pubkey, &vote_account, true); + stakes.store(&stake_pubkey, &stake_account, true); assert_eq!(stakes.vote_balance_and_staked(), 11); assert_eq!(stakes.vote_balance_and_warmed_staked(), 1); for (epoch, expected_warmed_stake) in ((genesis_epoch + 1)..=3).zip(&[2, 3, 4]) { - stakes = stakes.clone_with_epoch(epoch); + stakes = stakes.clone_with_epoch(epoch, true); // vote_balance_and_staked() always remain to return same lamports // while vote_balance_and_warmed_staked() gradually increases assert_eq!(stakes.vote_balance_and_staked(), 11); diff --git a/runtime/tests/stake.rs b/runtime/tests/stake.rs index 55b0bd65ec..a8025188af 100644 --- a/runtime/tests/stake.rs +++ b/runtime/tests/stake.rs @@ -80,6 +80,7 @@ fn warmed_up(bank: &Bank, stake_pubkey: &Pubkey) -> bool { ) .unwrap(), ), + true, ) } @@ -94,6 +95,7 @@ fn get_staked(bank: &Bank, stake_pubkey: &Pubkey) -> u64 { ) .unwrap(), ), + true, ) }