248 lines
7.9 KiB
Rust
248 lines
7.9 KiB
Rust
use crate::utils;
|
|
use log::*;
|
|
use solana_client::{client_error::Result as ClientResult, rpc_client::RpcClient};
|
|
use solana_notifier::Notifier;
|
|
use solana_sdk::{
|
|
clock::Slot,
|
|
epoch_schedule::EpochSchedule,
|
|
message::Message,
|
|
native_token::sol_to_lamports,
|
|
pubkey::Pubkey,
|
|
signature::{Keypair, Signer},
|
|
transaction::Transaction,
|
|
};
|
|
use solana_stake_program::{
|
|
stake_instruction,
|
|
stake_state::{Authorized as StakeAuthorized, Lockup},
|
|
};
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
rc::Rc,
|
|
str::FromStr,
|
|
thread::sleep,
|
|
time::Duration,
|
|
};
|
|
|
|
// The percentage of leader slots that validators complete in order to receive the stake
|
|
// reward at the end of a TPS round.
|
|
const MIN_LEADER_SLOT_PCT: f64 = 80.0;
|
|
|
|
#[derive(Default)]
|
|
pub struct LeaderRecord {
|
|
total_slots: u64,
|
|
missed_slots: u64,
|
|
}
|
|
|
|
impl LeaderRecord {
|
|
pub fn completed_slot_pct(&self) -> f64 {
|
|
if self.total_slots == 0 {
|
|
0f64
|
|
} else {
|
|
let completed_slots = self.total_slots - self.missed_slots;
|
|
100f64 * completed_slots as f64 / self.total_slots as f64
|
|
}
|
|
}
|
|
|
|
pub fn healthy(&self) -> bool {
|
|
self.completed_slot_pct() >= MIN_LEADER_SLOT_PCT
|
|
}
|
|
}
|
|
|
|
/// Calculate the leader record for each active validator
|
|
pub fn calculate_leader_records(
|
|
rpc_client: &RpcClient,
|
|
epoch_schedule: &EpochSchedule,
|
|
start_slot: Slot,
|
|
end_slot: Slot,
|
|
notifier: &Notifier,
|
|
) -> ClientResult<HashMap<Pubkey, LeaderRecord>> {
|
|
let start_epoch = epoch_schedule.get_epoch(start_slot);
|
|
let end_epoch = epoch_schedule.get_epoch(end_slot);
|
|
let confirmed_blocks: HashSet<_> = rpc_client
|
|
.get_confirmed_blocks(start_slot, Some(end_slot))?
|
|
.into_iter()
|
|
.collect();
|
|
|
|
let mut leader_records = HashMap::<Pubkey, LeaderRecord>::new();
|
|
for epoch in start_epoch..=end_epoch {
|
|
let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch);
|
|
let start_slot = std::cmp::max(start_slot, first_slot_in_epoch);
|
|
let last_slot_in_epoch = epoch_schedule.get_last_slot_in_epoch(epoch);
|
|
let end_slot = std::cmp::min(end_slot, last_slot_in_epoch);
|
|
|
|
rpc_client
|
|
.get_leader_schedule(Some(start_slot))?
|
|
.unwrap_or_else(|| utils::bail(notifier, "Error: Leader schedule was not found"))
|
|
.into_iter()
|
|
.map(|(pk, s)| (Pubkey::from_str(&pk).unwrap(), s))
|
|
.for_each(|(pubkey, leader_slots)| {
|
|
let mut record = leader_records.entry(pubkey).or_default();
|
|
for slot_index in leader_slots.iter() {
|
|
let slot = (*slot_index as u64) + first_slot_in_epoch;
|
|
if slot >= start_slot && slot <= end_slot {
|
|
record.total_slots += 1;
|
|
if !confirmed_blocks.contains(&slot) {
|
|
record.missed_slots += 1;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Ok(leader_records)
|
|
}
|
|
|
|
pub fn fetch_active_validators(rpc_client: &RpcClient) -> HashMap<Pubkey, Pubkey> {
|
|
match rpc_client.get_vote_accounts() {
|
|
Err(err) => {
|
|
warn!("Failed to get_vote_accounts(): {}", err);
|
|
HashMap::new()
|
|
}
|
|
Ok(vote_accounts) => vote_accounts
|
|
.current
|
|
.into_iter()
|
|
.filter_map(|info| {
|
|
if let (Ok(node_pubkey), Ok(vote_pubkey)) = (
|
|
Pubkey::from_str(&info.node_pubkey),
|
|
Pubkey::from_str(&info.vote_pubkey),
|
|
) {
|
|
Some((node_pubkey, vote_pubkey))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// Endlessly retry stake delegation until success
|
|
fn delegate_stake(
|
|
rpc_client: &RpcClient,
|
|
faucet_keypair: &Keypair,
|
|
vote_account_pubkey: &Pubkey,
|
|
sol_gift: u64,
|
|
) {
|
|
let stake_account_keypair = Keypair::new();
|
|
info!(
|
|
"delegate_stake: stake pubkey: {}",
|
|
stake_account_keypair.pubkey()
|
|
);
|
|
let mut retry_count = 0;
|
|
loop {
|
|
let recent_blockhash = loop {
|
|
match rpc_client.get_recent_blockhash() {
|
|
Ok(response) => break response.0,
|
|
Err(err) => {
|
|
error!("Failed to get recent blockhash: {}", err);
|
|
sleep(Duration::from_secs(5));
|
|
}
|
|
}
|
|
};
|
|
|
|
let instructions = stake_instruction::create_account_and_delegate_stake(
|
|
&faucet_keypair.pubkey(),
|
|
&stake_account_keypair.pubkey(),
|
|
&vote_account_pubkey,
|
|
&StakeAuthorized::auto(&faucet_keypair.pubkey()),
|
|
&Lockup::default(),
|
|
sol_to_lamports(sol_gift as f64),
|
|
);
|
|
let message = Message::new(&instructions, Some(&faucet_keypair.pubkey()));
|
|
let transaction = Transaction::new(
|
|
&[faucet_keypair, &stake_account_keypair],
|
|
message,
|
|
recent_blockhash,
|
|
);
|
|
|
|
// Check if stake was delegated but just failed to confirm on an earlier attempt
|
|
if retry_count > 0 {
|
|
if let Ok(stake_account) = rpc_client.get_account(&stake_account_keypair.pubkey()) {
|
|
if stake_account.owner == solana_stake_program::id() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Err(err) = rpc_client.send_and_confirm_transaction(&transaction) {
|
|
error!(
|
|
"Failed to delegate stake (retries: {}): {}",
|
|
retry_count, err
|
|
);
|
|
retry_count += 1;
|
|
sleep(Duration::from_secs(5));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Announce validator status leader slot performance
|
|
pub fn announce_results(
|
|
starting_validators: &HashMap<Pubkey, Pubkey>,
|
|
remaining_validators: &HashMap<Pubkey, Pubkey>,
|
|
pubkey_to_keybase: Rc<dyn Fn(&Pubkey) -> String>,
|
|
leader_records: &HashMap<Pubkey, LeaderRecord>,
|
|
notifier: &mut Notifier,
|
|
) {
|
|
let buffer_records = |keys: Vec<&Pubkey>, notifier: &mut Notifier| {
|
|
if keys.is_empty() {
|
|
notifier.send("* None");
|
|
return;
|
|
}
|
|
|
|
let mut validators = vec![];
|
|
for pubkey in keys {
|
|
let name = pubkey_to_keybase(pubkey);
|
|
if let Some(record) = leader_records.get(pubkey) {
|
|
validators.push(format!(
|
|
"* {} ({:.1}% leader efficiency)",
|
|
name,
|
|
record.completed_slot_pct()
|
|
));
|
|
}
|
|
}
|
|
validators.sort();
|
|
notifier.send(&validators.join("\n"));
|
|
};
|
|
|
|
let healthy: Vec<_> = remaining_validators
|
|
.keys()
|
|
.filter(|k| leader_records.get(k).map(|r| r.healthy()).unwrap_or(false))
|
|
.collect();
|
|
|
|
let unhealthy: Vec<_> = remaining_validators
|
|
.keys()
|
|
.filter(|k| leader_records.get(k).map(|r| !r.healthy()).unwrap_or(true))
|
|
.collect();
|
|
|
|
let inactive: Vec<_> = starting_validators
|
|
.keys()
|
|
.filter(|k| !remaining_validators.contains_key(k))
|
|
.collect();
|
|
|
|
notifier.send("Healthy Validators:");
|
|
buffer_records(healthy, notifier);
|
|
notifier.send("Unhealthy Validators:");
|
|
buffer_records(unhealthy, notifier);
|
|
notifier.send("Inactive Validators:");
|
|
buffer_records(inactive, notifier);
|
|
}
|
|
|
|
/// Award stake to the surviving validators by delegating stake to their vote account
|
|
pub fn award_stake(
|
|
rpc_client: &RpcClient,
|
|
faucet_keypair: &Keypair,
|
|
voters: Vec<(String, &Pubkey)>,
|
|
sol_gift: u64,
|
|
notifier: &mut Notifier,
|
|
) {
|
|
let mut buffer = vec![];
|
|
|
|
for (node_pubkey, vote_account_pubkey) in voters {
|
|
info!("Delegate {} SOL to {}", sol_gift, node_pubkey);
|
|
delegate_stake(rpc_client, faucet_keypair, vote_account_pubkey, sol_gift);
|
|
buffer.push(format!("Delegated {} SOL to {}", sol_gift, node_pubkey));
|
|
}
|
|
notifier.send(&buffer.join("\n"));
|
|
}
|