diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index b1d6ae497..07792a08e 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -19,7 +19,7 @@ use solana_sdk::{ stake_history::{StakeHistory, StakeHistoryEntry}, }; 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)] #[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 { pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 { self.delegation.stake(epoch, history) @@ -369,36 +378,41 @@ impl Stake { pub fn redeem_rewards( &mut self, - point_value: f64, + point_value: &PointValue, vote_state: &VoteState, stake_history: Option<&StakeHistory>, ) -> Option<(u64, u64)> { 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.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 - /// 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( + pub fn calculate_points( &self, - point_value: f64, vote_state: &VoteState, 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() { - return None; + return (0, 0); } 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() { // figure out how much this stake has seen that // for which the vote account has a record @@ -414,29 +428,56 @@ impl Stake { 0 }; - total_rewards += - (self.delegation.stake(*epoch, stake_history) * epoch_credits) as f64 * point_value; + points += u128::from(self.delegation.stake(*epoch, stake_history)) + * u128::from(epoch_credits); // don't want to assume anything about order of the iterator... credits_observed = credits_observed.max(*credits); } - // don't bother trying to collect fractional lamports - if total_rewards < 1f64 { + (points, credits_observed) + } + + /// 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; } - 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 { - // don't bother trying to collect fractional lamports + let rewards = u64::try_from(rewards).unwrap(); + + // 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; } - Some(( - voter_rewards as u64, - staker_rewards as u64, - credits_observed, - )) + Some((staker_rewards, voter_rewards, credits_observed)) } fn redelegate( @@ -836,17 +877,18 @@ impl<'a> StakeAccount for KeyedAccount<'a> { } // utility function, used by runtime +// returns a tuple of (stakers_reward,voters_reward) pub fn redeem_rewards( stake_account: &mut Account, vote_account: &mut Account, - point_value: f64, + point_value: &PointValue, stake_history: Option<&StakeHistory>, ) -> Result<(u64, u64), InstructionError> { if let StakeState::Stake(meta, mut stake) = stake_account.state()? { let vote_state: VoteState = StateMut::::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_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 { + 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)) + } else { + Err(InstructionError::InvalidAccountData) + } +} + // utility function, used by runtime::Stakes, tests pub fn new_stake_history_entry<'a, I>( epoch: Epoch, @@ -923,6 +981,43 @@ pub fn create_account( vote_account: &Account, rent: &Rent, 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 { let mut stake_account = Account::new(lamports, std::mem::size_of::(), &id()); @@ -941,7 +1036,7 @@ pub fn create_account( lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve); voter_pubkey, &vote_state, - std::u64::MAX, + activation_epoch, &Config::default(), ), )) @@ -954,7 +1049,7 @@ pub fn create_account( mod tests { use super::*; 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 std::cell::RefCell; @@ -2171,7 +2266,14 @@ mod tests { // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( 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 @@ -2180,8 +2282,15 @@ mod tests { // this one should be able to collect exactly 2 assert_eq!( - Some((0, stake_lamports * 2)), - stake.redeem_rewards(1.0, &vote_state, None) + Some((stake_lamports * 2, 0)), + stake.redeem_rewards( + &PointValue { + rewards: 1, + points: 1 + }, + &vote_state, + None + ) ); assert_eq!( @@ -2191,6 +2300,47 @@ mod tests { 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] fn test_stake_state_calculate_rewards() { let mut vote_state = VoteState::default(); @@ -2207,7 +2357,14 @@ mod tests { // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( 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 @@ -2216,15 +2373,29 @@ mod tests { // this one should be able to collect exactly 2 assert_eq!( - Some((0, stake.delegation.stake * 2, 2)), - stake.calculate_rewards(1.0, &vote_state, None) + Some((stake.delegation.stake * 2, 0, 2)), + stake.calculate_rewards( + &PointValue { + rewards: 2, + points: 2 // all his + }, + &vote_state, + None + ) ); stake.credits_observed = 1; // this one should be able to collect exactly 1 (already observed one) assert_eq!( - Some((0, stake.delegation.stake, 2)), - stake.calculate_rewards(1.0, &vote_state, None) + Some((stake.delegation.stake, 0, 2)), + stake.calculate_rewards( + &PointValue { + rewards: 1, + points: 1 + }, + &vote_state, + None + ) ); // put 1 credit in epoch 1 @@ -2233,16 +2404,30 @@ mod tests { stake.credits_observed = 2; // this one should be able to collect the one just added assert_eq!( - Some((0, stake.delegation.stake, 3)), - stake.calculate_rewards(1.0, &vote_state, None) + Some((stake.delegation.stake, 0, 3)), + stake.calculate_rewards( + &PointValue { + rewards: 2, + points: 2 + }, + &vote_state, + None + ) ); // put 1 credit in epoch 2 vote_state.increment_credits(2); // this one should be able to collect 2 now assert_eq!( - Some((0, stake.delegation.stake * 2, 4)), - stake.calculate_rewards(1.0, &vote_state, None) + Some((stake.delegation.stake * 2, 0, 4)), + stake.calculate_rewards( + &PointValue { + rewards: 2, + points: 2 + }, + &vote_state, + None + ) ); stake.credits_observed = 0; @@ -2250,13 +2435,20 @@ mod tests { // (2 credits at stake of 1) + (1 credit at a stake of 2) assert_eq!( Some(( - 0, stake.delegation.stake * 2 // epoch 0 + stake.delegation.stake // epoch 1 + stake.delegation.stake, // epoch 2 + 0, 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, @@ -2264,12 +2456,26 @@ mod tests { vote_state.commission = 1; assert_eq!( 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; assert_eq!( 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 + ) ); } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index f11f601bf..383c71b00 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -253,13 +253,21 @@ impl VoteState { /// /// if commission calculation is 100% one way or other, /// 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) { - 0 => (0.0, on, false), - 100 => (on, 0.0, false), + 0 => (0, on, false), + 100 => (on, 0, false), split => { - let mine = on * f64::from(split) / f64::from(100); - (mine, on - mine, true) + let on = u128::from(on); + // 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() { 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(); 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; - 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!( - (voter_portion.round(), staker_portion.round(), was_split), - (5.0, 5.0, true) - ); + assert_eq!((voter_portion, staker_portion, was_split), (5, 5, true)); } #[test] diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 0d726e7e7..5b47c6592 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -55,11 +55,12 @@ use solana_sdk::{ timing::years_as_slots, 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 std::{ cell::RefCell, collections::{HashMap, HashSet}, + convert::TryFrom, mem, ops::RangeInclusive, path::PathBuf, @@ -898,11 +899,13 @@ impl Bank { let inflation = self.inflation.read().unwrap(); (*inflation).validator(year) * self.capitalization() as f64 * period - }; + } as u64; - let validator_points = self.stakes.write().unwrap().claim_points(); - let validator_point_value = - self.check_point_value(validator_rewards / validator_points as f64); + let vote_balance_and_staked = self.stakes.read().unwrap().vote_balance_and_staked(); + + let validator_point_value = self.pay_validator_rewards(validator_rewards); + + // this sysvar could be retired... self.update_sysvar_account(&sysvar::rewards::id(), |account| { sysvar::rewards::create_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; + if let Some(rewards) = self.rewards.as_ref() { + assert_eq!( + validator_rewards_paid, + u64::try_from(rewards.iter().map(|(_pubkey, reward)| reward).sum::()).unwrap() + ); + } + + // verify that we didn't pay any more than we expected to + assert!(validator_rewards >= validator_rewards_paid); self.capitalization - .fetch_add(validator_rewards as u64, Ordering::Relaxed); + .fetch_add(validator_rewards_paid, Ordering::Relaxed); } - /// iterate over all stakes, redeem vote credits for each stake we can - /// successfully load and parse, return total payout - 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(); + /// 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, Account)> { + let mut accounts = HashMap::new(); - let total_validator_rewards = self + self.stakes + .read() + .unwrap() .stake_delegations() .iter() - .map(|(stake_pubkey, delegation)| { + .for_each(|(stake_pubkey, delegation)| { match ( self.get_account(&stake_pubkey), self.get_account(&delegation.voter_pubkey), ) { - (Some(mut stake_account), Some(mut vote_account)) => { - let rewards = stake_state::redeem_rewards( - &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) - .or_insert(0i64) += voters_reward as i64; - } - - if stakers_reward > 0 { - *validator_rewards.entry(*stake_pubkey).or_insert(0i64) += - stakers_reward as i64; - } - - stakers_reward + voters_reward - } else { - debug!( - "stake_state::redeem_rewards() failed for {}: {:?}", - stake_pubkey, rewards - ); - 0 - } + (Some(stake_account), Some(vote_account)) => { + let entry = accounts + .entry(delegation.voter_pubkey) + .or_insert((Vec::new(), vote_account)); + entry.0.push((*stake_pubkey, stake_account)); } - (_, _) => 0, + (_, _) => {} } + }); + + accounts + } + + /// iterate over all stakes, redeem vote credits for each stake we can + /// successfully load and parse, return total payout + fn pay_validator_rewards(&mut self, rewards: u64) -> f64 { + let stake_history = self.stakes.read().unwrap().history().clone(); + + let mut stake_delegation_accounts = self.stake_delegation_accounts(); + + let points: u128 = 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, Some(&stake_history)) + .unwrap_or(0) }) .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); - self.rewards = Some(validator_rewards.drain().collect()); - total_validator_rewards + self.rewards = Some(rewards.drain().collect()); + point_value.rewards as f64 / point_value.points as f64 } fn update_recent_blockhashes_locked(&self, locked_blockhash_queue: &BlockhashQueue) { @@ -986,21 +1041,6 @@ impl Bank { 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) { let collector_fees = self.collector_fees.load(Ordering::Relaxed) as u64; @@ -4730,7 +4770,18 @@ mod tests { } 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()... 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] fn test_purge_empty_accounts() { @@ -6364,23 +6498,6 @@ mod tests { 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] fn test_bank_get_program_accounts() { let (genesis_config, mint_keypair) = create_genesis_config(500); diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index cbad14915..09b14a0a1 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -282,7 +282,7 @@ mod test_bank_serialize { // These some what long test harness is required to freeze the ABI of // Bank's serialization due to versioned nature - #[frozen_abi(digest = "6MnT4MzuLHe4Uq96YaF3JF2gL1RvprzQHCaV9TaWgYLe")] + #[frozen_abi(digest = "FaZaic5p7bvdsKDxGJmaPVyp12AbAmURyYoGiUdx1Ksu")] #[derive(Serialize, AbiExample)] pub struct BankAbiTestWrapperFuture { #[serde(serialize_with = "wrapper_future")] @@ -305,7 +305,7 @@ mod test_bank_serialize { .serialize(s) } - #[frozen_abi(digest = "92KVEUQ8PwKe5DgZ6AyDQGAi9pvi7kfHdMBE4hcs5EQ4")] + #[frozen_abi(digest = "9g4bYykzsC86fULgu9iUh4kpvb1pxvAmipvyZPChLhws")] #[derive(Serialize, AbiExample)] pub struct BankAbiTestWrapperLegacy { #[serde(serialize_with = "wrapper_legacy")] diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 5553b9d81..8a731a3e7 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -15,9 +15,8 @@ pub struct Stakes { /// stake_delegations stake_delegations: HashMap, - /// unclaimed points. - // a point is a credit multiplied by the stake - points: u64, + /// unused + unused: u64, /// current epoch, used to calculate current stake epoch: Epoch, @@ -49,7 +48,7 @@ impl Stakes { Stakes { stake_delegations: self.stake_delegations.clone(), - points: self.points, + unused: self.unused, epoch, vote_accounts: self .vote_accounts @@ -88,6 +87,18 @@ impl Stakes { .sum() } + pub fn vote_balance_and_staked(&self) -> u64 { + self.stake_delegations + .iter() + .map(|(_, stake_delegation)| stake_delegation.stake) + .sum::() + + self + .vote_accounts + .iter() + .map(|(_pubkey, (_staked, vote_account))| vote_account.lamports) + .sum::() + } + pub fn is_stake(account: &Account) -> bool { solana_vote_program::check_id(&account.owner) || solana_stake_program::check_id(&account.owner) @@ -106,15 +117,6 @@ impl Stakes { |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())); } } else if solana_stake_program::check_id(&account.owner) { @@ -176,18 +178,6 @@ impl Stakes { .and_then(|(_k, (_stake, account))| VoteState::from(account)) .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)] @@ -195,9 +185,7 @@ pub mod tests { use super::*; use solana_sdk::{pubkey::Pubkey, rent::Rent}; use solana_stake_program::stake_state; - use solana_vote_program::vote_state::{ - self, VoteState, VoteStateVersions, MAX_LOCKOUT_HISTORY, - }; + use solana_vote_program::vote_state::{self, VoteState}; // set up some dummies for a staked node (( vote ) ( stake )) 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] fn test_stakes_basic() { for i in 0..4 { @@ -302,76 +322,6 @@ pub mod tests { 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] fn test_stakes_vote_account_disappear_reappear() { let mut stakes = Stakes::default(); @@ -528,4 +478,43 @@ pub mod tests { 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 + ); + } + } }