//cargo run 1 10 23 15 for 1d 10h 23mn 15s use borsh::BorshDeserialize; use chrono::{Datelike, Local, NaiveDate, NaiveTime, Timelike}; use core::str::FromStr; use serde_json; use solana_client::nonblocking::rpc_client::RpcClient; use solana_ledger::leader_schedule::LeaderSchedule; use solana_sdk::clock::NUM_CONSECUTIVE_LEADER_SLOTS; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; use solana_sdk::stake::state::StakeState; use std::collections::{BTreeMap, HashMap}; use std::env; use std::time::Duration; use time::{Duration as TimeDuration, OffsetDateTime, UtcOffset}; use tokio::fs::File; use tokio::io::AsyncWriteExt; //const RPC_URL: &str = "https://api.mainnet-beta.solana.com"; //const RPC_URL: &str = "https://api.testnet.solana.com"; //const RPC_URL: &str = "https://api.devnet.solana.com"; //const RPC_URL: &str = "http://localhost:8899"; const RPC_URL: &str = "http://192.168.88.31:8899"; //const SLOTS_IN_EPOCH: u64 = 432000; const MAX_EPOCH_VALUE: u64 = 18446744073709551615; #[tokio::main(flavor = "multi_thread", worker_threads = 16)] pub async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let args: Vec = env::args().collect(); if args.len() != 4 { eprintln!("Please provide 3 arguments: hour, minute and seconds"); std::process::exit(1); } let day: u64 = args[1] .parse() .expect("First argument should be a number representing the hour"); let hour: u64 = args[2] .parse() .expect("First argument should be a number representing the hour"); let minute: u64 = args[3] .parse() .expect("Second argument should be a number representing the minute"); let second: u64 = args[4] .parse() .expect("Third argument should be a number representing the seconds"); let seconds_until_target = day * 24 * 3600 + hour * 3600 + minute * 60 + second; log::info!("seconds_until_target:{}", seconds_until_target); let to_wait = Duration::from_secs(seconds_until_target as u64); tokio::time::sleep(to_wait).await; let mut counter = 0; let mut schedule_counter = 0; let mut epoch_offset = 0; loop { match write_schedule(0).await { Ok(()) => { epoch_offset = 0; schedule_counter += 1; if schedule_counter == 3 { break; } break; tokio::time::sleep(Duration::from_secs(30)).await; } Err(err) => { log::error!("error:{err}"); tokio::time::sleep(tokio::time::Duration::from_millis(2)).await; counter += 1; if counter == 5 { break; } } } } Ok(()) } async fn save_map(file_name: &str, map: &BTreeMap) -> anyhow::Result<()> { let serialized_map = serde_json::to_string(map).unwrap(); // Write to the file let mut file = File::create(file_name).await?; file.write_all(serialized_map.as_bytes()).await?; //log::info!("Files: {file_name}"); //log::info!("{}", serialized_map); Ok(()) } async fn write_schedule(epoch_offset: u64) -> anyhow::Result<()> { log::info!("start schedule calculus process"); let (schedule, stakes_aggregated) = process_schedule(epoch_offset).await?; let now = Local::now(); let date_string = format!( "{}_{}_{}-{}_{}_{}", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second() ); // Create the file name let file_name = format!("out_sc_{}.json", date_string); save_map(&file_name, &schedule).await?; let file_name = format!("out_st_{}.json", date_string); //filter with stake diff accont. let stakes_aggregated: BTreeMap = stakes_aggregated .into_iter() .filter_map(|(pk, (pa, va))| (pa != va).then_some((pk, (0u64, pa, va)))) .collect(); save_map(&file_name, &stakes_aggregated).await?; //show all schedule aggregated. // let mut print_finalized = schedule // .into_iter() // .map(|(key, values)| format!("{key}:{:?}", values)) // .collect::>(); // print_finalized.sort(); //log::info!("leader_schedule_finalized:{:?}", print_finalized); Ok(()) } async fn process_schedule( epoch_offset: u64, ) -> anyhow::Result<( BTreeMap, BTreeMap, )> { let rpc_client = RpcClient::new_with_commitment(RPC_URL.to_string(), CommitmentConfig::finalized()); let slot = rpc_client.get_slot().await?; // Fetch current epoch let epoch_info = rpc_client.get_epoch_info().await?; log::info!("process_schedule current_epoch:{epoch_info:?}"); let current_epoch = epoch_info.epoch; log::info!("current_slot:{slot:?}"); log::info!("epoch_info:{epoch_info:?}"); let call_program_account = true; let pa_stakes = if call_program_account { let mut stakes = HashMap::::new(); // Fetch stakes in current epoch let response = rpc_client .get_program_accounts(&solana_sdk::stake::program::id()) .await?; //log::info!("get_program_accounts:{:?}", response); for (pubkey, account) in response { // Zero-length accounts owned by the stake program are system accounts that were re-assigned and are to be // ignored if account.data.len() == 0 { continue; } match StakeState::deserialize(&mut account.data.as_slice())? { StakeState::Stake(_, stake) => { //log::info!("Program_accounts stake:{stake:#?}"); //On test validator all stakes are attributes to an account with stake.delegation.activation_epoch == MAX_EPOCH_VALUE. //It's considered as activated stake. if stake.delegation.activation_epoch == MAX_EPOCH_VALUE { log::info!("Found account with stake.delegation.activation_epoch == MAX_EPOCH_VALUE use it: {}", pubkey.to_string()); } else { // Ignore stake accounts activated in this epoch (or later, to include activation_epoch of // u64::MAX which indicates no activation ever happened) if stake.delegation.activation_epoch >= current_epoch { continue; } // Ignore stake accounts deactivated before this epoch if stake.delegation.deactivation_epoch < current_epoch { continue; } } // Add the stake in this stake account to the total for the delegated-to vote account log::info!("Stake {pubkey} account:{account:?} stake:{stake:?} "); *(stakes .entry(stake.delegation.voter_pubkey.clone()) .or_insert(0)) += stake.delegation.stake; } _ => (), } } stakes } else { //Put a dummy value if no PA stake are available. let mut stakes = HashMap::::new(); stakes.insert( Pubkey::from_str("9C4UgjzAch2ZTaBnjCyJ4oXLBnkyZZqdaB6fmRdPuHeD").unwrap(), 10, ); stakes }; log::info!("PA Stakes:{pa_stakes:?}"); let mut stakes_aggregated: BTreeMap = pa_stakes .iter() .map(|(pk, stake)| (pk.to_string(), (*stake, 0u64))) .collect(); let leader_schedule = calculate_leader_schedule( current_epoch + epoch_offset, pa_stakes, epoch_info.slots_in_epoch, ); let mut leader_schedule_aggregated: BTreeMap = leader_schedule .get_slot_leaders() .iter() .fold(BTreeMap::new(), |mut sc, l| { sc.entry(l.to_string()).or_insert((0, 0, 0)).1 += 1; sc }); // for (leader, nb) in leader_schedule_aggregated { // println!("{leader}:{nb}"); // } //let _ = verify_schedule(leader_schedule, RPC_URL.to_string()).await; //build vote account node key association table let vote_account = rpc_client.get_vote_accounts().await?; let note_vote_table = vote_account .current .iter() .chain(vote_account.delinquent.iter()) .map(|va| (va.node_pubkey.clone(), va.vote_pubkey.clone())) .collect::>(); log::info!("VOTE Stakes:{vote_account:#?}"); //get leader schedule from rpc let leader_schedule_finalized = rpc_client.get_leader_schedule(Some(slot)).await?; //Some(slot) let binding = "Vote key not found".to_string(); leader_schedule_finalized .unwrap() .into_iter() .for_each(|(key, slots)| { let vote_key = note_vote_table.get(&key.to_string()).unwrap_or(&binding); leader_schedule_aggregated .entry(vote_key.clone()) .or_insert((0, 0, 0)) .0 += slots.len() as u64 }); //build schedule from vote account. let vote_stackes: HashMap = vote_account .current .iter() .chain(vote_account.delinquent.iter()) .map(|va| { ( Pubkey::from_str(&va.vote_pubkey).unwrap(), va.activated_stake, ) }) .collect(); vote_stackes.iter().for_each(|(pk, stake)| { stakes_aggregated.entry(pk.to_string()).or_insert((0, 0)).1 = *stake; }); let leader_schedule_va = calculate_leader_schedule( current_epoch + epoch_offset, vote_stackes, epoch_info.slots_in_epoch, ); leader_schedule_va.get_slot_leaders().iter().for_each(|l| { leader_schedule_aggregated .entry(l.to_string()) .or_insert((0, 0, 0)) .2 += 1; }); //verify VA schedule let _ = verify_schedule(leader_schedule_va, ¬e_vote_table, RPC_URL.to_string()).await; // log::info!( // "vote account current:{:?}", // vote_account // .current // .iter() // .map(|va| format!("{}/{}", va.vote_pubkey, va.node_pubkey)) // .collect::>() // ); // log::info!( // "vote account delinquent:{:?}", // vote_account // .delinquent // .iter() // .map(|va| format!("{}/{}", va.vote_pubkey, va.node_pubkey)) // .collect::>() // ); log::info!("end process_schedule:{:?}", leader_schedule_aggregated); Ok((leader_schedule_aggregated, stakes_aggregated)) } //Copied from leader_schedule_utils.rs // Mostly cribbed from leader_schedule_utils fn calculate_leader_schedule( epoch: u64, stakes: HashMap, slots_in_epoch: u64, ) -> LeaderSchedule { let mut seed = [0u8; 32]; seed[0..8].copy_from_slice(&epoch.to_le_bytes()); let mut stakes: Vec<_> = stakes .iter() .map(|(pubkey, stake)| (*pubkey, *stake)) .collect(); sort_stakes(&mut stakes); LeaderSchedule::new(&stakes, seed, slots_in_epoch, NUM_CONSECUTIVE_LEADER_SLOTS) } // Cribbed from leader_schedule_utils fn sort_stakes(stakes: &mut Vec<(Pubkey, u64)>) { // Sort first by stake. If stakes are the same, sort by pubkey to ensure a // deterministic result. // Note: Use unstable sort, because we dedup right after to remove the equal elements. stakes.sort_unstable_by(|(l_pubkey, l_stake), (r_pubkey, r_stake)| { if r_stake == l_stake { r_pubkey.cmp(l_pubkey) } else { r_stake.cmp(l_stake) } }); // Now that it's sorted, we can do an O(n) dedup. stakes.dedup(); } fn seconds_until_target_time_with_time( target_hour: u8, target_minute: u8, target_second: u8, ) -> i64 { //let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH); //log::info!("{local_offset:?}"); //set UTC+2 let utcp2 = UtcOffset::from_hms(2, 0, 0).unwrap(); let now = OffsetDateTime::now_utc().to_offset(utcp2); //let now = OffsetDateTime::now_utc(); log::info!("now:{now:?}"); let mut target_time = now .date() .with_hms(target_hour, target_minute, target_second) .unwrap() .assume_offset(utcp2); // If the target time has passed for today, calculate for next day if now > target_time { log::info!("add one day"); target_time = target_time + TimeDuration::days(1); } log::info!("target_time:{target_time:?}"); let duration_until_target = target_time - now; duration_until_target.whole_seconds() } pub async fn verify_schedule( schedule: LeaderSchedule, node_vote_account_map: &HashMap, rpc_url: String, ) -> anyhow::Result<()> { let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); let Some(leader_schedule_finalized) = rpc_client.get_leader_schedule(None).await? else { log::info!("verify_schedule RPC return no schedule. Try later."); return Ok(()); }; //map leaderscheudle to HashMap> let mut input_leader_schedule: HashMap> = HashMap::new(); for (slot, pubkey) in schedule.get_slot_leaders().iter().copied().enumerate() { input_leader_schedule .entry(pubkey) .or_insert(vec![]) .push(slot); } log::trace!("verify_schedule input_leader_schedule:{input_leader_schedule:?} leader_schedule_finalized:{leader_schedule_finalized:?}"); //map rpc leader schedule node pubkey to vote account let mut leader_schedule_finalized: HashMap<&String, Vec> = leader_schedule_finalized.into_iter().filter_map(|(pk, slots)| match node_vote_account_map.get(&pk) { Some(vote_account) => Some((vote_account,slots)), None => { log::warn!("verify_schedule RPC get_leader_schedule return some Node account:{pk} that are not mapped by rpc get_vote_accounts"); None }, }).collect(); let mut vote_account_in_error: Vec = input_leader_schedule.into_iter().filter_map(|(input_vote_key, mut input_slot_list)| { let Some(mut rpc_strake_list) = leader_schedule_finalized.remove(&input_vote_key.to_string()) else { log::warn!("verify_schedule vote account not found in RPC:{input_vote_key}"); return Some(input_vote_key); }; input_slot_list.sort(); rpc_strake_list.sort(); if input_slot_list.into_iter().zip(rpc_strake_list.into_iter()).any(|(in_v, rpc)| in_v != rpc) { log::warn!("verify_schedule bad slots for {input_vote_key}"); // Caluclated:{input_slot_list:?} rpc:{rpc_strake_list:?} Some(input_vote_key) } else { None } }).collect(); if !leader_schedule_finalized.is_empty() { log::warn!( "verify_schedule RPC vote account not present in calculated schedule:{:?}", leader_schedule_finalized.keys() ); vote_account_in_error.append( &mut leader_schedule_finalized .keys() .map(|sk| Pubkey::from_str(sk).unwrap()) .collect::>(), ); } log::info!("verify_schedule these account are wrong:{vote_account_in_error:?}"); Ok(()) }