From dad5c62df5f83c92037b1adbff7e058ece4ecc84 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 11 Dec 2019 22:04:54 -0800 Subject: [PATCH] Add uptime column to show-validators (#7441) automerge --- cli/src/cluster_query.rs | 57 +++++++++++++++++++--- cli/src/vote.rs | 44 ++++++++++------- client/src/rpc_request.rs | 4 ++ core/src/rpc.rs | 86 +++++++++++++++++++++++++++------ programs/vote/src/vote_state.rs | 47 +++--------------- 5 files changed, 158 insertions(+), 80 deletions(-) diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 7ef5c4bef6..ab14230f35 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -13,6 +13,7 @@ use solana_client::{rpc_client::RpcClient, rpc_request::RpcVoteAccountInfo}; use solana_sdk::{ clock::{self, Slot}, commitment_config::CommitmentConfig, + epoch_schedule::{Epoch, EpochSchedule}, hash::Hash, pubkey::Pubkey, signature::{Keypair, KeypairUtil}, @@ -261,6 +262,20 @@ fn new_spinner_progress_bar() -> ProgressBar { progress_bar } +/// Aggregate epoch credit stats and return (total credits, total slots, total epochs) +pub fn aggregate_epoch_credits( + epoch_credits: &[(Epoch, u64, u64)], + epoch_schedule: &EpochSchedule, +) -> (u64, u64, u64) { + epoch_credits + .iter() + .fold((0, 0, 0), |acc, (epoch, credits, prev_credits)| { + let credits_earned = credits - prev_credits; + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); + (acc.0 + credits_earned, acc.1 + slots_in_epoch, acc.2 + 1) + }) +} + pub fn process_catchup(rpc_client: &RpcClient, node_pubkey: &Pubkey) -> ProcessResult { let cluster_nodes = rpc_client.get_cluster_nodes()?; @@ -551,6 +566,7 @@ pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult { } pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) -> ProcessResult { + let epoch_schedule = rpc_client.get_epoch_schedule()?; let vote_accounts = rpc_client.get_vote_accounts()?; let total_active_stake = vote_accounts .current @@ -593,19 +609,21 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) println!( "{}", style(format!( - " {:<44} {:<44} {} {} {} {}", + " {:<44} {:<44} {} {} {} {:>7} {}", "Identity Pubkey", "Vote Account Pubkey", "Commission", "Last Vote", "Root Block", + "Uptime", "Active Stake", )) .bold() ); fn print_vote_account( - vote_account: &RpcVoteAccountInfo, + vote_account: RpcVoteAccountInfo, + epoch_schedule: &EpochSchedule, total_active_stake: f64, use_lamports_unit: bool, delinquent: bool, @@ -617,8 +635,20 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) format!("{}", v) } } + + fn uptime(epoch_credits: Vec<(Epoch, u64, u64)>, epoch_schedule: &EpochSchedule) -> String { + let (total_credits, total_slots, _) = + aggregate_epoch_credits(&epoch_credits, &epoch_schedule); + if total_slots > 0 { + let total_uptime = 100_f64 * total_credits as f64 / total_slots as f64; + format!("{:.2}%", total_uptime) + } else { + "-".into() + } + } + println!( - "{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>12}", + "{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>7} {}", if delinquent { WARNING.to_string() } else { @@ -629,6 +659,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) vote_account.commission, non_zero_or_dash(vote_account.last_vote), non_zero_or_dash(vote_account.root_slot), + uptime(vote_account.epoch_credits, epoch_schedule), if vote_account.activated_stake > 0 { format!( "{} ({:.2}%)", @@ -641,11 +672,23 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) ); } - for vote_account in vote_accounts.current.iter() { - print_vote_account(vote_account, total_active_stake, use_lamports_unit, false); + for vote_account in vote_accounts.current.into_iter() { + print_vote_account( + vote_account, + &epoch_schedule, + total_active_stake, + use_lamports_unit, + false, + ); } - for vote_account in vote_accounts.delinquent.iter() { - print_vote_account(vote_account, total_active_stake, use_lamports_unit, true); + for vote_account in vote_accounts.delinquent.into_iter() { + print_vote_account( + vote_account, + &epoch_schedule, + total_active_stake, + use_lamports_unit, + true, + ); } Ok("".to_string()) diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 267e43e377..135ce28a4a 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -1,6 +1,10 @@ -use crate::cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, +use crate::{ + cli::{ + build_balance_message, check_account_for_fee, check_unique_pubkeys, + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, + }, + cluster_query::aggregate_epoch_credits, }; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{input_parsers::*, input_validators::*}; @@ -400,33 +404,37 @@ pub fn process_uptime( if !vote_state.votes.is_empty() { println!("Uptime:"); - let epoch_credits_vec: Vec<(u64, u64, u64)> = vote_state.epoch_credits().copied().collect(); - - let epoch_credits = if let Some(x) = span { - epoch_credits_vec.iter().rev().take(x as usize) + let epoch_credits: Vec<(u64, u64, u64)> = if let Some(x) = span { + vote_state + .epoch_credits() + .iter() + .rev() + .take(x as usize) + .cloned() + .collect() } else { - epoch_credits_vec.iter().rev().take(epoch_credits_vec.len()) + vote_state.epoch_credits().iter().rev().cloned().collect() }; if aggregate { - let (credits_earned, slots_in_epoch, epochs): (u64, u64, u64) = - epoch_credits.fold((0, 0, 0), |acc, (epoch, credits, prev_credits)| { - let credits_earned = credits - prev_credits; - let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); - (acc.0 + credits_earned, acc.1 + slots_in_epoch, acc.2 + 1) - }); - let total_uptime = credits_earned as f64 / slots_in_epoch as f64; - println!("{:.2}% over {} epochs", total_uptime * 100_f64, epochs,); + let (total_credits, total_slots, epochs) = + aggregate_epoch_credits(&epoch_credits, &epoch_schedule); + if total_slots > 0 { + let total_uptime = 100_f64 * total_credits as f64 / total_slots as f64; + println!("{:.2}% over {} epochs", total_uptime, epochs); + } else { + println!("Insufficient voting history available"); + } } else { for (epoch, credits, prev_credits) in epoch_credits { let credits_earned = credits - prev_credits; - let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); let uptime = credits_earned as f64 / slots_in_epoch as f64; println!("- epoch: {} {:.2}% uptime", epoch, uptime * 100_f64,); } } if let Some(x) = span { - if x > epoch_credits_vec.len() as u64 { + if x > vote_state.epoch_credits().len() as u64 { println!("(span longer than available epochs)"); } } diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index be96d70d99..105fa61b52 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -97,6 +97,10 @@ pub struct RpcVoteAccountInfo { /// Whether this account is staked for the current epoch pub epoch_vote_account: bool, + /// History of how many credits earned by the end of each epoch + /// each tuple is (Epoch, credits, prev_credits) + pub epoch_credits: Vec<(Epoch, u64, u64)>, + /// Most recent slot voted on by this vote account (0 if no votes exist) pub last_vote: u64, diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 468dc4144c..8061abe8da 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -251,6 +251,7 @@ impl JsonRpcRequestProcessor { activated_stake: *activated_stake, commission: vote_state.commission, root_slot: vote_state.root_slot.unwrap_or(0), + epoch_credits: vote_state.epoch_credits().clone(), epoch_vote_account, last_vote, } @@ -1014,7 +1015,10 @@ pub mod tests { system_transaction, transaction::TransactionError, }; - use solana_vote_program::{vote_instruction, vote_state::VoteInit}; + use solana_vote_program::{ + vote_instruction, + vote_state::{Vote, VoteInit, MAX_LOCKOUT_HISTORY}, + }; use std::{ collections::HashMap, sync::atomic::{AtomicBool, Ordering}, @@ -1022,20 +1026,23 @@ pub mod tests { }; const TEST_MINT_LAMPORTS: u64 = 1_000_000; + const TEST_SLOTS_PER_EPOCH: u64 = 50; struct RpcHandler { io: MetaIoHandler, meta: Meta, bank: Arc, + bank_forks: Arc>, blockhash: Hash, alice: Keypair, leader_pubkey: Pubkey, + leader_vote_keypair: Keypair, block_commitment_cache: Arc>, confirmed_block_signatures: Vec, } fn start_rpc_handler_with_tx(pubkey: &Pubkey) -> RpcHandler { - let (bank_forks, alice) = new_bank_forks(); + let (bank_forks, alice, leader_vote_keypair) = new_bank_forks(); let bank = bank_forks.read().unwrap().working_bank(); let commitment_slot0 = BlockCommitment::new([8; MAX_LOCKOUT_HISTORY]); @@ -1077,7 +1084,7 @@ pub mod tests { let request_processor = Arc::new(RwLock::new(JsonRpcRequestProcessor::new( JsonRpcConfig::default(), - bank_forks, + bank_forks.clone(), block_commitment_cache.clone(), blocktree, StorageState::default(), @@ -1107,9 +1114,11 @@ pub mod tests { io, meta, bank, + bank_forks, blockhash, alice, leader_pubkey, + leader_vote_keypair, block_commitment_cache, confirmed_block_signatures, } @@ -1120,7 +1129,7 @@ pub mod tests { let bob_pubkey = Pubkey::new_rand(); let exit = Arc::new(AtomicBool::new(false)); let validator_exit = create_validator_exit(&exit); - let (bank_forks, alice) = new_bank_forks(); + let (bank_forks, alice, _) = new_bank_forks(); let bank = bank_forks.read().unwrap().working_bank(); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); let ledger_path = get_tmp_ledger_path!(); @@ -1630,20 +1639,23 @@ pub mod tests { ); } - fn new_bank_forks() -> (Arc>, Keypair) { + fn new_bank_forks() -> (Arc>, Keypair, Keypair) { let GenesisConfigInfo { mut genesis_config, mint_keypair, - .. + voting_keypair, } = create_genesis_config(TEST_MINT_LAMPORTS); genesis_config.rent.lamports_per_byte_year = 50; genesis_config.rent.exemption_threshold = 2.0; + genesis_config.epoch_schedule = + EpochSchedule::custom(TEST_SLOTS_PER_EPOCH, TEST_SLOTS_PER_EPOCH, false); let bank = Bank::new(&genesis_config); ( Arc::new(RwLock::new(BankForks::new(bank.slot(), bank))), mint_keypair, + voting_keypair, ) } @@ -1905,8 +1917,10 @@ pub mod tests { let RpcHandler { io, meta, - bank, + mut bank, + bank_forks, alice, + leader_vote_keypair, .. } = start_rpc_handler_with_tx(&Pubkey::new_rand()); @@ -1936,7 +1950,42 @@ pub mod tests { .expect("process transaction"); assert_eq!(bank.vote_accounts().len(), 2); - let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getVoteAccounts"}}"#); + // Advance bank to the next epoch + for _ in 0..TEST_SLOTS_PER_EPOCH { + bank.freeze(); + + let instruction = vote_instruction::vote( + &leader_vote_keypair.pubkey(), + &leader_vote_keypair.pubkey(), + Vote { + slots: vec![bank.slot()], + hash: bank.hash(), + timestamp: None, + }, + ); + + bank = bank_forks.write().unwrap().insert(Bank::new_from_parent( + &bank, + &Pubkey::default(), + bank.slot() + 1, + )); + + let transaction = Transaction::new_signed_with_payer( + vec![instruction], + Some(&alice.pubkey()), + &[&alice, &leader_vote_keypair], + bank.last_blockhash(), + ); + + bank.process_transaction(&transaction) + .expect("process transaction"); + } + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getVoteAccounts","params":{}}}"#, + json!([CommitmentConfig::recent()]) + ); + let res = io.handle_request_sync(&req, meta.clone()); let result: Value = serde_json::from_str(&res.expect("actual response")) .expect("actual response deserialization"); @@ -1944,12 +1993,19 @@ pub mod tests { let vote_account_status: RpcVoteAccountStatus = serde_json::from_value(result["result"].clone()).unwrap(); - // The bootstrap leader vote account will be delinquent as it has stake but has never - // voted. The vote account with no stake should not be present. - assert!(vote_account_status.current.is_empty()); - assert_eq!(vote_account_status.delinquent.len(), 1); - for vote_account_info in vote_account_status.delinquent { - assert_ne!(vote_account_info.activated_stake, 0); - } + // The vote account with no stake should not be present. + assert!(vote_account_status.delinquent.is_empty()); + + // The leader vote account should be active and have voting history. + assert_eq!(vote_account_status.current.len(), 1); + let leader_info = &vote_account_status.current[0]; + assert_eq!( + leader_info.vote_pubkey, + leader_vote_keypair.pubkey().to_string() + ); + assert_ne!(leader_info.activated_stake, 0); + // Subtract one because the last vote always carries over to the next epoch + let expected_credits = TEST_SLOTS_PER_EPOCH - MAX_LOCKOUT_HISTORY as u64 - 1; + assert_eq!(leader_info.epoch_credits, vec![(0, expected_credits, 0)]); } } diff --git a/programs/vote/src/vote_state.rs b/programs/vote/src/vote_state.rs index dd0b7b209e..e52a017103 100644 --- a/programs/vote/src/vote_state.rs +++ b/programs/vote/src/vote_state.rs @@ -334,8 +334,8 @@ impl VoteState { /// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where /// credits for each epoch is credits - prev_credits; while redundant this makes /// calculating rewards over partial epochs nice and simple - pub fn epoch_credits(&self) -> impl Iterator { - self.epoch_credits.iter() + pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { + &self.epoch_credits } fn pop_expired_votes(&mut self, slot: Slot) { @@ -1236,13 +1236,7 @@ mod tests { let mut vote_state = VoteState::default(); assert_eq!(vote_state.credits(), 0); - assert_eq!( - vote_state - .epoch_credits() - .cloned() - .collect::>(), - vec![] - ); + assert_eq!(vote_state.epoch_credits().clone(), vec![]); let mut expected = vec![]; let mut credits = 0; @@ -1260,46 +1254,19 @@ mod tests { } assert_eq!(vote_state.credits(), credits); - assert_eq!( - vote_state - .epoch_credits() - .cloned() - .collect::>(), - expected - ); + assert_eq!(vote_state.epoch_credits().clone(), expected); } #[test] fn test_vote_state_epoch0_no_credits() { let mut vote_state = VoteState::default(); - assert_eq!( - vote_state - .epoch_credits() - .cloned() - .collect::>() - .len(), - 0 - ); + assert_eq!(vote_state.epoch_credits().len(), 0); vote_state.increment_credits(1); - assert_eq!( - vote_state - .epoch_credits() - .cloned() - .collect::>() - .len(), - 0 - ); + assert_eq!(vote_state.epoch_credits().len(), 0); vote_state.increment_credits(2); - assert_eq!( - vote_state - .epoch_credits() - .cloned() - .collect::>() - .len(), - 1 - ); + assert_eq!(vote_state.epoch_credits().len(), 1); } #[test]