Add uptime column to show-validators (#7441)

automerge
This commit is contained in:
Justin Starry 2019-12-11 22:04:54 -08:00 committed by Grimes
parent a1ab81a896
commit dad5c62df5
5 changed files with 158 additions and 80 deletions

View File

@ -13,6 +13,7 @@ use solana_client::{rpc_client::RpcClient, rpc_request::RpcVoteAccountInfo};
use solana_sdk::{ use solana_sdk::{
clock::{self, Slot}, clock::{self, Slot},
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
epoch_schedule::{Epoch, EpochSchedule},
hash::Hash, hash::Hash,
pubkey::Pubkey, pubkey::Pubkey,
signature::{Keypair, KeypairUtil}, signature::{Keypair, KeypairUtil},
@ -261,6 +262,20 @@ fn new_spinner_progress_bar() -> ProgressBar {
progress_bar 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 { pub fn process_catchup(rpc_client: &RpcClient, node_pubkey: &Pubkey) -> ProcessResult {
let cluster_nodes = rpc_client.get_cluster_nodes()?; 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 { 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 vote_accounts = rpc_client.get_vote_accounts()?;
let total_active_stake = vote_accounts let total_active_stake = vote_accounts
.current .current
@ -593,19 +609,21 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
println!( println!(
"{}", "{}",
style(format!( style(format!(
" {:<44} {:<44} {} {} {} {}", " {:<44} {:<44} {} {} {} {:>7} {}",
"Identity Pubkey", "Identity Pubkey",
"Vote Account Pubkey", "Vote Account Pubkey",
"Commission", "Commission",
"Last Vote", "Last Vote",
"Root Block", "Root Block",
"Uptime",
"Active Stake", "Active Stake",
)) ))
.bold() .bold()
); );
fn print_vote_account( fn print_vote_account(
vote_account: &RpcVoteAccountInfo, vote_account: RpcVoteAccountInfo,
epoch_schedule: &EpochSchedule,
total_active_stake: f64, total_active_stake: f64,
use_lamports_unit: bool, use_lamports_unit: bool,
delinquent: bool, delinquent: bool,
@ -617,8 +635,20 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
format!("{}", v) 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!( println!(
"{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>12}", "{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>7} {}",
if delinquent { if delinquent {
WARNING.to_string() WARNING.to_string()
} else { } else {
@ -629,6 +659,7 @@ pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool)
vote_account.commission, vote_account.commission,
non_zero_or_dash(vote_account.last_vote), non_zero_or_dash(vote_account.last_vote),
non_zero_or_dash(vote_account.root_slot), non_zero_or_dash(vote_account.root_slot),
uptime(vote_account.epoch_credits, epoch_schedule),
if vote_account.activated_stake > 0 { if vote_account.activated_stake > 0 {
format!( format!(
"{} ({:.2}%)", "{} ({:.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() { for vote_account in vote_accounts.current.into_iter() {
print_vote_account(vote_account, total_active_stake, use_lamports_unit, false); print_vote_account(
vote_account,
&epoch_schedule,
total_active_stake,
use_lamports_unit,
false,
);
} }
for vote_account in vote_accounts.delinquent.iter() { for vote_account in vote_accounts.delinquent.into_iter() {
print_vote_account(vote_account, total_active_stake, use_lamports_unit, true); print_vote_account(
vote_account,
&epoch_schedule,
total_active_stake,
use_lamports_unit,
true,
);
} }
Ok("".to_string()) Ok("".to_string())

View File

@ -1,6 +1,10 @@
use crate::cli::{ use crate::{
build_balance_message, check_account_for_fee, check_unique_pubkeys, cli::{
log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, 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 clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand};
use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_clap_utils::{input_parsers::*, input_validators::*};
@ -400,33 +404,37 @@ pub fn process_uptime(
if !vote_state.votes.is_empty() { if !vote_state.votes.is_empty() {
println!("Uptime:"); println!("Uptime:");
let epoch_credits_vec: Vec<(u64, u64, u64)> = vote_state.epoch_credits().copied().collect(); let epoch_credits: Vec<(u64, u64, u64)> = if let Some(x) = span {
vote_state
let epoch_credits = if let Some(x) = span { .epoch_credits()
epoch_credits_vec.iter().rev().take(x as usize) .iter()
.rev()
.take(x as usize)
.cloned()
.collect()
} else { } else {
epoch_credits_vec.iter().rev().take(epoch_credits_vec.len()) vote_state.epoch_credits().iter().rev().cloned().collect()
}; };
if aggregate { if aggregate {
let (credits_earned, slots_in_epoch, epochs): (u64, u64, u64) = let (total_credits, total_slots, epochs) =
epoch_credits.fold((0, 0, 0), |acc, (epoch, credits, prev_credits)| { aggregate_epoch_credits(&epoch_credits, &epoch_schedule);
let credits_earned = credits - prev_credits; if total_slots > 0 {
let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); let total_uptime = 100_f64 * total_credits as f64 / total_slots as f64;
(acc.0 + credits_earned, acc.1 + slots_in_epoch, acc.2 + 1) println!("{:.2}% over {} epochs", total_uptime, epochs);
}); } else {
let total_uptime = credits_earned as f64 / slots_in_epoch as f64; println!("Insufficient voting history available");
println!("{:.2}% over {} epochs", total_uptime * 100_f64, epochs,); }
} else { } else {
for (epoch, credits, prev_credits) in epoch_credits { for (epoch, credits, prev_credits) in epoch_credits {
let credits_earned = credits - prev_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; let uptime = credits_earned as f64 / slots_in_epoch as f64;
println!("- epoch: {} {:.2}% uptime", epoch, uptime * 100_f64,); println!("- epoch: {} {:.2}% uptime", epoch, uptime * 100_f64,);
} }
} }
if let Some(x) = span { 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)"); println!("(span longer than available epochs)");
} }
} }

View File

@ -97,6 +97,10 @@ pub struct RpcVoteAccountInfo {
/// Whether this account is staked for the current epoch /// Whether this account is staked for the current epoch
pub epoch_vote_account: bool, 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) /// Most recent slot voted on by this vote account (0 if no votes exist)
pub last_vote: u64, pub last_vote: u64,

View File

@ -251,6 +251,7 @@ impl JsonRpcRequestProcessor {
activated_stake: *activated_stake, activated_stake: *activated_stake,
commission: vote_state.commission, commission: vote_state.commission,
root_slot: vote_state.root_slot.unwrap_or(0), root_slot: vote_state.root_slot.unwrap_or(0),
epoch_credits: vote_state.epoch_credits().clone(),
epoch_vote_account, epoch_vote_account,
last_vote, last_vote,
} }
@ -1014,7 +1015,10 @@ pub mod tests {
system_transaction, system_transaction,
transaction::TransactionError, 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::{ use std::{
collections::HashMap, collections::HashMap,
sync::atomic::{AtomicBool, Ordering}, sync::atomic::{AtomicBool, Ordering},
@ -1022,20 +1026,23 @@ pub mod tests {
}; };
const TEST_MINT_LAMPORTS: u64 = 1_000_000; const TEST_MINT_LAMPORTS: u64 = 1_000_000;
const TEST_SLOTS_PER_EPOCH: u64 = 50;
struct RpcHandler { struct RpcHandler {
io: MetaIoHandler<Meta>, io: MetaIoHandler<Meta>,
meta: Meta, meta: Meta,
bank: Arc<Bank>, bank: Arc<Bank>,
bank_forks: Arc<RwLock<BankForks>>,
blockhash: Hash, blockhash: Hash,
alice: Keypair, alice: Keypair,
leader_pubkey: Pubkey, leader_pubkey: Pubkey,
leader_vote_keypair: Keypair,
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>, block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
confirmed_block_signatures: Vec<Signature>, confirmed_block_signatures: Vec<Signature>,
} }
fn start_rpc_handler_with_tx(pubkey: &Pubkey) -> RpcHandler { 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 bank = bank_forks.read().unwrap().working_bank();
let commitment_slot0 = BlockCommitment::new([8; MAX_LOCKOUT_HISTORY]); 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( let request_processor = Arc::new(RwLock::new(JsonRpcRequestProcessor::new(
JsonRpcConfig::default(), JsonRpcConfig::default(),
bank_forks, bank_forks.clone(),
block_commitment_cache.clone(), block_commitment_cache.clone(),
blocktree, blocktree,
StorageState::default(), StorageState::default(),
@ -1107,9 +1114,11 @@ pub mod tests {
io, io,
meta, meta,
bank, bank,
bank_forks,
blockhash, blockhash,
alice, alice,
leader_pubkey, leader_pubkey,
leader_vote_keypair,
block_commitment_cache, block_commitment_cache,
confirmed_block_signatures, confirmed_block_signatures,
} }
@ -1120,7 +1129,7 @@ pub mod tests {
let bob_pubkey = Pubkey::new_rand(); let bob_pubkey = Pubkey::new_rand();
let exit = Arc::new(AtomicBool::new(false)); let exit = Arc::new(AtomicBool::new(false));
let validator_exit = create_validator_exit(&exit); 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 bank = bank_forks.read().unwrap().working_bank();
let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default()));
let ledger_path = get_tmp_ledger_path!(); let ledger_path = get_tmp_ledger_path!();
@ -1630,20 +1639,23 @@ pub mod tests {
); );
} }
fn new_bank_forks() -> (Arc<RwLock<BankForks>>, Keypair) { fn new_bank_forks() -> (Arc<RwLock<BankForks>>, Keypair, Keypair) {
let GenesisConfigInfo { let GenesisConfigInfo {
mut genesis_config, mut genesis_config,
mint_keypair, mint_keypair,
.. voting_keypair,
} = create_genesis_config(TEST_MINT_LAMPORTS); } = create_genesis_config(TEST_MINT_LAMPORTS);
genesis_config.rent.lamports_per_byte_year = 50; genesis_config.rent.lamports_per_byte_year = 50;
genesis_config.rent.exemption_threshold = 2.0; 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); let bank = Bank::new(&genesis_config);
( (
Arc::new(RwLock::new(BankForks::new(bank.slot(), bank))), Arc::new(RwLock::new(BankForks::new(bank.slot(), bank))),
mint_keypair, mint_keypair,
voting_keypair,
) )
} }
@ -1905,8 +1917,10 @@ pub mod tests {
let RpcHandler { let RpcHandler {
io, io,
meta, meta,
bank, mut bank,
bank_forks,
alice, alice,
leader_vote_keypair,
.. ..
} = start_rpc_handler_with_tx(&Pubkey::new_rand()); } = start_rpc_handler_with_tx(&Pubkey::new_rand());
@ -1936,7 +1950,42 @@ pub mod tests {
.expect("process transaction"); .expect("process transaction");
assert_eq!(bank.vote_accounts().len(), 2); 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 res = io.handle_request_sync(&req, meta.clone());
let result: Value = serde_json::from_str(&res.expect("actual response")) let result: Value = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization"); .expect("actual response deserialization");
@ -1944,12 +1993,19 @@ pub mod tests {
let vote_account_status: RpcVoteAccountStatus = let vote_account_status: RpcVoteAccountStatus =
serde_json::from_value(result["result"].clone()).unwrap(); serde_json::from_value(result["result"].clone()).unwrap();
// The bootstrap leader vote account will be delinquent as it has stake but has never // The vote account with no stake should not be present.
// voted. The vote account with no stake should not be present. assert!(vote_account_status.delinquent.is_empty());
assert!(vote_account_status.current.is_empty());
assert_eq!(vote_account_status.delinquent.len(), 1); // The leader vote account should be active and have voting history.
for vote_account_info in vote_account_status.delinquent { assert_eq!(vote_account_status.current.len(), 1);
assert_ne!(vote_account_info.activated_stake, 0); 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)]);
} }
} }

View File

@ -334,8 +334,8 @@ impl VoteState {
/// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where /// 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 /// credits for each epoch is credits - prev_credits; while redundant this makes
/// calculating rewards over partial epochs nice and simple /// calculating rewards over partial epochs nice and simple
pub fn epoch_credits(&self) -> impl Iterator<Item = &(Epoch, u64, u64)> { pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> {
self.epoch_credits.iter() &self.epoch_credits
} }
fn pop_expired_votes(&mut self, slot: Slot) { fn pop_expired_votes(&mut self, slot: Slot) {
@ -1236,13 +1236,7 @@ mod tests {
let mut vote_state = VoteState::default(); let mut vote_state = VoteState::default();
assert_eq!(vote_state.credits(), 0); assert_eq!(vote_state.credits(), 0);
assert_eq!( assert_eq!(vote_state.epoch_credits().clone(), vec![]);
vote_state
.epoch_credits()
.cloned()
.collect::<Vec<(Epoch, u64, u64)>>(),
vec![]
);
let mut expected = vec![]; let mut expected = vec![];
let mut credits = 0; let mut credits = 0;
@ -1260,46 +1254,19 @@ mod tests {
} }
assert_eq!(vote_state.credits(), credits); assert_eq!(vote_state.credits(), credits);
assert_eq!( assert_eq!(vote_state.epoch_credits().clone(), expected);
vote_state
.epoch_credits()
.cloned()
.collect::<Vec<(Epoch, u64, u64)>>(),
expected
);
} }
#[test] #[test]
fn test_vote_state_epoch0_no_credits() { fn test_vote_state_epoch0_no_credits() {
let mut vote_state = VoteState::default(); let mut vote_state = VoteState::default();
assert_eq!( assert_eq!(vote_state.epoch_credits().len(), 0);
vote_state
.epoch_credits()
.cloned()
.collect::<Vec<(Epoch, u64, u64)>>()
.len(),
0
);
vote_state.increment_credits(1); vote_state.increment_credits(1);
assert_eq!( assert_eq!(vote_state.epoch_credits().len(), 0);
vote_state
.epoch_credits()
.cloned()
.collect::<Vec<(Epoch, u64, u64)>>()
.len(),
0
);
vote_state.increment_credits(2); vote_state.increment_credits(2);
assert_eq!( assert_eq!(vote_state.epoch_credits().len(), 1);
vote_state
.epoch_credits()
.cloned()
.collect::<Vec<(Epoch, u64, u64)>>()
.len(),
1
);
} }
#[test] #[test]