diff --git a/Cargo.lock b/Cargo.lock index 4c30f142a2..e743927e8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3717,6 +3717,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + [[package]] name = "semver-parser" version = "0.7.0" @@ -4279,6 +4285,7 @@ dependencies = [ "num-traits", "pretty-hex", "reqwest", + "semver 1.0.4", "serde", "serde_derive", "serde_json", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 177316f08c..fde590c8f3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,6 +26,7 @@ humantime = "2.0.1" num-traits = "0.2" pretty-hex = "0.2.1" reqwest = { version = "0.11.2", default-features = false, features = ["blocking", "rustls-tls", "json"] } +semver = "1.0.4" serde = "1.0.122" serde_derive = "1.0.103" serde_json = "1.0.56" diff --git a/cli/src/feature.rs b/cli/src/feature.rs index e5aa60f29f..2f49626e2f 100644 --- a/cli/src/feature.rs +++ b/cli/src/feature.rs @@ -18,7 +18,12 @@ use solana_sdk::{ pubkey::Pubkey, transaction::Transaction, }; -use std::{collections::HashMap, fmt, sync::Arc}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + fmt, + sync::Arc, +}; #[derive(Copy, Clone, Debug, PartialEq)] pub enum ForceActivation { @@ -222,7 +227,23 @@ pub fn process_feature_subcommand( } } -fn feature_set_stats(rpc_client: &RpcClient) -> Result, ClientError> { +#[derive(Debug, Default)] +struct WorkingFeatureSetStatsEntry { + stake: u64, + rpc_nodes_count: u32, + software_versions: HashSet>, +} +type WorkingFeatureSetStats = HashMap; + +#[derive(Debug, Default)] +struct FeatureSetStatsEntry { + stake_percent: f64, + rpc_nodes_percent: f32, + software_versions: Vec>, +} +type FeatureSetStats = HashMap; + +fn feature_set_stats(rpc_client: &RpcClient) -> Result { // Validator identity -> feature set let feature_sets = rpc_client .get_cluster_nodes()? @@ -232,6 +253,9 @@ fn feature_set_stats(rpc_client: &RpcClient) -> Result, contact_info.pubkey, contact_info.feature_set, contact_info.rpc.is_some(), + contact_info + .version + .and_then(|v| semver::Version::parse(&v).ok()), ) }) .collect::>(); @@ -253,33 +277,53 @@ fn feature_set_stats(rpc_client: &RpcClient) -> Result, }) .collect::>(); - let mut feature_set_stats: HashMap = HashMap::new(); + let mut feature_set_stats: WorkingFeatureSetStats = HashMap::new(); let mut total_rpc_nodes = 0; - for (node_id, feature_set, is_rpc) in feature_sets { + for (node_id, feature_set, is_rpc, version) in feature_sets { let feature_set = feature_set.unwrap_or(0); let feature_set_entry = feature_set_stats.entry(feature_set).or_default(); + feature_set_entry.software_versions.insert(version); + if let Some(vote_stake) = vote_stakes.get(&node_id) { - feature_set_entry.0 += *vote_stake; + feature_set_entry.stake += *vote_stake; } if is_rpc { - feature_set_entry.1 += 1; + feature_set_entry.rpc_nodes_count += 1; total_rpc_nodes += 1; } } Ok(feature_set_stats .into_iter() - .filter_map(|(feature_set, (active_stake, is_rpc))| { - let active_stake = active_stake as f64 * 100. / total_active_stake as f64; - let is_rpc = is_rpc as f32 * 100. / total_rpc_nodes as f32; - if active_stake >= 0.001 || is_rpc >= 0.001 { - Some((feature_set, (active_stake, is_rpc))) - } else { - None - } - }) + .filter_map( + |( + feature_set, + WorkingFeatureSetStatsEntry { + stake, + rpc_nodes_count, + software_versions, + }, + )| { + let stake_percent = (stake as f64 / total_active_stake as f64) * 100.; + let rpc_nodes_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.; + let mut software_versions = software_versions.into_iter().collect::>(); + software_versions.sort(); + if stake_percent >= 0.001 || rpc_nodes_percent >= 0.001 { + Some(( + feature_set, + FeatureSetStatsEntry { + stake_percent, + rpc_nodes_percent, + software_versions, + }, + )) + } else { + None + } + }, + ) .collect()) } @@ -291,7 +335,13 @@ fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result= 95., *rpc_percent >= 95.)) + .map( + |FeatureSetStatsEntry { + stake_percent, + rpc_nodes_percent, + .. + }| (*stake_percent >= 95., *rpc_nodes_percent >= 95.), + ) .unwrap_or((false, false)); if !stake_allowed && !rpc_allowed && !quiet { @@ -322,33 +372,96 @@ fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result>(); + feature_set_stats.sort_by(|l, r| { + match l.1.software_versions[0] + .cmp(&r.1.software_versions[0]) + .reverse() + { + Ordering::Equal => { + match l + .1 + .stake_percent + .partial_cmp(&r.1.stake_percent) + .unwrap() + .reverse() + { + Ordering::Equal => { + l.1.rpc_nodes_percent + .partial_cmp(&r.1.rpc_nodes_percent) + .unwrap() + .reverse() + } + o => o, + } + } + o => o, + } + }); + + let software_versions_title = "Software Version"; let feature_set_title = "Feature Set"; let stake_percent_title = "Stake"; let rpc_percent_title = "RPC"; let mut stats_output = Vec::new(); + let mut max_software_versions_len = software_versions_title.len(); let mut max_feature_set_len = feature_set_title.len(); let mut max_stake_percent_len = stake_percent_title.len(); let mut max_rpc_percent_len = rpc_percent_title.len(); - for (feature_set, (stake_percent, rpc_percent)) in feature_set_stats.iter() { - let me = *feature_set == my_feature_set; - let feature_set = if *feature_set == 0 { + for ( + feature_set, + FeatureSetStatsEntry { + stake_percent, + rpc_nodes_percent, + software_versions, + }, + ) in feature_set_stats.into_iter() + { + let me = feature_set == my_feature_set; + let feature_set = if feature_set == 0 { "unknown".to_string() } else { feature_set.to_string() }; let stake_percent = format!("{:.2}%", stake_percent); - let rpc_percent = format!("{:.2}%", rpc_percent); + let rpc_percent = format!("{:.2}%", rpc_nodes_percent); + + let mut has_unknown = false; + let mut software_versions = software_versions + .iter() + .filter_map(|v| { + if v.is_none() { + has_unknown = true; + } + v.as_ref() + }) + .map(ToString::to_string) + .collect::>(); + if has_unknown { + software_versions.push("unknown".to_string()); + } + let software_versions = software_versions.join(", "); + max_software_versions_len = max_software_versions_len.max(software_versions.len()); max_feature_set_len = max_feature_set_len.max(feature_set.len()); max_stake_percent_len = max_stake_percent_len.max(stake_percent.len()); max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len()); - stats_output.push((feature_set, stake_percent, rpc_percent, me)); + stats_output.push(( + software_versions, + feature_set, + stake_percent, + rpc_percent, + me, + )); } println!( "{}", style(format!( - "{1:<0$} {3:<2$} {5:<4$}", + "{1:<0$} {3:<2$} {5:<4$} {7:<6$}", + max_software_versions_len, + software_versions_title, max_feature_set_len, feature_set_title, max_stake_percent_len, @@ -358,9 +471,11 @@ fn feature_activation_allowed(rpc_client: &RpcClient, quiet: bool) -> Result0$} {3:>2$} {5:>4$} {6}", + "{1:<0$} {3:>2$} {5:>4$} {7:>6$} {8}", + max_software_versions_len, + software_versions, max_feature_set_len, feature_set, max_stake_percent_len,