1use {
5 solana_clock::Epoch,
6 solana_instruction::error::InstructionError,
7 solana_pubkey::Pubkey,
8 solana_stake_interface::state::{Delegation, Stake, StakeStateV2},
9 solana_sysvar::stake_history::StakeHistory,
10 solana_vote_interface::state::VoteState,
11 std::cmp::Ordering,
12};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct PointValue {
20 pub rewards: u64, pub points: u128, }
23
24#[derive(Debug, PartialEq, Eq)]
25pub(crate) struct CalculatedStakePoints {
26 pub(crate) points: u128,
27 pub(crate) new_credits_observed: u64,
28 pub(crate) force_credits_update_with_skipped_reward: bool,
29}
30
31#[derive(Debug)]
32pub enum InflationPointCalculationEvent {
33 CalculatedPoints(u64, u128, u128, u128),
34 SplitRewards(u64, u64, u64, PointValue),
35 EffectiveStakeAtRewardedEpoch(u64),
36 RentExemptReserve(u64),
37 Delegation(Delegation, Pubkey),
38 Commission(u8),
39 CreditsObserved(u64, Option<u64>),
40 Skipped(SkippedReason),
41}
42
43pub(crate) fn null_tracer() -> Option<impl Fn(&InflationPointCalculationEvent)> {
44 None::<fn(&_)>
45}
46
47#[derive(Debug)]
48pub enum SkippedReason {
49 DisabledInflation,
50 JustActivated,
51 TooEarlyUnfairSplit,
52 ZeroPoints,
53 ZeroPointValue,
54 ZeroReward,
55 ZeroCreditsAndReturnZero,
56 ZeroCreditsAndReturnCurrent,
57 ZeroCreditsAndReturnRewinded,
58}
59
60impl From<SkippedReason> for InflationPointCalculationEvent {
61 fn from(reason: SkippedReason) -> Self {
62 InflationPointCalculationEvent::Skipped(reason)
63 }
64}
65
66#[doc(hidden)]
68pub fn calculate_points(
69 stake_state: &StakeStateV2,
70 vote_state: &VoteState,
71 stake_history: &StakeHistory,
72 new_rate_activation_epoch: Option<Epoch>,
73) -> Result<u128, InstructionError> {
74 if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state {
75 Ok(calculate_stake_points(
76 stake,
77 vote_state,
78 stake_history,
79 null_tracer(),
80 new_rate_activation_epoch,
81 ))
82 } else {
83 Err(InstructionError::InvalidAccountData)
84 }
85}
86
87fn calculate_stake_points(
88 stake: &Stake,
89 vote_state: &VoteState,
90 stake_history: &StakeHistory,
91 inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
92 new_rate_activation_epoch: Option<Epoch>,
93) -> u128 {
94 calculate_stake_points_and_credits(
95 stake,
96 vote_state,
97 stake_history,
98 inflation_point_calc_tracer,
99 new_rate_activation_epoch,
100 )
101 .points
102}
103
104pub(crate) fn calculate_stake_points_and_credits(
108 stake: &Stake,
109 new_vote_state: &VoteState,
110 stake_history: &StakeHistory,
111 inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
112 new_rate_activation_epoch: Option<Epoch>,
113) -> CalculatedStakePoints {
114 let credits_in_stake = stake.credits_observed;
115 let credits_in_vote = new_vote_state.credits();
116 match credits_in_vote.cmp(&credits_in_stake) {
118 Ordering::Less => {
119 if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
120 inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnRewinded.into());
121 }
122 return CalculatedStakePoints {
141 points: 0,
142 new_credits_observed: credits_in_vote,
143 force_credits_update_with_skipped_reward: true,
144 };
145 }
146 Ordering::Equal => {
147 if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
148 inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnCurrent.into());
149 }
150 return CalculatedStakePoints {
152 points: 0,
153 new_credits_observed: credits_in_stake,
154 force_credits_update_with_skipped_reward: false,
155 };
156 }
157 Ordering::Greater => {}
158 }
159
160 let mut points = 0;
161 let mut new_credits_observed = credits_in_stake;
162
163 for (epoch, final_epoch_credits, initial_epoch_credits) in
164 new_vote_state.epoch_credits().iter().copied()
165 {
166 let stake_amount = u128::from(stake.delegation.stake(
167 epoch,
168 stake_history,
169 new_rate_activation_epoch,
170 ));
171
172 let earned_credits = if credits_in_stake < initial_epoch_credits {
175 final_epoch_credits - initial_epoch_credits
177 } else if credits_in_stake < final_epoch_credits {
178 final_epoch_credits - new_credits_observed
180 } else {
181 0
184 };
185 let earned_credits = u128::from(earned_credits);
186
187 new_credits_observed = new_credits_observed.max(final_epoch_credits);
189
190 let earned_points = stake_amount * earned_credits;
192 points += earned_points;
193
194 if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
195 inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints(
196 epoch,
197 stake_amount,
198 earned_credits,
199 earned_points,
200 ));
201 }
202 }
203
204 CalculatedStakePoints {
205 points,
206 new_credits_observed,
207 force_credits_update_with_skipped_reward: false,
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use {super::*, crate::stake_state::new_stake, solana_native_token::sol_to_lamports};
214
215 #[test]
216 fn test_stake_state_calculate_points_with_typical_values() {
217 let mut vote_state = VoteState::default();
218
219 let stake = new_stake(
222 sol_to_lamports(10_000_000f64),
223 &Pubkey::default(),
224 &vote_state,
225 u64::MAX,
226 );
227
228 let epoch_slots: u128 = 14 * 24 * 3600 * 160;
229 for _ in 0..epoch_slots {
232 vote_state.increment_credits(0, 1);
233 }
234
235 assert_eq!(
237 u128::from(stake.delegation.stake) * epoch_slots,
238 calculate_stake_points(
239 &stake,
240 &vote_state,
241 &StakeHistory::default(),
242 null_tracer(),
243 None
244 )
245 );
246 }
247}