917 lines
30 KiB
Rust
917 lines
30 KiB
Rust
use {
|
|
crate::{
|
|
cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
|
|
spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
|
|
},
|
|
clap::{App, AppSettings, Arg, ArgMatches, SubCommand},
|
|
console::style,
|
|
serde::{Deserialize, Serialize},
|
|
solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*},
|
|
solana_cli_output::{cli_version::CliVersion, QuietDisplay, VerboseDisplay},
|
|
solana_client::{
|
|
client_error::ClientError, rpc_client::RpcClient, rpc_request::MAX_MULTIPLE_ACCOUNTS,
|
|
},
|
|
solana_remote_wallet::remote_wallet::RemoteWalletManager,
|
|
solana_sdk::{
|
|
account::Account,
|
|
clock::Slot,
|
|
epoch_schedule::EpochSchedule,
|
|
feature::{self, Feature},
|
|
feature_set::FEATURE_NAMES,
|
|
message::Message,
|
|
pubkey::Pubkey,
|
|
transaction::Transaction,
|
|
},
|
|
std::{cmp::Ordering, collections::HashMap, fmt, str::FromStr, sync::Arc},
|
|
};
|
|
|
|
const DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS: Slot = 15_000_000; // ~90days
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub enum ForceActivation {
|
|
No,
|
|
Almost,
|
|
Yes,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum FeatureCliCommand {
|
|
Status {
|
|
features: Vec<Pubkey>,
|
|
display_all: bool,
|
|
},
|
|
Activate {
|
|
feature: Pubkey,
|
|
force: ForceActivation,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase", tag = "status", content = "sinceSlot")]
|
|
pub enum CliFeatureStatus {
|
|
Inactive,
|
|
Pending,
|
|
Active(Slot),
|
|
}
|
|
|
|
impl PartialOrd for CliFeatureStatus {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for CliFeatureStatus {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
match (self, other) {
|
|
(Self::Inactive, Self::Inactive) => Ordering::Equal,
|
|
(Self::Inactive, _) => Ordering::Greater,
|
|
(_, Self::Inactive) => Ordering::Less,
|
|
(Self::Pending, Self::Pending) => Ordering::Equal,
|
|
(Self::Pending, _) => Ordering::Greater,
|
|
(_, Self::Pending) => Ordering::Less,
|
|
(Self::Active(self_active_slot), Self::Active(other_active_slot)) => {
|
|
self_active_slot.cmp(other_active_slot)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliFeature {
|
|
pub id: String,
|
|
pub description: String,
|
|
#[serde(flatten)]
|
|
pub status: CliFeatureStatus,
|
|
}
|
|
|
|
impl PartialOrd for CliFeature {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for CliFeature {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
match self.status.cmp(&other.status) {
|
|
Ordering::Equal => self.id.cmp(&other.id),
|
|
ordering => ordering,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliFeatures {
|
|
pub features: Vec<CliFeature>,
|
|
#[serde(skip)]
|
|
pub epoch_schedule: EpochSchedule,
|
|
#[serde(skip)]
|
|
pub current_slot: Slot,
|
|
pub feature_activation_allowed: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub cluster_feature_sets: Option<CliClusterFeatureSets>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub cluster_software_versions: Option<CliClusterSoftwareVersions>,
|
|
#[serde(skip)]
|
|
pub inactive: bool,
|
|
}
|
|
|
|
impl fmt::Display for CliFeatures {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
if !self.features.is_empty() {
|
|
writeln!(
|
|
f,
|
|
"{}",
|
|
style(format!(
|
|
"{:<44} | {:<23} | {} | {}",
|
|
"Feature", "Status", "Activation Slot", "Description"
|
|
))
|
|
.bold()
|
|
)?;
|
|
}
|
|
for feature in &self.features {
|
|
writeln!(
|
|
f,
|
|
"{:<44} | {:<23} | {:<15} | {}",
|
|
feature.id,
|
|
match feature.status {
|
|
CliFeatureStatus::Inactive => style("inactive".to_string()).red(),
|
|
CliFeatureStatus::Pending => {
|
|
let current_epoch = self.epoch_schedule.get_epoch(self.current_slot);
|
|
style(format!("pending until epoch {}", current_epoch + 1)).yellow()
|
|
}
|
|
CliFeatureStatus::Active(activation_slot) => {
|
|
let activation_epoch = self.epoch_schedule.get_epoch(activation_slot);
|
|
style(format!("active since epoch {}", activation_epoch)).green()
|
|
}
|
|
},
|
|
match feature.status {
|
|
CliFeatureStatus::Active(activation_slot) => activation_slot.to_string(),
|
|
_ => "NA".to_string(),
|
|
},
|
|
feature.description,
|
|
)?;
|
|
}
|
|
|
|
if let Some(software_versions) = &self.cluster_software_versions {
|
|
write!(f, "{}", software_versions)?;
|
|
}
|
|
|
|
if let Some(feature_sets) = &self.cluster_feature_sets {
|
|
write!(f, "{}", feature_sets)?;
|
|
}
|
|
|
|
if self.inactive && !self.feature_activation_allowed {
|
|
writeln!(
|
|
f,
|
|
"{}",
|
|
style("\nFeature activation is not allowed at this time")
|
|
.bold()
|
|
.red()
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl QuietDisplay for CliFeatures {}
|
|
impl VerboseDisplay for CliFeatures {}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliClusterFeatureSets {
|
|
pub tool_feature_set: u32,
|
|
pub feature_sets: Vec<CliFeatureSetStats>,
|
|
#[serde(skip)]
|
|
pub stake_allowed: bool,
|
|
#[serde(skip)]
|
|
pub rpc_allowed: bool,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliClusterSoftwareVersions {
|
|
tool_software_version: CliVersion,
|
|
software_versions: Vec<CliSoftwareVersionStats>,
|
|
}
|
|
|
|
impl fmt::Display for CliClusterSoftwareVersions {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let software_version_title = "Software Version";
|
|
let stake_percent_title = "Stake";
|
|
let rpc_percent_title = "RPC";
|
|
let mut max_software_version_len = software_version_title.len();
|
|
let mut max_stake_percent_len = stake_percent_title.len();
|
|
let mut max_rpc_percent_len = rpc_percent_title.len();
|
|
|
|
let software_versions: Vec<_> = self
|
|
.software_versions
|
|
.iter()
|
|
.map(|software_version_stats| {
|
|
let stake_percent = format!("{:.2}%", software_version_stats.stake_percent);
|
|
let rpc_percent = format!("{:.2}%", software_version_stats.rpc_percent);
|
|
let software_version = software_version_stats.software_version.to_string();
|
|
|
|
max_software_version_len = max_software_version_len.max(software_version.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());
|
|
|
|
(software_version, stake_percent, rpc_percent)
|
|
})
|
|
.collect();
|
|
|
|
writeln!(
|
|
f,
|
|
"\n\n{}",
|
|
style(format!(
|
|
"Tool Software Version: {}",
|
|
self.tool_software_version
|
|
))
|
|
.bold()
|
|
)?;
|
|
writeln!(
|
|
f,
|
|
"{}",
|
|
style(format!(
|
|
"{1:<0$} {3:>2$} {5:>4$}",
|
|
max_software_version_len,
|
|
software_version_title,
|
|
max_stake_percent_len,
|
|
stake_percent_title,
|
|
max_rpc_percent_len,
|
|
rpc_percent_title,
|
|
))
|
|
.bold(),
|
|
)?;
|
|
for (software_version, stake_percent, rpc_percent) in software_versions {
|
|
let me = self.tool_software_version.to_string() == software_version;
|
|
writeln!(
|
|
f,
|
|
"{1:<0$} {3:>2$} {5:>4$} {6}",
|
|
max_software_version_len,
|
|
software_version,
|
|
max_stake_percent_len,
|
|
stake_percent,
|
|
max_rpc_percent_len,
|
|
rpc_percent,
|
|
if me { "<-- me" } else { "" },
|
|
)?;
|
|
}
|
|
writeln!(f)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for CliClusterFeatureSets {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut tool_feature_set_matches_cluster = false;
|
|
|
|
let software_versions_title = "Software Version";
|
|
let feature_set_title = "Feature Set";
|
|
let stake_percent_title = "Stake";
|
|
let rpc_percent_title = "RPC";
|
|
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();
|
|
|
|
let feature_sets: Vec<_> = self
|
|
.feature_sets
|
|
.iter()
|
|
.map(|feature_set_info| {
|
|
let me = if self.tool_feature_set == feature_set_info.feature_set {
|
|
tool_feature_set_matches_cluster = true;
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
let software_versions: Vec<_> = feature_set_info
|
|
.software_versions
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect();
|
|
let software_versions = software_versions.join(", ");
|
|
let feature_set = if feature_set_info.feature_set == 0 {
|
|
"unknown".to_string()
|
|
} else {
|
|
feature_set_info.feature_set.to_string()
|
|
};
|
|
let stake_percent = format!("{:.2}%", feature_set_info.stake_percent);
|
|
let rpc_percent = format!("{:.2}%", feature_set_info.rpc_percent);
|
|
|
|
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());
|
|
|
|
(
|
|
software_versions,
|
|
feature_set,
|
|
stake_percent,
|
|
rpc_percent,
|
|
me,
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
if !tool_feature_set_matches_cluster {
|
|
writeln!(
|
|
f,
|
|
"\n{}",
|
|
style("To activate features the tool and cluster feature sets must match, select a tool version that matches the cluster")
|
|
.bold())?;
|
|
} else {
|
|
if !self.stake_allowed {
|
|
write!(
|
|
f,
|
|
"\n{}",
|
|
style("To activate features the stake must be >= 95%")
|
|
.bold()
|
|
.red()
|
|
)?;
|
|
}
|
|
if !self.rpc_allowed {
|
|
write!(
|
|
f,
|
|
"\n{}",
|
|
style("To activate features the RPC nodes must be >= 95%")
|
|
.bold()
|
|
.red()
|
|
)?;
|
|
}
|
|
}
|
|
writeln!(
|
|
f,
|
|
"\n\n{}",
|
|
style(format!("Tool Feature Set: {}", self.tool_feature_set)).bold()
|
|
)?;
|
|
writeln!(
|
|
f,
|
|
"{}",
|
|
style(format!(
|
|
"{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,
|
|
stake_percent_title,
|
|
max_rpc_percent_len,
|
|
rpc_percent_title,
|
|
))
|
|
.bold(),
|
|
)?;
|
|
for (software_versions, feature_set, stake_percent, rpc_percent, me) in feature_sets {
|
|
writeln!(
|
|
f,
|
|
"{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,
|
|
stake_percent,
|
|
max_rpc_percent_len,
|
|
rpc_percent,
|
|
if me { "<-- me" } else { "" },
|
|
)?;
|
|
}
|
|
writeln!(f)
|
|
}
|
|
}
|
|
|
|
impl QuietDisplay for CliClusterFeatureSets {}
|
|
impl VerboseDisplay for CliClusterFeatureSets {}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliFeatureSetStats {
|
|
software_versions: Vec<CliVersion>,
|
|
feature_set: u32,
|
|
stake_percent: f64,
|
|
rpc_percent: f32,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CliSoftwareVersionStats {
|
|
software_version: CliVersion,
|
|
stake_percent: f64,
|
|
rpc_percent: f32,
|
|
}
|
|
|
|
pub trait FeatureSubCommands {
|
|
fn feature_subcommands(self) -> Self;
|
|
}
|
|
|
|
impl FeatureSubCommands for App<'_, '_> {
|
|
fn feature_subcommands(self) -> Self {
|
|
self.subcommand(
|
|
SubCommand::with_name("feature")
|
|
.about("Runtime feature management")
|
|
.setting(AppSettings::SubcommandRequiredElseHelp)
|
|
.subcommand(
|
|
SubCommand::with_name("status")
|
|
.about("Query runtime feature status")
|
|
.arg(
|
|
Arg::with_name("features")
|
|
.value_name("ADDRESS")
|
|
.validator(is_valid_pubkey)
|
|
.index(1)
|
|
.multiple(true)
|
|
.help("Feature status to query [default: all known features]"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("display_all")
|
|
.long("display-all")
|
|
.help("display all features regardless of age"),
|
|
),
|
|
)
|
|
.subcommand(
|
|
SubCommand::with_name("activate")
|
|
.about("Activate a runtime feature")
|
|
.arg(
|
|
Arg::with_name("feature")
|
|
.value_name("FEATURE_KEYPAIR")
|
|
.validator(is_valid_signer)
|
|
.index(1)
|
|
.required(true)
|
|
.help("The signer for the feature to activate"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("force")
|
|
.long("yolo")
|
|
.hidden(true)
|
|
.multiple(true)
|
|
.help("Override activation sanity checks. Don't use this flag"),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn known_feature(feature: &Pubkey) -> Result<(), CliError> {
|
|
if FEATURE_NAMES.contains_key(feature) {
|
|
Ok(())
|
|
} else {
|
|
Err(CliError::BadParameter(format!(
|
|
"Unknown feature: {}",
|
|
feature
|
|
)))
|
|
}
|
|
}
|
|
|
|
pub fn parse_feature_subcommand(
|
|
matches: &ArgMatches<'_>,
|
|
default_signer: &DefaultSigner,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
) -> Result<CliCommandInfo, CliError> {
|
|
let response = match matches.subcommand() {
|
|
("activate", Some(matches)) => {
|
|
let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
|
|
let mut signers = vec![default_signer.signer_from_path(matches, wallet_manager)?];
|
|
|
|
let force = match matches.occurrences_of("force") {
|
|
2 => ForceActivation::Yes,
|
|
1 => ForceActivation::Almost,
|
|
_ => ForceActivation::No,
|
|
};
|
|
|
|
signers.push(feature_signer.unwrap());
|
|
let feature = feature.unwrap();
|
|
|
|
known_feature(&feature)?;
|
|
|
|
CliCommandInfo {
|
|
command: CliCommand::Feature(FeatureCliCommand::Activate { feature, force }),
|
|
signers,
|
|
}
|
|
}
|
|
("status", Some(matches)) => {
|
|
let mut features = if let Some(features) = pubkeys_of(matches, "features") {
|
|
for feature in &features {
|
|
known_feature(feature)?;
|
|
}
|
|
features
|
|
} else {
|
|
FEATURE_NAMES.keys().cloned().collect()
|
|
};
|
|
let display_all =
|
|
matches.is_present("display_all") || features.len() < FEATURE_NAMES.len();
|
|
features.sort();
|
|
CliCommandInfo {
|
|
command: CliCommand::Feature(FeatureCliCommand::Status {
|
|
features,
|
|
display_all,
|
|
}),
|
|
signers: vec![],
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
Ok(response)
|
|
}
|
|
|
|
pub fn process_feature_subcommand(
|
|
rpc_client: &RpcClient,
|
|
config: &CliConfig,
|
|
feature_subcommand: &FeatureCliCommand,
|
|
) -> ProcessResult {
|
|
match feature_subcommand {
|
|
FeatureCliCommand::Status {
|
|
features,
|
|
display_all,
|
|
} => process_status(rpc_client, config, features, *display_all),
|
|
FeatureCliCommand::Activate { feature, force } => {
|
|
process_activate(rpc_client, config, *feature, *force)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct FeatureSetStatsEntry {
|
|
stake_percent: f64,
|
|
rpc_nodes_percent: f32,
|
|
software_versions: Vec<CliVersion>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
struct ClusterInfoStatsEntry {
|
|
stake_percent: f64,
|
|
rpc_percent: f32,
|
|
}
|
|
|
|
struct ClusterInfoStats {
|
|
stats_map: HashMap<(u32, CliVersion), ClusterInfoStatsEntry>,
|
|
}
|
|
|
|
impl ClusterInfoStats {
|
|
fn aggregate_by_feature_set(&self) -> HashMap<u32, FeatureSetStatsEntry> {
|
|
let mut feature_set_map = HashMap::<u32, FeatureSetStatsEntry>::new();
|
|
for ((feature_set, software_version), stats_entry) in &self.stats_map {
|
|
let mut map_entry = feature_set_map.entry(*feature_set).or_default();
|
|
map_entry.rpc_nodes_percent += stats_entry.rpc_percent;
|
|
map_entry.stake_percent += stats_entry.stake_percent;
|
|
map_entry.software_versions.push(software_version.clone());
|
|
}
|
|
for stats_entry in feature_set_map.values_mut() {
|
|
stats_entry
|
|
.software_versions
|
|
.sort_by(|l, r| l.cmp(r).reverse());
|
|
}
|
|
feature_set_map
|
|
}
|
|
|
|
fn aggregate_by_software_version(&self) -> HashMap<CliVersion, ClusterInfoStatsEntry> {
|
|
let mut software_version_map = HashMap::<CliVersion, ClusterInfoStatsEntry>::new();
|
|
for ((_feature_set, software_version), stats_entry) in &self.stats_map {
|
|
let mut map_entry = software_version_map
|
|
.entry(software_version.clone())
|
|
.or_default();
|
|
map_entry.rpc_percent += stats_entry.rpc_percent;
|
|
map_entry.stake_percent += stats_entry.stake_percent;
|
|
}
|
|
software_version_map
|
|
}
|
|
}
|
|
|
|
fn cluster_info_stats(rpc_client: &RpcClient) -> Result<ClusterInfoStats, ClientError> {
|
|
#[derive(Default)]
|
|
struct StatsEntry {
|
|
stake_lamports: u64,
|
|
rpc_nodes_count: u32,
|
|
}
|
|
|
|
let cluster_info_list = rpc_client
|
|
.get_cluster_nodes()?
|
|
.into_iter()
|
|
.map(|contact_info| {
|
|
(
|
|
contact_info.pubkey,
|
|
contact_info.feature_set,
|
|
contact_info.rpc.is_some(),
|
|
contact_info
|
|
.version
|
|
.and_then(|v| CliVersion::from_str(&v).ok())
|
|
.unwrap_or_else(CliVersion::unknown_version),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let vote_accounts = rpc_client.get_vote_accounts()?;
|
|
|
|
let mut total_active_stake: u64 = vote_accounts
|
|
.delinquent
|
|
.iter()
|
|
.map(|vote_account| vote_account.activated_stake)
|
|
.sum();
|
|
|
|
let vote_stakes = vote_accounts
|
|
.current
|
|
.into_iter()
|
|
.map(|vote_account| {
|
|
total_active_stake += vote_account.activated_stake;
|
|
(vote_account.node_pubkey, vote_account.activated_stake)
|
|
})
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
let mut cluster_info_stats: HashMap<(u32, CliVersion), StatsEntry> = HashMap::new();
|
|
let mut total_rpc_nodes = 0;
|
|
for (node_id, feature_set, is_rpc, version) in cluster_info_list {
|
|
let feature_set = feature_set.unwrap_or(0);
|
|
let stats_entry = cluster_info_stats
|
|
.entry((feature_set, version))
|
|
.or_default();
|
|
|
|
if let Some(vote_stake) = vote_stakes.get(&node_id) {
|
|
stats_entry.stake_lamports += *vote_stake;
|
|
}
|
|
|
|
if is_rpc {
|
|
stats_entry.rpc_nodes_count += 1;
|
|
total_rpc_nodes += 1;
|
|
}
|
|
}
|
|
|
|
Ok(ClusterInfoStats {
|
|
stats_map: cluster_info_stats
|
|
.into_iter()
|
|
.filter_map(
|
|
|(
|
|
cluster_config,
|
|
StatsEntry {
|
|
stake_lamports,
|
|
rpc_nodes_count,
|
|
},
|
|
)| {
|
|
let stake_percent = (stake_lamports as f64 / total_active_stake as f64) * 100.;
|
|
let rpc_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.;
|
|
if stake_percent >= 0.001 || rpc_percent >= 0.001 {
|
|
Some((
|
|
cluster_config,
|
|
ClusterInfoStatsEntry {
|
|
stake_percent,
|
|
rpc_percent,
|
|
},
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
)
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
// Feature activation is only allowed when 95% of the active stake is on the current feature set
|
|
fn feature_activation_allowed(
|
|
rpc_client: &RpcClient,
|
|
quiet: bool,
|
|
) -> Result<
|
|
(
|
|
bool,
|
|
Option<CliClusterFeatureSets>,
|
|
Option<CliClusterSoftwareVersions>,
|
|
),
|
|
ClientError,
|
|
> {
|
|
let cluster_info_stats = cluster_info_stats(rpc_client)?;
|
|
let feature_set_stats = cluster_info_stats.aggregate_by_feature_set();
|
|
|
|
let tool_version = solana_version::Version::default();
|
|
let tool_feature_set = tool_version.feature_set;
|
|
let tool_software_version = CliVersion::from(semver::Version::new(
|
|
tool_version.major as u64,
|
|
tool_version.minor as u64,
|
|
tool_version.patch as u64,
|
|
));
|
|
let (stake_allowed, rpc_allowed) = feature_set_stats
|
|
.get(&tool_feature_set)
|
|
.map(
|
|
|FeatureSetStatsEntry {
|
|
stake_percent,
|
|
rpc_nodes_percent,
|
|
..
|
|
}| (*stake_percent >= 95., *rpc_nodes_percent >= 95.),
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
let cluster_software_versions = if quiet {
|
|
None
|
|
} else {
|
|
let mut software_versions: Vec<_> = cluster_info_stats
|
|
.aggregate_by_software_version()
|
|
.into_iter()
|
|
.map(|(software_version, stats)| CliSoftwareVersionStats {
|
|
software_version,
|
|
stake_percent: stats.stake_percent,
|
|
rpc_percent: stats.rpc_percent,
|
|
})
|
|
.collect();
|
|
software_versions.sort_by(|l, r| l.software_version.cmp(&r.software_version).reverse());
|
|
Some(CliClusterSoftwareVersions {
|
|
software_versions,
|
|
tool_software_version,
|
|
})
|
|
};
|
|
|
|
let cluster_feature_sets = if quiet {
|
|
None
|
|
} else {
|
|
let mut feature_sets: Vec<_> = feature_set_stats
|
|
.into_iter()
|
|
.map(|(feature_set, stats_entry)| CliFeatureSetStats {
|
|
feature_set,
|
|
software_versions: stats_entry.software_versions,
|
|
rpc_percent: stats_entry.rpc_nodes_percent,
|
|
stake_percent: stats_entry.stake_percent,
|
|
})
|
|
.collect();
|
|
|
|
feature_sets.sort_by(|l, r| {
|
|
match l.software_versions[0]
|
|
.cmp(&r.software_versions[0])
|
|
.reverse()
|
|
{
|
|
Ordering::Equal => {
|
|
match l
|
|
.stake_percent
|
|
.partial_cmp(&r.stake_percent)
|
|
.unwrap()
|
|
.reverse()
|
|
{
|
|
Ordering::Equal => {
|
|
l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
|
|
}
|
|
o => o,
|
|
}
|
|
}
|
|
o => o,
|
|
}
|
|
});
|
|
Some(CliClusterFeatureSets {
|
|
tool_feature_set,
|
|
feature_sets,
|
|
stake_allowed,
|
|
rpc_allowed,
|
|
})
|
|
};
|
|
|
|
Ok((
|
|
stake_allowed && rpc_allowed,
|
|
cluster_feature_sets,
|
|
cluster_software_versions,
|
|
))
|
|
}
|
|
|
|
fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
|
|
feature::from_account(&account).map(|feature| match feature.activated_at {
|
|
None => CliFeatureStatus::Pending,
|
|
Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
|
|
})
|
|
}
|
|
|
|
fn get_feature_status(
|
|
rpc_client: &RpcClient,
|
|
feature_id: &Pubkey,
|
|
) -> Result<Option<CliFeatureStatus>, Box<dyn std::error::Error>> {
|
|
rpc_client
|
|
.get_account(feature_id)
|
|
.map(status_from_account)
|
|
.map_err(|e| e.into())
|
|
}
|
|
|
|
pub fn get_feature_is_active(
|
|
rpc_client: &RpcClient,
|
|
feature_id: &Pubkey,
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
get_feature_status(rpc_client, feature_id)
|
|
.map(|status| matches!(status, Some(CliFeatureStatus::Active(_))))
|
|
}
|
|
|
|
fn process_status(
|
|
rpc_client: &RpcClient,
|
|
config: &CliConfig,
|
|
feature_ids: &[Pubkey],
|
|
display_all: bool,
|
|
) -> ProcessResult {
|
|
let current_slot = rpc_client.get_slot()?;
|
|
let filter = if !display_all {
|
|
current_slot.checked_sub(DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS)
|
|
} else {
|
|
None
|
|
};
|
|
let mut inactive = false;
|
|
let mut features = vec![];
|
|
for feature_ids in feature_ids.chunks(MAX_MULTIPLE_ACCOUNTS) {
|
|
let mut feature_chunk = rpc_client
|
|
.get_multiple_accounts(feature_ids)
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.zip(feature_ids)
|
|
.map(|(account, feature_id)| {
|
|
let feature_name = FEATURE_NAMES.get(feature_id).unwrap();
|
|
account
|
|
.and_then(status_from_account)
|
|
.map(|feature_status| CliFeature {
|
|
id: feature_id.to_string(),
|
|
description: feature_name.to_string(),
|
|
status: feature_status,
|
|
})
|
|
.unwrap_or_else(|| {
|
|
inactive = true;
|
|
CliFeature {
|
|
id: feature_id.to_string(),
|
|
description: feature_name.to_string(),
|
|
status: CliFeatureStatus::Inactive,
|
|
}
|
|
})
|
|
})
|
|
.filter(|feature| match (filter, &feature.status) {
|
|
(Some(min_activation), CliFeatureStatus::Active(activation)) => {
|
|
activation > &min_activation
|
|
}
|
|
_ => true,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
features.append(&mut feature_chunk);
|
|
}
|
|
|
|
features.sort_unstable();
|
|
|
|
let (feature_activation_allowed, cluster_feature_sets, cluster_software_versions) =
|
|
feature_activation_allowed(rpc_client, features.len() <= 1)?;
|
|
let epoch_schedule = rpc_client.get_epoch_schedule()?;
|
|
let feature_set = CliFeatures {
|
|
features,
|
|
current_slot,
|
|
epoch_schedule,
|
|
feature_activation_allowed,
|
|
cluster_feature_sets,
|
|
cluster_software_versions,
|
|
inactive,
|
|
};
|
|
Ok(config.output_format.formatted_string(&feature_set))
|
|
}
|
|
|
|
fn process_activate(
|
|
rpc_client: &RpcClient,
|
|
config: &CliConfig,
|
|
feature_id: Pubkey,
|
|
force: ForceActivation,
|
|
) -> ProcessResult {
|
|
let account = rpc_client
|
|
.get_multiple_accounts(&[feature_id])?
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
|
|
if let Some(account) = account {
|
|
if feature::from_account(&account).is_some() {
|
|
return Err(format!("{} has already been activated", feature_id).into());
|
|
}
|
|
}
|
|
|
|
if !feature_activation_allowed(rpc_client, false)?.0 {
|
|
match force {
|
|
ForceActivation::Almost =>
|
|
return Err("Add force argument once more to override the sanity check to force feature activation ".into()),
|
|
ForceActivation::Yes => println!("FEATURE ACTIVATION FORCED"),
|
|
ForceActivation::No =>
|
|
return Err("Feature activation is not allowed at this time".into()),
|
|
}
|
|
}
|
|
|
|
let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?;
|
|
|
|
let blockhash = rpc_client.get_latest_blockhash()?;
|
|
let (message, _) = resolve_spend_tx_and_check_account_balance(
|
|
rpc_client,
|
|
false,
|
|
SpendAmount::Some(rent),
|
|
&blockhash,
|
|
&config.signers[0].pubkey(),
|
|
|lamports| {
|
|
Message::new(
|
|
&feature::activate_with_lamports(
|
|
&feature_id,
|
|
&config.signers[0].pubkey(),
|
|
lamports,
|
|
),
|
|
Some(&config.signers[0].pubkey()),
|
|
)
|
|
},
|
|
config.commitment,
|
|
)?;
|
|
let mut transaction = Transaction::new_unsigned(message);
|
|
transaction.try_sign(&config.signers, blockhash)?;
|
|
|
|
println!(
|
|
"Activating {} ({})",
|
|
FEATURE_NAMES.get(&feature_id).unwrap(),
|
|
feature_id
|
|
);
|
|
rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?;
|
|
Ok("".to_string())
|
|
}
|