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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 570 additions and 553 deletions

View File

@ -145,7 +145,6 @@ pub(crate) mod tests {
let leader_stake = Stake {
stake: BOOTSTRAP_LEADER_LAMPORTS,
epoch: 0,
..Stake::default()
};

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)
);
}

View File

@ -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);
// request slot_hashes syscall account after vote_pubkey
account_metas.insert(1, AccountMeta::new(slot_hashes::id(), false));
let account_metas = metas_for_authorized_signer(
vote_pubkey,
authorized_voter_pubkey,
&[
// request slot_hashes syscall account after vote_pubkey
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());
}

View File

@ -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);
}
self.epoch = epoch;
self.last_epoch_credits = self.credits;
}
self.credits += 1;
}
pub fn process_slot_vote_unchecked(&mut self, slot: u64) {
/// "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,
)
.unwrap();
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
);
}
}

View File

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

View File

@ -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::*;

View File

@ -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::*;

View File

@ -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();

View File

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

View File

@ -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(
&rpc_client,
config,
&mining_pool_account_pubkey,
&staking_account_pubkey,
&voting_account_pubkey,
),
WalletCommand::RedeemVoteCredits(staking_account_pubkey, voting_account_pubkey) => {
process_redeem_vote_credits(
&rpc_client,
config,
&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")