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:
Rob Walker
2019-06-21 20:43:24 -07:00
committed by GitHub
parent f39e74f0d7
commit a49f5378e2
12 changed files with 570 additions and 553 deletions

View File

@ -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

View File

@ -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, &current(&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(),
),

View File

@ -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, &current)
.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, &current)
.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)
);
}