rewrite vote credits redemption to eat from rewards_pools on an epoch-sensitive basis (#4775)
* move redemption to rewards pools * rewrite redemption, touch a few other things * re-establish test coverage
This commit is contained in:
@ -3,9 +3,8 @@
|
||||
//! * keep track of rewards
|
||||
//! * own mining pools
|
||||
|
||||
use crate::stake_state::StakeState;
|
||||
use crate::stake_state::create_rewards_pool;
|
||||
use rand::{thread_rng, Rng};
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::genesis_block::Builder;
|
||||
use solana_sdk::hash::{hash, Hash};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
@ -25,10 +24,7 @@ pub fn genesis(mut builder: Builder) -> Builder {
|
||||
let mut pubkey = id();
|
||||
|
||||
for _i in 0..NUM_REWARDS_POOLS {
|
||||
builder = builder.rewards_pool(
|
||||
pubkey,
|
||||
Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap(),
|
||||
);
|
||||
builder = builder.rewards_pool(pubkey, create_rewards_pool());
|
||||
pubkey = Pubkey::new(hash(pubkey.as_ref()).as_ref());
|
||||
}
|
||||
builder
|
||||
|
@ -11,30 +11,25 @@ use solana_sdk::system_instruction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum StakeInstruction {
|
||||
// Initialize the stake account as a MiningPool account
|
||||
///
|
||||
/// Expects 1 Accounts:
|
||||
/// 0 - MiningPool StakeAccount to be initialized
|
||||
InitializeMiningPool,
|
||||
|
||||
/// `Delegate` a stake to a particular node
|
||||
///
|
||||
/// Expects 2 Accounts:
|
||||
/// Expects 3 Accounts:
|
||||
/// 0 - Uninitialized StakeAccount to be delegated <= must have this signature
|
||||
/// 1 - VoteAccount to which this Stake will be delegated
|
||||
/// 2 - Current syscall Account that carries current bank epoch
|
||||
///
|
||||
/// The u64 is the portion of the Stake account balance to be activated,
|
||||
/// must be less than StakeAccount.lamports
|
||||
///
|
||||
/// This instruction resets rewards, so issue
|
||||
DelegateStake(u64),
|
||||
|
||||
/// Redeem credits in the stake account
|
||||
///
|
||||
/// Expects 3 Accounts:
|
||||
/// 0 - MiningPool Stake Account to redeem credits from
|
||||
/// 1 - Delegate StakeAccount to be updated
|
||||
/// 2 - VoteAccount to which the Stake is delegated,
|
||||
/// Expects 4 Accounts:
|
||||
/// 0 - Delegate StakeAccount to be updated with rewards
|
||||
/// 1 - VoteAccount to which the Stake is delegated,
|
||||
/// 2 - RewardsPool Stake Account from which to redeem credits
|
||||
/// 3 - Rewards syscall Account that carries points values
|
||||
RedeemVoteCredits,
|
||||
}
|
||||
|
||||
@ -63,36 +58,12 @@ pub fn create_stake_account_and_delegate_stake(
|
||||
instructions
|
||||
}
|
||||
|
||||
pub fn create_mining_pool_account(
|
||||
from_pubkey: &Pubkey,
|
||||
staker_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::create_account(
|
||||
from_pubkey,
|
||||
staker_pubkey,
|
||||
lamports,
|
||||
std::mem::size_of::<StakeState>() as u64,
|
||||
&id(),
|
||||
),
|
||||
Instruction::new(
|
||||
id(),
|
||||
&StakeInstruction::InitializeMiningPool,
|
||||
vec![AccountMeta::new(*staker_pubkey, false)],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn redeem_vote_credits(
|
||||
mining_pool_pubkey: &Pubkey,
|
||||
stake_pubkey: &Pubkey,
|
||||
vote_pubkey: &Pubkey,
|
||||
) -> Instruction {
|
||||
pub fn redeem_vote_credits(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*mining_pool_pubkey, false),
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new(*vote_pubkey, false),
|
||||
AccountMeta::new(crate::rewards_pools::random_id(), false),
|
||||
AccountMeta::new(syscall::rewards::id(), false),
|
||||
];
|
||||
Instruction::new(id(), &StakeInstruction::RedeemVoteCredits, account_metas)
|
||||
}
|
||||
@ -106,11 +77,6 @@ pub fn delegate_stake(stake_pubkey: &Pubkey, vote_pubkey: &Pubkey, stake: u64) -
|
||||
Instruction::new(id(), &StakeInstruction::DelegateStake(stake), account_metas)
|
||||
}
|
||||
|
||||
fn current(current_account: &KeyedAccount) -> Result<syscall::current::Current, InstructionError> {
|
||||
syscall::current::Current::from(current_account.account)
|
||||
.ok_or(InstructionError::InvalidArgument)
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
@ -130,29 +96,32 @@ pub fn process_instruction(
|
||||
|
||||
// TODO: data-driven unpack and dispatch of KeyedAccounts
|
||||
match deserialize(data).map_err(|_| InstructionError::InvalidInstructionData)? {
|
||||
StakeInstruction::InitializeMiningPool => {
|
||||
if !rest.is_empty() {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
me.initialize_mining_pool()
|
||||
}
|
||||
StakeInstruction::DelegateStake(stake) => {
|
||||
if rest.len() != 2 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
let vote = &rest[0];
|
||||
|
||||
me.delegate_stake(vote, stake, ¤t(&rest[1])?)
|
||||
me.delegate_stake(
|
||||
vote,
|
||||
stake,
|
||||
&syscall::current::from_keyed_account(&rest[1])?,
|
||||
)
|
||||
}
|
||||
StakeInstruction::RedeemVoteCredits => {
|
||||
if rest.len() != 2 {
|
||||
if rest.len() != 3 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
let (stake, vote) = rest.split_at_mut(1);
|
||||
let stake = &mut stake[0];
|
||||
let (vote, rest) = rest.split_at_mut(1);
|
||||
let vote = &mut vote[0];
|
||||
let (rewards_pool, rest) = rest.split_at_mut(1);
|
||||
let rewards_pool = &mut rewards_pool[0];
|
||||
|
||||
me.redeem_vote_credits(stake, vote)
|
||||
me.redeem_vote_credits(
|
||||
vote,
|
||||
rewards_pool,
|
||||
&syscall::rewards::from_keyed_account(&rest[0])?,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,6 +139,8 @@ mod tests {
|
||||
.map(|meta| {
|
||||
if syscall::current::check_id(&meta.pubkey) {
|
||||
syscall::current::create_account(1, 0, 0, 0)
|
||||
} else if syscall::rewards::check_id(&meta.pubkey) {
|
||||
syscall::rewards::create_account(1, 0.0, 0.0)
|
||||
} else {
|
||||
Account::default()
|
||||
}
|
||||
@ -190,11 +161,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_stake_process_instruction() {
|
||||
assert_eq!(
|
||||
process_instruction(&redeem_vote_credits(
|
||||
&Pubkey::default(),
|
||||
&Pubkey::default(),
|
||||
&Pubkey::default()
|
||||
)),
|
||||
process_instruction(&redeem_vote_credits(&Pubkey::default(), &Pubkey::default(),)),
|
||||
Err(InstructionError::InvalidAccountData),
|
||||
);
|
||||
assert_eq!(
|
||||
@ -265,7 +232,7 @@ mod tests {
|
||||
Err(InstructionError::InvalidAccountData),
|
||||
);
|
||||
|
||||
// gets the check in redeem_vote_credits
|
||||
// gets the deserialization checks in redeem_vote_credits
|
||||
assert_eq!(
|
||||
super::process_instruction(
|
||||
&Pubkey::default(),
|
||||
@ -273,6 +240,11 @@ mod tests {
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(&Pubkey::default(), false, &mut Account::default()),
|
||||
KeyedAccount::new(
|
||||
&syscall::rewards::id(),
|
||||
false,
|
||||
&mut syscall::rewards::create_account(1, 0.0, 0.0)
|
||||
),
|
||||
],
|
||||
&serialize(&StakeInstruction::RedeemVoteCredits).unwrap(),
|
||||
),
|
||||
|
@ -10,6 +10,7 @@ use solana_sdk::account_utils::State;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::syscall;
|
||||
use solana_sdk::timing::Epoch;
|
||||
use solana_vote_api::vote_state::VoteState;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
@ -31,18 +32,6 @@ impl Default for StakeState {
|
||||
StakeState::Uninitialized
|
||||
}
|
||||
}
|
||||
// TODO: trusted values of network parameters come from where?
|
||||
const TICKS_PER_SECOND: f64 = 10f64;
|
||||
const TICKS_PER_SLOT: f64 = 8f64;
|
||||
|
||||
// credits/yr or slots/yr is seconds/year * ticks/second * slots/tick
|
||||
const CREDITS_PER_YEAR: f64 = (365f64 * 24f64 * 3600f64) * TICKS_PER_SECOND / TICKS_PER_SLOT;
|
||||
|
||||
// TODO: 20% is a niiice rate... TODO: make this a member of MiningPool?
|
||||
const STAKE_REWARD_TARGET_RATE: f64 = 0.20;
|
||||
|
||||
#[cfg(test)]
|
||||
const STAKE_GETS_PAID_EVERY_VOTE: u64 = 200_000_000; // if numbers above (TICKS_YEAR) move, fix this
|
||||
|
||||
impl StakeState {
|
||||
// utility function, used by Stakes, tests
|
||||
@ -60,20 +49,96 @@ impl StakeState {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Stake {
|
||||
pub voter_pubkey: Pubkey,
|
||||
pub credits_observed: u64,
|
||||
pub stake: u64, // stake amount activated
|
||||
pub activated: Epoch, // epoch the stake was activated
|
||||
pub deactivated: Epoch, // epoch the stake was deactivated, std::Epoch::MAX if not deactivated
|
||||
}
|
||||
pub const STAKE_WARMUP_EPOCHS: u64 = 3;
|
||||
|
||||
impl Default for Stake {
|
||||
fn default() -> Self {
|
||||
Stake {
|
||||
voter_pubkey: Pubkey::default(),
|
||||
credits_observed: 0,
|
||||
stake: 0,
|
||||
activated: 0,
|
||||
deactivated: std::u64::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stake {
|
||||
pub fn stake(&self, epoch: u64) -> u64 {
|
||||
// before "activated" or after deactivated?
|
||||
if epoch < self.activated || epoch >= self.deactivated {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// curr slot | 0 | 1 | 2 ... | 100 | 101 | 102 | 103
|
||||
// action | activate | de-activate | |
|
||||
// | | | | | | | | |
|
||||
// | v | | | v | | |
|
||||
// stake | 1/3 | 2/3 | 3/3 ... | 3/3 | 2/3 | 1/3 | 0/3
|
||||
// -------------------------------------------------------------
|
||||
// activated | 0 ...
|
||||
// deactivated | std::u64::MAX ... 103 ...
|
||||
|
||||
// activate/deactivate can't possibly overlap
|
||||
// (see delegate_stake() and deactivate())
|
||||
if epoch - self.activated < STAKE_WARMUP_EPOCHS {
|
||||
// warmup
|
||||
(self.stake / STAKE_WARMUP_EPOCHS) * (epoch - self.activated + 1)
|
||||
} else if self.deactivated - epoch < STAKE_WARMUP_EPOCHS {
|
||||
// cooldown
|
||||
(self.stake / STAKE_WARMUP_EPOCHS) * (self.deactivated - epoch)
|
||||
} else {
|
||||
self.stake
|
||||
}
|
||||
}
|
||||
|
||||
/// for a given stake and vote_state, calculate what distributions and what updates should be made
|
||||
/// returns a tuple in the case of a payout of:
|
||||
/// * voter_rewards to be distributed
|
||||
/// * staker_rewards to be distributed
|
||||
/// * new value for credits_observed in the stake
|
||||
// returns None if there's no payout or if any deserved payout is < 1 lamport
|
||||
pub fn calculate_rewards(
|
||||
credits_observed: u64,
|
||||
stake: u64,
|
||||
&self,
|
||||
point_value: f64,
|
||||
vote_state: &VoteState,
|
||||
) -> Option<(u64, u64)> {
|
||||
if credits_observed >= vote_state.credits() {
|
||||
) -> Option<(u64, u64, u64)> {
|
||||
if self.credits_observed >= vote_state.credits() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let total_rewards = stake as f64
|
||||
* STAKE_REWARD_TARGET_RATE
|
||||
* (vote_state.credits() - credits_observed) as f64
|
||||
/ CREDITS_PER_YEAR;
|
||||
let mut credits_observed = self.credits_observed;
|
||||
let mut total_rewards = 0f64;
|
||||
for (epoch, credits, prev_credits) in vote_state.epoch_credits() {
|
||||
// figure out how much this stake has seen that
|
||||
// for which the vote account has a record
|
||||
let epoch_credits = if self.credits_observed < *prev_credits {
|
||||
// the staker observed the entire epoch
|
||||
credits - prev_credits
|
||||
} else if self.credits_observed < *credits {
|
||||
// the staker registered sometime during the epoch, partial credit
|
||||
credits - credits_observed
|
||||
} else {
|
||||
// the staker has already observed/redeemed this epoch, or activated
|
||||
// after this epoch
|
||||
0
|
||||
};
|
||||
|
||||
total_rewards += (self.stake(*epoch) * epoch_credits) as f64 * point_value;
|
||||
|
||||
// don't want to assume anything about order of the iterator...
|
||||
credits_observed = std::cmp::max(credits_observed, *credits);
|
||||
}
|
||||
|
||||
// don't bother trying to collect fractional lamports
|
||||
if total_rewards < 1f64 {
|
||||
@ -87,70 +152,34 @@ impl StakeState {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((voter_rewards as u64, staker_rewards as u64))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Stake {
|
||||
pub voter_pubkey: Pubkey,
|
||||
pub credits_observed: u64,
|
||||
pub stake: u64, // activated stake
|
||||
pub epoch: u64, // epoch the stake was activated
|
||||
pub prev_stake: u64, // for warmup, cooldown
|
||||
}
|
||||
pub const STAKE_WARMUP_EPOCHS: u64 = 3;
|
||||
|
||||
impl Stake {
|
||||
pub fn stake(&self, epoch: u64) -> u64 {
|
||||
// prev_stake for stuff in the past
|
||||
if epoch < self.epoch {
|
||||
return self.prev_stake;
|
||||
}
|
||||
if epoch - self.epoch >= STAKE_WARMUP_EPOCHS {
|
||||
return self.stake;
|
||||
}
|
||||
|
||||
if self.stake != 0 {
|
||||
// warmup
|
||||
// 1/3rd, then 2/3rds...
|
||||
(self.stake / STAKE_WARMUP_EPOCHS) * (epoch - self.epoch + 1)
|
||||
} else if self.prev_stake != 0 {
|
||||
// cool down
|
||||
// 3/3rds, then 2/3rds...
|
||||
self.prev_stake - ((self.prev_stake / STAKE_WARMUP_EPOCHS) * (epoch - self.epoch))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Some((
|
||||
voter_rewards as u64,
|
||||
staker_rewards as u64,
|
||||
credits_observed,
|
||||
))
|
||||
}
|
||||
|
||||
fn delegate(
|
||||
&mut self,
|
||||
stake: u64,
|
||||
voter_pubkey: &Pubkey,
|
||||
vote_state: &VoteState,
|
||||
epoch: u64, // current: &syscall::current::Current
|
||||
) {
|
||||
fn delegate(&mut self, stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, epoch: u64) {
|
||||
assert!(std::u64::MAX - epoch >= (STAKE_WARMUP_EPOCHS * 2));
|
||||
|
||||
// resets the current stake's credits
|
||||
self.voter_pubkey = *voter_pubkey;
|
||||
self.credits_observed = vote_state.credits();
|
||||
|
||||
// when this stake was activated
|
||||
self.epoch = epoch;
|
||||
self.activated = epoch;
|
||||
self.stake = stake;
|
||||
}
|
||||
|
||||
fn deactivate(&mut self, epoch: u64) {
|
||||
self.voter_pubkey = Pubkey::default();
|
||||
self.credits_observed = std::u64::MAX;
|
||||
self.prev_stake = self.stake(epoch);
|
||||
self.stake = 0;
|
||||
self.epoch = epoch;
|
||||
self.deactivated = std::cmp::max(
|
||||
epoch + STAKE_WARMUP_EPOCHS,
|
||||
self.activated + 2 * STAKE_WARMUP_EPOCHS - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StakeAccount {
|
||||
fn initialize_mining_pool(&mut self) -> Result<(), InstructionError>;
|
||||
fn delegate_stake(
|
||||
&mut self,
|
||||
vote_account: &KeyedAccount,
|
||||
@ -163,40 +192,13 @@ pub trait StakeAccount {
|
||||
) -> Result<(), InstructionError>;
|
||||
fn redeem_vote_credits(
|
||||
&mut self,
|
||||
stake_account: &mut KeyedAccount,
|
||||
vote_account: &mut KeyedAccount,
|
||||
rewards_account: &mut KeyedAccount,
|
||||
rewards: &syscall::rewards::Rewards,
|
||||
) -> Result<(), InstructionError>;
|
||||
}
|
||||
|
||||
impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
fn initialize_mining_pool(&mut self) -> Result<(), InstructionError> {
|
||||
if let StakeState::Uninitialized = self.state()? {
|
||||
self.set_state(&StakeState::MiningPool {
|
||||
epoch: 0,
|
||||
point_value: 0.0,
|
||||
})
|
||||
} else {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivate_stake(
|
||||
&mut self,
|
||||
current: &syscall::current::Current,
|
||||
) -> Result<(), InstructionError> {
|
||||
if self.signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
if let StakeState::Stake(mut stake) = self.state()? {
|
||||
stake.deactivate(current.epoch);
|
||||
|
||||
self.set_state(&StakeState::Stake(stake))
|
||||
} else {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
fn delegate_stake(
|
||||
&mut self,
|
||||
vote_account: &KeyedAccount,
|
||||
@ -226,14 +228,30 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
fn deactivate_stake(
|
||||
&mut self,
|
||||
current: &syscall::current::Current,
|
||||
) -> Result<(), InstructionError> {
|
||||
if self.signer_key().is_none() {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
if let StakeState::Stake(mut stake) = self.state()? {
|
||||
stake.deactivate(current.epoch);
|
||||
|
||||
self.set_state(&StakeState::Stake(stake))
|
||||
} else {
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
fn redeem_vote_credits(
|
||||
&mut self,
|
||||
stake_account: &mut KeyedAccount,
|
||||
vote_account: &mut KeyedAccount,
|
||||
rewards_account: &mut KeyedAccount,
|
||||
rewards: &syscall::rewards::Rewards,
|
||||
) -> Result<(), InstructionError> {
|
||||
if let (StakeState::MiningPool { .. }, StakeState::Stake(mut stake)) =
|
||||
(self.state()?, stake_account.state()?)
|
||||
if let (StakeState::Stake(mut stake), StakeState::RewardsPool) =
|
||||
(self.state()?, rewards_account.state()?)
|
||||
{
|
||||
let vote_state: VoteState = vote_account.state()?;
|
||||
|
||||
@ -241,25 +259,20 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
if stake.credits_observed > vote_state.credits() {
|
||||
return Err(InstructionError::InvalidAccountData);
|
||||
}
|
||||
|
||||
if let Some((stakers_reward, voters_reward)) = StakeState::calculate_rewards(
|
||||
stake.credits_observed,
|
||||
stake_account.account.lamports,
|
||||
&vote_state,
|
||||
) {
|
||||
if self.account.lamports < (stakers_reward + voters_reward) {
|
||||
if let Some((stakers_reward, voters_reward, credits_observed)) =
|
||||
stake.calculate_rewards(rewards.validator_point_value, &vote_state)
|
||||
{
|
||||
if rewards_account.account.lamports < (stakers_reward + voters_reward) {
|
||||
return Err(InstructionError::UnbalancedInstruction);
|
||||
}
|
||||
self.account.lamports -= stakers_reward + voters_reward;
|
||||
stake_account.account.lamports += stakers_reward;
|
||||
rewards_account.account.lamports -= stakers_reward + voters_reward;
|
||||
|
||||
self.account.lamports += stakers_reward;
|
||||
vote_account.account.lamports += voters_reward;
|
||||
|
||||
stake.credits_observed = vote_state.credits();
|
||||
stake.credits_observed = credits_observed;
|
||||
|
||||
stake_account.set_state(&StakeState::Stake(stake))
|
||||
self.set_state(&StakeState::Stake(stake))
|
||||
} else {
|
||||
// not worth collecting
|
||||
Err(InstructionError::CustomError(1))
|
||||
@ -283,8 +296,8 @@ pub fn create_stake_account(
|
||||
voter_pubkey: *voter_pubkey,
|
||||
credits_observed: vote_state.credits(),
|
||||
stake: lamports,
|
||||
epoch: 0,
|
||||
prev_stake: 0,
|
||||
activated: 0,
|
||||
deactivated: std::u64::MAX,
|
||||
}))
|
||||
.expect("set_state");
|
||||
|
||||
@ -292,14 +305,8 @@ pub fn create_stake_account(
|
||||
}
|
||||
|
||||
// utility function, used by Bank, tests, genesis
|
||||
pub fn create_mining_pool(lamports: u64, epoch: u64, point_value: f64) -> Account {
|
||||
let mut mining_pool_account = Account::new(lamports, std::mem::size_of::<StakeState>(), &id());
|
||||
|
||||
mining_pool_account
|
||||
.set_state(&StakeState::MiningPool { epoch, point_value })
|
||||
.expect("set_state");
|
||||
|
||||
mining_pool_account
|
||||
pub fn create_rewards_pool() -> Account {
|
||||
Account::new_data(std::u64::MAX, &StakeState::RewardsPool, &crate::id()).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -359,8 +366,8 @@ mod tests {
|
||||
voter_pubkey: vote_keypair.pubkey(),
|
||||
credits_observed: vote_state.credits(),
|
||||
stake: stake_lamports,
|
||||
epoch: 0,
|
||||
prev_stake: 0
|
||||
activated: 0,
|
||||
deactivated: std::u64::MAX,
|
||||
})
|
||||
);
|
||||
// verify that delegate_stake can't be called twice StakeState::default()
|
||||
@ -414,190 +421,180 @@ mod tests {
|
||||
#[test]
|
||||
fn test_stake_state_calculate_rewards() {
|
||||
let mut vote_state = VoteState::default();
|
||||
let mut vote_i = 0;
|
||||
let mut stake = Stake::default();
|
||||
|
||||
// put a credit in the vote_state
|
||||
while vote_state.credits() == 0 {
|
||||
vote_state.process_slot_vote_unchecked(vote_i);
|
||||
vote_i += 1;
|
||||
}
|
||||
// this guy can't collect now, not enough stake to get paid on 1 credit
|
||||
assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state));
|
||||
// this guy can
|
||||
// warmup makes this look like zero until WARMUP_EPOCHS
|
||||
stake.stake = 1;
|
||||
|
||||
// this one can't collect now, credits_observed == vote_state.credits()
|
||||
assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state));
|
||||
|
||||
// put 2 credits in at epoch 0
|
||||
vote_state.increment_credits(0);
|
||||
vote_state.increment_credits(0);
|
||||
|
||||
// this one can't collect now, no epoch credits have been saved off
|
||||
assert_eq!(None, stake.calculate_rewards(1_000_000_000.0, &vote_state));
|
||||
|
||||
// put 1 credit in epoch 1, pushes the 2 above into a redeemable state
|
||||
vote_state.increment_credits(1);
|
||||
|
||||
// still can't collect yet, warmup puts the kibosh on it
|
||||
assert_eq!(None, stake.calculate_rewards(1.0, &vote_state));
|
||||
|
||||
stake.stake = STAKE_WARMUP_EPOCHS;
|
||||
// this one should be able to collect exactly 2
|
||||
assert_eq!(
|
||||
Some((0, 1)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
);
|
||||
// but, there's not enough to split
|
||||
vote_state.commission = std::u32::MAX / 2;
|
||||
assert_eq!(
|
||||
None,
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
Some((0, 1 * 2, 2)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
|
||||
// put more credit in the vote_state
|
||||
while vote_state.credits() < 10 {
|
||||
vote_state.process_slot_vote_unchecked(vote_i);
|
||||
vote_i += 1;
|
||||
}
|
||||
vote_state.commission = 0;
|
||||
stake.stake = STAKE_WARMUP_EPOCHS;
|
||||
stake.credits_observed = 1;
|
||||
// this one should be able to collect exactly 1 (only observed one)
|
||||
assert_eq!(
|
||||
Some((0, 10)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
Some((0, 1 * 1, 2)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
vote_state.commission = std::u32::MAX;
|
||||
|
||||
stake.stake = STAKE_WARMUP_EPOCHS;
|
||||
stake.credits_observed = 2;
|
||||
// this one should be able to collect none because credits_observed >= credits in a
|
||||
// redeemable state (the 2 credits in epoch 0)
|
||||
assert_eq!(None, stake.calculate_rewards(1.0, &vote_state));
|
||||
|
||||
// put 1 credit in epoch 2, pushes the 1 for epoch 1 to redeemable
|
||||
vote_state.increment_credits(2);
|
||||
// this one should be able to collect two now, one credit by a stake of 2
|
||||
assert_eq!(
|
||||
Some((10, 0)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
Some((0, 2 * 1, 3)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
vote_state.commission = std::u32::MAX / 2;
|
||||
|
||||
stake.credits_observed = 0;
|
||||
// this one should be able to collect everything from t=0 a warmed up stake of 2
|
||||
// (2 credits at stake of 1) + (1 credit at a stake of 2)
|
||||
assert_eq!(
|
||||
Some((5, 5)),
|
||||
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||
Some((0, 2 * 1 + 1 * 2, 3)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
|
||||
// same as above, but is a really small commission out of 32 bits,
|
||||
// verify that None comes back on small redemptions where no one gets paid
|
||||
vote_state.commission = 1;
|
||||
assert_eq!(
|
||||
None, // would be Some((0, 2 * 1 + 1 * 2, 3)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
vote_state.commission = std::u32::MAX - 1;
|
||||
assert_eq!(
|
||||
None, // would be pSome((0, 2 * 1 + 1 * 2, 3)),
|
||||
stake.calculate_rewards(1.0, &vote_state)
|
||||
);
|
||||
// not even enough stake to get paid on 10 credits...
|
||||
assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stake_redeem_vote_credits() {
|
||||
let current = syscall::current::Current::default();
|
||||
let mut rewards = syscall::rewards::Rewards::default();
|
||||
rewards.validator_point_value = 100.0;
|
||||
|
||||
let vote_keypair = Keypair::new();
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..1000 {
|
||||
vote_state.process_slot_vote_unchecked(i);
|
||||
}
|
||||
|
||||
let vote_pubkey = vote_keypair.pubkey();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
let rewards_pool_pubkey = Pubkey::new_rand();
|
||||
let mut rewards_pool_account = create_rewards_pool();
|
||||
let mut rewards_pool_keyed_account =
|
||||
KeyedAccount::new(&rewards_pool_pubkey, false, &mut rewards_pool_account);
|
||||
|
||||
let pubkey = Pubkey::default();
|
||||
let mut stake_account = Account::new(
|
||||
STAKE_GETS_PAID_EVERY_VOTE,
|
||||
std::mem::size_of::<StakeState>(),
|
||||
&id(),
|
||||
);
|
||||
let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account);
|
||||
|
||||
// delegate the stake
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account, STAKE_GETS_PAID_EVERY_VOTE, ¤t)
|
||||
.is_ok());
|
||||
|
||||
let mut mining_pool_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut mining_pool_keyed_account =
|
||||
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
||||
|
||||
// not a mining pool yet...
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
|
||||
mining_pool_keyed_account
|
||||
.set_state(&StakeState::MiningPool {
|
||||
epoch: 0,
|
||||
point_value: 0.0,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// no movement in vote account, so no redemption needed
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::CustomError(1))
|
||||
);
|
||||
|
||||
// move the vote account forward
|
||||
vote_state.process_slot_vote_unchecked(1000);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
// now, no lamports in the pool!
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
Err(InstructionError::UnbalancedInstruction)
|
||||
);
|
||||
|
||||
// add a lamport to pool
|
||||
mining_pool_keyed_account.account.lamports = 2;
|
||||
assert!(mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account)
|
||||
.is_ok()); // yay
|
||||
|
||||
// lamports only shifted around, none made or lost
|
||||
assert_eq!(
|
||||
2 + 100 + STAKE_GETS_PAID_EVERY_VOTE,
|
||||
mining_pool_account.lamports + vote_account.lamports + stake_account.lamports
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stake_redeem_vote_credits_vote_errors() {
|
||||
let current = syscall::current::Current::default();
|
||||
|
||||
let vote_keypair = Keypair::new();
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..1000 {
|
||||
vote_state.process_slot_vote_unchecked(i);
|
||||
}
|
||||
|
||||
let vote_pubkey = vote_keypair.pubkey();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
|
||||
let pubkey = Pubkey::default();
|
||||
let stake_lamports = 0;
|
||||
let stake_lamports = 100;
|
||||
let mut stake_account =
|
||||
Account::new(stake_lamports, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account);
|
||||
|
||||
let vote_pubkey = Pubkey::new_rand();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
|
||||
// not delegated yet, deserialization fails
|
||||
assert_eq!(
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut vote_keyed_account,
|
||||
&mut rewards_pool_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
|
||||
// delegate the stake
|
||||
assert!(stake_keyed_account
|
||||
.delegate_stake(&vote_keyed_account, stake_lamports, ¤t)
|
||||
.is_ok());
|
||||
|
||||
let mut mining_pool_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
||||
let mut mining_pool_keyed_account =
|
||||
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
||||
mining_pool_keyed_account
|
||||
.set_state(&StakeState::MiningPool {
|
||||
epoch: 0,
|
||||
point_value: 0.0,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut vote_state = VoteState::default();
|
||||
for i in 0..100 {
|
||||
// go back in time, previous state had 1000 votes
|
||||
vote_state.process_slot_vote_unchecked(i);
|
||||
}
|
||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||
// voter credits lower than stake_delegate credits... TODO: is this an error?
|
||||
// no credits to claim
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut vote_keyed_account,
|
||||
&mut rewards_pool_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Err(InstructionError::CustomError(1))
|
||||
);
|
||||
|
||||
// swapped rewards and vote, deserialization of rewards_pool fails
|
||||
assert_eq!(
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut rewards_pool_keyed_account,
|
||||
&mut vote_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Err(InstructionError::InvalidAccountData)
|
||||
);
|
||||
|
||||
let vote1_keypair = Keypair::new();
|
||||
let vote1_pubkey = vote1_keypair.pubkey();
|
||||
let mut vote1_account =
|
||||
vote_state::create_account(&vote1_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
let mut vote1_keyed_account = KeyedAccount::new(&vote1_pubkey, false, &mut vote1_account);
|
||||
vote1_keyed_account.set_state(&vote_state).unwrap();
|
||||
let mut vote_account =
|
||||
vote_state::create_account(&vote_pubkey, &Pubkey::new_rand(), 0, 100);
|
||||
|
||||
let mut vote_state = VoteState::from(&vote_account).unwrap();
|
||||
// put in some credits in epoch 0 for which we should have a non-zero stake
|
||||
for _i in 0..100 {
|
||||
vote_state.increment_credits(0);
|
||||
}
|
||||
vote_state.increment_credits(1);
|
||||
|
||||
vote_state.to(&mut vote_account).unwrap();
|
||||
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
|
||||
|
||||
// some credits to claim, but rewards pool empty (shouldn't ever happen)
|
||||
rewards_pool_keyed_account.account.lamports = 1;
|
||||
assert_eq!(
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut vote_keyed_account,
|
||||
&mut rewards_pool_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Err(InstructionError::UnbalancedInstruction)
|
||||
);
|
||||
rewards_pool_keyed_account.account.lamports = std::u64::MAX;
|
||||
|
||||
// finally! some credits to claim
|
||||
assert_eq!(
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut vote_keyed_account,
|
||||
&mut rewards_pool_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
let wrong_vote_pubkey = Pubkey::new_rand();
|
||||
let mut wrong_vote_keyed_account =
|
||||
KeyedAccount::new(&wrong_vote_pubkey, false, &mut vote_account);
|
||||
|
||||
// wrong voter_pubkey...
|
||||
assert_eq!(
|
||||
mining_pool_keyed_account
|
||||
.redeem_vote_credits(&mut stake_keyed_account, &mut vote1_keyed_account),
|
||||
stake_keyed_account.redeem_vote_credits(
|
||||
&mut wrong_vote_keyed_account,
|
||||
&mut rewards_pool_keyed_account,
|
||||
&rewards
|
||||
),
|
||||
Err(InstructionError::InvalidArgument)
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user