Skip to main content

solana_runtime/bank/partitioned_epoch_rewards/
mod.rs

1mod calculation;
2mod distribution;
3mod epoch_rewards_hasher;
4mod sysvar;
5
6use {
7    super::Bank,
8    crate::{
9        inflation_rewards::points::PointValue, reward_info::RewardInfo,
10        stake_account::StakeAccount, stake_history::StakeHistory,
11    },
12    solana_account::{AccountSharedData, ReadableAccount},
13    solana_accounts_db::{
14        stake_rewards::StakeReward,
15        storable_accounts::{AccountForStorage, StorableAccounts},
16    },
17    solana_clock::Slot,
18    solana_pubkey::{Pubkey, PubkeyHasherBuilder},
19    solana_stake_interface::state::{Delegation, Stake},
20    solana_vote::vote_account::VoteAccounts,
21    std::{collections::HashMap, mem::MaybeUninit, sync::Arc},
22};
23
24/// Number of blocks for reward calculation and storing vote accounts.
25/// Distributing rewards to stake accounts begins AFTER this many blocks.
26const REWARD_CALCULATION_NUM_BLOCKS: u64 = 1;
27
28/// Total reward for a stake account, comprising inflation and block rewards.
29#[derive(Debug, Clone, PartialEq)]
30pub(crate) struct PartitionedStakeReward {
31    /// Stake account address
32    pub stake_pubkey: Pubkey,
33    /// Inflation reward information
34    pub inflation: InflationReward,
35    /// Block rewards due during distribution
36    pub block_reward: u64,
37}
38
39/// Just the inflation portion of a partitioned stake reward
40#[derive(Debug, Clone, PartialEq)]
41pub(crate) struct InflationReward {
42    /// `Stake` state to be stored in account
43    pub stake: Stake,
44    /// Stake reward for recording in the Bank on distribution
45    pub stake_reward: u64,
46    /// Reward commission in basis points (0-10,000 representing 0-100%) for
47    /// recording reward info.
48    //
49    // Note: This field becomes always `None` once SIMD-0232 is activated.
50    // After full activation, it can be removed on feature cleanup.
51    pub commission_bps: Option<u16>,
52}
53
54/// A vector of stake rewards.
55#[derive(Debug, Default, PartialEq)]
56pub(crate) struct PartitionedStakeRewards {
57    /// Inner vector.
58    rewards: Vec<Option<PartitionedStakeReward>>,
59    /// Number of stake rewards.
60    num_rewards: usize,
61}
62
63impl PartitionedStakeRewards {
64    pub(crate) fn with_capacity(capacity: usize) -> Self {
65        let rewards = Vec::with_capacity(capacity);
66        Self {
67            rewards,
68            num_rewards: 0,
69        }
70    }
71
72    /// Number of stake rewards.
73    pub(crate) fn num_rewards(&self) -> usize {
74        self.num_rewards
75    }
76
77    /// Total length, including both `Some` and `None` elements.
78    pub(crate) fn total_len(&self) -> usize {
79        self.rewards.len()
80    }
81
82    pub(crate) fn get(&self, index: usize) -> Option<&Option<PartitionedStakeReward>> {
83        self.rewards.get(index)
84    }
85
86    pub(crate) fn enumerated_rewards_iter(
87        &self,
88    ) -> impl Iterator<Item = (usize, &PartitionedStakeReward)> {
89        self.rewards
90            .iter()
91            .enumerate()
92            .filter_map(|(index, reward)| reward.as_ref().map(|reward| (index, reward)))
93    }
94
95    fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit<Option<PartitionedStakeReward>>] {
96        self.rewards.spare_capacity_mut()
97    }
98
99    /// Safety: all `total_len` elements must be initialized in `self.rewards`.
100    /// `num_stake_rewards` is the number of those elements that are `Some`.
101    unsafe fn assume_init(&mut self, num_stake_rewards: usize, total_len: usize) {
102        debug_assert!(num_stake_rewards <= total_len);
103        unsafe {
104            self.rewards.set_len(total_len);
105        }
106        self.num_rewards = num_stake_rewards;
107    }
108}
109
110#[cfg(test)]
111impl FromIterator<Option<PartitionedStakeReward>> for PartitionedStakeRewards {
112    fn from_iter<T: IntoIterator<Item = Option<PartitionedStakeReward>>>(iter: T) -> Self {
113        let mut len_some: usize = 0;
114        let rewards = Vec::from_iter(iter.into_iter().inspect(|reward| {
115            if reward.is_some() {
116                len_some = len_some.saturating_add(1);
117            }
118        }));
119        Self {
120            rewards,
121            num_rewards: len_some,
122        }
123    }
124}
125
126#[cfg(test)]
127impl FromIterator<PartitionedStakeReward> for PartitionedStakeRewards {
128    fn from_iter<T: IntoIterator<Item = PartitionedStakeReward>>(iter: T) -> Self {
129        let rewards = Vec::from_iter(iter.into_iter().map(Some));
130        let num_rewards = rewards.len();
131        Self {
132            rewards,
133            num_rewards,
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub(crate) struct StartBlockHeightAndRewards {
140    /// the block height of the slot at which rewards distribution began
141    pub(crate) distribution_starting_block_height: u64,
142    /// calculated epoch rewards before partitioning
143    pub(crate) all_stake_rewards: Arc<PartitionedStakeRewards>,
144}
145
146#[derive(Debug, Clone, PartialEq)]
147pub(crate) struct StartBlockHeightAndPartitionedRewards {
148    /// the block height of the slot at which rewards distribution began
149    pub(crate) distribution_starting_block_height: u64,
150
151    /// calculated epoch rewards pending distribution
152    pub(crate) all_stake_rewards: Arc<PartitionedStakeRewards>,
153
154    /// indices of calculated epoch rewards per partition, outer Vec is by
155    /// partition (one partition per block), inner Vec is the indices for one
156    /// partition.
157    pub(crate) partition_indices: Vec<Vec<usize>>,
158}
159
160/// Represent whether bank is in the reward phase or not.
161#[derive(Debug, Clone, PartialEq, Default)]
162pub(crate) enum EpochRewardStatus {
163    /// this bank is in the reward phase.
164    /// Contents are the start point for epoch reward calculation,
165    /// i.e. parent_slot and parent_block height for the starting
166    /// block of the current epoch.
167    Active(EpochRewardPhase),
168    /// this bank is outside of the rewarding phase.
169    #[default]
170    Inactive,
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub(crate) enum EpochRewardPhase {
175    Calculation(StartBlockHeightAndRewards),
176    Distribution(StartBlockHeightAndPartitionedRewards),
177}
178
179#[derive(Debug)]
180#[cfg_attr(test, derive(Clone))]
181pub(super) struct RewardCommission {
182    // Note: This field becomes always `None` once SIMD-0232 is activated.
183    // After full activation, it can be removed on feature cleanup.
184    pub(super) commission_bps: Option<u16>,
185    pub(super) commission_lamports: u64,
186    pub(super) burned_lamports: u64,
187    pub(super) is_vote_account: bool,
188}
189
190pub(super) type RewardCommissions = HashMap<Pubkey, RewardCommission, PubkeyHasherBuilder>;
191
192/// Helper struct to give the amounts distributed to commission accounts or
193/// burned in different manners
194#[derive(Debug, Default)]
195pub(super) struct RewardCommissionLamportAmounts {
196    /// Lamports distributed across all commission collectors, except the
197    /// incinerator.
198    ///
199    /// Also the maximum capitalization increase by the end of the first block
200    /// of the epoch.
201    pub(super) distributed_lamports: u64,
202    /// lamports distributed to the incinerator.
203    ///
204    /// Tracked separately to give a better upper bound for capitalization at
205    /// the end of the first block of an epoch, needed for Alpenglow's
206    /// `EpochInflationAccountState`
207    pub(super) distributed_to_incinerator_lamports: u64,
208    /// lamports burned from undistributed commissions
209    pub(super) burned_lamports: u64,
210}
211
212#[derive(Debug, Default)]
213pub(super) struct RewardCommissionAccounts {
214    /// accounts with rewards to be stored
215    pub(super) accounts_with_rewards: Vec<(Pubkey, RewardInfo, AccountSharedData)>,
216    /// amounts distributed to those accounts, and burned after calculation
217    pub(super) amounts: RewardCommissionLamportAmounts,
218}
219
220/// Wrapper struct to implement StorableAccounts for RewardCommissionAccounts
221pub(super) struct RewardCommissionAccountsStorable<'a> {
222    pub slot: Slot,
223    pub reward_commission_accounts: &'a RewardCommissionAccounts,
224}
225
226impl<'a> StorableAccounts<'a> for RewardCommissionAccountsStorable<'a> {
227    fn account<Ret>(
228        &self,
229        index: usize,
230        mut callback: impl for<'local> FnMut(AccountForStorage<'local>) -> Ret,
231    ) -> Ret {
232        let (pubkey, _, account) = &self.reward_commission_accounts.accounts_with_rewards[index];
233        callback((pubkey, account).into())
234    }
235
236    fn account_for_geyser<Ret>(
237        &self,
238        index: usize,
239        mut callback: impl for<'local> FnMut(&'local Pubkey, &'local AccountSharedData) -> Ret,
240    ) -> Ret {
241        let (pubkey, _, account) = &self.reward_commission_accounts.accounts_with_rewards[index];
242        callback(pubkey, account)
243    }
244
245    fn is_zero_lamport(&self, index: usize) -> bool {
246        self.reward_commission_accounts.accounts_with_rewards[index]
247            .2
248            .lamports()
249            == 0
250    }
251
252    fn data_len(&self, index: usize) -> usize {
253        self.reward_commission_accounts.accounts_with_rewards[index]
254            .2
255            .data()
256            .len()
257    }
258
259    fn pubkey(&self, index: usize) -> &Pubkey {
260        &self.reward_commission_accounts.accounts_with_rewards[index].0
261    }
262
263    fn slot(&self, _index: usize) -> Slot {
264        self.target_slot()
265    }
266
267    fn target_slot(&self) -> Slot {
268        self.slot
269    }
270
271    fn len(&self) -> usize {
272        self.reward_commission_accounts.accounts_with_rewards.len()
273    }
274}
275
276#[derive(Debug, Default)]
277/// result of calculating the stake rewards at end of epoch
278pub(super) struct StakeRewardCalculation {
279    /// each individual stake account to reward
280    stake_rewards: Arc<PartitionedStakeRewards>,
281    /// total lamports across all `stake_rewards`
282    total_stake_rewards_lamports: u64,
283}
284
285#[derive(Debug)]
286struct CalculateValidatorRewardsResult {
287    reward_commissions: RewardCommissions,
288    stake_reward_calculation: StakeRewardCalculation,
289    point_value: PointValue,
290}
291
292impl Default for CalculateValidatorRewardsResult {
293    fn default() -> Self {
294        Self {
295            reward_commissions: RewardCommissions::default(),
296            stake_reward_calculation: StakeRewardCalculation::default(),
297            point_value: PointValue {
298                points: 0,
299                rewards: 0,
300            },
301        }
302    }
303}
304
305pub(super) struct CachedVoteAccounts<'a> {
306    /// Snapshot of vote account state from the beginning of the epoch prior to
307    /// the rewarded epoch. This snapshot state is saved a full epoch before
308    /// being used to prevent last minute commission rugs.
309    ///
310    /// Developer note: This field is `Option` to handle large bank warps
311    pub(super) snapshot_epoch_vote_accounts: Option<&'a VoteAccounts>,
312    /// Vote account state from the beginning of the rewarded epoch.
313    ///
314    /// Developer note: This field is `Option` to handle large bank warps
315    pub(super) rewarded_epoch_vote_accounts: Option<&'a VoteAccounts>,
316    /// Vote account state from the end of the rewarded epoch / beginning of the
317    /// distribution epoch.
318    pub(super) distribution_epoch_vote_accounts: &'a VoteAccounts,
319}
320
321/// hold reward calc info to avoid recalculation across functions
322pub(super) struct EpochRewardCalculateParamInfo<'a> {
323    pub(super) stake_history: StakeHistory,
324    pub(super) stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount<Delegation>)>,
325    pub(super) cached_vote_accounts: CachedVoteAccounts<'a>,
326}
327
328/// Hold all results from calculating the rewards for partitioned distribution.
329/// This struct exists so we can have a function which does all the calculation with no
330/// side effects.
331#[derive(Debug)]
332pub(super) struct PartitionedRewardsCalculation {
333    reward_commissions: RewardCommissions,
334    stake_rewards: StakeRewardCalculation,
335    capitalization: u64,
336    point_value: PointValue,
337    /// Number of vote accounts in the distribution-epoch snapshot after
338    /// SIMD-0357 VAT filtering.
339    /// Surfaced for the `epoch_rewards` datapoint without re-running the
340    /// filter at distribution time.
341    num_filtered_vote_accounts: usize,
342}
343
344pub(crate) type StakeRewards = Vec<StakeReward>;
345
346#[derive(Debug, PartialEq)]
347pub struct KeyedRewardsAndNumPartitions {
348    pub keyed_rewards: Vec<(Pubkey, RewardInfo)>,
349    pub num_partitions: Option<u64>,
350}
351
352impl KeyedRewardsAndNumPartitions {
353    pub fn should_record(&self) -> bool {
354        !self.keyed_rewards.is_empty() || self.num_partitions.is_some()
355    }
356}
357
358impl Bank {
359    pub fn get_rewards_and_num_partitions(&self) -> KeyedRewardsAndNumPartitions {
360        let keyed_rewards = self.rewards.read().unwrap().clone();
361        let epoch_rewards_sysvar = self.get_epoch_rewards_sysvar();
362        // If partitioned epoch rewards are active and this Bank is the
363        // epoch-boundary block, populate num_partitions
364        let epoch_schedule = self.epoch_schedule();
365        let parent_epoch = epoch_schedule.get_epoch(self.parent_slot());
366        let is_first_block_in_epoch = self.epoch() > parent_epoch;
367
368        let num_partitions = (epoch_rewards_sysvar.active && is_first_block_in_epoch)
369            .then_some(epoch_rewards_sysvar.num_partitions);
370        KeyedRewardsAndNumPartitions {
371            keyed_rewards,
372            num_partitions,
373        }
374    }
375
376    pub(crate) fn set_epoch_reward_status_calculation(
377        &mut self,
378        distribution_starting_block_height: u64,
379        stake_rewards: Arc<PartitionedStakeRewards>,
380    ) {
381        self.epoch_reward_status =
382            EpochRewardStatus::Active(EpochRewardPhase::Calculation(StartBlockHeightAndRewards {
383                distribution_starting_block_height,
384                all_stake_rewards: stake_rewards,
385            }));
386    }
387
388    pub(crate) fn set_epoch_reward_status_distribution(
389        &mut self,
390        distribution_starting_block_height: u64,
391        all_stake_rewards: Arc<PartitionedStakeRewards>,
392        partition_indices: Vec<Vec<usize>>,
393    ) {
394        self.epoch_reward_status = EpochRewardStatus::Active(EpochRewardPhase::Distribution(
395            StartBlockHeightAndPartitionedRewards {
396                distribution_starting_block_height,
397                all_stake_rewards,
398                partition_indices,
399            },
400        ));
401    }
402
403    /// # stake accounts to store in one block during partitioned reward interval
404    pub(super) fn partitioned_rewards_stake_account_stores_per_block(&self) -> u64 {
405        self.partitioned_rewards_stake_account_stores_per_block
406    }
407
408    /// Calculate the number of blocks required to distribute rewards to all stake accounts.
409    pub(super) fn get_reward_distribution_num_blocks(
410        &self,
411        rewards: &PartitionedStakeRewards,
412    ) -> u64 {
413        let total_stake_accounts = rewards.num_rewards();
414        if self.epoch_schedule.warmup && self.epoch < self.first_normal_epoch() {
415            1
416        } else {
417            const MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH: u64 = 10;
418            let num_chunks = total_stake_accounts
419                .div_ceil(self.partitioned_rewards_stake_account_stores_per_block() as usize)
420                as u64;
421
422            // Limit the reward credit interval to 10% of the total number of slots in a epoch
423            num_chunks.clamp(
424                1,
425                (self.epoch_schedule.slots_per_epoch / MAX_FACTOR_OF_REWARD_BLOCKS_IN_EPOCH).max(1),
426            )
427        }
428    }
429
430    /// For testing only
431    pub fn force_reward_interval_end_for_tests(&mut self) {
432        self.epoch_reward_status = EpochRewardStatus::Inactive;
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use {
439        super::*,
440        crate::{
441            bank::{SlotLeader, tests::create_genesis_config},
442            bank_forks::BankForks,
443            genesis_utils::{
444                GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts,
445                deactivate_features,
446            },
447            runtime_config::RuntimeConfig,
448            stake_utils,
449            sysvar_account::from_account,
450        },
451        assert_matches::assert_matches,
452        rand::Rng,
453        solana_account::{Account, state_traits::StateMut},
454        solana_accounts_db::{
455            accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig},
456            partitioned_rewards::PartitionedEpochRewardsConfig,
457        },
458        solana_epoch_schedule::EpochSchedule,
459        solana_hash::Hash,
460        solana_keypair::Keypair,
461        solana_native_token::LAMPORTS_PER_SOL,
462        solana_reward_info::RewardType,
463        solana_signer::Signer,
464        solana_system_transaction as system_transaction,
465        solana_vote::vote_transaction,
466        solana_vote_interface::state::{MAX_LOCKOUT_HISTORY, VoteStateV4, VoteStateVersions},
467        solana_vote_program::vote_state::{self, TowerSync, handler::VoteStateHandler},
468        std::sync::{Arc, RwLock},
469    };
470
471    impl PartitionedStakeReward {
472        pub fn new_random() -> Self {
473            let mut rng = rand::rng();
474            let stake_reward = rng.random_range(1..200);
475            Self {
476                stake_pubkey: Pubkey::new_unique(),
477                inflation: InflationReward {
478                    stake: Stake {
479                        delegation: Delegation {
480                            voter_pubkey: Pubkey::new_unique(),
481                            stake: rng.random_range(1..200) + stake_reward,
482                            activation_epoch: 0,
483                            deactivation_epoch: u64::MAX,
484                            ..Default::default()
485                        },
486                        credits_observed: rng.random_range(1..200),
487                    },
488                    stake_reward,
489                    commission_bps: None,
490                },
491                block_reward: rng.random_range(0..10_000_000_000),
492            }
493        }
494
495        pub fn new_with_lamport_amounts(stake_reward: u64, block_reward: u64, stake: u64) -> Self {
496            Self {
497                stake_pubkey: Pubkey::new_unique(),
498                inflation: InflationReward {
499                    stake: Stake {
500                        delegation: Delegation {
501                            voter_pubkey: Pubkey::new_unique(),
502                            stake: stake + stake_reward,
503                            activation_epoch: 0,
504                            deactivation_epoch: u64::MAX,
505                            ..Default::default()
506                        },
507                        credits_observed: 0,
508                    },
509                    stake_reward,
510                    commission_bps: None,
511                },
512                block_reward,
513            }
514        }
515    }
516
517    pub fn build_partitioned_stake_rewards(
518        stake_rewards: &PartitionedStakeRewards,
519        partition_indices: &[Vec<usize>],
520    ) -> Vec<PartitionedStakeRewards> {
521        partition_indices
522            .iter()
523            .map(|partition_index| {
524                // partition_index is a Vec<usize> that contains the indices of the stake rewards
525                // that belong to this partition
526                partition_index
527                    .iter()
528                    .map(|&index| stake_rewards.get(index).unwrap().clone())
529                    .collect::<PartitionedStakeRewards>()
530            })
531            .collect::<Vec<_>>()
532    }
533
534    #[derive(Debug, PartialEq, Eq, Copy, Clone)]
535    enum RewardInterval {
536        /// the slot within the epoch is INSIDE the reward distribution interval
537        InsideInterval,
538        /// the slot within the epoch is OUTSIDE the reward distribution interval
539        OutsideInterval,
540    }
541
542    impl Bank {
543        /// Return `RewardInterval` enum for current bank
544        fn get_reward_interval(&self) -> RewardInterval {
545            if matches!(self.epoch_reward_status, EpochRewardStatus::Active(_)) {
546                RewardInterval::InsideInterval
547            } else {
548                RewardInterval::OutsideInterval
549            }
550        }
551
552        fn is_calculated(&self) -> bool {
553            matches!(
554                self.epoch_reward_status,
555                EpochRewardStatus::Active(EpochRewardPhase::Calculation(_))
556            )
557        }
558
559        fn is_partitioned(&self) -> bool {
560            matches!(
561                self.epoch_reward_status,
562                EpochRewardStatus::Active(EpochRewardPhase::Distribution(_))
563            )
564        }
565
566        fn get_epoch_rewards_from_cache(
567            &self,
568            parent_hash: &Hash,
569        ) -> Option<Arc<PartitionedRewardsCalculation>> {
570            self.epoch_rewards_calculation_cache
571                .lock()
572                .unwrap()
573                .get(parent_hash)
574                .cloned()
575        }
576
577        fn get_epoch_rewards_cache_len(&self) -> usize {
578            self.epoch_rewards_calculation_cache.lock().unwrap().len()
579        }
580    }
581
582    pub(super) const SLOTS_PER_EPOCH: u64 = 32;
583
584    pub(super) struct RewardBank {
585        pub(super) bank: Arc<Bank>,
586        pub(super) voters: Vec<Pubkey>,
587        pub(super) stakers: Vec<Pubkey>,
588    }
589
590    /// Helper functions to create a bank that pays some rewards
591    pub(super) fn create_default_reward_bank(
592        expected_num_delegations: usize,
593        advance_num_slots: u64,
594    ) -> (RewardBank, Arc<RwLock<BankForks>>) {
595        create_reward_bank(
596            expected_num_delegations,
597            PartitionedEpochRewardsConfig::default().stake_account_stores_per_block,
598            advance_num_slots,
599        )
600    }
601
602    pub(super) fn create_reward_bank(
603        expected_num_delegations: usize,
604        stake_account_stores_per_block: u64,
605        advance_num_slots: u64,
606    ) -> (RewardBank, Arc<RwLock<BankForks>>) {
607        create_reward_bank_with_specific_stakes(
608            vec![2_000_000_000; expected_num_delegations],
609            stake_account_stores_per_block,
610            advance_num_slots,
611        )
612    }
613
614    pub(super) fn create_reward_bank_with_specific_stakes(
615        stakes: Vec<u64>,
616        stake_account_stores_per_block: u64,
617        advance_num_slots: u64,
618    ) -> (RewardBank, Arc<RwLock<BankForks>>) {
619        // Disable slot time reduction features as they will override the custom
620        // stores per block provided in this test helper.
621        let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec();
622        let validator_keypairs = (0..stakes.len())
623            .map(|_| ValidatorVoteKeypairs::new_rand())
624            .collect::<Vec<_>>();
625
626        let GenesisConfigInfo {
627            mut genesis_config, ..
628        } = create_genesis_config_with_vote_accounts(1_000_000_000, &validator_keypairs, stakes);
629        genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH);
630        deactivate_features(&mut genesis_config, &features_to_deactivate);
631
632        let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING;
633        accounts_db_config.partitioned_epoch_rewards_config =
634            PartitionedEpochRewardsConfig::new_for_test(stake_account_stores_per_block);
635
636        let bank = Bank::new_from_genesis(
637            &genesis_config,
638            Arc::new(RuntimeConfig::default()),
639            Vec::new(),
640            None,
641            accounts_db_config,
642            None,
643            None,
644            Arc::default(),
645            None,
646            None,
647        );
648
649        // Fill bank_forks with banks with votes landing in the next slot
650        // Create enough banks such that vote account will root
651        populate_vote_accounts_with_votes(
652            &bank,
653            validator_keypairs.iter().map(|k| k.vote_keypair.pubkey()),
654            0,
655        );
656
657        // Advance some num slots; usually to the next epoch boundary to update
658        // EpochStakes
659        let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests();
660        let bank = Bank::new_from_parent_with_bank_forks(
661            &bank_forks,
662            bank,
663            SlotLeader::default(),
664            advance_num_slots,
665        );
666
667        (
668            RewardBank {
669                bank,
670                voters: validator_keypairs
671                    .iter()
672                    .map(|k| k.vote_keypair.pubkey())
673                    .collect(),
674                stakers: validator_keypairs
675                    .iter()
676                    .map(|k| k.stake_keypair.pubkey())
677                    .collect(),
678            },
679            bank_forks,
680        )
681    }
682
683    pub(super) fn populate_vote_accounts_with_votes(
684        bank: &Bank,
685        vote_pubkeys: impl IntoIterator<Item = Pubkey>,
686        commission: u8,
687    ) {
688        for vote_pubkey in vote_pubkeys {
689            let mut vote_account = bank
690                .get_account(&vote_pubkey)
691                .unwrap_or_else(|| panic!("missing vote account {vote_pubkey:?}"));
692            let mut vote_state = VoteStateHandler::new_v4(
693                VoteStateV4::deserialize(vote_account.data(), &vote_pubkey).unwrap(),
694            );
695            vote_state.set_commission(commission);
696            for i in 0..MAX_LOCKOUT_HISTORY + 42 {
697                vote_state::process_slot_vote_unchecked(&mut vote_state, i as u64);
698                let versioned = VoteStateVersions::V4(Box::new(vote_state.as_ref_v4().clone()));
699                vote_account.set_state(&versioned).unwrap();
700            }
701            bank.store_account_and_update_capitalization(&vote_pubkey, &vote_account);
702        }
703    }
704
705    #[test]
706    fn test_force_reward_interval_end() {
707        let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
708        let mut bank = Bank::new_for_tests(&genesis_config);
709
710        let expected_num = 100;
711
712        let stake_rewards = (0..expected_num)
713            .map(|_| Some(PartitionedStakeReward::new_random()))
714            .collect::<PartitionedStakeRewards>();
715
716        let partition_indices = vec![(0..expected_num).collect()];
717
718        bank.set_epoch_reward_status_distribution(
719            bank.block_height() + REWARD_CALCULATION_NUM_BLOCKS,
720            Arc::new(stake_rewards),
721            partition_indices,
722        );
723        assert!(bank.get_reward_interval() == RewardInterval::InsideInterval);
724
725        bank.force_reward_interval_end_for_tests();
726        assert!(bank.get_reward_interval() == RewardInterval::OutsideInterval);
727    }
728
729    /// Test get_reward_distribution_num_blocks during small epoch
730    /// The num_credit_blocks should be cap to 10% of the total number of blocks in the epoch.
731    #[test]
732    fn test_get_reward_distribution_num_blocks_cap() {
733        let (mut genesis_config, _mint_keypair) =
734            create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
735        genesis_config.epoch_schedule = EpochSchedule::custom(32, 32, false);
736
737        // Config stake reward distribution to be 10 per block
738        let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING;
739        accounts_db_config.partitioned_epoch_rewards_config =
740            PartitionedEpochRewardsConfig::new_for_test(10);
741
742        let bank = Bank::new_from_genesis(
743            &genesis_config,
744            Arc::new(RuntimeConfig::default()),
745            Vec::new(),
746            None,
747            accounts_db_config,
748            None,
749            Some(SlotLeader::new_unique()),
750            Arc::default(),
751            None,
752            None,
753        );
754
755        let stake_account_stores_per_block =
756            bank.partitioned_rewards_stake_account_stores_per_block();
757        assert_eq!(stake_account_stores_per_block, 10);
758
759        let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests();
760        let bank =
761            Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), 1);
762        assert_eq!(
763            bank.partitioned_rewards_stake_account_stores_per_block(),
764            stake_account_stores_per_block
765        );
766
767        let check_num_reward_distribution_blocks =
768            |num_stakes: u64, expected_num_reward_distribution_blocks: u64| {
769                // Given the short epoch, i.e. 32 slots, we should cap the number of reward distribution blocks to 32/10 = 3.
770                let stake_rewards = (0..num_stakes)
771                    .map(|_| Some(PartitionedStakeReward::new_random()))
772                    .collect::<PartitionedStakeRewards>();
773
774                assert_eq!(
775                    bank.get_reward_distribution_num_blocks(&stake_rewards),
776                    expected_num_reward_distribution_blocks
777                );
778            };
779
780        for test_record in [
781            // num_stakes, expected_num_reward_distribution_blocks
782            (0, 1),
783            (1, 1),
784            (stake_account_stores_per_block, 1),
785            (2 * stake_account_stores_per_block - 1, 2),
786            (2 * stake_account_stores_per_block, 2),
787            (3 * stake_account_stores_per_block - 1, 3),
788            (3 * stake_account_stores_per_block, 3),
789            (4 * stake_account_stores_per_block, 3), // cap at 3
790            (5 * stake_account_stores_per_block, 3), //cap at 3
791        ] {
792            check_num_reward_distribution_blocks(test_record.0, test_record.1);
793        }
794    }
795
796    /// Test get_reward_distribution_num_blocks during normal epoch gives the expected result
797    #[test]
798    fn test_get_reward_distribution_num_blocks_normal() {
799        agave_logger::setup();
800        let (mut genesis_config, _mint_keypair) =
801            create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
802        genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false);
803
804        let bank = Bank::new_for_tests(&genesis_config);
805
806        // Given 8k rewards, it will take 2 blocks to credit all the rewards
807        let expected_num = 8192;
808        let stake_rewards = (0..expected_num)
809            .map(|_| Some(PartitionedStakeReward::new_random()))
810            .collect::<PartitionedStakeRewards>();
811
812        assert_eq!(bank.get_reward_distribution_num_blocks(&stake_rewards), 2);
813    }
814
815    /// Test get_reward_distribution_num_blocks during warm up epoch gives the expected result.
816    /// The num_credit_blocks should be 1 during warm up epoch.
817    #[test]
818    fn test_get_reward_distribution_num_blocks_warmup() {
819        let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
820
821        let bank = Bank::new_for_tests(&genesis_config);
822        let rewards = PartitionedStakeRewards::default();
823        assert_eq!(bank.get_reward_distribution_num_blocks(&rewards), 1);
824    }
825
826    /// Test get_reward_distribution_num_blocks with `None` elements in the
827    /// partitioned stake rewards. `None` elements can occur if for any stake
828    /// delegation:
829    /// * there is no payout or if any deserved payout is < 1 lamport
830    /// * corresponding vote account was not found in cache and accounts-db
831    #[test]
832    fn test_get_reward_distribution_num_blocks_none() {
833        let rewards_all = 8192;
834        let expected_rewards_some = 6144;
835
836        let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL);
837        let bank = Bank::new_for_tests(&genesis_config);
838
839        let rewards = (0..rewards_all)
840            .map(|i| {
841                if i % 4 == 0 {
842                    None
843                } else {
844                    Some(PartitionedStakeReward::new_random())
845                }
846            })
847            .collect::<PartitionedStakeRewards>();
848        assert_eq!(rewards.rewards.len(), rewards_all);
849        assert_eq!(rewards.num_rewards(), expected_rewards_some);
850
851        assert_eq!(bank.get_reward_distribution_num_blocks(&rewards), 1);
852    }
853
854    #[test]
855    fn test_rewards_computation_and_partitioned_distribution_one_block() {
856        agave_logger::setup();
857
858        let starting_slot = SLOTS_PER_EPOCH - 1;
859        let (
860            RewardBank {
861                bank: mut previous_bank,
862                ..
863            },
864            bank_forks,
865        ) = create_default_reward_bank(100, starting_slot - 1);
866
867        // simulate block progress
868        for slot in starting_slot..=(2 * SLOTS_PER_EPOCH) + 2 {
869            let pre_cap = previous_bank.capitalization();
870            let curr_bank = Bank::new_from_parent_with_bank_forks(
871                bank_forks.as_ref(),
872                previous_bank.clone(),
873                SlotLeader::default(),
874                slot,
875            );
876            let post_cap = curr_bank.capitalization();
877
878            if slot % SLOTS_PER_EPOCH == 0 {
879                // This is the first block of the epoch. Reward computation should happen in this block.
880                // assert reward compute status activated at epoch boundary
881                assert_matches!(
882                    curr_bank.get_reward_interval(),
883                    RewardInterval::InsideInterval
884                );
885
886                assert!(curr_bank.is_calculated());
887
888                // after reward calculation, the cache should be filled.
889                assert!(
890                    curr_bank
891                        .get_epoch_rewards_from_cache(&curr_bank.parent_hash)
892                        .is_some()
893                );
894                assert_eq!(post_cap, pre_cap);
895
896                // Make a root the bank, which is the first bank in the epoch.
897                // This will clear the cache.
898                let _ = bank_forks.write().unwrap().set_root(slot, None, None);
899                assert_eq!(curr_bank.get_epoch_rewards_cache_len(), 0);
900            } else if slot == SLOTS_PER_EPOCH + 1 {
901                // 1. when curr_slot == SLOTS_PER_EPOCH + 1, the 2nd block of
902                // epoch 1, reward distribution should happen in this block.
903                // however, all stake rewards are paid at this block therefore
904                // reward_status should have transitioned to inactive. The cap
905                // should increase accordingly.
906                assert_matches!(
907                    curr_bank.get_reward_interval(),
908                    RewardInterval::OutsideInterval
909                );
910                let account = curr_bank
911                    .get_account(&solana_sysvar::epoch_rewards::id())
912                    .unwrap();
913                let epoch_rewards: solana_sysvar::epoch_rewards::EpochRewards =
914                    from_account(&account).unwrap();
915                assert_eq!(post_cap, pre_cap + epoch_rewards.distributed_rewards);
916            } else {
917                // 2. when curr_slot == SLOTS_PER_EPOCH + 2, the 3rd block of
918                // epoch 1 (or any other slot). reward distribution should have
919                // already completed. Therefore, reward_status should stay
920                // inactive and cap should stay the same.
921                assert_matches!(
922                    curr_bank.get_reward_interval(),
923                    RewardInterval::OutsideInterval
924                );
925
926                // slot is not in rewards, cap should not change
927                assert_eq!(post_cap, pre_cap);
928            }
929            // EpochRewards sysvar is created in the first block of epoch 1.
930            // Ensure the sysvar persists thereafter.
931            if slot >= SLOTS_PER_EPOCH {
932                let epoch_rewards_lamports =
933                    curr_bank.get_balance(&solana_sysvar::epoch_rewards::id());
934                assert!(epoch_rewards_lamports > 0);
935            }
936            previous_bank = curr_bank;
937        }
938    }
939
940    /// Test rewards computation and partitioned rewards distribution at the
941    /// epoch boundary (multiple reward distribution blocks)
942    #[test]
943    fn test_rewards_computation_and_partitioned_distribution_multi_blocks() {
944        agave_logger::setup();
945
946        let starting_slot = SLOTS_PER_EPOCH - 1;
947        let (
948            RewardBank {
949                bank: mut previous_bank,
950                ..
951            },
952            bank_forks,
953        ) = create_reward_bank(100, 50, starting_slot - 1);
954        let mut starting_hash = None;
955        let mut reward_distribution_completion_slot = None;
956
957        // simulate block progress
958        for slot in starting_slot..=SLOTS_PER_EPOCH.saturating_mul(2) {
959            let pre_cap = previous_bank.capitalization();
960
961            let pre_sysvar_account = previous_bank
962                .get_account(&solana_sysvar::epoch_rewards::id())
963                .unwrap_or_default();
964            let pre_epoch_rewards: solana_sysvar::epoch_rewards::EpochRewards =
965                from_account(&pre_sysvar_account).unwrap_or_default();
966            let pre_distributed_rewards = pre_epoch_rewards.distributed_rewards;
967            let curr_bank = Bank::new_from_parent_with_bank_forks(
968                bank_forks.as_ref(),
969                previous_bank.clone(),
970                SlotLeader::default(),
971                slot,
972            );
973            let post_cap = curr_bank.capitalization();
974
975            if slot < SLOTS_PER_EPOCH {
976                // Verify we haven't hit the rewards interval yet.
977                assert_matches!(
978                    curr_bank.get_reward_interval(),
979                    RewardInterval::OutsideInterval
980                );
981                assert_eq!(post_cap, pre_cap);
982            } else if slot == SLOTS_PER_EPOCH {
983                // This is the first block of epoch 1. Reward computation should happen in this block.
984                // assert reward compute status activated at epoch boundary
985                assert_matches!(
986                    curr_bank.get_reward_interval(),
987                    RewardInterval::InsideInterval
988                );
989
990                // calculation block, state should be calculated.
991                assert!(curr_bank.is_calculated());
992
993                // after reward calculation, the cache should be filled.
994                assert!(
995                    curr_bank
996                        .get_epoch_rewards_from_cache(&curr_bank.parent_hash)
997                        .is_some()
998                );
999                assert_eq!(curr_bank.get_epoch_rewards_cache_len(), 1);
1000                starting_hash = Some(curr_bank.parent_hash);
1001
1002                // Grab the number of slots to complete rewards payout from the
1003                // epoch rewards sysvar.
1004                let account = curr_bank
1005                    .get_account(&solana_sysvar::epoch_rewards::id())
1006                    .unwrap();
1007                let epoch_rewards: solana_sysvar::epoch_rewards::EpochRewards =
1008                    from_account(&account).unwrap();
1009                reward_distribution_completion_slot =
1010                    Some(SLOTS_PER_EPOCH + epoch_rewards.num_partitions);
1011            } else if slot
1012                < reward_distribution_completion_slot
1013                    .expect("epoch boundary must set completion slot")
1014            {
1015                // Reward distribution should be active in this range.
1016                assert_matches!(
1017                    curr_bank.get_reward_interval(),
1018                    RewardInterval::InsideInterval
1019                );
1020                assert!(curr_bank.is_partitioned());
1021
1022                let account = curr_bank
1023                    .get_account(&solana_sysvar::epoch_rewards::id())
1024                    .unwrap();
1025                let epoch_rewards: solana_sysvar::epoch_rewards::EpochRewards =
1026                    from_account(&account).unwrap();
1027                assert_eq!(
1028                    post_cap,
1029                    pre_cap + epoch_rewards.distributed_rewards - pre_distributed_rewards
1030                );
1031
1032                if slot == SLOTS_PER_EPOCH + 1 {
1033                    // The first block of the epoch has not rooted yet, so the
1034                    // cache should still have the results.
1035                    assert!(
1036                        curr_bank
1037                            .get_epoch_rewards_from_cache(&starting_hash.unwrap())
1038                            .is_some()
1039                    );
1040                    assert_eq!(curr_bank.get_epoch_rewards_cache_len(), 1);
1041
1042                    // Now make a root the first bank in the epoch.
1043                    // This should clear the cache.
1044                    let _ = bank_forks.write().unwrap().set_root(slot - 1, None, None);
1045                    assert_eq!(curr_bank.get_epoch_rewards_cache_len(), 0);
1046                }
1047            } else if slot
1048                == reward_distribution_completion_slot
1049                    .expect("epoch boundary must set completion slot")
1050            {
1051                // All stake rewards should have been paid by now. Therefore,
1052                // reward_status should have transitioned to inactive. The cap
1053                // should increase accordingly.
1054                assert_matches!(
1055                    curr_bank.get_reward_interval(),
1056                    RewardInterval::OutsideInterval
1057                );
1058
1059                let account = curr_bank
1060                    .get_account(&solana_sysvar::epoch_rewards::id())
1061                    .unwrap();
1062                let epoch_rewards: solana_sysvar::epoch_rewards::EpochRewards =
1063                    from_account(&account).unwrap();
1064                assert_eq!(
1065                    post_cap,
1066                    pre_cap + epoch_rewards.distributed_rewards - pre_distributed_rewards
1067                );
1068            } else {
1069                // First slot after rewards payout. Verify we are outside the
1070                // interval and capitalization doesn't change this slot.
1071                assert_matches!(
1072                    curr_bank.get_reward_interval(),
1073                    RewardInterval::OutsideInterval
1074                );
1075                assert_eq!(post_cap, pre_cap);
1076                break;
1077            }
1078            previous_bank = curr_bank;
1079        }
1080    }
1081
1082    /// Test that lamports can be sent to stake accounts regardless of rewards period.
1083    #[test]
1084    fn test_rewards_period_system_transfer() {
1085        let validator_vote_keypairs = ValidatorVoteKeypairs::new_rand();
1086        let validator_keypairs = vec![&validator_vote_keypairs];
1087        let GenesisConfigInfo {
1088            mut genesis_config,
1089            mint_keypair,
1090            ..
1091        } = create_genesis_config_with_vote_accounts(
1092            1_000_000_000,
1093            &validator_keypairs,
1094            vec![1_000_000_000; 1],
1095        );
1096
1097        // Add stake account to try to mutate
1098        let vote_key = validator_keypairs[0].vote_keypair.pubkey();
1099        let vote_account = genesis_config
1100            .accounts
1101            .iter()
1102            .find(|(address, _)| **address == vote_key)
1103            .map(|(_, account)| account)
1104            .unwrap()
1105            .clone();
1106
1107        let new_stake_signer = Keypair::new();
1108        let new_stake_address = new_stake_signer.pubkey();
1109        let new_stake_account = Account::from(stake_utils::create_stake_account(
1110            &new_stake_address,
1111            &vote_key,
1112            &vote_account.into(),
1113            &genesis_config.rent,
1114            2_000_000_000,
1115        ));
1116        genesis_config
1117            .accounts
1118            .extend(vec![(new_stake_address, new_stake_account)]);
1119
1120        let (mut previous_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config);
1121        let num_slots_in_epoch = previous_bank.get_slots_in_epoch(previous_bank.epoch());
1122        assert_eq!(num_slots_in_epoch, 32);
1123
1124        let transfer_amount = 5_000;
1125
1126        for slot in 1..=num_slots_in_epoch + 2 {
1127            let bank = Bank::new_from_parent_with_bank_forks(
1128                bank_forks.as_ref(),
1129                previous_bank.clone(),
1130                SlotLeader::default(),
1131                slot,
1132            );
1133
1134            // Fill bank_forks with banks with votes landing in the next slot
1135            // So that rewards will be paid out at the epoch boundary, i.e. slot = 32
1136            let tower_sync = TowerSync::new_from_slot(slot - 1, previous_bank.hash());
1137            let vote = vote_transaction::new_tower_sync_transaction(
1138                tower_sync,
1139                previous_bank.last_blockhash(),
1140                &validator_vote_keypairs.node_keypair,
1141                &validator_vote_keypairs.vote_keypair,
1142                &validator_vote_keypairs.vote_keypair,
1143                None,
1144            );
1145            bank.process_transaction(&vote).unwrap();
1146
1147            // Insert a transfer transaction from the mint to new stake account
1148            let system_tx = system_transaction::transfer(
1149                &mint_keypair,
1150                &new_stake_address,
1151                transfer_amount,
1152                bank.last_blockhash(),
1153            );
1154            let system_result = bank.process_transaction(&system_tx);
1155
1156            // Credits should always succeed
1157            assert!(system_result.is_ok());
1158
1159            // Push a dummy blockhash, so that the latest_blockhash() for the transfer transaction in each
1160            // iteration are different. Otherwise, all those transactions will be the same, and will not be
1161            // executed by the bank except the first one.
1162            bank.register_unique_recent_blockhash_for_test();
1163            previous_bank = bank;
1164        }
1165    }
1166
1167    #[test]
1168    fn test_get_rewards_and_partitions() {
1169        let starting_slot = SLOTS_PER_EPOCH - 1;
1170        let num_rewards = 100;
1171        let stake_account_stores_per_block = 50;
1172        let (RewardBank { bank, .. }, bank_forks) =
1173            create_reward_bank(num_rewards, stake_account_stores_per_block, starting_slot);
1174
1175        // Slot before the epoch boundary contains empty rewards (since fees are
1176        // off), and no partitions because not at the epoch boundary
1177        assert_eq!(
1178            bank.get_rewards_and_num_partitions(),
1179            KeyedRewardsAndNumPartitions {
1180                keyed_rewards: vec![],
1181                num_partitions: None,
1182            }
1183        );
1184
1185        let epoch_boundary_bank = Bank::new_from_parent_with_bank_forks(
1186            bank_forks.as_ref(),
1187            bank,
1188            SlotLeader::default(),
1189            SLOTS_PER_EPOCH,
1190        );
1191        // Slot at the epoch boundary contains voting rewards only, as well as partition data
1192        let KeyedRewardsAndNumPartitions {
1193            keyed_rewards,
1194            num_partitions,
1195        } = epoch_boundary_bank.get_rewards_and_num_partitions();
1196        for (_pubkey, reward) in keyed_rewards.iter() {
1197            assert_eq!(reward.reward_type, RewardType::Voting);
1198        }
1199        assert_eq!(keyed_rewards.len(), num_rewards);
1200        let expected_num_partitions = (num_rewards as u64)
1201            .div_ceil(epoch_boundary_bank.partitioned_rewards_stake_account_stores_per_block())
1202            .clamp(1, (SLOTS_PER_EPOCH / 10).max(1));
1203        assert_eq!(num_partitions, Some(expected_num_partitions));
1204
1205        let mut total_staking_rewards = 0;
1206        let mut previous_bank = epoch_boundary_bank;
1207        for partition_index in 0..expected_num_partitions {
1208            let partition_bank = Arc::new(Bank::new_from_parent(
1209                previous_bank,
1210                SlotLeader::default(),
1211                SLOTS_PER_EPOCH + partition_index + 1,
1212            ));
1213            // Slot after the epoch boundary contains partitioned staking
1214            // rewards, and no partition metadata because it's not an epoch
1215            // boundary bank.
1216            let KeyedRewardsAndNumPartitions {
1217                keyed_rewards,
1218                num_partitions,
1219            } = partition_bank.get_rewards_and_num_partitions();
1220            for (_pubkey, reward) in keyed_rewards.iter() {
1221                assert_eq!(reward.reward_type, RewardType::Staking);
1222            }
1223            total_staking_rewards += keyed_rewards.len();
1224            assert_eq!(num_partitions, None);
1225            previous_bank = partition_bank;
1226        }
1227
1228        // All rewards are recorded
1229        assert_eq!(total_staking_rewards, num_rewards);
1230        let bank = Bank::new_from_parent(
1231            previous_bank,
1232            SlotLeader::default(),
1233            SLOTS_PER_EPOCH + expected_num_partitions + 1,
1234        );
1235        // Next slot contains empty rewards (since fees are off), and no
1236        // partitions because not at the epoch boundary
1237        assert_eq!(
1238            bank.get_rewards_and_num_partitions(),
1239            KeyedRewardsAndNumPartitions {
1240                keyed_rewards: vec![],
1241                num_partitions: None,
1242            }
1243        );
1244    }
1245
1246    #[test]
1247    fn test_rewards_and_partitions_should_record() {
1248        let reward = RewardInfo {
1249            reward_type: RewardType::Voting,
1250            lamports: 55,
1251            post_balance: 5555,
1252            commission_bps: Some(500),
1253        };
1254
1255        let rewards_and_partitions = KeyedRewardsAndNumPartitions {
1256            keyed_rewards: vec![],
1257            num_partitions: None,
1258        };
1259        assert!(!rewards_and_partitions.should_record());
1260
1261        let rewards_and_partitions = KeyedRewardsAndNumPartitions {
1262            keyed_rewards: vec![(Pubkey::new_unique(), reward)],
1263            num_partitions: None,
1264        };
1265        assert!(rewards_and_partitions.should_record());
1266
1267        let rewards_and_partitions = KeyedRewardsAndNumPartitions {
1268            keyed_rewards: vec![],
1269            num_partitions: Some(42),
1270        };
1271        assert!(rewards_and_partitions.should_record());
1272
1273        let rewards_and_partitions = KeyedRewardsAndNumPartitions {
1274            keyed_rewards: vec![(Pubkey::new_unique(), reward)],
1275            num_partitions: Some(42),
1276        };
1277        assert!(rewards_and_partitions.should_record());
1278    }
1279}