diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index f9a785eb9b5..620f9c964f6 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -41,9 +41,12 @@ use { bank::{ builtins::{BuiltinPrototype, BUILTINS}, metrics::*, + partitioned_epoch_rewards::{ + EpochRewardCalculateParamInfo, EpochRewardStatus, RewardInterval, StakeRewards, + VoteRewardsAccounts, + }, }, bank_forks::BankForks, - epoch_rewards_hasher::hash_rewards_into_partitions, epoch_stakes::{EpochStakes, NodeVoteAccounts}, installed_scheduler_pool::{BankWithScheduler, InstalledSchedulerRwLock}, serde_snapshot::BankIncrementalSnapshotPersistence, @@ -83,7 +86,6 @@ use { ancestors::{Ancestors, AncestorsForSerialization}, blockhash_queue::BlockhashQueue, epoch_accounts_hash::EpochAccountsHash, - partitioned_rewards::PartitionedEpochRewardsConfig, sorted_storages::SortedStorages, stake_rewards::StakeReward, storable_accounts::StorableAccounts, @@ -173,7 +175,7 @@ use { }, }, solana_system_program::{get_system_account_kind, SystemAccountKind}, - solana_vote::vote_account::{VoteAccount, VoteAccounts, VoteAccountsHashMap}, + solana_vote::vote_account::{VoteAccount, VoteAccountsHashMap}, solana_vote_program::vote_state::VoteState, std::{ borrow::Cow, @@ -220,6 +222,7 @@ pub mod builtins; pub mod epoch_accounts_hash_utils; mod fee_distribution; mod metrics; +pub(crate) mod partitioned_epoch_rewards; mod serde_snapshot; mod sysvar_cache; #[cfg(test)] @@ -642,27 +645,6 @@ impl AbiExample for OptionalDropCallback { } } -#[derive(AbiExample, Debug, Clone, PartialEq, Serialize, Deserialize)] -pub(crate) struct StartBlockHeightAndRewards { - /// the block height of the slot at which rewards distribution began - pub(crate) start_block_height: u64, - /// calculated epoch rewards pending distribution, outer Vec is by partition (one partition per block) - pub(crate) stake_rewards_by_partition: Arc>, -} - -/// Represent whether bank is in the reward phase or not. -#[derive(AbiExample, AbiEnumVisitor, Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -pub(crate) enum EpochRewardStatus { - /// this bank is in the reward phase. - /// Contents are the start point for epoch reward calculation, - /// i.e. parent_slot and parent_block height for the starting - /// block of the current epoch. - Active(StartBlockHeightAndRewards), - /// this bank is outside of the rewarding phase. - #[default] - Inactive, -} - /// Manager for the state of all accounts and programs after processing its entries. /// AbiExample is needed even without Serialize/Deserialize; actual (de-)serialization /// are implemented elsewhere for versioning @@ -865,56 +847,6 @@ struct VoteReward { } type VoteRewards = DashMap; -#[derive(Debug, Default)] -struct VoteRewardsAccounts { - /// reward info for each vote account pubkey. - /// This type is used by `update_reward_history()` - rewards: Vec<(Pubkey, RewardInfo)>, - /// corresponds to pubkey in `rewards` - /// Some if account is to be stored. - /// None if to be skipped. - accounts_to_store: Vec>, -} - -/// hold reward calc info to avoid recalculation across functions -struct EpochRewardCalculateParamInfo<'a> { - stake_history: StakeHistory, - stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, - cached_vote_accounts: &'a VoteAccounts, -} - -/// Hold all results from calculating the rewards for partitioned distribution. -/// This struct exists so we can have a function which does all the calculation with no -/// side effects. -struct PartitionedRewardsCalculation { - vote_account_rewards: VoteRewardsAccounts, - stake_rewards_by_partition: StakeRewardCalculationPartitioned, - old_vote_balance_and_staked: u64, - validator_rewards: u64, - validator_rate: f64, - foundation_rate: f64, - prev_epoch_duration_in_years: f64, - capitalization: u64, -} - -/// result of calculating the stake rewards at beginning of new epoch -struct StakeRewardCalculationPartitioned { - /// each individual stake account to reward, grouped by partition - stake_rewards_by_partition: Vec, - /// total lamports across all `stake_rewards` - total_stake_rewards_lamports: u64, -} - -struct CalculateRewardsAndDistributeVoteRewardsResult { - /// total rewards for the epoch (including both vote rewards and stake rewards) - total_rewards: u64, - /// distributed vote rewards - distributed_rewards: u64, - /// stake rewards that still need to be distributed, grouped by partition - stake_rewards_by_partition: Vec, -} - -pub(crate) type StakeRewards = Vec; #[derive(Debug, Default)] pub struct NewBankOptions { @@ -950,14 +882,6 @@ struct StakeRewardCalculation { total_stake_rewards_lamports: u64, } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(super) enum RewardInterval { - /// the slot within the epoch is INSIDE the reward distribution interval - InsideInterval, - /// the slot within the epoch is OUTSIDE the reward distribution interval - OutsideInterval, -} - impl Bank { fn default_with_accounts(accounts: Accounts) -> Self { let mut bank = Self { @@ -1144,76 +1068,6 @@ impl Bank { rent_collector.clone_with_epoch(epoch) } - fn is_partitioned_rewards_feature_enabled(&self) -> bool { - self.feature_set - .is_active(&feature_set::enable_partitioned_epoch_reward::id()) - } - - pub(crate) fn set_epoch_reward_status_active( - &mut self, - stake_rewards_by_partition: Vec, - ) { - self.epoch_reward_status = EpochRewardStatus::Active(StartBlockHeightAndRewards { - start_block_height: self.block_height, - stake_rewards_by_partition: Arc::new(stake_rewards_by_partition), - }); - } - - fn partitioned_epoch_rewards_config(&self) -> &PartitionedEpochRewardsConfig { - &self - .rc - .accounts - .accounts_db - .partitioned_epoch_rewards_config - } - - /// # stake accounts to store in one block during partitioned reward interval - fn partitioned_rewards_stake_account_stores_per_block(&self) -> u64 { - self.partitioned_epoch_rewards_config() - .stake_account_stores_per_block - } - - /// reward calculation happens synchronously during the first block of the epoch boundary. - /// So, # blocks for reward calculation is 1. - fn get_reward_calculation_num_blocks(&self) -> Slot { - self.partitioned_epoch_rewards_config() - .reward_calculation_num_blocks - } - - /// Calculate the number of blocks required to distribute rewards to all stake accounts. - fn get_reward_distribution_num_blocks(&self, rewards: &StakeRewards) -> u64 { - let total_stake_accounts = rewards.len(); - if self.epoch_schedule.warmup && self.epoch < self.first_normal_epoch() { - 1 - } else { - const MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH: u64 = 10; - let num_chunks = solana_accounts_db::accounts_hash::AccountsHasher::div_ceil( - total_stake_accounts, - self.partitioned_rewards_stake_account_stores_per_block() as usize, - ) as u64; - - // Limit the reward credit interval to 10% of the total number of slots in a epoch - num_chunks.clamp( - 1, - (self.epoch_schedule.slots_per_epoch / MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH).max(1), - ) - } - } - - /// Return `RewardInterval` enum for current bank - fn get_reward_interval(&self) -> RewardInterval { - if matches!(self.epoch_reward_status, EpochRewardStatus::Active(_)) { - RewardInterval::InsideInterval - } else { - RewardInterval::OutsideInterval - } - } - - /// For testing only - pub fn force_reward_interval_end_for_tests(&mut self) { - self.epoch_reward_status = EpochRewardStatus::Inactive; - } - fn _new_from_parent( parent: Arc, collector_id: &Pubkey, @@ -1621,111 +1475,6 @@ impl Bank { ); } - /// partitioned reward distribution is complete. - /// So, deactivate the epoch rewards sysvar. - fn deactivate_epoch_reward_status(&mut self) { - assert!(matches!( - self.epoch_reward_status, - EpochRewardStatus::Active(_) - )); - self.epoch_reward_status = EpochRewardStatus::Inactive; - if let Some(account) = self.get_account(&sysvar::epoch_rewards::id()) { - if account.lamports() > 0 { - info!( - "burning {} extra lamports in EpochRewards sysvar account at slot {}", - account.lamports(), - self.slot() - ); - self.log_epoch_rewards_sysvar("burn"); - self.burn_and_purge_account(&sysvar::epoch_rewards::id(), account); - } - } - } - - fn force_partition_rewards_in_first_block_of_epoch(&self) -> bool { - self.partitioned_epoch_rewards_config() - .test_enable_partitioned_rewards - && self.get_reward_calculation_num_blocks() == 0 - && self.partitioned_rewards_stake_account_stores_per_block() == u64::MAX - } - - /// Begin the process of calculating and distributing rewards. - /// This process can take multiple slots. - fn begin_partitioned_rewards( - &mut self, - reward_calc_tracer: Option, - thread_pool: &ThreadPool, - parent_epoch: Epoch, - parent_slot: Slot, - parent_block_height: u64, - rewards_metrics: &mut RewardsMetrics, - ) { - let CalculateRewardsAndDistributeVoteRewardsResult { - total_rewards, - distributed_rewards, - stake_rewards_by_partition, - } = self.calculate_rewards_and_distribute_vote_rewards( - parent_epoch, - reward_calc_tracer, - thread_pool, - rewards_metrics, - ); - - let slot = self.slot(); - let credit_start = self.block_height() + self.get_reward_calculation_num_blocks(); - - self.set_epoch_reward_status_active(stake_rewards_by_partition); - - // create EpochRewards sysvar that holds the balance of undistributed rewards with - // (total_rewards, distributed_rewards, credit_start), total capital will increase by (total_rewards - distributed_rewards) - self.create_epoch_rewards_sysvar(total_rewards, distributed_rewards, credit_start); - - datapoint_info!( - "epoch-rewards-status-update", - ("start_slot", slot, i64), - ("start_block_height", self.block_height(), i64), - ("active", 1, i64), - ("parent_slot", parent_slot, i64), - ("parent_block_height", parent_block_height, i64), - ); - } - - /// Process reward distribution for the block if it is inside reward interval. - fn distribute_partitioned_epoch_rewards(&mut self) { - let EpochRewardStatus::Active(status) = &self.epoch_reward_status else { - return; - }; - - let height = self.block_height(); - let start_block_height = status.start_block_height; - let credit_start = start_block_height + self.get_reward_calculation_num_blocks(); - let credit_end_exclusive = credit_start + status.stake_rewards_by_partition.len() as u64; - assert!( - self.epoch_schedule.get_slots_in_epoch(self.epoch) - > credit_end_exclusive.saturating_sub(credit_start) - ); - - if height >= credit_start && height < credit_end_exclusive { - let partition_index = height - credit_start; - self.distribute_epoch_rewards_in_partition( - &status.stake_rewards_by_partition, - partition_index, - ); - } - - if height.saturating_add(1) >= credit_end_exclusive { - datapoint_info!( - "epoch-rewards-status-update", - ("slot", self.slot(), i64), - ("block_height", height, i64), - ("active", 0, i64), - ("start_block_height", start_block_height, i64), - ); - - self.deactivate_epoch_reward_status(); - } - } - pub fn byte_limit_for_scans(&self) -> Option { self.rc .accounts @@ -2410,149 +2159,6 @@ impl Bank { } } - /// Calculate rewards from previous epoch to prepare for partitioned distribution. - fn calculate_rewards_for_partitioning( - &self, - prev_epoch: Epoch, - reward_calc_tracer: Option, - thread_pool: &ThreadPool, - metrics: &mut RewardsMetrics, - ) -> PartitionedRewardsCalculation { - let capitalization = self.capitalization(); - let PrevEpochInflationRewards { - validator_rewards, - prev_epoch_duration_in_years, - validator_rate, - foundation_rate, - } = self.calculate_previous_epoch_inflation_rewards(capitalization, prev_epoch); - - let old_vote_balance_and_staked = self.stakes_cache.stakes().vote_balance_and_staked(); - - let (vote_account_rewards, mut stake_rewards) = self - .calculate_validator_rewards( - prev_epoch, - validator_rewards, - reward_calc_tracer, - thread_pool, - metrics, - ) - .unwrap_or_default(); - - let num_partitions = self.get_reward_distribution_num_blocks(&stake_rewards.stake_rewards); - let parent_blockhash = self - .parent() - .expect("Partitioned rewards calculation must still have access to parent Bank.") - .last_blockhash(); - let stake_rewards_by_partition = hash_rewards_into_partitions( - std::mem::take(&mut stake_rewards.stake_rewards), - &parent_blockhash, - num_partitions as usize, - ); - - PartitionedRewardsCalculation { - vote_account_rewards, - stake_rewards_by_partition: StakeRewardCalculationPartitioned { - stake_rewards_by_partition, - total_stake_rewards_lamports: stake_rewards.total_stake_rewards_lamports, - }, - old_vote_balance_and_staked, - validator_rewards, - validator_rate, - foundation_rate, - prev_epoch_duration_in_years, - capitalization, - } - } - - // Calculate rewards from previous epoch and distribute vote rewards - fn calculate_rewards_and_distribute_vote_rewards( - &self, - prev_epoch: Epoch, - reward_calc_tracer: Option, - thread_pool: &ThreadPool, - metrics: &mut RewardsMetrics, - ) -> CalculateRewardsAndDistributeVoteRewardsResult { - let PartitionedRewardsCalculation { - vote_account_rewards, - stake_rewards_by_partition, - old_vote_balance_and_staked, - validator_rewards, - validator_rate, - foundation_rate, - prev_epoch_duration_in_years, - capitalization, - } = self.calculate_rewards_for_partitioning( - prev_epoch, - reward_calc_tracer, - thread_pool, - metrics, - ); - let vote_rewards = self.store_vote_accounts_partitioned(vote_account_rewards, metrics); - - // update reward history of JUST vote_rewards, stake_rewards is vec![] here - self.update_reward_history(vec![], vote_rewards); - - let StakeRewardCalculationPartitioned { - stake_rewards_by_partition, - total_stake_rewards_lamports, - } = stake_rewards_by_partition; - - // the remaining code mirrors `update_rewards_with_thread_pool()` - - let new_vote_balance_and_staked = self.stakes_cache.stakes().vote_balance_and_staked(); - - // This is for vote rewards only. - let validator_rewards_paid = new_vote_balance_and_staked - old_vote_balance_and_staked; - self.assert_validator_rewards_paid(validator_rewards_paid); - - // verify that we didn't pay any more than we expected to - assert!(validator_rewards >= validator_rewards_paid + total_stake_rewards_lamports); - - info!( - "distributed vote rewards: {} out of {}, remaining {}", - validator_rewards_paid, validator_rewards, total_stake_rewards_lamports - ); - - let (num_stake_accounts, num_vote_accounts) = { - let stakes = self.stakes_cache.stakes(); - ( - stakes.stake_delegations().len(), - stakes.vote_accounts().len(), - ) - }; - self.capitalization - .fetch_add(validator_rewards_paid, 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), - ("epoch_duration_in_years", prev_epoch_duration_in_years, f64), - ("validator_rewards", validator_rewards_paid, 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), - ); - - CalculateRewardsAndDistributeVoteRewardsResult { - total_rewards: validator_rewards_paid + total_stake_rewards_lamports, - distributed_rewards: validator_rewards_paid, - stake_rewards_by_partition, - } - } - fn assert_validator_rewards_paid(&self, validator_rewards_paid: u64) { assert_eq!( validator_rewards_paid, @@ -2817,36 +2423,6 @@ impl Bank { } } - /// Calculate epoch reward and return vote and stake rewards. - fn calculate_validator_rewards( - &self, - rewarded_epoch: Epoch, - rewards: u64, - reward_calc_tracer: Option, - thread_pool: &ThreadPool, - metrics: &mut RewardsMetrics, - ) -> Option<(VoteRewardsAccounts, StakeRewardCalculation)> { - let stakes = self.stakes_cache.stakes(); - let reward_calculate_param = self.get_epoch_reward_calculate_param_info(&stakes); - - self.calculate_reward_points_partitioned( - &reward_calculate_param, - rewards, - thread_pool, - metrics, - ) - .map(|point_value| { - self.calculate_stake_vote_rewards( - &reward_calculate_param, - rewarded_epoch, - point_value, - thread_pool, - reward_calc_tracer, - metrics, - ) - }) - } - /// Load, calculate and payout epoch rewards for stake and vote accounts fn pay_validator_rewards_with_thread_pool( &mut self, @@ -2900,99 +2476,6 @@ impl Bank { } } - /// compare the vote and stake accounts between the normal rewards calculation code - /// and the partitioned rewards calculation code - /// `stake_rewards_expected` and `vote_rewards_expected` are the results of the normal rewards calculation code - /// This fn should have NO side effects. - /// This fn is only called in tests or with a debug cli arg prior to partitioned rewards feature activation. - fn compare_with_partitioned_rewards_results( - stake_rewards_expected: &[StakeReward], - vote_rewards_expected: &DashMap, - partitioned_rewards: PartitionedRewardsCalculation, - ) { - // put partitioned stake rewards in a hashmap - let mut stake_rewards: HashMap = HashMap::default(); - partitioned_rewards - .stake_rewards_by_partition - .stake_rewards_by_partition - .iter() - .flatten() - .for_each(|stake_reward| { - stake_rewards.insert(stake_reward.stake_pubkey, stake_reward); - }); - - // verify stake rewards match expected - stake_rewards_expected.iter().for_each(|stake_reward| { - let partitioned = stake_rewards.remove(&stake_reward.stake_pubkey).unwrap(); - assert_eq!(partitioned, stake_reward); - }); - assert!(stake_rewards.is_empty(), "{stake_rewards:?}"); - - let mut vote_rewards: HashMap = HashMap::default(); - partitioned_rewards - .vote_account_rewards - .accounts_to_store - .iter() - .enumerate() - .for_each(|(i, account)| { - if let Some(account) = account { - let reward = &partitioned_rewards.vote_account_rewards.rewards[i]; - vote_rewards.insert(reward.0, (reward.1, account.clone())); - } - }); - - // verify vote rewards match expected - vote_rewards_expected.iter().for_each(|entry| { - if entry.value().vote_needs_store { - let partitioned = vote_rewards.remove(entry.key()).unwrap(); - let mut to_store_partitioned = partitioned.1.clone(); - to_store_partitioned.set_lamports(partitioned.0.post_balance); - let mut to_store_normal = entry.value().vote_account.clone(); - _ = to_store_normal.checked_add_lamports(entry.value().vote_rewards); - assert_eq!(to_store_partitioned, to_store_normal, "{:?}", entry.key()); - } - }); - assert!(vote_rewards.is_empty(), "{vote_rewards:?}"); - info!( - "verified partitioned rewards calculation matching: {}, {}", - partitioned_rewards - .stake_rewards_by_partition - .stake_rewards_by_partition - .iter() - .map(|rewards| rewards.len()) - .sum::(), - partitioned_rewards - .vote_account_rewards - .accounts_to_store - .len() - ); - } - - /// compare the vote and stake accounts between the normal rewards calculation code - /// and the partitioned rewards calculation code - /// `stake_rewards_expected` and `vote_rewards_expected` are the results of the normal rewards calculation code - /// This fn should have NO side effects. - fn compare_with_partitioned_rewards( - &self, - stake_rewards_expected: &[StakeReward], - vote_rewards_expected: &DashMap, - rewarded_epoch: Epoch, - thread_pool: &ThreadPool, - reward_calc_tracer: Option, - ) { - let partitioned_rewards = self.calculate_rewards_for_partitioning( - rewarded_epoch, - reward_calc_tracer, - thread_pool, - &mut RewardsMetrics::default(), - ); - Self::compare_with_partitioned_rewards_results( - stake_rewards_expected, - vote_rewards_expected, - partitioned_rewards, - ); - } - fn load_vote_and_stake_accounts( &mut self, thread_pool: &ThreadPool, @@ -3018,68 +2501,6 @@ impl Bank { vote_with_stake_delegations_map } - /// Calculates epoch reward points from stake/vote accounts. - /// Returns reward lamports and points for the epoch or none if points == 0. - fn calculate_reward_points_partitioned( - &self, - reward_calculate_params: &EpochRewardCalculateParamInfo, - rewards: u64, - thread_pool: &ThreadPool, - metrics: &RewardsMetrics, - ) -> Option { - let EpochRewardCalculateParamInfo { - stake_history, - stake_delegations, - cached_vote_accounts, - } = reward_calculate_params; - - let solana_vote_program: Pubkey = solana_vote_program::id(); - - let get_vote_account = |vote_pubkey: &Pubkey| -> Option { - if let Some(vote_account) = cached_vote_accounts.get(vote_pubkey) { - return Some(vote_account.clone()); - } - // If accounts-db contains a valid vote account, then it should - // already have been cached in cached_vote_accounts; so the code - // below is only for sanity checking, and can be removed once - // the cache is deemed to be reliable. - let account = self.get_account_with_fixed_root(vote_pubkey)?; - VoteAccount::try_from(account).ok() - }; - - 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() - .map(|(_stake_pubkey, stake_account)| { - let delegation = stake_account.delegation(); - let vote_pubkey = delegation.voter_pubkey; - - let Some(vote_account) = get_vote_account(&vote_pubkey) else { - return 0; - }; - if vote_account.owner() != &solana_vote_program { - return 0; - } - let Ok(vote_state) = vote_account.vote_state() else { - return 0; - }; - - solana_stake_program::points::calculate_points( - stake_account.stake_state(), - vote_state, - stake_history, - new_warmup_cooldown_rate_epoch, - ) - .unwrap_or(0) - }) - .sum::() - })); - metrics.calculate_points_us.fetch_add(measure_us, Relaxed); - - (points > 0).then_some(PointValue { rewards, points }) - } - fn calculate_reward_points( &self, vote_with_stake_delegations_map: &VoteWithStakeDelegationsMap, @@ -3375,65 +2796,6 @@ impl Bank { .fetch_add(now.elapsed().as_micros() as u64, Relaxed); } - /// store stake rewards in partition - /// return the sum of all the stored rewards - /// - /// Note: even if staker's reward is 0, the stake account still needs to be stored because - /// credits observed has changed - fn store_stake_accounts_in_partition(&self, stake_rewards: &[StakeReward]) -> u64 { - // Verify that stake account `lamports + reward_amount` matches what we have in the - // rewarded account. This code will have a performance hit - an extra load and compare of - // the stake accounts. This is for debugging. Once we are confident, we can disable the - // check. - const VERIFY_REWARD_LAMPORT: bool = true; - - if VERIFY_REWARD_LAMPORT { - for r in stake_rewards { - let stake_pubkey = r.stake_pubkey; - let reward_amount = r.get_stake_reward(); - let post_stake_account = &r.stake_account; - if let Some(curr_stake_account) = self.get_account_with_fixed_root(&stake_pubkey) { - let pre_lamport = curr_stake_account.lamports(); - let post_lamport = post_stake_account.lamports(); - assert_eq!(pre_lamport + u64::try_from(reward_amount).unwrap(), post_lamport, - "stake account balance has changed since the reward calculation! account: {stake_pubkey}, pre balance: {pre_lamport}, post balance: {post_lamport}, rewards: {reward_amount}"); - } - } - } - - self.store_accounts((self.slot(), stake_rewards)); - stake_rewards - .iter() - .map(|stake_reward| stake_reward.stake_reward_info.lamports) - .sum::() as u64 - } - - fn store_vote_accounts_partitioned( - &self, - vote_account_rewards: VoteRewardsAccounts, - metrics: &RewardsMetrics, - ) -> Vec<(Pubkey, RewardInfo)> { - let (_, measure_us) = measure_us!({ - // reformat data to make it not sparse. - // `StorableAccounts` does not efficiently handle sparse data. - // Not all entries in `vote_account_rewards.accounts_to_store` have a Some(account) to store. - let to_store = vote_account_rewards - .accounts_to_store - .iter() - .filter_map(|account| account.as_ref()) - .enumerate() - .map(|(i, account)| (&vote_account_rewards.rewards[i].0, account)) - .collect::>(); - self.store_accounts((self.slot(), &to_store[..])); - }); - - metrics - .store_vote_accounts_us - .fetch_add(measure_us, Relaxed); - - vote_account_rewards.rewards - } - fn store_vote_accounts( &self, vote_account_rewards: VoteRewards, @@ -3542,129 +2904,6 @@ impl Bank { .for_each(|x| rewards.push((x.stake_pubkey, x.stake_reward_info))); } - /// insert non-zero stake rewards to self.rewards - /// Return the number of rewards inserted - fn update_reward_history_in_partition(&self, stake_rewards: &[StakeReward]) -> usize { - let mut rewards = self.rewards.write().unwrap(); - rewards.reserve(stake_rewards.len()); - let initial_len = rewards.len(); - stake_rewards - .iter() - .filter(|x| x.get_stake_reward() > 0) - .for_each(|x| rewards.push((x.stake_pubkey, x.stake_reward_info))); - rewards.len().saturating_sub(initial_len) - } - - /// Process reward credits for a partition of rewards - /// Store the rewards to AccountsDB, update reward history record and total capitalization. - fn distribute_epoch_rewards_in_partition( - &self, - all_stake_rewards: &[Vec], - partition_index: u64, - ) { - let pre_capitalization = self.capitalization(); - let this_partition_stake_rewards = &all_stake_rewards[partition_index as usize]; - - let (total_rewards_in_lamports, store_stake_accounts_us) = - measure_us!(self.store_stake_accounts_in_partition(this_partition_stake_rewards)); - - // increase total capitalization by the distributed rewards - self.capitalization - .fetch_add(total_rewards_in_lamports, Relaxed); - - // decrease distributed capital from epoch rewards sysvar - self.update_epoch_rewards_sysvar(total_rewards_in_lamports); - - // update reward history for this partitioned distribution - self.update_reward_history_in_partition(this_partition_stake_rewards); - - let metrics = RewardsStoreMetrics { - pre_capitalization, - post_capitalization: self.capitalization(), - total_stake_accounts_count: all_stake_rewards.len(), - partition_index, - store_stake_accounts_us, - store_stake_accounts_count: this_partition_stake_rewards.len(), - distributed_rewards: total_rewards_in_lamports, - }; - - report_partitioned_reward_metrics(self, metrics); - } - - /// true if it is ok to run partitioned rewards code. - /// This means the feature is activated or certain testing situations. - fn is_partitioned_rewards_code_enabled(&self) -> bool { - self.is_partitioned_rewards_feature_enabled() - || self - .partitioned_epoch_rewards_config() - .test_enable_partitioned_rewards - } - - /// Helper fn to log epoch_rewards sysvar - fn log_epoch_rewards_sysvar(&self, prefix: &str) { - if let Some(account) = self.get_account(&sysvar::epoch_rewards::id()) { - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = - from_account(&account).unwrap(); - info!( - "{prefix} epoch_rewards sysvar: {:?}", - (account.lamports(), epoch_rewards) - ); - } else { - info!("{prefix} epoch_rewards sysvar: none"); - } - } - - /// Create EpochRewards sysvar with calculated rewards - fn create_epoch_rewards_sysvar( - &self, - total_rewards: u64, - distributed_rewards: u64, - distribution_starting_block_height: u64, - ) { - assert!(self.is_partitioned_rewards_code_enabled()); - - let epoch_rewards = sysvar::epoch_rewards::EpochRewards { - total_rewards, - distributed_rewards, - distribution_starting_block_height, - active: true, - ..sysvar::epoch_rewards::EpochRewards::default() - }; - - self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { - let mut inherited_account_fields = - self.inherit_specially_retained_account_fields(account); - - assert!(total_rewards >= distributed_rewards); - // set the account lamports to the undistributed rewards - inherited_account_fields.0 = total_rewards - distributed_rewards; - create_account(&epoch_rewards, inherited_account_fields) - }); - - self.log_epoch_rewards_sysvar("create"); - } - - /// Update EpochRewards sysvar with distributed rewards - fn update_epoch_rewards_sysvar(&self, distributed: u64) { - assert!(self.is_partitioned_rewards_code_enabled()); - - let mut epoch_rewards: sysvar::epoch_rewards::EpochRewards = - from_account(&self.get_account(&sysvar::epoch_rewards::id()).unwrap()).unwrap(); - epoch_rewards.distribute(distributed); - - self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { - let mut inherited_account_fields = - self.inherit_specially_retained_account_fields(account); - - let lamports = inherited_account_fields.0; - assert!(lamports >= distributed); - inherited_account_fields.0 = lamports - distributed; - create_account(&epoch_rewards, inherited_account_fields) - }); - - self.log_epoch_rewards_sysvar("update"); - } - fn update_recent_blockhashes_locked(&self, locked_blockhash_queue: &BlockhashQueue) { #[allow(deprecated)] self.update_sysvar_account(&sysvar::recent_blockhashes::id(), |account| { diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs new file mode 100644 index 00000000000..bda22852c6c --- /dev/null +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -0,0 +1,637 @@ +use { + super::{ + epoch_rewards_hasher::hash_rewards_into_partitions, Bank, + CalculateRewardsAndDistributeVoteRewardsResult, EpochRewardCalculateParamInfo, + PartitionedRewardsCalculation, StakeRewardCalculationPartitioned, VoteRewardsAccounts, + }, + crate::bank::{ + PrevEpochInflationRewards, RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, + StakeRewardCalculation, VoteAccount, + }, + log::info, + rayon::{ + iter::{IntoParallelRefIterator, ParallelIterator}, + ThreadPool, + }, + solana_measure::measure_us, + solana_sdk::{ + clock::{Epoch, Slot}, + pubkey::Pubkey, + reward_info::RewardInfo, + }, + solana_stake_program::points::PointValue, + std::sync::atomic::Ordering::Relaxed, +}; + +impl Bank { + /// Begin the process of calculating and distributing rewards. + /// This process can take multiple slots. + pub(in crate::bank) fn begin_partitioned_rewards( + &mut self, + reward_calc_tracer: Option, + thread_pool: &ThreadPool, + parent_epoch: Epoch, + parent_slot: Slot, + parent_block_height: u64, + rewards_metrics: &mut RewardsMetrics, + ) { + let CalculateRewardsAndDistributeVoteRewardsResult { + total_rewards, + distributed_rewards, + stake_rewards_by_partition, + } = self.calculate_rewards_and_distribute_vote_rewards( + parent_epoch, + reward_calc_tracer, + thread_pool, + rewards_metrics, + ); + + let slot = self.slot(); + let credit_start = self.block_height() + self.get_reward_calculation_num_blocks(); + + self.set_epoch_reward_status_active(stake_rewards_by_partition); + + // create EpochRewards sysvar that holds the balance of undistributed rewards with + // (total_rewards, distributed_rewards, credit_start), total capital will increase by (total_rewards - distributed_rewards) + self.create_epoch_rewards_sysvar(total_rewards, distributed_rewards, credit_start); + + datapoint_info!( + "epoch-rewards-status-update", + ("start_slot", slot, i64), + ("start_block_height", self.block_height(), i64), + ("active", 1, i64), + ("parent_slot", parent_slot, i64), + ("parent_block_height", parent_block_height, i64), + ); + } + + // Calculate rewards from previous epoch and distribute vote rewards + fn calculate_rewards_and_distribute_vote_rewards( + &self, + prev_epoch: Epoch, + reward_calc_tracer: Option, + thread_pool: &ThreadPool, + metrics: &mut RewardsMetrics, + ) -> CalculateRewardsAndDistributeVoteRewardsResult { + let PartitionedRewardsCalculation { + vote_account_rewards, + stake_rewards_by_partition, + old_vote_balance_and_staked, + validator_rewards, + validator_rate, + foundation_rate, + prev_epoch_duration_in_years, + capitalization, + } = self.calculate_rewards_for_partitioning( + prev_epoch, + reward_calc_tracer, + thread_pool, + metrics, + ); + let vote_rewards = self.store_vote_accounts_partitioned(vote_account_rewards, metrics); + + // update reward history of JUST vote_rewards, stake_rewards is vec![] here + self.update_reward_history(vec![], vote_rewards); + + let StakeRewardCalculationPartitioned { + stake_rewards_by_partition, + total_stake_rewards_lamports, + } = stake_rewards_by_partition; + + // the remaining code mirrors `update_rewards_with_thread_pool()` + + let new_vote_balance_and_staked = self.stakes_cache.stakes().vote_balance_and_staked(); + + // This is for vote rewards only. + let validator_rewards_paid = new_vote_balance_and_staked - old_vote_balance_and_staked; + self.assert_validator_rewards_paid(validator_rewards_paid); + + // verify that we didn't pay any more than we expected to + assert!(validator_rewards >= validator_rewards_paid + total_stake_rewards_lamports); + + info!( + "distributed vote rewards: {} out of {}, remaining {}", + validator_rewards_paid, validator_rewards, total_stake_rewards_lamports + ); + + let (num_stake_accounts, num_vote_accounts) = { + let stakes = self.stakes_cache.stakes(); + ( + stakes.stake_delegations().len(), + stakes.vote_accounts().len(), + ) + }; + self.capitalization + .fetch_add(validator_rewards_paid, 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), + ("epoch_duration_in_years", prev_epoch_duration_in_years, f64), + ("validator_rewards", validator_rewards_paid, 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), + ); + + CalculateRewardsAndDistributeVoteRewardsResult { + total_rewards: validator_rewards_paid + total_stake_rewards_lamports, + distributed_rewards: validator_rewards_paid, + stake_rewards_by_partition, + } + } + + fn store_vote_accounts_partitioned( + &self, + vote_account_rewards: VoteRewardsAccounts, + metrics: &RewardsMetrics, + ) -> Vec<(Pubkey, RewardInfo)> { + let (_, measure_us) = measure_us!({ + // reformat data to make it not sparse. + // `StorableAccounts` does not efficiently handle sparse data. + // Not all entries in `vote_account_rewards.accounts_to_store` have a Some(account) to store. + let to_store = vote_account_rewards + .accounts_to_store + .iter() + .filter_map(|account| account.as_ref()) + .enumerate() + .map(|(i, account)| (&vote_account_rewards.rewards[i].0, account)) + .collect::>(); + self.store_accounts((self.slot(), &to_store[..])); + }); + + metrics + .store_vote_accounts_us + .fetch_add(measure_us, Relaxed); + + vote_account_rewards.rewards + } + + /// Calculate rewards from previous epoch to prepare for partitioned distribution. + pub(in crate::bank) fn calculate_rewards_for_partitioning( + &self, + prev_epoch: Epoch, + reward_calc_tracer: Option, + thread_pool: &ThreadPool, + metrics: &mut RewardsMetrics, + ) -> PartitionedRewardsCalculation { + let capitalization = self.capitalization(); + let PrevEpochInflationRewards { + validator_rewards, + prev_epoch_duration_in_years, + validator_rate, + foundation_rate, + } = self.calculate_previous_epoch_inflation_rewards(capitalization, prev_epoch); + + let old_vote_balance_and_staked = self.stakes_cache.stakes().vote_balance_and_staked(); + + let (vote_account_rewards, mut stake_rewards) = self + .calculate_validator_rewards( + prev_epoch, + validator_rewards, + reward_calc_tracer, + thread_pool, + metrics, + ) + .unwrap_or_default(); + + let num_partitions = self.get_reward_distribution_num_blocks(&stake_rewards.stake_rewards); + let parent_blockhash = self + .parent() + .expect("Partitioned rewards calculation must still have access to parent Bank.") + .last_blockhash(); + let stake_rewards_by_partition = hash_rewards_into_partitions( + std::mem::take(&mut stake_rewards.stake_rewards), + &parent_blockhash, + num_partitions as usize, + ); + + PartitionedRewardsCalculation { + vote_account_rewards, + stake_rewards_by_partition: StakeRewardCalculationPartitioned { + stake_rewards_by_partition, + total_stake_rewards_lamports: stake_rewards.total_stake_rewards_lamports, + }, + old_vote_balance_and_staked, + validator_rewards, + validator_rate, + foundation_rate, + prev_epoch_duration_in_years, + capitalization, + } + } + + /// Calculate epoch reward and return vote and stake rewards. + fn calculate_validator_rewards( + &self, + rewarded_epoch: Epoch, + rewards: u64, + reward_calc_tracer: Option, + thread_pool: &ThreadPool, + metrics: &mut RewardsMetrics, + ) -> Option<(VoteRewardsAccounts, StakeRewardCalculation)> { + let stakes = self.stakes_cache.stakes(); + let reward_calculate_param = self.get_epoch_reward_calculate_param_info(&stakes); + + self.calculate_reward_points_partitioned( + &reward_calculate_param, + rewards, + thread_pool, + metrics, + ) + .map(|point_value| { + self.calculate_stake_vote_rewards( + &reward_calculate_param, + rewarded_epoch, + point_value, + thread_pool, + reward_calc_tracer, + metrics, + ) + }) + } + + /// Calculates epoch reward points from stake/vote accounts. + /// Returns reward lamports and points for the epoch or none if points == 0. + fn calculate_reward_points_partitioned( + &self, + reward_calculate_params: &EpochRewardCalculateParamInfo, + rewards: u64, + thread_pool: &ThreadPool, + metrics: &RewardsMetrics, + ) -> Option { + let EpochRewardCalculateParamInfo { + stake_history, + stake_delegations, + cached_vote_accounts, + } = reward_calculate_params; + + let solana_vote_program: Pubkey = solana_vote_program::id(); + + let get_vote_account = |vote_pubkey: &Pubkey| -> Option { + if let Some(vote_account) = cached_vote_accounts.get(vote_pubkey) { + return Some(vote_account.clone()); + } + // If accounts-db contains a valid vote account, then it should + // already have been cached in cached_vote_accounts; so the code + // below is only for sanity checking, and can be removed once + // the cache is deemed to be reliable. + let account = self.get_account_with_fixed_root(vote_pubkey)?; + VoteAccount::try_from(account).ok() + }; + + 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() + .map(|(_stake_pubkey, stake_account)| { + let delegation = stake_account.delegation(); + let vote_pubkey = delegation.voter_pubkey; + + let Some(vote_account) = get_vote_account(&vote_pubkey) else { + return 0; + }; + if vote_account.owner() != &solana_vote_program { + return 0; + } + let Ok(vote_state) = vote_account.vote_state() else { + return 0; + }; + + solana_stake_program::points::calculate_points( + stake_account.stake_state(), + vote_state, + stake_history, + new_warmup_cooldown_rate_epoch, + ) + .unwrap_or(0) + }) + .sum::() + })); + metrics.calculate_points_us.fetch_add(measure_us, Relaxed); + + (points > 0).then_some(PointValue { rewards, points }) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + bank::{null_tracer, tests::create_genesis_config, VoteReward}, + genesis_utils::{ + create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs, + }, + stake_account::StakeAccount, + stakes::Stakes, + }, + rayon::ThreadPoolBuilder, + solana_sdk::{ + account::{accounts_equal, ReadableAccount, WritableAccount}, + native_token::{sol_to_lamports, LAMPORTS_PER_SOL}, + reward_type::RewardType, + signature::Signer, + stake::state::Delegation, + vote::state::{VoteStateVersions, MAX_LOCKOUT_HISTORY}, + }, + solana_vote_program::vote_state, + std::sync::RwLockReadGuard, + }; + + /// Helper function to create a bank that pays some rewards + fn create_reward_bank(expected_num_delegations: usize) -> (Bank, Vec, Vec) { + let validator_keypairs = (0..expected_num_delegations) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![2_000_000_000; expected_num_delegations], + ); + + let bank = Bank::new_for_tests(&genesis_config); + + // Fill bank_forks with banks with votes landing in the next slot + // Create enough banks such that vote account will root + for validator_vote_keypairs in &validator_keypairs { + let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); + let mut vote_account = bank.get_account(&vote_id).unwrap(); + // generate some rewards + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); + for i in 0..MAX_LOCKOUT_HISTORY + 42 { + if let Some(v) = vote_state.as_mut() { + vote_state::process_slot_vote_unchecked(v, i as u64) + } + let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + vote_state::to(&versioned, &mut vote_account).unwrap(); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; + } + bank.store_account_and_update_capitalization(&vote_id, &vote_account); + } + ( + bank, + validator_keypairs + .iter() + .map(|k| k.vote_keypair.pubkey()) + .collect(), + validator_keypairs + .iter() + .map(|k| k.stake_keypair.pubkey()) + .collect(), + ) + } + + #[test] + fn test_store_vote_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 expected_vote_rewards_num = 100; + + let vote_rewards = (0..expected_vote_rewards_num) + .map(|_| Some((Pubkey::new_unique(), VoteReward::new_random()))) + .collect::>(); + + let mut vote_rewards_account = VoteRewardsAccounts::default(); + vote_rewards.iter().for_each(|e| { + if let Some(p) = &e { + let info = RewardInfo { + reward_type: RewardType::Voting, + lamports: p.1.vote_rewards as i64, + post_balance: p.1.vote_rewards, + commission: Some(p.1.commission), + }; + vote_rewards_account.rewards.push((p.0, info)); + vote_rewards_account + .accounts_to_store + .push(e.as_ref().map(|p| p.1.vote_account.clone())); + } + }); + + let metrics = RewardsMetrics::default(); + + let stored_vote_accounts = + bank.store_vote_accounts_partitioned(vote_rewards_account, &metrics); + assert_eq!(expected_vote_rewards_num, stored_vote_accounts.len()); + + // load accounts to make sure they were stored correctly + vote_rewards.iter().for_each(|e| { + if let Some(p) = &e { + let (k, account) = (p.0, p.1.vote_account.clone()); + let loaded_account = bank.load_slow_with_fixed_root(&bank.ancestors, &k).unwrap(); + assert!(accounts_equal(&loaded_account.0, &account)); + } + }); + } + + #[test] + fn test_store_vote_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 vote_rewards = VoteRewardsAccounts::default(); + let metrics = RewardsMetrics::default(); + + let stored_vote_accounts = bank.store_vote_accounts_partitioned(vote_rewards, &metrics); + assert_eq!(expected, stored_vote_accounts.len()); + } + + #[test] + /// Test rewards computation and partitioned rewards distribution at the epoch boundary + fn test_rewards_computation() { + solana_logger::setup(); + + let expected_num_delegations = 100; + let bank = create_reward_bank(expected_num_delegations).0; + + // Calculate rewards + let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); + let mut rewards_metrics = RewardsMetrics::default(); + let expected_rewards = 100_000_000_000; + + let calculated_rewards = bank.calculate_validator_rewards( + 1, + expected_rewards, + null_tracer(), + &thread_pool, + &mut rewards_metrics, + ); + + let vote_rewards = &calculated_rewards.as_ref().unwrap().0; + let stake_rewards = &calculated_rewards.as_ref().unwrap().1; + + let total_vote_rewards: u64 = vote_rewards + .rewards + .iter() + .map(|reward| reward.1.lamports) + .sum::() as u64; + + // assert that total rewards matches the sum of vote rewards and stake rewards + assert_eq!( + stake_rewards.total_stake_rewards_lamports + total_vote_rewards, + expected_rewards + ); + + // assert that number of stake rewards matches + assert_eq!(stake_rewards.stake_rewards.len(), expected_num_delegations); + } + + #[test] + fn test_rewards_point_calculation() { + solana_logger::setup(); + + let expected_num_delegations = 100; + let (bank, _, _) = create_reward_bank(expected_num_delegations); + + 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>> = bank.stakes_cache.stakes(); + let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); + + let point_value = bank.calculate_reward_points_partitioned( + &reward_calculate_param, + 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() { + solana_logger::setup(); + + // bank with no rewards to distribute + let (genesis_config, _mint_keypair) = create_genesis_config(sol_to_lamports(1.0)); + 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>> = bank.stakes_cache.stakes(); + let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); + + let point_value = bank.calculate_reward_points_partitioned( + &reward_calculate_param, + expected_rewards, + &thread_pool, + &rewards_metrics, + ); + + assert!(point_value.is_none()); + } + + #[test] + fn test_calculate_stake_vote_rewards() { + solana_logger::setup(); + + let expected_num_delegations = 1; + let (bank, voters, stakers) = create_reward_bank(expected_num_delegations); + + let vote_pubkey = voters.first().unwrap(); + let mut vote_account = bank + .load_slow_with_fixed_root(&bank.ancestors, vote_pubkey) + .unwrap() + .0; + + 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, // lamports to split + points: 1000, // over these points + }; + let tracer = |_event: &RewardCalculationEvent| {}; + let reward_calc_tracer = Some(tracer); + let rewarded_epoch = bank.epoch(); + let stakes: RwLockReadGuard>> = bank.stakes_cache.stakes(); + let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); + let (vote_rewards_accounts, stake_reward_calculation) = bank.calculate_stake_vote_rewards( + &reward_calculate_param, + rewarded_epoch, + point_value, + &thread_pool, + reward_calc_tracer, + &mut rewards_metrics, + ); + + assert_eq!( + vote_rewards_accounts.rewards.len(), + vote_rewards_accounts.accounts_to_store.len() + ); + assert_eq!(vote_rewards_accounts.rewards.len(), 1); + let rewards = &vote_rewards_accounts.rewards[0]; + let account = &vote_rewards_accounts.accounts_to_store[0]; + let vote_rewards = 0; + let commision = 0; + _ = vote_account.checked_add_lamports(vote_rewards); + assert_eq!( + account.as_ref().unwrap().lamports(), + vote_account.lamports() + ); + assert!(accounts_equal(account.as_ref().unwrap(), &vote_account)); + assert_eq!( + rewards.1, + RewardInfo { + reward_type: RewardType::Voting, + lamports: vote_rewards as i64, + post_balance: vote_account.lamports(), + commission: Some(commision), + } + ); + assert_eq!(&rewards.0, vote_pubkey); + + assert_eq!(stake_reward_calculation.stake_rewards.len(), 1); + assert_eq!( + &stake_reward_calculation.stake_rewards[0].stake_pubkey, + stake_pubkey + ); + + let original_stake_lamport = stake_account.lamports(); + let rewards = stake_reward_calculation.stake_rewards[0] + .stake_reward_info + .lamports; + let expected_reward_info = RewardInfo { + reward_type: RewardType::Staking, + lamports: rewards, + post_balance: original_stake_lamport + rewards as u64, + commission: Some(commision), + }; + assert_eq!( + stake_reward_calculation.stake_rewards[0].stake_reward_info, + expected_reward_info, + ); + } +} diff --git a/runtime/src/bank/partitioned_epoch_rewards/compare.rs b/runtime/src/bank/partitioned_epoch_rewards/compare.rs new file mode 100644 index 00000000000..4ff4d67fdcd --- /dev/null +++ b/runtime/src/bank/partitioned_epoch_rewards/compare.rs @@ -0,0 +1,110 @@ +use { + super::{Bank, PartitionedRewardsCalculation}, + crate::bank::{RewardCalcTracer, RewardsMetrics, VoteReward}, + dashmap::DashMap, + log::info, + rayon::ThreadPool, + solana_accounts_db::stake_rewards::StakeReward, + solana_sdk::{ + account::{AccountSharedData, WritableAccount}, + clock::Epoch, + pubkey::Pubkey, + reward_info::RewardInfo, + }, + std::collections::HashMap, +}; + +impl Bank { + /// compare the vote and stake accounts between the normal rewards calculation code + /// and the partitioned rewards calculation code + /// `stake_rewards_expected` and `vote_rewards_expected` are the results of the normal rewards calculation code + /// This fn should have NO side effects. + pub(in crate::bank) fn compare_with_partitioned_rewards( + &self, + stake_rewards_expected: &[StakeReward], + vote_rewards_expected: &DashMap, + rewarded_epoch: Epoch, + thread_pool: &ThreadPool, + reward_calc_tracer: Option, + ) { + let partitioned_rewards = self.calculate_rewards_for_partitioning( + rewarded_epoch, + reward_calc_tracer, + thread_pool, + &mut RewardsMetrics::default(), + ); + Self::compare_with_partitioned_rewards_results( + stake_rewards_expected, + vote_rewards_expected, + partitioned_rewards, + ); + } + + /// compare the vote and stake accounts between the normal rewards calculation code + /// and the partitioned rewards calculation code + /// `stake_rewards_expected` and `vote_rewards_expected` are the results of the normal rewards calculation code + /// This fn should have NO side effects. + /// This fn is only called in tests or with a debug cli arg prior to partitioned rewards feature activation. + fn compare_with_partitioned_rewards_results( + stake_rewards_expected: &[StakeReward], + vote_rewards_expected: &DashMap, + partitioned_rewards: PartitionedRewardsCalculation, + ) { + // put partitioned stake rewards in a hashmap + let mut stake_rewards: HashMap = HashMap::default(); + partitioned_rewards + .stake_rewards_by_partition + .stake_rewards_by_partition + .iter() + .flatten() + .for_each(|stake_reward| { + stake_rewards.insert(stake_reward.stake_pubkey, stake_reward); + }); + + // verify stake rewards match expected + stake_rewards_expected.iter().for_each(|stake_reward| { + let partitioned = stake_rewards.remove(&stake_reward.stake_pubkey).unwrap(); + assert_eq!(partitioned, stake_reward); + }); + assert!(stake_rewards.is_empty(), "{stake_rewards:?}"); + + let mut vote_rewards: HashMap = HashMap::default(); + partitioned_rewards + .vote_account_rewards + .accounts_to_store + .iter() + .enumerate() + .for_each(|(i, account)| { + if let Some(account) = account { + let reward = &partitioned_rewards.vote_account_rewards.rewards[i]; + vote_rewards.insert(reward.0, (reward.1, account.clone())); + } + }); + + // verify vote rewards match expected + vote_rewards_expected.iter().for_each(|entry| { + if entry.value().vote_needs_store { + let partitioned = vote_rewards.remove(entry.key()).unwrap(); + let mut to_store_partitioned = partitioned.1.clone(); + to_store_partitioned.set_lamports(partitioned.0.post_balance); + let mut to_store_normal = entry.value().vote_account.clone(); + _ = to_store_normal.checked_add_lamports(entry.value().vote_rewards); + assert_eq!(to_store_partitioned, to_store_normal, "{:?}", entry.key()); + } + }); + assert!(vote_rewards.is_empty(), "{vote_rewards:?}"); + info!( + "verified partitioned rewards calculation matching: {}, {}", + partitioned_rewards + .stake_rewards_by_partition + .stake_rewards_by_partition + .iter() + .map(|rewards| rewards.len()) + .sum::(), + partitioned_rewards + .vote_account_rewards + .accounts_to_store + .len() + ); + } +} diff --git a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs new file mode 100644 index 00000000000..6975119a8bb --- /dev/null +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -0,0 +1,396 @@ +use { + super::{Bank, EpochRewardStatus}, + crate::bank::metrics::{report_partitioned_reward_metrics, RewardsStoreMetrics}, + solana_accounts_db::stake_rewards::StakeReward, + solana_measure::measure_us, + solana_sdk::account::ReadableAccount, + std::sync::atomic::Ordering::Relaxed, +}; + +impl Bank { + /// Process reward distribution for the block if it is inside reward interval. + pub(in crate::bank) fn distribute_partitioned_epoch_rewards(&mut self) { + let EpochRewardStatus::Active(status) = &self.epoch_reward_status else { + return; + }; + + let height = self.block_height(); + let start_block_height = status.start_block_height; + let credit_start = start_block_height + self.get_reward_calculation_num_blocks(); + let credit_end_exclusive = credit_start + status.stake_rewards_by_partition.len() as u64; + assert!( + self.epoch_schedule.get_slots_in_epoch(self.epoch) + > credit_end_exclusive.saturating_sub(credit_start) + ); + + if height >= credit_start && height < credit_end_exclusive { + let partition_index = height - credit_start; + self.distribute_epoch_rewards_in_partition( + &status.stake_rewards_by_partition, + partition_index, + ); + } + + if height.saturating_add(1) >= credit_end_exclusive { + datapoint_info!( + "epoch-rewards-status-update", + ("slot", self.slot(), i64), + ("block_height", height, i64), + ("active", 0, i64), + ("start_block_height", start_block_height, i64), + ); + + assert!(matches!( + self.epoch_reward_status, + EpochRewardStatus::Active(_) + )); + self.epoch_reward_status = EpochRewardStatus::Inactive; + self.destroy_epoch_rewards_sysvar(); + } + } + + /// Process reward credits for a partition of rewards + /// Store the rewards to AccountsDB, update reward history record and total capitalization. + fn distribute_epoch_rewards_in_partition( + &self, + all_stake_rewards: &[Vec], + partition_index: u64, + ) { + let pre_capitalization = self.capitalization(); + let this_partition_stake_rewards = &all_stake_rewards[partition_index as usize]; + + let (total_rewards_in_lamports, store_stake_accounts_us) = + measure_us!(self.store_stake_accounts_in_partition(this_partition_stake_rewards)); + + // increase total capitalization by the distributed rewards + self.capitalization + .fetch_add(total_rewards_in_lamports, Relaxed); + + // decrease distributed capital from epoch rewards sysvar + self.update_epoch_rewards_sysvar(total_rewards_in_lamports); + + // update reward history for this partitioned distribution + self.update_reward_history_in_partition(this_partition_stake_rewards); + + let metrics = RewardsStoreMetrics { + pre_capitalization, + post_capitalization: self.capitalization(), + total_stake_accounts_count: all_stake_rewards.len(), + partition_index, + store_stake_accounts_us, + store_stake_accounts_count: this_partition_stake_rewards.len(), + distributed_rewards: total_rewards_in_lamports, + }; + + report_partitioned_reward_metrics(self, metrics); + } + + /// insert non-zero stake rewards to self.rewards + /// Return the number of rewards inserted + fn update_reward_history_in_partition(&self, stake_rewards: &[StakeReward]) -> usize { + let mut rewards = self.rewards.write().unwrap(); + rewards.reserve(stake_rewards.len()); + let initial_len = rewards.len(); + stake_rewards + .iter() + .filter(|x| x.get_stake_reward() > 0) + .for_each(|x| rewards.push((x.stake_pubkey, x.stake_reward_info))); + rewards.len().saturating_sub(initial_len) + } + + /// store stake rewards in partition + /// return the sum of all the stored rewards + /// + /// Note: even if staker's reward is 0, the stake account still needs to be stored because + /// credits observed has changed + fn store_stake_accounts_in_partition(&self, stake_rewards: &[StakeReward]) -> u64 { + // Verify that stake account `lamports + reward_amount` matches what we have in the + // rewarded account. This code will have a performance hit - an extra load and compare of + // the stake accounts. This is for debugging. Once we are confident, we can disable the + // check. + const VERIFY_REWARD_LAMPORT: bool = true; + + if VERIFY_REWARD_LAMPORT { + for r in stake_rewards { + let stake_pubkey = r.stake_pubkey; + let reward_amount = r.get_stake_reward(); + let post_stake_account = &r.stake_account; + if let Some(curr_stake_account) = self.get_account_with_fixed_root(&stake_pubkey) { + let pre_lamport = curr_stake_account.lamports(); + let post_lamport = post_stake_account.lamports(); + assert_eq!(pre_lamport + u64::try_from(reward_amount).unwrap(), post_lamport, + "stake account balance has changed since the reward calculation! account: {stake_pubkey}, pre balance: {pre_lamport}, post balance: {post_lamport}, rewards: {reward_amount}"); + } + } + } + + self.store_accounts((self.slot(), stake_rewards)); + stake_rewards + .iter() + .map(|stake_reward| stake_reward.stake_reward_info.lamports) + .sum::() as u64 + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::bank::{ + partitioned_epoch_rewards::epoch_rewards_hasher::hash_rewards_into_partitions, + tests::create_genesis_config, + }, + rand::Rng, + solana_sdk::{ + account::from_account, epoch_schedule::EpochSchedule, feature_set, hash::Hash, + native_token::LAMPORTS_PER_SOL, sysvar, + }, + }; + + #[test] + fn test_distribute_partitioned_epoch_rewards() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let mut bank = Bank::new_for_tests(&genesis_config); + + let expected_num = 100; + + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + let stake_rewards = hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 2); + + bank.set_epoch_reward_status_active(stake_rewards); + + bank.distribute_partitioned_epoch_rewards(); + } + + #[test] + #[should_panic(expected = "self.epoch_schedule.get_slots_in_epoch")] + fn test_distribute_partitioned_epoch_rewards_too_many_partitions() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let mut bank = Bank::new_for_tests(&genesis_config); + + let expected_num = 1; + + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + let stake_rewards = hash_rewards_into_partitions( + stake_rewards, + &Hash::new(&[1; 32]), + bank.epoch_schedule().slots_per_epoch as usize + 1, + ); + + bank.set_epoch_reward_status_active(stake_rewards); + + bank.distribute_partitioned_epoch_rewards(); + } + + #[test] + fn test_distribute_partitioned_epoch_rewards_empty() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let mut bank = Bank::new_for_tests(&genesis_config); + + bank.set_epoch_reward_status_active(vec![]); + + bank.distribute_partitioned_epoch_rewards(); + } + + /// Test distribute partitioned epoch rewards + #[test] + fn test_distribute_partitioned_epoch_rewards_bank_capital_and_sysvar_balance() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); + + // Set up epoch_rewards sysvar with rewards with 1e9 lamports to distribute. + let total_rewards = 1_000_000_000; + bank.create_epoch_rewards_sysvar(total_rewards, 0, 42); + let pre_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + assert_eq!(pre_epoch_rewards_account.lamports(), total_rewards); + + // Set up a partition of rewards to distribute + let expected_num = 100; + let mut stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + let mut rewards_to_distribute = 0; + for stake_reward in &mut stake_rewards { + stake_reward.credit(100); + rewards_to_distribute += 100; + } + let all_rewards = vec![stake_rewards]; + + // Distribute rewards + let pre_cap = bank.capitalization(); + bank.distribute_epoch_rewards_in_partition(&all_rewards, 0); + let post_cap = bank.capitalization(); + let post_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + let expected_epoch_rewards_sysvar_lamports_remaining = + total_rewards - rewards_to_distribute; + + // Assert that epoch rewards sysvar lamports decreases by the distributed rewards + assert_eq!( + post_epoch_rewards_account.lamports(), + expected_epoch_rewards_sysvar_lamports_remaining + ); + + let epoch_rewards: sysvar::epoch_rewards::EpochRewards = + from_account(&post_epoch_rewards_account).unwrap(); + assert_eq!(epoch_rewards.total_rewards, total_rewards); + assert_eq!(epoch_rewards.distributed_rewards, rewards_to_distribute,); + + // Assert that the bank total capital didn't change + assert_eq!(pre_cap, post_cap); + } + + /// Test partitioned credits and reward history updates of epoch rewards do cover all the rewards + /// slice. + #[test] + fn test_epoch_credit_rewards_and_history_update() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + let mut bank = Bank::new_for_tests(&genesis_config); + + // setup the expected number of stake rewards + let expected_num = 12345; + + let mut stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + bank.store_accounts((bank.slot(), &stake_rewards[..])); + + // Simulate rewards + let mut expected_rewards = 0; + for stake_reward in &mut stake_rewards { + stake_reward.credit(1); + expected_rewards += 1; + } + + let stake_rewards_bucket = + hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 100); + bank.set_epoch_reward_status_active(stake_rewards_bucket.clone()); + + // Test partitioned stores + let mut total_rewards = 0; + let mut total_num_updates = 0; + + let pre_update_history_len = bank.rewards.read().unwrap().len(); + + for stake_rewards in stake_rewards_bucket { + let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); + let num_history_updates = bank.update_reward_history_in_partition(&stake_rewards); + assert_eq!(stake_rewards.len(), num_history_updates); + total_rewards += total_rewards_in_lamports; + total_num_updates += num_history_updates; + } + + let post_update_history_len = bank.rewards.read().unwrap().len(); + + // assert that all rewards are credited + assert_eq!(total_rewards, expected_rewards); + assert_eq!(total_num_updates, expected_num); + assert_eq!( + total_num_updates, + post_update_history_len - pre_update_history_len + ); + } + + #[test] + fn test_update_reward_history_in_partition() { + for zero_reward in [false, true] { + let (genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + + let mut expected_num = 100; + + let mut stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + let mut rng = rand::thread_rng(); + let i_zero = rng.gen_range(0..expected_num); + if zero_reward { + // pick one entry to have zero rewards so it gets ignored + stake_rewards[i_zero].stake_reward_info.lamports = 0; + } + + let num_in_history = bank.update_reward_history_in_partition(&stake_rewards); + + if zero_reward { + stake_rewards.remove(i_zero); + // -1 because one of them had zero rewards and was ignored + expected_num -= 1; + } + + bank.rewards + .read() + .unwrap() + .iter() + .zip(stake_rewards.iter()) + .for_each(|((k, reward_info), expected_stake_reward)| { + assert_eq!( + ( + &expected_stake_reward.stake_pubkey, + &expected_stake_reward.stake_reward_info + ), + (k, reward_info) + ); + }); + + assert_eq!(num_in_history, expected_num); + } + } + + #[test] + fn test_update_reward_history_in_partition_empty() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + + let stake_rewards = vec![]; + + let num_in_history = bank.update_reward_history_in_partition(&stake_rewards); + assert_eq!(num_in_history, 0); + } + + /// Test rewards computation and partitioned rewards distribution at the epoch boundary + #[test] + fn test_store_stake_accounts_in_partition() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + + let expected_num = 100; + + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + let expected_total = stake_rewards + .iter() + .map(|stake_reward| stake_reward.stake_reward_info.lamports) + .sum::() as u64; + + let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); + assert_eq!(expected_total, total_rewards_in_lamports); + } + + #[test] + fn test_store_stake_accounts_in_partition_empty() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + + let stake_rewards = vec![]; + + let expected_total = 0; + + let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); + assert_eq!(expected_total, total_rewards_in_lamports); + } +} diff --git a/runtime/src/epoch_rewards_hasher.rs b/runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs similarity index 68% rename from runtime/src/epoch_rewards_hasher.rs rename to runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs index b594b05a5cf..4495a59dca9 100644 --- a/runtime/src/epoch_rewards_hasher.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs @@ -1,9 +1,9 @@ use { - crate::bank::StakeRewards, + crate::bank::partitioned_epoch_rewards::StakeRewards, solana_sdk::{epoch_rewards_hasher::EpochRewardsHasher, hash::Hash}, }; -pub(crate) fn hash_rewards_into_partitions( +pub(in crate::bank::partitioned_epoch_rewards) fn hash_rewards_into_partitions( stake_rewards: StakeRewards, parent_blockhash: &Hash, num_partitions: usize, @@ -25,7 +25,13 @@ pub(crate) fn hash_rewards_into_partitions( #[cfg(test)] mod tests { - use {super::*, solana_accounts_db::stake_rewards::StakeReward, std::collections::HashMap}; + use { + super::*, + crate::bank::{tests::create_genesis_config, Bank}, + solana_accounts_db::stake_rewards::StakeReward, + solana_sdk::{epoch_schedule::EpochSchedule, native_token::LAMPORTS_PER_SOL}, + std::collections::HashMap, + }; #[test] fn test_hash_rewards_into_partitions() { @@ -85,6 +91,30 @@ mod tests { } } + /// Test that reward partition range panics when passing out of range partition index + #[test] + #[should_panic(expected = "index out of bounds: the len is 10 but the index is 15")] + fn test_get_stake_rewards_partition_range_panic() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + let mut bank = Bank::new_for_tests(&genesis_config); + + // simulate 40K - 1 rewards, the expected num of credit blocks should be 10. + let expected_num = 40959; + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + let stake_rewards_bucket = + hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 10); + + bank.set_epoch_reward_status_active(stake_rewards_bucket.clone()); + + // This call should panic, i.e. 15 is out of the num_credit_blocks + let _range = &stake_rewards_bucket[15]; + } + fn compare(a: &StakeRewards, b: &StakeRewards) { let mut a = a .iter() diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs new file mode 100644 index 00000000000..3af20f0ddc1 --- /dev/null +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -0,0 +1,653 @@ +mod calculation; +mod compare; +mod distribution; +mod epoch_rewards_hasher; +mod sysvar; + +use { + super::Bank, + crate::{stake_account::StakeAccount, stake_history::StakeHistory}, + solana_accounts_db::{ + partitioned_rewards::PartitionedEpochRewardsConfig, stake_rewards::StakeReward, + }, + solana_sdk::{ + account::AccountSharedData, clock::Slot, feature_set, pubkey::Pubkey, + reward_info::RewardInfo, stake::state::Delegation, + }, + solana_vote::vote_account::VoteAccounts, + std::sync::Arc, +}; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(super) enum RewardInterval { + /// the slot within the epoch is INSIDE the reward distribution interval + InsideInterval, + /// the slot within the epoch is OUTSIDE the reward distribution interval + OutsideInterval, +} + +#[derive(AbiExample, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub(crate) struct StartBlockHeightAndRewards { + /// the block height of the slot at which rewards distribution began + pub(crate) start_block_height: u64, + /// calculated epoch rewards pending distribution, outer Vec is by partition (one partition per block) + pub(crate) stake_rewards_by_partition: Arc>, +} + +/// Represent whether bank is in the reward phase or not. +#[derive(AbiExample, AbiEnumVisitor, Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum EpochRewardStatus { + /// this bank is in the reward phase. + /// Contents are the start point for epoch reward calculation, + /// i.e. parent_slot and parent_block height for the starting + /// block of the current epoch. + Active(StartBlockHeightAndRewards), + /// this bank is outside of the rewarding phase. + #[default] + Inactive, +} + +#[derive(Debug, Default)] +pub(super) struct VoteRewardsAccounts { + /// reward info for each vote account pubkey. + /// This type is used by `update_reward_history()` + pub(super) rewards: Vec<(Pubkey, RewardInfo)>, + /// corresponds to pubkey in `rewards` + /// Some if account is to be stored. + /// None if to be skipped. + pub(super) accounts_to_store: Vec>, +} + +/// hold reward calc info to avoid recalculation across functions +pub(super) struct EpochRewardCalculateParamInfo<'a> { + pub(super) stake_history: StakeHistory, + pub(super) stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, + pub(super) cached_vote_accounts: &'a VoteAccounts, +} + +/// Hold all results from calculating the rewards for partitioned distribution. +/// This struct exists so we can have a function which does all the calculation with no +/// side effects. +pub(super) struct PartitionedRewardsCalculation { + pub(super) vote_account_rewards: VoteRewardsAccounts, + pub(super) stake_rewards_by_partition: StakeRewardCalculationPartitioned, + pub(super) old_vote_balance_and_staked: u64, + pub(super) validator_rewards: u64, + pub(super) validator_rate: f64, + pub(super) foundation_rate: f64, + pub(super) prev_epoch_duration_in_years: f64, + pub(super) capitalization: u64, +} + +/// result of calculating the stake rewards at beginning of new epoch +pub(super) struct StakeRewardCalculationPartitioned { + /// each individual stake account to reward, grouped by partition + pub(super) stake_rewards_by_partition: Vec, + /// total lamports across all `stake_rewards` + pub(super) total_stake_rewards_lamports: u64, +} + +pub(super) struct CalculateRewardsAndDistributeVoteRewardsResult { + /// total rewards for the epoch (including both vote rewards and stake rewards) + pub(super) total_rewards: u64, + /// distributed vote rewards + pub(super) distributed_rewards: u64, + /// stake rewards that still need to be distributed, grouped by partition + pub(super) stake_rewards_by_partition: Vec, +} + +pub(crate) type StakeRewards = Vec; + +impl Bank { + pub(super) fn is_partitioned_rewards_feature_enabled(&self) -> bool { + self.feature_set + .is_active(&feature_set::enable_partitioned_epoch_reward::id()) + } + + pub(crate) fn set_epoch_reward_status_active( + &mut self, + stake_rewards_by_partition: Vec, + ) { + self.epoch_reward_status = EpochRewardStatus::Active(StartBlockHeightAndRewards { + start_block_height: self.block_height, + stake_rewards_by_partition: Arc::new(stake_rewards_by_partition), + }); + } + + pub(super) fn partitioned_epoch_rewards_config(&self) -> &PartitionedEpochRewardsConfig { + &self + .rc + .accounts + .accounts_db + .partitioned_epoch_rewards_config + } + + /// # stake accounts to store in one block during partitioned reward interval + pub(super) fn partitioned_rewards_stake_account_stores_per_block(&self) -> u64 { + self.partitioned_epoch_rewards_config() + .stake_account_stores_per_block + } + + /// reward calculation happens synchronously during the first block of the epoch boundary. + /// So, # blocks for reward calculation is 1. + pub(super) fn get_reward_calculation_num_blocks(&self) -> Slot { + self.partitioned_epoch_rewards_config() + .reward_calculation_num_blocks + } + + /// Calculate the number of blocks required to distribute rewards to all stake accounts. + pub(super) fn get_reward_distribution_num_blocks(&self, rewards: &StakeRewards) -> u64 { + let total_stake_accounts = rewards.len(); + if self.epoch_schedule.warmup && self.epoch < self.first_normal_epoch() { + 1 + } else { + const MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH: u64 = 10; + let num_chunks = solana_accounts_db::accounts_hash::AccountsHasher::div_ceil( + total_stake_accounts, + self.partitioned_rewards_stake_account_stores_per_block() as usize, + ) as u64; + + // Limit the reward credit interval to 10% of the total number of slots in a epoch + num_chunks.clamp( + 1, + (self.epoch_schedule.slots_per_epoch / MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH).max(1), + ) + } + } + + /// Return `RewardInterval` enum for current bank + pub(super) fn get_reward_interval(&self) -> RewardInterval { + if matches!(self.epoch_reward_status, EpochRewardStatus::Active(_)) { + RewardInterval::InsideInterval + } else { + RewardInterval::OutsideInterval + } + } + + /// true if it is ok to run partitioned rewards code. + /// This means the feature is activated or certain testing situations. + pub(super) fn is_partitioned_rewards_code_enabled(&self) -> bool { + self.is_partitioned_rewards_feature_enabled() + || self + .partitioned_epoch_rewards_config() + .test_enable_partitioned_rewards + } + + /// For testing only + pub fn force_reward_interval_end_for_tests(&mut self) { + self.epoch_reward_status = EpochRewardStatus::Inactive; + } + + pub(super) fn force_partition_rewards_in_first_block_of_epoch(&self) -> bool { + self.partitioned_epoch_rewards_config() + .test_enable_partitioned_rewards + && self.get_reward_calculation_num_blocks() == 0 + && self.partitioned_rewards_stake_account_stores_per_block() == u64::MAX + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + bank::tests::{create_genesis_config, new_bank_from_parent_with_bank_forks}, + genesis_utils::{ + create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs, + }, + }, + assert_matches::assert_matches, + solana_accounts_db::{ + accounts_db::{ + AccountShrinkThreshold, AccountsDbConfig, ACCOUNTS_DB_CONFIG_FOR_TESTING, + }, + accounts_index::AccountSecondaryIndexes, + partitioned_rewards::TestPartitionedEpochRewards, + }, + solana_program_runtime::runtime_config::RuntimeConfig, + solana_sdk::{ + epoch_schedule::EpochSchedule, + native_token::LAMPORTS_PER_SOL, + signature::Signer, + system_transaction, + vote::state::{VoteStateVersions, MAX_LOCKOUT_HISTORY}, + }, + solana_vote_program::{vote_state, vote_transaction}, + }; + + impl Bank { + /// Return the total number of blocks in reward interval (including both calculation and crediting). + pub(in crate::bank) fn get_reward_total_num_blocks(&self, rewards: &StakeRewards) -> u64 { + self.get_reward_calculation_num_blocks() + + self.get_reward_distribution_num_blocks(rewards) + } + } + + #[test] + fn test_force_reward_interval_end() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + let mut bank = Bank::new_for_tests(&genesis_config); + + let expected_num = 100; + + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + bank.set_epoch_reward_status_active(vec![stake_rewards]); + assert!(bank.get_reward_interval() == RewardInterval::InsideInterval); + + bank.force_reward_interval_end_for_tests(); + assert!(bank.get_reward_interval() == RewardInterval::OutsideInterval); + } + + #[test] + fn test_is_partitioned_reward_feature_enable() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + + let mut bank = Bank::new_for_tests(&genesis_config); + assert!(!bank.is_partitioned_rewards_feature_enabled()); + bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); + assert!(bank.is_partitioned_rewards_feature_enabled()); + } + + /// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during small epoch + /// The num_credit_blocks should be cap to 10% of the total number of blocks in the epoch. + #[test] + fn test_get_reward_distribution_num_blocks_cap() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(32, 32, false); + + // Config stake reward distribution to be 10 per block + let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING.clone(); + accounts_db_config.test_partitioned_epoch_rewards = + TestPartitionedEpochRewards::PartitionedEpochRewardsConfigRewardBlocks { + reward_calculation_num_blocks: 1, + stake_account_stores_per_block: 10, + }; + + let bank = Bank::new_with_paths( + &genesis_config, + Arc::new(RuntimeConfig::default()), + Vec::new(), + None, + None, + AccountSecondaryIndexes::default(), + AccountShrinkThreshold::default(), + false, + Some(accounts_db_config), + None, + Some(Pubkey::new_unique()), + Arc::default(), + ); + + let stake_account_stores_per_block = + bank.partitioned_rewards_stake_account_stores_per_block(); + assert_eq!(stake_account_stores_per_block, 10); + + let check_num_reward_distribution_blocks = + |num_stakes: u64, + expected_num_reward_distribution_blocks: u64, + expected_num_reward_computation_blocks: u64| { + // Given the short epoch, i.e. 32 slots, we should cap the number of reward distribution blocks to 32/10 = 3. + let stake_rewards = (0..num_stakes) + .map(|_| StakeReward::new_random()) + .collect::>(); + + assert_eq!( + bank.get_reward_distribution_num_blocks(&stake_rewards), + expected_num_reward_distribution_blocks + ); + assert_eq!( + bank.get_reward_calculation_num_blocks(), + expected_num_reward_computation_blocks + ); + assert_eq!( + bank.get_reward_total_num_blocks(&stake_rewards), + bank.get_reward_distribution_num_blocks(&stake_rewards) + + bank.get_reward_calculation_num_blocks(), + ); + }; + + for test_record in [ + // num_stakes, expected_num_reward_distribution_blocks, expected_num_reward_computation_blocks + (0, 1, 1), + (1, 1, 1), + (stake_account_stores_per_block, 1, 1), + (2 * stake_account_stores_per_block - 1, 2, 1), + (2 * stake_account_stores_per_block, 2, 1), + (3 * stake_account_stores_per_block - 1, 3, 1), + (3 * stake_account_stores_per_block, 3, 1), + (4 * stake_account_stores_per_block, 3, 1), // cap at 3 + (5 * stake_account_stores_per_block, 3, 1), //cap at 3 + ] { + check_num_reward_distribution_blocks(test_record.0, test_record.1, test_record.2); + } + } + + /// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during normal epoch gives the expected result + #[test] + fn test_get_reward_distribution_num_blocks_normal() { + solana_logger::setup(); + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + + let bank = Bank::new_for_tests(&genesis_config); + + // Given 8k rewards, it will take 2 blocks to credit all the rewards + let expected_num = 8192; + let stake_rewards = (0..expected_num) + .map(|_| StakeReward::new_random()) + .collect::>(); + + assert_eq!(bank.get_reward_distribution_num_blocks(&stake_rewards), 2); + assert_eq!(bank.get_reward_calculation_num_blocks(), 1); + assert_eq!( + bank.get_reward_total_num_blocks(&stake_rewards), + bank.get_reward_distribution_num_blocks(&stake_rewards) + + bank.get_reward_calculation_num_blocks(), + ); + } + + /// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during warm up epoch gives the expected result. + /// The num_credit_blocks should be 1 during warm up epoch. + #[test] + fn test_get_reward_distribution_num_blocks_warmup() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + + let bank = Bank::new_for_tests(&genesis_config); + let rewards = vec![]; + assert_eq!(bank.get_reward_distribution_num_blocks(&rewards), 1); + assert_eq!(bank.get_reward_calculation_num_blocks(), 1); + assert_eq!( + bank.get_reward_total_num_blocks(&rewards), + bank.get_reward_distribution_num_blocks(&rewards) + + bank.get_reward_calculation_num_blocks(), + ); + } + + #[test] + fn test_rewards_computation_and_partitioned_distribution_one_block() { + solana_logger::setup(); + + // setup the expected number of stake delegations + let expected_num_delegations = 100; + + let validator_keypairs = (0..expected_num_delegations) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![2_000_000_000; expected_num_delegations], + ); + + let bank0 = Bank::new_for_tests(&genesis_config); + let num_slots_in_epoch = bank0.get_slots_in_epoch(bank0.epoch()); + assert_eq!(num_slots_in_epoch, 32); + + let mut previous_bank = Arc::new(Bank::new_from_parent( + Arc::new(bank0), + &Pubkey::default(), + 1, + )); + + // simulate block progress + for slot in 2..=num_slots_in_epoch + 2 { + let pre_cap = previous_bank.capitalization(); + let curr_bank = Bank::new_from_parent(previous_bank, &Pubkey::default(), slot); + let post_cap = curr_bank.capitalization(); + + // Fill banks with banks with votes landing in the next slot + // Create enough banks such that vote account will root + for validator_vote_keypairs in validator_keypairs.iter() { + let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); + let mut vote_account = curr_bank.get_account(&vote_id).unwrap(); + // generate some rewards + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); + for i in 0..MAX_LOCKOUT_HISTORY + 42 { + if let Some(v) = vote_state.as_mut() { + vote_state::process_slot_vote_unchecked(v, i as u64) + } + let versioned = + VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + vote_state::to(&versioned, &mut vote_account).unwrap(); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; + } + curr_bank.store_account_and_update_capitalization(&vote_id, &vote_account); + } + + if slot == num_slots_in_epoch { + // This is the first block of epoch 1. Reward computation should happen in this block. + // assert reward compute status activated at epoch boundary + assert_matches!( + curr_bank.get_reward_interval(), + RewardInterval::InsideInterval + ); + + // cap should increase because of new epoch rewards + assert!(post_cap > pre_cap); + } else if slot == num_slots_in_epoch + 1 || slot == num_slots_in_epoch + 2 { + // 1. when curr_slot == num_slots_in_epoch + 1, the 2nd block of epoch 1, reward distribution should happen in this block. + // however, all stake rewards are paid at the this block therefore reward_status should have transitioned to inactive. And since + // rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. + // 2. when curr_slot == num_slots_in_epoch+2, the 3rd block of epoch 1. reward distribution should have already completed. Therefore, + // reward_status should stay inactive and cap should stay the same. + assert_matches!( + curr_bank.get_reward_interval(), + RewardInterval::OutsideInterval + ); + + assert_eq!(post_cap, pre_cap); + } else { + // slot is not in rewards, cap should not change + assert_eq!(post_cap, pre_cap); + } + previous_bank = Arc::new(curr_bank); + } + } + + /// Test rewards computation and partitioned rewards distribution at the epoch boundary (two reward distribution blocks) + #[test] + fn test_rewards_computation_and_partitioned_distribution_two_blocks() { + solana_logger::setup(); + + // Set up the expected number of stake delegations 100 + let expected_num_delegations = 100; + + let validator_keypairs = (0..expected_num_delegations) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![2_000_000_000; expected_num_delegations], + ); + genesis_config.epoch_schedule = EpochSchedule::custom(32, 32, false); + + // Config stake reward distribution to be 50 per block + // We will need two blocks for reward distribution. And we can assert that the expected bank + // capital changes before/during/after reward distribution. + let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING.clone(); + accounts_db_config.test_partitioned_epoch_rewards = + TestPartitionedEpochRewards::PartitionedEpochRewardsConfigRewardBlocks { + reward_calculation_num_blocks: 1, + stake_account_stores_per_block: 50, + }; + + let bank0 = Bank::new_with_paths( + &genesis_config, + Arc::new(RuntimeConfig::default()), + Vec::new(), + None, + None, + AccountSecondaryIndexes::default(), + AccountShrinkThreshold::default(), + false, + Some(accounts_db_config), + None, + None, + Arc::default(), + ); + + let num_slots_in_epoch = bank0.get_slots_in_epoch(bank0.epoch()); + assert_eq!(num_slots_in_epoch, 32); + + let mut previous_bank = Arc::new(Bank::new_from_parent( + Arc::new(bank0), + &Pubkey::default(), + 1, + )); + + // simulate block progress + for slot in 2..=num_slots_in_epoch + 3 { + let pre_cap = previous_bank.capitalization(); + let curr_bank = Bank::new_from_parent(previous_bank, &Pubkey::default(), slot); + let post_cap = curr_bank.capitalization(); + + // Fill banks with banks with votes landing in the next slot + // Create enough banks such that vote account will root + for validator_vote_keypairs in validator_keypairs.iter() { + let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); + let mut vote_account = curr_bank.get_account(&vote_id).unwrap(); + // generate some rewards + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); + for i in 0..MAX_LOCKOUT_HISTORY + 42 { + if let Some(v) = vote_state.as_mut() { + vote_state::process_slot_vote_unchecked(v, i as u64) + } + let versioned = + VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + vote_state::to(&versioned, &mut vote_account).unwrap(); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; + } + curr_bank.store_account_and_update_capitalization(&vote_id, &vote_account); + } + + if slot == num_slots_in_epoch { + // This is the first block of epoch 1. Reward computation should happen in this block. + // assert reward compute status activated at epoch boundary + assert_matches!( + curr_bank.get_reward_interval(), + RewardInterval::InsideInterval + ); + + // cap should increase because of new epoch rewards + assert!(post_cap > pre_cap); + } else if slot == num_slots_in_epoch + 1 { + // When curr_slot == num_slots_in_epoch + 1, the 2nd block of epoch 1, reward distribution should happen in this block. + // however, since rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. + assert_matches!( + curr_bank.get_reward_interval(), + RewardInterval::InsideInterval + ); + + assert_eq!(post_cap, pre_cap); + } else if slot == num_slots_in_epoch + 2 || slot == num_slots_in_epoch + 3 { + // 1. when curr_slot == num_slots_in_epoch + 2, the 3nd block of epoch 1, reward distribution should happen in this block. + // however, all stake rewards are paid at the this block therefore reward_status should have transitioned to inactive. And since + // rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. + // 2. when curr_slot == num_slots_in_epoch+2, the 3rd block of epoch 1. reward distribution should have already completed. Therefore, + // reward_status should stay inactive and cap should stay the same. + assert_matches!( + curr_bank.get_reward_interval(), + RewardInterval::OutsideInterval + ); + + assert_eq!(post_cap, pre_cap); + } else { + // slot is not in rewards, cap should not change + assert_eq!(post_cap, pre_cap); + } + previous_bank = Arc::new(curr_bank); + } + } + + /// Test that program execution that involves stake accounts should fail during reward period. + /// Any programs, which result in stake account changes, will throw `ProgramExecutionTemporarilyRestricted` error when + /// in reward period. + #[test] + fn test_program_execution_restricted_for_stake_account_in_reward_period() { + use solana_sdk::transaction::TransactionError::ProgramExecutionTemporarilyRestricted; + + let validator_vote_keypairs = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = vec![&validator_vote_keypairs]; + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![1_000_000_000; 1], + ); + + let node_key = &validator_keypairs[0].node_keypair; + let stake_key = &validator_keypairs[0].stake_keypair; + + let (mut previous_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let num_slots_in_epoch = previous_bank.get_slots_in_epoch(previous_bank.epoch()); + assert_eq!(num_slots_in_epoch, 32); + + for slot in 1..=num_slots_in_epoch + 2 { + let bank = new_bank_from_parent_with_bank_forks( + bank_forks.as_ref(), + previous_bank.clone(), + &Pubkey::default(), + slot, + ); + + // Fill bank_forks with banks with votes landing in the next slot + // So that rewards will be paid out at the epoch boundary, i.e. slot = 32 + let vote = vote_transaction::new_vote_transaction( + vec![slot - 1], + previous_bank.hash(), + previous_bank.last_blockhash(), + &validator_vote_keypairs.node_keypair, + &validator_vote_keypairs.vote_keypair, + &validator_vote_keypairs.vote_keypair, + None, + ); + bank.process_transaction(&vote).unwrap(); + + // Insert a transfer transaction from node account to stake account + let tx = system_transaction::transfer( + node_key, + &stake_key.pubkey(), + 1, + bank.last_blockhash(), + ); + let r = bank.process_transaction(&tx); + + if slot == num_slots_in_epoch { + // When the bank is at the beginning of the new epoch, i.e. slot 32, + // ProgramExecutionTemporarilyRestricted should be thrown for the transfer transaction. + assert_eq!( + r, + Err(ProgramExecutionTemporarilyRestricted { account_index: 1 }) + ); + } else { + // When the bank is outside of reward interval, the transfer transaction should not be affected and will succeed. + assert!(r.is_ok()); + } + + // Push a dummy blockhash, so that the latest_blockhash() for the transfer transaction in each + // iteration are different. Otherwise, all those transactions will be the same, and will not be + // executed by the bank except the first one. + bank.register_unique_recent_blockhash_for_test(); + previous_bank = bank; + } + } +} diff --git a/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs b/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs new file mode 100644 index 00000000000..23eb5c986c3 --- /dev/null +++ b/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs @@ -0,0 +1,157 @@ +use { + super::Bank, + log::info, + solana_sdk::{ + account::{ + create_account_shared_data_with_fields as create_account, from_account, ReadableAccount, + }, + sysvar, + }, +}; + +impl Bank { + /// Helper fn to log epoch_rewards sysvar + fn log_epoch_rewards_sysvar(&self, prefix: &str) { + if let Some(account) = self.get_account(&sysvar::epoch_rewards::id()) { + let epoch_rewards: sysvar::epoch_rewards::EpochRewards = + from_account(&account).unwrap(); + info!( + "{prefix} epoch_rewards sysvar: {:?}", + (account.lamports(), epoch_rewards) + ); + } else { + info!("{prefix} epoch_rewards sysvar: none"); + } + } + + /// Create EpochRewards sysvar with calculated rewards + pub(in crate::bank) fn create_epoch_rewards_sysvar( + &self, + total_rewards: u64, + distributed_rewards: u64, + distribution_starting_block_height: u64, + ) { + assert!(self.is_partitioned_rewards_code_enabled()); + + let epoch_rewards = sysvar::epoch_rewards::EpochRewards { + total_rewards, + distributed_rewards, + distribution_starting_block_height, + active: true, + ..sysvar::epoch_rewards::EpochRewards::default() + }; + + self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { + let mut inherited_account_fields = + self.inherit_specially_retained_account_fields(account); + + assert!(total_rewards >= distributed_rewards); + // set the account lamports to the undistributed rewards + inherited_account_fields.0 = total_rewards - distributed_rewards; + create_account(&epoch_rewards, inherited_account_fields) + }); + + self.log_epoch_rewards_sysvar("create"); + } + + /// Update EpochRewards sysvar with distributed rewards + pub(in crate::bank::partitioned_epoch_rewards) fn update_epoch_rewards_sysvar( + &self, + distributed: u64, + ) { + assert!(self.is_partitioned_rewards_code_enabled()); + + let mut epoch_rewards: sysvar::epoch_rewards::EpochRewards = + from_account(&self.get_account(&sysvar::epoch_rewards::id()).unwrap()).unwrap(); + epoch_rewards.distribute(distributed); + + self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { + let mut inherited_account_fields = + self.inherit_specially_retained_account_fields(account); + + let lamports = inherited_account_fields.0; + assert!(lamports >= distributed); + inherited_account_fields.0 = lamports - distributed; + create_account(&epoch_rewards, inherited_account_fields) + }); + + self.log_epoch_rewards_sysvar("update"); + } + + pub(in crate::bank::partitioned_epoch_rewards) fn destroy_epoch_rewards_sysvar(&self) { + if let Some(account) = self.get_account(&sysvar::epoch_rewards::id()) { + if account.lamports() > 0 { + info!( + "burning {} extra lamports in EpochRewards sysvar account at slot {}", + account.lamports(), + self.slot() + ); + self.log_epoch_rewards_sysvar("burn"); + self.burn_and_purge_account(&sysvar::epoch_rewards::id(), account); + } + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::bank::tests::create_genesis_config, + solana_sdk::{ + epoch_schedule::EpochSchedule, feature_set, hash::Hash, native_token::LAMPORTS_PER_SOL, + }, + }; + + /// Test `EpochRewards` sysvar creation, distribution, and burning. + /// This test covers the following epoch_rewards_sysvar bank member functions, i.e. + /// `create_epoch_rewards_sysvar`, `update_epoch_rewards_sysvar`, `burn_and_purge_account`. + #[test] + fn test_epoch_rewards_sysvar() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); + + let total_rewards = 1_000_000_000; // a large rewards so that the sysvar account is rent-exempted. + + // create epoch rewards sysvar + let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 0, + parent_blockhash: Hash::default(), + total_points: 0, + total_rewards, + distributed_rewards: 10, + active: true, + }; + + bank.create_epoch_rewards_sysvar(total_rewards, 10, 42); + let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + assert_eq!(account.lamports(), total_rewards - 10); + let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); + assert_eq!(epoch_rewards, expected_epoch_rewards); + + // make a distribution from epoch rewards sysvar + bank.update_epoch_rewards_sysvar(10); + let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + assert_eq!(account.lamports(), total_rewards - 20); + let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); + let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 0, + parent_blockhash: Hash::default(), + total_points: 0, + total_rewards, + distributed_rewards: 20, + active: true, + }; + assert_eq!(epoch_rewards, expected_epoch_rewards); + + // burn epoch rewards sysvar + bank.burn_and_purge_account(&sysvar::epoch_rewards::id(), account); + let account = bank.get_account(&sysvar::epoch_rewards::id()); + assert!(account.is_none()); + } +} diff --git a/runtime/src/bank/serde_snapshot.rs b/runtime/src/bank/serde_snapshot.rs index 372baec2e4a..2ddaf8adf5b 100644 --- a/runtime/src/bank/serde_snapshot.rs +++ b/runtime/src/bank/serde_snapshot.rs @@ -3,8 +3,8 @@ mod tests { use { crate::{ bank::{ - epoch_accounts_hash_utils, test_utils as bank_test_utils, Bank, EpochRewardStatus, - StartBlockHeightAndRewards, + epoch_accounts_hash_utils, partitioned_epoch_rewards::StartBlockHeightAndRewards, + test_utils as bank_test_utils, Bank, EpochRewardStatus, }, genesis_utils::activate_all_features, serde_snapshot::{ @@ -605,7 +605,7 @@ mod tests { // This some what long test harness is required to freeze the ABI of // Bank's serialization due to versioned nature - #[frozen_abi(digest = "7BH2s2Y1yKy396c3ixC4TTyvvpkyenAvWDSiZvY5yb7P")] + #[frozen_abi(digest = "8BVfyLYrPt1ranknjF4sLePjZaZjpKXXrHt4wKf47g3W")] #[derive(Serialize, AbiExample)] pub struct BankAbiTestWrapperNewer { #[serde(serialize_with = "wrapper_newer")] diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index dc3a3121558..be0e8f62707 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -10,7 +10,6 @@ use { accounts_background_service::{PrunedBanksRequestHandler, SendDroppedBankCallback}, bank_client::BankClient, bank_forks::BankForks, - epoch_rewards_hasher::hash_rewards_into_partitions, genesis_utils::{ self, activate_all_features, activate_feature, bootstrap_validator_stake_lamports, create_genesis_config_with_leader, create_genesis_config_with_vote_accounts, @@ -33,7 +32,6 @@ use { }, accounts_partition::{self, PartitionIndex, RentPayingAccountsByPartition}, ancestors::Ancestors, - partitioned_rewards::TestPartitionedEpochRewards, }, solana_inline_spl::token, solana_logger, @@ -113,7 +111,6 @@ use { vote_state::{ self, BlockTimestamp, Vote, VoteInit, VoteState, VoteStateVersions, MAX_LOCKOUT_HISTORY, }, - vote_transaction, }, std::{ collections::{HashMap, HashSet}, @@ -159,7 +156,7 @@ impl VoteReward { } } -fn new_bank_from_parent_with_bank_forks( +pub(in crate::bank) fn new_bank_from_parent_with_bank_forks( bank_forks: &RwLock, parent: Arc, collector_id: &Pubkey, @@ -191,7 +188,7 @@ fn create_genesis_config_no_tx_fee(lamports: u64) -> (GenesisConfig, Keypair) { (genesis_config, mint_keypair) } -fn create_genesis_config(lamports: u64) -> (GenesisConfig, Keypair) { +pub(in crate::bank) fn create_genesis_config(lamports: u64) -> (GenesisConfig, Keypair) { solana_sdk::genesis_config::create_genesis_config(lamports) } @@ -12126,841 +12123,6 @@ fn test_squash_timing_add_assign() { assert!(t0 == expected); } -/// Helper function to create a bank that pays some rewards -fn create_reward_bank(expected_num_delegations: usize) -> (Bank, Vec, Vec) { - let validator_keypairs = (0..expected_num_delegations) - .map(|_| ValidatorVoteKeypairs::new_rand()) - .collect::>(); - - let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![2_000_000_000; expected_num_delegations], - ); - - let bank = Bank::new_for_tests(&genesis_config); - - // Fill bank_forks with banks with votes landing in the next slot - // Create enough banks such that vote account will root - for validator_vote_keypairs in &validator_keypairs { - let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); - let mut vote_account = bank.get_account(&vote_id).unwrap(); - // generate some rewards - let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); - for i in 0..MAX_LOCKOUT_HISTORY + 42 { - if let Some(v) = vote_state.as_mut() { - vote_state::process_slot_vote_unchecked(v, i as u64) - } - let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - vote_state::to(&versioned, &mut vote_account).unwrap(); - match versioned { - VoteStateVersions::Current(v) => { - vote_state = Some(*v); - } - _ => panic!("Has to be of type Current"), - }; - } - bank.store_account_and_update_capitalization(&vote_id, &vote_account); - } - ( - bank, - validator_keypairs - .iter() - .map(|k| k.vote_keypair.pubkey()) - .collect(), - validator_keypairs - .iter() - .map(|k| k.stake_keypair.pubkey()) - .collect(), - ) -} - -#[test] -fn test_rewards_point_calculation() { - solana_logger::setup(); - - let expected_num_delegations = 100; - let (bank, _, _) = create_reward_bank(expected_num_delegations); - - 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>> = bank.stakes_cache.stakes(); - let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); - - let point_value = bank.calculate_reward_points_partitioned( - &reward_calculate_param, - 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() { - solana_logger::setup(); - - // bank with no rewards to distribute - let (genesis_config, _mint_keypair) = create_genesis_config(sol_to_lamports(1.0)); - 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>> = bank.stakes_cache.stakes(); - let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); - - let point_value = bank.calculate_reward_points_partitioned( - &reward_calculate_param, - expected_rewards, - &thread_pool, - &rewards_metrics, - ); - - assert!(point_value.is_none()); -} - -#[test] -fn test_force_reward_interval_end() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let mut bank = Bank::new_for_tests(&genesis_config); - - let expected_num = 100; - - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - bank.set_epoch_reward_status_active(vec![stake_rewards]); - assert!(bank.get_reward_interval() == RewardInterval::InsideInterval); - - bank.force_reward_interval_end_for_tests(); - assert!(bank.get_reward_interval() == RewardInterval::OutsideInterval); -} - -#[test] -fn test_is_partitioned_reward_feature_enable() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - - let mut bank = Bank::new_for_tests(&genesis_config); - assert!(!bank.is_partitioned_rewards_feature_enabled()); - bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); - assert!(bank.is_partitioned_rewards_feature_enabled()); -} - -/// Test that reward partition range panics when passing out of range partition index -#[test] -#[should_panic(expected = "index out of bounds: the len is 10 but the index is 15")] -fn test_get_stake_rewards_partition_range_panic() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); - let mut bank = Bank::new_for_tests(&genesis_config); - - // simulate 40K - 1 rewards, the expected num of credit blocks should be 10. - let expected_num = 40959; - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - let stake_rewards_bucket = - hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 10); - - bank.set_epoch_reward_status_active(stake_rewards_bucket.clone()); - - // This call should panic, i.e. 15 is out of the num_credit_blocks - let _range = &stake_rewards_bucket[15]; -} - -#[test] -fn test_deactivate_epoch_reward_status() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let mut bank = Bank::new_for_tests(&genesis_config); - - let expected_num = 100; - - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - bank.set_epoch_reward_status_active(vec![stake_rewards]); - - assert!(bank.get_reward_interval() == RewardInterval::InsideInterval); - bank.deactivate_epoch_reward_status(); - assert!(bank.get_reward_interval() == RewardInterval::OutsideInterval); -} - -#[test] -fn test_distribute_partitioned_epoch_rewards() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let mut bank = Bank::new_for_tests(&genesis_config); - - let expected_num = 100; - - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - let stake_rewards = hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 2); - - bank.set_epoch_reward_status_active(stake_rewards); - - bank.distribute_partitioned_epoch_rewards(); -} - -#[test] -#[should_panic(expected = "self.epoch_schedule.get_slots_in_epoch")] -fn test_distribute_partitioned_epoch_rewards_too_many_partitions() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let mut bank = Bank::new_for_tests(&genesis_config); - - let expected_num = 1; - - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - let stake_rewards = hash_rewards_into_partitions( - stake_rewards, - &Hash::new(&[1; 32]), - bank.epoch_schedule().slots_per_epoch as usize + 1, - ); - - bank.set_epoch_reward_status_active(stake_rewards); - - bank.distribute_partitioned_epoch_rewards(); -} - -#[test] -fn test_distribute_partitioned_epoch_rewards_empty() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let mut bank = Bank::new_for_tests(&genesis_config); - - bank.set_epoch_reward_status_active(vec![]); - - bank.distribute_partitioned_epoch_rewards(); -} - -/// Test partitioned credits and reward history updates of epoch rewards do cover all the rewards -/// slice. -#[test] -fn test_epoch_credit_rewards_and_history_update() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); - let mut bank = Bank::new_for_tests(&genesis_config); - - // setup the expected number of stake rewards - let expected_num = 12345; - - let mut stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - bank.store_accounts((bank.slot(), &stake_rewards[..])); - - // Simulate rewards - let mut expected_rewards = 0; - for stake_reward in &mut stake_rewards { - stake_reward.credit(1); - expected_rewards += 1; - } - - let stake_rewards_bucket = - hash_rewards_into_partitions(stake_rewards, &Hash::new(&[1; 32]), 100); - bank.set_epoch_reward_status_active(stake_rewards_bucket.clone()); - - // Test partitioned stores - let mut total_rewards = 0; - let mut total_num_updates = 0; - - let pre_update_history_len = bank.rewards.read().unwrap().len(); - - for stake_rewards in stake_rewards_bucket { - let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); - let num_history_updates = bank.update_reward_history_in_partition(&stake_rewards); - assert_eq!(stake_rewards.len(), num_history_updates); - total_rewards += total_rewards_in_lamports; - total_num_updates += num_history_updates; - } - - let post_update_history_len = bank.rewards.read().unwrap().len(); - - // assert that all rewards are credited - assert_eq!(total_rewards, expected_rewards); - assert_eq!(total_num_updates, expected_num); - assert_eq!( - total_num_updates, - post_update_history_len - pre_update_history_len - ); -} - -/// Test distribute partitioned epoch rewards -#[test] -fn test_distribute_partitioned_epoch_rewards_bank_capital_and_sysvar_balance() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); - let mut bank = Bank::new_for_tests(&genesis_config); - bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); - - // Set up epoch_rewards sysvar with rewards with 1e9 lamports to distribute. - let total_rewards = 1_000_000_000; - bank.create_epoch_rewards_sysvar(total_rewards, 0, 42); - let pre_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - assert_eq!(pre_epoch_rewards_account.lamports(), total_rewards); - - // Set up a partition of rewards to distribute - let expected_num = 100; - let mut stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - let mut rewards_to_distribute = 0; - for stake_reward in &mut stake_rewards { - stake_reward.credit(100); - rewards_to_distribute += 100; - } - let all_rewards = vec![stake_rewards]; - - // Distribute rewards - let pre_cap = bank.capitalization(); - bank.distribute_epoch_rewards_in_partition(&all_rewards, 0); - let post_cap = bank.capitalization(); - let post_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - let expected_epoch_rewards_sysvar_lamports_remaining = total_rewards - rewards_to_distribute; - - // Assert that epoch rewards sysvar lamports decreases by the distributed rewards - assert_eq!( - post_epoch_rewards_account.lamports(), - expected_epoch_rewards_sysvar_lamports_remaining - ); - - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = - from_account(&post_epoch_rewards_account).unwrap(); - assert_eq!(epoch_rewards.total_rewards, total_rewards); - assert_eq!(epoch_rewards.distributed_rewards, rewards_to_distribute,); - - // Assert that the bank total capital didn't change - assert_eq!(pre_cap, post_cap); -} - -#[test] -/// Test rewards computation and partitioned rewards distribution at the epoch boundary -fn test_rewards_computation() { - solana_logger::setup(); - - let expected_num_delegations = 100; - let bank = create_reward_bank(expected_num_delegations).0; - - // Calculate rewards - let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); - let mut rewards_metrics = RewardsMetrics::default(); - let expected_rewards = 100_000_000_000; - - let calculated_rewards = bank.calculate_validator_rewards( - 1, - expected_rewards, - null_tracer(), - &thread_pool, - &mut rewards_metrics, - ); - - let vote_rewards = &calculated_rewards.as_ref().unwrap().0; - let stake_rewards = &calculated_rewards.as_ref().unwrap().1; - - let total_vote_rewards: u64 = vote_rewards - .rewards - .iter() - .map(|reward| reward.1.lamports) - .sum::() as u64; - - // assert that total rewards matches the sum of vote rewards and stake rewards - assert_eq!( - stake_rewards.total_stake_rewards_lamports + total_vote_rewards, - expected_rewards - ); - - // assert that number of stake rewards matches - assert_eq!(stake_rewards.stake_rewards.len(), expected_num_delegations); -} - -/// Test rewards computation and partitioned rewards distribution at the epoch boundary (one reward distribution block) -#[test] -fn test_rewards_computation_and_partitioned_distribution_one_block() { - solana_logger::setup(); - - // setup the expected number of stake delegations - let expected_num_delegations = 100; - - let validator_keypairs = (0..expected_num_delegations) - .map(|_| ValidatorVoteKeypairs::new_rand()) - .collect::>(); - - let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![2_000_000_000; expected_num_delegations], - ); - - let bank0 = Bank::new_for_tests(&genesis_config); - let num_slots_in_epoch = bank0.get_slots_in_epoch(bank0.epoch()); - assert_eq!(num_slots_in_epoch, 32); - - let mut previous_bank = Arc::new(Bank::new_from_parent( - Arc::new(bank0), - &Pubkey::default(), - 1, - )); - - // simulate block progress - for slot in 2..=num_slots_in_epoch + 2 { - let pre_cap = previous_bank.capitalization(); - let curr_bank = Bank::new_from_parent(previous_bank, &Pubkey::default(), slot); - let post_cap = curr_bank.capitalization(); - - // Fill banks with banks with votes landing in the next slot - // Create enough banks such that vote account will root - for validator_vote_keypairs in validator_keypairs.iter() { - let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); - let mut vote_account = curr_bank.get_account(&vote_id).unwrap(); - // generate some rewards - let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); - for i in 0..MAX_LOCKOUT_HISTORY + 42 { - if let Some(v) = vote_state.as_mut() { - vote_state::process_slot_vote_unchecked(v, i as u64) - } - let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - vote_state::to(&versioned, &mut vote_account).unwrap(); - match versioned { - VoteStateVersions::Current(v) => { - vote_state = Some(*v); - } - _ => panic!("Has to be of type Current"), - }; - } - curr_bank.store_account_and_update_capitalization(&vote_id, &vote_account); - } - - if slot == num_slots_in_epoch { - // This is the first block of epoch 1. Reward computation should happen in this block. - // assert reward compute status activated at epoch boundary - assert_matches!( - curr_bank.get_reward_interval(), - RewardInterval::InsideInterval - ); - - // cap should increase because of new epoch rewards - assert!(post_cap > pre_cap); - } else if slot == num_slots_in_epoch + 1 || slot == num_slots_in_epoch + 2 { - // 1. when curr_slot == num_slots_in_epoch + 1, the 2nd block of epoch 1, reward distribution should happen in this block. - // however, all stake rewards are paid at the this block therefore reward_status should have transitioned to inactive. And since - // rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. - // 2. when curr_slot == num_slots_in_epoch+2, the 3rd block of epoch 1. reward distribution should have already completed. Therefore, - // reward_status should stay inactive and cap should stay the same. - assert_matches!( - curr_bank.get_reward_interval(), - RewardInterval::OutsideInterval - ); - - assert_eq!(post_cap, pre_cap); - } else { - // slot is not in rewards, cap should not change - assert_eq!(post_cap, pre_cap); - } - previous_bank = Arc::new(curr_bank); - } -} - -/// Test rewards computation and partitioned rewards distribution at the epoch boundary (two reward distribution blocks) -#[test] -fn test_rewards_computation_and_partitioned_distribution_two_blocks() { - solana_logger::setup(); - - // Set up the expected number of stake delegations 100 - let expected_num_delegations = 100; - - let validator_keypairs = (0..expected_num_delegations) - .map(|_| ValidatorVoteKeypairs::new_rand()) - .collect::>(); - - let GenesisConfigInfo { - mut genesis_config, .. - } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![2_000_000_000; expected_num_delegations], - ); - genesis_config.epoch_schedule = EpochSchedule::custom(32, 32, false); - - // Config stake reward distribution to be 50 per block - // We will need two blocks for reward distribution. And we can assert that the expected bank - // capital changes before/during/after reward distribution. - let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING.clone(); - accounts_db_config.test_partitioned_epoch_rewards = - TestPartitionedEpochRewards::PartitionedEpochRewardsConfigRewardBlocks { - reward_calculation_num_blocks: 1, - stake_account_stores_per_block: 50, - }; - - let bank0 = Bank::new_with_paths( - &genesis_config, - Arc::new(RuntimeConfig::default()), - Vec::new(), - None, - None, - AccountSecondaryIndexes::default(), - AccountShrinkThreshold::default(), - false, - Some(accounts_db_config), - None, - None, - Arc::default(), - ); - - let num_slots_in_epoch = bank0.get_slots_in_epoch(bank0.epoch()); - assert_eq!(num_slots_in_epoch, 32); - - let mut previous_bank = Arc::new(Bank::new_from_parent( - Arc::new(bank0), - &Pubkey::default(), - 1, - )); - - // simulate block progress - for slot in 2..=num_slots_in_epoch + 3 { - let pre_cap = previous_bank.capitalization(); - let curr_bank = Bank::new_from_parent(previous_bank, &Pubkey::default(), slot); - let post_cap = curr_bank.capitalization(); - - // Fill banks with banks with votes landing in the next slot - // Create enough banks such that vote account will root - for validator_vote_keypairs in validator_keypairs.iter() { - let vote_id = validator_vote_keypairs.vote_keypair.pubkey(); - let mut vote_account = curr_bank.get_account(&vote_id).unwrap(); - // generate some rewards - let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); - for i in 0..MAX_LOCKOUT_HISTORY + 42 { - if let Some(v) = vote_state.as_mut() { - vote_state::process_slot_vote_unchecked(v, i as u64) - } - let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - vote_state::to(&versioned, &mut vote_account).unwrap(); - match versioned { - VoteStateVersions::Current(v) => { - vote_state = Some(*v); - } - _ => panic!("Has to be of type Current"), - }; - } - curr_bank.store_account_and_update_capitalization(&vote_id, &vote_account); - } - - if slot == num_slots_in_epoch { - // This is the first block of epoch 1. Reward computation should happen in this block. - // assert reward compute status activated at epoch boundary - assert_matches!( - curr_bank.get_reward_interval(), - RewardInterval::InsideInterval - ); - - // cap should increase because of new epoch rewards - assert!(post_cap > pre_cap); - } else if slot == num_slots_in_epoch + 1 { - // When curr_slot == num_slots_in_epoch + 1, the 2nd block of epoch 1, reward distribution should happen in this block. - // however, since rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. - assert_matches!( - curr_bank.get_reward_interval(), - RewardInterval::InsideInterval - ); - - assert_eq!(post_cap, pre_cap); - } else if slot == num_slots_in_epoch + 2 || slot == num_slots_in_epoch + 3 { - // 1. when curr_slot == num_slots_in_epoch + 2, the 3nd block of epoch 1, reward distribution should happen in this block. - // however, all stake rewards are paid at the this block therefore reward_status should have transitioned to inactive. And since - // rewards are transferred from epoch_rewards sysvar to stake accounts. The cap should stay the same. - // 2. when curr_slot == num_slots_in_epoch+2, the 3rd block of epoch 1. reward distribution should have already completed. Therefore, - // reward_status should stay inactive and cap should stay the same. - assert_matches!( - curr_bank.get_reward_interval(), - RewardInterval::OutsideInterval - ); - - assert_eq!(post_cap, pre_cap); - } else { - // slot is not in rewards, cap should not change - assert_eq!(post_cap, pre_cap); - } - previous_bank = Arc::new(curr_bank); - } -} - -/// Test `EpochRewards` sysvar creation, distribution, and burning. -/// This test covers the following epoch_rewards_sysvar bank member functions, i.e. -/// `create_epoch_rewards_sysvar`, `update_epoch_rewards_sysvar`, `burn_and_purge_account`. -#[test] -fn test_epoch_rewards_sysvar() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); - let mut bank = Bank::new_for_tests(&genesis_config); - bank.activate_feature(&feature_set::enable_partitioned_epoch_reward::id()); - - let total_rewards = 1_000_000_000; // a large rewards so that the sysvar account is rent-exempted. - - // create epoch rewards sysvar - let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { - distribution_starting_block_height: 42, - num_partitions: 0, - parent_blockhash: Hash::default(), - total_points: 0, - total_rewards, - distributed_rewards: 10, - active: true, - }; - - bank.create_epoch_rewards_sysvar(total_rewards, 10, 42); - let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - assert_eq!(account.lamports(), total_rewards - 10); - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); - assert_eq!(epoch_rewards, expected_epoch_rewards); - - // make a distribution from epoch rewards sysvar - bank.update_epoch_rewards_sysvar(10); - let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - assert_eq!(account.lamports(), total_rewards - 20); - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); - let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { - distribution_starting_block_height: 42, - num_partitions: 0, - parent_blockhash: Hash::default(), - total_points: 0, - total_rewards, - distributed_rewards: 20, - active: true, - }; - assert_eq!(epoch_rewards, expected_epoch_rewards); - - // burn epoch rewards sysvar - bank.burn_and_purge_account(&sysvar::epoch_rewards::id(), account); - let account = bank.get_account(&sysvar::epoch_rewards::id()); - assert!(account.is_none()); -} - -/// Test that program execution that involves stake accounts should fail during reward period. -/// Any programs, which result in stake account changes, will throw `ProgramExecutionTemporarilyRestricted` error when -/// in reward period. -#[test] -fn test_program_execution_restricted_for_stake_account_in_reward_period() { - use solana_sdk::transaction::TransactionError::ProgramExecutionTemporarilyRestricted; - - let validator_vote_keypairs = ValidatorVoteKeypairs::new_rand(); - let validator_keypairs = vec![&validator_vote_keypairs]; - let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![1_000_000_000; 1], - ); - - let node_key = &validator_keypairs[0].node_keypair; - let stake_key = &validator_keypairs[0].stake_keypair; - - let (mut previous_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - let num_slots_in_epoch = previous_bank.get_slots_in_epoch(previous_bank.epoch()); - assert_eq!(num_slots_in_epoch, 32); - - for slot in 1..=num_slots_in_epoch + 2 { - let bank = new_bank_from_parent_with_bank_forks( - bank_forks.as_ref(), - previous_bank.clone(), - &Pubkey::default(), - slot, - ); - - // Fill bank_forks with banks with votes landing in the next slot - // So that rewards will be paid out at the epoch boundary, i.e. slot = 32 - let vote = vote_transaction::new_vote_transaction( - vec![slot - 1], - previous_bank.hash(), - previous_bank.last_blockhash(), - &validator_vote_keypairs.node_keypair, - &validator_vote_keypairs.vote_keypair, - &validator_vote_keypairs.vote_keypair, - None, - ); - bank.process_transaction(&vote).unwrap(); - - // Insert a transfer transaction from node account to stake account - let tx = - system_transaction::transfer(node_key, &stake_key.pubkey(), 1, bank.last_blockhash()); - let r = bank.process_transaction(&tx); - - if slot == num_slots_in_epoch { - // When the bank is at the beginning of the new epoch, i.e. slot 32, - // ProgramExecutionTemporarilyRestricted should be thrown for the transfer transaction. - assert_eq!( - r, - Err(ProgramExecutionTemporarilyRestricted { account_index: 1 }) - ); - } else { - // When the bank is outside of reward interval, the transfer transaction should not be affected and will succeed. - assert!(r.is_ok()); - } - - // Push a dummy blockhash, so that the latest_blockhash() for the transfer transaction in each - // iteration are different. Otherwise, all those transactions will be the same, and will not be - // executed by the bank except the first one. - bank.register_unique_recent_blockhash_for_test(); - previous_bank = bank; - } -} - -/// Test rewards computation and partitioned rewards distribution at the epoch boundary -#[test] -fn test_store_stake_accounts_in_partition() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let bank = Bank::new_for_tests(&genesis_config); - - let expected_num = 100; - - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - let expected_total = stake_rewards - .iter() - .map(|stake_reward| stake_reward.stake_reward_info.lamports) - .sum::() as u64; - - let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); - assert_eq!(expected_total, total_rewards_in_lamports); -} - -#[test] -fn test_store_stake_accounts_in_partition_empty() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let bank = Bank::new_for_tests(&genesis_config); - - let stake_rewards = vec![]; - - let expected_total = 0; - - let total_rewards_in_lamports = bank.store_stake_accounts_in_partition(&stake_rewards); - assert_eq!(expected_total, total_rewards_in_lamports); -} - -#[test] -fn test_update_reward_history_in_partition() { - for zero_reward in [false, true] { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let bank = Bank::new_for_tests(&genesis_config); - - let mut expected_num = 100; - - let mut stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - let mut rng = rand::thread_rng(); - let i_zero = rng.gen_range(0..expected_num); - if zero_reward { - // pick one entry to have zero rewards so it gets ignored - stake_rewards[i_zero].stake_reward_info.lamports = 0; - } - - let num_in_history = bank.update_reward_history_in_partition(&stake_rewards); - - if zero_reward { - stake_rewards.remove(i_zero); - // -1 because one of them had zero rewards and was ignored - expected_num -= 1; - } - - bank.rewards - .read() - .unwrap() - .iter() - .zip(stake_rewards.iter()) - .for_each(|((k, reward_info), expected_stake_reward)| { - assert_eq!( - ( - &expected_stake_reward.stake_pubkey, - &expected_stake_reward.stake_reward_info - ), - (k, reward_info) - ); - }); - - assert_eq!(num_in_history, expected_num); - } -} - -#[test] -fn test_update_reward_history_in_partition_empty() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - let bank = Bank::new_for_tests(&genesis_config); - - let stake_rewards = vec![]; - - let num_in_history = bank.update_reward_history_in_partition(&stake_rewards); - assert_eq!(num_in_history, 0); -} - -#[test] -fn test_store_vote_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 expected_vote_rewards_num = 100; - - let vote_rewards = (0..expected_vote_rewards_num) - .map(|_| Some((Pubkey::new_unique(), VoteReward::new_random()))) - .collect::>(); - - let mut vote_rewards_account = VoteRewardsAccounts::default(); - vote_rewards.iter().for_each(|e| { - if let Some(p) = &e { - let info = RewardInfo { - reward_type: RewardType::Voting, - lamports: p.1.vote_rewards as i64, - post_balance: p.1.vote_rewards, - commission: Some(p.1.commission), - }; - vote_rewards_account.rewards.push((p.0, info)); - vote_rewards_account - .accounts_to_store - .push(e.as_ref().map(|p| p.1.vote_account.clone())); - } - }); - - let metrics = RewardsMetrics::default(); - - let stored_vote_accounts = bank.store_vote_accounts_partitioned(vote_rewards_account, &metrics); - assert_eq!(expected_vote_rewards_num, stored_vote_accounts.len()); - - // load accounts to make sure they were stored correctly - vote_rewards.iter().for_each(|e| { - if let Some(p) = &e { - let (k, account) = (p.0, p.1.vote_account.clone()); - let loaded_account = bank.load_slow_with_fixed_root(&bank.ancestors, &k).unwrap(); - assert!(accounts_equal(&loaded_account.0, &account)); - } - }); -} - -#[test] -fn test_store_vote_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 vote_rewards = VoteRewardsAccounts::default(); - let metrics = RewardsMetrics::default(); - - let stored_vote_accounts = bank.store_vote_accounts_partitioned(vote_rewards, &metrics); - assert_eq!(expected, stored_vote_accounts.len()); -} - #[test] fn test_system_instruction_allocate() { let (genesis_config, mint_keypair) = create_genesis_config_no_tx_fee(sol_to_lamports(1.0)); @@ -13286,215 +12448,6 @@ fn test_calc_vote_accounts_to_store_normal() { } } -impl Bank { - /// Return the total number of blocks in reward interval (including both calculation and crediting). - fn get_reward_total_num_blocks(&self, rewards: &StakeRewards) -> u64 { - self.get_reward_calculation_num_blocks() + self.get_reward_distribution_num_blocks(rewards) - } -} - -/// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during normal epoch gives the expected result -#[test] -fn test_get_reward_distribution_num_blocks_normal() { - solana_logger::setup(); - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); - - let bank = Bank::new_for_tests(&genesis_config); - - // Given 8k rewards, it will take 2 blocks to credit all the rewards - let expected_num = 8192; - let stake_rewards = (0..expected_num) - .map(|_| StakeReward::new_random()) - .collect::>(); - - assert_eq!(bank.get_reward_distribution_num_blocks(&stake_rewards), 2); - assert_eq!(bank.get_reward_calculation_num_blocks(), 1); - assert_eq!( - bank.get_reward_total_num_blocks(&stake_rewards), - bank.get_reward_distribution_num_blocks(&stake_rewards) - + bank.get_reward_calculation_num_blocks(), - ); -} - -/// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during small epoch -/// The num_credit_blocks should be cap to 10% of the total number of blocks in the epoch. -#[test] -fn test_get_reward_distribution_num_blocks_cap() { - let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - genesis_config.epoch_schedule = EpochSchedule::custom(32, 32, false); - - // Config stake reward distribution to be 10 per block - let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING.clone(); - accounts_db_config.test_partitioned_epoch_rewards = - TestPartitionedEpochRewards::PartitionedEpochRewardsConfigRewardBlocks { - reward_calculation_num_blocks: 1, - stake_account_stores_per_block: 10, - }; - - let bank = Bank::new_with_paths( - &genesis_config, - Arc::new(RuntimeConfig::default()), - Vec::new(), - None, - None, - AccountSecondaryIndexes::default(), - AccountShrinkThreshold::default(), - false, - Some(accounts_db_config), - None, - Some(Pubkey::new_unique()), - Arc::default(), - ); - - let stake_account_stores_per_block = bank.partitioned_rewards_stake_account_stores_per_block(); - assert_eq!(stake_account_stores_per_block, 10); - - let check_num_reward_distribution_blocks = - |num_stakes: u64, - expected_num_reward_distribution_blocks: u64, - expected_num_reward_computation_blocks: u64| { - // Given the short epoch, i.e. 32 slots, we should cap the number of reward distribution blocks to 32/10 = 3. - let stake_rewards = (0..num_stakes) - .map(|_| StakeReward::new_random()) - .collect::>(); - - assert_eq!( - bank.get_reward_distribution_num_blocks(&stake_rewards), - expected_num_reward_distribution_blocks - ); - assert_eq!( - bank.get_reward_calculation_num_blocks(), - expected_num_reward_computation_blocks - ); - assert_eq!( - bank.get_reward_total_num_blocks(&stake_rewards), - bank.get_reward_distribution_num_blocks(&stake_rewards) - + bank.get_reward_calculation_num_blocks(), - ); - }; - - for test_record in [ - // num_stakes, expected_num_reward_distribution_blocks, expected_num_reward_computation_blocks - (0, 1, 1), - (1, 1, 1), - (stake_account_stores_per_block, 1, 1), - (2 * stake_account_stores_per_block - 1, 2, 1), - (2 * stake_account_stores_per_block, 2, 1), - (3 * stake_account_stores_per_block - 1, 3, 1), - (3 * stake_account_stores_per_block, 3, 1), - (4 * stake_account_stores_per_block, 3, 1), // cap at 3 - (5 * stake_account_stores_per_block, 3, 1), //cap at 3 - ] { - check_num_reward_distribution_blocks(test_record.0, test_record.1, test_record.2); - } -} - -/// Test get_reward_distribution_num_blocks, get_reward_calculation_num_blocks, get_reward_total_num_blocks during warm up epoch gives the expected result. -/// The num_credit_blocks should be 1 during warm up epoch. -#[test] -fn test_get_reward_distribution_num_blocks_warmup() { - let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); - - let bank = Bank::new_for_tests(&genesis_config); - let rewards = vec![]; - assert_eq!(bank.get_reward_distribution_num_blocks(&rewards), 1); - assert_eq!(bank.get_reward_calculation_num_blocks(), 1); - assert_eq!( - bank.get_reward_total_num_blocks(&rewards), - bank.get_reward_distribution_num_blocks(&rewards) - + bank.get_reward_calculation_num_blocks(), - ); -} - -#[test] -fn test_calculate_stake_vote_rewards() { - solana_logger::setup(); - - let expected_num_delegations = 1; - let (bank, voters, stakers) = create_reward_bank(expected_num_delegations); - - let vote_pubkey = voters.first().unwrap(); - let mut vote_account = bank - .load_slow_with_fixed_root(&bank.ancestors, vote_pubkey) - .unwrap() - .0; - - 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, // lamports to split - points: 1000, // over these points - }; - let tracer = |_event: &RewardCalculationEvent| {}; - let reward_calc_tracer = Some(tracer); - let rewarded_epoch = bank.epoch(); - let stakes: RwLockReadGuard>> = bank.stakes_cache.stakes(); - let reward_calculate_param = bank.get_epoch_reward_calculate_param_info(&stakes); - let (vote_rewards_accounts, stake_reward_calculation) = bank.calculate_stake_vote_rewards( - &reward_calculate_param, - rewarded_epoch, - point_value, - &thread_pool, - reward_calc_tracer, - &mut rewards_metrics, - ); - - assert_eq!( - vote_rewards_accounts.rewards.len(), - vote_rewards_accounts.accounts_to_store.len() - ); - assert_eq!(vote_rewards_accounts.rewards.len(), 1); - let rewards = &vote_rewards_accounts.rewards[0]; - let account = &vote_rewards_accounts.accounts_to_store[0]; - let vote_rewards = 0; - let commision = 0; - _ = vote_account.checked_add_lamports(vote_rewards); - assert_eq!( - account.as_ref().unwrap().lamports(), - vote_account.lamports() - ); - assert!(accounts_equal(account.as_ref().unwrap(), &vote_account)); - assert_eq!( - rewards.1, - RewardInfo { - reward_type: RewardType::Voting, - lamports: vote_rewards as i64, - post_balance: vote_account.lamports(), - commission: Some(commision), - } - ); - assert_eq!(&rewards.0, vote_pubkey); - - assert_eq!(stake_reward_calculation.stake_rewards.len(), 1); - assert_eq!( - &stake_reward_calculation.stake_rewards[0].stake_pubkey, - stake_pubkey - ); - - let original_stake_lamport = stake_account.lamports(); - let rewards = stake_reward_calculation.stake_rewards[0] - .stake_reward_info - .lamports; - let expected_reward_info = RewardInfo { - reward_type: RewardType::Staking, - lamports: rewards, - post_balance: original_stake_lamport + rewards as u64, - commission: Some(commision), - }; - assert_eq!( - stake_reward_calculation.stake_rewards[0].stake_reward_info, - expected_reward_info, - ); -} - #[test] fn test_register_hard_fork() { fn get_hard_forks(bank: &Bank) -> Vec { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 12eab54a41c..9ba006fc93e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,7 +11,6 @@ pub mod bank_forks; pub mod bank_utils; pub mod commitment; pub mod compute_budget_details; -mod epoch_rewards_hasher; pub mod epoch_stakes; pub mod genesis_utils; pub mod installed_scheduler_pool; diff --git a/runtime/src/serde_snapshot/newer.rs b/runtime/src/serde_snapshot/newer.rs index 004e6e61d54..d9c73d04422 100644 --- a/runtime/src/serde_snapshot/newer.rs +++ b/runtime/src/serde_snapshot/newer.rs @@ -5,7 +5,7 @@ use { *, }, crate::{ - bank::EpochRewardStatus, + bank::partitioned_epoch_rewards::EpochRewardStatus, stakes::{serde_stakes_enum_compat, StakesEnum}, }, solana_accounts_db::{accounts_hash::AccountsHash, ancestors::AncestorsForSerialization},