use {
super::{
Bank, CachedVoteAccounts, CalculateValidatorRewardsResult, EpochRewardCalculateParamInfo,
FilteredStakeDelegations, PartitionedRewardsCalculation, PartitionedStakeReward,
PartitionedStakeRewards, REWARD_CALCULATION_NUM_BLOCKS, RewardCommissionAccounts,
RewardCommissionAccountsStorable, StakeRewardCalculation,
epoch_rewards_hasher::hash_rewards_into_partitions,
},
crate::{
bank::{
EpochInflationRewards, RewardCalcTracer, RewardCalculationEvent, RewardCommission,
RewardCommissions, RewardsMetrics, null_tracer,
},
inflation_rewards::{
points::{DelegatedVoteState, PointValue, calculate_points},
redeem_rewards,
},
stake_account::StakeAccount,
stake_utils,
stakes::Stakes,
},
agave_feature_set as feature_set,
log::{debug, info},
rayon::{
ThreadPool,
iter::{IndexedParallelIterator, ParallelIterator},
},
solana_clock::{Epoch, Slot},
solana_measure::{measure::Measure, measure_us},
solana_native_token::LAMPORTS_PER_SOL,
solana_pubkey::Pubkey,
solana_stake_interface::{stake_history::StakeHistory, state::Delegation},
solana_sysvar::epoch_rewards::EpochRewards,
std::sync::{Arc, atomic::Ordering::Relaxed},
};
#[derive(Debug)]
struct DelegationRewards {
stake_reward: PartitionedStakeReward,
commission_pubkey: Pubkey,
reward_commission: RewardCommission,
}
#[derive(Default)]
struct RewardsAccumulator {
reward_commissions: RewardCommissions,
num_stake_rewards: usize,
total_stake_rewards_lamports: u64,
}
impl RewardsAccumulator {
fn add_reward(
&mut self,
commission_pubkey: Pubkey,
reward_commission: RewardCommission,
stakers_reward: u64,
) {
self.reward_commissions
.entry(commission_pubkey)
.and_modify(|dst_reward_commission| {
dst_reward_commission.commission_lamports = dst_reward_commission
.commission_lamports
.saturating_add(reward_commission.commission_lamports)
})
.or_insert(reward_commission);
self.num_stake_rewards = self.num_stake_rewards.saturating_add(1);
self.total_stake_rewards_lamports = self
.total_stake_rewards_lamports
.saturating_add(stakers_reward);
}
fn accumulate_into_larger(self, rhs: Self) -> Self {
let (mut dst, src) = if self.reward_commissions.len() >= rhs.reward_commissions.len() {
(self, rhs)
} else {
(rhs, self)
};
for (commission_pubkey, reward_commission) in src.reward_commissions {
dst.reward_commissions
.entry(commission_pubkey)
.and_modify(|dst_reward_commission: &mut RewardCommission| {
dst_reward_commission.commission_lamports = dst_reward_commission
.commission_lamports
.saturating_add(reward_commission.commission_lamports)
})
.or_insert(reward_commission);
}
dst.num_stake_rewards = dst.num_stake_rewards.saturating_add(src.num_stake_rewards);
dst.total_stake_rewards_lamports = dst
.total_stake_rewards_lamports
.saturating_add(src.total_stake_rewards_lamports);
dst
}
}
impl Bank {
#[allow(clippy::too_many_arguments)]
pub(in crate::bank) fn begin_partitioned_rewards(
&mut self,
parent_epoch: Epoch,
parent_slot: Slot,
parent_block_height: u64,
rewards_calculation: &PartitionedRewardsCalculation,
rewards_metrics: &RewardsMetrics,
) {
self.distribute_reward_commissions(parent_epoch, rewards_calculation, rewards_metrics);
let slot = self.slot();
let distribution_starting_block_height =
self.block_height() + REWARD_CALCULATION_NUM_BLOCKS;
let PartitionedRewardsCalculation {
reward_commission_accounts,
stake_rewards,
point_value,
..
} = rewards_calculation;
let distributed_rewards = reward_commission_accounts.total_reward_commission_lamports;
let stake_rewards = Arc::clone(&stake_rewards.stake_rewards);
let num_partitions = self.get_reward_distribution_num_blocks(&stake_rewards);
self.set_epoch_reward_status_calculation(distribution_starting_block_height, stake_rewards);
self.create_epoch_rewards_sysvar(
distributed_rewards,
distribution_starting_block_height,
num_partitions,
point_value,
);
datapoint_info!(
"epoch-rewards-status-update",
("start_slot", slot, i64),
("calculation_block_height", self.block_height(), i64),
("active", 1, i64),
("parent_slot", parent_slot, i64),
("parent_block_height", parent_block_height, i64),
);
}
pub(in crate::bank) fn calculate_rewards(
&self,
stake_history: &StakeHistory,
stake_delegations: Vec<(&Pubkey, &StakeAccount<Delegation>)>,
cached_vote_accounts: CachedVoteAccounts<'_>,
prev_epoch: Epoch,
reward_calc_tracer: Option<impl Fn(&RewardCalculationEvent) + Send + Sync>,
thread_pool: &ThreadPool,
metrics: &mut RewardsMetrics,
) -> Arc<PartitionedRewardsCalculation> {
let mut epoch_rewards_calculation_cache =
self.epoch_rewards_calculation_cache.lock().unwrap();
let rewards_calculation = epoch_rewards_calculation_cache
.entry(self.parent_hash)
.or_insert_with(|| {
let stake_delegations = self.filter_stake_delegations(stake_delegations);
Arc::new(self.calculate_rewards_for_partitioning(
stake_history,
&stake_delegations,
cached_vote_accounts,
prev_epoch,
reward_calc_tracer,
thread_pool,
metrics,
))
})
.clone();
drop(epoch_rewards_calculation_cache);
rewards_calculation
}
pub(in crate::bank) fn distribute_reward_commissions(
&mut self,
prev_epoch: Epoch,
rewards_calculation: &PartitionedRewardsCalculation,
rewards_metrics: &RewardsMetrics,
) {
let PartitionedRewardsCalculation {
reward_commission_accounts,
stake_rewards,
validator_rate,
foundation_rate,
capitalization,
point_value,
..
} = rewards_calculation;
let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports;
self.store_commission_accounts_partitioned(reward_commission_accounts, rewards_metrics);
self.update_reward_commissions(reward_commission_accounts);
let StakeRewardCalculation {
total_stake_rewards_lamports,
..
} = stake_rewards;
assert!(point_value.rewards >= total_reward_commissions + total_stake_rewards_lamports);
info!(
"distributed reward commissions: {} out of {}, remaining {}",
total_reward_commissions, point_value.rewards, total_stake_rewards_lamports
);
let (num_stake_accounts, num_vote_accounts) = {
let stakes = self.stakes_cache.stakes();
let filtered_vote_accounts =
self.maybe_filter_vote_accounts_for_vat(stakes.vote_accounts());
(
stakes.stake_delegations().len(),
filtered_vote_accounts.len(),
)
};
self.capitalization
.fetch_add(total_reward_commissions, Relaxed);
let active_stake = if let Some(stake_history_entry) =
self.stakes_cache.stakes().history().get(prev_epoch)
{
stake_history_entry.effective
} else {
0
};
datapoint_info!(
"epoch_rewards",
("slot", self.slot, i64),
("epoch", prev_epoch, i64),
("validator_rate", *validator_rate, f64),
("foundation_rate", *foundation_rate, f64),
("validator_rewards", total_reward_commissions, i64),
("active_stake", active_stake, i64),
("pre_capitalization", *capitalization, i64),
("post_capitalization", self.capitalization(), i64),
("num_stake_accounts", num_stake_accounts, i64),
("num_vote_accounts", num_vote_accounts, i64),
);
}
fn store_commission_accounts_partitioned(
&self,
reward_commission_accounts: &RewardCommissionAccounts,
metrics: &RewardsMetrics,
) {
let (_, measure_us) = measure_us!({
let storable = RewardCommissionAccountsStorable {
slot: self.slot(),
reward_commission_accounts,
};
self.store_accounts(storable);
});
metrics
.store_commission_accounts_us
.fetch_add(measure_us, Relaxed);
}
pub(super) fn calculate_rewards_for_partitioning<'a>(
&self,
stake_history: &StakeHistory,
stake_delegations: &'a FilteredStakeDelegations<'a>,
cached_vote_accounts: CachedVoteAccounts<'_>,
rewarded_epoch: Epoch,
reward_calc_tracer: Option<impl Fn(&RewardCalculationEvent) + Send + Sync>,
thread_pool: &ThreadPool,
metrics: &mut RewardsMetrics,
) -> PartitionedRewardsCalculation {
let capitalization = self.capitalization();
let EpochInflationRewards {
validator_rewards_lamports,
validator_rate,
foundation_rate,
} = self.calculate_epoch_inflation_rewards(capitalization, rewarded_epoch);
let CalculateValidatorRewardsResult {
reward_commission_accounts,
stake_reward_calculation: stake_rewards,
point_value,
} = self
.calculate_validator_rewards(
stake_history,
stake_delegations,
cached_vote_accounts,
rewarded_epoch,
validator_rewards_lamports,
reward_calc_tracer,
thread_pool,
metrics,
)
.unwrap_or_default();
info!(
"calculated rewards for epoch: {}, parent_slot: {}, parent_hash: {}",
self.epoch, self.parent_slot, self.parent_hash
);
PartitionedRewardsCalculation {
reward_commission_accounts,
stake_rewards,
validator_rate,
foundation_rate,
capitalization,
point_value,
}
}
fn calculate_validator_rewards<'a>(
&self,
stake_history: &StakeHistory,
stake_delegations: &'a FilteredStakeDelegations<'a>,
cached_vote_accounts: CachedVoteAccounts<'_>,
rewarded_epoch: Epoch,
rewards: u64,
reward_calc_tracer: Option<impl RewardCalcTracer>,
thread_pool: &ThreadPool,
metrics: &mut RewardsMetrics,
) -> Option<CalculateValidatorRewardsResult> {
self.calculate_reward_points_partitioned(
stake_history,
stake_delegations,
&cached_vote_accounts,
rewards,
thread_pool,
metrics,
)
.map(|point_value| {
let (reward_commission_accounts, stake_reward_calculation) = self
.calculate_stake_rewards_and_commissions(
stake_history,
stake_delegations,
cached_vote_accounts,
rewarded_epoch,
point_value.clone(),
thread_pool,
reward_calc_tracer,
metrics,
);
CalculateValidatorRewardsResult {
reward_commission_accounts,
stake_reward_calculation,
point_value,
}
})
}
pub(in crate::bank) fn filter_stake_delegations<'a>(
&self,
stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount<Delegation>)>,
) -> FilteredStakeDelegations<'a> {
let min_stake_delegation = if self
.feature_set
.is_active(&feature_set::stake_minimum_delegation_for_rewards::id())
{
let min_stake_delegation = stake_utils::get_minimum_delegation(
self.feature_set
.is_active(&agave_feature_set::upgrade_bpf_stake_program_to_v5::id()),
)
.max(LAMPORTS_PER_SOL);
Some(min_stake_delegation)
} else {
None
};
FilteredStakeDelegations {
stake_delegations,
min_stake_delegation,
}
}
fn get_epoch_params_for_recalculation<'a>(
&'a self,
rewarded_epoch: Epoch,
stakes: &'a Stakes<StakeAccount<Delegation>>,
) -> EpochRewardCalculateParamInfo<'a> {
let stake_history = stakes.history().clone();
let stake_delegations = stakes.stake_delegations_vec();
let stake_delegations = self.filter_stake_delegations(stake_delegations);
let leader_schedule_epoch = self.epoch_schedule().get_leader_schedule_epoch(self.slot());
let distribution_epoch_vote_accounts = self
.epoch_stakes(leader_schedule_epoch)
.expect("calculation should always run after Bank::update_epoch_stakes()")
.stakes()
.vote_accounts();
let cached_vote_accounts =
self.get_cached_vote_accounts(rewarded_epoch, distribution_epoch_vote_accounts);
EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
}
}
#[allow(clippy::too_many_arguments)]
fn redeem_delegation_rewards(
&self,
rewarded_epoch: Epoch,
stake_pubkey: &Pubkey,
stake_account: &StakeAccount<Delegation>,
point_value: &PointValue,
stake_history: &StakeHistory,
cached_vote_accounts: &CachedVoteAccounts<'_>,
reward_calc_tracer: Option<impl RewardCalcTracer>,
new_rate_activation_epoch: Option<Epoch>,
delay_commission_updates: bool,
commission_rate_in_basis_points: bool,
) -> Option<DelegationRewards> {
let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| {
move |inner_event: &_| {
outer(&RewardCalculationEvent::Staking(stake_pubkey, inner_event))
}
});
let CachedVoteAccounts {
snapshot_epoch_vote_accounts,
rewarded_epoch_vote_accounts,
distribution_epoch_vote_accounts,
} = cached_vote_accounts;
let stake_pubkey = *stake_pubkey;
let vote_pubkey = stake_account.delegation().voter_pubkey;
let Some(vote_account) = distribution_epoch_vote_accounts.get(&vote_pubkey) else {
debug!("could not find vote account {vote_pubkey} in cache");
return None;
};
let vote_state = vote_account.vote_state_view();
let stake_state = stake_account.stake_state();
let commission_bps = if delay_commission_updates {
let vote_state_for_commission = snapshot_epoch_vote_accounts
.and_then(|eva| eva.get(&vote_pubkey))
.or_else(|| rewarded_epoch_vote_accounts.and_then(|eva| eva.get(&vote_pubkey)))
.map(|vote_account| vote_account.vote_state_view())
.unwrap_or(vote_state);
if commission_rate_in_basis_points {
vote_state_for_commission.inflation_rewards_commission()
} else {
vote_state_for_commission.commission() as u16 * 100
}
} else if commission_rate_in_basis_points {
vote_state.inflation_rewards_commission()
} else {
vote_state.commission() as u16 * 100
};
match redeem_rewards(
rewarded_epoch,
stake_state,
commission_bps,
DelegatedVoteState::from(vote_state),
point_value,
stake_history,
reward_calc_tracer,
new_rate_activation_epoch,
commission_rate_in_basis_points,
) {
Ok((stake_reward, commission_lamports, stake)) => {
let stake_reward = PartitionedStakeReward {
stake_pubkey,
stake,
stake_reward,
commission_bps,
};
let vote_account = vote_account.into();
let reward_commission = RewardCommission {
commission_bps,
commission_account: vote_account,
commission_lamports,
};
Some(DelegationRewards {
stake_reward,
commission_pubkey: vote_pubkey,
reward_commission,
})
}
Err(e) => {
debug!("redeem_rewards() failed for {stake_pubkey}: {e:?}");
None
}
}
}
fn calculate_stake_rewards_and_commissions<'a>(
&self,
stake_history: &StakeHistory,
stake_delegations: &'a FilteredStakeDelegations<'a>,
cached_vote_accounts: CachedVoteAccounts<'_>,
rewarded_epoch: Epoch,
point_value: PointValue,
thread_pool: &ThreadPool,
reward_calc_tracer: Option<impl RewardCalcTracer>,
metrics: &mut RewardsMetrics,
) -> (RewardCommissionAccounts, StakeRewardCalculation) {
let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch();
let delay_commission_updates = self
.feature_set
.is_active(&agave_feature_set::delay_commission_updates::id());
let commission_rate_in_basis_points = self
.feature_set
.is_active(&feature_set::commission_rate_in_basis_points::id());
let mut measure_redeem_rewards = Measure::start("redeem-rewards");
let mut stake_rewards = PartitionedStakeRewards::with_capacity(stake_delegations.len());
let rewards_accumulator: RewardsAccumulator = thread_pool.install(|| {
stake_delegations
.par_iter()
.zip_eq(stake_rewards.spare_capacity_mut())
.with_min_len(500)
.filter_map(|(maybe_stake_delegation, stake_reward_ref)| {
let maybe_reward_record =
maybe_stake_delegation.and_then(|(stake_pubkey, stake_account)| {
self.redeem_delegation_rewards(
rewarded_epoch,
stake_pubkey,
stake_account,
&point_value,
stake_history,
&cached_vote_accounts,
reward_calc_tracer.as_ref(),
new_warmup_cooldown_rate_epoch,
delay_commission_updates,
commission_rate_in_basis_points,
)
});
let (stake_reward, maybe_reward_record) = match maybe_reward_record {
Some(res) => {
let DelegationRewards {
stake_reward,
commission_pubkey,
reward_commission,
} = res;
let stakers_reward = stake_reward.stake_reward;
(
Some(stake_reward),
Some((stakers_reward, commission_pubkey, reward_commission)),
)
}
None => (None, None),
};
stake_reward_ref.write(stake_reward);
maybe_reward_record
})
.fold(
RewardsAccumulator::default,
|mut rewards_accumulator,
(stakers_reward, commission_pubkey, reward_commission)| {
rewards_accumulator.add_reward(
commission_pubkey,
reward_commission,
stakers_reward,
);
rewards_accumulator
},
)
.reduce(
RewardsAccumulator::default,
|rewards_accumulator_a, rewards_accumulator_b| {
rewards_accumulator_a.accumulate_into_larger(rewards_accumulator_b)
},
)
});
let RewardsAccumulator {
reward_commissions,
num_stake_rewards,
total_stake_rewards_lamports,
} = rewards_accumulator;
unsafe {
stake_rewards.assume_init(num_stake_rewards);
}
let reward_commission_accounts = Self::calculate_commission_accounts(reward_commissions);
measure_redeem_rewards.stop();
metrics.redeem_rewards_us = measure_redeem_rewards.as_us();
(
reward_commission_accounts,
StakeRewardCalculation {
stake_rewards: Arc::new(stake_rewards),
total_stake_rewards_lamports,
},
)
}
fn calculate_reward_points_partitioned<'a>(
&self,
stake_history: &StakeHistory,
stake_delegations: &'a FilteredStakeDelegations<'a>,
cached_vote_accounts: &CachedVoteAccounts<'_>,
rewards: u64,
thread_pool: &ThreadPool,
metrics: &RewardsMetrics,
) -> Option<PointValue> {
let CachedVoteAccounts {
distribution_epoch_vote_accounts,
..
} = cached_vote_accounts;
let solana_vote_program: Pubkey = solana_vote_program::id();
let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch();
let (points, measure_us) = measure_us!(thread_pool.install(|| {
stake_delegations
.par_iter()
.filter_map(|stake_delegation| stake_delegation)
.map(|(_stake_pubkey, stake_account)| {
let vote_pubkey = stake_account.delegation().voter_pubkey;
let Some(vote_account) = distribution_epoch_vote_accounts.get(&vote_pubkey)
else {
return 0;
};
if vote_account.owner() != &solana_vote_program {
return 0;
}
calculate_points(
stake_account.stake_state(),
DelegatedVoteState::from(vote_account.vote_state_view()),
stake_history,
new_warmup_cooldown_rate_epoch,
)
.unwrap_or(0)
})
.sum::<u128>()
}));
metrics.calculate_points_us.fetch_add(measure_us, Relaxed);
(points > 0).then_some(PointValue { rewards, points })
}
pub(in crate::bank) fn recalculate_partitioned_rewards_if_active<F, TP>(
&mut self,
thread_pool_builder: F,
) where
F: FnOnce() -> TP,
TP: std::borrow::Borrow<ThreadPool>,
{
let epoch_rewards_sysvar = self.get_epoch_rewards_sysvar();
if epoch_rewards_sysvar.active {
let thread_pool = thread_pool_builder();
let (_, stake_rewards, partition_indices) =
self.recalculate_stake_rewards(&epoch_rewards_sysvar, thread_pool.borrow());
self.set_epoch_reward_status_distribution(
epoch_rewards_sysvar.distribution_starting_block_height,
stake_rewards,
partition_indices,
);
}
}
fn recalculate_stake_rewards(
&self,
epoch_rewards_sysvar: &EpochRewards,
thread_pool: &ThreadPool,
) -> (
RewardCommissionAccounts,
Arc<PartitionedStakeRewards>,
Vec<Vec<usize>>,
) {
assert!(epoch_rewards_sysvar.active);
let rewarded_epoch = self.epoch().saturating_sub(1);
let point_value = PointValue {
rewards: epoch_rewards_sysvar.total_rewards,
points: epoch_rewards_sysvar.total_points,
};
let stakes = self.stakes_cache.stakes();
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = self.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let (reward_commission_accounts, StakeRewardCalculation { stake_rewards, .. }) = self
.calculate_stake_rewards_and_commissions(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
point_value,
thread_pool,
null_tracer(),
&mut RewardsMetrics::default(), );
drop(stakes);
let partition_indices = hash_rewards_into_partitions(
&stake_rewards,
&epoch_rewards_sysvar.parent_blockhash,
epoch_rewards_sysvar.num_partitions as usize,
);
(reward_commission_accounts, stake_rewards, partition_indices)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
bank::{
RewardCommission, RewardInfo, null_tracer,
partitioned_epoch_rewards::{
EpochRewardPhase, EpochRewardStatus, PartitionedStakeRewards,
StartBlockHeightAndPartitionedRewards,
tests::{
RewardBank, SLOTS_PER_EPOCH, build_partitioned_stake_rewards,
create_default_reward_bank, create_reward_bank,
create_reward_bank_with_specific_stakes, populate_vote_accounts_with_votes,
},
},
tests::create_genesis_config,
},
genesis_utils::{self, GenesisConfigInfo, deactivate_features},
stake_account::StakeAccount,
stake_utils,
stakes::{Stakes, tests::create_staked_node_accounts},
},
agave_feature_set::{FeatureSet, delay_commission_updates},
agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED,
rayon::ThreadPoolBuilder,
solana_account::{
AccountSharedData, ReadableAccount, accounts_equal, state_traits::StateMut,
},
solana_accounts_db::partitioned_rewards::PartitionedEpochRewardsConfig,
solana_bls_signatures::keypair::Keypair as BLSKeypair,
solana_clock::Clock,
solana_epoch_schedule::EpochSchedule,
solana_keypair::Keypair,
solana_native_token::LAMPORTS_PER_SOL,
solana_reward_info::RewardType,
solana_signer::Signer,
solana_stake_interface::{
stake_flags::StakeFlags,
state::{Authorized, Delegation, Meta, Stake, StakeStateV2},
},
solana_vote_interface::state::{
BLS_PUBLIC_KEY_COMPRESSED_SIZE, VoteInitV2, VoteStateV4, VoteStateVersions,
},
solana_vote_program::vote_state::{self, create_bls_proof_of_possession},
std::{
collections::HashSet,
sync::{Arc, RwLockReadGuard},
},
test_case::test_case,
};
#[test]
fn test_store_commission_accounts_partitioned() {
let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
let bank = Bank::new_for_tests(&genesis_config);
let num_reward_commissions = 100;
let reward_commissions = (0..num_reward_commissions)
.map(|_| (Pubkey::new_unique(), RewardCommission::new_random()))
.collect::<Vec<_>>();
let mut reward_commission_accounts = RewardCommissionAccounts::default();
reward_commissions
.iter()
.for_each(|(commission_pubkey, reward_commission)| {
let info = RewardInfo {
reward_type: RewardType::Voting,
lamports: reward_commission.commission_lamports as i64,
post_balance: reward_commission.commission_lamports,
commission_bps: Some(reward_commission.commission_bps),
};
reward_commission_accounts.accounts_with_rewards.push((
*commission_pubkey,
info,
reward_commission.commission_account.clone(),
));
reward_commission_accounts.total_reward_commission_lamports +=
reward_commission.commission_lamports;
});
let metrics = RewardsMetrics::default();
let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports;
bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics);
assert_eq!(
num_reward_commissions,
reward_commission_accounts.accounts_with_rewards.len()
);
assert_eq!(
reward_commissions
.iter()
.map(|(_, reward_commission)| reward_commission.commission_lamports)
.sum::<u64>(),
total_reward_commissions
);
reward_commissions
.iter()
.for_each(|(commission_pubkey, reward_commission)| {
let loaded_account = bank
.load_slow_with_fixed_root(&bank.ancestors, commission_pubkey)
.unwrap();
assert!(accounts_equal(
&loaded_account.0,
&reward_commission.commission_account
));
});
}
#[test]
fn test_store_commission_accounts_partitioned_empty() {
let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
let bank = Bank::new_for_tests(&genesis_config);
let expected = 0;
let reward_commission_accounts = RewardCommissionAccounts::default();
let metrics = RewardsMetrics::default();
let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports;
bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics);
assert_eq!(
expected,
reward_commission_accounts.accounts_with_rewards.len()
);
assert_eq!(0, total_reward_commissions);
}
#[test]
fn test_rewards_computation() {
agave_logger::setup();
let delegations_with_rewards = 100;
let delegations_without_rewards = 10;
let stakes = (0..delegations_with_rewards)
.map(|_| 2_000_000_000)
.chain((0..delegations_without_rewards).map(|_| 500_000_000))
.collect::<Vec<_>>();
let bank = create_reward_bank_with_specific_stakes(
stakes,
PartitionedEpochRewardsConfig::default().stake_account_stores_per_block,
SLOTS_PER_EPOCH,
)
.0
.bank;
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let mut rewards_metrics = RewardsMetrics::default();
let expected_rewards = 100_000_000_000;
let stakes = bank.stakes_cache.stakes();
let rewarded_epoch = 1;
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let calculated_rewards = bank.calculate_validator_rewards(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
expected_rewards,
null_tracer(),
&thread_pool,
&mut rewards_metrics,
);
let reward_commission_accounts = &calculated_rewards
.as_ref()
.unwrap()
.reward_commission_accounts;
let stake_rewards = &calculated_rewards
.as_ref()
.unwrap()
.stake_reward_calculation;
let total_reward_commissions: u64 = reward_commission_accounts
.accounts_with_rewards
.iter()
.map(|(_, reward_info, _)| reward_info.lamports)
.sum::<i64>() as u64;
assert_eq!(
stake_rewards.total_stake_rewards_lamports + total_reward_commissions,
expected_rewards
);
assert_eq!(
stake_rewards.stake_rewards.num_rewards(),
delegations_with_rewards
);
}
#[test]
fn test_rewards_point_calculation() {
agave_logger::setup();
let expected_num_delegations = 100;
let RewardBank { bank, .. } =
create_default_reward_bank(expected_num_delegations, SLOTS_PER_EPOCH).0;
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let rewards_metrics = RewardsMetrics::default();
let expected_rewards = 100_000_000_000;
let stakes: RwLockReadGuard<Stakes<StakeAccount<Delegation>>> = bank.stakes_cache.stakes();
let rewarded_epoch = bank.epoch().saturating_sub(1);
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let point_value = bank.calculate_reward_points_partitioned(
&stake_history,
&stake_delegations,
&cached_vote_accounts,
expected_rewards,
&thread_pool,
&rewards_metrics,
);
assert!(point_value.is_some());
assert_eq!(point_value.as_ref().unwrap().rewards, expected_rewards);
assert_eq!(point_value.as_ref().unwrap().points, 8400000000000);
}
#[test]
fn test_rewards_point_calculation_empty() {
agave_logger::setup();
let (genesis_config, _mint_keypair) = create_genesis_config(LAMPORTS_PER_SOL);
let bank = Bank::new_for_tests(&genesis_config);
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let rewards_metrics: RewardsMetrics = RewardsMetrics::default();
let expected_rewards = 100_000_000_000;
let stakes: RwLockReadGuard<Stakes<StakeAccount<Delegation>>> = bank.stakes_cache.stakes();
let rewarded_epoch = bank.epoch().saturating_sub(1);
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let point_value = bank.calculate_reward_points_partitioned(
&stake_history,
&stake_delegations,
&cached_vote_accounts,
expected_rewards,
&thread_pool,
&rewards_metrics,
);
assert!(point_value.is_none());
}
struct EpochOperations {
epoch: Epoch,
vote_operations: Vec<(Pubkey, VoteOperations)>,
}
#[derive(Default)]
struct VoteOperations {
expected_reward_commission: Option<u8>,
create_with_balance: Option<u64>,
delegate_stake_amount: Option<u64>,
new_commission: Option<u8>,
earned_credits: Option<u64>,
}
fn recalculate_reward_commission_for_tests(
bank: &Bank,
commission_pubkey: &Pubkey,
) -> Option<RewardInfo> {
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let (reward_commissions, ..) =
bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool);
reward_commissions
.accounts_with_rewards
.iter()
.find(|(reward_address, ..)| reward_address == commission_pubkey)
.map(|(_, reward, _)| *reward)
}
fn apply_epoch_operations(mut bank: Bank, op: EpochOperations) -> Bank {
assert_eq!(bank.epoch(), op.epoch);
for (vote_address, vote_op) in &op.vote_operations {
if let Some(balance) = &vote_op.create_with_balance {
let identity = Keypair::new();
let bls_keypair =
BLSKeypair::derive_from_signer(&identity, BLS_KEYPAIR_DERIVE_SEED).unwrap();
let (bls_pubkey, bls_pop) =
create_bls_proof_of_possession(vote_address, &bls_keypair);
let vote_init = VoteInitV2 {
node_pubkey: identity.pubkey(),
authorized_voter: identity.pubkey(),
authorized_voter_bls_pubkey: bls_pubkey,
authorized_voter_bls_proof_of_possession: bls_pop,
..VoteInitV2::default()
};
let vote_state = VoteStateV4::new(&vote_init, &Clock::default());
let mut account = solana_account::AccountSharedData::new(
*balance,
VoteStateV4::size_of(),
&solana_vote_program::id(),
);
account
.serialize_data(&VoteStateVersions::new_v4(vote_state))
.unwrap();
bank.store_account(vote_address, &account);
}
if let Some(stake_amount) = &vote_op.delegate_stake_amount {
let size = StakeStateV2::size_of();
let rent_exempt_reserve = bank.rent_collector().rent.minimum_balance(size);
let lamports = rent_exempt_reserve + stake_amount;
let mut stake_account =
AccountSharedData::new(lamports, size, &solana_sdk_ids::stake::id());
let meta = Meta {
authorized: Authorized::auto(&Pubkey::new_unique()),
rent_exempt_reserve,
..Meta::default()
};
let stake = Stake {
delegation: Delegation::new(vote_address, *stake_amount, bank.epoch()),
credits_observed: 0,
};
stake_account
.set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty()))
.expect("set_state");
bank.store_account(&Pubkey::new_unique(), &stake_account);
}
let modify_vote_state = |modify_fn: &dyn Fn(&mut VoteStateV4)| {
let mut vote_account = bank.get_account(vote_address).unwrap();
let vote_state_versions = vote_account
.deserialize_data::<VoteStateVersions>()
.unwrap();
let VoteStateVersions::V4(mut vote_state) = vote_state_versions else {
panic!("unexpected version");
};
modify_fn(&mut vote_state);
vote_account
.serialize_data(&VoteStateVersions::V4(vote_state))
.unwrap();
bank.store_account(vote_address, &vote_account);
};
if let Some(commission) = vote_op.new_commission {
modify_vote_state(&|vote_state: &mut VoteStateV4| {
vote_state.inflation_rewards_commission_bps = commission as u16 * 100;
});
}
if let Some(earned_credits) = vote_op.earned_credits {
modify_vote_state(&|vote_state: &mut VoteStateV4| {
let last_credits = vote_state
.epoch_credits
.last()
.map(|(_epoch, credits, _)| *credits)
.unwrap_or(0);
vote_state.epoch_credits.push((
bank.epoch,
last_credits + earned_credits,
last_credits,
));
});
}
}
let slot = bank.slot + SLOTS_PER_EPOCH;
let prev_bank = Arc::new(bank);
bank = Bank::new_from_parent(prev_bank.clone(), &Pubkey::new_unique(), slot);
for (vote_address, vote_op) in &op.vote_operations {
let expected_commission = vote_op.expected_reward_commission;
let recalculated_vote_reward =
recalculate_reward_commission_for_tests(&bank, vote_address);
let vote_reward = bank
.rewards
.read()
.unwrap()
.iter()
.find(|(address, _reward)| address == vote_address)
.map(|(_address, reward)| *reward);
let prev_vote_balance = prev_bank
.get_account(vote_address)
.unwrap_or_default()
.lamports();
let vote_balance = bank.get_balance(vote_address);
if let Some(expected_commission) = &expected_commission {
let reward_lamports = vote_balance - prev_vote_balance;
let expected_vote_reward = RewardInfo {
reward_type: RewardType::Voting,
lamports: reward_lamports as i64,
post_balance: vote_balance,
commission_bps: Some(*expected_commission as u16 * 100),
};
assert_eq!(
vote_reward,
Some(expected_vote_reward),
"epoch {}: unexpected reward info",
op.epoch
);
assert_eq!(
recalculated_vote_reward,
Some(expected_vote_reward),
"epoch {}: unexpected recalculated reward info",
op.epoch
);
} else {
assert!(
vote_reward.map(|reward| reward.lamports).unwrap_or(0) == 0,
"epoch {}: expected no reward",
op.epoch
);
assert!(
recalculated_vote_reward
.map(|reward| reward.lamports)
.unwrap_or(0)
== 0,
"epoch {}: expected no recalculated reward",
op.epoch
);
}
}
bank
}
#[test_case(true; "delay_commission_updates")]
#[test_case(false; "instant_commission_updates")]
fn test_calculate_stake_vote_rewards_new_vote_account(delay_commission_updates: bool) {
let GenesisConfigInfo {
mut genesis_config, ..
} = genesis_utils::create_genesis_config_with_leader(
1_000_000 * LAMPORTS_PER_SOL,
&Pubkey::new_unique(),
42 * LAMPORTS_PER_SOL,
);
genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH);
if !delay_commission_updates {
deactivate_features(&mut genesis_config, &vec![delay_commission_updates::id()]);
}
let mut bank = Bank::new_for_tests(&genesis_config);
let vote_address = Pubkey::new_unique();
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 0,
vote_operations: vec![(
vote_address,
VoteOperations {
create_with_balance: Some(LAMPORTS_PER_SOL),
new_commission: Some(1),
earned_credits: Some(1000),
delegate_stake_amount: Some(LAMPORTS_PER_SOL),
..VoteOperations::default()
},
)],
},
);
bank = {
let expected_commission = if delay_commission_updates { 1 } else { 2 };
apply_epoch_operations(
bank,
EpochOperations {
epoch: 1,
vote_operations: vec![(
vote_address,
VoteOperations {
new_commission: Some(2),
earned_credits: Some(1000),
expected_reward_commission: Some(expected_commission),
..VoteOperations::default()
},
)],
},
)
};
let expected_commission = if delay_commission_updates { 1 } else { 3 };
apply_epoch_operations(
bank,
EpochOperations {
epoch: 2,
vote_operations: vec![(
vote_address,
VoteOperations {
new_commission: Some(3),
earned_credits: Some(1000),
expected_reward_commission: Some(expected_commission),
..VoteOperations::default()
},
)],
},
);
}
#[test]
fn test_calculate_stake_vote_rewards_prestaked_vote_account() {
let GenesisConfigInfo {
mut genesis_config, ..
} = genesis_utils::create_genesis_config_with_leader(
1_000_000 * LAMPORTS_PER_SOL,
&Pubkey::new_unique(),
42 * LAMPORTS_PER_SOL,
);
genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH);
let mut bank = Bank::new_for_tests(&genesis_config);
assert!(bank.feature_set.is_active(&delay_commission_updates::id()));
let vote_address = Pubkey::new_unique();
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 0,
vote_operations: vec![(
vote_address,
VoteOperations {
delegate_stake_amount: Some(LAMPORTS_PER_SOL),
..VoteOperations::default()
},
)],
},
);
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 1,
vote_operations: vec![(
vote_address,
VoteOperations {
create_with_balance: Some(LAMPORTS_PER_SOL),
new_commission: Some(1),
earned_credits: Some(1000),
expected_reward_commission: Some(1),
..VoteOperations::default()
},
)],
},
);
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 2,
vote_operations: vec![(
vote_address,
VoteOperations {
new_commission: Some(2),
earned_credits: Some(1000),
expected_reward_commission: Some(1),
..VoteOperations::default()
},
)],
},
);
apply_epoch_operations(
bank,
EpochOperations {
epoch: 3,
vote_operations: vec![(
vote_address,
VoteOperations {
new_commission: Some(3),
earned_credits: Some(1000),
expected_reward_commission: Some(1),
..VoteOperations::default()
},
)],
},
);
}
#[test]
fn test_calculate_stake_vote_rewards_genesis_vote_account() {
let GenesisConfigInfo {
mut genesis_config,
voting_keypair,
..
} = genesis_utils::create_genesis_config_with_leader(
1_000_000 * LAMPORTS_PER_SOL,
&Pubkey::new_unique(),
42 * LAMPORTS_PER_SOL,
);
genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH);
let mut bank = Bank::new_for_tests(&genesis_config);
assert!(bank.feature_set.is_active(&delay_commission_updates::id()));
let genesis_vote_address = voting_keypair.pubkey();
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 0,
vote_operations: vec![(
genesis_vote_address,
VoteOperations {
new_commission: Some(1),
earned_credits: Some(1000),
expected_reward_commission: Some(0),
..VoteOperations::default()
},
)],
},
);
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 1,
vote_operations: vec![(
genesis_vote_address,
VoteOperations {
new_commission: Some(2),
earned_credits: Some(1000),
expected_reward_commission: Some(0),
..VoteOperations::default()
},
)],
},
);
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 2,
vote_operations: vec![(
genesis_vote_address,
VoteOperations {
earned_credits: Some(1000),
expected_reward_commission: Some(1),
..VoteOperations::default()
},
)],
},
);
bank = apply_epoch_operations(
bank,
EpochOperations {
epoch: 3,
vote_operations: vec![(
genesis_vote_address,
VoteOperations {
earned_credits: Some(1000),
expected_reward_commission: Some(2),
..VoteOperations::default()
},
)],
},
);
apply_epoch_operations(
bank,
EpochOperations {
epoch: 4,
vote_operations: vec![(
genesis_vote_address,
VoteOperations {
earned_credits: Some(1000),
expected_reward_commission: Some(2),
..VoteOperations::default()
},
)],
},
);
}
#[test]
fn test_calculate_stake_vote_rewards() {
agave_logger::setup();
let expected_num_delegations = 1;
let RewardBank {
bank,
voters,
stakers,
} = create_default_reward_bank(expected_num_delegations, SLOTS_PER_EPOCH).0;
let vote_pubkey = voters.first().unwrap();
let stake_pubkey = *stakers.first().unwrap();
let stake_account = bank
.load_slow_with_fixed_root(&bank.ancestors, &stake_pubkey)
.unwrap()
.0;
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let mut rewards_metrics = RewardsMetrics::default();
let point_value = PointValue {
rewards: 100000, points: 1000, };
let tracer = |_event: &RewardCalculationEvent| {};
let reward_calc_tracer = Some(tracer);
let rewarded_epoch = bank.epoch();
let stakes: RwLockReadGuard<Stakes<StakeAccount<Delegation>>> = bank.stakes_cache.stakes();
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let (vote_rewards_accounts, stake_reward_calculation) = bank
.calculate_stake_rewards_and_commissions(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
point_value,
&thread_pool,
reward_calc_tracer,
&mut rewards_metrics,
);
drop(stakes);
let vote_account = bank
.load_slow_with_fixed_root(&bank.ancestors, vote_pubkey)
.unwrap()
.0;
let vote_state = VoteStateV4::deserialize(vote_account.data(), vote_pubkey).unwrap();
assert_eq!(vote_rewards_accounts.accounts_with_rewards.len(), 1);
let (vote_pubkey_from_result, rewards, account) =
&vote_rewards_accounts.accounts_with_rewards[0];
let vote_rewards = 0;
let commission_bps = vote_state.inflation_rewards_commission_bps;
assert_eq!(account.lamports(), vote_account.lamports());
assert!(accounts_equal(account, &vote_account));
assert_eq!(
*rewards,
RewardInfo {
reward_type: RewardType::Voting,
lamports: vote_rewards as i64,
post_balance: vote_account.lamports(),
commission_bps: Some(commission_bps),
}
);
assert_eq!(vote_pubkey_from_result, vote_pubkey);
assert_eq!(stake_reward_calculation.stake_rewards.num_rewards(), 1);
let expected_reward = {
let stake_reward = 8_400_000_000_000;
let stake_state: StakeStateV2 = stake_account.state().unwrap();
let mut stake = stake_state.stake().unwrap();
stake.credits_observed = vote_state.credits();
stake.delegation.stake += stake_reward;
PartitionedStakeReward {
stake,
stake_pubkey,
stake_reward,
commission_bps,
}
};
assert_eq!(
stake_reward_calculation
.stake_rewards
.get(0)
.unwrap()
.as_ref()
.unwrap(),
&expected_reward
);
}
fn compare_stake_rewards(
expected_stake_rewards: &[PartitionedStakeRewards],
received_stake_rewards: &[PartitionedStakeRewards],
) {
for (i, partition) in received_stake_rewards.iter().enumerate() {
let expected_partition = &expected_stake_rewards[i];
assert_eq!(partition, expected_partition);
}
}
#[test]
fn test_recalculate_stake_rewards() {
let expected_num_delegations = 4;
let num_rewards_per_block = 2;
let (RewardBank { bank, .. }, _) = create_reward_bank(
expected_num_delegations,
num_rewards_per_block,
SLOTS_PER_EPOCH,
);
let rewarded_epoch = bank.epoch();
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let mut rewards_metrics = RewardsMetrics::default();
let stakes = bank.stakes_cache.stakes();
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let PartitionedRewardsCalculation {
stake_rewards:
StakeRewardCalculation {
stake_rewards: expected_stake_rewards,
..
},
..
} = bank.calculate_rewards_for_partitioning(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
null_tracer(),
&thread_pool,
&mut rewards_metrics,
);
drop(stakes);
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let (_, recalculated_rewards, recalculated_partition_indices) =
bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool);
let recalculated_rewards =
build_partitioned_stake_rewards(&recalculated_rewards, &recalculated_partition_indices);
let expected_partition_indices = hash_rewards_into_partitions(
&expected_stake_rewards,
&epoch_rewards_sysvar.parent_blockhash,
epoch_rewards_sysvar.num_partitions as usize,
);
let expected_stake_rewards_partitioned =
build_partitioned_stake_rewards(&expected_stake_rewards, &expected_partition_indices);
assert_eq!(
expected_stake_rewards_partitioned.len(),
recalculated_rewards.len()
);
compare_stake_rewards(&expected_stake_rewards_partitioned, &recalculated_rewards);
let new_slot = bank.slot() + 2;
let bank = Arc::new(Bank::new_from_parent(bank, &Pubkey::default(), new_slot));
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let (_, recalculated_rewards, recalculated_partition_indices) =
bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool);
let recalculated_rewards =
build_partitioned_stake_rewards(&recalculated_rewards, &recalculated_partition_indices);
assert_eq!(
expected_stake_rewards_partitioned.len(),
recalculated_rewards.len()
);
assert_eq!(recalculated_rewards[0].num_rewards(), 0);
let starting_index = (bank.block_height() + 1
- epoch_rewards_sysvar.distribution_starting_block_height)
as usize;
compare_stake_rewards(
&expected_stake_rewards_partitioned[starting_index..],
&recalculated_rewards[starting_index..],
);
let new_slot = bank.slot() + 1;
let bank = Arc::new(Bank::new_from_parent(bank, &Pubkey::default(), new_slot));
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
assert!(!epoch_rewards_sysvar.active);
}
#[test]
#[should_panic]
fn test_recalculate_stake_rewards_distribution_complete() {
let expected_num_delegations = 2;
let num_rewards_per_block = 2;
let (RewardBank { bank, .. }, _) = create_reward_bank(
expected_num_delegations,
num_rewards_per_block,
SLOTS_PER_EPOCH,
);
let rewarded_epoch = bank.epoch();
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let mut rewards_metrics = RewardsMetrics::default();
let stakes = bank.stakes_cache.stakes();
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let PartitionedRewardsCalculation {
stake_rewards:
StakeRewardCalculation {
stake_rewards: expected_stake_rewards,
..
},
..
} = bank.calculate_rewards_for_partitioning(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
null_tracer(),
&thread_pool,
&mut rewards_metrics,
);
drop(stakes);
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let expected_partition_indices = hash_rewards_into_partitions(
&expected_stake_rewards,
&epoch_rewards_sysvar.parent_blockhash,
epoch_rewards_sysvar.num_partitions as usize,
);
let expected_stake_rewards =
build_partitioned_stake_rewards(&expected_stake_rewards, &expected_partition_indices);
let (_, recalculated_rewards, recalculated_partition_indices) =
bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool);
let recalculated_rewards =
build_partitioned_stake_rewards(&recalculated_rewards, &recalculated_partition_indices);
assert_eq!(expected_stake_rewards.len(), recalculated_rewards.len());
compare_stake_rewards(&expected_stake_rewards, &recalculated_rewards);
let new_slot = bank.slot() + 1;
let bank = Arc::new(Bank::new_from_parent(bank, &Pubkey::default(), new_slot));
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
assert!(!epoch_rewards_sysvar.active);
let _recalculated_rewards =
bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool);
}
#[test]
fn test_recalculate_partitioned_rewards() {
let expected_num_delegations = 3;
let num_rewards_per_block = 2;
let mut stakes = vec![2_000_000_000; expected_num_delegations];
stakes.push(40_000_000_000);
let (RewardBank { bank, .. }, _) = create_reward_bank_with_specific_stakes(
stakes,
num_rewards_per_block,
SLOTS_PER_EPOCH - 1,
);
let rewarded_epoch = bank.epoch();
let new_slot = bank.slot() + 1;
let mut bank = Bank::new_from_parent(bank, &Pubkey::default(), new_slot);
let expected_starting_block_height = bank.block_height() + 1;
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let mut rewards_metrics = RewardsMetrics::default();
let stakes = bank.stakes_cache.stakes();
let EpochRewardCalculateParamInfo {
stake_history,
stake_delegations,
cached_vote_accounts,
} = bank.get_epoch_params_for_recalculation(rewarded_epoch, &stakes);
let PartitionedRewardsCalculation {
stake_rewards:
StakeRewardCalculation {
stake_rewards: expected_stake_rewards,
..
},
point_value,
..
} = bank.calculate_rewards_for_partitioning(
&stake_history,
&stake_delegations,
cached_vote_accounts,
rewarded_epoch,
null_tracer(),
&thread_pool,
&mut rewards_metrics,
);
drop(stakes);
bank.recalculate_partitioned_rewards_if_active(|| &thread_pool);
let EpochRewardStatus::Active(EpochRewardPhase::Distribution(
StartBlockHeightAndPartitionedRewards {
distribution_starting_block_height,
all_stake_rewards: ref recalculated_rewards,
ref partition_indices,
},
)) = bank.epoch_reward_status
else {
panic!("{:?} not active", bank.epoch_reward_status);
};
assert_eq!(
expected_starting_block_height,
distribution_starting_block_height
);
let recalculated_rewards =
build_partitioned_stake_rewards(recalculated_rewards, partition_indices);
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let expected_partition_indices = hash_rewards_into_partitions(
&expected_stake_rewards,
&epoch_rewards_sysvar.parent_blockhash,
epoch_rewards_sysvar.num_partitions as usize,
);
let expected_stake_rewards =
build_partitioned_stake_rewards(&expected_stake_rewards, &expected_partition_indices);
assert_eq!(expected_stake_rewards.len(), recalculated_rewards.len());
compare_stake_rewards(&expected_stake_rewards, &recalculated_rewards);
let sysvar = bank.get_epoch_rewards_sysvar();
assert_eq!(point_value.rewards, sysvar.total_rewards);
let mut bank =
Bank::new_from_parent(Arc::new(bank), &Pubkey::default(), SLOTS_PER_EPOCH + 1);
bank.recalculate_partitioned_rewards_if_active(|| &thread_pool);
let EpochRewardStatus::Active(EpochRewardPhase::Distribution(
StartBlockHeightAndPartitionedRewards {
distribution_starting_block_height,
all_stake_rewards: ref recalculated_rewards,
ref partition_indices,
},
)) = bank.epoch_reward_status
else {
panic!("{:?} not active", bank.epoch_reward_status);
};
let recalculated_rewards =
build_partitioned_stake_rewards(recalculated_rewards, partition_indices);
assert_eq!(
expected_starting_block_height,
distribution_starting_block_height
);
assert_eq!(expected_stake_rewards.len(), recalculated_rewards.len());
assert_eq!(recalculated_rewards[0].num_rewards(), 0);
let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar();
let starting_index = (bank.block_height() + 1
- epoch_rewards_sysvar.distribution_starting_block_height)
as usize;
compare_stake_rewards(
&expected_stake_rewards[starting_index..],
&recalculated_rewards[starting_index..],
);
let mut bank =
Bank::new_from_parent(Arc::new(bank), &Pubkey::default(), SLOTS_PER_EPOCH + 2);
bank.recalculate_partitioned_rewards_if_active(|| &thread_pool);
assert_eq!(bank.epoch_reward_status, EpochRewardStatus::Inactive);
}
#[test]
fn test_initialize_after_snapshot_restore() {
let expected_num_stake_rewards = 3;
let num_rewards_per_block = 2;
let stakes = vec![
100_000_000, 2_000_000_000, 3_000_000_000, 4_000_000_000, ];
let (RewardBank { bank, .. }, _) = create_reward_bank_with_specific_stakes(
stakes,
num_rewards_per_block,
SLOTS_PER_EPOCH - 1,
);
let new_slot = bank.slot() + 1;
let mut bank = Bank::new_from_parent(bank, &Pubkey::default(), new_slot);
let EpochRewardStatus::Active(EpochRewardPhase::Calculation(calculation_status)) =
bank.epoch_reward_status.clone()
else {
panic!("{:?} not active calculation", bank.epoch_reward_status);
};
bank.feature_set = Arc::new(FeatureSet::default());
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
bank.initialize_after_snapshot_restore(|| &thread_pool);
let EpochRewardStatus::Active(EpochRewardPhase::Distribution(distribution_status)) =
bank.epoch_reward_status.clone()
else {
panic!("{:?} not active distribution", bank.epoch_reward_status);
};
assert_eq!(
calculation_status.all_stake_rewards,
distribution_status.all_stake_rewards
);
assert_eq!(
calculation_status.distribution_starting_block_height,
distribution_status.distribution_starting_block_height
);
assert_eq!(
calculation_status.all_stake_rewards.num_rewards(),
expected_num_stake_rewards
);
}
#[test]
fn test_initialize_after_snapshot_restore_preserves_vat_filtered_rewards() {
let num_validators = crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + 1;
let num_rewards_per_block = 64;
let stakes = (0..num_validators)
.map(|index| 2_000_000_000 + (num_validators - index) as u64)
.collect::<Vec<_>>();
let (
RewardBank {
bank,
voters,
stakers,
},
bank_forks,
) = create_reward_bank_with_specific_stakes(
stakes,
num_rewards_per_block,
SLOTS_PER_EPOCH - 1,
);
let filtered_vote_pubkey = *voters.last().unwrap();
let filtered_stake_pubkey = *stakers.last().unwrap();
let new_slot = bank.slot() + 1;
let mut bank = Bank::new_from_parent(bank, &Pubkey::default(), new_slot);
let leader_schedule_epoch = bank.epoch_schedule().get_leader_schedule_epoch(bank.slot());
let filtered_epoch_vote_accounts = bank
.epoch_stakes(leader_schedule_epoch)
.unwrap()
.stakes()
.vote_accounts();
assert_eq!(
bank.stakes_cache.stakes().vote_accounts().len(),
num_validators
);
assert_eq!(
filtered_epoch_vote_accounts.len(),
crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS
);
assert!(
filtered_epoch_vote_accounts
.get(&filtered_vote_pubkey)
.is_none()
);
let EpochRewardStatus::Active(EpochRewardPhase::Calculation(calculation_status)) =
bank.epoch_reward_status.clone()
else {
panic!("{:?} not active calculation", bank.epoch_reward_status);
};
assert_eq!(
calculation_status.all_stake_rewards.num_rewards(),
crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS
);
assert!(
calculation_status
.all_stake_rewards
.enumerated_rewards_iter()
.all(|(_, reward)| reward.stake_pubkey != filtered_stake_pubkey)
);
bank.feature_set = Arc::new(FeatureSet::default());
let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
bank.initialize_after_snapshot_restore(|| &thread_pool);
let EpochRewardStatus::Active(EpochRewardPhase::Distribution(distribution_status)) =
bank.epoch_reward_status.clone()
else {
panic!("{:?} not active distribution", bank.epoch_reward_status);
};
assert_eq!(
distribution_status.all_stake_rewards.num_rewards(),
crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS
);
assert!(
distribution_status
.all_stake_rewards
.enumerated_rewards_iter()
.all(|(_, reward)| reward.stake_pubkey != filtered_stake_pubkey)
);
assert_eq!(
calculation_status.distribution_starting_block_height,
distribution_status.distribution_starting_block_height
);
assert_eq!(
calculation_status.all_stake_rewards,
distribution_status.all_stake_rewards
);
let _ = &bank_forks; }
#[test]
fn test_reward_accumulator() {
let mut accumulator1 = RewardsAccumulator::default();
let mut accumulator2 = RewardsAccumulator::default();
let commission_pubkey_a = Pubkey::new_unique();
let commission_account_a = AccountSharedData::default();
let commission_pubkey_b = Pubkey::new_unique();
let commission_account_b = AccountSharedData::default();
let commission_pubkey_c = Pubkey::new_unique();
let commission_account_c = AccountSharedData::default();
accumulator1.add_reward(
commission_pubkey_a,
RewardCommission {
commission_account: commission_account_a.clone(),
commission_bps: 1_000,
commission_lamports: 50,
},
50,
);
accumulator1.add_reward(
commission_pubkey_b,
RewardCommission {
commission_account: commission_account_b.clone(),
commission_bps: 1_000,
commission_lamports: 50,
},
50,
);
accumulator2.add_reward(
commission_pubkey_b,
RewardCommission {
commission_account: commission_account_b,
commission_bps: 1_000,
commission_lamports: 30,
},
30,
);
accumulator2.add_reward(
commission_pubkey_c,
RewardCommission {
commission_account: commission_account_c,
commission_bps: 1_000,
commission_lamports: 50,
},
50,
);
assert_eq!(accumulator1.num_stake_rewards, 2);
assert_eq!(accumulator1.total_stake_rewards_lamports, 100);
let reward_commission_a_1 = accumulator1
.reward_commissions
.get(&commission_pubkey_a)
.unwrap();
assert_eq!(reward_commission_a_1.commission_bps, 1_000);
assert_eq!(reward_commission_a_1.commission_lamports, 50);
let reward_commission_b_1 = accumulator1
.reward_commissions
.get(&commission_pubkey_b)
.unwrap();
assert_eq!(reward_commission_b_1.commission_bps, 1_000);
assert_eq!(reward_commission_b_1.commission_lamports, 50);
let reward_commission_b_2 = accumulator2
.reward_commissions
.get(&commission_pubkey_b)
.unwrap();
assert_eq!(reward_commission_b_2.commission_bps, 1_000);
assert_eq!(reward_commission_b_2.commission_lamports, 30);
let reward_commission_c_2 = accumulator2
.reward_commissions
.get(&commission_pubkey_c)
.unwrap();
assert_eq!(reward_commission_c_2.commission_bps, 1_000);
assert_eq!(reward_commission_c_2.commission_lamports, 50);
let accumulator = accumulator1.accumulate_into_larger(accumulator2);
assert_eq!(accumulator.num_stake_rewards, 4);
assert_eq!(accumulator.total_stake_rewards_lamports, 180);
let reward_commission_a = accumulator
.reward_commissions
.get(&commission_pubkey_a)
.unwrap();
assert_eq!(reward_commission_a.commission_bps, 1_000);
assert_eq!(reward_commission_a.commission_lamports, 50);
let reward_commission_b = accumulator
.reward_commissions
.get(&commission_pubkey_b)
.unwrap();
assert_eq!(reward_commission_b.commission_bps, 1_000);
assert_eq!(reward_commission_b.commission_lamports, 80);
let reward_commission_c = accumulator
.reward_commissions
.get(&commission_pubkey_c)
.unwrap();
assert_eq!(reward_commission_c.commission_bps, 1_000);
assert_eq!(reward_commission_c.commission_lamports, 50);
}
#[test]
fn test_epoch_rewards_cache_multiple_forks() {
let (mut genesis_config, _mint_keypair) =
create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
const NUM_STAKES: usize = 1000;
for _i in 0..NUM_STAKES {
let vote_pubkey = Pubkey::new_unique();
let stake_pubkey = Pubkey::new_unique();
genesis_config.accounts.insert(
vote_pubkey,
vote_state::create_v4_account_with_authorized(
&vote_pubkey,
&vote_pubkey,
[0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
&vote_pubkey,
0,
&vote_pubkey,
0,
&vote_pubkey,
100_000_000_000,
)
.into(),
);
let stake_lamports = 1_000_000_000_000;
let stake_account = stake_utils::create_stake_account(
&stake_pubkey,
&vote_pubkey,
&vote_state::create_v4_account_with_authorized(
&vote_pubkey,
&vote_pubkey,
[0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE],
&vote_pubkey,
0,
&vote_pubkey,
0,
&vote_pubkey,
100_000_000_000,
),
&genesis_config.rent,
stake_lamports,
);
genesis_config
.accounts
.insert(stake_pubkey, stake_account.into());
}
let bank = Arc::new(Bank::new_for_tests(&genesis_config));
let next_epoch_slot = bank.get_slots_in_epoch(bank.epoch);
{
let cache = bank.epoch_rewards_calculation_cache.lock().unwrap();
assert!(
!cache.contains_key(&bank.parent_hash()),
"cache should be empty"
);
}
let bank_fork1 =
Bank::new_from_parent(Arc::clone(&bank), &Pubkey::default(), next_epoch_slot);
{
let cache = bank_fork1.epoch_rewards_calculation_cache.lock().unwrap();
assert!(
cache.contains_key(&bank_fork1.parent_hash()),
"cache should be populated"
);
}
let bank_fork2 = Bank::new_from_parent(bank, &Pubkey::default(), next_epoch_slot);
{
let cache = bank_fork2.epoch_rewards_calculation_cache.lock().unwrap();
assert!(
cache.contains_key(&bank_fork2.parent_hash()),
"cache should be populated"
);
}
}
fn add_voters_and_populate(
bank: &Arc<Bank>,
voters: &mut HashSet<Pubkey>,
stakers: &mut HashSet<Pubkey>,
count: usize,
stake_lamports: u64,
commission: u8,
) {
for _ in 0..count {
let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) =
create_staked_node_accounts(stake_lamports);
bank.store_account_and_update_capitalization(&vote_pubkey, &vote_account);
bank.store_account_and_update_capitalization(&stake_pubkey, &stake_account);
voters.insert(vote_pubkey);
stakers.insert(stake_pubkey);
}
populate_vote_accounts_with_votes(bank, voters.iter().copied(), commission);
}
#[allow(clippy::too_many_arguments)]
fn assert_cached_rewards(
bank: &Arc<Bank>,
expected_cache_len: usize,
expected_voters: &HashSet<Pubkey>,
expected_stakers: &HashSet<Pubkey>,
expected_reward_commissions: u64,
expected_stake_rewards: u64,
expected_rewards: u64,
expected_points: u128,
parent_capitalization: Option<u64>,
) {
let cache = bank.epoch_rewards_calculation_cache.lock().unwrap();
assert_eq!(cache.len(), expected_cache_len);
let partitioned = cache.get(&bank.parent_hash()).unwrap().as_ref();
let RewardCommissionAccounts {
accounts_with_rewards,
total_reward_commission_lamports,
..
} = &partitioned.reward_commission_accounts;
let StakeRewardCalculation {
stake_rewards,
total_stake_rewards_lamports,
..
} = &partitioned.stake_rewards;
let point_value = &partitioned.point_value;
let voters: HashSet<_> = accounts_with_rewards
.iter()
.map(|(pubkey, _reward, _acc)| *pubkey)
.collect();
let stakers: HashSet<_> = stake_rewards
.rewards
.iter()
.filter_map(|reward| reward.as_ref())
.map(|reward| reward.stake_pubkey)
.collect();
assert_eq!(expected_voters, &voters);
assert_eq!(expected_stakers, &stakers);
assert_eq!(
*total_reward_commission_lamports,
expected_reward_commissions
);
assert_eq!(*total_stake_rewards_lamports, expected_stake_rewards);
assert_eq!(point_value.rewards, expected_rewards);
assert_eq!(point_value.points, expected_points);
if let Some(parent_cap) = parent_capitalization {
assert_eq!(
bank.capitalization(),
parent_cap + expected_reward_commissions
);
}
}
#[test]
fn test_epoch_boundary() {
let delegations = 100;
let stake_lamports = 2_000_000_000;
let stakes: Vec<_> = (0..delegations).map(|_| stake_lamports).collect();
let (
RewardBank {
bank: bank1,
voters,
stakers,
..
},
_bank_forks,
) = create_reward_bank_with_specific_stakes(
stakes,
PartitionedEpochRewardsConfig::default().stake_account_stores_per_block,
SLOTS_PER_EPOCH,
);
let mut voters: HashSet<_> = voters.into_iter().collect();
let mut stakers: HashSet<_> = stakers.into_iter().collect();
let epoch_rewards_sysvar_balance = bank1.get_balance(&solana_sysvar::epoch_rewards::id());
assert_eq!(epoch_rewards_sysvar_balance, 1);
assert_cached_rewards(
&bank1,
1, &voters, &stakers, 0, 499500, 499542, 8_400_000_000_000u128, None, );
add_voters_and_populate(&bank1, &mut voters, &mut stakers, 5, 5_000_000_000, 10);
let parent_capitalization = bank1.capitalization();
let bank2 = Arc::new(Bank::new_from_parent(
Arc::clone(&bank1),
&Pubkey::default(),
SLOTS_PER_EPOCH * 2,
));
assert_cached_rewards(
&bank2,
2, &voters, &stakers, 5555, 494730, 500313, 9_450_000_000_000u128, Some(parent_capitalization), );
add_voters_and_populate(&bank2, &mut voters, &mut stakers, 10, 8_000_000_000, 10);
let parent_capitalization = bank2.capitalization();
let bank3 = Arc::new(Bank::new_from_parent(
Arc::clone(&bank2),
&Pubkey::default(),
SLOTS_PER_EPOCH * 3,
));
assert_cached_rewards(
&bank3,
3, &voters, &stakers, 17300, 485365, 502779, 12_810_000_000_000u128, Some(parent_capitalization), );
}
}