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:
parent
f39e74f0d7
commit
a49f5378e2
|
@ -145,7 +145,6 @@ pub(crate) mod tests {
|
|||
|
||||
let leader_stake = Stake {
|
||||
stake: BOOTSTRAP_LEADER_LAMPORTS,
|
||||
epoch: 0,
|
||||
..Stake::default()
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
Some((
|
||||
voter_rewards as u64,
|
||||
staker_rewards as u64,
|
||||
credits_observed,
|
||||
))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
fn delegate(&mut self, stake: u64, voter_pubkey: &Pubkey, vote_state: &VoteState, epoch: u64) {
|
||||
assert!(std::u64::MAX - epoch >= (STAKE_WARMUP_EPOCHS * 2));
|
||||
|
||||
fn delegate(
|
||||
&mut self,
|
||||
stake: u64,
|
||||
voter_pubkey: &Pubkey,
|
||||
vote_state: &VoteState,
|
||||
epoch: u64, // current: &syscall::current::Current
|
||||
) {
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use solana_metrics::datapoint_warn;
|
|||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::syscall::slot_hashes;
|
||||
use solana_sdk::syscall;
|
||||
use solana_sdk::system_instruction;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
|
@ -52,15 +52,22 @@ pub fn create_account(
|
|||
fn metas_for_authorized_signer(
|
||||
vote_pubkey: &Pubkey,
|
||||
authorized_voter_pubkey: &Pubkey, // currently authorized
|
||||
other_params: &[AccountMeta],
|
||||
) -> Vec<AccountMeta> {
|
||||
let is_own_signer = authorized_voter_pubkey == vote_pubkey;
|
||||
|
||||
// vote account
|
||||
let mut account_metas = vec![AccountMeta::new(*vote_pubkey, is_own_signer)];
|
||||
|
||||
for meta in other_params {
|
||||
account_metas.push(meta.clone());
|
||||
}
|
||||
|
||||
// append signer at the end
|
||||
if !is_own_signer {
|
||||
account_metas.push(AccountMeta::new(*authorized_voter_pubkey, true)) // signer
|
||||
}
|
||||
|
||||
account_metas
|
||||
}
|
||||
|
||||
|
@ -69,7 +76,7 @@ pub fn authorize_voter(
|
|||
authorized_voter_pubkey: &Pubkey, // currently authorized
|
||||
new_authorized_voter_pubkey: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey);
|
||||
let account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey, &[]);
|
||||
|
||||
Instruction::new(
|
||||
id(),
|
||||
|
@ -83,10 +90,16 @@ pub fn vote(
|
|||
authorized_voter_pubkey: &Pubkey,
|
||||
recent_votes: Vec<Vote>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = metas_for_authorized_signer(vote_pubkey, authorized_voter_pubkey);
|
||||
|
||||
let account_metas = metas_for_authorized_signer(
|
||||
vote_pubkey,
|
||||
authorized_voter_pubkey,
|
||||
&[
|
||||
// request slot_hashes syscall account after vote_pubkey
|
||||
account_metas.insert(1, AccountMeta::new(slot_hashes::id(), false));
|
||||
AccountMeta::new(syscall::slot_hashes::id(), false),
|
||||
// request current syscall account after that
|
||||
AccountMeta::new(syscall::current::id(), false),
|
||||
],
|
||||
);
|
||||
|
||||
Instruction::new(id(), &VoteInstruction::Vote(recent_votes), account_metas)
|
||||
}
|
||||
|
@ -119,9 +132,18 @@ pub fn process_instruction(
|
|||
}
|
||||
VoteInstruction::Vote(votes) => {
|
||||
datapoint_warn!("vote-native", ("count", 1, i64));
|
||||
let (slot_hashes, other_signers) = rest.split_at_mut(1);
|
||||
let slot_hashes = &mut slot_hashes[0];
|
||||
vote_state::process_votes(me, slot_hashes, other_signers, &votes)
|
||||
if rest.len() < 2 {
|
||||
Err(InstructionError::InvalidInstructionData)?;
|
||||
}
|
||||
let (slot_hashes_and_current, other_signers) = rest.split_at_mut(2);
|
||||
|
||||
vote_state::process_votes(
|
||||
me,
|
||||
&syscall::slot_hashes::from_keyed_account(&slot_hashes_and_current[0])?,
|
||||
&syscall::current::from_keyed_account(&slot_hashes_and_current[1])?,
|
||||
other_signers,
|
||||
&votes,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +163,20 @@ mod tests {
|
|||
}
|
||||
|
||||
fn process_instruction(instruction: &Instruction) -> Result<(), InstructionError> {
|
||||
let mut accounts = vec![];
|
||||
let mut accounts: Vec<_> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|meta| {
|
||||
if syscall::current::check_id(&meta.pubkey) {
|
||||
syscall::current::create_account(1, 0, 0, 0)
|
||||
} else if syscall::slot_hashes::check_id(&meta.pubkey) {
|
||||
syscall::slot_hashes::create_account(1, &[])
|
||||
} else {
|
||||
Account::default()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for _ in 0..instruction.accounts.len() {
|
||||
accounts.push(Account::default());
|
||||
}
|
||||
|
|
|
@ -9,30 +9,35 @@ use solana_sdk::account_utils::State;
|
|||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::instruction::InstructionError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::syscall::slot_hashes;
|
||||
use solana_sdk::syscall::current::Current;
|
||||
pub use solana_sdk::timing::{Epoch, Slot};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// Maximum number of votes to keep around
|
||||
pub const MAX_LOCKOUT_HISTORY: usize = 31;
|
||||
pub const INITIAL_LOCKOUT: usize = 2;
|
||||
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
// Maximum number of credits history to keep around
|
||||
// smaller numbers makes
|
||||
pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
|
||||
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub struct Vote {
|
||||
/// A vote for height slot
|
||||
pub slot: u64,
|
||||
pub slot: Slot,
|
||||
// signature of the bank's state at given slot
|
||||
pub hash: Hash,
|
||||
}
|
||||
|
||||
impl Vote {
|
||||
pub fn new(slot: u64, hash: Hash) -> Self {
|
||||
pub fn new(slot: Slot, hash: Hash) -> Self {
|
||||
Self { slot, hash }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Lockout {
|
||||
pub slot: u64,
|
||||
pub slot: Slot,
|
||||
pub confirmation_count: u32,
|
||||
}
|
||||
|
||||
|
@ -51,10 +56,10 @@ impl Lockout {
|
|||
|
||||
// The slot height at which this vote expires (cannot vote for any slot
|
||||
// less than this)
|
||||
pub fn expiration_slot(&self) -> u64 {
|
||||
pub fn expiration_slot(&self) -> Slot {
|
||||
self.slot + self.lockout()
|
||||
}
|
||||
pub fn is_expired(&self, slot: u64) -> bool {
|
||||
pub fn is_expired(&self, slot: Slot) -> bool {
|
||||
self.expiration_slot() < slot
|
||||
}
|
||||
}
|
||||
|
@ -68,21 +73,27 @@ pub struct VoteState {
|
|||
/// payout should be given to this VoteAccount
|
||||
pub commission: u32,
|
||||
pub root_slot: Option<u64>,
|
||||
|
||||
/// current epoch
|
||||
epoch: Epoch,
|
||||
/// current credits earned, monotonically increasing
|
||||
credits: u64,
|
||||
|
||||
/// credits as of previous epoch
|
||||
last_epoch_credits: u64,
|
||||
|
||||
/// history of how many credits earned by the end of each epoch
|
||||
/// each tuple is (Epoch, credits, prev_credits)
|
||||
epoch_credits: Vec<(Epoch, u64, u64)>,
|
||||
}
|
||||
|
||||
impl VoteState {
|
||||
pub fn new(vote_pubkey: &Pubkey, node_pubkey: &Pubkey, commission: u32) -> Self {
|
||||
let votes = VecDeque::new();
|
||||
let credits = 0;
|
||||
let root_slot = None;
|
||||
Self {
|
||||
votes,
|
||||
node_pubkey: *node_pubkey,
|
||||
authorized_voter_pubkey: *vote_pubkey,
|
||||
credits,
|
||||
commission,
|
||||
root_slot,
|
||||
..VoteState::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +103,7 @@ impl VoteState {
|
|||
let mut vote_state = Self::default();
|
||||
vote_state.votes = VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]);
|
||||
vote_state.root_slot = Some(std::u64::MAX);
|
||||
vote_state.epoch_credits = vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY];
|
||||
serialized_size(&vote_state).unwrap() as usize
|
||||
}
|
||||
|
||||
|
@ -136,11 +148,13 @@ impl VoteState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn process_votes(&mut self, votes: &[Vote], slot_hashes: &[(u64, Hash)]) {
|
||||
votes.iter().for_each(|v| self.process_vote(v, slot_hashes));
|
||||
pub fn process_votes(&mut self, votes: &[Vote], slot_hashes: &[(Slot, Hash)], epoch: Epoch) {
|
||||
votes
|
||||
.iter()
|
||||
.for_each(|v| self.process_vote(v, slot_hashes, epoch));
|
||||
}
|
||||
|
||||
pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(u64, Hash)]) {
|
||||
pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(Slot, Hash)], epoch: Epoch) {
|
||||
// Ignore votes for slots earlier than we already have votes for
|
||||
if self
|
||||
.votes
|
||||
|
@ -176,24 +190,45 @@ impl VoteState {
|
|||
|
||||
let vote = Lockout::new(&vote);
|
||||
|
||||
// TODO: Integrity checks
|
||||
// Verify the vote's bank hash matches what is expected
|
||||
|
||||
self.pop_expired_votes(vote.slot);
|
||||
|
||||
// Once the stack is full, pop the oldest vote and distribute rewards
|
||||
if self.votes.len() == MAX_LOCKOUT_HISTORY {
|
||||
let vote = self.votes.pop_front().unwrap();
|
||||
self.root_slot = Some(vote.slot);
|
||||
self.credits += 1;
|
||||
|
||||
self.increment_credits(epoch);
|
||||
}
|
||||
self.votes.push_back(vote);
|
||||
self.double_lockouts();
|
||||
}
|
||||
|
||||
pub fn process_vote_unchecked(&mut self, vote: &Vote) {
|
||||
self.process_vote(vote, &[(vote.slot, vote.hash)]);
|
||||
/// increment credits, record credits for last epoch if new epoch
|
||||
pub fn increment_credits(&mut self, epoch: Epoch) {
|
||||
// record credits by epoch
|
||||
|
||||
if epoch != self.epoch {
|
||||
// encode the delta, but be able to return partial for stakers who
|
||||
// attach halfway through an epoch
|
||||
self.epoch_credits
|
||||
.push((self.epoch, self.credits, self.last_epoch_credits));
|
||||
// if stakers do not claim before the epoch goes away they lose the
|
||||
// credits...
|
||||
if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
|
||||
self.epoch_credits.remove(0);
|
||||
}
|
||||
pub fn process_slot_vote_unchecked(&mut self, slot: u64) {
|
||||
self.epoch = epoch;
|
||||
self.last_epoch_credits = self.credits;
|
||||
}
|
||||
|
||||
self.credits += 1;
|
||||
}
|
||||
|
||||
/// "uncheckeds" used by tests, locktower
|
||||
pub fn process_vote_unchecked(&mut self, vote: &Vote) {
|
||||
self.process_vote(vote, &[(vote.slot, vote.hash)], self.epoch);
|
||||
}
|
||||
pub fn process_slot_vote_unchecked(&mut self, slot: Slot) {
|
||||
self.process_vote_unchecked(&Vote::new(slot, Hash::default()));
|
||||
}
|
||||
|
||||
|
@ -212,6 +247,13 @@ impl VoteState {
|
|||
self.credits
|
||||
}
|
||||
|
||||
/// Number of "credits" owed to this account from the mining pool on a per-epoch basis,
|
||||
/// starting from credits observed.
|
||||
/// Each tuple of (Epoch, u64) is the credits() delta as of the end of the Epoch
|
||||
pub fn epoch_credits(&self) -> impl Iterator<Item = &(Epoch, u64, u64)> {
|
||||
self.epoch_credits.iter()
|
||||
}
|
||||
|
||||
fn pop_expired_votes(&mut self, slot: u64) {
|
||||
loop {
|
||||
if self.votes.back().map_or(false, |v| v.is_expired(slot)) {
|
||||
|
@ -280,7 +322,8 @@ pub fn initialize_account(
|
|||
|
||||
pub fn process_votes(
|
||||
vote_account: &mut KeyedAccount,
|
||||
slot_hashes_account: &mut KeyedAccount,
|
||||
slot_hashes: &[(Slot, Hash)],
|
||||
current: &Current,
|
||||
other_signers: &[KeyedAccount],
|
||||
votes: &[Vote],
|
||||
) -> Result<(), InstructionError> {
|
||||
|
@ -290,12 +333,6 @@ pub fn process_votes(
|
|||
return Err(InstructionError::UninitializedAccount);
|
||||
}
|
||||
|
||||
if !slot_hashes::check_id(slot_hashes_account.unsigned_key()) {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
let slot_hashes: Vec<(u64, Hash)> = slot_hashes_account.state()?;
|
||||
|
||||
let authorized = Some(&vote_state.authorized_voter_pubkey);
|
||||
// find a signer that matches the authorized_voter_pubkey
|
||||
if vote_account.signer_key() != authorized
|
||||
|
@ -306,7 +343,7 @@ pub fn process_votes(
|
|||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
vote_state.process_votes(&votes, &slot_hashes);
|
||||
vote_state.process_votes(&votes, slot_hashes, current.epoch);
|
||||
vote_account.set_state(&vote_state)
|
||||
}
|
||||
|
||||
|
@ -319,12 +356,10 @@ pub fn create_account(
|
|||
) -> Account {
|
||||
let mut vote_account = Account::new(lamports, VoteState::size_of(), &id());
|
||||
|
||||
initialize_account(
|
||||
&mut KeyedAccount::new(vote_pubkey, false, &mut vote_account),
|
||||
node_pubkey,
|
||||
commission,
|
||||
)
|
||||
VoteState::new(vote_pubkey, node_pubkey, commission)
|
||||
.to(&mut vote_account)
|
||||
.unwrap();
|
||||
|
||||
vote_account
|
||||
}
|
||||
|
||||
|
@ -351,12 +386,9 @@ pub fn create_bootstrap_leader_account(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::vote_state;
|
||||
use bincode::serialized_size;
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::account_utils::State;
|
||||
use solana_sdk::hash::hash;
|
||||
use solana_sdk::syscall;
|
||||
use solana_sdk::syscall::slot_hashes;
|
||||
|
||||
const MAX_RECENT_VOTES: usize = 16;
|
||||
|
||||
|
@ -385,30 +417,20 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
fn create_test_slot_hashes_account(slot_hashes: &[(u64, Hash)]) -> (Pubkey, Account) {
|
||||
let mut slot_hashes_account = Account::new(
|
||||
0,
|
||||
serialized_size(&slot_hashes).unwrap() as usize,
|
||||
&syscall::id(),
|
||||
);
|
||||
slot_hashes_account
|
||||
.set_state(&slot_hashes.to_vec())
|
||||
.unwrap();
|
||||
(slot_hashes::id(), slot_hashes_account)
|
||||
}
|
||||
|
||||
fn simulate_process_vote(
|
||||
vote_pubkey: &Pubkey,
|
||||
vote_account: &mut Account,
|
||||
vote: &Vote,
|
||||
slot_hashes: &[(u64, Hash)],
|
||||
epoch: u64,
|
||||
) -> Result<VoteState, InstructionError> {
|
||||
let (slot_hashes_id, mut slot_hashes_account) =
|
||||
create_test_slot_hashes_account(slot_hashes);
|
||||
|
||||
process_votes(
|
||||
&mut KeyedAccount::new(vote_pubkey, true, vote_account),
|
||||
&mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account),
|
||||
slot_hashes,
|
||||
&Current {
|
||||
epoch,
|
||||
..Current::default()
|
||||
},
|
||||
&[],
|
||||
&[vote.clone()],
|
||||
)?;
|
||||
|
@ -421,7 +443,13 @@ mod tests {
|
|||
vote_account: &mut Account,
|
||||
vote: &Vote,
|
||||
) -> Result<VoteState, InstructionError> {
|
||||
simulate_process_vote(vote_pubkey, vote_account, vote, &[(vote.slot, vote.hash)])
|
||||
simulate_process_vote(
|
||||
vote_pubkey,
|
||||
vote_account,
|
||||
vote,
|
||||
&[(vote.slot, vote.hash)],
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -479,58 +507,45 @@ mod tests {
|
|||
&mut vote_account,
|
||||
&vote,
|
||||
&[(0, Hash::default())],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(vote_state.votes.len(), 0);
|
||||
|
||||
// wrong slot
|
||||
let vote_state =
|
||||
simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[(1, hash)]).unwrap();
|
||||
simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[(1, hash)], 0).unwrap();
|
||||
assert_eq!(vote_state.votes.len(), 0);
|
||||
|
||||
// empty slot_hashes
|
||||
let vote_state =
|
||||
simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[]).unwrap();
|
||||
simulate_process_vote(&vote_pubkey, &mut vote_account, &vote, &[], 0).unwrap();
|
||||
assert_eq!(vote_state.votes.len(), 0);
|
||||
|
||||
// this one would work, but the wrong account is passed for slot_hashes_id
|
||||
let (_slot_hashes_id, mut slot_hashes_account) =
|
||||
create_test_slot_hashes_account(&[(vote.slot, vote.hash)]);
|
||||
assert_eq!(
|
||||
process_votes(
|
||||
&mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account),
|
||||
&mut KeyedAccount::new(&Pubkey::default(), false, &mut slot_hashes_account),
|
||||
&[],
|
||||
&[vote.clone()],
|
||||
),
|
||||
Err(InstructionError::InvalidArgument)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_signature() {
|
||||
let (vote_pubkey, mut vote_account) = create_test_account();
|
||||
|
||||
let vote = vec![Vote::new(1, Hash::default())];
|
||||
|
||||
let (slot_hashes_id, mut slot_hashes_account) =
|
||||
create_test_slot_hashes_account(&[(1, Hash::default())]);
|
||||
let vote = Vote::new(1, Hash::default());
|
||||
|
||||
// unsigned
|
||||
let res = process_votes(
|
||||
&mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account),
|
||||
&mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account),
|
||||
&[(vote.slot, vote.hash)],
|
||||
&Current::default(),
|
||||
&[],
|
||||
&vote,
|
||||
&[vote],
|
||||
);
|
||||
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
|
||||
|
||||
// unsigned
|
||||
let res = process_votes(
|
||||
&mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account),
|
||||
&mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account),
|
||||
&[(vote.slot, vote.hash)],
|
||||
&Current::default(),
|
||||
&[],
|
||||
&vote,
|
||||
&[vote],
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
|
||||
|
@ -562,28 +577,28 @@ mod tests {
|
|||
assert_eq!(res, Ok(()));
|
||||
|
||||
// not signed by authorized voter
|
||||
let vote = vec![Vote::new(2, Hash::default())];
|
||||
let (slot_hashes_id, mut slot_hashes_account) =
|
||||
create_test_slot_hashes_account(&[(2, Hash::default())]);
|
||||
let vote = Vote::new(2, Hash::default());
|
||||
let res = process_votes(
|
||||
&mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account),
|
||||
&mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account),
|
||||
&[(vote.slot, vote.hash)],
|
||||
&Current::default(),
|
||||
&[],
|
||||
&vote,
|
||||
&[vote],
|
||||
);
|
||||
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
|
||||
|
||||
// signed by authorized voter
|
||||
let vote = vec![Vote::new(2, Hash::default())];
|
||||
let vote = Vote::new(2, Hash::default());
|
||||
let res = process_votes(
|
||||
&mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account),
|
||||
&mut KeyedAccount::new(&slot_hashes_id, false, &mut slot_hashes_account),
|
||||
&[(vote.slot, vote.hash)],
|
||||
&Current::default(),
|
||||
&[KeyedAccount::new(
|
||||
&authorized_voter_pubkey,
|
||||
true,
|
||||
&mut Account::default(),
|
||||
)],
|
||||
&vote,
|
||||
&[vote],
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
}
|
||||
|
@ -773,8 +788,8 @@ mod tests {
|
|||
.collect();
|
||||
let slot_hashes: Vec<_> = votes.iter().map(|vote| (vote.slot, vote.hash)).collect();
|
||||
|
||||
vote_state_a.process_votes(&votes, &slot_hashes);
|
||||
vote_state_b.process_votes(&votes, &slot_hashes);
|
||||
vote_state_a.process_votes(&votes, &slot_hashes, 0);
|
||||
vote_state_b.process_votes(&votes, &slot_hashes, 0);
|
||||
assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
|
||||
}
|
||||
|
||||
|
@ -796,4 +811,42 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_state_epoch_credits() {
|
||||
let mut vote_state = VoteState::default();
|
||||
|
||||
assert_eq!(vote_state.credits(), 0);
|
||||
assert_eq!(
|
||||
vote_state
|
||||
.epoch_credits()
|
||||
.cloned()
|
||||
.collect::<Vec<(Epoch, u64, u64)>>(),
|
||||
vec![]
|
||||
);
|
||||
|
||||
let mut expected = vec![];
|
||||
let mut credits = 0;
|
||||
let epochs = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64;
|
||||
for epoch in 0..epochs {
|
||||
for _j in 0..epoch {
|
||||
vote_state.increment_credits(epoch);
|
||||
credits += 1;
|
||||
}
|
||||
expected.push((epoch, credits, credits - epoch));
|
||||
}
|
||||
expected.pop(); // last one doesn't count, doesn't get saved off
|
||||
while expected.len() > MAX_EPOCH_CREDITS_HISTORY {
|
||||
expected.remove(0);
|
||||
}
|
||||
|
||||
assert_eq!(vote_state.credits(), credits);
|
||||
assert_eq!(
|
||||
vote_state
|
||||
.epoch_credits()
|
||||
.cloned()
|
||||
.collect::<Vec<(Epoch, u64, u64)>>(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -417,7 +417,7 @@ impl Bank {
|
|||
fn update_slot_hashes(&self) {
|
||||
let mut account = self
|
||||
.get_account(&slot_hashes::id())
|
||||
.unwrap_or_else(|| slot_hashes::create_account(1));
|
||||
.unwrap_or_else(|| slot_hashes::create_account(1, &[]));
|
||||
|
||||
let mut slot_hashes = SlotHashes::from(&account).unwrap();
|
||||
slot_hashes.add(self.slot(), self.hash());
|
||||
|
@ -546,6 +546,10 @@ impl Bank {
|
|||
self.capitalization
|
||||
.fetch_add(account.lamports as usize, Ordering::Relaxed);
|
||||
}
|
||||
for (pubkey, account) in genesis_block.rewards_pools.iter() {
|
||||
self.store_account(pubkey, account);
|
||||
}
|
||||
|
||||
// highest staked node is the first collector
|
||||
self.collector_id = self
|
||||
.stakes
|
||||
|
|
|
@ -4,6 +4,8 @@ use crate::account::Account;
|
|||
use crate::syscall;
|
||||
use bincode::serialized_size;
|
||||
|
||||
pub use crate::timing::{Epoch, Slot};
|
||||
|
||||
crate::solana_name_id!(ID, "Sysca11Current11111111111111111111111111111");
|
||||
|
||||
const ID: [u8; 32] = [
|
||||
|
@ -14,9 +16,9 @@ const ID: [u8; 32] = [
|
|||
#[repr(C)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
|
||||
pub struct Current {
|
||||
pub slot: u64,
|
||||
pub epoch: u64,
|
||||
pub stakers_epoch: u64,
|
||||
pub slot: Slot,
|
||||
pub epoch: Epoch,
|
||||
pub stakers_epoch: Epoch,
|
||||
}
|
||||
|
||||
impl Current {
|
||||
|
@ -32,7 +34,7 @@ impl Current {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_account(lamports: u64, slot: u64, epoch: u64, stakers_epoch: u64) -> Account {
|
||||
pub fn create_account(lamports: u64, slot: Slot, epoch: Epoch, stakers_epoch: Epoch) -> Account {
|
||||
Account::new_data(
|
||||
lamports,
|
||||
&Current {
|
||||
|
@ -45,6 +47,15 @@ pub fn create_account(lamports: u64, slot: u64, epoch: u64, stakers_epoch: u64)
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
use crate::account::KeyedAccount;
|
||||
use crate::instruction::InstructionError;
|
||||
pub fn from_keyed_account(account: &KeyedAccount) -> Result<Current, InstructionError> {
|
||||
if !check_id(account.unsigned_key()) {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
Current::from(account.account).ok_or(InstructionError::InvalidArgument)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -47,6 +47,16 @@ pub fn create_account(
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
use crate::account::KeyedAccount;
|
||||
use crate::instruction::InstructionError;
|
||||
pub fn from_keyed_account(account: &KeyedAccount) -> Result<Rewards, InstructionError> {
|
||||
if !check_id(account.unsigned_key()) {
|
||||
dbg!(account.unsigned_key());
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
Rewards::from(account.account).ok_or(InstructionError::InvalidAccountData)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -9,6 +9,8 @@ use crate::syscall;
|
|||
use bincode::serialized_size;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub use crate::timing::Slot;
|
||||
|
||||
/// "Sysca11SlotHashes11111111111111111111111111"
|
||||
/// slot hashes account pubkey
|
||||
const ID: [u8; 32] = [
|
||||
|
@ -29,7 +31,7 @@ pub const MAX_SLOT_HASHES: usize = 512; // 512 slots to get your vote in
|
|||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
pub struct SlotHashes {
|
||||
// non-pub to keep control of size
|
||||
inner: Vec<(u64, Hash)>,
|
||||
inner: Vec<(Slot, Hash)>,
|
||||
}
|
||||
|
||||
impl SlotHashes {
|
||||
|
@ -46,10 +48,15 @@ impl SlotHashes {
|
|||
})
|
||||
.unwrap() as usize
|
||||
}
|
||||
pub fn add(&mut self, slot: u64, hash: Hash) {
|
||||
pub fn add(&mut self, slot: Slot, hash: Hash) {
|
||||
self.inner.insert(0, (slot, hash));
|
||||
self.inner.truncate(MAX_SLOT_HASHES);
|
||||
}
|
||||
pub fn new(slot_hashes: &[(Slot, Hash)]) -> Self {
|
||||
Self {
|
||||
inner: slot_hashes.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SlotHashes {
|
||||
|
@ -59,8 +66,19 @@ impl Deref for SlotHashes {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_account(lamports: u64) -> Account {
|
||||
Account::new(lamports, SlotHashes::size_of(), &syscall::id())
|
||||
pub fn create_account(lamports: u64, slot_hashes: &[(Slot, Hash)]) -> Account {
|
||||
let mut account = Account::new(lamports, SlotHashes::size_of(), &syscall::id());
|
||||
SlotHashes::new(slot_hashes).to(&mut account).unwrap();
|
||||
account
|
||||
}
|
||||
|
||||
use crate::account::KeyedAccount;
|
||||
use crate::instruction::InstructionError;
|
||||
pub fn from_keyed_account(account: &KeyedAccount) -> Result<SlotHashes, InstructionError> {
|
||||
if !check_id(account.unsigned_key()) {
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
SlotHashes::from(account.account).ok_or(InstructionError::InvalidArgument)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -71,7 +89,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_create_account() {
|
||||
let lamports = 42;
|
||||
let account = create_account(lamports);
|
||||
let account = create_account(lamports, &[]);
|
||||
assert_eq!(account.data.len(), SlotHashes::size_of());
|
||||
let slot_hashes = SlotHashes::from(&account);
|
||||
assert_eq!(slot_hashes, Some(SlotHashes { inner: vec![] }));
|
||||
let mut slot_hashes = slot_hashes.unwrap();
|
||||
|
|
|
@ -57,3 +57,11 @@ pub fn timestamp() -> u64 {
|
|||
.expect("create timestamp in timing");
|
||||
duration_as_ms(&now)
|
||||
}
|
||||
|
||||
/// Slot is a unit of time given to a leader for encoding,
|
||||
/// is some some number of Ticks long. Use a u64 to count them.
|
||||
pub type Slot = u64;
|
||||
|
||||
/// Epoch is a unit of time a given leader schedule is honored,
|
||||
/// some number of Slots. Use a u64 to count them.
|
||||
pub type Epoch = u64;
|
||||
|
|
|
@ -51,9 +51,8 @@ pub enum WalletCommand {
|
|||
CreateVoteAccount(Pubkey, Pubkey, u32, u64),
|
||||
ShowVoteAccount(Pubkey),
|
||||
CreateStakeAccount(Pubkey, u64),
|
||||
CreateMiningPoolAccount(Pubkey, u64),
|
||||
DelegateStake(Keypair, Pubkey, u64),
|
||||
RedeemVoteCredits(Pubkey, Pubkey, Pubkey),
|
||||
RedeemVoteCredits(Pubkey, Pubkey),
|
||||
ShowStakeAccount(Pubkey),
|
||||
CreateStorageMiningPoolAccount(Pubkey, u64),
|
||||
CreateReplicatorStorageAccount(Pubkey, Pubkey),
|
||||
|
@ -238,15 +237,6 @@ pub fn parse_command(
|
|||
lamports,
|
||||
))
|
||||
}
|
||||
("create-mining-pool-account", Some(matches)) => {
|
||||
let mining_pool_account_pubkey =
|
||||
value_of(matches, "mining_pool_account_pubkey").unwrap();
|
||||
let lamports = matches.value_of("lamports").unwrap().parse()?;
|
||||
Ok(WalletCommand::CreateMiningPoolAccount(
|
||||
mining_pool_account_pubkey,
|
||||
lamports,
|
||||
))
|
||||
}
|
||||
("delegate-stake", Some(matches)) => {
|
||||
let staking_account_keypair =
|
||||
keypair_of(matches, "staking_account_keypair_file").unwrap();
|
||||
|
@ -259,12 +249,9 @@ pub fn parse_command(
|
|||
))
|
||||
}
|
||||
("redeem-vote-credits", Some(matches)) => {
|
||||
let mining_pool_account_pubkey =
|
||||
value_of(matches, "mining_pool_account_pubkey").unwrap();
|
||||
let staking_account_pubkey = value_of(matches, "staking_account_pubkey").unwrap();
|
||||
let voting_account_pubkey = value_of(matches, "voting_account_pubkey").unwrap();
|
||||
Ok(WalletCommand::RedeemVoteCredits(
|
||||
mining_pool_account_pubkey,
|
||||
staking_account_pubkey,
|
||||
voting_account_pubkey,
|
||||
))
|
||||
|
@ -273,15 +260,6 @@ pub fn parse_command(
|
|||
let staking_account_pubkey = value_of(matches, "staking_account_pubkey").unwrap();
|
||||
Ok(WalletCommand::ShowStakeAccount(staking_account_pubkey))
|
||||
}
|
||||
("create-storage-mining-pool-account", Some(matches)) => {
|
||||
let storage_mining_pool_account_pubkey =
|
||||
value_of(matches, "storage_mining_pool_account_pubkey").unwrap();
|
||||
let lamports = matches.value_of("lamports").unwrap().parse()?;
|
||||
Ok(WalletCommand::CreateStorageMiningPoolAccount(
|
||||
storage_mining_pool_account_pubkey,
|
||||
lamports,
|
||||
))
|
||||
}
|
||||
("create-replicator-storage-account", Some(matches)) => {
|
||||
let account_owner = value_of(matches, "storage_account_owner").unwrap();
|
||||
let storage_account_pubkey = value_of(matches, "storage_account_pubkey").unwrap();
|
||||
|
@ -580,28 +558,6 @@ fn process_create_stake_account(
|
|||
Ok(signature_str.to_string())
|
||||
}
|
||||
|
||||
fn process_create_mining_pool_account(
|
||||
rpc_client: &RpcClient,
|
||||
config: &WalletConfig,
|
||||
mining_pool_account_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> ProcessResult {
|
||||
let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
let ixs = stake_instruction::create_mining_pool_account(
|
||||
&config.keypair.pubkey(),
|
||||
mining_pool_account_pubkey,
|
||||
lamports,
|
||||
);
|
||||
let mut tx = Transaction::new_signed_with_payer(
|
||||
ixs,
|
||||
Some(&config.keypair.pubkey()),
|
||||
&[&config.keypair],
|
||||
recent_blockhash,
|
||||
);
|
||||
let signature_str = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair])?;
|
||||
Ok(signature_str.to_string())
|
||||
}
|
||||
|
||||
fn process_delegate_stake(
|
||||
rpc_client: &RpcClient,
|
||||
config: &WalletConfig,
|
||||
|
@ -631,13 +587,11 @@ fn process_delegate_stake(
|
|||
fn process_redeem_vote_credits(
|
||||
rpc_client: &RpcClient,
|
||||
config: &WalletConfig,
|
||||
mining_pool_account_pubkey: &Pubkey,
|
||||
staking_account_pubkey: &Pubkey,
|
||||
voting_account_pubkey: &Pubkey,
|
||||
) -> ProcessResult {
|
||||
let (recent_blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?;
|
||||
let ixs = vec![stake_instruction::redeem_vote_credits(
|
||||
mining_pool_account_pubkey,
|
||||
staking_account_pubkey,
|
||||
voting_account_pubkey,
|
||||
)];
|
||||
|
@ -663,15 +617,7 @@ fn process_show_stake_account(
|
|||
println!("account lamports: {}", stake_account.lamports);
|
||||
println!("voter pubkey: {}", stake.voter_pubkey);
|
||||
println!("credits observed: {}", stake.credits_observed);
|
||||
println!("epoch: {}", stake.epoch);
|
||||
println!("activated stake: {}", stake.stake);
|
||||
println!("previous stake: {}", stake.prev_stake);
|
||||
Ok("".to_string())
|
||||
}
|
||||
Ok(StakeState::MiningPool { epoch, point_value }) => {
|
||||
println!("account lamports: {}", stake_account.lamports);
|
||||
println!("epoch: {}", epoch);
|
||||
println!("point_value: {}", point_value);
|
||||
println!("stake: {}", stake.stake);
|
||||
Ok("".to_string())
|
||||
}
|
||||
_ => Err(WalletError::RpcRequestError(
|
||||
|
@ -1066,15 +1012,6 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult {
|
|||
process_create_stake_account(&rpc_client, config, &staking_account_pubkey, *lamports)
|
||||
}
|
||||
|
||||
WalletCommand::CreateMiningPoolAccount(mining_pool_account_pubkey, lamports) => {
|
||||
process_create_mining_pool_account(
|
||||
&rpc_client,
|
||||
config,
|
||||
&mining_pool_account_pubkey,
|
||||
*lamports,
|
||||
)
|
||||
}
|
||||
|
||||
WalletCommand::DelegateStake(staking_account_keypair, voting_account_pubkey, lamports) => {
|
||||
process_delegate_stake(
|
||||
&rpc_client,
|
||||
|
@ -1085,17 +1022,14 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult {
|
|||
)
|
||||
}
|
||||
|
||||
WalletCommand::RedeemVoteCredits(
|
||||
mining_pool_account_pubkey,
|
||||
staking_account_pubkey,
|
||||
voting_account_pubkey,
|
||||
) => process_redeem_vote_credits(
|
||||
WalletCommand::RedeemVoteCredits(staking_account_pubkey, voting_account_pubkey) => {
|
||||
process_redeem_vote_credits(
|
||||
&rpc_client,
|
||||
config,
|
||||
&mining_pool_account_pubkey,
|
||||
&staking_account_pubkey,
|
||||
&voting_account_pubkey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
WalletCommand::ShowStakeAccount(staking_account_pubkey) => {
|
||||
process_show_stake_account(&rpc_client, config, &staking_account_pubkey)
|
||||
|
@ -1417,27 +1351,6 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
|
|||
.help("Vote account pubkey"),
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("create-mining-pool-account")
|
||||
.about("Create staking mining pool account")
|
||||
.arg(
|
||||
Arg::with_name("mining_pool_account_pubkey")
|
||||
.index(1)
|
||||
.value_name("PUBKEY")
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
.validator(is_pubkey)
|
||||
.help("Staking mining pool account address to fund"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("lamports")
|
||||
.index(2)
|
||||
.value_name("NUM")
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
.help("The number of lamports to assign to the mining pool account"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("create-stake-account")
|
||||
.about("Create staking account")
|
||||
|
|
Loading…
Reference in New Issue