parent
d6d032dd49
commit
417f0e41fa
|
@ -4651,6 +4651,24 @@ dependencies = [
|
||||||
"tempfile",
|
"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]]
|
[[package]]
|
||||||
name = "solana-stake-program"
|
name = "solana-stake-program"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
|
@ -24,6 +24,7 @@ members = [
|
||||||
"logger",
|
"logger",
|
||||||
"log-analyzer",
|
"log-analyzer",
|
||||||
"merkle-tree",
|
"merkle-tree",
|
||||||
|
"stake-o-matic",
|
||||||
"streamer",
|
"streamer",
|
||||||
"measure",
|
"measure",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub struct RpcClientRequest {
|
||||||
|
|
||||||
impl RpcClientRequest {
|
impl RpcClientRequest {
|
||||||
pub fn new(url: String) -> Self {
|
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 {
|
pub fn new_with_timeout(url: String, timeout: Duration) -> Self {
|
||||||
|
|
|
@ -1338,7 +1338,6 @@ impl RpcSol for RpcSolImpl {
|
||||||
let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?;
|
let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?;
|
||||||
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||||
let tpu_addr = get_tpu_addr(&meta.cluster_info)?;
|
let tpu_addr = get_tpu_addr(&meta.cluster_info)?;
|
||||||
trace!("send_transaction: leader is {:?}", &tpu_addr);
|
|
||||||
transactions_socket
|
transactions_socket
|
||||||
.send_to(&wire_transaction, tpu_addr)
|
.send_to(&wire_transaction, tpu_addr)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
|
|
|
@ -100,6 +100,7 @@ else
|
||||||
solana-net-shaper
|
solana-net-shaper
|
||||||
solana-stake-accounts
|
solana-stake-accounts
|
||||||
solana-stake-monitor
|
solana-stake-monitor
|
||||||
|
solana-stake-o-matic
|
||||||
solana-sys-tuner
|
solana-sys-tuner
|
||||||
solana-tokens
|
solana-tokens
|
||||||
solana-validator
|
solana-validator
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
target/
|
||||||
|
*.csv
|
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
|
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"]
|
|
@ -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.
|
|
@ -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<Pubkey>,
|
||||||
|
|
||||||
|
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<String>>(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::<HashSet<_>>();
|
||||||
|
|
||||||
|
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<Pubkey>, HashSet<Pubkey>), Box<dyn error::Error>> {
|
||||||
|
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<Slot> = 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<u64, Box<dyn error::Error>> {
|
||||||
|
// 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<Vec<(Transaction, String)>> {
|
||||||
|
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<Vec<ConfirmedTransaction>, Box<dyn error::Error>> {
|
||||||
|
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<TransactionStatus>,
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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<ConfirmedTransaction>,
|
||||||
|
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<dyn error::Error>> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -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",
|
||||||
|
]
|
||||||
|
);
|
Loading…
Reference in New Issue