diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 9bbe143de2..da92443ae4 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -42,8 +42,8 @@ use { builtins::{BuiltinPrototype, BUILTINS}, metrics::*, partitioned_epoch_rewards::{ - EpochRewardCalculateParamInfo, EpochRewardStatus, PartitionedRewardsCalculation, - RewardInterval, StakeRewards, VoteRewardsAccounts, + EpochRewardCalculateParamInfo, EpochRewardStatus, RewardInterval, StakeRewards, + VoteRewardsAccounts, }, }, bank_forks::BankForks, @@ -2476,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, diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 0921f97a2f..bda22852c6 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -1,14 +1,12 @@ use { super::{ - Bank, CalculateRewardsAndDistributeVoteRewardsResult, EpochRewardCalculateParamInfo, + epoch_rewards_hasher::hash_rewards_into_partitions, Bank, + CalculateRewardsAndDistributeVoteRewardsResult, EpochRewardCalculateParamInfo, PartitionedRewardsCalculation, StakeRewardCalculationPartitioned, VoteRewardsAccounts, }, - crate::{ - bank::{ - PrevEpochInflationRewards, RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, - StakeRewardCalculation, VoteAccount, - }, - epoch_rewards_hasher::hash_rewards_into_partitions, + crate::bank::{ + PrevEpochInflationRewards, RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, + StakeRewardCalculation, VoteAccount, }, log::info, rayon::{ 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 0000000000..4ff4d67fdc --- /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 index 79c73ed5b9..6975119a8b 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -136,8 +136,9 @@ impl Bank { mod tests { use { super::*, - crate::{ - bank::tests::create_genesis_config, epoch_rewards_hasher::hash_rewards_into_partitions, + crate::bank::{ + partitioned_epoch_rewards::epoch_rewards_hasher::hash_rewards_into_partitions, + tests::create_genesis_config, }, rand::Rng, solana_sdk::{ diff --git a/runtime/src/epoch_rewards_hasher.rs b/runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs similarity index 69% rename from runtime/src/epoch_rewards_hasher.rs rename to runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs index ddf45a9095..4495a59dca 100644 --- a/runtime/src/epoch_rewards_hasher.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/epoch_rewards_hasher.rs @@ -3,7 +3,7 @@ use { 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 index e50b521767..3af20f0ddc 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -1,5 +1,7 @@ mod calculation; +mod compare; mod distribution; +mod epoch_rewards_hasher; mod sysvar; use { @@ -188,7 +190,13 @@ impl Bank { mod tests { use { super::*, - crate::bank::tests::create_genesis_config, + 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, @@ -197,7 +205,14 @@ mod tests { partitioned_rewards::TestPartitionedEpochRewards, }, solana_program_runtime::runtime_config::RuntimeConfig, - solana_sdk::{epoch_schedule::EpochSchedule, native_token::LAMPORTS_PER_SOL}, + 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 { @@ -352,4 +367,287 @@ mod tests { + 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/tests.rs b/runtime/src/bank/tests.rs index 05a0d662f9..31e189e079 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, @@ -12126,307 +12123,6 @@ fn test_squash_timing_add_assign() { assert!(t0 == expected); } -/// 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 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 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] fn test_system_instruction_allocate() { let (genesis_config, mint_keypair) = create_genesis_config_no_tx_fee(sol_to_lamports(1.0)); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 12eab54a41..9ba006fc93 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;