Add --graph-forks option (#6732)
This commit is contained in:
parent
d207a34736
commit
cea13e964c
|
@ -3583,6 +3583,7 @@ dependencies = [
|
||||||
"solana-logger 0.21.0",
|
"solana-logger 0.21.0",
|
||||||
"solana-runtime 0.21.0",
|
"solana-runtime 0.21.0",
|
||||||
"solana-sdk 0.21.0",
|
"solana-sdk 0.21.0",
|
||||||
|
"solana-vote-api 0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -19,6 +19,7 @@ solana-ledger = { path = "../ledger", version = "0.21.0" }
|
||||||
solana-logger = { path = "../logger", version = "0.21.0" }
|
solana-logger = { path = "../logger", version = "0.21.0" }
|
||||||
solana-runtime = { path = "../runtime", version = "0.21.0" }
|
solana-runtime = { path = "../runtime", version = "0.21.0" }
|
||||||
solana-sdk = { path = "../sdk", version = "0.21.0" }
|
solana-sdk = { path = "../sdk", version = "0.21.0" }
|
||||||
|
solana-vote-api = { path = "../programs/vote_api", version = "0.21.0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "0.11"
|
assert_cmd = "0.11"
|
||||||
|
|
|
@ -2,12 +2,16 @@ use clap::{
|
||||||
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg, SubCommand,
|
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, Arg, SubCommand,
|
||||||
};
|
};
|
||||||
use solana_ledger::{
|
use solana_ledger::{
|
||||||
bank_forks::SnapshotConfig, bank_forks_utils, blocktree::Blocktree, blocktree_processor,
|
bank_forks::{BankForks, SnapshotConfig},
|
||||||
|
bank_forks_utils,
|
||||||
|
blocktree::Blocktree,
|
||||||
|
blocktree_processor,
|
||||||
rooted_slot_iterator::RootedSlotIterator,
|
rooted_slot_iterator::RootedSlotIterator,
|
||||||
};
|
};
|
||||||
use solana_sdk::{clock::Slot, genesis_block::GenesisBlock};
|
use solana_sdk::{clock::Slot, genesis_block::GenesisBlock, native_token::lamports_to_sol};
|
||||||
|
use solana_vote_api::vote_state::VoteState;
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{stdout, Write},
|
io::{stdout, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -71,6 +75,168 @@ fn output_ledger(blocktree: Blocktree, starting_slot: Slot, method: LedgerOutput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn graph_forks(
|
||||||
|
dot_file: &str,
|
||||||
|
bank_forks: BankForks,
|
||||||
|
bank_forks_info: Vec<blocktree_processor::BankForksInfo>,
|
||||||
|
) {
|
||||||
|
// Search all forks and collect the last vote made by each validator
|
||||||
|
let mut last_votes = HashMap::new();
|
||||||
|
for bfi in &bank_forks_info {
|
||||||
|
let bank = bank_forks.banks.get(&bfi.bank_slot).unwrap();
|
||||||
|
|
||||||
|
let total_stake = bank
|
||||||
|
.vote_accounts()
|
||||||
|
.iter()
|
||||||
|
.fold(0, |acc, (_, (stake, _))| acc + stake);
|
||||||
|
for (_, (stake, vote_account)) in bank.vote_accounts() {
|
||||||
|
let vote_state = VoteState::from(&vote_account).unwrap_or_default();
|
||||||
|
if let Some(last_vote) = vote_state.votes.iter().last() {
|
||||||
|
let entry =
|
||||||
|
last_votes
|
||||||
|
.entry(vote_state.node_pubkey)
|
||||||
|
.or_insert((0, None, 0, total_stake));
|
||||||
|
if entry.0 < last_vote.slot {
|
||||||
|
*entry = (last_vote.slot, vote_state.root_slot, stake, total_stake);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure the stake distribution at all the nodes containing the last vote from each
|
||||||
|
// validator
|
||||||
|
let mut slot_stake_and_vote_count = HashMap::new();
|
||||||
|
for (last_vote_slot, _, stake, total_stake) in last_votes.values() {
|
||||||
|
let entry = slot_stake_and_vote_count
|
||||||
|
.entry(last_vote_slot)
|
||||||
|
.or_insert((0, 0, *total_stake));
|
||||||
|
entry.0 += 1;
|
||||||
|
entry.1 += stake;
|
||||||
|
assert_eq!(entry.2, *total_stake)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dot = vec!["digraph {".to_string()];
|
||||||
|
|
||||||
|
// Build a subgraph consisting of all banks and links to their parent banks
|
||||||
|
dot.push(" subgraph cluster_banks {".to_string());
|
||||||
|
dot.push(" style=invis".to_string());
|
||||||
|
let mut styled_slots = HashSet::new();
|
||||||
|
for bfi in &bank_forks_info {
|
||||||
|
let bank = bank_forks.banks.get(&bfi.bank_slot).unwrap();
|
||||||
|
let mut bank = bank.clone();
|
||||||
|
|
||||||
|
let mut first = true;
|
||||||
|
loop {
|
||||||
|
if !styled_slots.contains(&bank.slot()) {
|
||||||
|
dot.push(format!(
|
||||||
|
r#" "{}"[label="{} (epoch {})\nleader: {}{}",style="{}{}"];"#,
|
||||||
|
bank.slot(),
|
||||||
|
bank.slot(),
|
||||||
|
bank.epoch(),
|
||||||
|
bank.collector_id(),
|
||||||
|
if let Some((votes, stake, total_stake)) =
|
||||||
|
slot_stake_and_vote_count.get(&bank.slot())
|
||||||
|
{
|
||||||
|
format!(
|
||||||
|
"\nvotes: {}, stake: {:.1} SOL ({:.1}%)",
|
||||||
|
votes,
|
||||||
|
lamports_to_sol(*stake),
|
||||||
|
*stake as f64 / *total_stake as f64 * 100.,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
},
|
||||||
|
if first { "filled," } else { "" },
|
||||||
|
if !bank.is_votable() { "dotted," } else { "" }
|
||||||
|
));
|
||||||
|
styled_slots.insert(bank.slot());
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
match bank.parent() {
|
||||||
|
None => {
|
||||||
|
if bank.slot() > 0 {
|
||||||
|
dot.push(format!(r#" "{}" -> "...""#, bank.slot(),));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(parent) => {
|
||||||
|
let slot_distance = bank.slot() - parent.slot();
|
||||||
|
let penwidth = if bank.epoch() > parent.epoch() {
|
||||||
|
"5"
|
||||||
|
} else {
|
||||||
|
"1"
|
||||||
|
};
|
||||||
|
let link_label = if slot_distance > 1 {
|
||||||
|
format!("label=\"{} slots\",color=red", slot_distance)
|
||||||
|
} else {
|
||||||
|
"color=blue".to_string()
|
||||||
|
};
|
||||||
|
dot.push(format!(
|
||||||
|
r#" "{}" -> "{}"[{},penwidth={}];"#,
|
||||||
|
bank.slot(),
|
||||||
|
parent.slot(),
|
||||||
|
link_label,
|
||||||
|
penwidth
|
||||||
|
));
|
||||||
|
|
||||||
|
bank = parent.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dot.push(" }".to_string());
|
||||||
|
|
||||||
|
// Strafe the banks with links from validators to the bank they last voted on,
|
||||||
|
// while collecting information about the absent votes and stakes
|
||||||
|
let mut absent_stake = 0;
|
||||||
|
let mut absent_votes = 0;
|
||||||
|
let mut lowest_last_vote_slot = std::u64::MAX;
|
||||||
|
let mut lowest_total_stake = 0;
|
||||||
|
for (node_pubkey, (last_vote_slot, root_slot, stake, total_stake)) in &last_votes {
|
||||||
|
dot.push(format!(
|
||||||
|
r#" "{}"[shape=box,label="validator: {}\nstake: {} SOL\nlast vote slot: {}\nroot slot: {}"];"#,
|
||||||
|
node_pubkey,
|
||||||
|
node_pubkey,
|
||||||
|
lamports_to_sol(*stake),
|
||||||
|
last_vote_slot,
|
||||||
|
root_slot.unwrap_or(0)
|
||||||
|
));
|
||||||
|
dot.push(format!(
|
||||||
|
r#" "{}" -> "{}" [style=dotted,label="last vote"];"#,
|
||||||
|
node_pubkey,
|
||||||
|
if styled_slots.contains(&last_vote_slot) {
|
||||||
|
last_vote_slot.to_string()
|
||||||
|
} else {
|
||||||
|
if *last_vote_slot < lowest_last_vote_slot {
|
||||||
|
lowest_last_vote_slot = *last_vote_slot;
|
||||||
|
lowest_total_stake = *total_stake;
|
||||||
|
}
|
||||||
|
absent_votes += 1;
|
||||||
|
absent_stake += stake;
|
||||||
|
"...".to_string()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotate the final "..." node with absent vote and stake information
|
||||||
|
if absent_votes > 0 {
|
||||||
|
dot.push(format!(
|
||||||
|
r#" "..."[label="...\nvotes: {}, stake: {:.1} SOL {:.1}%"];"#,
|
||||||
|
absent_votes,
|
||||||
|
lamports_to_sol(absent_stake),
|
||||||
|
absent_stake as f64 / lowest_total_stake as f64 * 100.,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
dot.push("}".to_string());
|
||||||
|
|
||||||
|
match File::create(dot_file).and_then(|mut file| file.write_all(&dot.join("\n").into_bytes())) {
|
||||||
|
Ok(_) => println!("Wrote {}", dot_file),
|
||||||
|
Err(err) => eprintln!("Unable to write {}: {}", dot_file, err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const DEFAULT_ROOT_COUNT: &str = "1";
|
const DEFAULT_ROOT_COUNT: &str = "1";
|
||||||
solana_logger::setup_with_filter("solana=info");
|
solana_logger::setup_with_filter("solana=info");
|
||||||
|
@ -94,20 +260,35 @@ fn main() {
|
||||||
.global(true)
|
.global(true)
|
||||||
.help("Use directory for ledger location"),
|
.help("Use directory for ledger location"),
|
||||||
)
|
)
|
||||||
.subcommand(SubCommand::with_name("print").about("Print the ledger").arg(&starting_slot_arg))
|
.subcommand(
|
||||||
.subcommand(SubCommand::with_name("print-slot").about("Print the contents of one slot").arg(
|
SubCommand::with_name("print")
|
||||||
|
.about("Print the ledger")
|
||||||
|
.arg(&starting_slot_arg)
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("print-slot")
|
||||||
|
.about("Print the contents of one slot")
|
||||||
|
.arg(
|
||||||
Arg::with_name("slot")
|
Arg::with_name("slot")
|
||||||
.index(1)
|
.index(1)
|
||||||
.value_name("SLOT")
|
.value_name("SLOT")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.required(true)
|
.required(true)
|
||||||
.help("The slot to print"),
|
.help("The slot to print"),
|
||||||
))
|
)
|
||||||
.subcommand(SubCommand::with_name("bounds").about("Print lowest and highest non-empty slots. Note: This ignores gaps in slots"))
|
)
|
||||||
.subcommand(SubCommand::with_name("json").about("Print the ledger in JSON format").arg(&starting_slot_arg))
|
.subcommand(
|
||||||
|
SubCommand::with_name("bounds")
|
||||||
|
.about("Print lowest and highest non-empty slots. Note: This ignores gaps in slots")
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("json")
|
||||||
|
.about("Print the ledger in JSON format")
|
||||||
|
.arg(&starting_slot_arg)
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("verify")
|
SubCommand::with_name("verify")
|
||||||
.about("Verify the ledger's PoH")
|
.about("Verify the ledger")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("no_snapshot")
|
Arg::with_name("no_snapshot")
|
||||||
.long("no-snapshot")
|
.long("no-snapshot")
|
||||||
|
@ -129,20 +310,30 @@ fn main() {
|
||||||
.help("Halt processing at the given slot"),
|
.help("Halt processing at the given slot"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
clap::Arg::with_name("skip_poh_verify")
|
Arg::with_name("skip_poh_verify")
|
||||||
.long("skip-poh-verify")
|
.long("skip-poh-verify")
|
||||||
.takes_value(false)
|
.takes_value(false)
|
||||||
.help("Skip ledger PoH verification"),
|
.help("Skip ledger PoH verification"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
).subcommand(SubCommand::with_name("prune").about("Prune the ledger at the block height").arg(
|
Arg::with_name("graph_forks")
|
||||||
|
.long("graph-forks")
|
||||||
|
.value_name("FILENAME.GV")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("Create a Graphviz DOT file representing the active forks once the ledger is verified"),
|
||||||
|
)
|
||||||
|
).subcommand(
|
||||||
|
SubCommand::with_name("prune")
|
||||||
|
.about("Prune the ledger at the block height")
|
||||||
|
.arg(
|
||||||
Arg::with_name("slot_list")
|
Arg::with_name("slot_list")
|
||||||
.long("slot-list")
|
.long("slot-list")
|
||||||
.value_name("FILENAME")
|
.value_name("FILENAME")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.required(true)
|
.required(true)
|
||||||
.help("The location of the YAML file with a list of rollback slot heights and hashes"),
|
.help("The location of the YAML file with a list of rollback slot heights and hashes"),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("list-roots")
|
SubCommand::with_name("list-roots")
|
||||||
.about("Output upto last <num-roots> root hashes and their heights starting at the given block height")
|
.about("Output upto last <num-roots> root hashes and their heights starting at the given block height")
|
||||||
|
@ -152,13 +343,17 @@ fn main() {
|
||||||
.value_name("NUM")
|
.value_name("NUM")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.required(true)
|
.required(true)
|
||||||
.help("Maximum block height")).arg(
|
.help("Maximum block height")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
Arg::with_name("slot_list")
|
Arg::with_name("slot_list")
|
||||||
.long("slot-list")
|
.long("slot-list")
|
||||||
.value_name("FILENAME")
|
.value_name("FILENAME")
|
||||||
.required(false)
|
.required(false)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("The location of the output YAML file. A list of rollback slot heights and hashes will be written to the file.")).arg(
|
.help("The location of the output YAML file. A list of rollback slot heights and hashes will be written to the file.")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
Arg::with_name("num_roots")
|
Arg::with_name("num_roots")
|
||||||
.long("num-roots")
|
.long("num-roots")
|
||||||
.value_name("NUM")
|
.value_name("NUM")
|
||||||
|
@ -166,7 +361,8 @@ fn main() {
|
||||||
.default_value(DEFAULT_ROOT_COUNT)
|
.default_value(DEFAULT_ROOT_COUNT)
|
||||||
.required(false)
|
.required(false)
|
||||||
.help("Number of roots in the output"),
|
.help("Number of roots in the output"),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let ledger_path = PathBuf::from(value_t_or_exit!(matches, "ledger", String));
|
let ledger_path = PathBuf::from(value_t_or_exit!(matches, "ledger", String));
|
||||||
|
@ -234,8 +430,12 @@ fn main() {
|
||||||
snapshot_config.as_ref(),
|
snapshot_config.as_ref(),
|
||||||
process_options,
|
process_options,
|
||||||
) {
|
) {
|
||||||
Ok((_bank_forks, _bank_forks_info, _leader_schedule_cache)) => {
|
Ok((bank_forks, bank_forks_info, _leader_schedule_cache)) => {
|
||||||
println!("Ok");
|
println!("Ok");
|
||||||
|
|
||||||
|
if let Some(dot_file) = arg_matches.value_of("graph_forks") {
|
||||||
|
graph_forks(&dot_file, bank_forks, bank_forks_info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Ledger verification failed: {:?}", err);
|
eprintln!("Ledger verification failed: {:?}", err);
|
||||||
|
|
Loading…
Reference in New Issue