From 0e2722c638c16d3d6ff426477b9640f4868ec415 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Tue, 31 Mar 2020 08:23:42 -0700 Subject: [PATCH] solana-validator now supports multiple --authorized-voter arguments (#9174) * Use Epoch type * Vote account's authorized voter is now supported without a validator restart --- clap-utils/src/input_parsers.rs | 15 +++ core/src/replay_stage.rs | 144 ++++++++++++++++++------- core/src/tvu.rs | 10 +- core/src/validator.rs | 28 ++--- local-cluster/src/local_cluster.rs | 6 +- programs/vote/src/authorized_voters.rs | 22 ++-- programs/vote/src/vote_state/mod.rs | 10 +- validator/src/main.rs | 142 +++++++++++------------- 8 files changed, 222 insertions(+), 155 deletions(-) diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 24ed38f83..73931774a 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -62,6 +62,21 @@ pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option { } } +pub fn keypairs_of(matches: &ArgMatches<'_>, name: &str) -> Option> { + matches.values_of(name).map(|values| { + values + .filter_map(|value| { + if value == ASK_KEYWORD { + let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); + keypair_from_seed_phrase(name, skip_validation, true).ok() + } else { + read_keypair_file(value).ok() + } + }) + .collect() + }) +} + // Return a pubkey for an argument that can itself be parsed into a pubkey, // or is a filename that can be read as a keypair pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option { diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 711780f91..a99c9166b 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -33,7 +33,10 @@ use solana_sdk::{ timing::{self, duration_as_ms}, transaction::Transaction, }; -use solana_vote_program::vote_instruction; +use solana_vote_program::{ + vote_instruction, + vote_state::{Vote, VoteState}, +}; use std::{ collections::{HashMap, HashSet}, ops::Deref, @@ -89,7 +92,7 @@ struct SkippedSlotsInfo { pub struct ReplayStageConfig { pub my_pubkey: Pubkey, pub vote_account: Pubkey, - pub voting_keypair: Option>, + pub authorized_voter_keypairs: Vec>, pub exit: Arc, pub subscriptions: Arc, pub leader_schedule_cache: Arc, @@ -121,7 +124,7 @@ impl ReplayStage { let ReplayStageConfig { my_pubkey, vote_account, - voting_keypair, + authorized_voter_keypairs, exit, subscriptions, leader_schedule_cache, @@ -328,7 +331,7 @@ impl ReplayStage { &mut tower, &mut progress, &vote_account, - &voting_keypair, + &authorized_voter_keypairs, &cluster_info, &blockstore, &leader_schedule_cache, @@ -706,8 +709,8 @@ impl ReplayStage { bank_forks: &Arc>, tower: &mut Tower, progress: &mut ProgressMap, - vote_account: &Pubkey, - voting_keypair: &Option>, + vote_account_pubkey: &Pubkey, + authorized_voter_keypairs: &[Arc], cluster_info: &Arc>, blockstore: &Arc, leader_schedule_cache: &Arc, @@ -723,7 +726,7 @@ impl ReplayStage { inc_new_counter_info!("replay_stage-voted_empty_bank", 1); } trace!("handle votable bank {}", bank.slot()); - let (vote, tower_index) = tower.new_vote_from_bank(bank, vote_account); + let (vote, tower_index) = tower.new_vote_from_bank(bank, vote_account_pubkey); if let Some(new_root) = tower.record_bank_vote(vote) { // get the root bank before squash let root_bank = bank_forks @@ -771,30 +774,91 @@ impl ReplayStage { lockouts_sender, ); - if let Some(ref voting_keypair) = voting_keypair { - let node_keypair = cluster_info.read().unwrap().keypair.clone(); - - // Send our last few votes along with the new one - let vote_ix = vote_instruction::vote( - &vote_account, - &voting_keypair.pubkey(), - tower.last_vote_and_timestamp(), - ); - - let mut vote_tx = - Transaction::new_with_payer(vec![vote_ix], Some(&node_keypair.pubkey())); - - let blockhash = bank.last_blockhash(); - vote_tx.partial_sign(&[node_keypair.as_ref()], blockhash); - vote_tx.partial_sign(&[voting_keypair.as_ref()], blockhash); - cluster_info - .write() - .unwrap() - .push_vote(tower_index, vote_tx); - } + Self::push_vote( + cluster_info, + bank, + vote_account_pubkey, + authorized_voter_keypairs, + tower.last_vote_and_timestamp(), + tower_index, + ); Ok(()) } + fn push_vote( + cluster_info: &Arc>, + bank: &Arc, + vote_account_pubkey: &Pubkey, + authorized_voter_keypairs: &[Arc], + vote: Vote, + tower_index: usize, + ) { + if authorized_voter_keypairs.is_empty() { + return; + } + + let vote_state = + if let Some((_, vote_account)) = bank.vote_accounts().get(vote_account_pubkey) { + if let Some(vote_state) = VoteState::from(&vote_account) { + vote_state + } else { + warn!( + "Vote account {} is unreadable. Unable to vote", + vote_account_pubkey, + ); + return; + } + } else { + warn!( + "Vote account {} does not exist. Unable to vote", + vote_account_pubkey, + ); + return; + }; + + let authorized_voter_pubkey = + if let Some(authorized_voter_pubkey) = vote_state.get_authorized_voter(bank.epoch()) { + authorized_voter_pubkey + } else { + warn!( + "Vote account {} has no authorized voter for epoch {}. Unable to vote", + vote_account_pubkey, + bank.epoch() + ); + return; + }; + + let authorized_voter_keypair = match authorized_voter_keypairs + .iter() + .find(|keypair| keypair.pubkey() == authorized_voter_pubkey) + { + None => { + warn!("The authorized keypair {} for vote account {} is not available. Unable to vote", + authorized_voter_pubkey, vote_account_pubkey); + return; + } + Some(authorized_voter_keypair) => authorized_voter_keypair, + }; + let node_keypair = cluster_info.read().unwrap().keypair.clone(); + + // Send our last few votes along with the new one + let vote_ix = vote_instruction::vote( + &vote_account_pubkey, + &authorized_voter_keypair.pubkey(), + vote, + ); + + let mut vote_tx = Transaction::new_with_payer(vec![vote_ix], Some(&node_keypair.pubkey())); + + let blockhash = bank.last_blockhash(); + vote_tx.partial_sign(&[node_keypair.as_ref()], blockhash); + vote_tx.partial_sign(&[authorized_voter_keypair.as_ref()], blockhash); + cluster_info + .write() + .unwrap() + .push_vote(tower_index, vote_tx); + } + fn update_commitment_cache( bank: Arc, root: Slot, @@ -1621,7 +1685,7 @@ pub(crate) mod tests { struct ValidatorInfo { stake: u64, keypair: Keypair, - voting_keypair: Keypair, + authorized_voter_keypair: Keypair, staking_keypair: Keypair, } @@ -1676,7 +1740,7 @@ pub(crate) mod tests { .iter() .map(|validator| { vote_state::create_account( - &validator.voting_keypair.pubkey(), + &validator.authorized_voter_keypair.pubkey(), &validator.keypair.pubkey(), 0, validator.stake, @@ -1690,7 +1754,7 @@ pub(crate) mod tests { .map(|(i, validator)| { stake_state::create_account( &validator.staking_keypair.pubkey(), - &validator.voting_keypair.pubkey(), + &validator.authorized_voter_keypair.pubkey(), &genesis_vote_accounts[i], &Rent::default(), validator.stake, @@ -1703,7 +1767,7 @@ pub(crate) mod tests { for i in 0..validators.len() { genesis_config.accounts.insert( - validators[i].voting_keypair.pubkey(), + validators[i].authorized_voter_keypair.pubkey(), genesis_vote_accounts[i].clone(), ); genesis_config.accounts.insert( @@ -1737,7 +1801,7 @@ pub(crate) mod tests { for validator in validators.iter() { vote( &bank_forks.banks[&neutral_fork.fork[index]].clone(), - &validator.voting_keypair.pubkey(), + &validator.authorized_voter_keypair.pubkey(), neutral_fork.fork[index - 1], ); } @@ -1777,7 +1841,7 @@ pub(crate) mod tests { for voter_index in fork_info.voters.iter() { vote( &bank_forks.banks[&fork_info.fork[index]].clone(), - &validators[*voter_index].voting_keypair.pubkey(), + &validators[*voter_index].authorized_voter_keypair.pubkey(), last_bank.slot(), ); } @@ -1863,20 +1927,20 @@ pub(crate) mod tests { ValidatorInfo { stake: 34_000_000, keypair: Keypair::new(), - voting_keypair: Keypair::new(), + authorized_voter_keypair: Keypair::new(), staking_keypair: Keypair::new(), }, ValidatorInfo { stake: 33_000_000, keypair: Keypair::new(), - voting_keypair: Keypair::new(), + authorized_voter_keypair: Keypair::new(), staking_keypair: Keypair::new(), }, // Malicious Node ValidatorInfo { stake: 33_000_000, keypair: Keypair::new(), - voting_keypair: Keypair::new(), + authorized_voter_keypair: Keypair::new(), staking_keypair: Keypair::new(), }, ]; @@ -1906,18 +1970,18 @@ pub(crate) mod tests { Blockstore::open(&ledger_path) .expect("Expected to be able to open database ledger"), ); - let validator_voting_keypairs: Vec<_> = (0..20) + let validator_authorized_voter_keypairs: Vec<_> = (0..20) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) .collect(); - let validator_voting_keys: HashMap<_, _> = validator_voting_keypairs + let validator_voting_keys: HashMap<_, _> = validator_authorized_voter_keypairs .iter() .map(|v| (v.node_keypair.pubkey(), v.vote_keypair.pubkey())) .collect(); let GenesisConfigInfo { genesis_config, .. } = genesis_utils::create_genesis_config_with_vote_accounts( 10_000, - &validator_voting_keypairs, + &validator_authorized_voter_keypairs, 100, ); let bank0 = Bank::new(&genesis_config); diff --git a/core/src/tvu.rs b/core/src/tvu.rs index c2318065d..49fe9292f 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -81,7 +81,7 @@ impl Tvu { #[allow(clippy::new_ret_no_self, clippy::too_many_arguments)] pub fn new( vote_account: &Pubkey, - voting_keypair: Option>, + authorized_voter_keypairs: Vec>, storage_keypair: &Arc, bank_forks: &Arc>, cluster_info: &Arc>, @@ -179,7 +179,7 @@ impl Tvu { let replay_stage_config = ReplayStageConfig { my_pubkey: keypair.pubkey(), vote_account: *vote_account, - voting_keypair, + authorized_voter_keypairs, exit: exit.clone(), subscriptions: subscriptions.clone(), leader_schedule_cache: leader_schedule_cache.clone(), @@ -287,14 +287,14 @@ pub mod tests { let bank = bank_forks.working_bank(); let (exit, poh_recorder, poh_service, _entry_receiver) = create_test_recorder(&bank, &blockstore, None); - let voting_keypair = Keypair::new(); + let vote_keypair = Keypair::new(); let storage_keypair = Arc::new(Keypair::new()); let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); let (retransmit_slots_sender, _retransmit_slots_receiver) = unbounded(); let tvu = Tvu::new( - &voting_keypair.pubkey(), - Some(Arc::new(voting_keypair)), + &vote_keypair.pubkey(), + vec![Arc::new(vote_keypair)], &storage_keypair, &Arc::new(RwLock::new(bank_forks)), &cref1, diff --git a/core/src/validator.rs b/core/src/validator.rs index cec231afc..73041ff00 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -151,7 +151,7 @@ impl Validator { keypair: &Arc, ledger_path: &Path, vote_account: &Pubkey, - authorized_voter: &Arc, + mut authorized_voter_keypairs: Vec>, storage_keypair: &Arc, entrypoint_info_option: Option<&ContactInfo>, poh_verify: bool, @@ -162,7 +162,15 @@ impl Validator { warn!("identity: {}", id); warn!("vote account: {}", vote_account); - warn!("authorized voter: {}", authorized_voter.pubkey()); + + if config.voting_disabled { + warn!("voting disabled"); + authorized_voter_keypairs.clear(); + } else { + for authorized_voter_keypair in &authorized_voter_keypairs { + warn!("authorized voter: {}", authorized_voter_keypair.pubkey()); + } + } report_target_features(); info!("entrypoint: {:?}", entrypoint_info_option); @@ -386,11 +394,7 @@ impl Validator { let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); let tvu = Tvu::new( vote_account, - if config.voting_disabled { - None - } else { - Some(authorized_voter.clone()) - }, + authorized_voter_keypairs, storage_keypair, &bank_forks, &cluster_info, @@ -728,7 +732,7 @@ impl TestValidator { &node_keypair, &ledger_path, &leader_voting_keypair.pubkey(), - &leader_voting_keypair, + vec![leader_voting_keypair.clone()], &storage_keypair, None, true, @@ -836,7 +840,7 @@ mod tests { &Arc::new(validator_keypair), &validator_ledger_path, &voting_keypair.pubkey(), - &voting_keypair, + vec![voting_keypair.clone()], &storage_keypair, Some(&leader_node.info), true, @@ -861,7 +865,7 @@ mod tests { .genesis_config; let (validator_ledger_path, _blockhash) = create_new_tmp_ledger!(&genesis_config); ledger_paths.push(validator_ledger_path.clone()); - let voting_keypair = Arc::new(Keypair::new()); + let vote_account_keypair = Arc::new(Keypair::new()); let storage_keypair = Arc::new(Keypair::new()); let config = ValidatorConfig { rpc_ports: Some(( @@ -874,8 +878,8 @@ mod tests { validator_node, &Arc::new(validator_keypair), &validator_ledger_path, - &voting_keypair.pubkey(), - &voting_keypair, + &vote_account_keypair.pubkey(), + vec![vote_account_keypair.clone()], &storage_keypair, Some(&leader_node.info), true, diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index a4447a697..593514249 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -219,7 +219,7 @@ impl LocalCluster { &leader_keypair, &leader_ledger_path, &leader_voting_keypair.pubkey(), - &leader_voting_keypair, + vec![leader_voting_keypair.clone()], &leader_storage_keypair, None, true, @@ -367,7 +367,7 @@ impl LocalCluster { &validator_keypair, &ledger_path, &voting_keypair.pubkey(), - &voting_keypair, + vec![voting_keypair.clone()], &storage_keypair, Some(&self.entry_point_info), true, @@ -687,7 +687,7 @@ impl Cluster for LocalCluster { &validator_info.keypair, &validator_info.ledger_path, &validator_info.voting_keypair.pubkey(), - &validator_info.voting_keypair, + vec![validator_info.voting_keypair.clone()], &validator_info.storage_keypair, entry_point_info, true, diff --git a/programs/vote/src/authorized_voters.rs b/programs/vote/src/authorized_voters.rs index 964314802..8606c2ccb 100644 --- a/programs/vote/src/authorized_voters.rs +++ b/programs/vote/src/authorized_voters.rs @@ -1,26 +1,26 @@ use log::*; use serde_derive::{Deserialize, Serialize}; -use solana_sdk::pubkey::Pubkey; +use solana_sdk::{clock::Epoch, pubkey::Pubkey}; use std::collections::BTreeMap; #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct AuthorizedVoters { - authorized_voters: BTreeMap, + authorized_voters: BTreeMap, } impl AuthorizedVoters { - pub fn new(epoch: u64, pubkey: Pubkey) -> Self { + pub fn new(epoch: Epoch, pubkey: Pubkey) -> Self { let mut authorized_voters = BTreeMap::new(); authorized_voters.insert(epoch, pubkey); Self { authorized_voters } } - pub fn get_authorized_voter(&self, epoch: u64) -> Option { + pub fn get_authorized_voter(&self, epoch: Epoch) -> Option { self.get_or_calculate_authorized_voter_for_epoch(epoch) .map(|(pubkey, _)| pubkey) } - pub fn get_and_cache_authorized_voter_for_epoch(&mut self, epoch: u64) -> Option { + pub fn get_and_cache_authorized_voter_for_epoch(&mut self, epoch: Epoch) -> Option { let res = self.get_or_calculate_authorized_voter_for_epoch(epoch); res.map(|(pubkey, existed)| { @@ -31,11 +31,11 @@ impl AuthorizedVoters { }) } - pub fn insert(&mut self, epoch: u64, authorized_voter: Pubkey) { + pub fn insert(&mut self, epoch: Epoch, authorized_voter: Pubkey) { self.authorized_voters.insert(epoch, authorized_voter); } - pub fn purge_authorized_voters(&mut self, current_epoch: u64) -> bool { + pub fn purge_authorized_voters(&mut self, current_epoch: Epoch) -> bool { // Iterate through the keys in order, filtering out the ones // less than the current epoch let expired_keys: Vec<_> = self @@ -72,14 +72,18 @@ impl AuthorizedVoters { self.authorized_voters.len() } - pub fn contains(&self, epoch: u64) -> bool { + pub fn contains(&self, epoch: Epoch) -> bool { self.authorized_voters.get(&epoch).is_some() } + pub fn iter(&self) -> std::collections::btree_map::Iter { + self.authorized_voters.iter() + } + // Returns the authorized voter at the given epoch if the epoch is >= the // current epoch, and a bool indicating whether the entry for this epoch // exists in the self.authorized_voter map - fn get_or_calculate_authorized_voter_for_epoch(&self, epoch: u64) -> Option<(Pubkey, bool)> { + fn get_or_calculate_authorized_voter_for_epoch(&self, epoch: Epoch) -> Option<(Pubkey, bool)> { let res = self.authorized_voters.get(&epoch); if res.is_none() { // If no authorized voter has been set yet for this epoch, diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 925d43399..418acf9e5 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -161,7 +161,7 @@ pub struct VoteState { pub commission: u8, pub votes: VecDeque, - pub root_slot: Option, + pub root_slot: Option, /// the signer for vote transactions authorized_voters: AuthorizedVoters, @@ -190,7 +190,7 @@ impl VoteState { } } - pub fn get_authorized_voter(&self, epoch: u64) -> Option { + pub fn get_authorized_voter(&self, epoch: Epoch) -> Option { self.authorized_voters.get_authorized_voter(epoch) } @@ -436,8 +436,8 @@ impl VoteState { fn set_new_authorized_voter( &mut self, authorized_pubkey: &Pubkey, - current_epoch: u64, - target_epoch: u64, + current_epoch: Epoch, + target_epoch: Epoch, verify: F, ) -> Result<(), InstructionError> where @@ -494,7 +494,7 @@ impl VoteState { Ok(()) } - fn get_and_update_authorized_voter(&mut self, current_epoch: u64) -> Option { + fn get_and_update_authorized_voter(&mut self, current_epoch: Epoch) -> Option { let pubkey = self .authorized_voters .get_and_cache_authorized_voter_for_epoch(current_epoch) diff --git a/validator/src/main.rs b/validator/src/main.rs index 030c3d059..baa9b130f 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -5,7 +5,7 @@ use clap::{ use log::*; use rand::{thread_rng, Rng}; use solana_clap_utils::{ - input_parsers::{keypair_of, pubkey_of}, + input_parsers::{keypair_of, keypairs_of, pubkey_of}, input_validators::{is_keypair_or_ask_keyword, is_pubkey, is_pubkey_or_keypair, is_slot}, keypair::SKIP_SEED_PHRASE_VALIDATION_ARG, }; @@ -271,70 +271,58 @@ fn get_rpc_node( fn check_vote_account( rpc_client: &RpcClient, - vote_pubkey: &Pubkey, - voting_pubkey: &Pubkey, - node_pubkey: &Pubkey, + identity_pubkey: &Pubkey, + vote_account_address: &Pubkey, + authorized_voter_pubkeys: &[Pubkey], ) -> Result<(), String> { - let found_vote_account = rpc_client - .get_account(vote_pubkey) - .map_err(|err| format!("Vote account not found: {}", err.to_string()))?; + let vote_account = rpc_client + .get_account(vote_account_address) + .map_err(|err| format!("vote account not found: {}", err.to_string()))?; - if found_vote_account.owner != solana_vote_program::id() { + if vote_account.owner != solana_vote_program::id() { return Err(format!( "not a vote account (owned by {}): {}", - found_vote_account.owner, vote_pubkey + vote_account.owner, vote_account_address )); } - let found_node_account = rpc_client - .get_account(node_pubkey) + let identity_account = rpc_client + .get_account(identity_pubkey) .map_err(|err| format!("Identity account not found: {}", err.to_string()))?; - let found_vote_account = solana_vote_program::vote_state::VoteState::from(&found_vote_account); - if let Some(found_vote_account) = found_vote_account { - if found_vote_account.authorized_voters().is_empty() { + let vote_state = solana_vote_program::vote_state::VoteState::from(&vote_account); + if let Some(vote_state) = vote_state { + if vote_state.authorized_voters().is_empty() { return Err("Vote account not yet initialized".to_string()); } - let epoch_info = rpc_client - .get_epoch_info() - .map_err(|err| format!("Failed to get epoch info: {}", err.to_string()))?; - - let mut authorized_voter; - authorized_voter = found_vote_account.get_authorized_voter(epoch_info.epoch); - if authorized_voter.is_none() { - // Must have gotten a clock on the boundary - authorized_voter = found_vote_account.get_authorized_voter(epoch_info.epoch + 1); - } - - let authorized_voter = authorized_voter.expect( - "Could not get the authorized voter, which only happens if the - client gets an epoch earlier than the current epoch, - but the received epoch should not be off by more than - one epoch", - ); - - if authorized_voter != *voting_pubkey { + if vote_state.node_pubkey != *identity_pubkey { return Err(format!( - "account's authorized voter ({}) does not match to the given voting keypair ({}).", - authorized_voter, voting_pubkey + "vote account's identity ({}) does not match the validator's identity {}).", + vote_state.node_pubkey, identity_pubkey )); } - if found_vote_account.node_pubkey != *node_pubkey { - return Err(format!( - "account's node pubkey ({}) does not match to the given identity keypair ({}).", - found_vote_account.node_pubkey, node_pubkey - )); + + for (_, vote_account_authorized_voter_pubkey) in vote_state.authorized_voters().iter() { + if !authorized_voter_pubkeys.contains(&vote_account_authorized_voter_pubkey) { + return Err(format!( + "authorized voter {} not available", + vote_account_authorized_voter_pubkey + )); + } } } else { - return Err(format!("invalid vote account data: {}", vote_pubkey)); + return Err(format!( + "invalid vote account data for {}", + vote_account_address + )); } // Maybe we can calculate minimum voting fee; rather than 1 lamport - if found_node_account.lamports <= 1 { + if identity_account.lamports <= 1 { return Err(format!( - "unfunded identity account ({}): only {} lamports (needs more fund to vote)", - node_pubkey, found_node_account.lamports + "underfunded identity account ({}): only {} lamports available", + identity_pubkey, identity_account.lamports )); } @@ -414,29 +402,22 @@ pub fn main() { Arg::with_name("identity") .short("i") .long("identity") - .alias("identity-keypair") // --identity-keypair is legacy for <= v1.0.6 users .value_name("PATH") .takes_value(true) .validator(is_keypair_or_ask_keyword) .help("Validator identity keypair"), ) .arg( - Arg::with_name("authorized_voter") + Arg::with_name("authorized_voter_keypairs") .long("authorized-voter") .value_name("PATH") .takes_value(true) .validator(is_keypair_or_ask_keyword) .requires("vote_account") - .help("Authorized voter keypair [default: value of --identity]"), - ) - .arg( - Arg::with_name("deprecated_voting_keypair") - .long("voting-keypair") - .value_name("PATH") - .takes_value(true) - .hidden(true) // Don't document this argument, it's legacy for <= v1.0.6 users - .conflicts_with_all(&["authorized_voter", "vote_account"]) - .validator(is_keypair_or_ask_keyword), + .multiple(true) + .help("Include an additional authorized voter keypair. \ + May be specified multiple times. \ + [default: the --identity keypair]"), ) .arg( Arg::with_name("vote_account") @@ -445,7 +426,9 @@ pub fn main() { .takes_value(true) .validator(is_pubkey_or_keypair) .requires("identity") - .help("Validator vote account public key. If unspecified voting will be disabled") + .help("Validator vote account public key. If unspecified voting will be disabled. \ + The authorized voter for the account must either be the --identity keypair \ + or with the --authorized-voter argument") ) .arg( Arg::with_name("storage_keypair") @@ -734,13 +717,9 @@ pub fn main() { let identity_keypair = Arc::new(keypair_of(&matches, "identity").unwrap_or_else(Keypair::new)); - let authorized_voter = keypair_of(&matches, "authorized_voter") - .or_else(|| { - // Legacy v1.0.6 argument support - keypair_of(&matches, "deprecated_voting_keypair") - }) - .map(Arc::new) - .unwrap_or_else(|| identity_keypair.clone()); + let authorized_voter_keypairs = keypairs_of(&matches, "authorized_voter_keypairs") + .map(|keypairs| keypairs.into_iter().map(Arc::new).collect()) + .unwrap_or_else(|| vec![identity_keypair.clone()]); let storage_keypair = keypair_of(&matches, "storage_keypair").unwrap_or_else(Keypair::new); @@ -808,16 +787,9 @@ pub fn main() { }; let vote_account = pubkey_of(&matches, "vote_account").unwrap_or_else(|| { - if matches.is_present("deprecated_voting_keypair") { - // Legacy v1.0.6 behaviour of using `--voting-keypair` as `--vote-account` - keypair_of(&matches, "deprecated_voting_keypair") - .unwrap() - .pubkey() - } else { - warn!("--vote-account not specified, validator will not vote"); - validator_config.voting_disabled = true; - Keypair::new().pubkey() - } + warn!("--vote-account not specified, validator will not vote"); + validator_config.voting_disabled = true; + Keypair::new().pubkey() }); let dynamic_port_range = @@ -1032,7 +1004,6 @@ pub fn main() { tcp_listeners, &udp_sockets, ); - if !no_genesis_fetch { let (cluster_info, gossip_exit_flag, gossip_service) = start_gossip_node( &identity_keypair, @@ -1109,13 +1080,22 @@ pub fn main() { if !validator_config.voting_disabled && !no_check_vote_account { check_vote_account( &rpc_client, - &vote_account, - &authorized_voter.pubkey(), &identity_keypair.pubkey(), - ) - } else { - Ok(()) + &vote_account, + &authorized_voter_keypairs.iter().map(|k| k.pubkey()).collect::>(), + + ).unwrap_or_else(|err| { + // Consider failures here to be more likely due to user error (eg, + // incorrect `solana-validator` command-line arguments) rather than the + // RPC node failing. + // + // Power users can always use the `--no-check-vote-account` option to + // bypass this check entirely + error!("{}", err); + exit(1); + }); } + Ok(()) }); if result.is_ok() { @@ -1153,7 +1133,7 @@ pub fn main() { &identity_keypair, &ledger_path, &vote_account, - &authorized_voter, + authorized_voter_keypairs, &Arc::new(storage_keypair), cluster_entrypoint.as_ref(), !skip_poh_verify,