diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5c0f4e08a04400..c144cf5d947733 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -282,6 +282,17 @@ impl AddAssign for SquashTiming { } } +/// Returned from `Bank::is_alpenglow_active_in_epoch()`. +#[derive(Debug)] +pub(crate) enum AlpenglowEpochStatus { + /// This is a full tower epoch + Tower, + /// The epoch started in tower and then switched to alpenglow + MigrationEpoch, + /// This is a full alpenglow epoch + FullAlpenglow, +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct CollectorFeeDetails { transaction_fee: u64, diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 40cfc4aeb04f25..d0682bf314b1e9 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -8,11 +8,14 @@ use { }, crate::{ bank::{ - RewardCalcTracer, RewardCalculationEvent, RewardCommission, RewardCommissions, - RewardsMetrics, null_tracer, + AlpenglowEpochStatus, RewardCalcTracer, RewardCalculationEvent, RewardCommission, + RewardCommissions, RewardsMetrics, null_tracer, }, inflation_rewards::{ - points::{CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points}, + points::{ + AlpenglowStakeState, CalculationEnvironment, DelegatedVoteState, PointValue, + calculate_points, + }, redeem_rewards, }, stake_account::StakeAccount, @@ -432,6 +435,7 @@ impl Bank { new_rate_activation_epoch: Option, delay_commission_updates: bool, commission_rate_in_basis_points: bool, + ag_epoch_status: &AlpenglowEpochStatus, ) -> Option { // curry closure to add the contextual stake_pubkey let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| { @@ -479,6 +483,18 @@ impl Bank { vote_state.commission() as u16 * 100 }; + let ag_stake_state = match ag_epoch_status { + AlpenglowEpochStatus::Tower => AlpenglowStakeState::Tower, + AlpenglowEpochStatus::FullAlpenglow => AlpenglowStakeState::Alpenglow { + vote_pubkey, + epoch_stakes: &self.epoch_stakes, + }, + AlpenglowEpochStatus::MigrationEpoch => AlpenglowStakeState::Migrating { + vote_pubkey, + epoch_stakes: &self.epoch_stakes, + }, + }; + match redeem_rewards( stake_state, commission_bps, @@ -492,6 +508,7 @@ impl Bank { }, reward_calc_tracer, stake_account.lamports(), + ag_stake_state, ) { Ok((stake_reward, commission_lamports, stake)) => { let stake_reward = PartitionedStakeReward { @@ -519,6 +536,22 @@ impl Bank { } } + /// Returns the status of alpenglow activation in `epoch`. + pub(crate) fn is_alpenglow_active_in_epoch(&self, epoch: Epoch) -> AlpenglowEpochStatus { + let Some(genesis_cert) = self.get_alpenglow_genesis_certificate() else { + return AlpenglowEpochStatus::Tower; + }; + let cert_slot = genesis_cert.cert_type.slot(); + match ( + cert_slot <= self.epoch_schedule.get_first_slot_in_epoch(epoch), + cert_slot <= self.epoch_schedule.get_last_slot_in_epoch(epoch), + ) { + (true, _) => AlpenglowEpochStatus::FullAlpenglow, + (false, true) => AlpenglowEpochStatus::MigrationEpoch, + (false, false) => AlpenglowEpochStatus::Tower, + } + } + /// Calculates epoch rewards for stake/commission accounts /// Returns commission accounts, stake rewards, and the sum of all stake rewards in lamports fn calculate_stake_rewards_and_commissions<'a>( @@ -537,6 +570,8 @@ impl Bank { let delay_commission_updates = feature_snapshot.delay_commission_updates; let commission_rate_in_basis_points = feature_snapshot.commission_rate_in_basis_points; + let ag_epoch_status = self.is_alpenglow_active_in_epoch(rewarded_epoch); + let mut measure_redeem_rewards = Measure::start("redeem-rewards"); // For N stake delegations, where N is >1,000,000, we produce: // * N stake rewards, @@ -567,6 +602,7 @@ impl Bank { new_warmup_cooldown_rate_epoch, delay_commission_updates, commission_rate_in_basis_points, + &ag_epoch_status, ) }); let (stake_reward, maybe_reward_record) = match maybe_reward_record { diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 19613aab4b5205..67953752655f99 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -1,5 +1,8 @@ use { - crate::{bank::Bank, validated_block_finalization::ValidatedBlockFinalizationCert}, + crate::{ + bank::{AlpenglowEpochStatus, Bank}, + validated_block_finalization::ValidatedBlockFinalizationCert, + }, epoch_inflation_account_state::{EpochInflationAccountState, EpochInflationState}, log::info, solana_account::{AccountSharedData, ReadableAccount}, @@ -9,7 +12,7 @@ use { solana_vote_interface::state::{ LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY, VoteStateV4, VoteStateVersions, }, - std::collections::VecDeque, + std::collections::{HashSet, VecDeque}, thiserror::Error, }; @@ -63,6 +66,11 @@ pub(super) fn calculate_and_pay_voting_reward_and_update_vote_state( return Ok(()); }; + debug_assert_eq!( + validators_to_reward.len(), + validators_to_reward.iter().collect::>().len() + ); + let current_slot = bank.slot(); let (reward_slot_accounts, reward_slot_total_stake) = { let epoch_stakes = bank.epoch_stakes_from_slot(reward_slot).ok_or( @@ -136,6 +144,7 @@ pub(super) fn calculate_and_pay_voting_reward_and_update_vote_state( continue; }; if let Some(account_data) = pay_reward_update_vote_state( + bank, current_epoch, reward_slot, current_slot_account, @@ -153,6 +162,7 @@ pub(super) fn calculate_and_pay_voting_reward_and_update_vote_state( match current_vote_accounts.get(¤t_slot_leader_vote_pubkey) { Some((_, leader_account)) => { if let Some(account_data) = pay_reward_update_vote_state( + bank, current_epoch, reward_slot, leader_account, @@ -209,6 +219,7 @@ fn calculate_reward( /// /// TODO: this is using VoteStateV4 explicitly. When we upstream, we will use VoteStateHandle API. fn pay_reward_update_vote_state( + bank: &Bank, current_epoch: Epoch, reward_slot: Slot, account: &VoteAccount, @@ -222,7 +233,7 @@ fn pay_reward_update_vote_state( }; match vote_state_versions { VoteStateVersions::V4(mut vote_state) => { - increment_credits(&mut vote_state, current_epoch, reward); + increment_credits(bank, &mut vote_state, current_epoch, reward); update_vote_state( &mut vote_state, reward_slot, @@ -243,37 +254,71 @@ fn pay_reward_update_vote_state( } } +fn ensure_marker(bank: &Bank, vote_state: &mut VoteStateV4, epoch: Epoch) { + let marker_epoch = Epoch::MAX; + let marker_elem = (marker_epoch, u64::MAX, u64::MAX); + let epoch_credits = &mut vote_state.epoch_credits; + match bank.is_alpenglow_active_in_epoch(epoch) { + AlpenglowEpochStatus::Tower => (), + AlpenglowEpochStatus::FullAlpenglow => { + if epoch_credits.is_empty() { + epoch_credits.push(marker_elem); + } + } + AlpenglowEpochStatus::MigrationEpoch => match epoch_credits.len() { + 0 => { + epoch_credits.push(marker_elem); + } + 1 => { + panic!(); + } + _ => { + let ind0 = epoch_credits.len() - 1; + let ind1 = epoch_credits.len() - 2; + if epoch_credits[ind0].0 == marker_epoch || epoch_credits[ind1].0 == marker_epoch { + } else { + epoch_credits.push(marker_elem); + } + } + }, + } +} + /// Stores rewards as credits in the current vote state. /// /// TODO: this is using VoteStateV4 explicitly. When we upstream, we will use VoteStateHandle API. -fn increment_credits(vote_state: &mut VoteStateV4, epoch: Epoch, credits: u64) { - // never seen a credit - if vote_state.epoch_credits.is_empty() { - vote_state.epoch_credits.push((epoch, 0, 0)); - } else if epoch != vote_state.epoch_credits.last().unwrap().0 { - let (_, credits, prev_credits) = *vote_state.epoch_credits.last().unwrap(); - - if credits != prev_credits { - // if credits were earned previous epoch - // append entry at end of list for the new epoch - vote_state.epoch_credits.push((epoch, credits, credits)); - } else { - // else just move the current epoch - vote_state.epoch_credits.last_mut().unwrap().0 = epoch; +fn increment_credits( + bank: &Bank, + vote_state: &mut VoteStateV4, + new_epoch: Epoch, + new_credits: u64, +) { + ensure_marker(bank, vote_state, new_epoch); + match vote_state.epoch_credits.last_mut() { + None => { + vote_state.epoch_credits.push((new_epoch, new_credits, 0)); } - - // Remove too old epoch_credits - if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { - vote_state.epoch_credits.remove(0); + Some((epoch, final_credits, initial_credits)) => { + if *epoch == Epoch::MAX { + vote_state.epoch_credits.push((new_epoch, new_credits, 0)); + return; + } + if *epoch == new_epoch { + *final_credits = final_credits.saturating_add(new_credits); + return; + } + if final_credits == initial_credits { + *epoch = new_epoch; + *final_credits = final_credits.saturating_add(new_credits); + return; + } + let elem = (new_epoch, new_credits + *final_credits, *final_credits); + vote_state.epoch_credits.push(elem); + if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + vote_state.epoch_credits.remove(0); + } } } - - vote_state.epoch_credits.last_mut().unwrap().1 = vote_state - .epoch_credits - .last() - .unwrap() - .1 - .saturating_add(credits); } /// Updates `root_slot` and `votes` in vote state using the rewards and finalization certificates from the footer @@ -315,22 +360,30 @@ mod tests { use { super::*, crate::{ + bank::VAT_TO_BURN_PER_EPOCH, bank_forks::BankForks, genesis_utils::{ - ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, + ValidatorVoteKeypairs, activate_all_features_alpenglow, + create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_leader_ex, }, + inflation_rewards::commission_split, + stake_utils, test_utils::new_rand_vote_account, validated_block_finalization::ValidatedBlockFinalizationCert, }, + agave_feature_set::FeatureSet, agave_votor_messages::{ consensus_message::{Certificate, CertificateType}, reward_certificate::NUM_SLOTS_FOR_REWARD, }, bitvec::prelude::*, rand::seq::IndexedRandom, - solana_account::ReadableAccount, + solana_account::{Account, ReadableAccount, WritableAccount}, solana_bls_signatures::Signature as BLSSignature, + solana_cluster_type::ClusterType, solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeRateGovernor, solana_genesis_config::GenesisConfig, solana_hash::Hash, solana_keypair::Keypair, @@ -339,6 +392,13 @@ mod tests { solana_rent::Rent, solana_signer::Signer, solana_signer_store::encode_base2, + solana_stake_interface::state::StakeStateV2, + solana_vote_interface::state::VoteStateVersions, + std::{ + collections::HashMap, + sync::{Arc, RwLock}, + }, + test_case::test_matrix, }; fn get_vote_state_v4(bank: &Bank, vote_pubkey: &Pubkey) -> VoteStateV4 { @@ -396,22 +456,39 @@ mod tests { #[test] fn increment_credits_works() { + let num_validators = 3; + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let (bank, _bank_forks) = initial_state(&validators, 0); let mut vote_state = VoteStateV4::default(); let epoch = 1234; let credits = 543432; - increment_credits(&mut vote_state, epoch, credits); + increment_credits(&bank, &mut vote_state, epoch, credits); assert_eq!(credits, vote_state.epoch_credits.last().unwrap().1); } #[test] fn pay_reward_works() { + let num_validators = 3; + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let (bank, _bank_forks) = initial_state(&validators, 0); let account = VoteAccount::try_from(new_rand_vote_account(&mut rand::rng(), None, true)).unwrap(); let epoch = 1234; let reward = 3453423; - let account_shared_data = - pay_reward_update_vote_state(epoch, 0, &account, Pubkey::default(), reward, None) - .unwrap(); + let account_shared_data = pay_reward_update_vote_state( + &bank, + epoch, + 0, + &account, + Pubkey::default(), + reward, + None, + ) + .unwrap(); let vote_state_versions: VoteStateVersions = bincode::deserialize(&account_shared_data.data_clone()).unwrap(); let VoteStateVersions::V4(vote_state) = vote_state_versions else { @@ -493,8 +570,8 @@ mod tests { let VoteStateVersions::V4(vote_state) = vote_state_versions else { panic!(); }; - assert_eq!(vote_state.epoch_credits.len(), 1); - let got_reward = vote_state.epoch_credits[0].1; + assert_eq!(vote_state.epoch_credits.len(), 2); + let got_reward = vote_state.epoch_credits[1].1; let total_stake = bank .epoch_stakes_from_slot(reward_slot) .unwrap() @@ -699,4 +776,493 @@ mod tests { } } } + + fn find_leader(validators: &[ValidatorVoteKeypairs]) -> SlotLeader { + let node_pubkey = validators[0].node_keypair.pubkey(); + let vote_pubkey = validators[0].vote_keypair.pubkey(); + SlotLeader { + id: node_pubkey, + vote_address: vote_pubkey, + } + } + + fn into_vote_state_v4(account: &Account) -> Box { + let vote_state_versions = bincode::deserialize(&account.data).unwrap(); + let VoteStateVersions::V4(v4) = vote_state_versions else { + panic!(); + }; + v4 + } + + fn set_commission( + genesis_config: &mut GenesisConfig, + validators: &[ValidatorVoteKeypairs], + commission_bps: u16, + ) { + for validator in validators { + let vote_pubkey = validator.vote_keypair.pubkey(); + let account = genesis_config.accounts.get_mut(&vote_pubkey).unwrap(); + let mut vote_state = into_vote_state_v4(account); + vote_state.inflation_rewards_commission_bps = commission_bps; + VoteStateV4::serialize( + &VoteStateVersions::V4(vote_state), + account.data_as_mut_slice(), + ) + .unwrap(); + } + } + + #[derive(Debug)] + struct RewardState { + vote_pubkey: Pubkey, + stake_prev_lamports: u64, + vote_prev_lamports: u64, + expected_reward: u64, + } + + #[derive(Debug)] + struct Staker { + lamports: u64, + expected_rewards: u64, + } + + impl Staker { + fn new( + rent: &Rent, + bank: &Bank, + commission_bps: u16, + pubkey: Pubkey, + validator_reward: u64, + validator_stake: u64, + ) -> (Self, u64) { + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let lamports = bank.get_account(&pubkey).unwrap().lamports(); + if lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + return ( + Self { + lamports, + expected_rewards: 0, + }, + 0, + ); + } + let stake = lamports - rent_exempt_reserve; + let stake_weighted_reward = validator_reward * stake / validator_stake; + let (voter_reward, staker_reward, is_split) = + commission_split(commission_bps, stake_weighted_reward); + assert!(is_split); + ( + Self { + lamports, + expected_rewards: staker_reward, + }, + voter_reward, + ) + } + } + + #[derive(Debug)] + struct State { + voter_lamports: u64, + voter_expected_reward: u64, + stakers: HashMap, + } + + impl State { + fn new( + bank: &Bank, + voter_pubkey: Pubkey, + staker_pubkeys: &[Pubkey], + rent: &Rent, + pay_leader: bool, + commission_bps: u16, + num_reward_slots: u64, + ) -> Self { + let rent_exempt_reserve = rent.minimum_balance(StakeStateV2::size_of()); + let voter_lamports = bank.get_account(&voter_pubkey).unwrap().lamports(); + let validator_stake = staker_pubkeys + .iter() + .map(|pubkey| { + let lamports = bank.get_account(pubkey).unwrap().lamports(); + lamports - rent_exempt_reserve + }) + .sum::(); + + let vote_rewards = { + let epoch_state = EpochInflationAccountState::new_from_bank(bank) + .unwrap() + .get_epoch_state(bank.epoch()) + .unwrap(); + let total_stake = bank + .epoch_stakes_from_slot(bank.slot()) + .unwrap() + .total_stake(); + let (validator_reward, leader_reward) = + calculate_reward(&epoch_state, total_stake, validator_stake); + let vote_rewards = if pay_leader { + validator_reward + leader_reward + } else { + validator_reward + }; + vote_rewards * num_reward_slots + }; + + let mut voter_expected_reward = 0; + let mut stakers = HashMap::new(); + for staker_pubkey in staker_pubkeys { + let (staker, voter_reward) = Staker::new( + rent, + bank, + commission_bps, + *staker_pubkey, + vote_rewards, + validator_stake, + ); + voter_expected_reward += voter_reward; + stakers.insert(*staker_pubkey, staker); + } + + State { + voter_lamports, + stakers, + voter_expected_reward, + } + } + } + + impl RewardState { + fn new(keypair: &ValidatorVoteKeypairs, bank: &Bank) -> Self { + let stake_pubkey = keypair.stake_keypair.pubkey(); + let vote_pubkey = keypair.vote_keypair.pubkey(); + + let stake_account = bank.get_account(&stake_pubkey).unwrap(); + let vote_account = bank.get_account(&vote_pubkey).unwrap(); + + let stake_prev_lamports = stake_account.lamports(); + let vote_prev_lamports = vote_account.lamports(); + + let vote_account = bank.get_account(&vote_pubkey).unwrap(); + let vote_state_versions = bincode::deserialize(vote_account.data()).unwrap(); + let VoteStateVersions::V4(vote_state) = vote_state_versions else { + panic!(); + }; + assert_eq!(vote_state.epoch_credits.len(), 2); + let (epoch, final_credit, initial_credit) = vote_state.epoch_credits[1]; + + assert_eq!(epoch, bank.epoch()); + assert_eq!(initial_credit, 0); + let expected_reward = final_credit; + + Self { + vote_pubkey, + stake_prev_lamports, + vote_prev_lamports, + expected_reward, + } + } + } + + #[derive(Debug)] + struct ValidatorRewardState { + prev_lamports: u64, + expected_rewards: u64, + } + + fn initial_state( + validator_keypairs: &[ValidatorVoteKeypairs], + commission_bps: u16, + ) -> (Arc, Arc>) { + let per_validator_stake = LAMPORTS_PER_SOL; + let mut genesis_config_info = create_genesis_config_with_alpenglow_vote_accounts( + 1_000_000_000, + validator_keypairs, + vec![per_validator_stake; validator_keypairs.len()], + ); + genesis_config_info.genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + genesis_config_info.genesis_config.rent = Rent::default(); + set_commission( + &mut genesis_config_info.genesis_config, + validator_keypairs, + commission_bps, + ); + + let (bank_epoch0, bank_forks) = + Bank::new_with_bank_forks_for_tests(&genesis_config_info.genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + let epoch1_slot = bank_epoch0.epoch_schedule.get_first_slot_in_epoch(1); + let bank_epoch1 = Arc::new(Bank::new_from_parent( + bank_epoch0, + SlotLeader::new_unique(), + epoch1_slot, + )); + assert_eq!(bank_epoch1.epoch(), 1); + (bank_epoch1, bank_forks) + } + + fn reward_validators( + bank: Arc, + validators: &[ValidatorVoteKeypairs], + num_reward_slots: u64, + ) -> (Arc, HashMap) { + let validators_to_reward = validators + .iter() + .map(|k| k.vote_keypair.pubkey()) + .collect::>() + .into_iter() + .collect::>(); + + let mut looping_bank = bank; + for _ in 0..num_reward_slots { + calculate_and_pay_voting_reward_and_update_vote_state( + &looping_bank, + Some((looping_bank.slot() - 100, validators_to_reward.clone())), + None, + ) + .unwrap(); + + let leader = *looping_bank.leader(); + let slot = looping_bank.slot() + 1; + looping_bank = Arc::new(Bank::new_from_parent(looping_bank, leader, slot)); + } + + let map = validators + .iter() + .map(|keypair| { + let stake_pubkey = keypair.stake_keypair.pubkey(); + let reward_state = RewardState::new(keypair, &looping_bank); + (stake_pubkey, reward_state) + }) + .collect::>(); + (looping_bank, map) + } + + fn test_vote_reward_payout_impl( + validators: &[ValidatorVoteKeypairs], + pay_leader: bool, + commission_bps: u16, + initial_bank: Option<(Arc, Arc>)>, + num_reward_slots: u64, + ) -> (Arc, HashMap) { + let (initial_bank, _bank_forks) = + initial_bank.unwrap_or(initial_state(validators, commission_bps)); + + let reward_epoch = initial_bank.epoch() + 1; + let reward_epoch_slot = initial_bank + .epoch_schedule + .get_first_slot_in_epoch(reward_epoch); + let leader = if pay_leader { + find_leader(validators) + } else { + SlotLeader::new_unique() + }; + let reward_bank = Arc::new(Bank::new_from_parent( + initial_bank, + leader, + reward_epoch_slot, + )); + assert_eq!(reward_bank.epoch(), reward_epoch); + + let (reward_bank, rewarded_validators) = + reward_validators(reward_bank, validators, num_reward_slots); + + let payout_epoch = reward_bank.epoch() + 1; + let payout_epoch_slot = reward_bank + .epoch_schedule + .get_first_slot_in_epoch(payout_epoch); + let payout_bank = + Bank::new_from_parent(reward_bank, find_leader(validators), payout_epoch_slot); + assert_eq!(payout_bank.epoch(), payout_epoch); + + // Need to progress banks a few times for the rewards to be paid. + let mut prev_bank = Arc::new(payout_bank); + for i in 0..10 { + let leader = SlotLeader::new_unique(); + let slot = prev_bank.slot() + 1 + i; + prev_bank = Arc::new(Bank::new_from_parent(prev_bank, leader, slot)); + } + + (prev_bank, rewarded_validators) + } + + #[test_matrix([true, false], [1_000, 5_000])] + fn test_vote_reward_payout(pay_leader: bool, commission_bps: u16) { + let num_validators = 3; + let num_reward_slots = 10; + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let (final_bank, rewarded_validators) = test_vote_reward_payout_impl( + &validators, + pay_leader, + commission_bps, + None, + num_reward_slots, + ); + let mut voter_rewards = HashMap::new(); + + for (stake_pubkey, reward_state) in rewarded_validators { + let stake_account = final_bank.get_account(&stake_pubkey).unwrap(); + let stake_cur = stake_account.lamports(); + + let (vote_expected_reward, stake_expected_reward, is_split) = + commission_split(commission_bps, reward_state.expected_reward); + + let validator_reward_state = + voter_rewards + .entry(reward_state.vote_pubkey) + .or_insert(ValidatorRewardState { + prev_lamports: reward_state.vote_prev_lamports, + expected_rewards: 0, + }); + validator_reward_state.expected_rewards += vote_expected_reward; + + assert!(is_split); + assert_ne!( + stake_expected_reward, 0, + "stake_expected_reward {stake_expected_reward} should not be 0" + ); + assert_ne!( + vote_expected_reward, 0, + "vote_expected_reward {vote_expected_reward} should not be 0" + ); + assert_eq!( + stake_cur - reward_state.stake_prev_lamports, + stake_expected_reward + ); + + // Due to rounding issues, off by 1 errors are possible. + let total_reward = stake_expected_reward + vote_expected_reward; + assert!( + total_reward.max(reward_state.expected_reward) + - total_reward.min(reward_state.expected_reward) + <= 1 + ); + } + + for (vote_pubkey, state) in voter_rewards { + let vote_account = final_bank.get_account(&vote_pubkey).unwrap(); + let vote_cur = vote_account.lamports(); + assert_eq!(vote_cur - state.prev_lamports, state.expected_rewards); + } + } + + #[test_matrix([true, false], [1_000, 5_000])] + fn test_multiple_delegators(pay_leader: bool, commission_bps: u16) { + let num_validators = 1; + let num_reward_slots = 10; + let mint_keypair = Keypair::new(); + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let validator_lamports = 890_880; + let mut genesis_config = create_genesis_config_with_leader_ex( + 1_000_000_000, + &mint_keypair.pubkey(), + &validators[0].node_keypair.pubkey(), + &validators[0].vote_keypair.pubkey(), + &validators[0].stake_keypair.pubkey(), + Some(validators[0].bls_keypair.public.to_bytes_compressed()), + LAMPORTS_PER_SOL, + validator_lamports, + FeeRateGovernor::new(0, 0), + Rent::default(), + ClusterType::Development, + &FeatureSet::all_enabled(), + vec![], + ); + genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + activate_all_features_alpenglow(&mut genesis_config); + set_commission(&mut genesis_config, &validators, commission_bps); + + let vote_account = genesis_config + .accounts + .get(&validators[0].vote_keypair.pubkey()) + .unwrap() + .clone() + .into(); + + let staker_keypairs = (0..5).map(|_| Keypair::new()).collect::>(); + let stake = LAMPORTS_PER_SOL * 2; + for keypair in &staker_keypairs { + let stake_pubkey = keypair.pubkey(); + let account = Account::from(stake_utils::create_stake_account( + &stake_pubkey, + &validators[0].vote_keypair.pubkey(), + &vote_account, + &genesis_config.rent, + stake, + )); + genesis_config.accounts.insert(stake_pubkey, account); + } + + let (bank_epoch0, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + let epoch1_slot = bank_epoch0.epoch_schedule.get_first_slot_in_epoch(1); + let initial_bank = Arc::new(Bank::new_from_parent( + bank_epoch0, + SlotLeader::new_unique(), + epoch1_slot, + )); + assert_eq!(initial_bank.epoch(), 1); + + let staker_pubkeys = { + let mut pubkeys = staker_keypairs + .iter() + .map(|k| k.pubkey()) + .collect::>(); + pubkeys.push(validators[0].stake_keypair.pubkey()); + pubkeys + }; + let prev_state = State::new( + &initial_bank, + validators[0].vote_keypair.pubkey(), + &staker_pubkeys, + &genesis_config.rent, + pay_leader, + commission_bps, + num_reward_slots, + ); + + let (final_bank, _) = test_vote_reward_payout_impl( + &validators, + pay_leader, + commission_bps, + Some((initial_bank, bank_forks)), + num_reward_slots, + ); + + let final_state = State::new( + &final_bank, + validators[0].vote_keypair.pubkey(), + &staker_pubkeys, + &genesis_config.rent, + pay_leader, + commission_bps, + num_reward_slots, + ); + + for pubkey in prev_state.stakers.keys() { + let before = prev_state.stakers.get(pubkey).unwrap(); + let after = final_state.stakers.get(pubkey).unwrap(); + let diff = after.lamports - before.lamports; + assert_eq!( + diff, before.expected_rewards, + "before={} after={} diff={diff} expected={}", + before.lamports, after.lamports, before.expected_rewards + ); + } + + let voter_diff = + final_state.voter_lamports + VAT_TO_BURN_PER_EPOCH - prev_state.voter_lamports; + // Due to rounding issues, off by 1 errors are possible. + assert!( + voter_diff.abs_diff(prev_state.voter_expected_reward) <= 1, + "final_lamports={} prev_lamports={} diff={voter_diff} expected={}", + final_state.voter_lamports, + prev_state.voter_lamports, + prev_state.voter_expected_reward + ); + } } diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index f1e113fb133853..14a119e4d4a408 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -1,10 +1,12 @@ //! Information about stake and voter rewards based on stake state. - +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; use { self::points::{ CalculatedStakePoints, CalculationEnvironment, DelegatedVoteState, InflationPointCalculationEvent, SkippedReason, calculate_stake_points_and_credits, }, + crate::inflation_rewards::points::AlpenglowStakeState, solana_instruction::error::InstructionError, solana_stake_interface::{ error::StakeError, @@ -34,6 +36,7 @@ pub(crate) fn redeem_rewards<'a>( calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, stake_account_lamports_for_trace: u64, + ag_stake_state: AlpenglowStakeState, ) -> Result<(u64, u64, Stake), InstructionError> { if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { @@ -73,6 +76,7 @@ pub(crate) fn redeem_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer, + ag_stake_state, ) { Ok((stakers_reward, voters_reward, stake)) } else { @@ -89,6 +93,7 @@ fn redeem_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_stake_state: AlpenglowStakeState, ) -> Option<(u64, u64)> { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer(&InflationPointCalculationEvent::CreditsObserved( @@ -102,6 +107,7 @@ fn redeem_stake_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer.as_ref(), + ag_stake_state, ) .map(|calculated_stake_rewards| { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { @@ -132,6 +138,7 @@ fn calculate_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_stake_state: AlpenglowStakeState, ) -> Option { let CalculationEnvironment { stake_history, @@ -140,9 +147,11 @@ fn calculate_stake_rewards<'a>( rewarded_epoch, .. } = calculation_environment; + // ensure to run to trigger (optional) inflation_point_calc_tracer let CalculatedStakePoints { - points, + tower_points, + ag_points, new_credits_observed, mut force_credits_update_with_skipped_reward, } = calculate_stake_points_and_credits( @@ -151,6 +160,7 @@ fn calculate_stake_rewards<'a>( stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, + &ag_stake_state, ); // Drive credits_observed forward unconditionally when rewards are disabled @@ -176,7 +186,7 @@ fn calculate_stake_rewards<'a>( }); } - if points == 0 { + if tower_points == 0 && ag_points == 0 { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); } @@ -189,12 +199,30 @@ fn calculate_stake_rewards<'a>( return None; } - // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. - let rewards = points - .checked_mul(u128::from(point_value.rewards)) - .expect("Rewards intermediate calculation should fit within u128") - .checked_div(point_value.points) - .unwrap(); + let rewards = match ag_stake_state { + AlpenglowStakeState::Alpenglow { .. } => { + // In alpenglow, `points` represents the actual reward that this `vote_state` earned. + ag_points + } + AlpenglowStakeState::Tower | AlpenglowStakeState::Calculating => { + // In tower, `points` still needs to be scaled by `point_value` to calculate this + // `vote_state` earned. + // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. + tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() + } + AlpenglowStakeState::Migrating { .. } => { + tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() + + ag_points + } + }; let rewards = u64::try_from(rewards).expect("Rewards should fit within u64"); @@ -238,6 +266,7 @@ fn calculate_stake_rewards<'a>( /// indicate with false for was_split /// /// DEVELOPER NOTE: This function used to be a method on VoteState, but was moved here +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] fn commission_split(commission_bps: u16, on: u64) -> (u64, u64, bool) { const MAX_BPS: u16 = 10_000; const MAX_BPS_U128: u128 = MAX_BPS as u128; @@ -329,6 +358,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -354,6 +384,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -393,6 +424,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -422,6 +454,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -448,6 +481,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -477,6 +511,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -504,6 +539,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -533,6 +569,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -556,6 +593,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); vote_state.set_inflation_rewards_commission_bps(9900); @@ -576,6 +614,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -603,6 +642,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -630,12 +670,14 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); assert_eq!( CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: 4, force_credits_update_with_skipped_reward: false, }, @@ -644,7 +686,8 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &AlpenglowStakeState::Tower ) ); @@ -654,7 +697,8 @@ mod tests { // this is new behavior 1; return the post-recreation rewound credits from the vote account assert_eq!( CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: 4, force_credits_update_with_skipped_reward: true, }, @@ -663,14 +707,16 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &AlpenglowStakeState::Tower ) ); // this is new behavior 2; don't hint when credits both from stake and vote are identical stake.credits_observed = 4; assert_eq!( CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: 4, force_credits_update_with_skipped_reward: false, }, @@ -679,7 +725,8 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &AlpenglowStakeState::Tower, ) ); @@ -708,6 +755,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); @@ -736,6 +784,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); } @@ -765,6 +814,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ); } @@ -802,6 +852,7 @@ mod tests { commission_rate_in_basis_points, }, null_tracer(), + AlpenglowStakeState::Tower, ) ); } diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 2f22b364a86ba1..70a3610a2c04e1 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -1,6 +1,7 @@ //! Information about points calculation based on stake state. use { + crate::epoch_stakes::VersionedEpochStakes, solana_clock::Epoch, solana_instruction::error::InstructionError, solana_pubkey::Pubkey, @@ -9,7 +10,7 @@ use { state::{Delegation, Stake, StakeStateV2}, }, solana_vote::vote_state_view::VoteStateView, - std::cmp::Ordering, + std::{cmp::Ordering, collections::HashMap}, }; /// captures a rewards round as lamports to be awarded @@ -24,7 +25,8 @@ pub struct PointValue { #[derive(Debug, PartialEq, Eq)] pub(crate) struct CalculatedStakePoints { - pub(crate) points: u128, + pub(crate) tower_points: u128, + pub(crate) ag_points: u128, pub(crate) new_credits_observed: u64, pub(crate) force_credits_update_with_skipped_reward: bool, } @@ -69,6 +71,7 @@ pub enum SkippedReason { ZeroCreditsAndReturnZero, ZeroCreditsAndReturnCurrent, ZeroCreditsAndReturnRewound, + GetTotalStakeFailed(GetTotalStakeError), } impl From for InflationPointCalculationEvent { @@ -125,8 +128,207 @@ fn calculate_stake_points( stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, + &AlpenglowStakeState::Calculating, ) - .points + .tower_points +} + +/// Alpenglow related state needed in `calculate_stake_points_and_credits`. +#[derive(Debug)] +pub(crate) enum AlpenglowStakeState<'a> { + /// Function is called for calculating rewards. + Calculating, + /// Function is called when Tower is active for the entire epoch. + Tower, + /// Function is called when we migrate from Tower to Alpenglow. + Migrating { + /// Pubkey for the vote account of the validator that the stake is delegated to. + vote_pubkey: Pubkey, + /// `epoch_stakes` from the current bank. + epoch_stakes: &'a HashMap, + }, + /// Function is called when Alpenglow is active for the entire epoch. + Alpenglow { + /// Pubkey for the vote account of the validator that the stake is delegated to. + vote_pubkey: Pubkey, + /// `epoch_stakes` from the current bank. + epoch_stakes: &'a HashMap, + }, +} + +/// Different errors possible in `AlpenglowStakeState::get_total_stake()`. +#[derive(Debug)] +pub enum GetTotalStakeError { + NoEpochStakes(Epoch), + RankForVotePubkeyNotFound(Epoch, Pubkey), + EntryForRankNotFound(Epoch, Pubkey, u16), +} + +impl<'a> AlpenglowStakeState<'a> { + /// Returns the total stake delegated to `self.vote_pubkey` in the given epoch. + fn get_total_stake( + vote_pubkey: Pubkey, + epoch_stakes: &HashMap, + epoch: Epoch, + ) -> Result { + let rank_map = epoch_stakes + .get(&epoch) + .ok_or(GetTotalStakeError::NoEpochStakes(epoch))? + .bls_pubkey_to_rank_map(); + let rank = *rank_map.get_rank_for_vote_pubkey(&vote_pubkey).ok_or( + GetTotalStakeError::RankForVotePubkeyNotFound(epoch, vote_pubkey), + )?; + let entry = rank_map.get_pubkey_stake_entry(rank as usize).ok_or( + GetTotalStakeError::EntryForRankNotFound(epoch, vote_pubkey, rank), + )?; + Ok(entry.stake) + } +} + +fn tower_epoch_credits_iter( + stake: &Stake, + epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, +) -> (u128, u64) { + let mut points = 0; + let credits_in_stake = stake.credits_observed; + let mut new_credits_observed = credits_in_stake; + + for (epoch, final_epoch_credits, initial_epoch_credits) in epoch_credits_iter { + let stake_amount = u128::from(stake.delegation.stake( + epoch, + stake_history, + new_rate_activation_epoch, + )); + + // figure out how much this stake has seen that + // for which the vote account has a record + let earned_credits = if credits_in_stake < initial_epoch_credits { + // the staker observed the entire epoch + final_epoch_credits - initial_epoch_credits + } else if credits_in_stake < final_epoch_credits { + // the staker registered sometime during the epoch, partial credit + final_epoch_credits - new_credits_observed + } else { + // the staker has already observed or been redeemed this epoch + // or was activated after this epoch + 0 + }; + let earned_credits = u128::from(earned_credits); + + // don't want to assume anything about order of the iterator... + new_credits_observed = new_credits_observed.max(final_epoch_credits); + + // finally calculate points for this epoch + let earned_points = stake_amount * earned_credits; + points += earned_points; + + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + } + (points, new_credits_observed) +} + +fn ag_epoch_credits_iter( + stake: &Stake, + epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + vote_pubkey: Pubkey, + epoch_stakes: &HashMap, +) -> (u128, u64) { + let mut points = 0; + let credits_in_stake = stake.credits_observed; + let mut new_credits_observed = credits_in_stake; + + for (epoch, final_epoch_credits, initial_epoch_credits) in epoch_credits_iter { + let stake_amount = u128::from(stake.delegation.stake( + epoch, + stake_history, + new_rate_activation_epoch, + )); + + // figure out how much this stake has seen that + // for which the vote account has a record + let earned_credits = if credits_in_stake < initial_epoch_credits { + // the staker observed the entire epoch + final_epoch_credits - initial_epoch_credits + } else if credits_in_stake < final_epoch_credits { + // the staker registered sometime during the epoch, partial credit + final_epoch_credits - new_credits_observed + } else { + // the staker has already observed or been redeemed this epoch + // or was activated after this epoch + 0 + }; + let earned_credits = u128::from(earned_credits); + + // don't want to assume anything about order of the iterator... + new_credits_observed = new_credits_observed.max(final_epoch_credits); + + let earned_points = { + if earned_credits == 0 { + earned_credits + } else { + let total_stake = + AlpenglowStakeState::get_total_stake(vote_pubkey, epoch_stakes, epoch).unwrap(); + earned_credits * stake_amount / total_stake as u128 + } + }; + points += earned_points; + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + } + (points, new_credits_observed) +} + +fn migrating_epoch_credits_iter( + stake: &Stake, + mut epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + vote_pubkey: Pubkey, + epoch_stakes: &HashMap, +) -> (u128, u128, u64) { + let tower = epoch_credits_iter + .by_ref() + .take_while(|(epoch, _, _)| *epoch != Epoch::MAX); + let (tower_points, tower_new_credits_observed) = tower_epoch_credits_iter( + stake, + tower, + stake_history, + inflation_point_calc_tracer.as_ref(), + new_rate_activation_epoch, + ); + + let (ag_points, ag_new_credits_observed) = ag_epoch_credits_iter( + stake, + epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + vote_pubkey, + epoch_stakes, + ); + + let new_credits_observed = tower_new_credits_observed.max(ag_new_credits_observed); + (tower_points, ag_points, new_credits_observed) } /// for a given stake and vote_state, calculate how many @@ -138,6 +340,7 @@ pub(crate) fn calculate_stake_points_and_credits( stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + ag_stake_state: &AlpenglowStakeState, ) -> CalculatedStakePoints { let credits_in_stake = stake.credits_observed; let credits_in_vote = vote_state.credits; @@ -166,7 +369,8 @@ pub(crate) fn calculate_stake_points_and_credits( // hint with true to indicate some exceptional credits handling is needed return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_vote, force_credits_update_with_skipped_reward: true, }; @@ -177,7 +381,8 @@ pub(crate) fn calculate_stake_points_and_credits( } // don't hint caller and return current value if credits remain unchanged (= delinquent) return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_stake, force_credits_update_with_skipped_reward: false, }; @@ -185,51 +390,48 @@ pub(crate) fn calculate_stake_points_and_credits( Ordering::Greater => {} } - let mut points = 0; - let mut new_credits_observed = credits_in_stake; - - for epoch_credits_item in vote_state.epoch_credits_iter { - let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item; - let stake_amount = u128::from(stake.delegation.stake( - epoch, + let (points, ag_points, new_credits_observed) = match ag_stake_state { + AlpenglowStakeState::Calculating | AlpenglowStakeState::Tower => { + let (points, credits) = tower_epoch_credits_iter( + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + ); + (points, 0, credits) + } + AlpenglowStakeState::Migrating { + vote_pubkey, + epoch_stakes, + } => migrating_epoch_credits_iter( + stake, + vote_state.epoch_credits_iter, stake_history, + inflation_point_calc_tracer, new_rate_activation_epoch, - )); - - // figure out how much this stake has seen that - // for which the vote account has a record - let earned_credits = if credits_in_stake < initial_epoch_credits { - // the staker observed the entire epoch - final_epoch_credits - initial_epoch_credits - } else if credits_in_stake < final_epoch_credits { - // the staker registered sometime during the epoch, partial credit - final_epoch_credits - new_credits_observed - } else { - // the staker has already observed or been redeemed this epoch - // or was activated after this epoch - 0 - }; - let earned_credits = u128::from(earned_credits); - - // don't want to assume anything about order of the iterator... - new_credits_observed = new_credits_observed.max(final_epoch_credits); - - // finally calculate points for this epoch - let earned_points = stake_amount * earned_credits; - points += earned_points; - - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( - epoch, - stake_amount, - earned_credits, - earned_points, - )); + *vote_pubkey, + epoch_stakes, + ), + AlpenglowStakeState::Alpenglow { + vote_pubkey, + epoch_stakes, + } => { + let (ag_points, credits) = ag_epoch_credits_iter( + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + *vote_pubkey, + epoch_stakes, + ); + (0, ag_points, credits) } - } - + }; CalculatedStakePoints { - points, + tower_points: points, + ag_points, new_credits_observed, force_credits_update_with_skipped_reward: false, }