diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 9e33fdfa3d..7195dde8f4 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -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(&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)]); + } } diff --git a/src/active_stakers.rs b/src/active_stakers.rs deleted file mode 100644 index 884b3ed18e..0000000000 --- a/src/active_stakers.rs +++ /dev/null @@ -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 { - self.stakes.iter().map(|(pubkey, _stake)| *pubkey).collect() - } - - /// Return the sorted pubkeys of each staker. Useful for testing. - pub fn sorted_pubkeys(&self) -> Vec { - 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(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( - 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)]); - } -} diff --git a/src/leader_confirmation_service.rs b/src/leader_confirmation_service.rs index 2623fd0f18..52144b5233 100644 --- a/src/leader_confirmation_service.rs +++ b/src/leader_confirmation_service.rs @@ -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; diff --git a/src/leader_schedule.rs b/src/leader_schedule.rs index 2c86dae8a5..768e883528 100644 --- a/src/leader_schedule.rs +++ b/src/leader_schedule.rs @@ -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()) } } diff --git a/src/leader_scheduler.rs b/src/leader_scheduler.rs index c54927c134..385e5922b8 100644 --- a/src/leader_scheduler.rs +++ b/src/leader_scheduler.rs @@ -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 { + 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!( diff --git a/src/lib.rs b/src/lib.rs index 611dd2af3e..67afad24e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/voting_keypair.rs b/src/voting_keypair.rs index ed988fdb3a..6d06ddbd9a 100644 --- a/src/voting_keypair.rs +++ b/src/voting_keypair.rs @@ -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(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( + 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); + } +}