Add --minimum-release-version argument to allow for destaking of nodes that fail to keep with software updates
This commit is contained in:
parent
7ea2d32243
commit
ac0e9a6ce1
|
@ -5234,6 +5234,7 @@ version = "1.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"log 0.4.11",
|
"log 0.4.11",
|
||||||
|
"semver 0.9.0",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"solana-clap-utils",
|
"solana-clap-utils",
|
||||||
"solana-cli-config",
|
"solana-cli-config",
|
||||||
|
|
|
@ -11,6 +11,7 @@ version = "1.6.0"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
|
semver = "0.9.0"
|
||||||
serde_yaml = "0.8.13"
|
serde_yaml = "0.8.13"
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "1.6.0" }
|
solana-clap-utils = { path = "../clap-utils", version = "1.6.0" }
|
||||||
solana-client = { path = "../client", version = "1.6.0" }
|
solana-client = { path = "../client", version = "1.6.0" }
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
#![allow(clippy::integer_arithmetic)]
|
#![allow(clippy::integer_arithmetic)]
|
||||||
use clap::{crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg};
|
use {
|
||||||
use log::*;
|
clap::{
|
||||||
use solana_clap_utils::{
|
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg,
|
||||||
|
ArgMatches,
|
||||||
|
},
|
||||||
|
log::*,
|
||||||
|
solana_clap_utils::{
|
||||||
input_parsers::{keypair_of, pubkey_of},
|
input_parsers::{keypair_of, pubkey_of},
|
||||||
input_validators::{is_amount, is_keypair, is_pubkey_or_keypair, is_url, is_valid_percentage},
|
input_validators::{
|
||||||
};
|
is_amount, is_keypair, is_pubkey_or_keypair, is_url, is_valid_percentage,
|
||||||
use solana_cli_output::display::format_labeled_address;
|
},
|
||||||
use solana_client::{
|
},
|
||||||
|
solana_cli_output::display::format_labeled_address,
|
||||||
|
solana_client::{
|
||||||
client_error, rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig,
|
client_error, rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig,
|
||||||
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcVoteAccountInfo,
|
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcVoteAccountInfo,
|
||||||
};
|
},
|
||||||
use solana_metrics::datapoint_info;
|
solana_metrics::datapoint_info,
|
||||||
use solana_notifier::Notifier;
|
solana_notifier::Notifier,
|
||||||
use solana_sdk::{
|
solana_sdk::{
|
||||||
account_utils::StateMut,
|
account_utils::StateMut,
|
||||||
clock::{Epoch, Slot},
|
clock::{Epoch, Slot},
|
||||||
commitment_config::CommitmentConfig,
|
commitment_config::CommitmentConfig,
|
||||||
|
@ -21,10 +27,9 @@ use solana_sdk::{
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Keypair, Signature, Signer},
|
signature::{Keypair, Signature, Signer},
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
};
|
},
|
||||||
use solana_stake_program::{stake_instruction, stake_state::StakeState};
|
solana_stake_program::{stake_instruction, stake_state::StakeState},
|
||||||
|
std::{
|
||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
error,
|
error,
|
||||||
fs::File,
|
fs::File,
|
||||||
|
@ -33,10 +38,34 @@ use std::{
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod validator_list;
|
mod validator_list;
|
||||||
|
|
||||||
|
pub fn is_release_version(string: String) -> Result<(), String> {
|
||||||
|
if string.starts_with('v') && semver::Version::parse(string.split_at(1).1).is_ok() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
semver::Version::parse(&string)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|err| format!("{:?}", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release_version_of(matches: &ArgMatches<'_>, name: &str) -> Option<semver::Version> {
|
||||||
|
matches
|
||||||
|
.value_of(name)
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.map(|string| {
|
||||||
|
if string.starts_with('v') {
|
||||||
|
semver::Version::parse(string.split_at(1).1)
|
||||||
|
} else {
|
||||||
|
semver::Version::parse(&string)
|
||||||
|
}
|
||||||
|
.expect("semver::Version")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
json_rpc_url: String,
|
json_rpc_url: String,
|
||||||
|
@ -71,6 +100,9 @@ struct Config {
|
||||||
max_commission: u8,
|
max_commission: u8,
|
||||||
|
|
||||||
address_labels: HashMap<String, String>,
|
address_labels: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// If Some(), destake validators with a version less than this version
|
||||||
|
minimum_release_version: Option<semver::Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config() -> Config {
|
fn get_config() -> Config {
|
||||||
|
@ -182,6 +214,14 @@ fn get_config() -> Config {
|
||||||
.validator(is_valid_percentage)
|
.validator(is_valid_percentage)
|
||||||
.help("Vote accounts with a larger commission than this amount will not be staked")
|
.help("Vote accounts with a larger commission than this amount will not be staked")
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("minimum_release_version")
|
||||||
|
.long("minimum-release-version")
|
||||||
|
.value_name("SEMVER")
|
||||||
|
.takes_value(true)
|
||||||
|
.validator(is_release_version)
|
||||||
|
.help("Remove the base and bonus stake from validators with a release version older than this one")
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let config = if let Some(config_file) = matches.value_of("config_file") {
|
let config = if let Some(config_file) = matches.value_of("config_file") {
|
||||||
|
@ -202,6 +242,7 @@ fn get_config() -> Config {
|
||||||
let baseline_stake_amount =
|
let baseline_stake_amount =
|
||||||
sol_to_lamports(value_t_or_exit!(matches, "baseline_stake_amount", f64));
|
sol_to_lamports(value_t_or_exit!(matches, "baseline_stake_amount", f64));
|
||||||
let bonus_stake_amount = sol_to_lamports(value_t_or_exit!(matches, "bonus_stake_amount", f64));
|
let bonus_stake_amount = sol_to_lamports(value_t_or_exit!(matches, "bonus_stake_amount", f64));
|
||||||
|
let minimum_release_version = release_version_of(&matches, "minimum_release_version");
|
||||||
|
|
||||||
let (json_rpc_url, validator_list) = match cluster.as_str() {
|
let (json_rpc_url, validator_list) = match cluster.as_str() {
|
||||||
"mainnet-beta" => (
|
"mainnet-beta" => (
|
||||||
|
@ -259,6 +300,7 @@ fn get_config() -> Config {
|
||||||
max_commission,
|
max_commission,
|
||||||
max_poor_block_producer_percentage,
|
max_poor_block_producer_percentage,
|
||||||
address_labels: config.address_labels,
|
address_labels: config.address_labels,
|
||||||
|
minimum_release_version,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("RPC URL: {}", config.json_rpc_url);
|
info!("RPC URL: {}", config.json_rpc_url);
|
||||||
|
@ -653,6 +695,35 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cluster_nodes_with_old_version: HashSet<String> = match config.minimum_release_version {
|
||||||
|
Some(ref minimum_release_version) => rpc_client
|
||||||
|
.get_cluster_nodes()?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|rpc_contact_info| {
|
||||||
|
if let Ok(pubkey) = Pubkey::from_str(&rpc_contact_info.pubkey) {
|
||||||
|
if config.validator_list.contains(&pubkey) {
|
||||||
|
if let Some(ref version) = rpc_contact_info.version {
|
||||||
|
if let Ok(semver) = semver::Version::parse(version) {
|
||||||
|
if semver < *minimum_release_version {
|
||||||
|
return Some(rpc_contact_info.pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => HashSet::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref minimum_release_version) = config.minimum_release_version {
|
||||||
|
info!(
|
||||||
|
"Validators running a release older than {}: {:?}",
|
||||||
|
minimum_release_version, cluster_nodes_with_old_version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let source_stake_balance = validate_source_stake_account(&rpc_client, &config)?;
|
let source_stake_balance = validate_source_stake_account(&rpc_client, &config)?;
|
||||||
|
|
||||||
let epoch_info = rpc_client.get_epoch_info()?;
|
let epoch_info = rpc_client.get_epoch_info()?;
|
||||||
|
@ -666,6 +737,11 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
let too_many_poor_block_producers = poor_block_producers.len()
|
let too_many_poor_block_producers = poor_block_producers.len()
|
||||||
> quality_block_producers.len() * config.max_poor_block_producer_percentage / 100;
|
> quality_block_producers.len() * config.max_poor_block_producer_percentage / 100;
|
||||||
|
|
||||||
|
// If more than 10% of the cluster is running an older version, disable de-staking of old
|
||||||
|
// validators. The user probably provided a bad `--minimum-release-version` argument
|
||||||
|
let too_many_old_validators = cluster_nodes_with_old_version.len()
|
||||||
|
> (poor_block_producers.len() + quality_block_producers.len()) / 10;
|
||||||
|
|
||||||
// Fetch vote account status for all the validator_listed validators
|
// Fetch vote account status for all the validator_listed validators
|
||||||
let vote_account_status = rpc_client.get_vote_accounts()?;
|
let vote_account_status = rpc_client.get_vote_accounts()?;
|
||||||
let vote_account_info = vote_account_status
|
let vote_account_info = vote_account_status
|
||||||
|
@ -689,14 +765,15 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
|
|
||||||
for RpcVoteAccountInfo {
|
for RpcVoteAccountInfo {
|
||||||
commission,
|
commission,
|
||||||
node_pubkey,
|
node_pubkey: node_pubkey_str,
|
||||||
root_slot,
|
root_slot,
|
||||||
vote_pubkey,
|
vote_pubkey,
|
||||||
..
|
..
|
||||||
} in &vote_account_info
|
} in &vote_account_info
|
||||||
{
|
{
|
||||||
let formatted_node_pubkey = format_labeled_address(&node_pubkey, &config.address_labels);
|
let formatted_node_pubkey =
|
||||||
let node_pubkey = Pubkey::from_str(&node_pubkey).unwrap();
|
format_labeled_address(&node_pubkey_str, &config.address_labels);
|
||||||
|
let node_pubkey = Pubkey::from_str(&node_pubkey_str).unwrap();
|
||||||
let baseline_seed = &vote_pubkey.to_string()[..32];
|
let baseline_seed = &vote_pubkey.to_string()[..32];
|
||||||
let bonus_seed = &format!("A{{{}", vote_pubkey)[..32];
|
let bonus_seed = &format!("A{{{}", vote_pubkey)[..32];
|
||||||
let vote_pubkey = Pubkey::from_str(&vote_pubkey).unwrap();
|
let vote_pubkey = Pubkey::from_str(&vote_pubkey).unwrap();
|
||||||
|
@ -831,6 +908,40 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
lamports_to_sol(config.bonus_stake_amount),
|
lamports_to_sol(config.bonus_stake_amount),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
} else if !too_many_old_validators
|
||||||
|
&& cluster_nodes_with_old_version.contains(node_pubkey_str)
|
||||||
|
{
|
||||||
|
// Deactivate baseline stake
|
||||||
|
delegate_stake_transactions.push((
|
||||||
|
Transaction::new_unsigned(Message::new(
|
||||||
|
&[stake_instruction::deactivate_stake(
|
||||||
|
&baseline_stake_address,
|
||||||
|
&config.authorized_staker.pubkey(),
|
||||||
|
)],
|
||||||
|
Some(&config.authorized_staker.pubkey()),
|
||||||
|
)),
|
||||||
|
format!(
|
||||||
|
"🧮 `{}` is running an old software release. Removed ◎{} baseline stake",
|
||||||
|
formatted_node_pubkey,
|
||||||
|
lamports_to_sol(config.baseline_stake_amount),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Deactivate bonus stake
|
||||||
|
delegate_stake_transactions.push((
|
||||||
|
Transaction::new_unsigned(Message::new(
|
||||||
|
&[stake_instruction::deactivate_stake(
|
||||||
|
&bonus_stake_address,
|
||||||
|
&config.authorized_staker.pubkey(),
|
||||||
|
)],
|
||||||
|
Some(&config.authorized_staker.pubkey()),
|
||||||
|
)),
|
||||||
|
format!(
|
||||||
|
"🧮 `{}` is running an old software release. Removed ◎{} bonus stake",
|
||||||
|
formatted_node_pubkey,
|
||||||
|
lamports_to_sol(config.bonus_stake_amount),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
// Validator is not considered delinquent if its root slot is less than 256 slots behind the current
|
// Validator is not considered delinquent if its root slot is less than 256 slots behind the current
|
||||||
// slot. This is very generous.
|
// slot. This is very generous.
|
||||||
|
@ -1019,6 +1130,15 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if too_many_old_validators {
|
||||||
|
let message =
|
||||||
|
"Note: Something is wrong, too many validators classified as running an older release";
|
||||||
|
warn!("{}", message);
|
||||||
|
if !config.dry_run {
|
||||||
|
notifier.send(&message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !process_confirmations(
|
if !process_confirmations(
|
||||||
confirmations,
|
confirmations,
|
||||||
if config.dry_run {
|
if config.dry_run {
|
||||||
|
|
Loading…
Reference in New Issue