Split ActiveStakers over Bank and LeaderScheduler

This commit is contained in:
Greg Fitzgerald 2019-02-23 22:00:55 -07:00
parent f89e83ae49
commit a1070e9572
7 changed files with 307 additions and 339 deletions

View File

@ -663,6 +663,48 @@ impl Bank {
.collect()
}
fn sort_stakes(stakes: &mut Vec<(Pubkey, u64)>) {
// Sort first by stake. If stakes are the same, sort by pubkey to ensure a
// deterministic result.
// Note: Use unstable sort, because we dedup right after to remove the equal elements.
stakes.sort_unstable_by(|(pubkey0, stake0), (pubkey1, stake1)| {
if stake0 == stake1 {
pubkey0.cmp(&pubkey1)
} else {
stake0.cmp(&stake1)
}
});
// Now that it's sorted, we can do an O(n) dedup.
stakes.dedup();
}
/// Return a sorted, filtered list of staker/stake pairs.
pub fn filtered_stakes<F>(&self, filter: F) -> Vec<(Pubkey, u64)>
where
F: Fn(&VoteState) -> bool,
{
let mut stakes: Vec<_> = self
.vote_states(filter)
.iter()
.filter_map(|vote_state| {
let pubkey = vote_state.staker_id;
let stake = self.get_balance(&pubkey);
if stake > 0 {
Some((pubkey, stake))
} else {
None
}
})
.collect();
Self::sort_stakes(&mut stakes);
stakes
}
pub fn stakes(&self) -> Vec<(Pubkey, u64)> {
self.filtered_stakes(|_| true)
}
/// Return the number of ticks per slot that should be used calls to slot_height().
pub fn ticks_per_slot(&self) -> u64 {
self.ticks_per_slot
@ -1366,4 +1408,30 @@ mod tests {
}
}
#[test]
fn test_sort_stakes_basic() {
let pubkey0 = Keypair::new().pubkey();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 2), (pubkey1, 1)];
Bank::sort_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey1, 1), (pubkey0, 2)]);
}
#[test]
fn test_sort_stakes_with_dup() {
let pubkey0 = Keypair::new().pubkey();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 1), (pubkey1, 2), (pubkey0, 1)];
Bank::sort_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey0, 1), (pubkey1, 2)]);
}
#[test]
fn test_sort_stakes_with_equal_stakes() {
let pubkey0 = Pubkey::default();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 1), (pubkey1, 1)];
Bank::sort_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey0, 1), (pubkey1, 1)]);
}
}

View File

