diff --git a/book/src/api-reference/jsonrpc-api.md b/book/src/api-reference/jsonrpc-api.md index 20c7ed299..31813d541 100644 --- a/book/src/api-reference/jsonrpc-api.md +++ b/book/src/api-reference/jsonrpc-api.md @@ -405,7 +405,7 @@ Returns the leader schedule for an epoch #### Parameters: -* `slot` - (optional) Fetch the leader schedule for the epoch that corresponds to the provided slot. If unspecified, the leader schedule for the current epoch is fetch +* `slot` - (optional) Fetch the leader schedule for the epoch that corresponds to the provided slot. If unspecified, the leader schedule for the current epoch is fetched * `object` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) #### Results: diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 5399a2df5..e979744f2 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -21,7 +21,7 @@ use solana_faucet::faucet::request_airdrop_transaction; use solana_faucet::faucet_mock::request_airdrop_transaction; use solana_sdk::{ bpf_loader, - clock::Slot, + clock::{Epoch, Slot}, commitment_config::CommitmentConfig, fee_calculator::FeeCalculator, hash::Hash, @@ -106,6 +106,10 @@ pub enum CliCommand { timeout: Duration, commitment_config: CommitmentConfig, }, + ShowBlockProduction { + epoch: Option, + slot_limit: Option, + }, ShowGossip, ShowValidators { use_lamports_unit: bool, @@ -336,6 +340,7 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result parse_get_slot(matches), ("get-transaction-count", Some(matches)) => parse_get_transaction_count(matches), ("ping", Some(matches)) => parse_cluster_ping(matches), + ("show-block-production", Some(matches)) => parse_show_block_production(matches), ("show-gossip", Some(_matches)) => Ok(CliCommandInfo { command: CliCommand::ShowGossip, require_keypair: false, @@ -1126,6 +1131,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { timeout, commitment_config, ), + CliCommand::ShowBlockProduction { epoch, slot_limit } => { + process_show_block_production(&rpc_client, *epoch, *slot_limit) + } CliCommand::ShowGossip => process_show_gossip(&rpc_client), CliCommand::ShowValidators { use_lamports_unit } => { process_show_validators(&rpc_client, *use_lamports_unit) diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index ab14230f3..f09103845 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -5,7 +5,7 @@ use crate::{ }, display::println_name_value, }; -use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; +use clap::{value_t, value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use console::{style, Emoji}; use indicatif::{ProgressBar, ProgressStyle}; use solana_clap_utils::{input_parsers::*, input_validators::*}; @@ -20,7 +20,7 @@ use solana_sdk::{ system_transaction, }; use std::{ - collections::VecDeque, + collections::{HashMap, VecDeque}, net::SocketAddr, thread::sleep, time::{Duration, Instant}, @@ -149,6 +149,22 @@ impl ClusterQuerySubCommands for App<'_, '_> { ), ), ) + .subcommand( + SubCommand::with_name("show-block-production") + .about("Show information about block production") + .arg( + Arg::with_name("epoch") + .long("epoch") + .takes_value(true) + .help("Epoch to show block production for [default: current epoch]"), + ) + .arg( + Arg::with_name("slot_limit") + .long("slot-limit") + .takes_value(true) + .help("Limit results to this many slots from the end of the epoch [default: full epoch]"), + ), + ) .subcommand( SubCommand::with_name("show-gossip") .about("Show the current gossip network nodes"), @@ -394,6 +410,138 @@ pub fn process_get_slot( Ok(slot.to_string()) } +pub fn parse_show_block_production(matches: &ArgMatches<'_>) -> Result { + let epoch = value_t!(matches, "epoch", Epoch).ok(); + let slot_limit = value_t!(matches, "slot_limit", u64).ok(); + + Ok(CliCommandInfo { + command: CliCommand::ShowBlockProduction { epoch, slot_limit }, + require_keypair: false, + }) +} + +pub fn process_show_block_production( + rpc_client: &RpcClient, + epoch: Option, + slot_limit: Option, +) -> ProcessResult { + let epoch_schedule = rpc_client.get_epoch_schedule()?; + let epoch_info = rpc_client.get_epoch_info_with_commitment(CommitmentConfig::max())?; + + let epoch = epoch.unwrap_or(epoch_info.epoch); + + if epoch > epoch_info.epoch { + return Err(format!("Epoch {} is in the future", epoch).into()); + } + + let end_slot = std::cmp::min( + epoch_info.absolute_slot, + epoch_schedule.get_last_slot_in_epoch(epoch), + ); + let start_slot = { + let start_slot = epoch_schedule.get_first_slot_in_epoch(epoch); + std::cmp::max( + end_slot.saturating_sub(slot_limit.unwrap_or(start_slot)), + start_slot, + ) + }; + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message("Connecting..."); + progress_bar.set_message(&format!("Fetching leader schedule for epoch {}...", epoch)); + + let leader_schedule = rpc_client + .get_leader_schedule_with_commitment(Some(start_slot), CommitmentConfig::max())?; + + if leader_schedule.is_none() { + return Err(format!("Unable to fetch leader schedule for slot {}", start_slot).into()); + } + let leader_schedule = leader_schedule.unwrap(); + + progress_bar.set_message(&format!( + "Fetching confirmed blocks between slots {} and {}...", + start_slot, end_slot + )); + let confirmed_blocks = rpc_client.get_confirmed_blocks(start_slot, Some(end_slot))?; + + let total_slots = (end_slot - start_slot + 1) as usize; + let total_blocks = confirmed_blocks.len(); + let total_slots_missed = total_slots - total_blocks; + let mut leader_slot_count = HashMap::new(); + let mut leader_missed_slots = HashMap::new(); + + for slot_index in 0..total_slots { + let leader = { + let mut leader = None; + for (pubkey, leader_slots) in leader_schedule.iter() { + if leader_slots.contains(&slot_index) { + leader = Some(pubkey); + break; + } + } + leader.unwrap() + }; + + let slot = start_slot + slot_index as u64; + let remaining_slots = total_slots - slot_index; + progress_bar.set_message(&format!( + "Checking slot {} ({:.}% done, {} slots remaining)...", + slot, + 100 * slot_index / total_slots, + remaining_slots, + )); + let slot_count = leader_slot_count.entry(leader).or_insert(0); + *slot_count += 1; + + let missed_slots = leader_missed_slots.entry(leader).or_insert(0); + + if !confirmed_blocks.contains(&slot) { + *missed_slots += 1; + } + } + + progress_bar.finish_and_clear(); + println!( + "\n{}", + style(format!( + " {:<44} {:>15} {:>15} {:>15} {:>23}", + "Identity Pubkey", + "Leader Slots", + "Blocks Produced", + "Missed Slots", + "Missed Block Percentage", + )) + .bold() + ); + + for (leader, leader_slots) in leader_slot_count.iter() { + let missed_slots = leader_missed_slots.get(leader).unwrap(); + let blocks_produced = leader_slots - missed_slots; + println!( + " {:<44} {:>15} {:>15} {:>15} {:>22.2}%", + leader, + leader_slots, + blocks_produced, + missed_slots, + *missed_slots as f64 / *leader_slots as f64 * 100. + ); + } + + println!( + "\n {:<44} {:>15} {:>15} {:>15} {:>22.2}%", + format!("Epoch {} total:", epoch), + total_slots, + total_blocks, + total_slots_missed, + total_slots_missed as f64 / total_slots as f64 * 100. + ); + println!( + " (using data from {} slots: {} to {})", + total_slots, start_slot, end_slot + ); + Ok("".to_string()) +} + pub fn process_get_transaction_count( rpc_client: &RpcClient, commitment_config: &CommitmentConfig, diff --git a/sdk/src/commitment_config.rs b/sdk/src/commitment_config.rs index 74eaee8f3..b0f75096f 100644 --- a/sdk/src/commitment_config.rs +++ b/sdk/src/commitment_config.rs @@ -19,6 +19,12 @@ impl CommitmentConfig { } } + pub fn max() -> Self { + Self { + commitment: CommitmentLevel::Max, + } + } + pub fn ok(&self) -> Option { if self == &Self::default() { None