solana/cli/src/feature.rs

416 lines
14 KiB
Rust
Raw Normal View History

2020-09-23 13:36:34 -07:00
use crate::{
cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
};
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use console::style;
use serde::{Deserialize, Serialize};
2020-09-23 13:36:34 -07:00
use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*};
use solana_cli_output::{QuietDisplay, VerboseDisplay};
2020-09-24 13:30:38 -07:00
use solana_client::{client_error::ClientError, rpc_client::RpcClient};
2020-09-23 13:36:34 -07:00
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{
clock::Slot,
2020-09-23 13:36:34 -07:00
feature::{self, Feature},
feature_set::FEATURE_NAMES,
message::Message,
pubkey::Pubkey,
transaction::Transaction,
};
use std::{collections::HashMap, fmt, sync::Arc};
2020-09-23 13:36:34 -07:00
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ForceActivation {
No,
Almost,
Yes,
}
2020-09-23 13:36:34 -07:00
#[derive(Debug, PartialEq)]
pub enum FeatureCliCommand {
Status {
features: Vec<Pubkey>,
},
Activate {
feature: Pubkey,
force: ForceActivation,
},
2020-09-23 13:36:34 -07:00
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "status", content = "sinceSlot")]
pub enum CliFeatureStatus {
Inactive,
Pending,
Active(Slot),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFeature {
pub id: String,
pub description: String,
#[serde(flatten)]
pub status: CliFeatureStatus,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFeatures {
pub features: Vec<CliFeature>,
pub feature_activation_allowed: bool,
#[serde(skip)]
pub inactive: bool,
}
impl fmt::Display for CliFeatures {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.features.len() > 1 {
writeln!(
f,
"{}",
style(format!(
2021-02-08 12:08:29 -08:00
"{:<44} | {:<27} | {}",
"Feature", "Status", "Description"
))
.bold()
)?;
}
for feature in &self.features {
writeln!(
f,
2021-02-08 12:08:29 -08:00
"{:<44} | {:<27} | {}",
feature.id,
match feature.status {
CliFeatureStatus::Inactive => style("inactive".to_string()).red(),
CliFeatureStatus::Pending => style("activation pending".to_string()).yellow(),
CliFeatureStatus::Active(activation_slot) =>
style(format!("active since slot {}", activation_slot)).green(),
},
feature.description,
)?;
}
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 {}
2020-09-23 13:36:34 -07:00
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(
2020-09-24 13:30:38 -07:00
Arg::with_name("features")
2020-09-23 13:36:34 -07:00
.value_name("ADDRESS")
.validator(is_valid_pubkey)
.index(1)
2020-09-24 13:30:38 -07:00
.multiple(true)
.help("Feature status to query [default: all known features]"),
2020-09-23 13:36:34 -07:00
),
)
.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)
2020-09-24 13:30:38 -07:00
.required(true)
2020-09-23 13:36:34 -07:00
.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"),
2020-09-23 13:36:34 -07:00
),
),
)
}
}
fn known_feature(feature: &Pubkey) -> Result<(), CliError> {
if FEATURE_NAMES.contains_key(feature) {
Ok(())
} else {
Err(CliError::BadParameter(format!(
"Unknown feature: {}",
feature
)))
}
}
2020-09-24 13:30:38 -07:00
pub fn parse_feature_subcommand(
2020-09-23 13:36:34 -07:00
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,
};
2020-09-23 13:36:34 -07:00
signers.push(feature_signer.unwrap());
let feature = feature.unwrap();
known_feature(&feature)?;
CliCommandInfo {
command: CliCommand::Feature(FeatureCliCommand::Activate { feature, force }),
2020-09-23 13:36:34 -07:00
signers,
}
}
("status", Some(matches)) => {
2020-09-24 13:30:38 -07:00
let mut features = if let Some(features) = pubkeys_of(matches, "features") {
for feature in &features {
known_feature(feature)?;
}
features
2020-09-23 13:36:34 -07:00
} else {
FEATURE_NAMES.keys().cloned().collect()
};
features.sort();
CliCommandInfo {
command: CliCommand::Feature(FeatureCliCommand::Status { features }),
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 } => process_status(rpc_client, config, features),
FeatureCliCommand::Activate { feature, force } => {
process_activate(rpc_client, config, *feature, *force)
}
2020-09-23 13:36:34 -07:00
}
}
fn active_stake_by_feature_set(rpc_client: &RpcClient) -> Result<HashMap<u32, u64>, ClientError> {
// Validator identity -> feature set
2020-09-23 13:36:34 -07:00
let feature_set_map = rpc_client
.get_cluster_nodes()?
.into_iter()
.map(|contact_info| (contact_info.pubkey, contact_info.feature_set))
.collect::<HashMap<_, _>>();
let vote_accounts = rpc_client.get_vote_accounts()?;
let total_active_stake: u64 = vote_accounts
.current
.iter()
.chain(vote_accounts.delinquent.iter())
.map(|vote_account| vote_account.activated_stake)
.sum();
// Sum all active stake by feature set
let mut active_stake_by_feature_set = HashMap::new();
for vote_account in vote_accounts.current {
if let Some(Some(feature_set)) = feature_set_map.get(&vote_account.node_pubkey) {
*active_stake_by_feature_set.entry(*feature_set).or_default() +=
vote_account.activated_stake;
} else {
*active_stake_by_feature_set
.entry(0 /* "unknown" */)
.or_default() += vote_account.activated_stake;
}
}
// Convert active stake to a percentage so the caller doesn't need `total_active_stake`
for (_, val) in active_stake_by_feature_set.iter_mut() {
*val = *val * 100 / total_active_stake;
}
Ok(active_stake_by_feature_set)
}
// 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, ClientError> {
let my_feature_set = solana_version::Version::default().feature_set;
let active_stake_by_feature_set = active_stake_by_feature_set(rpc_client)?;
let feature_activation_allowed = active_stake_by_feature_set
.get(&my_feature_set)
.map(|percentage| *percentage >= 95)
.unwrap_or(false);
if !feature_activation_allowed && !quiet {
if active_stake_by_feature_set.get(&my_feature_set).is_none() {
println!(
"{}",
style("To activate features the tool and cluster feature sets must match, select a tool version that matches the cluster")
.bold());
} else {
println!(
"{}",
style("To activate features the stake must be >= 95%").bold()
);
}
println!(
"{}",
style(format!("Tool Feture Set: {}", my_feature_set)).bold()
);
println!("{}", style("Cluster Feature Sets and Stakes:").bold());
for (feature_set, percentage) in active_stake_by_feature_set.iter() {
if *feature_set == 0 {
println!("unknown - {}%", percentage);
2020-09-23 13:36:34 -07:00
} else {
println!(
"{} - {}% {}",
feature_set,
percentage,
if *feature_set == my_feature_set {
2020-09-30 20:17:29 -07:00
" <-- me"
} else {
""
}
);
2020-09-23 13:36:34 -07:00
}
}
println!();
}
2020-09-23 13:36:34 -07:00
Ok(feature_activation_allowed)
2020-09-23 13:36:34 -07:00
}
fn process_status(
rpc_client: &RpcClient,
config: &CliConfig,
feature_ids: &[Pubkey],
) -> ProcessResult {
let mut features: Vec<CliFeature> = vec![];
2020-09-23 13:36:34 -07:00
let mut inactive = false;
for (i, account) in rpc_client
.get_multiple_accounts(feature_ids)?
.into_iter()
.enumerate()
{
let feature_id = &feature_ids[i];
let feature_name = FEATURE_NAMES.get(feature_id).unwrap();
if let Some(account) = account {
2020-10-30 13:40:55 -07:00
if let Some(feature) = feature::from_account(&account) {
let feature_status = match feature.activated_at {
None => CliFeatureStatus::Pending,
Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
};
features.push(CliFeature {
id: feature_id.to_string(),
description: feature_name.to_string(),
status: feature_status,
});
2020-09-23 13:36:34 -07:00
continue;
}
}
inactive = true;
features.push(CliFeature {
id: feature_id.to_string(),
description: feature_name.to_string(),
status: CliFeatureStatus::Inactive,
});
2020-09-23 13:36:34 -07:00
}
let feature_activation_allowed = feature_activation_allowed(rpc_client, features.len() <= 1)?;
let feature_set = CliFeatures {
features,
feature_activation_allowed,
inactive,
};
Ok(config.output_format.formatted_string(&feature_set))
2020-09-23 13:36:34 -07:00
}
fn process_activate(
rpc_client: &RpcClient,
config: &CliConfig,
feature_id: Pubkey,
force: ForceActivation,
2020-09-23 13:36:34 -07:00
) -> ProcessResult {
let account = rpc_client
.get_multiple_accounts(&[feature_id])?
.into_iter()
.next()
.unwrap();
2020-09-23 13:36:34 -07:00
if let Some(account) = account {
2020-10-30 13:40:55 -07:00
if feature::from_account(&account).is_some() {
2020-09-23 13:36:34 -07:00
return Err(format!("{} has already been activated", feature_id).into());
}
}
if !feature_activation_allowed(rpc_client, false)? {
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()),
}
2020-09-23 13:36:34 -07:00
}
let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?;
let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let (message, _) = resolve_spend_tx_and_check_account_balance(
rpc_client,
false,
SpendAmount::Some(rent),
&fee_calculator,
&config.signers[0].pubkey(),
|lamports| {
Message::new(
2020-10-30 13:40:55 -07:00
&feature::activate_with_lamports(
&feature_id,
&config.signers[0].pubkey(),
lamports,
),
2020-09-23 13:36:34 -07:00
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())
}