@ -1,324 +0,0 @@
use solana_runtime::bank::Bank;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::timing::DEFAULT_SLOTS_PER_EPOCH;
use solana_sdk::vote_program::VoteState;
pub const DEFAULT_ACTIVE_WINDOW_NUM_SLOTS: u64 = DEFAULT_SLOTS_PER_EPOCH;
// Return true of the latest vote is between the lower and upper bounds (inclusive)
fn is_active_staker(vote_state: &VoteState, lower_bound: u64, upper_bound: u64) -> bool {
vote_state
.votes
.back()
.filter(|vote| vote.slot_height >= lower_bound && vote.slot_height <= upper_bound)
.is_some()
}
pub fn rank_stakes(stakes: &mut Vec<(Pubkey, u64)>) {
// Rank first by stake. If stakes are the same we rank by pubkey to ensure a
// deterministic result.
// Note: Use unstable sort, because we dedup right after to remove the equal elements.
stakes.sort_unstable_by(|(pubkey0, stake0), (pubkey1, stake1)| {
if stake0 == stake1 {
pubkey0.cmp(&pubkey1)
} else {
stake0.cmp(&stake1)
}
});
// Now that it's sorted, we can do an O(n) dedup.
stakes.dedup();
}
/// The set of stakers that have voted near the time of construction
pub struct ActiveStakers {
stakes: Vec<(Pubkey, u64)>,
}
impl ActiveStakers {
pub fn new_with_bounds(bank: &Bank, active_window_num_slots: u64, upper_bound: u64) -> Self {
let lower_bound = upper_bound.saturating_sub(active_window_num_slots);
let mut stakes: Vec<_> = bank
.vote_states(|vote_state| is_active_staker(vote_state, lower_bound, upper_bound))
.iter()
.filter_map(|vote_state| {
let pubkey = vote_state.staker_id;
let stake = bank.get_balance(&pubkey);
if stake > 0 {
Some((pubkey, stake))
} else {
None
}
})
.collect();
rank_stakes(&mut stakes);
Self { stakes }
}
pub fn new(bank: &Bank) -> Self {
Self::new_with_bounds(bank, DEFAULT_ACTIVE_WINDOW_NUM_SLOTS, bank.slot_height())
}
pub fn sorted_stakes(&self) -> Vec<(Pubkey, u64)> {
self.stakes.clone()
}
/// Return the pubkeys of each staker.
pub fn pubkeys(&self) -> Vec<Pubkey> {
self.stakes.iter().map(|(pubkey, _stake)| *pubkey).collect()
}
/// Return the sorted pubkeys of each staker. Useful for testing.
pub fn sorted_pubkeys(&self) -> Vec<Pubkey> {
let mut pubkeys = self.pubkeys();
pubkeys.sort_unstable();
pubkeys
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use solana_runtime::bank::Bank;
use solana_sdk::genesis_block::GenesisBlock;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::vote_transaction::VoteTransaction;
pub fn new_vote_account(
from_keypair: &Keypair,
voting_pubkey: &Pubkey,
bank: &Bank,
num_tokens: u64,
) {
let last_id = bank.last_id();
let tx = VoteTransaction::new_account(from_keypair, *voting_pubkey, last_id, num_tokens, 0);
bank.process_transaction(&tx).unwrap();
}
pub fn push_vote<T: KeypairUtil>(voting_keypair: &T, bank: &Bank, slot_height: u64) {
let last_id = bank.last_id();
let tx = VoteTransaction::new_vote(voting_keypair, slot_height, last_id, 0);
bank.process_transaction(&tx).unwrap();
}
pub fn new_vote_account_with_vote<T: KeypairUtil>(
from_keypair: &Keypair,
voting_keypair: &T,
bank: &Bank,
num_tokens: u64,
slot_height: u64,
) {
new_vote_account(from_keypair, &voting_keypair.pubkey(), bank, num_tokens);
push_vote(voting_keypair, bank, slot_height);
}
#[test]
fn test_active_set() {
solana_logger::setup();
let leader_id = Keypair::new().pubkey();
let active_window_tick_length = 1000;
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(10000, leader_id, 500);
let bank = Bank::new(&genesis_block);
let bootstrap_ids = vec![genesis_block.bootstrap_leader_id];
// Insert a bunch of votes at height "start_height"
let start_height = 3;
let num_old_ids = 20;
let mut old_ids = vec![];
for _ in 0..num_old_ids {
let new_keypair = Keypair::new();
let pk = new_keypair.pubkey();
old_ids.push(pk);
// Give the account some stake
bank.transfer(5, &mint_keypair, pk, genesis_block.last_id())
.unwrap();
// Create a vote account and push a vote
new_vote_account_with_vote(&new_keypair, &Keypair::new(), &bank, 1, start_height);
}
old_ids.sort();
// Insert a bunch of votes at height "start_height + active_window_tick_length"
let num_new_ids = 10;
let mut new_ids = vec![];
for _ in 0..num_new_ids {
let new_keypair = Keypair::new();
let pk = new_keypair.pubkey();
new_ids.push(pk);
// Give the account some stake
bank.transfer(5, &mint_keypair, pk, genesis_block.last_id())
.unwrap();
// Create a vote account and push a vote
let slot_height = start_height + active_window_tick_length + 1;
new_vote_account_with_vote(&new_keypair, &Keypair::new(), &bank, 1, slot_height);
}
new_ids.sort();
// Query for the active set at various heights
let result = ActiveStakers::new_with_bounds(&bank, active_window_tick_length, 0).pubkeys();
assert_eq!(result, bootstrap_ids);
let result =
ActiveStakers::new_with_bounds(&bank, active_window_tick_length, start_height - 1)
.pubkeys();
assert_eq!(result, bootstrap_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + start_height - 1,
)
.sorted_pubkeys();
assert_eq!(result, old_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + start_height,
)
.sorted_pubkeys();
assert_eq!(result, old_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + start_height + 1,
)
.sorted_pubkeys();
assert_eq!(result, new_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height,
)
.sorted_pubkeys();
assert_eq!(result, new_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height + 1,
)
.sorted_pubkeys();
assert_eq!(result, new_ids);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height + 2,
)
.sorted_pubkeys();
assert_eq!(result.len(), 0);
}
#[test]
fn test_multiple_vote() {
let leader_keypair = Keypair::new();
let leader_id = leader_keypair.pubkey();
let active_window_tick_length = 1000;
let (genesis_block, _mint_keypair) = GenesisBlock::new_with_leader(10000, leader_id, 500);
let bank = Bank::new(&genesis_block);
// Bootstrap leader should be in the active set even without explicit votes
{
let result = ActiveStakers::new_with_bounds(&bank, active_window_tick_length, 0)
.sorted_pubkeys();
assert_eq!(result, vec![leader_id]);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length,
)
.sorted_pubkeys();
assert_eq!(result, vec![leader_id]);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + 1,
)
.sorted_pubkeys();
assert_eq!(result.len(), 0);
}
// Check that a node that votes twice in a row will get included in the active
// window
// Create a vote account
let voting_keypair = Keypair::new();
new_vote_account_with_vote(&leader_keypair, &voting_keypair, &bank, 1, 1);
{
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + 1,
)
.sorted_pubkeys();
assert_eq!(result, vec![leader_id]);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + 2,
)
.sorted_pubkeys();
assert_eq!(result.len(), 0);
}
// Vote at slot_height 2
push_vote(&voting_keypair, &bank, 2);
{
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + 2,
)
.sorted_pubkeys();
assert_eq!(result, vec![leader_id]);
let result = ActiveStakers::new_with_bounds(
&bank,
active_window_tick_length,
active_window_tick_length + 3,
)
.sorted_pubkeys();
assert_eq!(result.len(), 0);
}
}
#[test]
fn test_rank_stakes_basic() {
let pubkey0 = Keypair::new().pubkey();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 2), (pubkey1, 1)];
rank_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey1, 1), (pubkey0, 2)]);
}
#[test]
fn test_rank_stakes_with_dup() {
let pubkey0 = Keypair::new().pubkey();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 1), (pubkey1, 2), (pubkey0, 1)];
rank_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey0, 1), (pubkey1, 2)]);
}
#[test]
fn test_rank_stakes_with_equal_stakes() {
let pubkey0 = Pubkey::default();
let pubkey1 = Keypair::new().pubkey();
let mut stakes = vec![(pubkey0, 1), (pubkey1, 1)];
rank_stakes(&mut stakes);
assert_eq!(stakes, vec![(pubkey0, 1), (pubkey1, 1)]);
}
}

