From 417f0e41fae64f61fed441b920f9398924733f4a Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Wed, 20 May 2020 16:15:03 -0700 Subject: [PATCH] Add stake-o-matic (#10044) automerge --- Cargo.lock | 18 + Cargo.toml | 1 + client/src/rpc_client_request.rs | 2 +- core/src/rpc.rs | 1 - scripts/cargo-install-all.sh | 1 + stake-o-matic/.gitignore | 2 + stake-o-matic/Cargo.toml | 26 + stake-o-matic/README.md | 21 + stake-o-matic/src/main.rs | 845 +++++++++++++++++++++++++++++++ stake-o-matic/src/whitelist.rs | 270 ++++++++++ 10 files changed, 1185 insertions(+), 2 deletions(-) create mode 100644 stake-o-matic/.gitignore create mode 100644 stake-o-matic/Cargo.toml create mode 100644 stake-o-matic/README.md create mode 100644 stake-o-matic/src/main.rs create mode 100644 stake-o-matic/src/whitelist.rs diff --git a/Cargo.lock b/Cargo.lock index d196ba8f79..59395ff2b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4651,6 +4651,24 @@ dependencies = [ "tempfile", ] +[[package]] +name = "solana-stake-o-matic" +version = "1.2.0" +dependencies = [ + "clap", + "log 0.4.8", + "serde_yaml", + "solana-clap-utils", + "solana-cli-config", + "solana-client", + "solana-logger", + "solana-metrics", + "solana-notifier", + "solana-sdk", + "solana-stake-program", + "solana-transaction-status", +] + [[package]] name = "solana-stake-program" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 64a801bdae..5108ab02bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "logger", "log-analyzer", "merkle-tree", + "stake-o-matic", "streamer", "measure", "metrics", diff --git a/client/src/rpc_client_request.rs b/client/src/rpc_client_request.rs index 22d6cf452e..6857103900 100644 --- a/client/src/rpc_client_request.rs +++ b/client/src/rpc_client_request.rs @@ -15,7 +15,7 @@ pub struct RpcClientRequest { impl RpcClientRequest { pub fn new(url: String) -> Self { - Self::new_with_timeout(url, Duration::from_secs(20)) + Self::new_with_timeout(url, Duration::from_secs(30)) } pub fn new_with_timeout(url: String, timeout: Duration) -> Self { diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 6f9be6b3ef..784da5ce63 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1338,7 +1338,6 @@ impl RpcSol for RpcSolImpl { let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?; let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); let tpu_addr = get_tpu_addr(&meta.cluster_info)?; - trace!("send_transaction: leader is {:?}", &tpu_addr); transactions_socket .send_to(&wire_transaction, tpu_addr) .map_err(|err| { diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index 9ce5115963..6690d585ce 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -100,6 +100,7 @@ else solana-net-shaper solana-stake-accounts solana-stake-monitor + solana-stake-o-matic solana-sys-tuner solana-tokens solana-validator diff --git a/stake-o-matic/.gitignore b/stake-o-matic/.gitignore new file mode 100644 index 0000000000..e3c0d4bb35 --- /dev/null +++ b/stake-o-matic/.gitignore @@ -0,0 +1,2 @@ +target/ +*.csv diff --git a/stake-o-matic/Cargo.toml b/stake-o-matic/Cargo.toml new file mode 100644 index 0000000000..28725fb564 --- /dev/null +++ b/stake-o-matic/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["Solana Maintainers "] +description = "I will find you and I will stake you" +edition = "2018" +homepage = "https://solana.com/" +license = "Apache-2.0" +name = "solana-stake-o-matic" +repository = "https://github.com/solana-labs/stake-o-matic" +version = "1.2.0" + +[dependencies] +clap = "2.33.0" +log = "0.4.8" +serde_yaml = "0.8.12" +solana-clap-utils = { path = "../clap-utils", version = "1.2.0" } +solana-client = { path = "../client", version = "1.2.0" } +solana-cli-config = { path = "../cli-config", version = "1.2.0" } +solana-logger = { path = "../logger", version = "1.2.0" } +solana-metrics = { path = "../metrics", version = "1.2.0" } +solana-notifier = { path = "../notifier", version = "1.2.0" } +solana-sdk = { path = "../sdk", version = "1.2.0" } +solana-stake-program = { path = "../programs/stake", version = "1.2.0" } +solana-transaction-status = { path = "../transaction-status", version = "1.2.0" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/stake-o-matic/README.md b/stake-o-matic/README.md new file mode 100644 index 0000000000..37a8960d3f --- /dev/null +++ b/stake-o-matic/README.md @@ -0,0 +1,21 @@ +## Effortlessly Manage Cluster Stakes +The testnet and mainnet-beta clusters currently have a large population of +validators that need to be staked by a central authority. + +## Staking Criteria +1. All non-delinquent validators receive 5,000 SOL stake +1. Additionally, non-deliquent validators that have produced a block in 75% of + their slots in the previous epoch receive bonus stake of 50,000 SOL + +A validator that is delinquent for more than 24 hours will have all stake +removed. However stake-o-matic has no memory, so if the same validator resolves +their delinquency then they will be re-staked again + +## Validator Whitelist +To be eligible for staking, a validator's identity pubkey must be added to a +YAML whitelist file. + +## Stake Account Management +Stake-o-matic will split the individual validator stake accounts from a master +stake account, and must be given the authorized staker keypair for the master +stake account. diff --git a/stake-o-matic/src/main.rs b/stake-o-matic/src/main.rs new file mode 100644 index 0000000000..b53b0f8a61 --- /dev/null +++ b/stake-o-matic/src/main.rs @@ -0,0 +1,845 @@ +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_keypair, is_pubkey_or_keypair, is_url}, +}; +use solana_client::{ + client_error, rpc_client::RpcClient, 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}, + message::Message, + native_token::*, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, + transaction::Transaction, +}; +use solana_stake_program::{stake_instruction, stake_state::StakeState}; +use solana_transaction_status::TransactionStatus; + +use std::{ + collections::{HashMap, HashSet}, + error, + fs::File, + iter::FromIterator, + path::PathBuf, + process, + str::FromStr, + thread::sleep, + time::{Duration, Instant}, +}; + +mod whitelist; + +struct Config { + json_rpc_url: String, + cluster: String, + source_stake_address: Pubkey, + authorized_staker: Keypair, + + /// Only validators with an identity pubkey in this whitelist will be staked + whitelist: HashSet, + + dry_run: bool, + + /// Amount of lamports to stake any validator in the whitelist that is not delinquent + baseline_stake_amount: u64, + + /// Amount of additional lamports to stake quality block producers in the whitelist + bonus_stake_amount: u64, + + /// Quality validators produce a block in more than this percentage of their leader slots + quality_block_producer_percentage: usize, + + /// A delinquent validator gets this number of slots of grace (from the current slot) before it + /// will be fully destaked. The grace period is intended to account for unexpected bugs that + /// cause a validator to go down + delinquent_grace_slot_distance: u64, +} + +fn get_config() -> Config { + let matches = App::new(crate_name!()) + .about(crate_description!()) + .version(crate_version!()) + .arg({ + let arg = Arg::with_name("config_file") + .short("C") + .long("config") + .value_name("PATH") + .takes_value(true) + .global(true) + .help("Configuration file to use"); + if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { + arg.default_value(&config_file) + } else { + arg + } + }) + .arg( + Arg::with_name("json_rpc_url") + .long("url") + .value_name("URL") + .takes_value(true) + .validator(is_url) + .help("JSON RPC URL for the cluster") + .conflicts_with("cluster") + ) + .arg( + Arg::with_name("cluster") + .long("cluster") + .value_name("NAME") + .possible_values(&["mainnet-beta", "testnet"]) + .takes_value(true) + .help("Name of the cluster to operate on") + ) + .arg( + Arg::with_name("whitelist_file") + .long("whitelist") + .value_name("FILE") + .required(true) + .takes_value(true) + .conflicts_with("cluster") + .help("File containing an YAML array of validator pubkeys eligible for staking") + ) + .arg( + Arg::with_name("confirm") + .long("confirm") + .takes_value(false) + .help("Confirm that the stake adjustments should actually be made") + ) + .arg( + Arg::with_name("source_stake_address") + .index(1) + .value_name("ADDRESS") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("The source stake account for splitting individual validator stake accounts from") + ) + .arg( + Arg::with_name("authorized_staker") + .index(2) + .value_name("KEYPAIR") + .validator(is_keypair) + .required(true) + .takes_value(true) + ) .get_matches(); + + let config = if let Some(config_file) = matches.value_of("config_file") { + solana_cli_config::Config::load(config_file).unwrap_or_default() + } else { + solana_cli_config::Config::default() + }; + + let source_stake_address = pubkey_of(&matches, "source_stake_address").unwrap(); + let authorized_staker = keypair_of(&matches, "authorized_staker").unwrap(); + let dry_run = !matches.is_present("confirm"); + let cluster = value_t!(matches, "cluster", String).unwrap_or_else(|_| "unknown".into()); + + let (json_rpc_url, whitelist) = match cluster.as_str() { + "mainnet-beta" => ( + "http://api.mainnet-beta.solana.com".into(), + whitelist::mainnet_beta_validators(), + ), + "testnet" => ( + "http://testnet.solana.com".into(), + whitelist::testnet_validators(), + ), + "unknown" => { + let whitelist_file = File::open(value_t_or_exit!(matches, "whitelist_file", PathBuf)) + .unwrap_or_else(|err| { + error!("Unable to open whitelist: {}", err); + process::exit(1); + }); + + let whitelist = serde_yaml::from_reader::<_, Vec>(whitelist_file) + .unwrap_or_else(|err| { + error!("Unable to read whitelist: {}", err); + process::exit(1); + }) + .into_iter() + .map(|p| { + Pubkey::from_str(&p).unwrap_or_else(|err| { + error!("Invalid whitelist pubkey '{}': {}", p, err); + process::exit(1); + }) + }) + .collect(); + ( + value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| config.json_rpc_url), + whitelist, + ) + } + _ => unreachable!(), + }; + let whitelist = whitelist.into_iter().collect::>(); + + let config = Config { + json_rpc_url, + cluster, + source_stake_address, + authorized_staker, + whitelist, + dry_run, + baseline_stake_amount: sol_to_lamports(5000.), + bonus_stake_amount: sol_to_lamports(50_000.), + delinquent_grace_slot_distance: 21600, // ~24 hours worth of slots at 2.5 slots per second + quality_block_producer_percentage: 75, + }; + + info!("RPC URL: {}", config.json_rpc_url); + config +} + +fn get_stake_account( + rpc_client: &RpcClient, + address: &Pubkey, +) -> Result<(u64, StakeState), String> { + let account = rpc_client.get_account(address).map_err(|e| { + format!( + "Failed to fetch stake account {}: {}", + address, + e.to_string() + ) + })?; + + if account.owner != solana_stake_program::id() { + return Err(format!( + "not a stake account (owned by {}): {}", + account.owner, address + )); + } + + account + .state() + .map_err(|e| { + format!( + "Failed to decode stake account at {}: {}", + address, + e.to_string() + ) + }) + .map(|stake_state| (account.lamports, stake_state)) +} + +/// Split validators into quality/poor lists based on their block production over the given `epoch` +fn classify_block_producers( + rpc_client: &RpcClient, + config: &Config, + epoch: Epoch, +) -> Result<(HashSet, HashSet), Box> { + let epoch_schedule = rpc_client.get_epoch_schedule()?; + let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch); + let last_slot_in_epoch = epoch_schedule.get_last_slot_in_epoch(epoch); + + let minimum_ledger_slot = rpc_client.minimum_ledger_slot()?; + if minimum_ledger_slot >= last_slot_in_epoch { + return Err(format!( + "Minimum ledger slot is newer than the last epoch: {} > {}", + minimum_ledger_slot, last_slot_in_epoch + ) + .into()); + } + + let first_slot = if minimum_ledger_slot > first_slot_in_epoch { + minimum_ledger_slot + } else { + first_slot_in_epoch + }; + + let confirmed_blocks = rpc_client.get_confirmed_blocks(first_slot, Some(last_slot_in_epoch))?; + let confirmed_blocks: HashSet = HashSet::from_iter(confirmed_blocks.into_iter()); + + let mut poor_block_producers = HashSet::new(); + let mut quality_block_producers = HashSet::new(); + + let leader_schedule = rpc_client.get_leader_schedule(Some(first_slot))?.unwrap(); + for (validator_identity, relative_slots) in leader_schedule { + let mut validator_blocks = 0; + let mut validator_slots = 0; + for relative_slot in relative_slots { + let slot = first_slot_in_epoch + relative_slot as Slot; + if slot >= first_slot { + validator_slots += 1; + if confirmed_blocks.contains(&slot) { + validator_blocks += 1; + } + } + } + trace!( + "Validator {} produced {} blocks in {} slots", + validator_identity, + validator_blocks, + validator_slots + ); + if validator_slots > 0 { + let validator_identity = Pubkey::from_str(&validator_identity)?; + if validator_blocks * 100 / validator_slots > config.quality_block_producer_percentage { + quality_block_producers.insert(validator_identity); + } else { + poor_block_producers.insert(validator_identity); + } + } + } + + trace!("quality_block_producers: {:?}", quality_block_producers); + trace!("poor_block_producers: {:?}", poor_block_producers); + Ok((quality_block_producers, poor_block_producers)) +} + +fn validate_source_stake_account( + rpc_client: &RpcClient, + config: &Config, +) -> Result> { + // check source stake account + let (source_stake_balance, source_stake_state) = + get_stake_account(&rpc_client, &config.source_stake_address)?; + + info!( + "stake account balance: {} SOL", + lamports_to_sol(source_stake_balance) + ); + match &source_stake_state { + StakeState::Initialized(_) | StakeState::Stake(_, _) => source_stake_state + .authorized() + .map_or(Ok(source_stake_balance), |authorized| { + if authorized.staker != config.authorized_staker.pubkey() { + Err(format!( + "The authorized staker for the source stake account is not {}", + config.authorized_staker.pubkey() + ) + .into()) + } else { + Ok(source_stake_balance) + } + }), + _ => Err(format!( + "Source stake account is not in the initialized state: {:?}", + source_stake_state + ) + .into()), + } +} + +struct ConfirmedTransaction { + success: bool, + signature: Signature, + memo: String, +} + +/// Simulate a list of transactions and filter out the ones that will fail +fn simulate_transactions( + rpc_client: &RpcClient, + candidate_transactions: Vec<(Transaction, String)>, +) -> client_error::Result> { + let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?; + + info!( + "Simulating {} transactions with blockhash {}", + candidate_transactions.len(), + blockhash + ); + let mut simulated_transactions = vec![]; + for (mut transaction, memo) in candidate_transactions { + transaction.message.recent_blockhash = blockhash; + + let sim_result = rpc_client.simulate_transaction(&transaction, false)?; + if sim_result.value.err.is_some() { + trace!( + "filtering out transaction due to simulation failure: {:?}", + sim_result + ); + } else { + simulated_transactions.push((transaction, memo)) + } + } + info!( + "Successfully simulating {} transactions", + simulated_transactions.len() + ); + Ok(simulated_transactions) +} + +#[allow(clippy::cognitive_complexity)] // Yeah I know... +fn transact( + rpc_client: &RpcClient, + dry_run: bool, + transactions: Vec<(Transaction, String)>, + authorized_staker: &Keypair, +) -> Result, Box> { + let authorized_staker_balance = rpc_client.get_balance(&authorized_staker.pubkey())?; + info!( + "Authorized staker balance: {} SOL", + lamports_to_sol(authorized_staker_balance) + ); + + let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + info!("{} transactions to send", transactions.len()); + + let required_fee = transactions.iter().fold(0, |fee, (transaction, _)| { + fee + fee_calculator.calculate_fee(&transaction.message) + }); + info!("Required fee: {} SOL", lamports_to_sol(required_fee)); + if required_fee > authorized_staker_balance { + return Err("Authorized staker has insufficient funds".into()); + } + + struct PendingTransaction { + transaction: Transaction, + memo: String, + last_status: Option, + last_status_update: Instant, + }; + + let mut pending_transactions = HashMap::new(); + for (mut transaction, memo) in transactions.into_iter() { + transaction.sign(&[authorized_staker], blockhash); + + if !dry_run { + rpc_client.send_transaction(&transaction)?; + } + pending_transactions.insert( + transaction.signatures[0], + PendingTransaction { + transaction, + memo, + last_status: None, + last_status_update: Instant::now(), + }, + ); + } + + let mut finalized_transactions = vec![]; + loop { + if pending_transactions.is_empty() { + break; + } + + if rpc_client + .get_fee_calculator_for_blockhash(&blockhash)? + .is_none() + { + error!( + "Blockhash {} expired with {} pending transactions", + blockhash, + pending_transactions.len() + ); + + for (signature, pending_transaction) in pending_transactions.into_iter() { + finalized_transactions.push(ConfirmedTransaction { + success: false, + signature, + memo: pending_transaction.memo, + }); + } + break; + } + + let pending_signatures = pending_transactions.keys().cloned().collect::>(); + let mut statuses = vec![]; + for pending_signatures_chunk in + pending_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS - 1) + { + trace!( + "checking {} pending_signatures", + pending_signatures_chunk.len() + ); + statuses.extend( + rpc_client + .get_signature_statuses(&pending_signatures_chunk)? + .value + .into_iter(), + ) + } + assert_eq!(statuses.len(), pending_signatures.len()); + + let now = Instant::now(); + let mut progressing_pending_transactions = 0; + + for (signature, status) in pending_signatures.into_iter().zip(statuses.into_iter()) { + let mut pending_transaction = pending_transactions.get_mut(&signature).unwrap(); + + trace!("{}: status={:?}", signature, status); + let confirmed = if dry_run { + Some(true) + } else if let Some(status) = &status { + if status.confirmations.is_none() { + Some(status.err.is_none()) + } else { + None + } + } else { + None + }; + + if let Some(success) = confirmed { + debug!("{}: confirmed", signature); + let pending_transaction = pending_transactions.remove(&signature).unwrap(); + finalized_transactions.push(ConfirmedTransaction { + success, + signature, + memo: pending_transaction.memo, + }); + } else if pending_transaction.last_status != status { + debug!("{}: made progress", signature); + progressing_pending_transactions += 1; + pending_transaction.last_status = status; + pending_transaction.last_status_update = now; + } else if now + .duration_since(pending_transaction.last_status_update) + .as_secs() + > 10 + { + info!("{} - stale transaction, resending", signature); + if !dry_run { + rpc_client.send_transaction(&pending_transaction.transaction)?; + } + pending_transaction.last_status = None; + pending_transaction.last_status_update = now; + } else { + debug!("{}: no progress", signature); + } + } + + info!( + "{} pending transactions ({} of which made progress), {} finalized transactions", + pending_transactions.len(), + progressing_pending_transactions, + finalized_transactions.len() + ); + sleep(Duration::from_millis(4000)); + } + + Ok(finalized_transactions) +} + +fn process_confirmations( + confirmations: Vec, + notifier: Option<&Notifier>, +) -> bool { + let mut ok = true; + for ConfirmedTransaction { + success, + signature, + memo, + } in confirmations + { + if success { + info!("OK: {}: {}", signature, memo); + if let Some(notifier) = notifier { + notifier.send(&memo) + } + } else { + error!("FAIL: {}: {}", signature, memo); + ok = false + } + } + ok +} + +fn main() -> Result<(), Box> { + solana_logger::setup_with_default("solana=info"); + let config = get_config(); + + let notifier = Notifier::default(); + let rpc_client = RpcClient::new(config.json_rpc_url.clone()); + + let source_stake_balance = validate_source_stake_account(&rpc_client, &config)?; + + let epoch_info = rpc_client.get_epoch_info()?; + let last_epoch = epoch_info.epoch - 1; + + info!("Epoch info: {:?}", epoch_info); + + let (quality_block_producers, _poor_block_producers) = + classify_block_producers(&rpc_client, &config, last_epoch)?; + + // Fetch vote account status for all the whitelisted validators + let vote_account_status = rpc_client.get_vote_accounts()?; + let vote_account_info = vote_account_status + .current + .into_iter() + .chain(vote_account_status.delinquent.into_iter()) + .filter_map(|vai| { + let node_pubkey = Pubkey::from_str(&vai.node_pubkey).ok()?; + if config.whitelist.contains(&node_pubkey) { + Some(vai) + } else { + None + } + }) + .collect::>(); + + let mut source_stake_lamports_required = 0; + let mut create_stake_transactions = vec![]; + let mut delegate_stake_transactions = vec![]; + + for RpcVoteAccountInfo { + vote_pubkey, + node_pubkey, + root_slot, + .. + } in &vote_account_info + { + let node_pubkey = Pubkey::from_str(&node_pubkey).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(); + + let baseline_stake_address = Pubkey::create_with_seed( + &config.authorized_staker.pubkey(), + baseline_seed, + &solana_stake_program::id(), + ) + .unwrap(); + let bonus_stake_address = Pubkey::create_with_seed( + &config.authorized_staker.pubkey(), + bonus_seed, + &solana_stake_program::id(), + ) + .unwrap(); + + // Transactions to create the baseline and bonus stake accounts + if let Ok((balance, _)) = get_stake_account(&rpc_client, &baseline_stake_address) { + if balance != config.baseline_stake_amount { + error!( + "Unexpected balance in stake account {}: {}, expected {}", + baseline_stake_address, balance, config.baseline_stake_amount + ); + process::exit(1); + } + } else { + info!( + "Need to create baseline stake account for validator {}", + node_pubkey + ); + source_stake_lamports_required += config.baseline_stake_amount; + create_stake_transactions.push(( + Transaction::new_unsigned(Message::new_with_payer( + &stake_instruction::split_with_seed( + &config.source_stake_address, + &config.authorized_staker.pubkey(), + config.baseline_stake_amount, + &baseline_stake_address, + &config.authorized_staker.pubkey(), + baseline_seed, + ), + Some(&config.authorized_staker.pubkey()), + )), + format!( + "Creating baseline stake account for validator {} ({})", + node_pubkey, baseline_stake_address + ), + )); + } + + if let Ok((balance, _)) = get_stake_account(&rpc_client, &bonus_stake_address) { + if balance != config.bonus_stake_amount { + error!( + "Unexpected balance in stake account {}: {}, expected {}", + bonus_stake_address, balance, config.bonus_stake_amount + ); + process::exit(1); + } + } else { + info!( + "Need to create bonus stake account for validator {}", + node_pubkey + ); + source_stake_lamports_required += config.bonus_stake_amount; + create_stake_transactions.push(( + Transaction::new_unsigned(Message::new_with_payer( + &stake_instruction::split_with_seed( + &config.source_stake_address, + &config.authorized_staker.pubkey(), + config.bonus_stake_amount, + &bonus_stake_address, + &config.authorized_staker.pubkey(), + bonus_seed, + ), + Some(&config.authorized_staker.pubkey()), + )), + format!( + "Creating bonus stake account for validator {} ({})", + node_pubkey, bonus_stake_address + ), + )); + } + + // Validator is not considered delinquent if its root slot is less than 256 slots behind the current + // slot. This is very generous. + if *root_slot > epoch_info.absolute_slot - 256 { + datapoint_info!( + "validator-status", + ("cluster", config.cluster, String), + ("id", node_pubkey.to_string(), String), + ("slot", epoch_info.absolute_slot, i64), + ("ok", true, bool) + ); + + // Delegate baseline stake + delegate_stake_transactions.push(( + Transaction::new_unsigned(Message::new_with_payer( + &[stake_instruction::delegate_stake( + &baseline_stake_address, + &config.authorized_staker.pubkey(), + &vote_pubkey, + )], + Some(&config.authorized_staker.pubkey()), + )), + format!( + "🥩 `{}` is current. Added ◎{} baseline stake", + node_pubkey, + lamports_to_sol(config.baseline_stake_amount), + ), + )); + + if quality_block_producers.contains(&node_pubkey) { + // Delegate bonus stake + delegate_stake_transactions.push(( + Transaction::new_unsigned( + Message::new_with_payer( + &[stake_instruction::delegate_stake( + &bonus_stake_address, + &config.authorized_staker.pubkey(), + &vote_pubkey, + )], + Some(&config.authorized_staker.pubkey()), + )), + format!( + "🏅 `{}` was a quality block producer during epoch {}. Added ◎{} bonus stake", + node_pubkey, + last_epoch, + lamports_to_sol(config.bonus_stake_amount), + ), + )); + } else { + // Deactivate bonus stake + delegate_stake_transactions.push(( + Transaction::new_unsigned( + Message::new_with_payer( + &[stake_instruction::deactivate_stake( + &bonus_stake_address, + &config.authorized_staker.pubkey(), + )], + Some(&config.authorized_staker.pubkey()), + )), + format!( + "💔 `{}` was a poor block producer during epoch {}. Removed ◎{} bonus stake", + node_pubkey, + last_epoch, + lamports_to_sol(config.bonus_stake_amount), + ), + )); + } + } else { + // Destake the validator if it has been delinquent for longer than the grace period + if *root_slot + < epoch_info + .absolute_slot + .saturating_sub(config.delinquent_grace_slot_distance) + { + // Deactivate baseline stake + delegate_stake_transactions.push(( + Transaction::new_unsigned(Message::new_with_payer( + &[stake_instruction::deactivate_stake( + &baseline_stake_address, + &config.authorized_staker.pubkey(), + )], + Some(&config.authorized_staker.pubkey()), + )), + format!( + "🏖️ `{}` is delinquent. Removed ◎{} baseline stake", + node_pubkey, + lamports_to_sol(config.baseline_stake_amount), + ), + )); + + // Deactivate bonus stake + delegate_stake_transactions.push(( + Transaction::new_unsigned(Message::new_with_payer( + &[stake_instruction::deactivate_stake( + &bonus_stake_address, + &config.authorized_staker.pubkey(), + )], + Some(&config.authorized_staker.pubkey()), + )), + format!( + "🏖️ `{}` is delinquent. Removed ◎{} bonus stake", + node_pubkey, + lamports_to_sol(config.bonus_stake_amount), + ), + )); + + datapoint_info!( + "validator-status", + ("cluster", config.cluster, String), + ("id", node_pubkey.to_string(), String), + ("slot", epoch_info.absolute_slot, i64), + ("ok", false, bool) + ); + } else { + // The validator is still considered current for the purposes of metrics reporting, + datapoint_info!( + "validator-status", + ("cluster", config.cluster, String), + ("id", node_pubkey.to_string(), String), + ("slot", epoch_info.absolute_slot, i64), + ("ok", true, bool) + ); + } + } + } + + if create_stake_transactions.is_empty() { + info!("All stake accounts exist"); + } else { + info!( + "{} SOL is required to create {} stake accounts", + lamports_to_sol(source_stake_lamports_required), + create_stake_transactions.len() + ); + if source_stake_balance < source_stake_lamports_required { + error!( + "Source stake account has insufficient balance: {} SOL, but {} SOL is required", + lamports_to_sol(source_stake_balance), + lamports_to_sol(source_stake_lamports_required) + ); + process::exit(1); + } + + let create_stake_transactions = + simulate_transactions(&rpc_client, create_stake_transactions)?; + let confirmations = transact( + &rpc_client, + config.dry_run, + create_stake_transactions, + &config.authorized_staker, + )?; + + if !process_confirmations(confirmations, None) { + error!("Failed to create one or more stake accounts. Unable to continue"); + process::exit(1); + } + } + + let delegate_stake_transactions = + simulate_transactions(&rpc_client, delegate_stake_transactions)?; + let confirmations = transact( + &rpc_client, + config.dry_run, + delegate_stake_transactions, + &config.authorized_staker, + )?; + + if !process_confirmations(confirmations, Some(¬ifier)) { + process::exit(1); + } + + Ok(()) +} diff --git a/stake-o-matic/src/whitelist.rs b/stake-o-matic/src/whitelist.rs new file mode 100644 index 0000000000..15ba80959c --- /dev/null +++ b/stake-o-matic/src/whitelist.rs @@ -0,0 +1,270 @@ +solana_sdk::pubkeys!( + testnet_validators, + [ + "123vij84ecQEKUvQ7gYMKxKwKF6PbYSzCzzURYA4xULY", + "234u57PuEif5LkTBwS7rHzu1XF5VWg79ddLLDkYBh44Q", + "23SUe5fzmLws1M58AnGnvnUBRUKJmzCpnFQwv4M4b9Er", + "2CGskjnksG9YwAFMJkPDwsKx1iRAXJSzfpAxyoWzGj6M", + "2DvsPbbKrBaJm7SbdVvRjZL1NGCU3MwciGCoCw42fTMu", + "2GAdxV8QafdRnkTwy9AuX8HvVcNME6JqK2yANaDunhXp", + "2JT1SRSm61vvHKErY2PCnHUtMsumoh69jrC7bojd9f1x", + "2Pik6jn6yLQVi8jmwvZCibTygPWvhh3pXoGJrGT3eVGf", + "2RYnM1C5XuzWzZu4sD7TyJTgxQTKzFVHG6jNtbK65q2y", + "2X5JSTLN9m2wm3ejCxfWRNMieuC2VMtaMWSoqLPbC4Pq", + "2XAHomUvH3LFjYSxzSfcbwS73JgynpQHfapMNMJ8isL9", + "2YLPihCDxqztR5be69jhoNDPMxV6KeTJ2X2LtVBXDgp4", + "2ZZkgKcBfp4tW8qCLj2yjxRYh9CuvEVJWb6e2KKS91Mj", + "2ZeChc7Res7fUVdcGCDTJfRd9N8R21hiBPLAuJsqHHwh", + "2ibbdJtxwzzzhK3zc7FR3cfea2ATHwCJ8ybcG7WzKtBd", + "2jYzt9Ly7dNzNpMV9sbHBNuwMEvVdSi9L8yJCTJT21ki", + "2jrM8c8ZhpX9CovseJ2sRsrU9yzsFrP7d3gCi5ESU5Rs", + "2tZoLFgcbeW8Howq8QMRnExvuwHFUeEnx9ZhHq2qX77E", + "2yDwZer11v2TTj86WeHzRDpE4HJVbyJ3fJ8H4AkUtWTc", + "31cud34DHkL5zM4ZiHXgsrkAu13Jeck7ahvkPU9i4Jze", + "33LfdA2yKS6m7E8pSanrKTKYMhpYHEGaSWtNNB5s7xnm", + "368KipD4nBzVs4AizHj1iU4TErSSqmZaNGVyzHx8TVXM", + "3ANJb42D3pkVtntgT6VtW2cD3icGVyoHi2NGwtXYHQAs", + "3FhfNWGiqDsy4xAXiS74WUb5GLfK7FVnn6kxt3CYLgvr", + "3HitRjngqhAgVuNdFwtR1Lp5tQavbJri8MvKUq5Jpw1N", + "3W4fe5WTAS4iPzBhjGP8a1LHBTx8vbscqThXT1THqEGC", + "3ckQZncmgmS1aZCC7Eot659hoBQ8k85ExddD7fu9qJ2c", + "3i7sS5McrJ7EzU8nbdA5rcXT9kNiSxLxhwyfuxbsDvBj", + "3pzTi41c6NAbZyTiyPEAQtmi2K5MyWZJMxx6nDvWPgnQ", + "3w6hQh7Ndx93eqbaEMLyR3BwqtRxT2XVumavvU93mcRk", + "3wz211BhQAE2n5fjDQSStM2iSizhNRyJDNRkDEc1YwMF", + "3xUTkgPKNJZ3dkpDMV8zWV34BkmvKanguKipv6M9x2Mt", + "47UuTGPAQZX2HnVcfxKk8b1BtA4rRTduVaHnvxzQe6AJ", + "4Bx5bzjmPrU1g74AHfYpTMXvspBt8GnvZVQW3ba9z4Af", + "4FZSiJpGgprsVxkzc2F8v3bgnRpk8Ez1Dq7ohXwY1q9V", + "4GhLBaxr1oEHWpoGnWh3mcRXUkBU5EEQZv3L27c7ohoq", + "4Nh8T1d4YBZHEuQNRmFbLXPT5HbWicqPxGeKZ5SdAr4i", + "4WufhXsUhPc7cdHXYxxDrYZVVLKa9jCDGC4ccfmuBvu2", + "4XWxphAh1Ji9p3dYMNRNtW3sbmr5Z1cvsGyJXJx5Jvfy", + "4YGgmwyqztpJeAi3pzHQ4Gf9cWrMHCjZaWeWoCK6zz6X", + "4ZtE2XX6oQThPpdjwKXVMphTTZctbWwYxmcCV6xR11RT", + "4dWYFeMhh2Q6bqXdV7CCd4mJC81im2k6CXCBKVPShXjT", + "4fBQr617DmhjekLFckh2JkGWNboKQbpRchNrXwDQdjSv", + "4gEKnFpiJ8XC6DdFw4D65uYQeMF8x7KDqMrBPrDVjMPb", + "4gMboaRFTTxQ6iPoH3NmxLw6Ux3SEAGkQjfrBT1suDZd", + "4vXPjSaZfydRqhnM85uFqDWqYcFyA744R2tjZQN8Nff4", + "4vgoKb76Z2vj9V9z7hoQpZkkwJrkL1z35LWNd9EXSi2o", + "55nmQ8gdWpNW5tLPoBPsqDkLm1W24cmY5DbMMXZKSP8U", + "55ofKaF1xdfgC9mB4zUhrffdx7CVoxTbNo7GeQLyj3YL", + "57DPUrAncC4BUY7KBqRMCQUt4eQeMaJWpmLQwsL35ojZ", + "58J9ucd9Qc6gMD8QHh2sHTyJyD8kdjHRQZkEAyAZ72YA", + "59WHuha1QunWmupWhFA4vr3WMUe8BLN7dc8HUsJ4YC86", + "5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on", + "5FPQXMJxXKJmuShQCiTRYPzXL9LBfgphprhXR54pr2eu", + "5H3sMCaSJdN2k1hyuFTzq2BrZHUq7CinTa82hJS6EDTf", + "5KZRD6hDCZd1dgM9rJLweFFRkHu3qAgBhHgQ96Wz1VSn", + "5MGfrpdVsyifhn2x62j6PnBWQk2c5xTUK1o8KFrwPLWG", + "5NH47Zk9NAzfbtqNpUtn8CQgNZeZE88aa2NRpfe7DyTD", + "5NorYZBbtgbouD3toX3761ZGbaYTWrNSDNci4G4zV8eo", + "5PLDu8auwqtMkHW9zdPsfUcvyESZ45umFc4r8cWUw3Zp", + "5TkrtJfHoX85sti8xSVvfggVV9SDvhjYjiXe9PqMJVN9", + "5dLMRyPWx6rdPGZpZ7uuZiqry96dUT5yz48u62Gzugi6", + "5qsT9h2TuTMPLtX6gcD2DG6mcZwechxmWhKRFHbCt6Pu", + "5rxRt2GVpSUFJTqQ5E4urqJCDbcBPakb46t6URyxQ5Za", + "5sjXEuFCerACmhdyhSmxGLD7TfvmXcg2XnPQP2o25kYT", + "5uTcsQSrUffYo6RYSWj75SuGMkJ4v9x5RYuQoTc5aWGR", + "5ueaf3XmwPAqk92VvUvQfFvwY1XycV4ZFoznxffUz3Hh", + "5vxoRv2P12q4K4cWPCJkvPjg6jYnuCYxzF3juJZJiwba", + "6PwxMMGLFnAf9sjMHfVr15z9fntjYTNPxJ7gFhkFxkXi", + "6TkKqq15wXjqEjNg9zqTKADwuVATR9dW3rkNnsYme1ea", + "6ZEbKFxTjEKGC9HUqzy9z4ccJ8Aq3ktPKEzHGDosQJo4", + "6nrkRvzUpTst8teZJawMFFHrmixJ2sxAUxPKrqoGwCB8", + "6qJPxxgZHCQKBvbGC9zCsuuPtHMMLszVCoiCvEhVULyJ", + "6t8zWy766tsHBVNxhMwsTGiEYkGtjaZncRU3vcSEYtHU", + "6zCt5z72rfN9sRk2hTgc1LeFDbEBfXYmW6xtSNmgyama", + "71bhKKL89U3dNHzuZVZ7KarqV6XtHEgjXjvJTsguD11B", + "74U9LiSPv2gb8bBZSh5uVNf89W4wZ8zs9B8EvRVVwr87", + "75yzn7njq32yY1ieCZxFNVFZQWtEbQHpaaG6dSZFfmX5", + "77uXenX1Y9T2D1pcnHnYsYiwTTHbnzkyrKX5fQFMGVCR", + "787PK2WaCUZCyYEmuYQSGmoxu7MyqK1usn43FfiVwhcB", + "7FnrBgjPb1y8PNjzRLihQWUvky37F7wkvRb7MUL89Q8P", + "7X3csFXUN2AZph83GC2FZCpkCTZXfVWssaJ72cpwG96w", + "7arfejY2YxX9QrmzHrhu3rG3HofjMqKtfBzQLf8s3Wop", + "7dEjSFnrm66CJ7Aj5mC1hsYmMzmGgWPr6iZNhcvANZ1w", + "7mmRxJNttYcJVNsJmiLjTHYmNDt32EpzUSgLDoBsTKKK", + "7yiFPLeyH2B2p3xGrr2Y5nmk8B52nEaa6j55Nk3u6648", + "7yzVecfpWupdJwVQby3inMggGSotFSnSrhvPGPp6JGdU", + "836riBS2E6qfxjnrTkQdzD1JkFAoQDyUjTmzi38Gg84w", + "84BoHfEzq1FccYEMZPeuTwc68DQC7LS6Bm5dRMsKypA5", + "86hxQGfre1vvVsYP7jPBka8ySrECZQewjG3MfxoCoBYf", + "87VQhN7dUfS9wacre7vqRm561bNUk5PwUB8xmroc2yEw", + "8ASvX43WvDF4LHe2ioNUbn3A4ZRD9iC6rsxyjo5sVGLp", + "8FaFEcUFgvJns6RAU4dso3aTm2qfzZMt2xXtSgCh3kn9", + "8NndwQsrH4f6xF6DW1tt7ESMEJpKz346AGqURKMXcNhT", + "8PTjAikKoAybKXcEPnDSoy8wSNNikUBJ1iKawJKQwXnB", + "8XvoJswfYCxWf2WkUmNBjtDWwonmKugYhxBruNpSfS4a", + "8ZjS3d1bQihC3p5voM8by2xi5PoBNxzTJtaQ9rvxUbbB", + "8aZ5CJf9qYnQtT2XYuDKASj2pCiPwhWoNsc2rBqc9s1n", + "8dHEsm9aLBt7q6zu3ESfRXkS2eCwkbbzzynfd2QxDzms", + "8diJdQj3y4QbkjfnXr95SXoktiJ1ad965ZkeFsmutfyz", + "8fLtWUfZSpAJk7h4XhvM6TqGjXQxiwzWkymxmGtJoGdu", + "8noQwzDhpb67yzfREDKvymKWtSdPZtbfjm3pxPYA4bag", + "8nvD1CUE48WdcmRdvbyWcM5LdJKRTNP3tXT6Qp2CSND5", + "8oRw7qpj6XgLGXYCDuNoTMCqoJnDd6A8LTpNyqApSfkA", + "8onJp7KyshoMcxVm5CemhPgGvA1hdSnHbcjLCvJidV8o", + "8t6UUXRkQTBpanRoMjxNxio1baXXkEdeLniCVJGMdzLJ", + "8uJR7fEaTiriYLe4YMFkeeADdRkexxm8jkFCGRjPBvJD", + "8xFQP5mPt9Nzr5wMeUsH4azMM3zyGqrUn3LeopKSwptX", + "8yS3Zc45xptsaay9iaUSpfdb5gaKcQaKAShVvEUFKpeN", + "9Q8xe8KgzVf2tKwdXgjNaYdJwvChihmjhcHdae7c4jPb", + "9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv", + "9a95Qsywtw2LKK9GkiR9dpTnoPGMVrLBLTAMPnaUwDjY", + "9dCMmPpfNuWKyZ2D1iRstMCc1rhr2DbHVFrZ9wFncQjp", + "9fMqL641B7nQZ1xktU35qXuFESxMB7pkqmqVANtovzmE", + "9gmnbM2GUVXiTfCg1Pj3ZTJdqyKdS81kjBWwwnZbS4MR", + "9mbQ9mrRjBGiUVnz9Tdf7PuAkWomw3GLZ6Sup3R93Gw8", + "9oKrJ9iiEnCC7bewcRFbcdo4LKL2PhUEqcu8gH2eDbVM", + "9qpfxCUAPyaYPchHgRGXmNDDhPiNmJR39Lfx4A49Uh1P", + "9uEazQxpRTyYX1hHMMySDBWad7zb54K9PkHKeZemK2m7", + "9waRyqWAoP68etU17DdWamgpTnPb3skY7Er9kRZMzCfS", + "A5hMwgm8QfooAuCMw9Rw2S9vXbBwCknFMhhUwKKHvYeJ", + "A9XUvhm5yKVs9Z3tYdyiAYRx9mNr2rqnv2VkY8D1N4uZ", + "ACv5dTk7THbmUpHYGhgPzMhWr7oqHSkuPJpa5RfvmG5H", + "AEPNDgaApdcfZEZpww458Az9i2NZrwxsVCdiUih4EaRh", + "AKqueA5Vfmf6BWTXuPdWxrYCDNPGi5gDLrNpdc1CSEzy", + "AbF88hkkpZ28VaT3vYn4xu5CeNC8G6Dq9cc8ciRR4fY5", + "Ag51oCkwGy5rkbGEYrcP9GDjvFGMJrEdLxvedLSTSR15", + "AiWqv1dqsbvkUMec7G4DmM88ka7SaoqDPkn5U2iuvqnn", + "AicKhNhJmkdqafRDjKLPgVqLzXLzJ8pS6aVrYrRkq1iq", + "AkVMbJq8pqKEe87uFaxjmt35tX2cNhUJTJwv13iioHu7", + "AkkJv1meyo2Ax2XTXEXWpvHTh4F8a68Lja5dx3TaX47K", + "AmNRXJSSaGJrXaYBahD1PBoBR2ApkfLBAJ7SiRK4772R", + "AoUwfPuiEek2thVRDhMP7HbQb9rguyab4rDiz2NAfwwA", + "Aom2EwxRjtcCZBDwqvaZiEZDPgmw4AsPQGVrsLa2srCg", + "B4xFUYq2TDmz6PsiCj29vFyvmYvqTRAtnhQ3uSmxQVFd", + "B52Da5MCyTcyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY", + "B5FNFrfrrfpBggFBp9h6Js93mrz1Tham1z1GgP3bDgNc", + "B6ZramCQhQWcq4Vxo3H5y1MYvo37wRZiAwKk88qaYNiF", + "B8T2dbvYaM4gJfpLLbuBScuWKFhnG6KspRK72v5D5uoK", + "BLfpk2WoF8RnerCqjxHe7qFJjpEE427riMkf2fS6vUB6", + "BMZY98zbjg2ey4XNfhBQXhEuvVqzaJ1T3AKD2quL3wnK", + "BSF2yD9mqzaixDaLEraF1en82EWaXx7wbaCqSuKppqG5", + "BV8sS1jn1AvGAptY5TxNZdcm7aa49MZCXSpXQjzjdnYG", + "BWiftESMUsve87rkjU7HsaA7fkiJRAbv3xZLQrmKZtnz", + "BhbARoxdh2MT3vb4awXraZFPzSwBdmF9pGgURKNsjBqC", + "C4N1bMSzbfDwHGMitxyufNZPaAkNYx8vJxHRnHWrptAT", + "CM1c6z3pRNgHFcfZG4z3wE31jaR8c4gCYQBJVEoCUyq8", + "CPPVEbGFbX3XAThetvfveCE1vYLWUwwJGT7DxkPAWb8D", + "CV3F19YAhoW7DpfHQ5W9t2Zomb9h21NRi8k6hCA36Sk6", + "CZWpCTN4rCWer8fm5ZqFdx82CDiCJjZLKZ5Ti2gdmchQ", + "CcorN3BoG1XMZehZ9Xib9YLo4mcvo7pzeVurC28gYYqX", + "ChorusM5BVgnAKbg9PF15285LkqeCoZWK2p9s35T7J2A", + "CiY5RjWPs1XyegKyBLcG7Ue7YMf98eiEnmvqnSuSKbob", + "CjhKCjNC1WUgBjAGst3D2XmSsWHvt3zGasasmogPTY6J", + "Co7UqfqzXzTjhBwvam3zhNi4p8dKtdSrfh6rQykoNMy7", + "CsaAGRau3ZvyMQvJ9CWSqbqeVv9zw2Am8FhnL9sr6jTk", + "Csrv9JCbebTKu1uBWqkfwuPHwVCXsYDrQmeXf19onbsY", + "CtxU5HwVbgspJVtWxwjuP8wXUMdkjYJ4EJwJ3jvZh4zu", + "D23NCAVxinE53BTemguZCheAqCdMGfNTUzWdoWvq4Xj", + "D2NjDkcv8Y1dWGdtWAKPT4em2D3sYzM8AzMTpCG1RVf7", + "D52Q6Ap8RVMw1EvJYTdEABP6M5SPg98aToMcqw7KVLD9", + "D5JqF3qkLkeJKKEi145oMseEGc1ym9cWKtBKtg4ZBBnN", + "D71JRzjPpHipt8NAWnWb3yZoXezbkGXqSf7TVCir6wvT", + "DFb6qaAkd5DTnFVYLDjzJNfsUPygP8GHHebN1CBv25cf", + "DJvMQcb3ZtXC49LsaMvAo4x1rzCxjNfBfZtvkUeR4mAx", + "DKnZytVA5wKbNPYW1pvPpoE5YeSsxu12KJFa95gBAGm7", + "DV78gathrorcpWsWrUkWrWNowLXpizKsPBupStzeAJnL", + "DaB3ZwVtGLzSjazk5STQEu3MkJR2nkK3tDdCPAvx9QpM", + "DadnDZbFH5BHHRHD7TaobaSQ7QATXgvWegHUcZ7ZGzmW", + "Db2V7nPHc4sPHne87nYXPGn8Kv8rMsiWCAjgAXmpqcpC", + "DciwdVV1DXimdsgRGQuQ45zYVjZNaof6a6EZ1JjaCsvx", + "DfTeDaxk4RufkVbykedVnqa1r9S3z3oKFYL3FFmPdr1o", + "Dq5r3zG6XGBcXNDRSWPSc7DiWwjqcGoiVcEhZ9mXEAaV", + "Drkj3wbHHmE2iCnqXHKFTmwPkuSc4bsFdgAmqv6eXuWi", + "DsnqNtwKA817a2VQypWEzaRXY2soq5Jgc68MgFBMR35p", + "Dx4bMuKpGaxAnd53QYDyKhD45PjuFLx16mrgoRK36STf", + "DzxNmWD99qvkPdDR94ojXhUrpzT8VdqB5ktYX7fZr4gc", + "EBxhSfAWW2Cfouvj1k242W6U8krZVAxJS47SG8UKb4ch", + ] +); + +solana_sdk::pubkeys!( + mainnet_beta_validators, + [ + "23SUe5fzmLws1M58AnGnvnUBRUKJmzCpnFQwv4M4b9Er", + "2NJZ1Ajcwtc7hZdVXTXrh2SAiAXnFkVm6MWcGjBZfPkS", + "2Ue9zGmDnvYRrJNEjuAdNkbbickw6fKWtbeNM7T2rakg", + "2jS8AX38m8F9C5juToW1FmTufEbb1DfDzZJj9HSJcWwo", + "2mMGsb5uy1Q4Dvezr8HK2E8SJoChcb2X7b61tJPaVHHd", + "2us4ysyNvYJHkYi7CtRuW413Mwi34kjjFQGhZDch4DEN", + "2vj1Ggh29cQTCL8RGqKF3Mn1pUHd9GMUGz6VjPXfgaiH", + "34viN9UrGJaVabrrSZDs8MnKwVt34nw2wv4Xkwk64shV", + "3B2mGaZoFwzAnWCoZ4EAKdps4FbYbDKQ48jo8u1XWynU", + "3KNGMiXwhy2CAWVNpLoUt25sNngFnX1mZpaiEeVccBA6", + "3RXKQBRv7xKTQeNdLSPhCiD4QcUfxEQ12rtgUkMf5LnS", + "3fA6TU7fQCkNDDKYJeCY4Ag2gCatEsYnYL4SpkSDYfCw", + "3nRhescC7HMYC8gKti3ENiBe8LnKZUs2gzYPAjYniQCP", + "4ULWSuaNnhQntP3DVxg1xa4yeNLNpDnAw3gTtrhPHzEA", + "4XspXDcJy3DWZsVdaXrt8pE1xhcLpXDKkhj9XyjmWWNy", + "4YGgmwyqztpJeAi3pzHQ4Gf9cWrMHCjZaWeWoCK6zz6X", + "4h5muqwz35tyPQdAXkZMyVM5cnGN5oXouTZL2AFA1Fjh", + "4tS3UZfuRHzXuPenvErtRPtnZfY1KHhT96JBCQsLzKqW", + "5XKJwdKB2Hs7pkEXzifAysjSk6q7Rt6k5KfHwmAMPtoQ", + "5ya8UPujuXN8cys1EaaqMMH3auby3HTHZki73Q4Yfkff", + "6BDNnr38moGRQyvx1Ehs9cM6tJDFK7LF6mUvLziyNrzW", + "6TkKqq15wXjqEjNg9zqTKADwuVATR9dW3rkNnsYme1ea", + "6cgsK8ph5tNUCiKG5WXLMZFX1CoL4jzuVouTPBwPC8fk", + "6iSLGrFY1zCktMVkALyGqcQVqp4rUmKkozHG23EXwPwt", + "6wsSvrZPbjWeNNZ92KWtj94pdHj8v8sRbKsu1ZSpztpP", + "6yf57R7U1J1VXszE7CobdYEWQNJMPRfWEgGfaRsVNk32", + "7CyNBLaoav9fZhX4D2WGrL5XCuMroSgDut68vtL8NB9p", + "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2", + "7nYNfGS6VVxzCZmfbLGpsXYFm2LS9XRrva9hZahFmpqh", + "8GLRbAstsabZuZUx73AoyfGi1FRCWSUhRgMugFyofEz7", + "8LSwP5qYbmuUfKLGwi8XaKJnai9HyZAJTnBovyWebRfd", + "8ebFZA8NPLBZD91CwsG1HWQsa2B5Ludgdyf5Hi3sYhhs", + "8gcMmaEAXfRZKcXHeHV2x8R1ebb78inTr9xhhEuNNoTt", + "8pyp3vfVPRziYdAYEyqkwytdBbdVbQmHqfQAVDcRV3w", + "9CqDvvGvSNVZDo5RskCAv4fTubpFCs9RLTrjUxEYrvNA", + "9H61MfefWLuCr8aoUTrqLN94ZZnu68pEqMHhnYxFSB4y", + "9JJQN1WpJ8QvH6XK1xAMbSpgHSiwqBgWaeCh3ViEFmtN", + "9xg5FUCSJvvFkUF2dLoSHAR72DnA3qQqzTzWhHibZ33c", + "A79u1awz7CqnxmNYEVtzWwSzup3eKPNW6w2Jrd56oZ3y", + "AoUwfPuiEek2thVRDhMP7HbQb9rguyab4rDiz2NAfwwA", + "ArmanP3SBD1DVSoKCqK7d6S2kLpyncYey2Ybf3o5XkTn", + "Awes4Tr6TX8JDzEhCZY2QVNimT6iD1zWHzf1vNyGvpLM", + "AyYqAhyCCxRrNGbm3dY4aGY9SyaQC9UvPTSHvMQK4YW2", + "BZBKHmW1DhBaAPojxWBQ26vGz42Y7MtNviFZWpc6nGLb", + "CAo1dCGYrB6NhHh5xb1cGjUiu86iyCfMTENxgHumSve4", + "CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S", + "Certusm1sa411sMpV9FPqU5dXAYhmmhygvxJ23S6hJ24", + "ChorusmmK7i1AxXeiTtQgQZhQNiXYU84ULeaYF1EH15n", + "CjmXSapt1ouz3CZzgkRJckBEwMSo5fVdVrizLeRscwYD", + "D6pUrfgc5ZyXSfgtCBYozydRSz92pse1S7AZP58muEYk", + "DDnAqxJVFo2GVTujibHt5cjevHMSE9bo8HJaydHoshdp", + "DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ", + "DNWsTLvsMixgUcbM93437U8DWmJ9bZikQeBxQLHbeH5L", + "Dokia75SVtetShgapUBoVFfYjL99fQyr1twxKKyTZKa3", + "Dq3piY2ZcBvNN84j2EhDLtTzRAw95za7Eau89pNcmSd5", + "E6cyDdEH8fiyCTusmWcZVhapAvvp2LK24zMLg4KrrAkt", + "Ev8D9dwYdfebkdLgAjwiJtCkqS882Uvrit5qN6NTeHMy", + "EvnRmnMrd69kFdbLMxWkTn1icZ7DCceRhvmb2SJXqDo4", + "FGiEdzde7Fco2WLpNQMat299hUVoykJdaA5hxdmCzHiS", + "Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk", + "FopTvQaGp6K5FadWKZtsLJmrX7gnNGFS2fQ7rv5KHyE1", + "Fudp7uPDYNYQRxoq1Q4JiwJnzyxhVz37bGqRki3PBzS", + "G2TBEh2ahNGS9tGnuBNyDduNjyfUtGhMcssgRb8b6KfH", + "GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ", + "HJFFKSJxhoeRCoe3xJPyCaS6sGCnknXcLfH3Vg7fBh2M", + "HahmUFR44BXFP7fVLsnd4pyaE7GoN1KKV1hdL2eVUpok", + "HavuVVDXXsJqMzPwQ4KcF5kFm2xqjbChhyi1bgGeCQif", + "HzrEstnLfzsijhaD6z5frkSE2vWZEH5EUfn3bU9swo1f", + "HzvGtvXFzMeJwNYcUu5pw8yyRxF2tLEvDSSFsAEBcBK2", + "LunaowJnt875WWoqDkhHhE93SNYHa6tfFNVn1rqc57c", + "MCFmmmXdzTKjBEoMggi8JGFJmd856uYSowuH2sCU5kx", + "SFundNVpuWk89g211WKUZGkuu4BsKSp7PbnmRsPZLos", + "XkCriyrNwS3G4rzAXtG5B1nnvb5Ka1JtCku93VqeKAr", + "a1phaKk6UbG1P2ZCpfMVFUeRM5E2EZhGvUjqWHRsrip", + "ateamuvZX4iy2xYrNrMyGgwtTpWFiurNzHpmJMZwjar", + "fishfishrD9BwrQQiAcG6YeYZVUYVJf3tb9QGQPMJqF", + "forb5u56XgvzxiKfRt4FVNFQKJrd2LWAfNCsCqL6P7q", + "nCN1wrDwLYg3zBZ8DbDt6dDHJAtWk6Ms1VK5neZ1fQt", + "spcti6GQVvinbtHU9UAkbXhjTcBJaba1NVx4tmK4M5F", + "superCMS6AucZe9aykaks7kUAj3oqB52yMMV81A8exa", + "uEhHSnCXvWgtgvVaYscPHjG13G3peMmngQQ2ghC54i3", + ] +);