366 lines
12 KiB
Rust
366 lines
12 KiB
Rust
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};
|
|
use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::*};
|
|
use solana_cli_output::{QuietDisplay, VerboseDisplay};
|
|
use solana_client::{client_error::ClientError, rpc_client::RpcClient};
|
|
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
|
use solana_runtime::{
|
|
feature::{self, Feature},
|
|
feature_set::FEATURE_NAMES,
|
|
};
|
|
use solana_sdk::{
|
|
clock::Slot, message::Message, pubkey::Pubkey, system_instruction, transaction::Transaction,
|
|
};
|
|
use std::{collections::HashMap, fmt, sync::Arc};
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FeatureCliCommand {
|
|
Status { features: Vec<Pubkey> },
|
|
Activate { feature: Pubkey },
|
|
}
|
|
|
|
#[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!(
|
|
"{:<44} {:<40} {}",
|
|
"Feature", "Description", "Status"
|
|
))
|
|
.bold()
|
|
)?;
|
|
}
|
|
for feature in &self.features {
|
|
writeln!(
|
|
f,
|
|
"{:<44} {:<40} {}",
|
|
feature.id,
|
|
feature.description,
|
|
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(),
|
|
}
|
|
)?;
|
|
}
|
|
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 {}
|
|
|
|
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]"),
|
|
),
|
|
)
|
|
.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"),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
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)?];
|
|
signers.push(feature_signer.unwrap());
|
|
let feature = feature.unwrap();
|
|
|
|
known_feature(&feature)?;
|
|
|
|
CliCommandInfo {
|
|
command: CliCommand::Feature(FeatureCliCommand::Activate { feature }),
|
|
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()
|
|
};
|
|
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 } => process_activate(rpc_client, config, *feature),
|
|
}
|
|
}
|
|
|
|
fn active_stake_by_feature_set(rpc_client: &RpcClient) -> Result<HashMap<u32, u64>, ClientError> {
|
|
// Validator identity -> feature set
|
|
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) -> 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 {
|
|
println!("\n{}", style("Stake By Feature Set:").bold());
|
|
for (feature_set, percentage) in active_stake_by_feature_set.iter() {
|
|
if *feature_set == 0 {
|
|
println!("unknown - {}%", percentage);
|
|
} else {
|
|
println!(
|
|
"{} - {}% {}",
|
|
feature_set,
|
|
percentage,
|
|
if *feature_set == my_feature_set {
|
|
" <-- me"
|
|
} else {
|
|
""
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(feature_activation_allowed)
|
|
}
|
|
|
|
fn process_status(
|
|
rpc_client: &RpcClient,
|
|
config: &CliConfig,
|
|
feature_ids: &[Pubkey],
|
|
) -> ProcessResult {
|
|
let mut features: Vec<CliFeature> = vec![];
|
|
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 {
|
|
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,
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
inactive = true;
|
|
features.push(CliFeature {
|
|
id: feature_id.to_string(),
|
|
description: feature_name.to_string(),
|
|
status: CliFeatureStatus::Inactive,
|
|
});
|
|
}
|
|
|
|
let feature_set = CliFeatures {
|
|
features,
|
|
feature_activation_allowed: feature_activation_allowed(rpc_client)?,
|
|
inactive,
|
|
};
|
|
Ok(config.output_format.formatted_string(&feature_set))
|
|
}
|
|
|
|
fn process_activate(
|
|
rpc_client: &RpcClient,
|
|
config: &CliConfig,
|
|
feature_id: Pubkey,
|
|
) -> 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)? {
|
|
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, 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(
|
|
&[
|
|
system_instruction::transfer(
|
|
&config.signers[0].pubkey(),
|
|
&feature_id,
|
|
lamports,
|
|
),
|
|
system_instruction::allocate(&feature_id, Feature::size_of() as u64),
|
|
system_instruction::assign(&feature_id, &feature::id()),
|
|
],
|
|
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())
|
|
}
|