Skip to main content

solana_runtime/inflation_rewards/
points.rs

1//! Information about points calculation based on stake state.
2
3use {
4    crate::{
5        alpenglow_epoch_type::{AlpenglowEpochType, RewardEpochDelegatedStakes},
6        stake_delegation::delegation_effective_stake,
7    },
8    agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT,
9    log::error,
10    solana_clock::Epoch,
11    solana_instruction::error::InstructionError,
12    solana_pubkey::Pubkey,
13    solana_stake_history::StakeHistory,
14    solana_stake_interface::state::{Delegation, Stake, StakeStateV2},
15    solana_vote::vote_state_view::VoteStateView,
16    std::cmp::Ordering,
17};
18
19/// captures a rewards round as lamports to be awarded
20///  and the total points over which those lamports
21///  are to be distributed
22//  basically read as rewards/points, but in integers instead of as an f64
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct PointValue {
25    pub rewards: u64, // lamports to split
26    pub points: u128, // over these points
27}
28
29#[derive(Debug, PartialEq, Eq)]
30pub(crate) struct CalculatedStakePoints {
31    pub(crate) tower_points: u128,
32    pub(crate) ag_points: u128,
33    pub(crate) new_credits_observed: u64,
34    pub(crate) force_credits_update_with_skipped_reward: bool,
35}
36
37/// Combination of info needed to calculate rewards
38pub(crate) struct CalculationEnvironment<'a> {
39    pub(crate) rewarded_epoch: Epoch,
40    pub(crate) point_value: &'a PointValue,
41    pub(crate) stake_history: &'a StakeHistory,
42    pub(crate) new_rate_activation_epoch: Option<Epoch>,
43    pub(crate) commission_rate_in_basis_points: bool,
44    pub(crate) adjust_delegations_for_rent: bool,
45    pub(crate) use_fixed_point_stake_math: bool,
46}
47
48#[derive(Debug)]
49pub enum InflationPointCalculationEvent {
50    CalculatedPoints(u64, u128, u128, u128),
51    SplitRewards(u64, u64, u64, PointValue),
52    EffectiveStakeAtRewardedEpoch(u64),
53    PriorTotalLamports(u64),
54    Delegation(Delegation, Pubkey),
55    /// Commission as a percentage (0-100).
56    Commission(u8),
57    /// Commission in basis points (0-10,000 representing 0-100%).
58    /// Used when `commission_rate_in_basis_points` feature is active.
59    CommissionBps(u16),
60    CreditsObserved(u64, Option<u64>),
61    Skipped(SkippedReason),
62}
63
64pub(crate) fn null_tracer() -> Option<impl Fn(&InflationPointCalculationEvent)> {
65    None::<fn(&_)>
66}
67
68#[derive(Debug)]
69pub enum SkippedReason {
70    DisabledInflation,
71    JustActivated,
72    TooEarlyUnfairSplit,
73    ZeroPoints,
74    ZeroPointValue,
75    ZeroReward,
76    ZeroCreditsAndReturnZero,
77    ZeroCreditsAndReturnCurrent,
78    ZeroCreditsAndReturnRewound,
79}
80
81impl From<SkippedReason> for InflationPointCalculationEvent {
82    fn from(reason: SkippedReason) -> Self {
83        InflationPointCalculationEvent::Skipped(reason)
84    }
85}
86
87// DEVELOPER NOTE: The commission is intentionally not included here because it
88// is determined from past epoch vote state.
89pub(crate) struct DelegatedVoteState<'a> {
90    pub(crate) credits: u64,
91    pub(crate) epoch_credits_iter: Box<dyn Iterator<Item = (Epoch, u64, u64)> + 'a>,
92}
93
94impl<'a> From<&'a VoteStateView> for DelegatedVoteState<'a> {
95    fn from(vote_state: &'a VoteStateView) -> Self {
96        DelegatedVoteState {
97            credits: vote_state.credits(),
98            epoch_credits_iter: Box::new(vote_state.epoch_credits_iter().map(Into::into)),
99        }
100    }
101}
102
103pub(crate) fn calculate_points_for_tower(
104    stake_state: &StakeStateV2,
105    vote_state: DelegatedVoteState,
106    stake_history: &StakeHistory,
107    new_rate_activation_epoch: Option<Epoch>,
108    use_fixed_point_stake_math: bool,
109) -> Result<u128, InstructionError> {
110    if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state {
111        Ok(calculate_stake_points_for_tower(
112            stake,
113            vote_state,
114            stake_history,
115            null_tracer(),
116            new_rate_activation_epoch,
117            use_fixed_point_stake_math,
118        ))
119    } else {
120        Err(InstructionError::InvalidAccountData)
121    }
122}
123
124/// This function is used to calculate `PointValue::points` for tower epoch and tower portion of
125/// the migration epoch.
126fn calculate_stake_points_for_tower(
127    stake: &Stake,
128    vote_state: DelegatedVoteState,
129    stake_history: &StakeHistory,
130    inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
131    new_rate_activation_epoch: Option<Epoch>,
132    use_fixed_point_stake_math: bool,
133) -> u128 {
134    calculate_stake_points_and_credits(
135        stake,
136        vote_state,
137        stake_history,
138        inflation_point_calc_tracer,
139        new_rate_activation_epoch,
140        &AlpenglowEpochType::Tower,
141        use_fixed_point_stake_math,
142    )
143    .tower_points
144}
145
146fn record_error(msg: String) {
147    error!("{msg}");
148    datapoint_error!(
149        "PER-total-stake-calculation-failure",
150        ("error", msg, String)
151    );
152}
153
154/// Returns how many credits the stake actually earned based on what it has already observed and
155/// the vote account has recorded.
156///
157/// Also updates the `new_credits_observed` as a side effect.
158fn calc_earned_credits(
159    stake: &Stake,
160    final_epoch_credits: u64,
161    initial_epoch_credits: u64,
162    new_credits_observed: &mut u64,
163) -> u128 {
164    let credits_in_stake = stake.credits_observed;
165
166    // figure out how much this stake has seen that
167    //   for which the vote account has a record
168    let earned_credits = if credits_in_stake < initial_epoch_credits {
169        // the staker observed the entire epoch
170        final_epoch_credits - initial_epoch_credits
171    } else if credits_in_stake < final_epoch_credits {
172        // the staker registered sometime during the epoch, partial credit
173        final_epoch_credits - *new_credits_observed
174    } else {
175        // the staker has already observed or been redeemed this epoch
176        //  or was activated after this epoch
177        0
178    };
179    *new_credits_observed = (*new_credits_observed).max(final_epoch_credits);
180    u128::from(earned_credits)
181}
182
183/// Calculates tower points.
184///
185/// Returns (points calculated, new_credits_observed,
186/// true if AG_MIGRATION_EPOCH_CREDIT was seen else false)
187fn tower_epoch_credits_iter(
188    stake: &Stake,
189    epoch_credits_iter: impl Iterator<Item = (Epoch, u64, u64)>,
190    stake_history: &StakeHistory,
191    inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
192    new_rate_activation_epoch: Option<Epoch>,
193    use_fixed_point_stake_math: bool,
194) -> (u128, u64, bool) {
195    let mut points = 0;
196    let credits_in_stake = stake.credits_observed;
197    let mut new_credits_observed = credits_in_stake;
198    let mut saw_marker = false;
199
200    for entry in epoch_credits_iter {
201        if entry == AG_MIGRATION_EPOCH_CREDIT {
202            saw_marker = true;
203            break;
204        }
205        let (epoch, final_epoch_credits, initial_epoch_credits) = entry;
206        let earned_credits = calc_earned_credits(
207            stake,
208            final_epoch_credits,
209            initial_epoch_credits,
210            &mut new_credits_observed,
211        );
212        let stake_amount = u128::from(delegation_effective_stake(
213            &stake.delegation,
214            epoch,
215            stake_history,
216            new_rate_activation_epoch,
217            use_fixed_point_stake_math,
218        ));
219
220        // finally calculate points for this epoch
221        let earned_points = stake_amount * earned_credits;
222        points += earned_points;
223
224        if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
225            inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints(
226                epoch,
227                stake_amount,
228                earned_credits,
229                earned_points,
230            ));
231        }
232    }
233    (points, new_credits_observed, saw_marker)
234}
235
236/// Calculate alpenglow points for `stake` based on the vote account's `reward_epoch_credits`
237///
238/// This value is the lamports paid to the vote account * `stake_amount` / `vote_account_stake`
239/// `vote_account_stake` is fetched from the precomputed `reward_epoch_delegated_stakes` for the
240/// reward epoch
241///
242/// Returns (alpenglow points, new_credits_observed)
243fn calculate_alpenglow_points(
244    stake: &Stake,
245    reward_epoch_credits: Option<(Epoch, u64, u64)>,
246    stake_history: &StakeHistory,
247    inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
248    new_rate_activation_epoch: Option<Epoch>,
249    use_fixed_point_stake_math: bool,
250    reward_epoch_delegated_stakes: &RewardEpochDelegatedStakes,
251) -> Result<(u128, u64), CalculatedStakePoints> {
252    let Some((epoch, final_epoch_credits, initial_epoch_credits)) = reward_epoch_credits else {
253        return Ok((0, stake.credits_observed));
254    };
255    if epoch != reward_epoch_delegated_stakes.epoch {
256        // In this case, the vote account did not record any credits in this epoch
257        // The latest entry is from a prior epoch - thus the delegation gets 0 rewards
258        return Ok((0, stake.credits_observed));
259    }
260
261    let (earned_credits, new_credits_observed) = {
262        let mut new_credits_observed = stake.credits_observed;
263        let earned_credits = calc_earned_credits(
264            stake,
265            final_epoch_credits,
266            initial_epoch_credits,
267            &mut new_credits_observed,
268        );
269        (earned_credits, new_credits_observed)
270    };
271
272    let stake_amount = u128::from(delegation_effective_stake(
273        &stake.delegation,
274        epoch,
275        stake_history,
276        new_rate_activation_epoch,
277        use_fixed_point_stake_math,
278    ));
279
280    let earned_points = if earned_credits == 0 || stake_amount == 0 {
281        0
282    } else {
283        let Some(total_stake) = reward_epoch_delegated_stakes
284            .delegated_stakes
285            .get(&stake.delegation.voter_pubkey)
286            .copied()
287            .filter(|stake| *stake != 0)
288        else {
289            record_error(format!(
290                "AG delegated stake denominator for vote_pubkey={} in epoch={} failed",
291                stake.delegation.voter_pubkey, reward_epoch_delegated_stakes.epoch
292            ));
293            return Err(CalculatedStakePoints {
294                tower_points: 0,
295                ag_points: 0,
296                new_credits_observed,
297                force_credits_update_with_skipped_reward: true,
298            });
299        };
300        earned_credits * stake_amount / total_stake as u128
301    };
302
303    if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
304        inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints(
305            epoch,
306            stake_amount,
307            earned_credits,
308            earned_points,
309        ));
310    }
311    Ok((earned_points, new_credits_observed))
312}
313
314/// Calculates the tower and alpenglow points for `stake` based on the vote account's `reward_epoch_credits`
315/// for the alpenglow migration epoch
316///
317/// Expects the epoch_credits_iter is sorted in ascending epoch order (excluding the migration marker)
318/// Returns (tower_points, alpenglow points, new_credits_observed)
319fn calculate_migration_points(
320    stake: &Stake,
321    mut epoch_credits_iter: impl Iterator<Item = (Epoch, u64, u64)>,
322    stake_history: &StakeHistory,
323    inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
324    new_rate_activation_epoch: Option<Epoch>,
325    use_fixed_point_stake_math: bool,
326    reward_epoch_delegated_stakes: &RewardEpochDelegatedStakes,
327) -> Result<(u128, u128, u64), CalculatedStakePoints> {
328    let (tower_points, tower_new_credits_observed, saw_marker) = tower_epoch_credits_iter(
329        stake,
330        epoch_credits_iter.by_ref(),
331        stake_history,
332        inflation_point_calc_tracer.as_ref(),
333        new_rate_activation_epoch,
334        use_fixed_point_stake_math,
335    );
336    let (ag_points, ag_new_credits_observed) = if saw_marker {
337        calculate_alpenglow_points(
338            stake,
339            epoch_credits_iter.next(),
340            stake_history,
341            inflation_point_calc_tracer,
342            new_rate_activation_epoch,
343            use_fixed_point_stake_math,
344            reward_epoch_delegated_stakes,
345        )?
346    } else {
347        (0, stake.credits_observed)
348    };
349
350    let new_credits_observed = tower_new_credits_observed.max(ag_new_credits_observed);
351    Ok((tower_points, ag_points, new_credits_observed))
352}
353
354/// for a given stake and vote_state, calculate how many
355///   points were earned (credits * stake) and new value
356///   for credits_observed were the points paid
357pub(crate) fn calculate_stake_points_and_credits(
358    stake: &Stake,
359    vote_state: DelegatedVoteState,
360    stake_history: &StakeHistory,
361    inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
362    new_rate_activation_epoch: Option<Epoch>,
363    ag_epoch_type: &AlpenglowEpochType,
364    use_fixed_point_stake_math: bool,
365) -> CalculatedStakePoints {
366    let credits_in_stake = stake.credits_observed;
367    let credits_in_vote = vote_state.credits;
368    // if there is no newer credits since observed, return no point
369    match credits_in_vote.cmp(&credits_in_stake) {
370        Ordering::Less => {
371            if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
372                inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnRewound.into());
373            }
374            // Don't adjust stake.activation_epoch for simplicity:
375            //  - generally fast-forwarding stake.activation_epoch forcibly (for
376            //    artificial re-activation with re-warm-up) skews the stake
377            //    history sysvar. And properly handling all the cases
378            //    regarding deactivation epoch/warm-up/cool-down without
379            //    introducing incentive skew is hard.
380            //  - Conceptually, it should be acceptable for the staked SOLs at
381            //    the recreated vote to receive rewards again immediately after
382            //    rewind even if it looks like instant activation. That's
383            //    because it must have passed the required warmed-up at least
384            //    once in the past already
385            //  - Also such a stake account remains to be a part of overall
386            //    effective stake calculation even while the vote account is
387            //    missing for (indefinite) time or remains to be pre-remove
388            //    credits score. It should be treated equally to staking with
389            //    delinquent validator with no differentiation.
390
391            // hint with true to indicate some exceptional credits handling is needed
392            return CalculatedStakePoints {
393                tower_points: 0,
394                ag_points: 0,
395                new_credits_observed: credits_in_vote,
396                force_credits_update_with_skipped_reward: true,
397            };
398        }
399        Ordering::Equal => {
400            if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
401                inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnCurrent.into());
402            }
403            // don't hint caller and return current value if credits remain unchanged (= delinquent)
404            return CalculatedStakePoints {
405                tower_points: 0,
406                ag_points: 0,
407                new_credits_observed: credits_in_stake,
408                force_credits_update_with_skipped_reward: false,
409            };
410        }
411        Ordering::Greater => {}
412    }
413
414    let (tower_points, ag_points, new_credits_observed) = match ag_epoch_type {
415        AlpenglowEpochType::Tower => {
416            let (points, credits, _) = tower_epoch_credits_iter(
417                stake,
418                vote_state.epoch_credits_iter,
419                stake_history,
420                inflation_point_calc_tracer,
421                new_rate_activation_epoch,
422                use_fixed_point_stake_math,
423            );
424            (points, 0, credits)
425        }
426        AlpenglowEpochType::MigrationEpoch {
427            migration_epoch,
428            reward_epoch_delegated_stakes,
429            ..
430        } => {
431            debug_assert_eq!(reward_epoch_delegated_stakes.epoch, *migration_epoch);
432            match calculate_migration_points(
433                stake,
434                vote_state.epoch_credits_iter,
435                stake_history,
436                inflation_point_calc_tracer,
437                new_rate_activation_epoch,
438                use_fixed_point_stake_math,
439                reward_epoch_delegated_stakes,
440            ) {
441                Ok(r) => r,
442                Err(e) => return e,
443            }
444        }
445        AlpenglowEpochType::Alpenglow {
446            migration_epoch,
447            reward_epoch_delegated_stakes,
448        } => {
449            debug_assert!(reward_epoch_delegated_stakes.epoch > *migration_epoch);
450            let (ag_points, credits) = match calculate_alpenglow_points(
451                stake,
452                vote_state.epoch_credits_iter.last(),
453                stake_history,
454                inflation_point_calc_tracer,
455                new_rate_activation_epoch,
456                use_fixed_point_stake_math,
457                reward_epoch_delegated_stakes,
458            ) {
459                Ok(result) => result,
460                Err(e) => return e,
461            };
462            (0, ag_points, credits)
463        }
464    };
465    CalculatedStakePoints {
466        tower_points,
467        ag_points,
468        new_credits_observed,
469        force_credits_update_with_skipped_reward: false,
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use {
476        super::*,
477        solana_native_token::LAMPORTS_PER_SOL,
478        solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler},
479        std::cell::Cell,
480    };
481
482    impl<'a> From<&'a VoteStateV4> for DelegatedVoteState<'a> {
483        fn from(vote_state: &'a VoteStateV4) -> Self {
484            DelegatedVoteState {
485                credits: vote_state.credits(),
486                epoch_credits_iter: Box::new(vote_state.epoch_credits.iter().copied()),
487            }
488        }
489    }
490
491    fn new_stake(
492        stake: u64,
493        voter_pubkey: &Pubkey,
494        vote_state: &VoteStateV4,
495        activation_epoch: Epoch,
496    ) -> Stake {
497        Stake {
498            delegation: Delegation::new(voter_pubkey, stake, activation_epoch),
499            credits_observed: vote_state.credits(),
500        }
501    }
502
503    #[test]
504    fn test_stake_state_calculate_points_with_typical_values() {
505        let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default());
506
507        // bootstrap means fully-vested stake at epoch 0 with
508        //  10_000_000 SOL is a big but not unreasonable stake
509        let stake = new_stake(
510            10_000_000 * LAMPORTS_PER_SOL,
511            &Pubkey::default(),
512            vote_state.as_ref_v4(),
513            u64::MAX,
514        );
515
516        let epoch_slots: u128 = 14 * 24 * 3600 * 160;
517        // put 193,536,000 credits in at epoch 0, typical for a 14-day epoch
518        //  this loop takes a few seconds...
519        for _ in 0..epoch_slots {
520            vote_state.increment_credits(0, 1);
521        }
522
523        // no overflow on points
524        assert_eq!(
525            u128::from(stake.delegation.stake) * epoch_slots,
526            calculate_stake_points_for_tower(
527                &stake,
528                DelegatedVoteState::from(vote_state.as_ref_v4()),
529                &StakeHistory::default(),
530                null_tracer(),
531                None,
532                true,
533            )
534        );
535    }
536
537    #[test]
538    fn test_tower_epoch_credits_iter() {
539        let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL;
540        let credits = 1235;
541
542        let stake = new_stake(
543            stake_lamports,
544            &Pubkey::default(),
545            VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(),
546            u64::MAX,
547        );
548
549        let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)];
550        let mut epoch_credits_iter = epoch_credits.into_iter();
551        let (points, new_credits, saw_marker) = tower_epoch_credits_iter(
552            &stake,
553            epoch_credits_iter.by_ref(),
554            &StakeHistory::default(),
555            null_tracer(),
556            None,
557            true,
558        );
559        assert_eq!(points, credits as u128 * stake_lamports as u128 * 2);
560        assert_eq!(new_credits, credits * 2);
561        assert_eq!(epoch_credits_iter.next(), None);
562        assert!(!saw_marker);
563
564        let epoch_credits = vec![
565            (0, credits, 0),
566            (1, credits * 2, credits),
567            AG_MIGRATION_EPOCH_CREDIT,
568        ];
569        let mut epoch_credits_iter = epoch_credits.into_iter();
570        let (points, new_credits, saw_marker) = tower_epoch_credits_iter(
571            &stake,
572            epoch_credits_iter.by_ref(),
573            &StakeHistory::default(),
574            null_tracer(),
575            None,
576            true,
577        );
578        assert_eq!(points, credits as u128 * stake_lamports as u128 * 2);
579        assert_eq!(new_credits, credits * 2);
580        assert_eq!(epoch_credits_iter.next(), None);
581        assert!(saw_marker);
582
583        let epoch_credits = vec![
584            (0, credits, 0),
585            (1, credits * 2, credits),
586            AG_MIGRATION_EPOCH_CREDIT,
587            (1, credits * 3, credits * 2),
588        ];
589        let mut epoch_credits_iter = epoch_credits.into_iter();
590        let (points, new_credits, saw_marker) = tower_epoch_credits_iter(
591            &stake,
592            epoch_credits_iter.by_ref(),
593            &StakeHistory::default(),
594            null_tracer(),
595            None,
596            true,
597        );
598        assert_eq!(points, credits as u128 * stake_lamports as u128 * 2);
599        assert_eq!(new_credits, credits * 2);
600        assert_eq!(
601            epoch_credits_iter.next().unwrap(),
602            (1, credits * 3, credits * 2)
603        );
604        assert!(saw_marker);
605    }
606
607    #[test]
608    fn test_calculate_alpenglow_points() {
609        let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL;
610        let total_stake = stake_lamports * 2;
611        let credits = 1235;
612
613        let stake = new_stake(
614            stake_lamports,
615            &Pubkey::default(),
616            VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(),
617            u64::MAX,
618        );
619
620        let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
621            epoch: 1,
622            delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
623        };
624
625        let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)];
626        let (points, new_credits) = calculate_alpenglow_points(
627            &stake,
628            epoch_credits.into_iter().last(),
629            &StakeHistory::default(),
630            null_tracer(),
631            None,
632            true,
633            &reward_epoch_delegated_stakes,
634        )
635        .unwrap();
636        assert_eq!(
637            points,
638            credits as u128 * stake_lamports as u128 / total_stake as u128
639        );
640        assert_eq!(new_credits, credits * 2);
641
642        let epoch_credits = vec![
643            (0, credits, 0),
644            AG_MIGRATION_EPOCH_CREDIT,
645            (0, credits * 2, credits),
646            (1, credits * 3, credits * 2),
647        ];
648        let (points, new_credits) = calculate_alpenglow_points(
649            &stake,
650            epoch_credits.into_iter().last(),
651            &StakeHistory::default(),
652            null_tracer(),
653            None,
654            true,
655            &reward_epoch_delegated_stakes,
656        )
657        .unwrap();
658        assert_eq!(
659            points,
660            credits as u128 * stake_lamports as u128 / total_stake as u128
661        );
662        assert_eq!(new_credits, credits * 3);
663
664        let missing_reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
665            epoch: 2,
666            delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
667        };
668        let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)];
669        let (points, new_credits) = calculate_alpenglow_points(
670            &stake,
671            epoch_credits.into_iter().last(),
672            &StakeHistory::default(),
673            null_tracer(),
674            None,
675            true,
676            &missing_reward_epoch_delegated_stakes,
677        )
678        .unwrap();
679        assert_eq!(points, 0);
680        assert_eq!(new_credits, 0);
681    }
682
683    #[test]
684    fn test_calculate_alpenglow_points_uses_current_delegated_stake_denominator() {
685        let stake_lamports = 200;
686        let current_delegated_total = 200;
687        let credits = 10;
688
689        let stake = Stake {
690            delegation: Delegation {
691                voter_pubkey: Pubkey::default(),
692                stake: stake_lamports,
693                activation_epoch: u64::MAX,
694                deactivation_epoch: u64::MAX,
695                ..Default::default()
696            },
697            credits_observed: 0,
698        };
699
700        let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
701            epoch: 1,
702            delegated_stakes: [(Pubkey::default(), current_delegated_total)]
703                .into_iter()
704                .collect(),
705        };
706        let epoch_credits = vec![
707            (0, credits, 0),
708            AG_MIGRATION_EPOCH_CREDIT,
709            (0, credits * 2, credits),
710            (1, credits * 3, credits * 2),
711        ];
712
713        let (points, new_credits) = calculate_alpenglow_points(
714            &stake,
715            epoch_credits.into_iter().last(),
716            &StakeHistory::default(),
717            null_tracer(),
718            None,
719            true,
720            &reward_epoch_delegated_stakes,
721        )
722        .unwrap();
723
724        assert_eq!(points, credits as u128);
725        assert_eq!(new_credits, credits * 3);
726    }
727
728    #[test]
729    fn test_alpenglow_uses_rewarded_epoch_credits_only() {
730        let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL;
731        let total_stake = stake_lamports * 2;
732        let credits = 1235;
733
734        let stake = Stake {
735            delegation: Delegation {
736                voter_pubkey: Pubkey::default(),
737                stake: stake_lamports,
738                activation_epoch: u64::MAX,
739                deactivation_epoch: u64::MAX,
740                ..Default::default()
741            },
742            credits_observed: 0,
743        };
744        let vote_state = VoteStateV4 {
745            epoch_credits: (1..=64)
746                .map(|epoch| {
747                    let initial_credits = (epoch - 1) * credits;
748                    (epoch, initial_credits + credits, initial_credits)
749                })
750                .collect(),
751            ..VoteStateV4::default()
752        };
753
754        let ag_epoch_type = AlpenglowEpochType::Alpenglow {
755            migration_epoch: 0,
756            reward_epoch_delegated_stakes: RewardEpochDelegatedStakes {
757                epoch: 64,
758                delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
759            },
760        };
761
762        let calculated_points_events = Cell::new(0);
763        let tracer = |event: &InflationPointCalculationEvent| {
764            if matches!(event, InflationPointCalculationEvent::CalculatedPoints(..)) {
765                calculated_points_events.set(calculated_points_events.get() + 1);
766            }
767        };
768
769        let calculated_stake_points = calculate_stake_points_and_credits(
770            &stake,
771            DelegatedVoteState::from(&vote_state),
772            &StakeHistory::default(),
773            Some(tracer),
774            None,
775            &ag_epoch_type,
776            true,
777        );
778
779        assert_eq!(calculated_points_events.get(), 1);
780        assert_eq!(
781            calculated_stake_points,
782            CalculatedStakePoints {
783                tower_points: 0,
784                ag_points: credits as u128 * stake_lamports as u128 / total_stake as u128,
785                new_credits_observed: credits * 64,
786                force_credits_update_with_skipped_reward: false,
787            }
788        );
789    }
790
791    #[test]
792    fn test_alpenglow_rewarded_epoch_uses_delegated_stake_denominator() {
793        let stake_lamports = 200;
794        let current_delegated_total = 200;
795        let credits = 10;
796
797        let stake = Stake {
798            delegation: Delegation {
799                voter_pubkey: Pubkey::default(),
800                stake: stake_lamports,
801                activation_epoch: u64::MAX,
802                deactivation_epoch: u64::MAX,
803                ..Default::default()
804            },
805            credits_observed: 0,
806        };
807        let vote_state = VoteStateV4 {
808            epoch_credits: vec![(1, credits, 0)],
809            ..VoteStateV4::default()
810        };
811
812        let ag_epoch_type = AlpenglowEpochType::Alpenglow {
813            migration_epoch: 0,
814            reward_epoch_delegated_stakes: RewardEpochDelegatedStakes {
815                epoch: 1,
816                delegated_stakes: [(Pubkey::default(), current_delegated_total)]
817                    .into_iter()
818                    .collect(),
819            },
820        };
821
822        let calculated_stake_points = calculate_stake_points_and_credits(
823            &stake,
824            DelegatedVoteState::from(&vote_state),
825            &StakeHistory::default(),
826            null_tracer(),
827            None,
828            &ag_epoch_type,
829            true,
830        );
831
832        assert_eq!(
833            calculated_stake_points,
834            CalculatedStakePoints {
835                tower_points: 0,
836                ag_points: credits as u128,
837                new_credits_observed: credits,
838                force_credits_update_with_skipped_reward: false,
839            }
840        );
841    }
842
843    #[test]
844    fn test_calculate_migration_points() {
845        let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL;
846        let total_stake = stake_lamports * 2;
847        let credits = 1235;
848
849        let stake = new_stake(
850            stake_lamports,
851            &Pubkey::default(),
852            VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(),
853            u64::MAX,
854        );
855
856        let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)];
857        let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
858            epoch: 2,
859            delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
860        };
861        let (tower_points, ag_points, new_credits) = calculate_migration_points(
862            &stake,
863            epoch_credits.into_iter(),
864            &StakeHistory::default(),
865            null_tracer(),
866            None,
867            true,
868            &reward_epoch_delegated_stakes,
869        )
870        .unwrap();
871        assert_eq!(tower_points, credits as u128 * stake_lamports as u128 * 2);
872        assert_eq!(ag_points, 0);
873        assert_eq!(new_credits, credits * 2);
874
875        let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
876            epoch: 0,
877            delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
878        };
879        let epoch_credits = vec![
880            (0, credits, 0),
881            AG_MIGRATION_EPOCH_CREDIT,
882            (0, credits * 2, credits),
883        ];
884        let (tower_points, ag_points, new_credits) = calculate_migration_points(
885            &stake,
886            epoch_credits.into_iter(),
887            &StakeHistory::default(),
888            null_tracer(),
889            None,
890            true,
891            &reward_epoch_delegated_stakes,
892        )
893        .unwrap();
894        assert_eq!(tower_points, credits as u128 * stake_lamports as u128);
895        assert_eq!(
896            ag_points,
897            credits as u128 * stake_lamports as u128 / total_stake as u128
898        );
899        assert_eq!(new_credits, credits * 2);
900
901        let epoch_credits = vec![AG_MIGRATION_EPOCH_CREDIT, (0, credits * 2, credits)];
902        let (tower_points, ag_points, new_credits) = calculate_migration_points(
903            &stake,
904            epoch_credits.into_iter(),
905            &StakeHistory::default(),
906            null_tracer(),
907            None,
908            true,
909            &reward_epoch_delegated_stakes,
910        )
911        .unwrap();
912        assert_eq!(tower_points, 0);
913        assert_eq!(
914            ag_points,
915            credits as u128 * stake_lamports as u128 / total_stake as u128
916        );
917        assert_eq!(new_credits, credits * 2);
918    }
919
920    #[test]
921    fn test_changing_total_stake() {
922        let pubkey = Pubkey::new_unique();
923        let staker_delegation = LAMPORTS_PER_SOL;
924        let reward_epoch_validator_stake = staker_delegation * 5;
925        let stake = Stake {
926            delegation: Delegation {
927                voter_pubkey: pubkey,
928                stake: staker_delegation,
929                activation_epoch: u64::MAX,
930                deactivation_epoch: u64::MAX,
931                ..Default::default()
932            },
933            credits_observed: 0,
934        };
935
936        let credits = 1235;
937        let epoch_credits = vec![
938            (0, credits, 0),
939            AG_MIGRATION_EPOCH_CREDIT,
940            (0, credits * 2, credits),
941            (1, credits * 3, credits * 2),
942            (2, credits * 4, credits * 3),
943        ];
944        let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
945            epoch: 2,
946            delegated_stakes: [(pubkey, reward_epoch_validator_stake)]
947                .into_iter()
948                .collect(),
949        };
950        let (points, new_credits) = calculate_alpenglow_points(
951            &stake,
952            epoch_credits.into_iter().last(),
953            &StakeHistory::default(),
954            null_tracer(),
955            None,
956            true,
957            &reward_epoch_delegated_stakes,
958        )
959        .unwrap();
960        assert_eq!(new_credits, credits * 4);
961        let expected_points = credits * staker_delegation / reward_epoch_validator_stake;
962        assert_eq!(points, expected_points as u128);
963    }
964
965    #[test]
966    fn test_stake_activating_deactivating() {
967        let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL;
968        let credits = 1235;
969
970        for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] {
971            let stake = Stake {
972                delegation: Delegation {
973                    voter_pubkey: Pubkey::default(),
974                    stake: stake_lamports,
975                    activation_epoch,
976                    deactivation_epoch,
977                    ..Default::default()
978                },
979                credits_observed: 0,
980            };
981            let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)];
982            let mut epoch_credits_iter = epoch_credits.into_iter();
983            let (points, new_credits, saw_marker) = tower_epoch_credits_iter(
984                &stake,
985                epoch_credits_iter.by_ref(),
986                &StakeHistory::default(),
987                null_tracer(),
988                None,
989                true,
990            );
991            assert_eq!(points, credits as u128 * stake_lamports as u128);
992            assert_eq!(new_credits, credits * 2);
993            assert_eq!(epoch_credits_iter.next(), None);
994            assert!(!saw_marker);
995        }
996
997        for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] {
998            let stake = Stake {
999                delegation: Delegation {
1000                    voter_pubkey: Pubkey::default(),
1001                    stake: stake_lamports,
1002                    activation_epoch,
1003                    deactivation_epoch,
1004                    ..Default::default()
1005                },
1006                credits_observed: 0,
1007            };
1008
1009            let total_stake = stake_lamports * 2;
1010            let epoch_credits = vec![
1011                (0, credits, 0),
1012                AG_MIGRATION_EPOCH_CREDIT,
1013                (0, credits * 2, credits),
1014                (1, credits * 3, credits * 2),
1015            ];
1016            let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
1017                epoch: 1,
1018                delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
1019            };
1020            let (points, new_credits) = calculate_alpenglow_points(
1021                &stake,
1022                epoch_credits.into_iter().last(),
1023                &StakeHistory::default(),
1024                null_tracer(),
1025                None,
1026                true,
1027                &reward_epoch_delegated_stakes,
1028            )
1029            .unwrap();
1030            let expected_points = if activation_epoch == 0 {
1031                credits as u128 * stake_lamports as u128 / total_stake as u128
1032            } else {
1033                0
1034            };
1035            assert_eq!(points, expected_points);
1036            assert_eq!(new_credits, credits * 3);
1037        }
1038
1039        for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] {
1040            let stake = Stake {
1041                delegation: Delegation {
1042                    voter_pubkey: Pubkey::default(),
1043                    stake: stake_lamports,
1044                    activation_epoch,
1045                    deactivation_epoch,
1046                    ..Default::default()
1047                },
1048                credits_observed: 0,
1049            };
1050
1051            let total_stake = stake_lamports * 2;
1052            let epoch_credits = vec![
1053                (0, credits, 0),
1054                AG_MIGRATION_EPOCH_CREDIT,
1055                (0, credits * 2, credits),
1056                (1, credits * 3, credits * 2),
1057            ];
1058            let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes {
1059                epoch: 0,
1060                delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(),
1061            };
1062            let (tower_points, ag_points, new_credits) = calculate_migration_points(
1063                &stake,
1064                epoch_credits.into_iter(),
1065                &StakeHistory::default(),
1066                null_tracer(),
1067                None,
1068                true,
1069                &reward_epoch_delegated_stakes,
1070            )
1071            .unwrap();
1072            if activation_epoch == 0 {
1073                assert_eq!(tower_points, 0);
1074            } else {
1075                assert_eq!(tower_points, credits as u128 * stake_lamports as u128);
1076            }
1077            let expected_ag_points = if activation_epoch == 0 {
1078                0
1079            } else {
1080                credits as u128 * stake_lamports as u128 / total_stake as u128
1081            };
1082            assert_eq!(ag_points, expected_ag_points);
1083            assert_eq!(new_credits, credits * 2);
1084        }
1085    }
1086}