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]