solana/programs/stake_api/src/stake_state.rs

274 lines
9.5 KiB
Rust
Raw Normal View History

//! Stake state
//! * delegate stakes to vote accounts
//! * keep track of rewards
//! * own mining pools
//use crate::{check_id, id};
//use log::*;
use serde_derive::{Deserialize, Serialize};
use solana_sdk::account::KeyedAccount;
use solana_sdk::instruction::InstructionError;
use solana_sdk::instruction_processor_utils::State;
use solana_sdk::pubkey::Pubkey;
use solana_vote_api::vote_state::VoteState;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub enum StakeState {
Delegate {
voter_id: Pubkey,
credits_observed: u64,
},
MiningPool,
}
impl Default for StakeState {
fn default() -> Self {
StakeState::Delegate {
voter_id: Pubkey::default(),
credits_observed: 0,
}
}
}
pub trait StakeAccount {
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError>;
fn redeem_vote_credits(
&mut self,
stake_account: &mut KeyedAccount,
vote_account: &KeyedAccount,
) -> Result<(), InstructionError>;
}
impl<'a> StakeAccount for KeyedAccount<'a> {
fn delegate_stake(&mut self, vote_account: &KeyedAccount) -> Result<(), InstructionError> {
if self.signer_key().is_none() {
return Err(InstructionError::MissingRequiredSignature);
}
if let StakeState::Delegate { .. } = self.state()? {
let vote_state: VoteState = vote_account.state()?;
self.set_state(&StakeState::Delegate {
voter_id: *vote_account.unsigned_key(),
credits_observed: vote_state.credits(),
})
} else {
Err(InstructionError::InvalidAccountData)
}
}
fn redeem_vote_credits(
&mut self,
stake_account: &mut KeyedAccount,
vote_account: &KeyedAccount,
) -> Result<(), InstructionError> {
if let (
StakeState::MiningPool,
StakeState::Delegate {
voter_id,
mut credits_observed,
},
) = (self.state()?, stake_account.state()?)
{
let vote_state: VoteState = vote_account.state()?;
if voter_id != *vote_account.unsigned_key() {
return Err(InstructionError::InvalidArgument);
}
if credits_observed > vote_state.credits() {
return Err(InstructionError::InvalidAccountData);
}
let credits = vote_state.credits() - credits_observed;
credits_observed = vote_state.credits();
if self.account.lamports < credits {
return Err(InstructionError::UnbalancedInstruction);
}
// TODO: commission and network inflation parameter
// mining pool lamports reduced by credits * network_inflation_param
// stake_account and vote_account lamports up by the net
// split by a commission in vote_state
self.account.lamports -= credits;
stake_account.account.lamports += credits;
stake_account.set_state(&StakeState::Delegate {
voter_id,
credits_observed,
})
} else {
Err(InstructionError::InvalidAccountData)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id;
use solana_sdk::account::Account;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_vote_api::vote_instruction::Vote;
use solana_vote_api::vote_state::create_vote_account;
#[test]
fn test_stake_delegate_stake() {
let vote_keypair = Keypair::new();
let mut vote_state = VoteState::default();
for i in 0..1000 {
vote_state.process_vote(Vote::new(i));
}
let vote_pubkey = vote_keypair.pubkey();
let mut vote_account = create_vote_account(100);
let mut vote_keyed_account = KeyedAccount::new(&vote_pubkey, false, &mut vote_account);
vote_keyed_account.set_state(&vote_state).unwrap();
let stake_pubkey = Pubkey::default();
let mut stake_account = Account::new(0, std::mem::size_of::<StakeState>(), &id());
let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &mut stake_account);
assert_eq!(
stake_keyed_account.delegate_stake(&vote_keyed_account),
Err(InstructionError::MissingRequiredSignature)
);
let mut stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &mut stake_account);
assert!(stake_keyed_account
.delegate_stake(&vote_keyed_account)
.is_ok());
let stake_state: StakeState = stake_keyed_account.state().unwrap();
assert_eq!(
stake_state,
StakeState::Delegate {
voter_id: vote_keypair.pubkey(),
credits_observed: vote_state.credits()
}
);
let stake_state = StakeState::MiningPool;
stake_keyed_account.set_state(&stake_state).unwrap();
assert!(stake_keyed_account
.delegate_stake(&vote_keyed_account)
.is_err());
}
#[test]
fn test_stake_redeem_vote_credits() {
let vote_keypair = Keypair::new();
let mut vote_state = VoteState::default();
for i in 0..1000 {
vote_state.process_vote(Vote::new(i));
}
let vote_pubkey = vote_keypair.pubkey();
let mut vote_account = create_vote_account(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 mut stake_account = Account::new(0, 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)
.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);
// no mining pool yet...
assert_eq!(
mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
Err(InstructionError::InvalidAccountData)
);
mining_pool_keyed_account
.set_state(&StakeState::MiningPool)
.unwrap();
// no movement in vote account, so no redemption needed
assert!(mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account)
.is_ok());
// move the vote account forward
vote_state.process_vote(Vote::new(1000));
vote_keyed_account.set_state(&vote_state).unwrap();
// no lamports in the pool
assert_eq!(
mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
Err(InstructionError::UnbalancedInstruction)
);
// add a lamport
mining_pool_keyed_account.account.lamports = 2;
assert!(mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account)
.is_ok());
}
#[test]
fn test_stake_redeem_vote_credits_vote_errors() {
let vote_keypair = Keypair::new();
let mut vote_state = VoteState::default();
for i in 0..1000 {
vote_state.process_vote(Vote::new(i));
}
let vote_pubkey = vote_keypair.pubkey();
let mut vote_account = create_vote_account(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 mut stake_account = Account::new(0, 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)
.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)
.unwrap();
let mut vote_state = VoteState::default();
for i in 0..100 {
// go back in time, previous state had 1000 votes
vote_state.process_vote(Vote::new(i));
}
vote_keyed_account.set_state(&vote_state).unwrap();
// voter credits lower than stake_delegate credits... TODO: is this an error?
assert_eq!(
mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote_keyed_account),
Err(InstructionError::InvalidAccountData)
);
let vote1_keypair = Keypair::new();
let vote1_pubkey = vote1_keypair.pubkey();
let mut vote1_account = create_vote_account(100);
let mut vote1_keyed_account = KeyedAccount::new(&vote1_pubkey, false, &mut vote1_account);
vote1_keyed_account.set_state(&vote_state).unwrap();
// wrong voter_id...
assert_eq!(
mining_pool_keyed_account
.redeem_vote_credits(&mut stake_keyed_account, &vote1_keyed_account),
Err(InstructionError::InvalidArgument)
);
}
}