Add --minimum-release-version argument to allow for destaking of nodes that fail to keep with software updates
This commit is contained in:
		
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -5234,6 +5234,7 @@ version = "1.6.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "clap",
 | 
			
		||||
 "log 0.4.11",
 | 
			
		||||
 "semver 0.9.0",
 | 
			
		||||
 "serde_yaml",
 | 
			
		||||
 "solana-clap-utils",
 | 
			
		||||
 "solana-cli-config",
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ version = "1.6.0"
 | 
			
		||||
[dependencies]
 | 
			
		||||
clap = "2.33.0"
 | 
			
		||||
log = "0.4.11"
 | 
			
		||||
semver = "0.9.0"
 | 
			
		||||
serde_yaml = "0.8.13"
 | 
			
		||||
solana-clap-utils = { path = "../clap-utils", version = "1.6.0" }
 | 
			
		||||
solana-client = { path = "../client", version = "1.6.0" }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +1,71 @@
 | 
			
		||||
#![allow(clippy::integer_arithmetic)]
 | 
			
		||||
use clap::{crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg};
 | 
			
		||||
use log::*;
 | 
			
		||||
use solana_clap_utils::{
 | 
			
		||||
    input_parsers::{keypair_of, pubkey_of},
 | 
			
		||||
    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::{
 | 
			
		||||
    client_error, rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig,
 | 
			
		||||
    rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcVoteAccountInfo,
 | 
			
		||||
};
 | 
			
		||||
use solana_metrics::datapoint_info;
 | 
			
		||||
use solana_notifier::Notifier;
 | 
			
		||||
use solana_sdk::{
 | 
			
		||||
    account_utils::StateMut,
 | 
			
		||||
    clock::{Epoch, Slot},
 | 
			
		||||
    commitment_config::CommitmentConfig,
 | 
			
		||||
    message::Message,
 | 
			
		||||
    native_token::*,
 | 
			
		||||
    pubkey::Pubkey,
 | 
			
		||||
    signature::{Keypair, Signature, Signer},
 | 
			
		||||
    transaction::Transaction,
 | 
			
		||||
};
 | 
			
		||||
use solana_stake_program::{stake_instruction, stake_state::StakeState};
 | 
			
		||||
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::{HashMap, HashSet},
 | 
			
		||||
    error,
 | 
			
		||||
    fs::File,
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    process,
 | 
			
		||||
    str::FromStr,
 | 
			
		||||
    thread::sleep,
 | 
			
		||||
    time::Duration,
 | 
			
		||||
use {
 | 
			
		||||
    clap::{
 | 
			
		||||
        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_validators::{
 | 
			
		||||
            is_amount, is_keypair, is_pubkey_or_keypair, is_url, is_valid_percentage,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    solana_cli_output::display::format_labeled_address,
 | 
			
		||||
    solana_client::{
 | 
			
		||||
        client_error, rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig,
 | 
			
		||||
        rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcVoteAccountInfo,
 | 
			
		||||
    },
 | 
			
		||||
    solana_metrics::datapoint_info,
 | 
			
		||||
    solana_notifier::Notifier,
 | 
			
		||||
    solana_sdk::{
 | 
			
		||||
        account_utils::StateMut,
 | 
			
		||||
        clock::{Epoch, Slot},
 | 
			
		||||
        commitment_config::CommitmentConfig,
 | 
			
		||||
        message::Message,
 | 
			
		||||
        native_token::*,
 | 
			
		||||
        pubkey::Pubkey,
 | 
			
		||||
        signature::{Keypair, Signature, Signer},
 | 
			
		||||
        transaction::Transaction,
 | 
			
		||||
    },
 | 
			
		||||
    solana_stake_program::{stake_instruction, stake_state::StakeState},
 | 
			
		||||
    std::{
 | 
			
		||||
        collections::{HashMap, HashSet},
 | 
			
		||||
        error,
 | 
			
		||||
        fs::File,
 | 
			
		||||
        path::PathBuf,
 | 
			
		||||
        process,
 | 
			
		||||
        str::FromStr,
 | 
			
		||||
        thread::sleep,
 | 
			
		||||
        time::Duration,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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)]
 | 
			
		||||
struct Config {
 | 
			
		||||
    json_rpc_url: String,
 | 
			
		||||
@@ -71,6 +100,9 @@ struct Config {
 | 
			
		||||
    max_commission: u8,
 | 
			
		||||
 | 
			
		||||
    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 {
 | 
			
		||||
@@ -182,6 +214,14 @@ fn get_config() -> Config {
 | 
			
		||||
                .validator(is_valid_percentage)
 | 
			
		||||
                .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();
 | 
			
		||||
 | 
			
		||||
    let config = if let Some(config_file) = matches.value_of("config_file") {
 | 
			
		||||
@@ -202,6 +242,7 @@ fn get_config() -> Config {
 | 
			
		||||
    let baseline_stake_amount =
 | 
			
		||||
        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 minimum_release_version = release_version_of(&matches, "minimum_release_version");
 | 
			
		||||
 | 
			
		||||
    let (json_rpc_url, validator_list) = match cluster.as_str() {
 | 
			
		||||
        "mainnet-beta" => (
 | 
			
		||||
@@ -259,6 +300,7 @@ fn get_config() -> Config {
 | 
			
		||||
        max_commission,
 | 
			
		||||
        max_poor_block_producer_percentage,
 | 
			
		||||
        address_labels: config.address_labels,
 | 
			
		||||
        minimum_release_version,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    info!("RPC URL: {}", config.json_rpc_url);
 | 
			
		||||
@@ -653,6 +695,35 @@ fn main() -> Result<(), Box<dyn error::Error>> {
 | 
			
		||||
        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 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()
 | 
			
		||||
        > 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
 | 
			
		||||
    let vote_account_status = rpc_client.get_vote_accounts()?;
 | 
			
		||||
    let vote_account_info = vote_account_status
 | 
			
		||||
@@ -689,14 +765,15 @@ fn main() -> Result<(), Box<dyn error::Error>> {
 | 
			
		||||
 | 
			
		||||
    for RpcVoteAccountInfo {
 | 
			
		||||
        commission,
 | 
			
		||||
        node_pubkey,
 | 
			
		||||
        node_pubkey: node_pubkey_str,
 | 
			
		||||
        root_slot,
 | 
			
		||||
        vote_pubkey,
 | 
			
		||||
        ..
 | 
			
		||||
    } in &vote_account_info
 | 
			
		||||
    {
 | 
			
		||||
        let formatted_node_pubkey = format_labeled_address(&node_pubkey, &config.address_labels);
 | 
			
		||||
        let node_pubkey = Pubkey::from_str(&node_pubkey).unwrap();
 | 
			
		||||
        let formatted_node_pubkey =
 | 
			
		||||
            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 bonus_seed = &format!("A{{{}", vote_pubkey)[..32];
 | 
			
		||||
        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),
 | 
			
		||||
                ),
 | 
			
		||||
            ));
 | 
			
		||||
        } 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
 | 
			
		||||
        // 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(
 | 
			
		||||
        confirmations,
 | 
			
		||||
        if config.dry_run {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user