View File

@ -137,9 +137,9 @@ impl Service for LeaderConfirmationService {
}
#[cfg(test)]
pub mod tests {
mod tests {
use super::*;
use crate::active_stakers::tests::{new_vote_account, push_vote};
use crate::voting_keypair::tests::{new_vote_account, push_vote};
use crate::voting_keypair::VotingKeypair;
use bincode::serialize;
use solana_sdk::genesis_block::GenesisBlock;

View File

@ -1,4 +1,3 @@
use crate::active_stakers::ActiveStakers;
use rand::distributions::{Distribution, WeightedIndex};
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
@ -30,14 +29,9 @@ impl LeaderSchedule {
}
pub fn new_with_bank(bank: &Bank) -> Self {
let active_stakers = ActiveStakers::new(&bank);
let mut seed = [0u8; 32];
seed.copy_from_slice(bank.last_id().as_ref());
Self::new(
&active_stakers.sorted_stakes(),
&seed,
bank.slots_per_epoch(),
)
Self::new(&bank.stakes(), &seed, bank.slots_per_epoch())
}
}

View File

@ -1,7 +1,6 @@
//! The `leader_scheduler` module implements a structure and functions for tracking and
//! managing the schedule for leader rotation
use crate::active_stakers::{ActiveStakers, DEFAULT_ACTIVE_WINDOW_NUM_SLOTS};
use crate::entry::{create_ticks, next_entry_mut, Entry};
use crate::voting_keypair::VotingKeypair;
use bincode::serialize;
@ -12,10 +11,13 @@ use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::system_transaction::SystemTransaction;
use solana_sdk::timing::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT};
use solana_sdk::vote_program::VoteState;
use solana_sdk::vote_transaction::VoteTransaction;
use std::io::Cursor;
use std::sync::Arc;
pub const DEFAULT_ACTIVE_WINDOW_NUM_SLOTS: u64 = DEFAULT_SLOTS_PER_EPOCH;
#[derive(Clone)]
pub struct LeaderSchedulerConfig {
pub ticks_per_slot: u64,
@ -47,6 +49,24 @@ impl Default for LeaderSchedulerConfig {
}
}
// Return true of the latest vote is between the lower and upper bounds (inclusive)
fn is_active_staker(vote_state: &VoteState, lower_bound: u64, upper_bound: u64) -> bool {
vote_state
.votes
.back()
.filter(|vote| vote.slot_height >= lower_bound && vote.slot_height <= upper_bound)
.is_some()
}
fn get_active_stakes(
bank: &Bank,
active_window_num_slots: u64,
upper_bound: u64,
) -> Vec<(Pubkey, u64)> {
let lower_bound = upper_bound.saturating_sub(active_window_num_slots);
bank.filtered_stakes(|vote_state| is_active_staker(vote_state, lower_bound, upper_bound))
}
#[derive(Clone, Debug)]
pub struct LeaderScheduler {
// A leader slot duration in ticks
@ -242,9 +262,7 @@ impl LeaderScheduler {
self.seed = Self::calculate_seed(tick_height);
let slot = self.tick_height_to_slot(tick_height);
let ranked_active_set =
ActiveStakers::new_with_bounds(&bank, self.active_window_num_slots, slot)
.sorted_stakes();
let ranked_active_set = get_active_stakes(&bank, self.active_window_num_slots, slot);
if ranked_active_set.is_empty() {
info!(
@ -398,9 +416,186 @@ pub fn make_active_set_entries(
#[cfg(test)]
pub mod tests {
use super::*;
use crate::active_stakers::tests::new_vote_account_with_vote;
use crate::voting_keypair::tests::{new_vote_account_with_vote, push_vote};
use hashbrown::HashSet;
use solana_runtime::bank::Bank;
use solana_sdk::genesis_block::{GenesisBlock, BOOTSTRAP_LEADER_TOKENS};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::timing::DEFAULT_SLOTS_PER_EPOCH;
fn get_active_pubkeys(
bank: &Bank,
active_window_num_slots: u64,
upper_bound: u64,
) -> Vec<Pubkey> {
let stakes = get_active_stakes(bank, active_window_num_slots, upper_bound);
stakes.into_iter().map(|x| x.0).collect()
}
#[test]
fn test_active_set() {
solana_logger::setup();
let leader_id = Keypair::new().pubkey();
let active_window_tick_length = 1000;
let (genesis_block, mint_keypair) = GenesisBlock::new_with_leader(10000, leader_id, 500);
let bank = Bank::new(&genesis_block);
let bootstrap_ids = vec![genesis_block.bootstrap_leader_id];
// Insert a bunch of votes at height "start_height"
let start_height = 3;
let num_old_ids = 20;
let mut old_ids = vec![];
for _ in 0..num_old_ids {
let new_keypair = Keypair::new();
let pk = new_keypair.pubkey();
old_ids.push(pk);
// Give the account some stake
bank.transfer(5, &mint_keypair, pk, genesis_block.last_id())
.unwrap();
// Create a vote account and push a vote
new_vote_account_with_vote(&new_keypair, &Keypair::new(), &bank, 1, start_height);
}
old_ids.sort();
// Insert a bunch of votes at height "start_height + active_window_tick_length"
let num_new_ids = 10;
let mut new_ids = vec![];
for _ in 0..num_new_ids {
let new_keypair = Keypair::new();
let pk = new_keypair.pubkey();
new_ids.push(pk);
// Give the account some stake
bank.transfer(5, &mint_keypair, pk, genesis_block.last_id())
.unwrap();
// Create a vote account and push a vote
let slot_height = start_height + active_window_tick_length + 1;
new_vote_account_with_vote(&new_keypair, &Keypair::new(), &bank, 1, slot_height);
}
new_ids.sort();
// Query for the active set at various heights
let result = get_active_pubkeys(&bank, active_window_tick_length, 0);
assert_eq!(result, bootstrap_ids);
let result = get_active_pubkeys(&bank, active_window_tick_length, start_height - 1);
assert_eq!(result, bootstrap_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + start_height - 1,
);
assert_eq!(result, old_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + start_height,
);
assert_eq!(result, old_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + start_height + 1,
);
assert_eq!(result, new_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height,
);
assert_eq!(result, new_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height + 1,
);
assert_eq!(result, new_ids);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
2 * active_window_tick_length + start_height + 2,
);
assert_eq!(result.len(), 0);
}
#[test]
fn test_multiple_vote() {
let leader_keypair = Keypair::new();
let leader_id = leader_keypair.pubkey();
let active_window_tick_length = 1000;
let (genesis_block, _mint_keypair) = GenesisBlock::new_with_leader(10000, leader_id, 500);
let bank = Bank::new(&genesis_block);
// Bootstrap leader should be in the active set even without explicit votes
{
let result = get_active_pubkeys(&bank, active_window_tick_length, 0);
assert_eq!(result, vec![leader_id]);
let result =
get_active_pubkeys(&bank, active_window_tick_length, active_window_tick_length);
assert_eq!(result, vec![leader_id]);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + 1,
);
assert_eq!(result.len(), 0);
}
// Check that a node that votes twice in a row will get included in the active
// window
// Create a vote account
let voting_keypair = Keypair::new();
new_vote_account_with_vote(&leader_keypair, &voting_keypair, &bank, 1, 1);
{
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + 1,
);
assert_eq!(result, vec![leader_id]);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + 2,
);
assert_eq!(result.len(), 0);
}
// Vote at slot_height 2
push_vote(&voting_keypair, &bank, 2);
{
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + 2,
);
assert_eq!(result, vec![leader_id]);
let result = get_active_pubkeys(
&bank,
active_window_tick_length,
active_window_tick_length + 3,
);
assert_eq!(result.len(), 0);
}
}
fn run_scheduler_test(num_validators: u64, ticks_per_slot: u64, slots_per_epoch: u64) {
info!(

View File

@ -7,7 +7,6 @@
//!
#![cfg_attr(feature = "unstable", feature(test))]
pub mod active_stakers;
pub mod bank_forks;
pub mod banking_stage;
pub mod blob_fetch_stage;

View File

@ -95,3 +95,39 @@ impl VotingKeypair {
Self::new_with_signer(keypair, Box::new(LocalVoteSigner::default()))
}
}
#[cfg(test)]
pub mod tests {
use solana_runtime::bank::Bank;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, KeypairUtil};
use solana_sdk::vote_transaction::VoteTransaction;
pub fn new_vote_account(
from_keypair: &Keypair,
voting_pubkey: &Pubkey,
bank: &Bank,
num_tokens: u64,
) {
let last_id = bank.last_id();
let tx = VoteTransaction::new_account(from_keypair, *voting_pubkey, last_id, num_tokens, 0);
bank.process_transaction(&tx).unwrap();
}
pub fn push_vote<T: KeypairUtil>(voting_keypair: &T, bank: &Bank, slot_height: u64) {
let last_id = bank.last_id();
let tx = VoteTransaction::new_vote(voting_keypair, slot_height, last_id, 0);
bank.process_transaction(&tx).unwrap();
}
pub fn new_vote_account_with_vote<T: KeypairUtil>(
from_keypair: &Keypair,
voting_keypair: &T,
bank: &Bank,
num_tokens: u64,
slot_height: u64,
) {
new_vote_account(from_keypair, &voting_keypair.pubkey(), bank, num_tokens);
push_vote(voting_keypair, bank, slot_height);
}
}