parent
72b7419e1c
commit
79bf3cf70d
|
@ -91,7 +91,8 @@ pub fn process_instruction(
|
||||||
}
|
}
|
||||||
let (stake, vote) = rest.split_at_mut(1);
|
let (stake, vote) = rest.split_at_mut(1);
|
||||||
let stake = &mut stake[0];
|
let stake = &mut stake[0];
|
||||||
let vote = &vote[0];
|
let vote = &mut vote[0];
|
||||||
|
|
||||||
me.redeem_vote_credits(stake, vote)
|
me.redeem_vote_credits(stake, vote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,13 +29,56 @@ impl Default for StakeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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 move, fix this
|
||||||
|
|
||||||
|
impl StakeState {
|
||||||
|
pub fn calculate_rewards(
|
||||||
|
credits_observed: u64,
|
||||||
|
stake: u64,
|
||||||
|
vote_state: &VoteState,
|
||||||
|
) -> Option<(u64, u64)> {
|
||||||
|
if 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;
|
||||||
|
|
||||||
|
// don't bother trying to collect fractional lamports
|
||||||
|
if total_rewards < 1f64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(total_rewards);
|
||||||
|
|
||||||
|
if (voter_rewards < 1f64 || staker_rewards < 1f64) && is_split {
|
||||||
|
// don't bother trying to collect fractional lamports
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((voter_rewards as u64, staker_rewards as u64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait StakeAccount {
|
pub trait StakeAccount {
|
||||||
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError>;
|
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError>;
|
||||||
fn redeem_vote_credits(
|
fn redeem_vote_credits(
|
||||||
&mut self,
|
&mut self,
|
||||||
stake_account: &mut KeyedAccount,
|
stake_account: &mut KeyedAccount,
|
||||||
vote_account: &KeyedAccount,
|
vote_account: &mut KeyedAccount,
|
||||||
) -> Result<(), InstructionError>;
|
) -> Result<(), InstructionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,13 +102,13 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||||
fn redeem_vote_credits(
|
fn redeem_vote_credits(
|
||||||
&mut self,
|
&mut self,
|
||||||
stake_account: &mut KeyedAccount,
|
stake_account: &mut KeyedAccount,
|
||||||
vote_account: &KeyedAccount,
|
vote_account: &mut KeyedAccount,
|
||||||
) -> Result<(), InstructionError> {
|
) -> Result<(), InstructionError> {
|
||||||
if let (
|
if let (
|
||||||
StakeState::MiningPool,
|
StakeState::MiningPool,
|
||||||
StakeState::Delegate {
|
StakeState::Delegate {
|
||||||
voter_id,
|
voter_id,
|
||||||
mut credits_observed,
|
credits_observed,
|
||||||
},
|
},
|
||||||
) = (self.state()?, stake_account.state()?)
|
) = (self.state()?, stake_account.state()?)
|
||||||
{
|
{
|
||||||
|
@ -79,23 +122,26 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||||
return Err(InstructionError::InvalidAccountData);
|
return Err(InstructionError::InvalidAccountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
let credits = vote_state.credits() - credits_observed;
|
if let Some((stakers_reward, voters_reward)) = StakeState::calculate_rewards(
|
||||||
credits_observed = vote_state.credits();
|
credits_observed,
|
||||||
|
stake_account.account.lamports,
|
||||||
if self.account.lamports < credits {
|
&vote_state,
|
||||||
|
) {
|
||||||
|
if self.account.lamports < (stakers_reward + voters_reward) {
|
||||||
return Err(InstructionError::UnbalancedInstruction);
|
return Err(InstructionError::UnbalancedInstruction);
|
||||||
}
|
}
|
||||||
// TODO: commission and network inflation parameter
|
self.account.lamports -= stakers_reward + voters_reward;
|
||||||
// mining pool lamports reduced by credits * network_inflation_param
|
stake_account.account.lamports += stakers_reward;
|
||||||
// stake_account and vote_account lamports up by the net
|
vote_account.account.lamports += voters_reward;
|
||||||
// split by a commission in vote_state
|
|
||||||
self.account.lamports -= credits;
|
|
||||||
stake_account.account.lamports += credits;
|
|
||||||
|
|
||||||
stake_account.set_state(&StakeState::Delegate {
|
stake_account.set_state(&StakeState::Delegate {
|
||||||
voter_id,
|
voter_id,
|
||||||
credits_observed,
|
credits_observed: vote_state.credits(),
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// not worth collecting
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(InstructionError::InvalidAccountData)
|
Err(InstructionError::InvalidAccountData)
|
||||||
}
|
}
|
||||||
|
@ -153,6 +199,53 @@ mod tests {
|
||||||
.delegate_stake(&vote_keyed_account)
|
.delegate_stake(&vote_keyed_account)
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_stake_state_calculate_rewards() {
|
||||||
|
let mut vote_state = VoteState::default();
|
||||||
|
let mut vote_i = 0;
|
||||||
|
|
||||||
|
// put a credit in the vote_state
|
||||||
|
while vote_state.credits() == 0 {
|
||||||
|
vote_state.process_vote(Vote::new(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
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// put more credit in the vote_state
|
||||||
|
while vote_state.credits() < 10 {
|
||||||
|
vote_state.process_vote(Vote::new(vote_i));
|
||||||
|
vote_i += 1;
|
||||||
|
}
|
||||||
|
vote_state.commission = 0;
|
||||||
|
assert_eq!(
|
||||||
|
Some((0, 10)),
|
||||||
|
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||||
|
);
|
||||||
|
vote_state.commission = std::u32::MAX;
|
||||||
|
assert_eq!(
|
||||||
|
Some((10, 0)),
|
||||||
|
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||||
|
);
|
||||||
|
vote_state.commission = std::u32::MAX / 2;
|
||||||
|
assert_eq!(
|
||||||
|
Some((5, 5)),
|
||||||
|
StakeState::calculate_rewards(0, STAKE_GETS_PAID_EVERY_VOTE, &vote_state)
|
||||||
|
);
|
||||||
|
// not even enough stake to get paid on 10 credits...
|
||||||
|
assert_eq!(None, StakeState::calculate_rewards(0, 100, &vote_state));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stake_redeem_vote_credits() {
|
fn test_stake_redeem_vote_credits() {
|
||||||
|
@ -168,7 +261,11 @@ mod tests {
|
||||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||||
|
|
||||||
let pubkey = Pubkey::default();
|
let pubkey = Pubkey::default();
|
||||||
let mut stake_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
|
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);
|
let mut stake_keyed_account = KeyedAccount::new(&pubkey, true, &mut stake_account);
|
||||||
|
|
||||||
// delegate the stake
|
// delegate the stake
|
||||||
|
@ -180,10 +277,10 @@ mod tests {
|
||||||
let mut mining_pool_keyed_account =
|
let mut mining_pool_keyed_account =
|
||||||
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
KeyedAccount::new(&pubkey, true, &mut mining_pool_account);
|
||||||
|
|
||||||
// no mining pool yet...
|
// not a mining pool yet...
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mining_pool_keyed_account
|
mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||||
Err(InstructionError::InvalidAccountData)
|
Err(InstructionError::InvalidAccountData)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -193,25 +290,31 @@ mod tests {
|
||||||
|
|
||||||
// no movement in vote account, so no redemption needed
|
// no movement in vote account, so no redemption needed
|
||||||
assert!(mining_pool_keyed_account
|
assert!(mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account)
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
|
|
||||||
// move the vote account forward
|
// move the vote account forward
|
||||||
vote_state.process_vote(Vote::new(1000));
|
vote_state.process_vote(Vote::new(1000));
|
||||||
vote_keyed_account.set_state(&vote_state).unwrap();
|
vote_keyed_account.set_state(&vote_state).unwrap();
|
||||||
|
|
||||||
// no lamports in the pool
|
// now, no lamports in the pool!
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mining_pool_keyed_account
|
mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||||
Err(InstructionError::UnbalancedInstruction)
|
Err(InstructionError::UnbalancedInstruction)
|
||||||
);
|
);
|
||||||
|
|
||||||
// add a lamport
|
// add a lamport to pool
|
||||||
mining_pool_keyed_account.account.lamports = 2;
|
mining_pool_keyed_account.account.lamports = 2;
|
||||||
assert!(mining_pool_keyed_account
|
assert!(mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account)
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account)
|
||||||
.is_ok());
|
.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]
|
#[test]
|
||||||
|
@ -252,7 +355,7 @@ mod tests {
|
||||||
// voter credits lower than stake_delegate credits... TODO: is this an error?
|
// voter credits lower than stake_delegate credits... TODO: is this an error?
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mining_pool_keyed_account
|
mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote_keyed_account),
|
||||||
Err(InstructionError::InvalidAccountData)
|
Err(InstructionError::InvalidAccountData)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -265,7 +368,7 @@ mod tests {
|
||||||
// wrong voter_id...
|
// wrong voter_id...
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mining_pool_keyed_account
|
mining_pool_keyed_account
|
||||||
.redeem_vote_credits(&mut stake_keyed_account, &vote1_keyed_account),
|
.redeem_vote_credits(&mut stake_keyed_account, &mut vote1_keyed_account),
|
||||||
Err(InstructionError::InvalidArgument)
|
Err(InstructionError::InvalidArgument)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,9 @@ pub struct VoteState {
|
||||||
pub votes: VecDeque<Lockout>,
|
pub votes: VecDeque<Lockout>,
|
||||||
pub delegate_id: Pubkey,
|
pub delegate_id: Pubkey,
|
||||||
pub authorized_voter_id: Pubkey,
|
pub authorized_voter_id: Pubkey,
|
||||||
|
/// fraction of std::u32::MAX that represents what part of a rewards
|
||||||
|
/// payout should be given to this VoteAccount
|
||||||
|
pub commission: u32,
|
||||||
pub root_slot: Option<u64>,
|
pub root_slot: Option<u64>,
|
||||||
credits: u64,
|
credits: u64,
|
||||||
}
|
}
|
||||||
|
@ -58,11 +61,13 @@ impl VoteState {
|
||||||
let votes = VecDeque::new();
|
let votes = VecDeque::new();
|
||||||
let credits = 0;
|
let credits = 0;
|
||||||
let root_slot = None;
|
let root_slot = None;
|
||||||
|
let commission = 0;
|
||||||
Self {
|
Self {
|
||||||
votes,
|
votes,
|
||||||
delegate_id: *staker_id,
|
delegate_id: *staker_id,
|
||||||
authorized_voter_id: *staker_id,
|
authorized_voter_id: *staker_id,
|
||||||
credits,
|
credits,
|
||||||
|
commission,
|
||||||
root_slot,
|
root_slot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +92,21 @@ impl VoteState {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns commission split as (voter_portion, staker_portion) tuple
|
||||||
|
///
|
||||||
|
/// if commission calculation is 100% one way or other,
|
||||||
|
/// indicate with None for the 0% side
|
||||||
|
pub fn commission_split(&self, on: f64) -> (f64, f64, bool) {
|
||||||
|
match self.commission {
|
||||||
|
0 => (0.0, on, false),
|
||||||
|
std::u32::MAX => (on, 0.0, false),
|
||||||
|
split => {
|
||||||
|
let mine = on * f64::from(split) / f64::from(std::u32::MAX);
|
||||||
|
(mine, on - mine, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_vote(&mut self, vote: Vote) {
|
pub fn process_vote(&mut self, vote: Vote) {
|
||||||
// Ignore votes for slots earlier than we already have votes for
|
// Ignore votes for slots earlier than we already have votes for
|
||||||
if self
|
if self
|
||||||
|
|
Loading…
Reference in New Issue