use {
solana_clock::Epoch,
solana_instruction::error::InstructionError,
solana_pubkey::Pubkey,
solana_stake_interface::{
stake_history::StakeHistory,
state::{Delegation, Stake, StakeStateV2},
},
solana_vote::vote_state_view::VoteStateView,
std::cmp::Ordering,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PointValue {
pub rewards: u64, pub points: u128, }
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct CalculatedStakePoints {
pub(crate) points: u128,
pub(crate) new_credits_observed: u64,
pub(crate) force_credits_update_with_skipped_reward: bool,
}
#[derive(Debug)]
pub enum InflationPointCalculationEvent {
CalculatedPoints(u64, u128, u128, u128),
SplitRewards(u64, u64, u64, PointValue),
EffectiveStakeAtRewardedEpoch(u64),
RentExemptReserve(u64),
Delegation(Delegation, Pubkey),
Commission(u8),
CommissionBps(u16),
CreditsObserved(u64, Option<u64>),
Skipped(SkippedReason),
}
pub(crate) fn null_tracer() -> Option<impl Fn(&InflationPointCalculationEvent)> {
None::<fn(&_)>
}
#[derive(Debug)]
pub enum SkippedReason {
DisabledInflation,
JustActivated,
TooEarlyUnfairSplit,
ZeroPoints,
ZeroPointValue,
ZeroReward,
ZeroCreditsAndReturnZero,
ZeroCreditsAndReturnCurrent,
ZeroCreditsAndReturnRewound,
}
impl From<SkippedReason> for InflationPointCalculationEvent {
fn from(reason: SkippedReason) -> Self {
InflationPointCalculationEvent::Skipped(reason)
}
}
pub(crate) struct DelegatedVoteState<'a> {
pub(crate) credits: u64,
pub(crate) epoch_credits_iter: Box<dyn Iterator<Item = (Epoch, u64, u64)> + 'a>,
}
impl<'a> From<&'a VoteStateView> for DelegatedVoteState<'a> {
fn from(vote_state: &'a VoteStateView) -> Self {
DelegatedVoteState {
credits: vote_state.credits(),
epoch_credits_iter: Box::new(vote_state.epoch_credits_iter().map(Into::into)),
}
}
}
pub(crate) fn calculate_points(
stake_state: &StakeStateV2,
vote_state: DelegatedVoteState,
stake_history: &StakeHistory,
new_rate_activation_epoch: Option<Epoch>,
) -> Result<u128, InstructionError> {
if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state {
Ok(calculate_stake_points(
stake,
vote_state,
stake_history,
null_tracer(),
new_rate_activation_epoch,
))
} else {
Err(InstructionError::InvalidAccountData)
}
}
fn calculate_stake_points(
stake: &Stake,
vote_state: DelegatedVoteState,
stake_history: &StakeHistory,
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
new_rate_activation_epoch: Option<Epoch>,
) -> u128 {
calculate_stake_points_and_credits(
stake,
vote_state,
stake_history,
inflation_point_calc_tracer,
new_rate_activation_epoch,
)
.points
}
pub(crate) fn calculate_stake_points_and_credits(
stake: &Stake,
vote_state: DelegatedVoteState,
stake_history: &StakeHistory,
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
new_rate_activation_epoch: Option<Epoch>,
) -> CalculatedStakePoints {
let credits_in_stake = stake.credits_observed;
let credits_in_vote = vote_state.credits;
match credits_in_vote.cmp(&credits_in_stake) {
Ordering::Less => {
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnRewound.into());
}
return CalculatedStakePoints {
points: 0,
new_credits_observed: credits_in_vote,
force_credits_update_with_skipped_reward: true,
};
}
Ordering::Equal => {
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnCurrent.into());
}
return CalculatedStakePoints {
points: 0,
new_credits_observed: credits_in_stake,
force_credits_update_with_skipped_reward: false,
};
}
Ordering::Greater => {}
}
let mut points = 0;
let mut new_credits_observed = credits_in_stake;
for epoch_credits_item in vote_state.epoch_credits_iter {
let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item;
let stake_amount = u128::from(stake.delegation.stake(
epoch,
stake_history,
new_rate_activation_epoch,
));
let earned_credits = if credits_in_stake < initial_epoch_credits {
final_epoch_credits - initial_epoch_credits
} else if credits_in_stake < final_epoch_credits {
final_epoch_credits - new_credits_observed
} else {
0
};
let earned_credits = u128::from(earned_credits);
new_credits_observed = new_credits_observed.max(final_epoch_credits);
let earned_points = stake_amount * earned_credits;
points += earned_points;
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints(
epoch,
stake_amount,
earned_credits,
earned_points,
));
}
}
CalculatedStakePoints {
points,
new_credits_observed,
force_credits_update_with_skipped_reward: false,
}
}
#[cfg(test)]
mod tests {
use {
super::*,
solana_native_token::LAMPORTS_PER_SOL,
solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandle},
};
impl<'a> From<&'a VoteStateV4> for DelegatedVoteState<'a> {
fn from(vote_state: &'a VoteStateV4) -> Self {
DelegatedVoteState {
credits: vote_state.credits(),
epoch_credits_iter: Box::new(vote_state.epoch_credits.iter().copied()),
}
}
}
fn new_stake(
stake: u64,
voter_pubkey: &Pubkey,
vote_state: &VoteStateV4,
activation_epoch: Epoch,
) -> Stake {
Stake {
delegation: Delegation::new(voter_pubkey, stake, activation_epoch),
credits_observed: vote_state.credits(),
}
}
#[test]
fn test_stake_state_calculate_points_with_typical_values() {
let mut vote_state = VoteStateV4::default();
let stake = new_stake(
10_000_000 * LAMPORTS_PER_SOL,
&Pubkey::default(),
&vote_state,
u64::MAX,
);
let epoch_slots: u128 = 14 * 24 * 3600 * 160;
for _ in 0..epoch_slots {
vote_state.increment_credits(0, 1);
}
assert_eq!(
u128::from(stake.delegation.stake) * epoch_slots,
calculate_stake_points(
&stake,
DelegatedVoteState::from(&vote_state),
&StakeHistory::default(),
null_tracer(),
None
)
);
}
}