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