From 0fff5b8bf86721ec88bb69d142a41cc12f6a6cf9 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Mon, 18 Jul 2022 22:46:01 -0600 Subject: [PATCH] ledger-tool: optionally `graph` vote states --- ledger-tool/src/main.rs | 145 ++++++++++++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 28 deletions(-) diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 30b273d379..45c351ff19 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -399,8 +399,58 @@ fn render_dot(dot: String, output_file: &str, output_format: &str) -> io::Result Ok(()) } +#[derive(Clone, Copy, Debug)] +enum GraphVoteAccountMode { + Disabled, + LastOnly, + WithHistory, +} + +impl GraphVoteAccountMode { + const DISABLED: &'static str = "disabled"; + const LAST_ONLY: &'static str = "last-only"; + const WITH_HISTORY: &'static str = "with-history"; + const ALL_MODE_STRINGS: &'static [&'static str] = + &[Self::DISABLED, Self::LAST_ONLY, Self::WITH_HISTORY]; + + fn is_enabled(&self) -> bool { + !matches!(self, Self::Disabled) + } +} + +impl AsRef for GraphVoteAccountMode { + fn as_ref(&self) -> &str { + match self { + Self::Disabled => Self::DISABLED, + Self::LastOnly => Self::LAST_ONLY, + Self::WithHistory => Self::WITH_HISTORY, + } + } +} + +impl Default for GraphVoteAccountMode { + fn default() -> Self { + Self::Disabled + } +} + +struct GraphVoteAccountModeError(String); + +impl FromStr for GraphVoteAccountMode { + type Err = GraphVoteAccountModeError; + fn from_str(s: &str) -> Result { + match s { + Self::DISABLED => Ok(Self::Disabled), + Self::LAST_ONLY => Ok(Self::LastOnly), + Self::WITH_HISTORY => Ok(Self::WithHistory), + _ => Err(GraphVoteAccountModeError(s.to_string())), + } + } +} + struct GraphConfig { include_all_votes: bool, + vote_account_mode: GraphVoteAccountMode, } #[allow(clippy::cognitive_complexity)] @@ -555,36 +605,60 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { validator_votes.remove(last_vote_slot); }); - dot.push(format!( - r#" "last vote {}"[shape=box,label="Latest validator vote: {}\nstake: {} SOL\nroot slot: {}\nvote history:\n{}"];"#, - node_pubkey, - node_pubkey, - lamports_to_sol(*stake), - vote_state.root_slot.unwrap_or(0), - vote_state - .votes - .iter() - .map(|vote| format!("slot {} (conf={})", vote.slot, vote.confirmation_count)) - .collect::>() - .join("\n") - )); + let maybe_styled_last_vote_slot = styled_slots.get(last_vote_slot); + if maybe_styled_last_vote_slot.is_none() { + 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; + }; - dot.push(format!( - r#" "last vote {}" -> "{}" [style=dashed,label="latest 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; + if config.vote_account_mode.is_enabled() { + let vote_history = + if matches!(config.vote_account_mode, GraphVoteAccountMode::WithHistory) { + format!( + "vote history:\n{}", + vote_state + .votes + .iter() + .map(|vote| format!( + "slot {} (conf={})", + vote.slot, vote.confirmation_count + )) + .collect::>() + .join("\n") + ) + } else { + format!( + "last vote slot: {}", + vote_state + .votes + .back() + .map(|vote| vote.slot.to_string()) + .unwrap_or_else(|| "none".to_string()) + ) + }; + dot.push(format!( + r#" "last vote {}"[shape=box,label="Latest validator vote: {}\nstake: {} SOL\nroot slot: {}\n{}"];"#, + node_pubkey, + node_pubkey, + lamports_to_sol(*stake), + vote_state.root_slot.unwrap_or(0), + vote_history, + )); - "...".to_string() - }, - )); + dot.push(format!( + r#" "last vote {}" -> "{}" [style=dashed,label="latest vote"];"#, + node_pubkey, + if let Some(styled_last_vote_slot) = maybe_styled_last_vote_slot { + styled_last_vote_slot.to_string() + } else { + "...".to_string() + }, + )); + } } // Annotate the final "..." node with absent vote and stake information @@ -1170,6 +1244,7 @@ fn main() { let default_bootstrap_validator_stake_lamports = &sol_to_lamports(0.5) .max(rent.minimum_balance(StakeState::size_of())) .to_string(); + let default_graph_vote_account_mode = GraphVoteAccountMode::default(); let mut measure_total_execution_time = Measure::start("ledger tool"); let matches = App::new(crate_name!()) @@ -1477,6 +1552,15 @@ fn main() { .takes_value(true) .help("Output file"), ) + .arg( + Arg::with_name("vote_account_mode") + .long("vote-account-mode") + .takes_value(true) + .value_name("MODE") + .default_value(default_graph_vote_account_mode.as_ref()) + .possible_values(GraphVoteAccountMode::ALL_MODE_STRINGS) + .help("Specify if and how to graph vote accounts. Enabling will incur significant rendering overhead, especially `with-history`") + ) ).subcommand( SubCommand::with_name("create-snapshot") .about("Create a new ledger snapshot") @@ -2415,6 +2499,11 @@ fn main() { let output_file = value_t_or_exit!(arg_matches, "graph_filename", String); let graph_config = GraphConfig { include_all_votes: arg_matches.is_present("include_all_votes"), + vote_account_mode: value_t_or_exit!( + arg_matches, + "vote_account_mode", + GraphVoteAccountMode + ), }; let process_options = ProcessOptions {