Skip to main content

solana_runtime/inflation_rewards/
points.rs

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