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