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