fix rewards points (#10914)
* fix rewards points
* fixups
* * verify that we don't spend more in rewards than we've allocated for rewards
* purge f64s from calculations that could be done with integers
* test typical values
* simplify iteration over delegations some
* fixups
* Use try_from
* Add a comment for commission_split()
* Add assertion to detect inconsistent reward dist.
* Fix vote_balance_and_staked
* Don't overwrite accounts with stale copies
* Fix CI...
* Add tests for vote_balance_and_staked
* Add test for the determinism of update_rewards
* Revert "Don't overwrite accounts with stale copies"
This reverts commit 9886d085a6.
* Make stake_delegation_accounts to return hashmap
Co-authored-by: Ryo Onodera <ryoqun@gmail.com>
			
			
This commit is contained in:
		| @@ -19,7 +19,7 @@ use solana_sdk::{ | |||||||
|     stake_history::{StakeHistory, StakeHistoryEntry}, |     stake_history::{StakeHistory, StakeHistoryEntry}, | ||||||
| }; | }; | ||||||
| use solana_vote_program::vote_state::{VoteState, VoteStateVersions}; | use solana_vote_program::vote_state::{VoteState, VoteStateVersions}; | ||||||
| use std::collections::HashSet; | use std::{collections::HashSet, convert::TryFrom}; | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)] | ||||||
| #[allow(clippy::large_enum_variant)] | #[allow(clippy::large_enum_variant)] | ||||||
| @@ -362,6 +362,15 @@ impl Authorized { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// captures a rewards round as lamports to be awarded | ||||||
|  | ///  and the total points over which those lamports | ||||||
|  | ///  are to be distributed | ||||||
|  | //  basically read as rewards/points, but in integers instead of as an f64 | ||||||
|  | pub struct PointValue { | ||||||
|  |     pub rewards: u64, // lamports to split | ||||||
|  |     pub points: u128, // over these points | ||||||
|  | } | ||||||
|  |  | ||||||
| impl Stake { | impl Stake { | ||||||
|     pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { |     pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { | ||||||
|         self.delegation.stake(epoch, history) |         self.delegation.stake(epoch, history) | ||||||
| @@ -369,36 +378,41 @@ impl Stake { | |||||||
|  |  | ||||||
|     pub fn redeem_rewards( |     pub fn redeem_rewards( | ||||||
|         &mut self, |         &mut self, | ||||||
|         point_value: f64, |         point_value: &PointValue, | ||||||
|         vote_state: &VoteState, |         vote_state: &VoteState, | ||||||
|         stake_history: Option<&StakeHistory>, |         stake_history: Option<&StakeHistory>, | ||||||
|     ) -> Option<(u64, u64)> { |     ) -> Option<(u64, u64)> { | ||||||
|         self.calculate_rewards(point_value, vote_state, stake_history) |         self.calculate_rewards(point_value, vote_state, stake_history) | ||||||
|             .map(|(voters_reward, stakers_reward, credits_observed)| { |             .map(|(stakers_reward, voters_reward, credits_observed)| { | ||||||
|                 self.credits_observed = credits_observed; |                 self.credits_observed = credits_observed; | ||||||
|                 self.delegation.stake += stakers_reward; |                 self.delegation.stake += stakers_reward; | ||||||
|                 (voters_reward, stakers_reward) |                 (stakers_reward, voters_reward) | ||||||
|             }) |             }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// for a given stake and vote_state, calculate what distributions and what updates should be made |     pub fn calculate_points( | ||||||
|     /// returns a tuple in the case of a payout of: |  | ||||||
|     ///   * voter_rewards to be distributed |  | ||||||
|     ///   * staker_rewards to be distributed |  | ||||||
|     ///   * new value for credits_observed in the stake |  | ||||||
|     //  returns None if there's no payout or if any deserved payout is < 1 lamport |  | ||||||
|     pub fn calculate_rewards( |  | ||||||
|         &self, |         &self, | ||||||
|         point_value: f64, |  | ||||||
|         vote_state: &VoteState, |         vote_state: &VoteState, | ||||||
|         stake_history: Option<&StakeHistory>, |         stake_history: Option<&StakeHistory>, | ||||||
|     ) -> Option<(u64, u64, u64)> { |     ) -> u128 { | ||||||
|  |         self.calculate_points_and_credits(vote_state, stake_history) | ||||||
|  |             .0 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// for a given stake and vote_state, calculate how many | ||||||
|  |     ///   points were earned (credits * stake) and new value | ||||||
|  |     ///   for credits_observed were the points paid | ||||||
|  |     pub fn calculate_points_and_credits( | ||||||
|  |         &self, | ||||||
|  |         vote_state: &VoteState, | ||||||
|  |         stake_history: Option<&StakeHistory>, | ||||||
|  |     ) -> (u128, u64) { | ||||||
|         if self.credits_observed >= vote_state.credits() { |         if self.credits_observed >= vote_state.credits() { | ||||||
|             return None; |             return (0, 0); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let mut credits_observed = self.credits_observed; |         let mut credits_observed = self.credits_observed; | ||||||
|         let mut total_rewards = 0f64; |         let mut points = 0u128; | ||||||
|         for (epoch, credits, prev_credits) in vote_state.epoch_credits() { |         for (epoch, credits, prev_credits) in vote_state.epoch_credits() { | ||||||
|             // figure out how much this stake has seen that |             // figure out how much this stake has seen that | ||||||
|             //   for which the vote account has a record |             //   for which the vote account has a record | ||||||
| @@ -414,29 +428,56 @@ impl Stake { | |||||||
|                 0 |                 0 | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             total_rewards += |             points += u128::from(self.delegation.stake(*epoch, stake_history)) | ||||||
|                 (self.delegation.stake(*epoch, stake_history) * epoch_credits) as f64 * point_value; |                 * u128::from(epoch_credits); | ||||||
|  |  | ||||||
|             // don't want to assume anything about order of the iterator... |             // don't want to assume anything about order of the iterator... | ||||||
|             credits_observed = credits_observed.max(*credits); |             credits_observed = credits_observed.max(*credits); | ||||||
|         } |         } | ||||||
|         // don't bother trying to collect fractional lamports |         (points, credits_observed) | ||||||
|         if total_rewards < 1f64 { |     } | ||||||
|  |  | ||||||
|  |     /// for a given stake and vote_state, calculate what distributions and what updates should be made | ||||||
|  |     /// returns a tuple in the case of a payout of: | ||||||
|  |     ///   * staker_rewards to be distributed | ||||||
|  |     ///   * voter_rewards to be distributed | ||||||
|  |     ///   * new value for credits_observed in the stake | ||||||
|  |     //  returns None if there's no payout or if any deserved payout is < 1 lamport | ||||||
|  |     pub fn calculate_rewards( | ||||||
|  |         &self, | ||||||
|  |         point_value: &PointValue, | ||||||
|  |         vote_state: &VoteState, | ||||||
|  |         stake_history: Option<&StakeHistory>, | ||||||
|  |     ) -> Option<(u64, u64, u64)> { | ||||||
|  |         let (points, credits_observed) = | ||||||
|  |             self.calculate_points_and_credits(vote_state, stake_history); | ||||||
|  |  | ||||||
|  |         if points == 0 || point_value.points == 0 { | ||||||
|             return None; |             return None; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(total_rewards); |         let rewards = points | ||||||
|  |             .checked_mul(u128::from(point_value.rewards)) | ||||||
|  |             .unwrap() | ||||||
|  |             .checked_div(point_value.points) | ||||||
|  |             .unwrap(); | ||||||
|  |  | ||||||
|         if (voter_rewards < 1f64 || staker_rewards < 1f64) && is_split { |         let rewards = u64::try_from(rewards).unwrap(); | ||||||
|             // don't bother trying to collect fractional lamports |  | ||||||
|  |         // don't bother trying to split if fractional lamports got truncated | ||||||
|  |         if rewards == 0 { | ||||||
|  |             return None; | ||||||
|  |         } | ||||||
|  |         let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(rewards); | ||||||
|  |  | ||||||
|  |         if (voter_rewards == 0 || staker_rewards == 0) && is_split { | ||||||
|  |             // don't collect if we lose a whole lamport somewhere | ||||||
|  |             //  is_split means there should be tokens on both sides, | ||||||
|  |             //  uncool to move credits_observed if one side didn't get paid | ||||||
|             return None; |             return None; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Some(( |         Some((staker_rewards, voter_rewards, credits_observed)) | ||||||
|             voter_rewards as u64, |  | ||||||
|             staker_rewards as u64, |  | ||||||
|             credits_observed, |  | ||||||
|         )) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn redelegate( |     fn redelegate( | ||||||
| @@ -836,17 +877,18 @@ impl<'a> StakeAccount for KeyedAccount<'a> { | |||||||
| } | } | ||||||
|  |  | ||||||
| // utility function, used by runtime | // utility function, used by runtime | ||||||
|  | // returns a tuple of (stakers_reward,voters_reward) | ||||||
| pub fn redeem_rewards( | pub fn redeem_rewards( | ||||||
|     stake_account: &mut Account, |     stake_account: &mut Account, | ||||||
|     vote_account: &mut Account, |     vote_account: &mut Account, | ||||||
|     point_value: f64, |     point_value: &PointValue, | ||||||
|     stake_history: Option<&StakeHistory>, |     stake_history: Option<&StakeHistory>, | ||||||
| ) -> Result<(u64, u64), InstructionError> { | ) -> Result<(u64, u64), InstructionError> { | ||||||
|     if let StakeState::Stake(meta, mut stake) = stake_account.state()? { |     if let StakeState::Stake(meta, mut stake) = stake_account.state()? { | ||||||
|         let vote_state: VoteState = |         let vote_state: VoteState = | ||||||
|             StateMut::<VoteStateVersions>::state(vote_account)?.convert_to_current(); |             StateMut::<VoteStateVersions>::state(vote_account)?.convert_to_current(); | ||||||
|  |  | ||||||
|         if let Some((voters_reward, stakers_reward)) = |         if let Some((stakers_reward, voters_reward)) = | ||||||
|             stake.redeem_rewards(point_value, &vote_state, stake_history) |             stake.redeem_rewards(point_value, &vote_state, stake_history) | ||||||
|         { |         { | ||||||
|             stake_account.lamports += stakers_reward; |             stake_account.lamports += stakers_reward; | ||||||
| @@ -863,6 +905,22 @@ pub fn redeem_rewards( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // utility function, used by runtime | ||||||
|  | pub fn calculate_points( | ||||||
|  |     stake_account: &Account, | ||||||
|  |     vote_account: &Account, | ||||||
|  |     stake_history: Option<&StakeHistory>, | ||||||
|  | ) -> Result<u128, InstructionError> { | ||||||
|  |     if let StakeState::Stake(_meta, stake) = stake_account.state()? { | ||||||
|  |         let vote_state: VoteState = | ||||||
|  |             StateMut::<VoteStateVersions>::state(vote_account)?.convert_to_current(); | ||||||
|  |  | ||||||
|  |         Ok(stake.calculate_points(&vote_state, stake_history)) | ||||||
|  |     } else { | ||||||
|  |         Err(InstructionError::InvalidAccountData) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| // utility function, used by runtime::Stakes, tests | // utility function, used by runtime::Stakes, tests | ||||||
| pub fn new_stake_history_entry<'a, I>( | pub fn new_stake_history_entry<'a, I>( | ||||||
|     epoch: Epoch, |     epoch: Epoch, | ||||||
| @@ -923,6 +981,43 @@ pub fn create_account( | |||||||
|     vote_account: &Account, |     vote_account: &Account, | ||||||
|     rent: &Rent, |     rent: &Rent, | ||||||
|     lamports: u64, |     lamports: u64, | ||||||
|  | ) -> Account { | ||||||
|  |     do_create_account( | ||||||
|  |         authorized, | ||||||
|  |         voter_pubkey, | ||||||
|  |         vote_account, | ||||||
|  |         rent, | ||||||
|  |         lamports, | ||||||
|  |         Epoch::MAX, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // utility function, used by tests | ||||||
|  | pub fn create_account_with_activation_epoch( | ||||||
|  |     authorized: &Pubkey, | ||||||
|  |     voter_pubkey: &Pubkey, | ||||||
|  |     vote_account: &Account, | ||||||
|  |     rent: &Rent, | ||||||
|  |     lamports: u64, | ||||||
|  |     activation_epoch: Epoch, | ||||||
|  | ) -> Account { | ||||||
|  |     do_create_account( | ||||||
|  |         authorized, | ||||||
|  |         voter_pubkey, | ||||||
|  |         vote_account, | ||||||
|  |         rent, | ||||||
|  |         lamports, | ||||||
|  |         activation_epoch, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn do_create_account( | ||||||
|  |     authorized: &Pubkey, | ||||||
|  |     voter_pubkey: &Pubkey, | ||||||
|  |     vote_account: &Account, | ||||||
|  |     rent: &Rent, | ||||||
|  |     lamports: u64, | ||||||
|  |     activation_epoch: Epoch, | ||||||
| ) -> Account { | ) -> Account { | ||||||
|     let mut stake_account = Account::new(lamports, std::mem::size_of::<StakeState>(), &id()); |     let mut stake_account = Account::new(lamports, std::mem::size_of::<StakeState>(), &id()); | ||||||
|  |  | ||||||
| @@ -941,7 +1036,7 @@ pub fn create_account( | |||||||
|                 lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve); |                 lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve); | ||||||
|                 voter_pubkey, |                 voter_pubkey, | ||||||
|                 &vote_state, |                 &vote_state, | ||||||
|                 std::u64::MAX, |                 activation_epoch, | ||||||
|                 &Config::default(), |                 &Config::default(), | ||||||
|             ), |             ), | ||||||
|         )) |         )) | ||||||
| @@ -954,7 +1049,7 @@ pub fn create_account( | |||||||
| mod tests { | mod tests { | ||||||
|     use super::*; |     use super::*; | ||||||
|     use crate::id; |     use crate::id; | ||||||
|     use solana_sdk::{account::Account, pubkey::Pubkey, system_program}; |     use solana_sdk::{account::Account, native_token, pubkey::Pubkey, system_program}; | ||||||
|     use solana_vote_program::vote_state; |     use solana_vote_program::vote_state; | ||||||
|     use std::cell::RefCell; |     use std::cell::RefCell; | ||||||
|  |  | ||||||
| @@ -2171,7 +2266,14 @@ mod tests { | |||||||
|         // this one can't collect now, credits_observed == vote_state.credits() |         // this one can't collect now, credits_observed == vote_state.credits() | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             None, |             None, | ||||||
|             stake.redeem_rewards(1_000_000_000.0, &vote_state, None) |             stake.redeem_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 1_000_000_000, | ||||||
|  |                     points: 1 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // put 2 credits in at epoch 0 |         // put 2 credits in at epoch 0 | ||||||
| @@ -2180,8 +2282,15 @@ mod tests { | |||||||
|  |  | ||||||
|         // this one should be able to collect exactly 2 |         // this one should be able to collect exactly 2 | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some((0, stake_lamports * 2)), |             Some((stake_lamports * 2, 0)), | ||||||
|             stake.redeem_rewards(1.0, &vote_state, None) |             stake.redeem_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 1, | ||||||
|  |                     points: 1 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
| @@ -2191,6 +2300,47 @@ mod tests { | |||||||
|         assert_eq!(stake.credits_observed, 2); |         assert_eq!(stake.credits_observed, 2); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_stake_state_calculate_points_with_typical_values() { | ||||||
|  |         let mut vote_state = VoteState::default(); | ||||||
|  |  | ||||||
|  |         // bootstrap means fully-vested stake at epoch 0 with | ||||||
|  |         //  10_000_000 SOL is a big but not unreasaonable stake | ||||||
|  |         let stake = Stake::new( | ||||||
|  |             native_token::sol_to_lamports(10_000_000f64), | ||||||
|  |             &Pubkey::default(), | ||||||
|  |             &vote_state, | ||||||
|  |             std::u64::MAX, | ||||||
|  |             &Config::default(), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // this one can't collect now, credits_observed == vote_state.credits() | ||||||
|  |         assert_eq!( | ||||||
|  |             None, | ||||||
|  |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 1_000_000_000, | ||||||
|  |                     points: 1 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         let epoch_slots: u128 = 14 * 24 * 3600 * 160; | ||||||
|  |         // put 193,536,000 credits in at epoch 0, typical for a 14-day epoch | ||||||
|  |         //  this loop takes a few seconds... | ||||||
|  |         for _ in 0..epoch_slots { | ||||||
|  |             vote_state.increment_credits(0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // no overflow on points | ||||||
|  |         assert_eq!( | ||||||
|  |             u128::from(stake.delegation.stake) * epoch_slots, | ||||||
|  |             stake.calculate_points(&vote_state, None) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_stake_state_calculate_rewards() { |     fn test_stake_state_calculate_rewards() { | ||||||
|         let mut vote_state = VoteState::default(); |         let mut vote_state = VoteState::default(); | ||||||
| @@ -2207,7 +2357,14 @@ mod tests { | |||||||
|         // this one can't collect now, credits_observed == vote_state.credits() |         // this one can't collect now, credits_observed == vote_state.credits() | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             None, |             None, | ||||||
|             stake.calculate_rewards(1_000_000_000.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 1_000_000_000, | ||||||
|  |                     points: 1 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // put 2 credits in at epoch 0 |         // put 2 credits in at epoch 0 | ||||||
| @@ -2216,15 +2373,29 @@ mod tests { | |||||||
|  |  | ||||||
|         // this one should be able to collect exactly 2 |         // this one should be able to collect exactly 2 | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some((0, stake.delegation.stake * 2, 2)), |             Some((stake.delegation.stake * 2, 0, 2)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 2, | ||||||
|  |                     points: 2 // all his | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         stake.credits_observed = 1; |         stake.credits_observed = 1; | ||||||
|         // this one should be able to collect exactly 1 (already observed one) |         // this one should be able to collect exactly 1 (already observed one) | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some((0, stake.delegation.stake, 2)), |             Some((stake.delegation.stake, 0, 2)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 1, | ||||||
|  |                     points: 1 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // put 1 credit in epoch 1 |         // put 1 credit in epoch 1 | ||||||
| @@ -2233,16 +2404,30 @@ mod tests { | |||||||
|         stake.credits_observed = 2; |         stake.credits_observed = 2; | ||||||
|         // this one should be able to collect the one just added |         // this one should be able to collect the one just added | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some((0, stake.delegation.stake, 3)), |             Some((stake.delegation.stake, 0, 3)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 2, | ||||||
|  |                     points: 2 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // put 1 credit in epoch 2 |         // put 1 credit in epoch 2 | ||||||
|         vote_state.increment_credits(2); |         vote_state.increment_credits(2); | ||||||
|         // this one should be able to collect 2 now |         // this one should be able to collect 2 now | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some((0, stake.delegation.stake * 2, 4)), |             Some((stake.delegation.stake * 2, 0, 4)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 2, | ||||||
|  |                     points: 2 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         stake.credits_observed = 0; |         stake.credits_observed = 0; | ||||||
| @@ -2250,13 +2435,20 @@ mod tests { | |||||||
|         // (2 credits at stake of 1) + (1 credit at a stake of 2) |         // (2 credits at stake of 1) + (1 credit at a stake of 2) | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             Some(( |             Some(( | ||||||
|                 0, |  | ||||||
|                 stake.delegation.stake * 2 // epoch 0 |                 stake.delegation.stake * 2 // epoch 0 | ||||||
|                     + stake.delegation.stake // epoch 1 |                     + stake.delegation.stake // epoch 1 | ||||||
|                     + stake.delegation.stake, // epoch 2 |                     + stake.delegation.stake, // epoch 2 | ||||||
|  |                 0, | ||||||
|                 4 |                 4 | ||||||
|             )), |             )), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 4, | ||||||
|  |                     points: 4 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // same as above, but is a really small commission out of 32 bits, |         // same as above, but is a really small commission out of 32 bits, | ||||||
| @@ -2264,12 +2456,26 @@ mod tests { | |||||||
|         vote_state.commission = 1; |         vote_state.commission = 1; | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             None, // would be Some((0, 2 * 1 + 1 * 2, 4)), |             None, // would be Some((0, 2 * 1 + 1 * 2, 4)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 4, | ||||||
|  |                     points: 4 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|         vote_state.commission = 99; |         vote_state.commission = 99; | ||||||
|         assert_eq!( |         assert_eq!( | ||||||
|             None, // would be Some((0, 2 * 1 + 1 * 2, 4)), |             None, // would be Some((0, 2 * 1 + 1 * 2, 4)), | ||||||
|             stake.calculate_rewards(1.0, &vote_state, None) |             stake.calculate_rewards( | ||||||
|  |                 &PointValue { | ||||||
|  |                     rewards: 4, | ||||||
|  |                     points: 4 | ||||||
|  |                 }, | ||||||
|  |                 &vote_state, | ||||||
|  |                 None | ||||||
|  |             ) | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -253,13 +253,21 @@ impl VoteState { | |||||||
|     /// |     /// | ||||||
|     ///  if commission calculation is 100% one way or other, |     ///  if commission calculation is 100% one way or other, | ||||||
|     ///   indicate with false for was_split |     ///   indicate with false for was_split | ||||||
|     pub fn commission_split(&self, on: f64) -> (f64, f64, bool) { |     pub fn commission_split(&self, on: u64) -> (u64, u64, bool) { | ||||||
|         match self.commission.min(100) { |         match self.commission.min(100) { | ||||||
|             0 => (0.0, on, false), |             0 => (0, on, false), | ||||||
|             100 => (on, 0.0, false), |             100 => (on, 0, false), | ||||||
|             split => { |             split => { | ||||||
|                 let mine = on * f64::from(split) / f64::from(100); |                 let on = u128::from(on); | ||||||
|                 (mine, on - mine, true) |                 // Calculate mine and theirs independently and symmetrically instead of | ||||||
|  |                 // using the remainder of the other to treat them strictly equally. | ||||||
|  |                 // This is also to cancel the rewarding if either of the parties | ||||||
|  |                 // should receive only fractional lamports, resulting in not being rewarded at all. | ||||||
|  |                 // Thus, note that we intentionally discard any residual fractional lamports. | ||||||
|  |                 let mine = on * u128::from(split) / 100u128; | ||||||
|  |                 let theirs = on * u128::from(100 - split) / 100u128; | ||||||
|  |  | ||||||
|  |                 (mine as u64, theirs as u64, true) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -1562,19 +1570,22 @@ mod tests { | |||||||
|     fn test_vote_state_commission_split() { |     fn test_vote_state_commission_split() { | ||||||
|         let vote_state = VoteState::default(); |         let vote_state = VoteState::default(); | ||||||
|  |  | ||||||
|         assert_eq!(vote_state.commission_split(1.0), (0.0, 1.0, false)); |         assert_eq!(vote_state.commission_split(1), (0, 1, false)); | ||||||
|  |  | ||||||
|         let mut vote_state = VoteState::default(); |         let mut vote_state = VoteState::default(); | ||||||
|         vote_state.commission = std::u8::MAX; |         vote_state.commission = std::u8::MAX; | ||||||
|         assert_eq!(vote_state.commission_split(1.0), (1.0, 0.0, false)); |         assert_eq!(vote_state.commission_split(1), (1, 0, false)); | ||||||
|  |  | ||||||
|  |         vote_state.commission = 99; | ||||||
|  |         assert_eq!(vote_state.commission_split(10), (9, 0, true)); | ||||||
|  |  | ||||||
|  |         vote_state.commission = 1; | ||||||
|  |         assert_eq!(vote_state.commission_split(10), (0, 9, true)); | ||||||
|  |  | ||||||
|         vote_state.commission = 50; |         vote_state.commission = 50; | ||||||
|         let (voter_portion, staker_portion, was_split) = vote_state.commission_split(10.0); |         let (voter_portion, staker_portion, was_split) = vote_state.commission_split(10); | ||||||
|  |  | ||||||
|         assert_eq!( |         assert_eq!((voter_portion, staker_portion, was_split), (5, 5, true)); | ||||||
|             (voter_portion.round(), staker_portion.round(), was_split), |  | ||||||
|             (5.0, 5.0, true) |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|   | |||||||
| @@ -55,11 +55,12 @@ use solana_sdk::{ | |||||||
|     timing::years_as_slots, |     timing::years_as_slots, | ||||||
|     transaction::{Result, Transaction, TransactionError}, |     transaction::{Result, Transaction, TransactionError}, | ||||||
| }; | }; | ||||||
| use solana_stake_program::stake_state::{self, Delegation}; | use solana_stake_program::stake_state::{self, Delegation, PointValue}; | ||||||
| use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::VoteState}; | use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::VoteState}; | ||||||
| use std::{ | use std::{ | ||||||
|     cell::RefCell, |     cell::RefCell, | ||||||
|     collections::{HashMap, HashSet}, |     collections::{HashMap, HashSet}, | ||||||
|  |     convert::TryFrom, | ||||||
|     mem, |     mem, | ||||||
|     ops::RangeInclusive, |     ops::RangeInclusive, | ||||||
|     path::PathBuf, |     path::PathBuf, | ||||||
| @@ -898,11 +899,13 @@ impl Bank { | |||||||
|             let inflation = self.inflation.read().unwrap(); |             let inflation = self.inflation.read().unwrap(); | ||||||
|  |  | ||||||
|             (*inflation).validator(year) * self.capitalization() as f64 * period |             (*inflation).validator(year) * self.capitalization() as f64 * period | ||||||
|         }; |         } as u64; | ||||||
|  |  | ||||||
|         let validator_points = self.stakes.write().unwrap().claim_points(); |         let vote_balance_and_staked = self.stakes.read().unwrap().vote_balance_and_staked(); | ||||||
|         let validator_point_value = |  | ||||||
|             self.check_point_value(validator_rewards / validator_points as f64); |         let validator_point_value = self.pay_validator_rewards(validator_rewards); | ||||||
|  |  | ||||||
|  |         // this sysvar could be retired... | ||||||
|         self.update_sysvar_account(&sysvar::rewards::id(), |account| { |         self.update_sysvar_account(&sysvar::rewards::id(), |account| { | ||||||
|             sysvar::rewards::create_account( |             sysvar::rewards::create_account( | ||||||
|                 self.inherit_sysvar_account_balance(account), |                 self.inherit_sysvar_account_balance(account), | ||||||
| @@ -910,65 +913,117 @@ impl Bank { | |||||||
|             ) |             ) | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         let validator_rewards = self.pay_validator_rewards(validator_point_value); |         let validator_rewards_paid = | ||||||
|  |             self.stakes.read().unwrap().vote_balance_and_staked() - vote_balance_and_staked; | ||||||
|         self.capitalization |         if let Some(rewards) = self.rewards.as_ref() { | ||||||
|             .fetch_add(validator_rewards as u64, Ordering::Relaxed); |             assert_eq!( | ||||||
|  |                 validator_rewards_paid, | ||||||
|  |                 u64::try_from(rewards.iter().map(|(_pubkey, reward)| reward).sum::<i64>()).unwrap() | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     /// iterate over all stakes, redeem vote credits for each stake we can |         // verify that we didn't pay any more than we expected to | ||||||
|     ///   successfully load and parse, return total payout |         assert!(validator_rewards >= validator_rewards_paid); | ||||||
|     fn pay_validator_rewards(&mut self, point_value: f64) -> u64 { |  | ||||||
|         let stake_history = self.stakes.read().unwrap().history().clone(); |  | ||||||
|         let mut validator_rewards = HashMap::new(); |  | ||||||
|  |  | ||||||
|         let total_validator_rewards = self |         self.capitalization | ||||||
|  |             .fetch_add(validator_rewards_paid, Ordering::Relaxed); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// map stake delegations into resolved (pubkey, account) pairs | ||||||
|  |     ///  returns a map (has to be copied) of loaded | ||||||
|  |     ///   ( Vec<(staker info)> (voter account) ) keyed by voter pubkey | ||||||
|  |     /// | ||||||
|  |     fn stake_delegation_accounts(&self) -> HashMap<Pubkey, (Vec<(Pubkey, Account)>, Account)> { | ||||||
|  |         let mut accounts = HashMap::new(); | ||||||
|  |  | ||||||
|  |         self.stakes | ||||||
|  |             .read() | ||||||
|  |             .unwrap() | ||||||
|             .stake_delegations() |             .stake_delegations() | ||||||
|             .iter() |             .iter() | ||||||
|             .map(|(stake_pubkey, delegation)| { |             .for_each(|(stake_pubkey, delegation)| { | ||||||
|                 match ( |                 match ( | ||||||
|                     self.get_account(&stake_pubkey), |                     self.get_account(&stake_pubkey), | ||||||
|                     self.get_account(&delegation.voter_pubkey), |                     self.get_account(&delegation.voter_pubkey), | ||||||
|                 ) { |                 ) { | ||||||
|                     (Some(mut stake_account), Some(mut vote_account)) => { |                     (Some(stake_account), Some(vote_account)) => { | ||||||
|                         let rewards = stake_state::redeem_rewards( |                         let entry = accounts | ||||||
|                             &mut stake_account, |  | ||||||
|                             &mut vote_account, |  | ||||||
|                             point_value, |  | ||||||
|                             Some(&stake_history), |  | ||||||
|                         ); |  | ||||||
|                         if let Ok((stakers_reward, voters_reward)) = rewards { |  | ||||||
|                             self.store_account(&stake_pubkey, &stake_account); |  | ||||||
|                             self.store_account(&delegation.voter_pubkey, &vote_account); |  | ||||||
|  |  | ||||||
|                             if voters_reward > 0 { |  | ||||||
|                                 *validator_rewards |  | ||||||
|                             .entry(delegation.voter_pubkey) |                             .entry(delegation.voter_pubkey) | ||||||
|                                     .or_insert(0i64) += voters_reward as i64; |                             .or_insert((Vec::new(), vote_account)); | ||||||
|  |                         entry.0.push((*stake_pubkey, stake_account)); | ||||||
|  |                     } | ||||||
|  |                     (_, _) => {} | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         accounts | ||||||
|     } |     } | ||||||
|  |  | ||||||
|                             if stakers_reward > 0 { |     /// iterate over all stakes, redeem vote credits for each stake we can | ||||||
|                                 *validator_rewards.entry(*stake_pubkey).or_insert(0i64) += |     ///   successfully load and parse, return total payout | ||||||
|                                     stakers_reward as i64; |     fn pay_validator_rewards(&mut self, rewards: u64) -> f64 { | ||||||
|                             } |         let stake_history = self.stakes.read().unwrap().history().clone(); | ||||||
|  |  | ||||||
|                             stakers_reward + voters_reward |         let mut stake_delegation_accounts = self.stake_delegation_accounts(); | ||||||
|                         } else { |  | ||||||
|                             debug!( |         let points: u128 = stake_delegation_accounts | ||||||
|                                 "stake_state::redeem_rewards() failed for {}: {:?}", |             .iter() | ||||||
|                                 stake_pubkey, rewards |             .flat_map(|(_vote_pubkey, (stake_group, vote_account))| { | ||||||
|                             ); |                 stake_group | ||||||
|                             0 |                     .iter() | ||||||
|                         } |                     .map(move |(_stake_pubkey, stake_account)| (stake_account, vote_account)) | ||||||
|                     } |             }) | ||||||
|                     (_, _) => 0, |             .map(|(stake_account, vote_account)| { | ||||||
|                 } |                 stake_state::calculate_points(&stake_account, &vote_account, Some(&stake_history)) | ||||||
|  |                     .unwrap_or(0) | ||||||
|             }) |             }) | ||||||
|             .sum(); |             .sum(); | ||||||
|  |  | ||||||
|  |         if points == 0 { | ||||||
|  |             return 0.0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let point_value = PointValue { rewards, points }; | ||||||
|  |  | ||||||
|  |         let mut rewards = HashMap::new(); | ||||||
|  |  | ||||||
|  |         // pay according to point value | ||||||
|  |         for (vote_pubkey, (stake_group, vote_account)) in stake_delegation_accounts.iter_mut() { | ||||||
|  |             let mut vote_account_changed = false; | ||||||
|  |             for (stake_pubkey, stake_account) in stake_group.iter_mut() { | ||||||
|  |                 let redeemed = stake_state::redeem_rewards( | ||||||
|  |                     stake_account, | ||||||
|  |                     vote_account, | ||||||
|  |                     &point_value, | ||||||
|  |                     Some(&stake_history), | ||||||
|  |                 ); | ||||||
|  |                 if let Ok((stakers_reward, voters_reward)) = redeemed { | ||||||
|  |                     self.store_account(&stake_pubkey, &stake_account); | ||||||
|  |                     vote_account_changed = true; | ||||||
|  |  | ||||||
|  |                     if voters_reward > 0 { | ||||||
|  |                         *rewards.entry(*vote_pubkey).or_insert(0i64) += voters_reward as i64; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if stakers_reward > 0 { | ||||||
|  |                         *rewards.entry(*stake_pubkey).or_insert(0i64) += stakers_reward as i64; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     debug!( | ||||||
|  |                         "stake_state::redeem_rewards() failed for {}: {:?}", | ||||||
|  |                         stake_pubkey, redeemed | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if vote_account_changed { | ||||||
|  |                 self.store_account(&vote_pubkey, &vote_account); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         assert_eq!(self.rewards, None); |         assert_eq!(self.rewards, None); | ||||||
|         self.rewards = Some(validator_rewards.drain().collect()); |         self.rewards = Some(rewards.drain().collect()); | ||||||
|         total_validator_rewards |         point_value.rewards as f64 / point_value.points as f64 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn update_recent_blockhashes_locked(&self, locked_blockhash_queue: &BlockhashQueue) { |     fn update_recent_blockhashes_locked(&self, locked_blockhash_queue: &BlockhashQueue) { | ||||||
| @@ -986,21 +1041,6 @@ impl Bank { | |||||||
|         self.update_recent_blockhashes_locked(&blockhash_queue); |         self.update_recent_blockhashes_locked(&blockhash_queue); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // If the point values are not `normal`, bring them back into range and |  | ||||||
|     // set them to the last value or 0. |  | ||||||
|     fn check_point_value(&self, mut validator_point_value: f64) -> f64 { |  | ||||||
|         let rewards = sysvar::rewards::Rewards::from_account( |  | ||||||
|             &self |  | ||||||
|                 .get_account(&sysvar::rewards::id()) |  | ||||||
|                 .unwrap_or_else(|| sysvar::rewards::create_account(1, 0.0)), |  | ||||||
|         ) |  | ||||||
|         .unwrap_or_else(Default::default); |  | ||||||
|         if !validator_point_value.is_normal() { |  | ||||||
|             validator_point_value = rewards.validator_point_value; |  | ||||||
|         } |  | ||||||
|         validator_point_value |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fn collect_fees(&self) { |     fn collect_fees(&self) { | ||||||
|         let collector_fees = self.collector_fees.load(Ordering::Relaxed) as u64; |         let collector_fees = self.collector_fees.load(Ordering::Relaxed) as u64; | ||||||
|  |  | ||||||
| @@ -4730,7 +4770,18 @@ mod tests { | |||||||
|         } |         } | ||||||
|         bank.store_account(&vote_id, &vote_account); |         bank.store_account(&vote_id, &vote_account); | ||||||
|  |  | ||||||
|         let validator_points = bank.stakes.read().unwrap().points(); |         let validator_points: u128 = bank | ||||||
|  |             .stake_delegation_accounts() | ||||||
|  |             .iter() | ||||||
|  |             .flat_map(|(_vote_pubkey, (stake_group, vote_account))| { | ||||||
|  |                 stake_group | ||||||
|  |                     .iter() | ||||||
|  |                     .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) | ||||||
|  |             }) | ||||||
|  |             .sum(); | ||||||
|  |  | ||||||
|         // put a child bank in epoch 1, which calls update_rewards()... |         // put a child bank in epoch 1, which calls update_rewards()... | ||||||
|         let bank1 = Bank::new_from_parent( |         let bank1 = Bank::new_from_parent( | ||||||
| @@ -4774,6 +4825,89 @@ mod tests { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fn do_test_bank_update_rewards_determinism() -> u64 { | ||||||
|  |         // create a bank that ticks really slowly... | ||||||
|  |         let bank = Arc::new(Bank::new(&GenesisConfig { | ||||||
|  |             accounts: (0..42) | ||||||
|  |                 .map(|_| { | ||||||
|  |                     ( | ||||||
|  |                         Pubkey::new_rand(), | ||||||
|  |                         Account::new(1_000_000_000, 0, &Pubkey::default()), | ||||||
|  |                     ) | ||||||
|  |                 }) | ||||||
|  |                 .collect(), | ||||||
|  |             // set it up so the first epoch is a full year long | ||||||
|  |             poh_config: PohConfig { | ||||||
|  |                 target_tick_duration: Duration::from_secs( | ||||||
|  |                     SECONDS_PER_YEAR as u64 | ||||||
|  |                         / MINIMUM_SLOTS_PER_EPOCH as u64 | ||||||
|  |                         / DEFAULT_TICKS_PER_SLOT, | ||||||
|  |                 ), | ||||||
|  |                 hashes_per_tick: None, | ||||||
|  |                 target_tick_count: None, | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             ..GenesisConfig::default() | ||||||
|  |         })); | ||||||
|  |  | ||||||
|  |         // enable lazy rent collection because this test depends on rent-due accounts | ||||||
|  |         // not being eagerly-collected for exact rewards calculation | ||||||
|  |         bank.lazy_rent_collection.store(true, Ordering::Relaxed); | ||||||
|  |  | ||||||
|  |         assert_eq!(bank.capitalization(), 42 * 1_000_000_000); | ||||||
|  |         assert_eq!(bank.rewards, None); | ||||||
|  |  | ||||||
|  |         let vote_id = Pubkey::new_rand(); | ||||||
|  |         let mut vote_account = vote_state::create_account(&vote_id, &Pubkey::new_rand(), 50, 100); | ||||||
|  |         let (stake_id1, stake_account1) = crate::stakes::tests::create_stake_account(123, &vote_id); | ||||||
|  |         let (stake_id2, stake_account2) = crate::stakes::tests::create_stake_account(456, &vote_id); | ||||||
|  |  | ||||||
|  |         // set up accounts | ||||||
|  |         bank.store_account(&stake_id1, &stake_account1); | ||||||
|  |         bank.store_account(&stake_id2, &stake_account2); | ||||||
|  |  | ||||||
|  |         // generate some rewards | ||||||
|  |         let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); | ||||||
|  |         for i in 0..MAX_LOCKOUT_HISTORY + 42 { | ||||||
|  |             if let Some(v) = vote_state.as_mut() { | ||||||
|  |                 v.process_slot_vote_unchecked(i as u64) | ||||||
|  |             } | ||||||
|  |             let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); | ||||||
|  |             VoteState::to(&versioned, &mut vote_account).unwrap(); | ||||||
|  |             bank.store_account(&vote_id, &vote_account); | ||||||
|  |             match versioned { | ||||||
|  |                 VoteStateVersions::Current(v) => { | ||||||
|  |                     vote_state = Some(*v); | ||||||
|  |                 } | ||||||
|  |                 _ => panic!("Has to be of type Current"), | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         bank.store_account(&vote_id, &vote_account); | ||||||
|  |  | ||||||
|  |         // put a child bank in epoch 1, which calls update_rewards()... | ||||||
|  |         let bank1 = Bank::new_from_parent( | ||||||
|  |             &bank, | ||||||
|  |             &Pubkey::default(), | ||||||
|  |             bank.get_slots_in_epoch(bank.epoch()) + 1, | ||||||
|  |         ); | ||||||
|  |         // verify that there's inflation | ||||||
|  |         assert_ne!(bank1.capitalization(), bank.capitalization()); | ||||||
|  |  | ||||||
|  |         bank1.capitalization() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_bank_update_rewards_determinism() { | ||||||
|  |         // The same reward should be distributed given same credits | ||||||
|  |         let expected_capitalization = do_test_bank_update_rewards_determinism(); | ||||||
|  |         // Repeat somewhat large number of iterations to expose possible different behavior | ||||||
|  |         // depending on the randamly-seeded HashMap ordering | ||||||
|  |         for _ in 0..30 { | ||||||
|  |             let actual_capitalization = do_test_bank_update_rewards_determinism(); | ||||||
|  |             assert_eq!(actual_capitalization, expected_capitalization); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Test that purging 0 lamports accounts works. |     // Test that purging 0 lamports accounts works. | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_purge_empty_accounts() { |     fn test_purge_empty_accounts() { | ||||||
| @@ -6364,23 +6498,6 @@ mod tests { | |||||||
|         assert!(bank.is_delta.load(Ordering::Relaxed)); |         assert!(bank.is_delta.load(Ordering::Relaxed)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     #[allow(clippy::float_cmp)] |  | ||||||
|     fn test_check_point_value() { |  | ||||||
|         let (genesis_config, _) = create_genesis_config(500); |  | ||||||
|         let bank = Arc::new(Bank::new(&genesis_config)); |  | ||||||
|  |  | ||||||
|         // check that point values are 0 if no previous value was known and current values are not normal |  | ||||||
|         assert_eq!(bank.check_point_value(std::f64::INFINITY), 0.0); |  | ||||||
|  |  | ||||||
|         bank.store_account( |  | ||||||
|             &sysvar::rewards::id(), |  | ||||||
|             &sysvar::rewards::create_account(1, 1.0), |  | ||||||
|         ); |  | ||||||
|         // check that point values are the previous value if current values are not normal |  | ||||||
|         assert_eq!(bank.check_point_value(std::f64::INFINITY), 1.0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_bank_get_program_accounts() { |     fn test_bank_get_program_accounts() { | ||||||
|         let (genesis_config, mint_keypair) = create_genesis_config(500); |         let (genesis_config, mint_keypair) = create_genesis_config(500); | ||||||
|   | |||||||
| @@ -282,7 +282,7 @@ mod test_bank_serialize { | |||||||
|  |  | ||||||
|     // These some what long test harness is required to freeze the ABI of |     // These some what long test harness is required to freeze the ABI of | ||||||
|     // Bank's serialization due to versioned nature |     // Bank's serialization due to versioned nature | ||||||
|     #[frozen_abi(digest = "6MnT4MzuLHe4Uq96YaF3JF2gL1RvprzQHCaV9TaWgYLe")] |     #[frozen_abi(digest = "FaZaic5p7bvdsKDxGJmaPVyp12AbAmURyYoGiUdx1Ksu")] | ||||||
|     #[derive(Serialize, AbiExample)] |     #[derive(Serialize, AbiExample)] | ||||||
|     pub struct BankAbiTestWrapperFuture { |     pub struct BankAbiTestWrapperFuture { | ||||||
|         #[serde(serialize_with = "wrapper_future")] |         #[serde(serialize_with = "wrapper_future")] | ||||||
| @@ -305,7 +305,7 @@ mod test_bank_serialize { | |||||||
|         .serialize(s) |         .serialize(s) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[frozen_abi(digest = "92KVEUQ8PwKe5DgZ6AyDQGAi9pvi7kfHdMBE4hcs5EQ4")] |     #[frozen_abi(digest = "9g4bYykzsC86fULgu9iUh4kpvb1pxvAmipvyZPChLhws")] | ||||||
|     #[derive(Serialize, AbiExample)] |     #[derive(Serialize, AbiExample)] | ||||||
|     pub struct BankAbiTestWrapperLegacy { |     pub struct BankAbiTestWrapperLegacy { | ||||||
|         #[serde(serialize_with = "wrapper_legacy")] |         #[serde(serialize_with = "wrapper_legacy")] | ||||||
|   | |||||||
| @@ -15,9 +15,8 @@ pub struct Stakes { | |||||||
|     /// stake_delegations |     /// stake_delegations | ||||||
|     stake_delegations: HashMap<Pubkey, Delegation>, |     stake_delegations: HashMap<Pubkey, Delegation>, | ||||||
|  |  | ||||||
|     /// unclaimed points. |     /// unused | ||||||
|     //  a point is a credit multiplied by the stake |     unused: u64, | ||||||
|     points: u64, |  | ||||||
|  |  | ||||||
|     /// current epoch, used to calculate current stake |     /// current epoch, used to calculate current stake | ||||||
|     epoch: Epoch, |     epoch: Epoch, | ||||||
| @@ -49,7 +48,7 @@ impl Stakes { | |||||||
|  |  | ||||||
|             Stakes { |             Stakes { | ||||||
|                 stake_delegations: self.stake_delegations.clone(), |                 stake_delegations: self.stake_delegations.clone(), | ||||||
|                 points: self.points, |                 unused: self.unused, | ||||||
|                 epoch, |                 epoch, | ||||||
|                 vote_accounts: self |                 vote_accounts: self | ||||||
|                     .vote_accounts |                     .vote_accounts | ||||||
| @@ -88,6 +87,18 @@ impl Stakes { | |||||||
|             .sum() |             .sum() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn vote_balance_and_staked(&self) -> u64 { | ||||||
|  |         self.stake_delegations | ||||||
|  |             .iter() | ||||||
|  |             .map(|(_, stake_delegation)| stake_delegation.stake) | ||||||
|  |             .sum::<u64>() | ||||||
|  |             + self | ||||||
|  |                 .vote_accounts | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(_pubkey, (_staked, vote_account))| vote_account.lamports) | ||||||
|  |                 .sum::<u64>() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn is_stake(account: &Account) -> bool { |     pub fn is_stake(account: &Account) -> bool { | ||||||
|         solana_vote_program::check_id(&account.owner) |         solana_vote_program::check_id(&account.owner) | ||||||
|             || solana_stake_program::check_id(&account.owner) |             || solana_stake_program::check_id(&account.owner) | ||||||
| @@ -106,15 +117,6 @@ impl Stakes { | |||||||
|                     |v| v.0, |                     |v| v.0, | ||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|                 // count any increase in points, can only go forward |  | ||||||
|                 let old_credits = old |  | ||||||
|                     .and_then(|(_stake, old_account)| VoteState::credits_from(old_account)) |  | ||||||
|                     .unwrap_or(0); |  | ||||||
|  |  | ||||||
|                 let credits = VoteState::credits_from(account).unwrap_or(old_credits); |  | ||||||
|  |  | ||||||
|                 self.points += credits.saturating_sub(old_credits) * stake; |  | ||||||
|  |  | ||||||
|                 self.vote_accounts.insert(*pubkey, (stake, account.clone())); |                 self.vote_accounts.insert(*pubkey, (stake, account.clone())); | ||||||
|             } |             } | ||||||
|         } else if solana_stake_program::check_id(&account.owner) { |         } else if solana_stake_program::check_id(&account.owner) { | ||||||
| @@ -176,18 +178,6 @@ impl Stakes { | |||||||
|             .and_then(|(_k, (_stake, account))| VoteState::from(account)) |             .and_then(|(_k, (_stake, account))| VoteState::from(account)) | ||||||
|             .map(|vote_state| vote_state.node_pubkey) |             .map(|vote_state| vote_state.node_pubkey) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// currently unclaimed points |  | ||||||
|     pub fn points(&self) -> u64 { |  | ||||||
|         self.points |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// "claims" points, resets points to 0 |  | ||||||
|     pub fn claim_points(&mut self) -> u64 { |  | ||||||
|         let points = self.points; |  | ||||||
|         self.points = 0; |  | ||||||
|         points |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| @@ -195,9 +185,7 @@ pub mod tests { | |||||||
|     use super::*; |     use super::*; | ||||||
|     use solana_sdk::{pubkey::Pubkey, rent::Rent}; |     use solana_sdk::{pubkey::Pubkey, rent::Rent}; | ||||||
|     use solana_stake_program::stake_state; |     use solana_stake_program::stake_state; | ||||||
|     use solana_vote_program::vote_state::{ |     use solana_vote_program::vote_state::{self, VoteState}; | ||||||
|         self, VoteState, VoteStateVersions, MAX_LOCKOUT_HISTORY, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     //  set up some dummies for a staked node     ((     vote      )  (     stake     )) |     //  set up some dummies for a staked node     ((     vote      )  (     stake     )) | ||||||
|     pub fn create_staked_node_accounts(stake: u64) -> ((Pubkey, Account), (Pubkey, Account)) { |     pub fn create_staked_node_accounts(stake: u64) -> ((Pubkey, Account), (Pubkey, Account)) { | ||||||
| @@ -224,6 +212,38 @@ pub mod tests { | |||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn create_warming_staked_node_accounts( | ||||||
|  |         stake: u64, | ||||||
|  |         epoch: Epoch, | ||||||
|  |     ) -> ((Pubkey, Account), (Pubkey, Account)) { | ||||||
|  |         let vote_pubkey = Pubkey::new_rand(); | ||||||
|  |         let vote_account = vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 1); | ||||||
|  |         ( | ||||||
|  |             (vote_pubkey, vote_account), | ||||||
|  |             create_warming_stake_account(stake, epoch, &vote_pubkey), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //   add stake to a vote_pubkey                               (   stake    ) | ||||||
|  |     pub fn create_warming_stake_account( | ||||||
|  |         stake: u64, | ||||||
|  |         epoch: Epoch, | ||||||
|  |         vote_pubkey: &Pubkey, | ||||||
|  |     ) -> (Pubkey, Account) { | ||||||
|  |         let stake_pubkey = Pubkey::new_rand(); | ||||||
|  |         ( | ||||||
|  |             stake_pubkey, | ||||||
|  |             stake_state::create_account_with_activation_epoch( | ||||||
|  |                 &stake_pubkey, | ||||||
|  |                 &vote_pubkey, | ||||||
|  |                 &vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 1), | ||||||
|  |                 &Rent::free(), | ||||||
|  |                 stake, | ||||||
|  |                 epoch, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_stakes_basic() { |     fn test_stakes_basic() { | ||||||
|         for i in 0..4 { |         for i in 0..4 { | ||||||
| @@ -302,76 +322,6 @@ pub mod tests { | |||||||
|         assert_eq!(stakes.highest_staked_node(), Some(vote11_node_pubkey)) |         assert_eq!(stakes.highest_staked_node(), Some(vote11_node_pubkey)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |  | ||||||
|     fn test_stakes_points() { |  | ||||||
|         let mut stakes = Stakes::default(); |  | ||||||
|         stakes.epoch = 4; |  | ||||||
|  |  | ||||||
|         let stake = 42; |  | ||||||
|         assert_eq!(stakes.points(), 0); |  | ||||||
|         assert_eq!(stakes.claim_points(), 0); |  | ||||||
|         assert_eq!(stakes.claim_points(), 0); |  | ||||||
|  |  | ||||||
|         let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = |  | ||||||
|             create_staked_node_accounts(stake); |  | ||||||
|  |  | ||||||
|         stakes.store(&vote_pubkey, &vote_account); |  | ||||||
|         stakes.store(&stake_pubkey, &stake_account); |  | ||||||
|  |  | ||||||
|         assert_eq!(stakes.points(), 0); |  | ||||||
|         assert_eq!(stakes.claim_points(), 0); |  | ||||||
|  |  | ||||||
|         let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); |  | ||||||
|         for i in 0..MAX_LOCKOUT_HISTORY + 42 { |  | ||||||
|             if let Some(v) = vote_state.as_mut() { |  | ||||||
|                 v.process_slot_vote_unchecked(i as u64) |  | ||||||
|             } |  | ||||||
|             let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); |  | ||||||
|             VoteState::to(&versioned, &mut vote_account).unwrap(); |  | ||||||
|             match versioned { |  | ||||||
|                 VoteStateVersions::Current(v) => { |  | ||||||
|                     vote_state = Some(*v); |  | ||||||
|                 } |  | ||||||
|                 _ => panic!("Has to be of type Current"), |  | ||||||
|             }; |  | ||||||
|             stakes.store(&vote_pubkey, &vote_account); |  | ||||||
|             assert_eq!( |  | ||||||
|                 stakes.points(), |  | ||||||
|                 vote_state.as_ref().unwrap().credits() * stake |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         vote_account.lamports = 0; |  | ||||||
|         stakes.store(&vote_pubkey, &vote_account); |  | ||||||
|         assert_eq!( |  | ||||||
|             stakes.points(), |  | ||||||
|             vote_state.as_ref().unwrap().credits() * stake |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         assert_eq!( |  | ||||||
|             stakes.claim_points(), |  | ||||||
|             vote_state.as_ref().unwrap().credits() * stake |  | ||||||
|         ); |  | ||||||
|         assert_eq!(stakes.claim_points(), 0); |  | ||||||
|         assert_eq!(stakes.claim_points(), 0); |  | ||||||
|  |  | ||||||
|         // points come out of nowhere, but don't care here ;) |  | ||||||
|         vote_account.lamports = 1; |  | ||||||
|         stakes.store(&vote_pubkey, &vote_account); |  | ||||||
|         assert_eq!( |  | ||||||
|             stakes.points(), |  | ||||||
|             vote_state.as_ref().unwrap().credits() * stake |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         // test going backwards, should never go backwards |  | ||||||
|         let old_vote_state = vote_state; |  | ||||||
|         let vote_account = vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 1); |  | ||||||
|         stakes.store(&vote_pubkey, &vote_account); |  | ||||||
|         assert_eq!( |  | ||||||
|             stakes.points(), |  | ||||||
|             old_vote_state.as_ref().unwrap().credits() * stake |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     fn test_stakes_vote_account_disappear_reappear() { |     fn test_stakes_vote_account_disappear_reappear() { | ||||||
|         let mut stakes = Stakes::default(); |         let mut stakes = Stakes::default(); | ||||||
| @@ -528,4 +478,43 @@ pub mod tests { | |||||||
|             assert_eq!(vote_accounts.get(&vote_pubkey).unwrap().0, 0); |             assert_eq!(vote_accounts.get(&vote_pubkey).unwrap().0, 0); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_vote_balance_and_staked_empty() { | ||||||
|  |         let stakes = Stakes::default(); | ||||||
|  |         assert_eq!(stakes.vote_balance_and_staked(), 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn test_vote_balance_and_staked_normal() { | ||||||
|  |         let mut stakes = Stakes::default(); | ||||||
|  |         impl Stakes { | ||||||
|  |             pub fn vote_balance_and_warmed_staked(&self) -> u64 { | ||||||
|  |                 self.vote_accounts | ||||||
|  |                     .iter() | ||||||
|  |                     .map(|(_pubkey, (staked, account))| staked + account.lamports) | ||||||
|  |                     .sum() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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); | ||||||
|  |  | ||||||
|  |         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); | ||||||
|  |             // 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); | ||||||
|  |             assert_eq!( | ||||||
|  |                 stakes.vote_balance_and_warmed_staked(), | ||||||
|  |                 *expected_warmed_stake | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user