|
|
@@ -150,7 +150,36 @@ pub(super) struct CalculateRewardsAndDistributeVoteRewardsResult {
|
|
|
|
|
|
pub(crate) type StakeRewards = Vec<StakeReward>;
|
|
|
|
|
|
+#[derive(Debug, PartialEq)]
|
|
|
+pub struct KeyedRewardsAndNumPartitions {
|
|
|
+ pub keyed_rewards: Vec<(Pubkey, RewardInfo)>,
|
|
|
+ pub num_partitions: Option<u64>,
|
|
|
+}
|
|
|
+
|
|
|
+impl KeyedRewardsAndNumPartitions {
|
|
|
+ pub fn should_record(&self) -> bool {
|
|
|
+ !self.keyed_rewards.is_empty() || self.num_partitions.is_some()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
impl Bank {
|
|
|
+ pub fn get_rewards_and_num_partitions(&self) -> KeyedRewardsAndNumPartitions {
|
|
|
+ let keyed_rewards = self.rewards.read().unwrap().clone();
|
|
|
+ let epoch_rewards_sysvar = self.get_epoch_rewards_sysvar();
|
|
|
+ // If partitioned epoch rewards are active and this Bank is the
|
|
|
+ // epoch-boundary block, populate num_partitions
|
|
|
+ let epoch_schedule = self.epoch_schedule();
|
|
|
+ let parent_epoch = epoch_schedule.get_epoch(self.parent_slot());
|
|
|
+ let is_first_block_in_epoch = self.epoch() > parent_epoch;
|
|
|
+
|
|
|
+ let num_partitions = (epoch_rewards_sysvar.active && is_first_block_in_epoch)
|
|
|
+ .then_some(epoch_rewards_sysvar.num_partitions);
|
|
|
+ KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards,
|
|
|
+ num_partitions,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
pub(super) fn is_partitioned_rewards_feature_enabled(&self) -> bool {
|
|
|
self.feature_set
|
|
|
.is_active(&feature_set::enable_partitioned_epoch_reward::id())
|
|
|
@@ -248,6 +277,7 @@ mod tests {
|
|
|
account::Account,
|
|
|
epoch_schedule::EpochSchedule,
|
|
|
native_token::LAMPORTS_PER_SOL,
|
|
|
+ reward_type::RewardType,
|
|
|
signature::Signer,
|
|
|
signer::keypair::Keypair,
|
|
|
stake::instruction::StakeError,
|
|
|
@@ -684,7 +714,7 @@ mod tests {
|
|
|
|
|
|
/// Test that program execution that attempts to mutate a stake account
|
|
|
/// incorrectly should fail during reward period. A credit should succeed,
|
|
|
- /// but a withdrawal shoudl fail.
|
|
|
+ /// but a withdrawal should fail.
|
|
|
#[test]
|
|
|
fn test_program_execution_restricted_for_stake_account_in_reward_period() {
|
|
|
use solana_sdk::transaction::TransactionError::InstructionError;
|
|
|
@@ -800,4 +830,242 @@ mod tests {
|
|
|
previous_bank = bank;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_get_rewards_and_partitions() {
|
|
|
+ let starting_slot = SLOTS_PER_EPOCH - 1;
|
|
|
+ let num_rewards = 100;
|
|
|
+ let stake_account_stores_per_block = 50;
|
|
|
+ let RewardBank { bank, .. } =
|
|
|
+ create_reward_bank(num_rewards, stake_account_stores_per_block, starting_slot);
|
|
|
+
|
|
|
+ assert!(bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot before the epoch boundary contains empty rewards (since fees are
|
|
|
+ // off), and no partitions because not at the epoch boundary
|
|
|
+ assert_eq!(
|
|
|
+ bank.get_rewards_and_num_partitions(),
|
|
|
+ KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: None,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ let epoch_boundary_bank = Arc::new(Bank::new_from_parent(
|
|
|
+ bank,
|
|
|
+ &Pubkey::default(),
|
|
|
+ SLOTS_PER_EPOCH,
|
|
|
+ ));
|
|
|
+ assert!(epoch_boundary_bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot at the epoch boundary contains voting rewards only, as well as partition data
|
|
|
+ let KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards,
|
|
|
+ num_partitions,
|
|
|
+ } = epoch_boundary_bank.get_rewards_and_num_partitions();
|
|
|
+ for (_pubkey, reward) in keyed_rewards.iter() {
|
|
|
+ assert_eq!(reward.reward_type, RewardType::Voting);
|
|
|
+ }
|
|
|
+ assert_eq!(keyed_rewards.len(), num_rewards);
|
|
|
+ assert_eq!(
|
|
|
+ num_partitions,
|
|
|
+ Some(num_rewards as u64 / stake_account_stores_per_block)
|
|
|
+ );
|
|
|
+
|
|
|
+ let mut total_staking_rewards = 0;
|
|
|
+
|
|
|
+ let partition0_bank = Arc::new(Bank::new_from_parent(
|
|
|
+ epoch_boundary_bank,
|
|
|
+ &Pubkey::default(),
|
|
|
+ SLOTS_PER_EPOCH + 1,
|
|
|
+ ));
|
|
|
+ assert!(partition0_bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot after the epoch boundary contains first partition of staking
|
|
|
+ // rewards, and no partitions because not at the epoch boundary
|
|
|
+ let KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards,
|
|
|
+ num_partitions,
|
|
|
+ } = partition0_bank.get_rewards_and_num_partitions();
|
|
|
+ for (_pubkey, reward) in keyed_rewards.iter() {
|
|
|
+ assert_eq!(reward.reward_type, RewardType::Staking);
|
|
|
+ }
|
|
|
+ total_staking_rewards += keyed_rewards.len();
|
|
|
+ assert_eq!(num_partitions, None);
|
|
|
+
|
|
|
+ let partition1_bank = Arc::new(Bank::new_from_parent(
|
|
|
+ partition0_bank,
|
|
|
+ &Pubkey::default(),
|
|
|
+ SLOTS_PER_EPOCH + 2,
|
|
|
+ ));
|
|
|
+ assert!(partition1_bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot 2 after the epoch boundary contains second partition of staking
|
|
|
+ // rewards, and no partitions because not at the epoch boundary
|
|
|
+ let KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards,
|
|
|
+ num_partitions,
|
|
|
+ } = partition1_bank.get_rewards_and_num_partitions();
|
|
|
+ for (_pubkey, reward) in keyed_rewards.iter() {
|
|
|
+ assert_eq!(reward.reward_type, RewardType::Staking);
|
|
|
+ }
|
|
|
+ total_staking_rewards += keyed_rewards.len();
|
|
|
+ assert_eq!(num_partitions, None);
|
|
|
+
|
|
|
+ // All rewards are recorded
|
|
|
+ assert_eq!(total_staking_rewards, num_rewards);
|
|
|
+
|
|
|
+ let bank = Bank::new_from_parent(partition1_bank, &Pubkey::default(), SLOTS_PER_EPOCH + 3);
|
|
|
+ assert!(bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Next slot contains empty rewards (since fees are off), and no
|
|
|
+ // partitions because not at the epoch boundary
|
|
|
+ assert_eq!(
|
|
|
+ bank.get_rewards_and_num_partitions(),
|
|
|
+ KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: None,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_get_rewards_and_partitions_before_feature() {
|
|
|
+ let starting_slot = SLOTS_PER_EPOCH - 1;
|
|
|
+ let num_rewards = 100;
|
|
|
+
|
|
|
+ let validator_keypairs = (0..num_rewards)
|
|
|
+ .map(|_| ValidatorVoteKeypairs::new_rand())
|
|
|
+ .collect::<Vec<_>>();
|
|
|
+
|
|
|
+ let GenesisConfigInfo {
|
|
|
+ mut genesis_config, ..
|
|
|
+ } = create_genesis_config_with_vote_accounts(
|
|
|
+ 1_000_000_000,
|
|
|
+ &validator_keypairs,
|
|
|
+ vec![2_000_000_000; num_rewards],
|
|
|
+ );
|
|
|
+ genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH);
|
|
|
+
|
|
|
+ // Set feature to inactive
|
|
|
+ genesis_config
|
|
|
+ .accounts
|
|
|
+ .remove(&feature_set::enable_partitioned_epoch_reward::id());
|
|
|
+
|
|
|
+ let bank = Bank::new_for_tests(&genesis_config);
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests();
|
|
|
+ let bank = new_bank_from_parent_with_bank_forks(
|
|
|
+ &bank_forks,
|
|
|
+ bank,
|
|
|
+ &Pubkey::default(),
|
|
|
+ starting_slot,
|
|
|
+ );
|
|
|
+
|
|
|
+ assert!(!bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot before the epoch boundary contains empty rewards (since fees are
|
|
|
+ // off), and no partitions because feature is inactive
|
|
|
+ assert_eq!(
|
|
|
+ bank.get_rewards_and_num_partitions(),
|
|
|
+ KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: None,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ let epoch_boundary_bank = Arc::new(Bank::new_from_parent(
|
|
|
+ bank,
|
|
|
+ &Pubkey::default(),
|
|
|
+ SLOTS_PER_EPOCH,
|
|
|
+ ));
|
|
|
+ assert!(!epoch_boundary_bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot at the epoch boundary contains voting rewards and staking rewards; still no partitions
|
|
|
+ let KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards,
|
|
|
+ num_partitions,
|
|
|
+ } = epoch_boundary_bank.get_rewards_and_num_partitions();
|
|
|
+ let mut voting_rewards_count = 0;
|
|
|
+ let mut staking_rewards_count = 0;
|
|
|
+ for (_pubkey, reward) in keyed_rewards.iter() {
|
|
|
+ match reward.reward_type {
|
|
|
+ RewardType::Voting => {
|
|
|
+ voting_rewards_count += 1;
|
|
|
+ }
|
|
|
+ RewardType::Staking => {
|
|
|
+ staking_rewards_count += 1;
|
|
|
+ }
|
|
|
+ _ => {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assert_eq!(
|
|
|
+ keyed_rewards.len(),
|
|
|
+ voting_rewards_count + staking_rewards_count
|
|
|
+ );
|
|
|
+ assert_eq!(voting_rewards_count, num_rewards);
|
|
|
+ assert_eq!(staking_rewards_count, num_rewards);
|
|
|
+ assert!(num_partitions.is_none());
|
|
|
+
|
|
|
+ let bank =
|
|
|
+ Bank::new_from_parent(epoch_boundary_bank, &Pubkey::default(), SLOTS_PER_EPOCH + 1);
|
|
|
+ assert!(!bank.is_partitioned_rewards_feature_enabled());
|
|
|
+ // Slot after the epoch boundary contains empty rewards (since fees are
|
|
|
+ // off), and no partitions because feature is inactive
|
|
|
+ assert_eq!(
|
|
|
+ bank.get_rewards_and_num_partitions(),
|
|
|
+ KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: None,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_rewards_and_partitions_should_record() {
|
|
|
+ let reward = RewardInfo {
|
|
|
+ reward_type: RewardType::Voting,
|
|
|
+ lamports: 55,
|
|
|
+ post_balance: 5555,
|
|
|
+ commission: Some(5),
|
|
|
+ };
|
|
|
+
|
|
|
+ let rewards_and_partitions = KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: None,
|
|
|
+ };
|
|
|
+ assert!(!rewards_and_partitions.should_record());
|
|
|
+
|
|
|
+ let rewards_and_partitions = KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![(Pubkey::new_unique(), reward)],
|
|
|
+ num_partitions: None,
|
|
|
+ };
|
|
|
+ assert!(rewards_and_partitions.should_record());
|
|
|
+
|
|
|
+ let rewards_and_partitions = KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![],
|
|
|
+ num_partitions: Some(42),
|
|
|
+ };
|
|
|
+ assert!(rewards_and_partitions.should_record());
|
|
|
+
|
|
|
+ let rewards_and_partitions = KeyedRewardsAndNumPartitions {
|
|
|
+ keyed_rewards: vec![(Pubkey::new_unique(), reward)],
|
|
|
+ num_partitions: Some(42),
|
|
|
+ };
|
|
|
+ assert!(rewards_and_partitions.should_record());
|
|
|
+ }
|
|
|
}
|