diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 10efd9d033e334..a71d17cd267f2c 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -48,7 +48,6 @@ use { serde_snapshot::BankIncrementalSnapshotPersistence, snapshot_hash::SnapshotHash, stake_account::StakeAccount, - stake_history::StakeHistory, stake_weighted_timestamp::{ calculate_stake_weighted_timestamp, MaxAllowableDrift, MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST, MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW_V2, @@ -65,8 +64,7 @@ use { log::*, rayon::{ iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, - slice::ParallelSlice, - ThreadPool, ThreadPoolBuilder, + ThreadPoolBuilder, }, serde::Serialize, solana_accounts_db::{ @@ -88,7 +86,6 @@ use { blockhash_queue::BlockhashQueue, epoch_accounts_hash::EpochAccountsHash, sorted_storages::SortedStorages, - stake_rewards::StakeReward, storable_accounts::StorableAccounts, }, solana_bpf_loader_program::syscalls::{ @@ -157,10 +154,7 @@ use { }, transaction_context::{TransactionAccount, TransactionReturnData}, }, - solana_stake_program::{ - points::{InflationPointCalculationEvent, PointValue}, - stake_state::StakeStateV2, - }, + solana_stake_program::points::InflationPointCalculationEvent, solana_svm::{ account_loader::{collect_rent_from_account, LoadedTransaction}, account_overrides::AccountOverrides, @@ -2442,343 +2436,6 @@ impl Bank { } } - fn _load_vote_and_stake_accounts( - &self, - thread_pool: &ThreadPool, - reward_calc_tracer: Option, - ) -> LoadVoteAndStakeAccountsResult { - let stakes = self.stakes_cache.stakes(); - let stake_delegations = self.filter_stake_delegations(&stakes); - - // Obtain all unique voter pubkeys from stake delegations. - fn merge(mut acc: HashSet, other: HashSet) -> HashSet { - if acc.len() < other.len() { - return merge(other, acc); - } - acc.extend(other); - acc - } - let voter_pubkeys = thread_pool.install(|| { - stake_delegations - .par_iter() - .fold( - HashSet::default, - |mut voter_pubkeys, (_stake_pubkey, stake_account)| { - voter_pubkeys.insert(stake_account.delegation().voter_pubkey); - voter_pubkeys - }, - ) - .reduce(HashSet::default, merge) - }); - // Obtain vote-accounts for unique voter pubkeys. - let cached_vote_accounts = stakes.vote_accounts(); - let solana_vote_program: Pubkey = solana_vote_program::id(); - let vote_accounts_cache_miss_count = AtomicUsize::default(); - let get_vote_account = |vote_pubkey: &Pubkey| -> Option { - if let Some(vote_account) = cached_vote_accounts.get(vote_pubkey) { - return Some(vote_account.clone()); - } - // If accounts-db contains a valid vote account, then it should - // already have been cached in cached_vote_accounts; so the code - // below is only for sanity check, and can be removed once - // vote_accounts_cache_miss_count is shown to be always zero. - let account = self.get_account_with_fixed_root(vote_pubkey)?; - if account.owner() == &solana_vote_program - && VoteState::deserialize(account.data()).is_ok() - { - vote_accounts_cache_miss_count.fetch_add(1, Relaxed); - } - VoteAccount::try_from(account).ok() - }; - let invalid_vote_keys = DashMap::::new(); - let make_vote_delegations_entry = |vote_pubkey| { - let Some(vote_account) = get_vote_account(&vote_pubkey) else { - invalid_vote_keys.insert(vote_pubkey, InvalidCacheEntryReason::Missing); - return None; - }; - if vote_account.owner() != &solana_vote_program { - invalid_vote_keys.insert(vote_pubkey, InvalidCacheEntryReason::WrongOwner); - return None; - } - let vote_with_stake_delegations = VoteWithStakeDelegations { - vote_state: Arc::new(vote_account.vote_state().clone()), - vote_account: AccountSharedData::from(vote_account), - delegations: Vec::default(), - }; - Some((vote_pubkey, vote_with_stake_delegations)) - }; - let vote_with_stake_delegations_map: DashMap = - thread_pool.install(|| { - voter_pubkeys - .into_par_iter() - .filter_map(make_vote_delegations_entry) - .collect() - }); - // Join stake accounts with vote-accounts. - let push_stake_delegation = |(stake_pubkey, stake_account): (&Pubkey, &StakeAccount<_>)| { - let delegation = stake_account.delegation(); - let Some(mut vote_delegations) = - vote_with_stake_delegations_map.get_mut(&delegation.voter_pubkey) - else { - return; - }; - if let Some(reward_calc_tracer) = reward_calc_tracer.as_ref() { - let delegation = - InflationPointCalculationEvent::Delegation(*delegation, solana_vote_program); - let event = RewardCalculationEvent::Staking(stake_pubkey, &delegation); - reward_calc_tracer(&event); - } - let stake_delegation = (*stake_pubkey, stake_account.clone()); - vote_delegations.delegations.push(stake_delegation); - }; - thread_pool.install(|| { - stake_delegations - .into_par_iter() - .for_each(push_stake_delegation); - }); - LoadVoteAndStakeAccountsResult { - vote_with_stake_delegations_map, - invalid_vote_keys, - vote_accounts_cache_miss_count: vote_accounts_cache_miss_count.into_inner(), - } - } - - fn load_vote_and_stake_accounts( - &mut self, - thread_pool: &ThreadPool, - reward_calc_tracer: Option, - metrics: &mut RewardsMetrics, - ) -> VoteWithStakeDelegationsMap { - let ( - LoadVoteAndStakeAccountsResult { - vote_with_stake_delegations_map, - invalid_vote_keys, - vote_accounts_cache_miss_count, - }, - load_vote_and_stake_accounts_us, - ) = measure_us!({ - self._load_vote_and_stake_accounts(thread_pool, reward_calc_tracer.as_ref()) - }); - metrics - .load_vote_and_stake_accounts_us - .fetch_add(load_vote_and_stake_accounts_us, Relaxed); - metrics.vote_accounts_cache_miss_count += vote_accounts_cache_miss_count; - self.stakes_cache - .handle_invalid_keys(invalid_vote_keys, self.slot()); - vote_with_stake_delegations_map - } - - fn calculate_reward_points( - &self, - vote_with_stake_delegations_map: &VoteWithStakeDelegationsMap, - rewards: u64, - stake_history: &StakeHistory, - thread_pool: &ThreadPool, - metrics: &RewardsMetrics, - ) -> Option { - let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); - let (points, calculate_points_us) = measure_us!(thread_pool.install(|| { - vote_with_stake_delegations_map - .par_iter() - .map(|entry| { - let VoteWithStakeDelegations { - vote_state, - delegations, - .. - } = entry.value(); - - delegations - .par_iter() - .map(|(_stake_pubkey, stake_account)| { - solana_stake_program::points::calculate_points( - stake_account.stake_state(), - vote_state, - stake_history, - new_warmup_cooldown_rate_epoch, - ) - .unwrap_or(0) - }) - .sum::() - }) - .sum() - })); - metrics - .calculate_points_us - .fetch_add(calculate_points_us, Relaxed); - - (points > 0).then_some(PointValue { rewards, points }) - } - - fn redeem_rewards( - &self, - vote_with_stake_delegations_map: DashMap, - rewarded_epoch: Epoch, - point_value: PointValue, - stake_history: &StakeHistory, - thread_pool: &ThreadPool, - reward_calc_tracer: Option, - metrics: &mut RewardsMetrics, - ) -> (VoteRewards, StakeRewards) { - let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); - let vote_account_rewards: VoteRewards = - DashMap::with_capacity(vote_with_stake_delegations_map.len()); - let stake_delegation_iterator = vote_with_stake_delegations_map.into_par_iter().flat_map( - |( - vote_pubkey, - VoteWithStakeDelegations { - vote_state, - vote_account, - delegations, - }, - )| { - vote_account_rewards.insert( - vote_pubkey, - VoteReward { - vote_account, - commission: vote_state.commission, - vote_rewards: 0, - vote_needs_store: false, - }, - ); - delegations - .into_par_iter() - .map(move |delegation| (vote_pubkey, Arc::clone(&vote_state), delegation)) - }, - ); - - let (stake_rewards, redeem_rewards_us) = measure_us!(thread_pool.install(|| { - stake_delegation_iterator - .filter_map(|(vote_pubkey, vote_state, (stake_pubkey, stake_account))| { - // curry closure to add the contextual stake_pubkey - let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| { - // inner - move |inner_event: &_| { - outer(&RewardCalculationEvent::Staking(&stake_pubkey, inner_event)) - } - }); - let (mut stake_account, stake_state) = - <(AccountSharedData, StakeStateV2)>::from(stake_account); - let redeemed = solana_stake_program::rewards::redeem_rewards( - rewarded_epoch, - stake_state, - &mut stake_account, - &vote_state, - &point_value, - stake_history, - reward_calc_tracer.as_ref(), - new_warmup_cooldown_rate_epoch, - ); - if let Ok((stakers_reward, voters_reward)) = redeemed { - // track voter rewards - if let Some(VoteReward { - vote_account: _, - commission: _, - vote_rewards: vote_rewards_sum, - vote_needs_store, - }) = vote_account_rewards.get_mut(&vote_pubkey).as_deref_mut() - { - *vote_needs_store = true; - *vote_rewards_sum = vote_rewards_sum.saturating_add(voters_reward); - } - - let post_balance = stake_account.lamports(); - return Some(StakeReward { - stake_pubkey, - stake_reward_info: RewardInfo { - reward_type: RewardType::Staking, - lamports: i64::try_from(stakers_reward).unwrap(), - post_balance, - commission: Some(vote_state.commission), - }, - stake_account, - }); - } else { - debug!( - "solana_stake_program::rewards::redeem_rewards() failed for {}: {:?}", - stake_pubkey, redeemed - ); - } - None - }) - .collect() - })); - metrics.redeem_rewards_us += redeem_rewards_us; - (vote_account_rewards, stake_rewards) - } - - fn store_stake_accounts( - &self, - thread_pool: &ThreadPool, - stake_rewards: &[StakeReward], - metrics: &RewardsMetrics, - ) { - // store stake account even if stake_reward is 0 - // because credits observed has changed - let now = Instant::now(); - let slot = self.slot(); - self.stakes_cache.update_stake_accounts( - thread_pool, - stake_rewards, - self.new_warmup_cooldown_rate_epoch(), - ); - assert!(!self.freeze_started()); - thread_pool.install(|| { - stake_rewards.par_chunks(512).for_each(|chunk| { - let to_store = (slot, chunk); - self.update_bank_hash_stats(&to_store); - self.rc.accounts.store_accounts_cached(to_store); - }) - }); - metrics - .store_stake_accounts_us - .fetch_add(now.elapsed().as_micros() as u64, Relaxed); - } - - fn store_vote_accounts( - &self, - vote_account_rewards: VoteRewards, - metrics: &RewardsMetrics, - ) -> Vec<(Pubkey, RewardInfo)> { - let (vote_rewards, store_vote_accounts_us) = measure_us!(vote_account_rewards - .into_iter() - .filter_map( - |( - vote_pubkey, - VoteReward { - mut vote_account, - commission, - vote_rewards, - vote_needs_store, - }, - )| { - if let Err(err) = vote_account.checked_add_lamports(vote_rewards) { - debug!("reward redemption failed for {}: {:?}", vote_pubkey, err); - return None; - } - - if vote_needs_store { - self.store_account(&vote_pubkey, &vote_account); - } - - Some(( - vote_pubkey, - RewardInfo { - reward_type: RewardType::Voting, - lamports: vote_rewards as i64, - post_balance: vote_account.lamports(), - commission: Some(commission), - }, - )) - }, - ) - .collect::>()); - - metrics - .store_vote_accounts_us - .fetch_add(store_vote_accounts_us, Relaxed); - vote_rewards - } - /// return reward info for each vote account /// return account data for each vote account that needs to be stored /// This return value is a little awkward at the moment so that downstream existing code in the non-partitioned rewards code path can be re-used without duplication or modification. diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index 628e6d3c1cbfe9..edc9203edbd5c8 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -14,6 +14,7 @@ use { genesis_sysvar_and_builtin_program_lamports, GenesisConfigInfo, ValidatorVoteKeypairs, }, snapshot_bank_utils, snapshot_utils, + stake_history::StakeHistory, status_cache::MAX_CACHE_ENTRIES, }, agave_transaction_view::static_account_keys_frame::MAX_STATIC_ACCOUNTS_PER_PACKET, @@ -21,7 +22,7 @@ use { crossbeam_channel::{bounded, unbounded}, itertools::Itertools, rand::Rng, - rayon::ThreadPoolBuilder, + rayon::{ThreadPool, ThreadPoolBuilder}, serde::{Deserialize, Serialize}, solana_accounts_db::{ accounts::AccountAddressFilter, @@ -412,6 +413,11 @@ fn bank1_sysvar_delta() -> u64 { SLOT_HASHES_SYSVAR_MIN_BALANCE } +fn bank2_sysvar_delta() -> u64 { + const EPOCH_REWARDS_SYSVAR_MIN_BALANCE: u64 = 1_454_640; + EPOCH_REWARDS_SYSVAR_MIN_BALANCE +} + #[test] fn test_bank_capitalization() { let bank0 = Arc::new(Bank::new_for_tests(&GenesisConfig { @@ -1899,6 +1905,109 @@ fn test_bank_update_vote_stake_rewards() { }); } +impl Bank { + #[cfg(test)] + fn _load_vote_and_stake_accounts( + &self, + thread_pool: &ThreadPool, + reward_calc_tracer: Option, + ) -> LoadVoteAndStakeAccountsResult { + let stakes = self.stakes_cache.stakes(); + let stake_delegations = self.filter_stake_delegations(&stakes); + // Obtain all unique voter pubkeys from stake delegations. + fn merge(mut acc: HashSet, other: HashSet) -> HashSet { + if acc.len() < other.len() { + return merge(other, acc); + } + acc.extend(other); + acc + } + let voter_pubkeys = thread_pool.install(|| { + stake_delegations + .par_iter() + .fold( + HashSet::default, + |mut voter_pubkeys, (_stake_pubkey, stake_account)| { + voter_pubkeys.insert(stake_account.delegation().voter_pubkey); + voter_pubkeys + }, + ) + .reduce(HashSet::default, merge) + }); + // Obtain vote-accounts for unique voter pubkeys. + let cached_vote_accounts = stakes.vote_accounts(); + let solana_vote_program: Pubkey = solana_vote_program::id(); + let vote_accounts_cache_miss_count = AtomicUsize::default(); + let get_vote_account = |vote_pubkey: &Pubkey| -> Option { + if let Some(vote_account) = cached_vote_accounts.get(vote_pubkey) { + return Some(vote_account.clone()); + } + // If accounts-db contains a valid vote account, then it should + // already have been cached in cached_vote_accounts; so the code + // below is only for sanity check, and can be removed once + // vote_accounts_cache_miss_count is shown to be always zero. + let account = self.get_account_with_fixed_root(vote_pubkey)?; + if account.owner() == &solana_vote_program + && VoteState::deserialize(account.data()).is_ok() + { + vote_accounts_cache_miss_count.fetch_add(1, Relaxed); + } + VoteAccount::try_from(account).ok() + }; + let invalid_vote_keys = DashMap::::new(); + let make_vote_delegations_entry = |vote_pubkey| { + let Some(vote_account) = get_vote_account(&vote_pubkey) else { + invalid_vote_keys.insert(vote_pubkey, InvalidCacheEntryReason::Missing); + return None; + }; + if vote_account.owner() != &solana_vote_program { + invalid_vote_keys.insert(vote_pubkey, InvalidCacheEntryReason::WrongOwner); + return None; + } + let vote_with_stake_delegations = VoteWithStakeDelegations { + vote_state: Arc::new(vote_account.vote_state().clone()), + vote_account: AccountSharedData::from(vote_account), + delegations: Vec::default(), + }; + Some((vote_pubkey, vote_with_stake_delegations)) + }; + let vote_with_stake_delegations_map: DashMap = + thread_pool.install(|| { + voter_pubkeys + .into_par_iter() + .filter_map(make_vote_delegations_entry) + .collect() + }); + // Join stake accounts with vote-accounts. + let push_stake_delegation = |(stake_pubkey, stake_account): (&Pubkey, &StakeAccount<_>)| { + let delegation = stake_account.delegation(); + let Some(mut vote_delegations) = + vote_with_stake_delegations_map.get_mut(&delegation.voter_pubkey) + else { + return; + }; + if let Some(reward_calc_tracer) = reward_calc_tracer.as_ref() { + let delegation = + InflationPointCalculationEvent::Delegation(*delegation, solana_vote_program); + let event = RewardCalculationEvent::Staking(stake_pubkey, &delegation); + reward_calc_tracer(&event); + } + let stake_delegation = (*stake_pubkey, stake_account.clone()); + vote_delegations.delegations.push(stake_delegation); + }; + thread_pool.install(|| { + stake_delegations + .into_par_iter() + .for_each(push_stake_delegation); + }); + LoadVoteAndStakeAccountsResult { + vote_with_stake_delegations_map, + invalid_vote_keys, + vote_accounts_cache_miss_count: vote_accounts_cache_miss_count.into_inner(), + } + } +} + #[cfg(test)] fn check_bank_update_vote_stake_rewards(load_vote_and_stake_accounts: F) where @@ -1975,26 +2084,54 @@ where load_vote_and_stake_accounts(&bank0); - // put a child bank in epoch 1, which calls update_rewards()... - let bank1 = Bank::new_from_parent( + // put a child bank in epoch 1 + let bank1 = Arc::new(Bank::new_from_parent( bank0.clone(), &Pubkey::default(), bank0.get_slots_in_epoch(bank0.epoch()) + 1, - ); + )); + // verify that there's inflation assert_ne!(bank1.capitalization(), bank0.capitalization()); + // check voting rewards show up in rewards vector + assert_eq!( + *bank1.rewards.read().unwrap(), + vec![( + vote_id, + RewardInfo { + reward_type: RewardType::Voting, + lamports: 0, + post_balance: bank1.get_balance(&vote_id), + commission: Some(0), + } + ),] + ); + bank1.freeze(); + + // advance past partitioned epoch staking rewards delivery + let bank2 = Arc::new(Bank::new_from_parent( + bank1.clone(), + &Pubkey::default(), + bank1.slot() + 1, + )); + // verify that there's inflation + assert_ne!(bank2.capitalization(), bank0.capitalization()); + // verify the inflation is represented in validator_points - let paid_rewards = bank1.capitalization() - bank0.capitalization() - bank1_sysvar_delta(); + let paid_rewards = bank2.capitalization() + - bank0.capitalization() + - bank1_sysvar_delta() + - bank2_sysvar_delta(); - // this assumes that no new builtins or precompiles were activated in bank1 + // this assumes that no new builtins or precompiles were activated in bank1 or bank2 let PrevEpochInflationRewards { validator_rewards, .. - } = bank1.calculate_previous_epoch_inflation_rewards(bank0.capitalization(), bank0.epoch()); + } = bank2.calculate_previous_epoch_inflation_rewards(bank0.capitalization(), bank0.epoch()); // verify the stake and vote accounts are the right size assert!( - ((bank1.get_balance(&stake_id) - stake_account.lamports() + bank1.get_balance(&vote_id) + ((bank2.get_balance(&stake_id) - stake_account.lamports() + bank2.get_balance(&vote_id) - vote_account.lamports()) as f64 - validator_rewards as f64) .abs() @@ -2004,34 +2141,24 @@ where // verify the rewards are the right size assert!((validator_rewards as f64 - paid_rewards as f64).abs() < 1.0); // rounding, truncating - // verify validator rewards show up in bank1.rewards vector + // verify validator rewards show up in rewards vectors assert_eq!( - *bank1.rewards.read().unwrap(), - vec![ - ( - vote_id, - RewardInfo { - reward_type: RewardType::Voting, - lamports: 0, - post_balance: bank1.get_balance(&vote_id), - commission: Some(0), - } - ), - ( - stake_id, - RewardInfo { - reward_type: RewardType::Staking, - lamports: validator_rewards as i64, - post_balance: bank1.get_balance(&stake_id), - commission: Some(0), - } - ) - ] + *bank2.rewards.read().unwrap(), + vec![( + stake_id, + RewardInfo { + reward_type: RewardType::Staking, + lamports: validator_rewards as i64, + post_balance: bank2.get_balance(&stake_id), + commission: Some(0), + } + )] ); - bank1.freeze(); + bank2.freeze(); add_root_and_flush_write_cache(&bank0); add_root_and_flush_write_cache(&bank1); - assert!(bank1.calculate_and_verify_capitalization(true)); + add_root_and_flush_write_cache(&bank2); + assert!(bank2.calculate_and_verify_capitalization(true)); } fn do_test_bank_update_rewards_determinism() -> u64 {