use {
crate::bank::{Bank, EpochInflationRewards},
solana_account::AccountSharedData,
solana_clock::{Epoch, Slot},
solana_pubkey::Pubkey,
solana_system_interface::program as system_program,
std::sync::LazyLock,
thiserror::Error,
};
static VOTE_REWARD_ACCOUNT_ADDR: LazyLock<Pubkey> = LazyLock::new(|| {
let (pubkey, _) = Pubkey::find_program_address(
&[b"vote_reward_account"],
&agave_feature_set::alpenglow::id(),
);
pubkey
});
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct VoteRewardAccountState {
epoch_validator_rewards_lamports: u64,
}
impl VoteRewardAccountState {
fn new_from_bank(bank: &Bank) -> Self {
match bank.get_account(&VOTE_REWARD_ACCOUNT_ADDR) {
None => {
let state = Self {
epoch_validator_rewards_lamports: 0,
};
state.set_state(bank);
state
}
Some(acct) => {
acct.deserialize_data().unwrap()
}
}
}
fn set_state(&self, bank: &Bank) {
let account_size = bincode::serialized_size(&self).unwrap();
let lamports = bank
.rent_collector()
.rent
.minimum_balance(account_size as usize);
let account = AccountSharedData::new_data(lamports, &self, &system_program::ID).unwrap();
bank.store_account_and_update_capitalization(&VOTE_REWARD_ACCOUNT_ADDR, &account);
}
pub(crate) fn new_epoch_update_account(
bank: &Bank,
prev_epoch: Epoch,
prev_epoch_capitalization: u64,
additional_validator_rewards: u64,
) {
let EpochInflationRewards {
validator_rewards_lamports,
epoch_duration_in_years: _,
validator_rate: _,
foundation_rate: _,
} = bank.calculate_epoch_inflation_rewards(
prev_epoch_capitalization + additional_validator_rewards,
prev_epoch,
);
let state = Self {
epoch_validator_rewards_lamports: validator_rewards_lamports,
};
state.set_state(bank);
}
#[cfg(test)]
pub(crate) fn rent_needed_for_account(bank: &Bank) -> u64 {
let state = Self {
epoch_validator_rewards_lamports: 0,
};
let account_size = bincode::serialized_size(&state).unwrap();
bank.rent_collector()
.rent
.minimum_balance(account_size as usize)
}
}
#[derive(Debug, PartialEq, Eq, Error)]
pub enum PayVoteRewardError {
#[error("missing epoch stakes")]
MissingEpochStakes,
#[error("missing validator")]
MissingValidator,
}
pub(super) fn calculate_and_pay_voting_reward(
bank: &Bank,
reward_slot_and_validators: Option<(Slot, Vec<Pubkey>)>,
) -> Result<(), PayVoteRewardError> {
let Some((reward_slot, validators)) = reward_slot_and_validators else {
return Ok(());
};
let (vote_accounts, total_stake_lamports) = {
let epoch_stakes = bank
.epoch_stakes_from_slot(reward_slot)
.ok_or(PayVoteRewardError::MissingEpochStakes)?;
(
epoch_stakes.stakes().vote_accounts().as_ref(),
epoch_stakes.total_stake(),
)
};
let epoch_validator_rewards_lamports =
VoteRewardAccountState::new_from_bank(bank).epoch_validator_rewards_lamports;
let mut total_leader_reward_lamports = 0u64;
for validator in validators {
let (validator_stake_lamports, _) = vote_accounts
.get(&validator)
.ok_or(PayVoteRewardError::MissingValidator)?;
let reward_lamports = calculate_voting_reward(
bank.epoch_schedule().slots_per_epoch,
epoch_validator_rewards_lamports,
total_stake_lamports,
*validator_stake_lamports,
);
let validator_reward_lamports = reward_lamports / 2;
let leader_reward_lamports = reward_lamports - validator_reward_lamports;
total_leader_reward_lamports =
total_leader_reward_lamports.saturating_add(leader_reward_lamports);
}
Ok(())
}
fn calculate_voting_reward(
slots_per_epoch: u64,
epoch_validator_rewards_lamports: u64,
total_stake_lamports: u64,
validator_stake_lamports: u64,
) -> u64 {
let numerator = epoch_validator_rewards_lamports as u128 * validator_stake_lamports as u128;
let denominator = slots_per_epoch as u128 * total_stake_lamports as u128;
(numerator / denominator).try_into().unwrap()
}
#[cfg(test)]
mod tests {
use {super::*, solana_genesis_config::GenesisConfig, solana_native_token::LAMPORTS_PER_SOL};
#[test]
fn calculate_voting_reward_does_not_panic() {
let circulating_supply = 566_000_000 * LAMPORTS_PER_SOL;
let bank = Bank::new_for_tests(&GenesisConfig::default());
let EpochInflationRewards {
validator_rewards_lamports,
..
} = bank.calculate_epoch_inflation_rewards(circulating_supply, 1);
calculate_voting_reward(
bank.epoch_schedule().slots_per_epoch,
validator_rewards_lamports,
circulating_supply,
circulating_supply,
);
}
#[test]
fn serialization_works() {
let bank = Bank::new_for_tests(&GenesisConfig::default());
let state = VoteRewardAccountState {
epoch_validator_rewards_lamports: 1234,
};
state.set_state(&bank);
let deserialized = VoteRewardAccountState::new_from_bank(&bank);
assert_eq!(state, deserialized);
}
#[test]
fn epoch_update_works() {
let bank = Bank::new_for_tests(&GenesisConfig::default());
let prev_epoch = 1;
let prev_epoch_capitalization = 12345;
let additional_validator_rewards = 6789;
VoteRewardAccountState::new_epoch_update_account(
&bank,
prev_epoch,
prev_epoch_capitalization,
additional_validator_rewards,
);
let VoteRewardAccountState {
epoch_validator_rewards_lamports,
} = VoteRewardAccountState::new_from_bank(&bank);
let EpochInflationRewards {
validator_rewards_lamports,
..
} = bank.calculate_epoch_inflation_rewards(
prev_epoch_capitalization + additional_validator_rewards,
prev_epoch,
);
assert_eq!(epoch_validator_rewards_lamports, validator_rewards_lamports);
}
}