//! Ramp up TPS for Tour de SOL until all validators drop out mod results; mod stake; mod tps; mod utils; mod voters; use clap::{crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg}; use log::*; use results::Results; use solana_client::rpc_client::RpcClient; use solana_metrics::datapoint_info; use solana_sdk::{genesis_config::GenesisConfig, signature::read_keypair_file}; use solana_stake_program::config::{id as stake_config_id, Config as StakeConfig}; use std::{ collections::HashMap, fs, path::PathBuf, process::{exit, Command}, rc::Rc, thread::sleep, time::Duration, }; const NUM_BENCH_CLIENTS: usize = 2; const TDS_ENTRYPOINT: &str = "tds.solana.com"; const TMP_LEDGER_PATH: &str = ".tmp/ledger"; const FAUCET_KEYPAIR_PATH: &str = "faucet-keypair.json"; const PUBKEY_MAP_FILE: &str = "validators/all-username.yml"; const RESULTS_FILE: &str = "results.yml"; const DEFAULT_TX_COUNT_BASELINE: &str = "5000"; const DEFAULT_TX_COUNT_INCREMENT: &str = "5000"; const DEFAULT_TPS_ROUND_MINUTES: &str = "60"; const THREAD_BATCH_SLEEP_MS: &str = "1000"; const DEFAULT_INITIAL_SOL_BALANCE: &str = "1"; // Transaction count increments linearly each round fn tx_count_for_round(tps_round: u32, base: u64, incr: u64) -> u64 { base + u64::from(tps_round - 1) * incr } // Gift will double the staked lamports each round. fn gift_for_round(tps_round: u32, initial_balance: u64) -> u64 { if tps_round > 1 { initial_balance * 2u64.pow(tps_round - 2) } else { 0 } } #[allow(clippy::cognitive_complexity)] fn main() { solana_logger::setup_with_default("solana=debug"); solana_metrics::set_panic_hook("ramp-tps"); let mut notifier = solana_notifier::Notifier::default(); let matches = App::new(crate_name!()) .about(crate_description!()) .version(crate_version!()) .arg( Arg::with_name("faucet_keypair_path") .long("faucet-keypair-path") .short("k") .value_name("PATH") .takes_value(true) .default_value(FAUCET_KEYPAIR_PATH) .help("Path to the faucet keypair for stake award distribution"), ) .arg( Arg::with_name("net_dir") .long("net-dir") .value_name("DIR") .takes_value(true) .help("The directory used for running commands on the cluster"), ) .arg( Arg::with_name("pubkey_map_file") .long("pubkey-map-file") .value_name("FILE") .default_value(PUBKEY_MAP_FILE) .takes_value(true) .help("YAML file that maps validator identity pubkeys to keybase user id"), ) .arg( Arg::with_name("results_file") .long("results-file") .value_name("FILE") .default_value(RESULTS_FILE) .takes_value(true) .help("YAML file that lists the results for each round"), ) .arg( Arg::with_name("round") .long("round") .value_name("NUM") .takes_value(true) .default_value("1") .help("The starting round of TPS ramp up"), ) .arg( Arg::with_name("round_minutes") .long("round-minutes") .value_name("NUM") .takes_value(true) .default_value(DEFAULT_TPS_ROUND_MINUTES) .help("The duration in minutes of a TPS round"), ) .arg( Arg::with_name("tx_count_baseline") .long("tx-count-baseline") .value_name("NUM") .takes_value(true) .default_value(DEFAULT_TX_COUNT_BASELINE) .help("The tx-count of round 1"), ) .arg( Arg::with_name("tx_count_increment") .long("tx-count-increment") .value_name("NUM") .takes_value(true) .default_value(DEFAULT_TX_COUNT_INCREMENT) .help("The tx-count increment for the next round"), ) .arg( Arg::with_name("initial_balance") .long("initial-balance") .value_name("SOL") .takes_value(true) .default_value(DEFAULT_INITIAL_SOL_BALANCE) .help("The number of SOL that each partipant started with"), ) .arg( Arg::with_name("entrypoint") .short("n") .long("entrypoint") .value_name("HOST") .takes_value(true) .default_value(TDS_ENTRYPOINT) .validator(utils::is_host) .help("The entrypoint used for RPC calls"), ) .arg( Arg::with_name("stake_activation_epoch") .long("stake-activation-epoch") .value_name("NUM") .takes_value(true) .help("The stake activated in this epoch must fully warm up before the first round begins"), ) .arg( Arg::with_name("destake_net_nodes_epoch") .long("destake-net-nodes-epoch") .value_name("NUM") .takes_value(true) .default_value("9") .help("The epoch for which to run destake-net-nodes.sh at"), ) .get_matches(); let pubkey_map_file = value_t_or_exit!(matches, "pubkey_map_file", String); let pubkey_map: HashMap = serde_yaml::from_reader(fs::File::open(&pubkey_map_file).unwrap_or_else(|err| { eprintln!( "Error: Unable to open --pubkey-map-file {}: {}", pubkey_map_file, err ); exit(1); })) .unwrap_or_else(|err| { eprintln!( "Error: Unable to parse --pubkey-map-file {}: {}", pubkey_map_file, err ); exit(1); }); let pubkey_to_keybase = Rc::new(move |pubkey: &solana_sdk::pubkey::Pubkey| -> String { let pubkey = pubkey.to_string(); match pubkey_map.get(&pubkey) { Some(keybase) => format!("{} ({})", keybase, pubkey), None => pubkey, } }); let net_dir = value_t_or_exit!(matches, "net_dir", String); let faucet_keypair_path = value_t_or_exit!(matches, "faucet_keypair_path", String); let faucet_keypair = read_keypair_file(&faucet_keypair_path) .unwrap_or_else(|err| panic!("Unable to read {}: {}", faucet_keypair_path, err)); let mut tps_round = value_t_or_exit!(matches, "round", u32).max(1); let results_file_name = value_t_or_exit!(matches, "results_file", String); let previous_results = Results::read(&results_file_name); let mut tps_round_results = Results::new(results_file_name, previous_results, tps_round); let tx_count_baseline = value_t_or_exit!(matches, "tx_count_baseline", u64); let tx_count_increment = value_t_or_exit!(matches, "tx_count_increment", u64); let round_minutes = value_t_or_exit!(matches, "round_minutes", u64).max(1); let round_duration = Duration::from_secs(round_minutes * 60); let initial_balance = value_t_or_exit!(matches, "initial_balance", u64); let tmp_ledger_path = PathBuf::from(TMP_LEDGER_PATH); let _ = fs::remove_dir_all(&tmp_ledger_path); fs::create_dir_all(&tmp_ledger_path).expect("failed to create temp ledger path"); notifier.send("Hi!"); datapoint_info!("ramp-tps", ("event", "boot", String),); let entrypoint_str = matches.value_of("entrypoint").unwrap(); debug!("Connecting to {}", entrypoint_str); let entrypoint_addr = solana_net_utils::parse_host_port(&format!("{}:8899", entrypoint_str)) .expect("failed to parse entrypoint address"); utils::download_genesis(&entrypoint_addr, &tmp_ledger_path).expect("genesis download failed"); let genesis_config = GenesisConfig::load(&tmp_ledger_path).expect("failed to load genesis block"); debug!("Fetching current slot..."); let rpc_client = RpcClient::new_socket_with_timeout(entrypoint_addr, Duration::from_secs(10)); let current_slot = rpc_client.get_slot().expect("failed to fetch current slot"); debug!("Current slot: {}", current_slot); let epoch_schedule = &genesis_config.epoch_schedule; let first_normal_slot = epoch_schedule.first_normal_slot; debug!("First normal slot: {}", first_normal_slot); let sleep_slots = first_normal_slot.saturating_sub(current_slot); if sleep_slots > 0 { notifier.send(&format!( "Waiting for warm-up epochs to complete (epoch {})", epoch_schedule.first_normal_epoch )); utils::sleep_until_epoch( &rpc_client, ¬ifier, &genesis_config, current_slot, epoch_schedule.first_normal_epoch, ); } debug!("Fetching stake config..."); let stake_config_account = rpc_client .get_account(&stake_config_id()) .expect("failed to fetch stake config"); let stake_config = StakeConfig::from(&stake_config_account).unwrap(); // Check if destake-net-nodes.sh should be run { let epoch_info = rpc_client.get_epoch_info().unwrap(); let destake_net_nodes_epoch = value_t_or_exit!(matches, "destake_net_nodes_epoch", u64); if epoch_info.epoch >= destake_net_nodes_epoch { info!( "Current epoch {} >= destake_net_nodes_epoch of {}, skipping destake-net-nodes.sh", epoch_info.epoch, destake_net_nodes_epoch ); } else { info!( "Waiting for destake-net-nodes epoch {}", destake_net_nodes_epoch ); utils::sleep_until_epoch( &rpc_client, ¬ifier, &genesis_config, epoch_info.absolute_slot, destake_net_nodes_epoch, ); info!("Destaking net nodes..."); Command::new("bash") .args(&["destake-net-nodes.sh", &net_dir]) .spawn() .unwrap(); info!("Done destaking net nodes"); } } // Wait for the next epoch, or --stake-activation-epoch { let epoch_info = rpc_client.get_epoch_info().unwrap(); let activation_epoch = value_t!(matches, "stake_activation_epoch", u64) .ok() .unwrap_or(epoch_info.epoch - 1); debug!("Current epoch info: {:?}", &epoch_info); debug!("Activation epoch is: {:?}", activation_epoch); stake::wait_for_warm_up( activation_epoch, epoch_info, &rpc_client, &stake_config, &genesis_config, ¬ifier, ); } let mut tps_sampler = tps::Sampler::new(&entrypoint_addr); loop { notifier.send(&format!("Round {}!", tps_round)); let tx_count = tx_count_for_round(tps_round, tx_count_baseline, tx_count_increment); datapoint_info!( "ramp-tps", ("event", "round-start", String), ("round", tps_round, i64), ("tx_count", tx_count, i64) ); let latest_slot = rpc_client.get_slot().unwrap_or_else(|err| { utils::bail( ¬ifier, &format!("Error: get latest slot failed: {}", err), ); }); sleep(Duration::from_secs(5)); let round_start_slot = rpc_client.get_slot().unwrap_or_else(|err| { utils::bail( ¬ifier, &format!("Error: get round start slot failed: {}", err), ); }); if round_start_slot == latest_slot { utils::bail( ¬ifier, &format!("Slot is not advancing from {}", latest_slot), ); } let starting_validators = voters::fetch_active_validators(&rpc_client); datapoint_info!( "ramp-tps", ("event", "start-transactions", String), ("round", tps_round, i64), ("validators", starting_validators.len(), i64) ); notifier.send(&format!( "There are {} validators present:", starting_validators.len() )); let mut validators: Vec<_> = starting_validators .keys() .map(|node_pubkey| format!("* {}", pubkey_to_keybase(&node_pubkey))) .collect(); validators.sort(); notifier.send(&validators.join("\n")); let client_tx_count = tx_count / NUM_BENCH_CLIENTS as u64; notifier.send(&format!( "Starting transactions for {} minutes (batch size={})", round_minutes, tx_count, )); info!( "Running bench-tps={}='--tx_count={} --thread-batch-sleep-ms={}'", NUM_BENCH_CLIENTS, client_tx_count, THREAD_BATCH_SLEEP_MS ); for client_id in 0..NUM_BENCH_CLIENTS { Command::new("bash") .args(&[ "wrapper-bench-tps.sh", &net_dir, &client_id.to_string(), &client_tx_count.to_string(), THREAD_BATCH_SLEEP_MS, ]) .spawn() .unwrap(); } let bench_warmup_secs = 60; info!( "Sleeping {}s to allow bench-tps to warmup", bench_warmup_secs ); sleep(Duration::from_secs(bench_warmup_secs)); tps_sampler.start_sampling_thread(); sleep(round_duration); tps_sampler.stop_sampling_thread(); for client_id in 0..NUM_BENCH_CLIENTS { Command::new("bash") .args(&[ "wrapper-bench-tps.sh", &net_dir, &client_id.to_string(), "0", // Setting txCount to 0 will kill bench-tps THREAD_BATCH_SLEEP_MS, ]) .spawn() .unwrap(); } datapoint_info!( "ramp-tps", ("event", "stop-transactions", String), ("round", tps_round, i64), ); notifier.send("Transactions stopped"); tps_sampler.report_results(¬ifier); let remaining_validators = voters::fetch_active_validators(&rpc_client); let remaining_keybase = remaining_validators .keys() .map(|k| pubkey_to_keybase(k)) .collect(); tps_round_results .record(tps_round, remaining_keybase) .unwrap_or_else(|err| { warn!("Failed to record round results: {}", err); }); if remaining_validators.is_empty() { utils::bail(¬ifier, "No validators remain"); } datapoint_info!( "ramp-tps", ("event", "calculate-leader-records", String), ("round", tps_round, i64), ("validators", remaining_validators.len(), i64) ); let round_end_slot = rpc_client.get_slot().unwrap_or_else(|err| { utils::bail( ¬ifier, &format!("Error: get round end slot failed: {}", err), ); }); let leader_records = voters::calculate_leader_records( &rpc_client, &epoch_schedule, round_start_slot, round_end_slot, ¬ifier, ) .unwrap_or_else(|err| { utils::bail( ¬ifier, &format!("Error: Could not calculate leader records: {}", err), ); }); voters::announce_results( &starting_validators, &remaining_validators, pubkey_to_keybase.clone(), &leader_records, &mut notifier, ); datapoint_info!( "ramp-tps", ("event", "gifting", String), ("round", tps_round, i64) ); let healthy_validators: Vec<_> = remaining_validators .iter() .filter(|(k, _)| leader_records.get(k).map(|r| r.healthy()).unwrap_or(false)) .map(|(node_pubkey, vote_account_pubkey)| { (pubkey_to_keybase(&node_pubkey), vote_account_pubkey) }) .collect(); let next_gift = gift_for_round(tps_round + 1, initial_balance); voters::award_stake( &rpc_client, &faucet_keypair, healthy_validators, next_gift, &mut notifier, ); datapoint_info!( "ramp-tps", ("event", "new-stake-warmup", String), ("round", tps_round, i64) ); // Wait for stake to warm up before starting the next round let epoch_info = rpc_client.get_epoch_info().unwrap(); debug!("Current epoch info: {:?}", &epoch_info); let current_epoch = epoch_info.epoch; stake::wait_for_warm_up( current_epoch, epoch_info, &rpc_client, &stake_config, &genesis_config, ¬ifier, ); tps_round += 1; } }