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 9886d085a69d695f48970f48f2f567e62ba05414.

* Make stake_delegation_accounts to return hashmap

Co-authored-by: Ryo Onodera <ryoqun@gmail.com>
This commit is contained in:
Rob Walker 2020-07-20 21:57:25 -07:00 committed by GitHub
parent b0d1c70718
commit 7cc2a6801b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 562 additions and 239 deletions

View File

@ -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
)
); );
} }

View File

@ -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]

View File

@ -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;
if let Some(rewards) = self.rewards.as_ref() {
assert_eq!(
validator_rewards_paid,
u64::try_from(rewards.iter().map(|(_pubkey, reward)| reward).sum::<i64>()).unwrap()
);
}
// verify that we didn't pay any more than we expected to
assert!(validator_rewards >= validator_rewards_paid);
self.capitalization 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 /// map stake delegations into resolved (pubkey, account) pairs
/// successfully load and parse, return total payout /// returns a map (has to be copied) of loaded
fn pay_validator_rewards(&mut self, point_value: f64) -> u64 { /// ( Vec<(staker info)> (voter account) ) keyed by voter pubkey
let stake_history = self.stakes.read().unwrap().history().clone(); ///
let mut validator_rewards = HashMap::new(); fn stake_delegation_accounts(&self) -> HashMap<Pubkey, (Vec<(Pubkey, Account)>, Account)> {
let mut accounts = HashMap::new();
let total_validator_rewards = self 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, .entry(delegation.voter_pubkey)
&mut vote_account, .or_insert((Vec::new(), vote_account));
point_value, entry.0.push((*stake_pubkey, stake_account));
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
}
} }
(_, _) => 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(); .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);

View File

@ -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")]

View File

@ -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
);
}
}
} }