1use {
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#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct PointValue {
27 pub rewards: u64, pub points: u128, }
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
39pub(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(u8),
59 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
89pub(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
126fn 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
156fn 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 let earned_credits = if credits_in_stake < initial_epoch_credits {
171 final_epoch_credits - initial_epoch_credits
173 } else if credits_in_stake < final_epoch_credits {
174 final_epoch_credits - *new_credits_observed
176 } else {
177 0
180 };
181 *new_credits_observed = (*new_credits_observed).max(final_epoch_credits);
182 u128::from(earned_credits)
183}
184
185fn 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 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
238fn 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 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
316fn 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
356pub(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 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 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 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 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 for _ in 0..epoch_slots {
522 vote_state.increment_credits(0, 1);
523 }
524
525 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}