diff --git a/.buildkite/alpenglow/pipeline.sh b/.buildkite/alpenglow/pipeline.sh new file mode 100755 index 0000000000..fbbf767df0 --- /dev/null +++ b/.buildkite/alpenglow/pipeline.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +cat < PathBuf { + manifest_path + .parent() + .unwrap() + .to_owned() + .join("spl-alpenglow_vote.so") +} + +fn generate_github_rev(rev: &str) -> PathBuf { + // Form the glob that searches for the git repo's manifest path under ~/.cargo/git/checkouts + let git_checkouts_path = PathBuf::from(env::var("CARGO_HOME").unwrap()) + .join("git") + .join("checkouts"); + + let glob_str = format!( + "{}/alpenglow-vote-*/{}/Cargo.toml", + git_checkouts_path.to_str().unwrap(), + rev + ); + + // Find the manifest path + let manifest_path = glob::glob(&glob_str) + .unwrap_or_else(|_| panic!("Failed to read glob: {}", &glob_str)) + .filter_map(Result::ok) + .next() + .unwrap_or_else(|| { + panic!( + "Couldn't find path to git repo with glob {} and revision {}", + &glob_str, rev + ) + }); + + fetch_shared_object_path(&manifest_path) +} + +fn generate_local_checkout(path: &str) -> PathBuf { + let err = || { + format!("Local checkout path must be of the form: /x/y/z/alpenglow-vote-project-path/program. In particular, alpenglow-vote-project-path is the local checkout, which might typically just be called alpenglow-vote. Current checkout path: {}", path) + }; + let path = PathBuf::from(path); + + // Ensure that path ends with "program" + if path + .file_name() + .and_then(|p| p.to_str()) + .unwrap_or_else(|| panic!("{}", err())) + != "program" + { + panic!("{}", err()); + } + + // If this is a relative path, then make it absolute by determining the relative path with + // respect to the project directory, and not the current CARGO_MANIFEST_DIR. + let path = if path.is_relative() { + PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .to_owned() + .join(path) + } else { + path + }; + + // Turn the path into an absolute path + let path = std::path::absolute(path).unwrap(); + let manifest_path = path.parent().unwrap().to_owned().join("Cargo.toml"); + + fetch_shared_object_path(&manifest_path) +} + +fn main() { + // Get the project's Cargo.toml + let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let project_cargo_toml_path = PathBuf::from(&cargo_manifest_dir) + .join("..") + .join("Cargo.toml"); + + // Parse the Cargo file. + let project_cargo_toml_contents = + fs::read_to_string(&project_cargo_toml_path).expect("Couldn't read root Cargo.toml."); + + let project_cargo_toml = project_cargo_toml_contents + .parse::() + .expect("Couldn't parse root Cargo.toml into a valid toml::Value."); + + // Find alpenglow-vote + let workspace_dependencies = &project_cargo_toml["workspace"]["dependencies"]; + + let err = "alpenglow-vote must either be of form: (1) if you're trying to fetch from a git repo: { git = \"...\", rev = \"...\" } or (2) if you're trying to use a local checkout of alpenglow-vote : { path = \"...\" }"; + + let alpenglow_vote = workspace_dependencies + .get("alpenglow-vote") + .expect("Couldn't find alpenglow-vote under workspace.dependencies in root Cargo.toml.") + .as_table() + .expect(err); + + // Are we trying to build alpenglow-vote from Github or a local checkout? + let so_src_path = if alpenglow_vote.contains_key("git") && alpenglow_vote.contains_key("rev") { + build_print::custom_println!( + "Compiling", + green, + "spl-alpenglow_vote.so: building from github rev: {:?}", + &alpenglow_vote + ); + generate_github_rev(alpenglow_vote["rev"].as_str().unwrap()) + } else if alpenglow_vote.contains_key("path") { + build_print::custom_println!( + "Compiling", + green, + "spl-alpenglow_vote.so: building from local checkout: {:?}", + &alpenglow_vote + ); + generate_local_checkout(alpenglow_vote["path"].as_str().unwrap()) + } else { + panic!("{}", err); + }; + + // Copy the .so to project_dir/target/tmp/ + let so_dest_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .to_owned() + .join("target") + .join("alpenglow-vote-so") + .join("spl_alpenglow-vote.so"); + + fs::create_dir_all(so_dest_path.parent().unwrap()) + .unwrap_or_else(|_| panic!("Couldn't create path: {:?}", &so_dest_path)); + + fs::copy(&so_src_path, &so_dest_path).unwrap_or_else(|err| { + panic!( + "Couldn't copy alpenglow_vote from {:?} to {:?}:\n{}", + &so_src_path, &so_dest_path, err + ) + }); + + build_print::custom_println!( + "[build-alpenglow-vote]", + green, + "spl-alpenglow_vote.so: successfully built alpenglow_vote! Copying {} -> {}", + so_src_path.display(), + so_dest_path.display(), + ); + + // Save the destination path as an environment variable that can later be invoked in Rust code + println!( + "cargo:rustc-env=ALPENGLOW_VOTE_SO_PATH={}", + so_dest_path.display() + ); + + // Re-build if we detect a change in either (1) the alpenglow-vote src or (2) this build script + println!("cargo::rerun-if-changed={}", so_src_path.display()); + println!("cargo::rerun-if-changed=build.rs"); +} diff --git a/build-alpenglow-vote/src/lib.rs b/build-alpenglow-vote/src/lib.rs new file mode 100644 index 0000000000..394408bee6 --- /dev/null +++ b/build-alpenglow-vote/src/lib.rs @@ -0,0 +1,12 @@ +/// Path to the alpenglow-vote shared object +pub const ALPENGLOW_VOTE_SO_PATH: &str = env!("ALPENGLOW_VOTE_SO_PATH"); + +#[cfg(test)] +mod tests { + use {crate::ALPENGLOW_VOTE_SO_PATH, std::path::Path}; + + #[test] + pub fn ensure_alpenglow_vote_so_path_exists() { + assert!(Path::new(ALPENGLOW_VOTE_SO_PATH).exists()); + } +} diff --git a/ci/docker-run.sh b/ci/docker-run.sh index 427ee7b319..3fdb954c0c 100755 --- a/ci/docker-run.sh +++ b/ci/docker-run.sh @@ -91,7 +91,12 @@ fi # Ensure files are created with the current host uid/gid if [[ -z "$SOLANA_DOCKER_RUN_NOSETUID" ]]; then - ARGS+=(--user "$(id -u):$(id -g)") + ARGS+=( + --user "$(id -u):$(id -g)" + --volume "/etc/passwd:/etc/passwd:ro" + --volume "/etc/group:/etc/group:ro" + --volume "/var/lib/buildkite-agent:/var/lib/buildkite-agent" + ) fi if [[ -n $SOLANA_ALLOCATE_TTY ]]; then @@ -117,6 +122,7 @@ ARGS+=( --env CI_PULL_REQUEST --env CI_REPO_SLUG --env CRATES_IO_TOKEN + --env CARGO_NET_GIT_FETCH_WITH_CLI ) # Also propagate environment variables needed for codecov diff --git a/ci/stable/run-partition.sh b/ci/stable/run-partition.sh index 1d8aeaa293..22737e76cc 100755 --- a/ci/stable/run-partition.sh +++ b/ci/stable/run-partition.sh @@ -34,6 +34,7 @@ ARGS=( --partition hash:"$((INDEX + 1))/$LIMIT" --verbose --exclude solana-local-cluster + --exclude solana-cargo-build-sbf --no-tests=warn ) diff --git a/ci/test-miri.sh b/ci/test-miri.sh index 76f357e675..6447af5027 100755 --- a/ci/test-miri.sh +++ b/ci/test-miri.sh @@ -8,6 +8,11 @@ source ci/rust-version.sh nightly # miri is very slow; so only run very few of selective tests! _ cargo "+${rust_nightly}" miri test -p solana-unified-scheduler-logic +# test big endian branch +_ cargo "+${rust_nightly}" miri test --target s390x-unknown-linux-gnu -p solana-vote -- "vote_state_view" --skip "arbitrary" +# test little endian branch for UB +_ cargo "+${rust_nightly}" miri test -p solana-vote -- "vote_state_view" --skip "arbitrary" + # run intentionally-#[ignored] ub triggering tests for each to make sure they fail (! _ cargo "+${rust_nightly}" miri test -p solana-unified-scheduler-logic -- \ --ignored --exact "utils::tests::test_ub_illegally_created_multiple_tokens") diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index da9298ca77..b917be2702 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -13,6 +13,7 @@ edition = { workspace = true } chrono = { workspace = true, features = ["default"] } clap = "2.33.0" rpassword = { workspace = true } +solana-bls-signatures = { workspace = true } solana-clock = { workspace = true } solana-cluster-type = { workspace = true } solana-commitment-config = { workspace = true } diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 3cf90d464b..29114c4dda 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -5,6 +5,7 @@ use { }, chrono::DateTime, clap::ArgMatches, + solana_bls_signatures::Pubkey as BLSPubkey, solana_clock::UnixTimestamp, solana_cluster_type::ClusterType, solana_commitment_config::CommitmentConfig, @@ -104,6 +105,19 @@ pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { }) } +pub fn bls_pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { + matches.values_of(name).map(|values| { + values + .map(|value| { + BLSPubkey::from_str(value).unwrap_or_else(|_| { + //TODO(wen): support reading BLS keypair files + panic!("Failed to parse BLS public key from value: {}", value) + }) + }) + .collect() + }) +} + // Return pubkey/signature pairs for a string of the form pubkey=signature pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option> { matches.values_of(name).map(|values| { @@ -199,6 +213,7 @@ mod tests { use { super::*, clap::{App, Arg}, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey}, solana_keypair::write_keypair_file, std::fs, }; @@ -329,6 +344,23 @@ mod tests { ); } + #[test] + fn test_bls_pubkeys_of() { + let bls_pubkey1: BLSPubkey = BLSKeypair::new().public.into(); + let bls_pubkey2: BLSPubkey = BLSKeypair::new().public.into(); + let matches = app().get_matches_from(vec![ + "test", + "--multiple", + &bls_pubkey1.to_string(), + "--multiple", + &bls_pubkey2.to_string(), + ]); + assert_eq!( + bls_pubkeys_of(&matches, "multiple"), + Some(vec![bls_pubkey1, bls_pubkey2]) + ); + } + #[test] fn test_lamports_of_sol() { let matches = app().get_matches_from(vec!["test", "--single", "50"]); diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 91a7af9032..d99757e488 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -30,6 +30,7 @@ serde_derive = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["solana-signer-derive"] } solana-borsh = { workspace = true } solana-bpf-loader-program = { workspace = true } solana-clap-utils = { workspace = true } @@ -85,7 +86,9 @@ solana-transaction-error = { workspace = true } solana-transaction-status = { workspace = true } solana-udp-client = { workspace = true } solana-version = { workspace = true } +solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor-messages = { workspace = true } spl-memo = { workspace = true, features = ["no-entrypoint"] } thiserror = { workspace = true } tiny-bip39 = { workspace = true } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a7d5d5cbf8..51bfb964b3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -339,6 +339,7 @@ pub enum CliCommand { memo: Option, fee_payer: SignerIndex, compute_unit_price: Option, + is_alpenglow: bool, }, ShowVoteAccount { pubkey: Pubkey, @@ -1475,6 +1476,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { memo, fee_payer, compute_unit_price, + is_alpenglow, } => process_create_vote_account( &rpc_client, config, @@ -1492,6 +1494,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { memo.as_ref(), *fee_payer, *compute_unit_price, + *is_alpenglow, ), CliCommand::ShowVoteAccount { pubkey: vote_account_pubkey, @@ -2153,6 +2156,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }; config.signers = vec![&keypair, &bob_keypair, &identity_keypair]; let result = process_command(&config); @@ -2432,6 +2436,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }; config.signers = vec![&keypair, &bob_keypair, &identity_keypair]; assert!(process_command(&config).is_err()); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 65dfe462b3..919db93186 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1747,7 +1747,26 @@ pub fn process_deactivate_stake_account( &vote_account_address, rpc_client.commitment(), )?; - if !eligible_for_deactivate_delinquent(&vote_state.epoch_credits, current_epoch) { + + let is_eligible_for_deactivate_delinquent = match vote_state { + crate::vote::VoteStateWrapper::VoteState(ref vote_state) => { + eligible_for_deactivate_delinquent(&vote_state.epoch_credits, current_epoch) + } + crate::vote::VoteStateWrapper::AlpenglowVoteState(ref vote_state) => { + let credits = vote_state.epoch_credits().credits(); + let prev_credits = vote_state.epoch_credits().prev_credits(); + + let mut epoch_credits = vec![]; + + if credits != 0 && prev_credits != 0 { + epoch_credits.push((current_epoch, credits, prev_credits)); + }; + + eligible_for_deactivate_delinquent(epoch_credits.as_slice(), current_epoch) + } + }; + + if is_eligible_for_deactivate_delinquent { return Err(CliError::BadParameter(format!( "Stake has not been delinquent for {} epochs", stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION, diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 71c95a09b8..003bdcf4b6 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -15,6 +15,7 @@ use { }, clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}, solana_account::Account, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey}, solana_clap_utils::{ compute_budget::{compute_unit_price_arg, ComputeUnitLimit, COMPUTE_UNIT_PRICE_ARG}, fee_payer::{fee_payer_arg, FEE_PAYER_ARG}, @@ -40,12 +41,18 @@ use { solana_system_interface::error::SystemError, solana_transaction::Transaction, solana_vote_program::{ + authorized_voters::AuthorizedVoters, vote_error::VoteError, vote_instruction::{self, withdraw, CreateVoteAccountConfig}, vote_state::{ - VoteAuthorize, VoteInit, VoteState, VoteStateVersions, VOTE_CREDITS_MAXIMUM_PER_SLOT, + BlockTimestamp, VoteAuthorize, VoteInit, VoteState, VoteStateVersions, + VOTE_CREDITS_MAXIMUM_PER_SLOT, }, }, + solana_votor_messages::{ + bls_message::BLS_KEYPAIR_DERIVE_SEED, instruction::InitializeAccountInstructionData, + state::VoteState as AlpenglowVoteState, + }, std::rc::Rc, }; @@ -119,6 +126,15 @@ impl VoteSubCommands for App<'_, '_> { will be at a derived address of the VOTE ACCOUNT pubkey", ), ) + .arg( + Arg::with_name("alpenglow") + .long("alpenglow") + .takes_value(false) + .help( + "When enabled, creates an Alpenglow vote account. When disabled, \ + creates a POH vote account.", + ), + ) .offline_args() .nonce_args(false) .arg(fee_payer_arg()) @@ -474,6 +490,7 @@ pub fn parse_create_vote_account( signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name); + let is_alpenglow = matches.is_present("alpenglow"); if !allow_unsafe { if authorized_withdrawer == vote_account_pubkey.unwrap() { @@ -515,6 +532,7 @@ pub fn parse_create_vote_account( memo, fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), compute_unit_price, + is_alpenglow, }, signers: signer_info.signers, }) @@ -808,6 +826,7 @@ pub fn process_create_vote_account( memo: Option<&String>, fee_payer: SignerIndex, compute_unit_price: Option, + is_alpenglow: bool, ) -> ProcessResult { let vote_account = config.signers[vote_account]; let vote_account_pubkey = vote_account.pubkey(); @@ -829,48 +848,85 @@ pub fn process_create_vote_account( )?; let required_balance = rpc_client - .get_minimum_balance_for_rent_exemption(VoteState::size_of())? + .get_minimum_balance_for_rent_exemption(if is_alpenglow { + solana_votor_messages::state::VoteState::size() + } else { + VoteState::size_of() + })? .max(1); + let amount = SpendAmount::Some(required_balance); let fee_payer = config.signers[fee_payer]; let nonce_authority = config.signers[nonce_authority]; - let space = VoteStateVersions::vote_state_size_of(true) as u64; - let compute_unit_limit = match blockhash_query { BlockhashQuery::None(_) | BlockhashQuery::FeeCalculator(_, _) => ComputeUnitLimit::Default, BlockhashQuery::All(_) => ComputeUnitLimit::Simulated, }; + let build_message = |lamports| { - let vote_init = VoteInit { - node_pubkey: identity_pubkey, - authorized_voter: authorized_voter.unwrap_or(identity_pubkey), - authorized_withdrawer, - commission, - }; - let mut create_vote_account_config = CreateVoteAccountConfig { - space, - ..CreateVoteAccountConfig::default() - }; - let to = if let Some(seed) = seed { - create_vote_account_config.with_seed = Some((&vote_account_pubkey, seed)); - &vote_account_address + let node_pubkey = identity_pubkey; + let authorized_voter = authorized_voter.unwrap_or(identity_pubkey); + + let from_pubkey = &config.signers[0].pubkey(); + let to_pubkey = &vote_account_address; + + let mut ixs = if is_alpenglow { + let bls_keypair = + BLSKeypair::derive_from_signer(&identity_account, BLS_KEYPAIR_DERIVE_SEED).unwrap(); + let bls_pubkey: BLSPubkey = bls_keypair.public.into(); + let initialize_account_ixn_meta = InitializeAccountInstructionData { + node_pubkey, + authorized_voter, + authorized_withdrawer, + commission, + bls_pubkey, + }; + + let create_ix = solana_system_interface::instruction::create_account( + from_pubkey, + to_pubkey, + lamports, + solana_votor_messages::state::VoteState::size() as u64, + &solana_votor_messages::id(), + ); + + let init_ix = solana_votor_messages::instruction::initialize_account( + *to_pubkey, + &initialize_account_ixn_meta, + ); + + vec![create_ix, init_ix] } else { - &vote_account_pubkey + let vote_init = VoteInit { + node_pubkey, + authorized_voter, + authorized_withdrawer, + commission, + }; + let mut create_vote_account_config = CreateVoteAccountConfig { + space: VoteStateVersions::vote_state_size_of(true) as u64, + ..CreateVoteAccountConfig::default() + }; + if let Some(seed) = seed { + create_vote_account_config.with_seed = Some((&vote_account_pubkey, seed)); + } + + vote_instruction::create_account_with_config( + from_pubkey, + to_pubkey, + &vote_init, + lamports, + create_vote_account_config, + ) }; - let ixs = vote_instruction::create_account_with_config( - &config.signers[0].pubkey(), - to, - &vote_init, - lamports, - create_vote_account_config, - ) - .with_memo(memo) - .with_compute_unit_config(&ComputeUnitConfig { - compute_unit_price, - compute_unit_limit, - }); + ixs = ixs + .with_memo(memo) + .with_compute_unit_config(&ComputeUnitConfig { + compute_unit_price, + compute_unit_limit, + }); if let Some(nonce_account) = &nonce_account { Message::new_with_nonce( @@ -976,15 +1032,15 @@ pub fn process_vote_authorize( if let Some(vote_state) = vote_state { let current_epoch = rpc_client.get_epoch_info()?.epoch; let current_authorized_voter = vote_state - .authorized_voters() .get_authorized_voter(current_epoch) .ok_or_else(|| { CliError::RpcRequestError( "Invalid vote account state; no authorized voters found".to_string(), ) })?; + check_current_authority( - &[current_authorized_voter, vote_state.authorized_withdrawer], + &[current_authorized_voter, vote_state.authorized_withdrawer()], &authorized.pubkey(), )?; if let Some(signer) = new_authorized_signer { @@ -1004,7 +1060,10 @@ pub fn process_vote_authorize( (new_authorized_pubkey, "new_authorized_pubkey".to_string()), )?; if let Some(vote_state) = vote_state { - check_current_authority(&[vote_state.authorized_withdrawer], &authorized.pubkey())? + check_current_authority( + &[vote_state.authorized_withdrawer()], + &authorized.pubkey(), + )? } } } @@ -1257,11 +1316,71 @@ pub fn process_vote_update_commission( } } +#[allow(clippy::large_enum_variant)] +pub(crate) enum VoteStateWrapper { + VoteState(VoteState), + AlpenglowVoteState(AlpenglowVoteState), +} + +impl VoteStateWrapper { + pub fn get_authorized_voter(&self, epoch: u64) -> Option { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.get_authorized_voter(epoch), + VoteStateWrapper::AlpenglowVoteState(vote_state) => { + vote_state.get_authorized_voter(epoch) + } + } + } + + pub fn authorized_withdrawer(&self) -> Pubkey { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.authorized_withdrawer, + VoteStateWrapper::AlpenglowVoteState(vote_state) => *vote_state.authorized_withdrawer(), + } + } + + pub fn node_pubkey(&self) -> Pubkey { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.node_pubkey, + VoteStateWrapper::AlpenglowVoteState(vote_state) => *vote_state.node_pubkey(), + } + } + + pub fn credits(&self) -> u64 { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.credits(), + VoteStateWrapper::AlpenglowVoteState(vote_state) => { + vote_state.epoch_credits().credits() + } + } + } + + pub fn commission(&self) -> u8 { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.commission, + VoteStateWrapper::AlpenglowVoteState(vote_state) => vote_state.commission(), + } + } + + pub fn last_timestamp(&self) -> BlockTimestamp { + match self { + VoteStateWrapper::VoteState(vote_state) => vote_state.last_timestamp.clone(), + VoteStateWrapper::AlpenglowVoteState(vote_state) => BlockTimestamp { + slot: vote_state.latest_timestamp_legacy_format().slot, + timestamp: vote_state.latest_timestamp_legacy_format().timestamp, + }, + } + } +} + +const SOLANA_VOTE_PROGRAM_ID: Pubkey = solana_vote_program::id(); +const ALPENGLOW_VOTE_PROGRAM_ID: Pubkey = solana_votor_messages::id(); + pub(crate) fn get_vote_account( rpc_client: &RpcClient, vote_account_pubkey: &Pubkey, commitment_config: CommitmentConfig, -) -> Result<(Account, VoteState), Box> { +) -> Result<(Account, VoteStateWrapper), Box> { let vote_account = rpc_client .get_account_with_commitment(vote_account_pubkey, commitment_config)? .value @@ -1269,19 +1388,32 @@ pub(crate) fn get_vote_account( CliError::RpcRequestError(format!("{vote_account_pubkey:?} account does not exist")) })?; - if vote_account.owner != solana_vote_program::id() { - return Err(CliError::RpcRequestError(format!( - "{vote_account_pubkey:?} is not a vote account" - )) - .into()); - } - let vote_state = VoteState::deserialize(&vote_account.data).map_err(|_| { - CliError::RpcRequestError( - "Account data could not be deserialized to vote state".to_string(), - ) - })?; + let vote_state_wrapper = match vote_account.owner { + SOLANA_VOTE_PROGRAM_ID => VoteStateWrapper::VoteState( + VoteState::deserialize(&vote_account.data).map_err(|_| { + CliError::RpcRequestError( + "Account data could not be deserialized to vote state".to_string(), + ) + })?, + ), + + ALPENGLOW_VOTE_PROGRAM_ID => VoteStateWrapper::AlpenglowVoteState( + *AlpenglowVoteState::deserialize(&vote_account.data).map_err(|_| { + CliError::RpcRequestError( + "Account data could not be deserialized to vote state".to_string(), + ) + })?, + ), + + _ => { + return Err(CliError::RpcRequestError(format!( + "{vote_account_pubkey:?} is not a vote account" + )) + .into()) + } + }; - Ok((vote_account, vote_state)) + Ok((vote_account, vote_state_wrapper)) } pub fn process_show_vote_account( @@ -1303,55 +1435,73 @@ pub fn process_show_vote_account( let mut votes: Vec = vec![]; let mut epoch_voting_history: Vec = vec![]; - if !vote_state.votes.is_empty() { - for vote in &vote_state.votes { - votes.push(vote.into()); + let mut epoch_rewards = None; + + // TODO: handle Alpenglow case + if let VoteStateWrapper::VoteState(ref vote_state) = vote_state { + if !vote_state.votes.is_empty() { + for vote in &vote_state.votes { + votes.push(vote.into()); + } + for (epoch, credits, prev_credits) in vote_state.epoch_credits().iter().copied() { + let credits_earned = credits.saturating_sub(prev_credits); + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); + let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); + let max_credits_per_slot = if is_tvc_active { + VOTE_CREDITS_MAXIMUM_PER_SLOT + } else { + 1 + }; + epoch_voting_history.push(CliEpochVotingHistory { + epoch, + slots_in_epoch, + credits_earned, + credits, + prev_credits, + max_credits_per_slot, + }); + } } - for (epoch, credits, prev_credits) in vote_state.epoch_credits().iter().copied() { - let credits_earned = credits.saturating_sub(prev_credits); - let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); - let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); - let max_credits_per_slot = if is_tvc_active { - VOTE_CREDITS_MAXIMUM_PER_SLOT - } else { - 1 - }; - epoch_voting_history.push(CliEpochVotingHistory { - epoch, - slots_in_epoch, - credits_earned, - credits, - prev_credits, - max_credits_per_slot, + + epoch_rewards = + with_rewards.and_then(|num_epochs| { + match crate::stake::fetch_epoch_rewards( + rpc_client, + vote_account_address, + num_epochs, + starting_epoch, + ) { + Ok(rewards) => Some(rewards), + Err(error) => { + eprintln!("Failed to fetch epoch rewards: {error:?}"); + None + } + } }); - } } - let epoch_rewards = - with_rewards.and_then(|num_epochs| { - match crate::stake::fetch_epoch_rewards( - rpc_client, - vote_account_address, - num_epochs, - starting_epoch, - ) { - Ok(rewards) => Some(rewards), - Err(error) => { - eprintln!("Failed to fetch epoch rewards: {error:?}"); - None - } - } - }); + let authorized_voters = match vote_state { + VoteStateWrapper::VoteState(ref vote_state) => vote_state.authorized_voters(), + // TODO: implement this properly for AlpenglowVoteState + VoteStateWrapper::AlpenglowVoteState(_) => &AuthorizedVoters::default(), + }; + + let root_slot = match vote_state { + VoteStateWrapper::VoteState(ref vote_state) => vote_state.root_slot, + // TODO: no real equivalent for Alpenglow - we should really change + // process_show_vote_account properly + VoteStateWrapper::AlpenglowVoteState(_) => None, + }; let vote_account_data = CliVoteAccount { account_balance: vote_account.lamports, - validator_identity: vote_state.node_pubkey.to_string(), - authorized_voters: vote_state.authorized_voters().into(), - authorized_withdrawer: vote_state.authorized_withdrawer.to_string(), + validator_identity: vote_state.node_pubkey().to_string(), + authorized_voters: authorized_voters.into(), + authorized_withdrawer: vote_state.authorized_withdrawer().to_string(), credits: vote_state.credits(), - commission: vote_state.commission, - root_slot: vote_state.root_slot, - recent_timestamp: vote_state.last_timestamp.clone(), + commission: vote_state.commission(), + root_slot, + recent_timestamp: vote_state.last_timestamp(), votes, epoch_voting_history, use_lamports_unit, @@ -1853,6 +2003,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), @@ -1887,6 +2038,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), @@ -1928,6 +2080,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), @@ -1981,6 +2134,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), @@ -2024,6 +2178,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), @@ -2063,6 +2218,7 @@ mod tests { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }, signers: vec![ Box::new(read_keypair_file(&default_keypair_file).unwrap()), diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 2eb0dc35ba..8915a026ef 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -93,6 +93,7 @@ fn test_stake_delegation_force() { memo: None, fee_payer: 0, compute_unit_price: None, + is_alpenglow: false, }; process_command(&config).unwrap(); diff --git a/cli/tests/vote.rs b/cli/tests/vote.rs index 8591e88d45..86efec0e0d 100644 --- a/cli/tests/vote.rs +++ b/cli/tests/vote.rs @@ -59,6 +59,7 @@ fn test_vote_authorize_and_withdraw(compute_unit_price: Option) { memo: None, fee_payer: 0, compute_unit_price, + is_alpenglow: false, }; process_command(&config).unwrap(); let vote_account = rpc_client @@ -287,6 +288,7 @@ fn test_offline_vote_authorize_and_withdraw(compute_unit_price: Option) { memo: None, fee_payer: 0, compute_unit_price, + is_alpenglow: false, }; process_command(&config_payer).unwrap(); let vote_account = rpc_client diff --git a/core/Cargo.toml b/core/Cargo.toml index 7f89cdc930..dc25eeab39 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,7 @@ arrayvec = { workspace = true } assert_matches = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } +bitvec = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } @@ -50,6 +51,9 @@ serde_derive = { workspace = true } slab = { workspace = true } solana-accounts-db = { workspace = true } solana-bloom = { workspace = true } +solana-bls-signatures = { workspace = true, features = [ + "solana-signer-derive", +] } solana-builtins-default-costs = { workspace = true } solana-client = { workspace = true } solana-compute-budget = { workspace = true } @@ -97,6 +101,8 @@ solana-unified-scheduler-pool = { workspace = true } solana-version = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor = { workspace = true } +solana-votor-messages = { workspace = true } solana-wen-restart = { workspace = true } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } @@ -111,7 +117,9 @@ fs_extra = { workspace = true } serde_json = { workspace = true } serial_test = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` +solana-account = { workspace = true } solana-address-lookup-table-program = { workspace = true } +solana-build-alpenglow-vote = { workspace = true } solana-compute-budget-program = { workspace = true } solana-core = { path = ".", features = ["dev-context-only-utils"] } solana-cost-model = { workspace = true, features = ["dev-context-only-utils"] } @@ -119,6 +127,7 @@ solana-ledger = { workspace = true, features = ["dev-context-only-utils"] } solana-logger = { workspace = true } solana-net-utils = { workspace = true, features = ["dev-context-only-utils"] } solana-poh = { workspace = true, features = ["dev-context-only-utils"] } +solana-poh-config = { workspace = true } solana-program-runtime = { workspace = true, features = ["metrics"] } solana-rpc = { workspace = true, features = ["dev-context-only-utils"] } solana-sdk = { workspace = true, features = ["dev-context-only-utils"] } diff --git a/core/benches/consumer.rs b/core/benches/consumer.rs index 3a89cdfd39..e36650a521 100644 --- a/core/benches/consumer.rs +++ b/core/benches/consumer.rs @@ -2,14 +2,12 @@ #![feature(test)] use { - crossbeam_channel::{unbounded, Receiver}, + crossbeam_channel::Receiver, rayon::{ iter::IndexedParallelIterator, prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, }, - solana_core::banking_stage::{ - committer::Committer, consumer::Consumer, qos_service::QosService, - }, + solana_core::banking_stage::consumer::Consumer, solana_entry::entry::Entry, solana_ledger::{ blockstore::Blockstore, @@ -81,13 +79,6 @@ fn create_transactions(bank: &Bank, num: usize) -> Vec) -> Consumer { - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let committer = Committer::new(None, replay_vote_sender, Arc::default()); - let transaction_recorder = poh_recorder.read().unwrap().new_recorder(); - Consumer::new(committer, transaction_recorder, QosService::new(0), None) -} - struct BenchFrame { bank: Arc, _bank_forks: Arc>, @@ -156,7 +147,7 @@ fn bench_process_and_record_transactions(bencher: &mut Bencher, batch_size: usiz poh_service, signal_receiver: _signal_receiver, } = setup(); - let consumer = create_consumer(&poh_recorder); + let consumer = Consumer::from(&*poh_recorder); let transactions = create_transactions(&bank, 2_usize.pow(20)); let mut transaction_iter = transactions.chunks(batch_size); diff --git a/core/benches/sigverify_stage.rs b/core/benches/sigverify_stage.rs index a0d272db5b..e3d9b3b6cf 100644 --- a/core/benches/sigverify_stage.rs +++ b/core/benches/sigverify_stage.rs @@ -13,7 +13,7 @@ use { }, solana_core::{ banking_trace::BankingTracer, - sigverify::TransactionSigVerifier, + sigverifier::ed25519_sigverifier::TransactionSigVerifier, sigverify_stage::{SigVerifier, SigVerifyStage}, }, solana_measure::measure::Measure, diff --git a/core/src/banking_simulation.rs b/core/src/banking_simulation.rs index 82c72f7711..755a896d9d 100644 --- a/core/src/banking_simulation.rs +++ b/core/src/banking_simulation.rs @@ -45,6 +45,7 @@ use { }, solana_streamer::socket::SocketAddrSpace, solana_turbine::broadcast_stage::{BroadcastStage, BroadcastStageType}, + solana_votor::event::VotorEventReceiver, std::{ collections::BTreeMap, fmt::Display, @@ -419,6 +420,7 @@ struct SimulatorLoop { leader_schedule_cache: Arc, retransmit_slots_sender: Sender, retracer: Arc, + _completed_block_receiver: VotorEventReceiver, } impl SimulatorLoop { @@ -739,6 +741,7 @@ impl BankingSimulator { &genesis_config.poh_config, None, exit.clone(), + false, ); let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let poh_service = PohService::new( @@ -749,6 +752,7 @@ impl BankingSimulator { DEFAULT_PINNED_CPU_CORE, DEFAULT_HASHES_PER_BATCH, record_receiver, + || {}, ); // Enable BankingTracer to approximate the real environment as close as possible because @@ -788,6 +792,7 @@ impl BankingSimulator { let (replay_vote_sender, _replay_vote_receiver) = unbounded(); let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (completed_block_sender, completed_block_receiver) = unbounded(); let shred_version = compute_shred_version( &genesis_config.hash(), Some(&bank_forks.read().unwrap().root_bank().hard_forks()), @@ -815,6 +820,7 @@ impl BankingSimulator { bank_forks.clone(), shred_version, sender, + completed_block_sender, ); info!("Start banking stage!..."); @@ -887,6 +893,7 @@ impl BankingSimulator { leader_schedule_cache, retransmit_slots_sender, retracer, + _completed_block_receiver: completed_block_receiver, }; let simulator_threads = SimulatorThreads { diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index ab9ce8985b..f2920ff0a0 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -1,9 +1,6 @@ //! The `banking_stage` processes Transaction messages. It is intended to be used //! to construct a software pipeline. The stage uses all available CPU cores and //! can do its processing in parallel with signature verification on the GPU. - -#[cfg(feature = "dev-context-only-utils")] -use qualifier_attr::qualifiers; use { self::{ committer::Committer, @@ -38,7 +35,12 @@ use { bank::Bank, bank_forks::BankForks, prioritization_fee_cache::PrioritizationFeeCache, vote_sender_types::ReplayVoteSender, }, - solana_sdk::{pubkey::Pubkey, timing::AtomicInterval}, + solana_runtime_transaction::runtime_transaction::RuntimeTransaction, + solana_sdk::{ + pubkey::Pubkey, + timing::AtomicInterval, + transaction::{MessageHash, SanitizedTransaction, VersionedTransaction}, + }, std::{ cmp, env, ops::Deref, @@ -722,8 +724,7 @@ impl BankingStage { } } -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -pub(crate) fn update_bank_forks_and_poh_recorder_for_new_tpu_bank( +pub fn update_bank_forks_and_poh_recorder_for_new_tpu_bank( bank_forks: &RwLock, poh_recorder: &RwLock, tpu_bank: Bank, @@ -736,6 +737,78 @@ pub(crate) fn update_bank_forks_and_poh_recorder_for_new_tpu_bank( .set_bank(tpu_bank, track_transaction_indexes); } +#[allow(dead_code)] +pub fn commit_certificate( + bank: &Arc, + poh_recorder: &RwLock, + certificate: Vec, +) -> bool { + if certificate.is_empty() { + return true; + } + let consumer = Consumer::from(poh_recorder); + let runtime_transactions: Result>, _> = + certificate + .into_iter() + .map(|versioned_tx| { + // Short circuits on first error because + // transactions in the certificate need to + // be guaranteed to not fail + RuntimeTransaction::try_create( + versioned_tx, + MessageHash::Compute, + None, + &**bank, + bank.get_reserved_account_keys(), + ) + }) + .collect(); + + //TODO: guarantee these transactions don't fail + if let Err(e) = runtime_transactions { + error!( + "Error in bank {} creating runtime transaction in certificate {:?}", + bank.slot(), + e + ); + return false; + } + + let runtime_transactions = runtime_transactions.unwrap(); + let summary = consumer.process_transactions(bank, &Instant::now(), &runtime_transactions); + + if summary.reached_max_poh_height { + error!("Slot took too long to ingest votes {}", bank.slot()); + datapoint_error!( + "vote_certificate_commit_failure", + ("error", "slot took too long to ingest votes", String), + ("slot", bank.slot(), i64) + ); + // TODO: check if 2/3 of the stake landed, otherwise return false + return false; + } + + if summary.error_counters.total.0 != 0 { + error!( + "Vote certificate commit failure {} errors occured", + summary.error_counters.total.0 + ); + datapoint_error!( + "vote_certificate_commit_failure", + ( + "error", + format!("{} errors occurred", summary.error_counters.total.0), + String + ), + ("slot", bank.slot(), i64) + ); + // TODO: check if 2/3 of the stake landed, otherwise return false + return false; + } + + true +} + #[cfg(test)] mod tests { use { diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index 3c5234d3c0..e2fe8ab49c 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -11,14 +11,15 @@ use { vote_storage::{ConsumeScannerPayload, VoteStorage}, BankingStageStats, }, + crossbeam_channel::unbounded, itertools::Itertools, solana_feature_set as feature_set, solana_fee::FeeFeatures, solana_ledger::token_balances::collect_token_balances, solana_measure::{measure::Measure, measure_us}, solana_poh::poh_recorder::{ - BankStart, PohRecorderError, RecordTransactionsSummary, RecordTransactionsTimings, - TransactionRecorder, + BankStart, PohRecorder, PohRecorderError, RecordTransactionsSummary, + RecordTransactionsTimings, TransactionRecorder, }, solana_runtime::{ bank::{Bank, LoadAndExecuteTransactionsOutput}, @@ -42,7 +43,7 @@ use { solana_timings::ExecuteTimings, std::{ num::Saturating, - sync::{atomic::Ordering, Arc}, + sync::{atomic::Ordering, Arc, RwLock}, time::Instant, }, }; @@ -93,6 +94,20 @@ pub struct Consumer { log_messages_bytes_limit: Option, } +impl From<&RwLock> for Consumer { + fn from(poh_recorder: &RwLock) -> Self { + let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let committer = Committer::new(None, replay_vote_sender, Arc::default()); + let transaction_recorder: TransactionRecorder = poh_recorder.read().unwrap().new_recorder(); + Self::new( + committer, + transaction_recorder, + QosService::new(u32::MAX), + None, + ) + } +} + impl Consumer { pub fn new( committer: Committer, @@ -200,6 +215,7 @@ impl Consumer { .. } = process_transactions_summary; + // TODO(ashwin): add a minus delta if reached_max_poh_height || !bank_start.should_working_bank_still_be_processing_txs() { payload.reached_end_of_slot = true; } @@ -281,7 +297,7 @@ impl Consumer { /// /// Returns the number of transactions successfully processed by the bank, which may be less /// than the total number if max PoH height was reached and the bank halted - fn process_transactions( + pub(crate) fn process_transactions( &self, bank: &Arc, bank_creation_time: &Instant, diff --git a/core/src/banking_stage/decision_maker.rs b/core/src/banking_stage/decision_maker.rs index bde4701233..3908b87f73 100644 --- a/core/src/banking_stage/decision_maker.rs +++ b/core/src/banking_stage/decision_maker.rs @@ -1,4 +1,5 @@ use { + solana_feature_set, solana_poh::poh_recorder::{BankStart, PohRecorder}, solana_sdk::{ clock::{ @@ -8,7 +9,7 @@ use { pubkey::Pubkey, }, std::{ - sync::{Arc, RwLock}, + sync::{atomic::Ordering, Arc, RwLock}, time::{Duration, Instant}, }, }; @@ -116,9 +117,22 @@ impl DecisionMaker { } fn bank_start(poh_recorder: &PohRecorder) -> Option { - poh_recorder - .bank_start() - .filter(|bank_start| bank_start.should_working_bank_still_be_processing_txs()) + poh_recorder.bank_start().filter(|bank_start| { + let first_alpenglow_slot = bank_start + .working_bank + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()) + .unwrap_or(u64::MAX); + let contains_valid_certificate = + if bank_start.working_bank.slot() >= first_alpenglow_slot { + bank_start + .contains_valid_certificate + .load(Ordering::Relaxed) + } else { + true + }; + contains_valid_certificate && bank_start.should_working_bank_still_be_processing_txs() + }) } fn would_be_leader_shortly(poh_recorder: &PohRecorder) -> bool { @@ -141,13 +155,18 @@ mod tests { use { super::*, core::panic, - solana_ledger::{blockstore::Blockstore, genesis_utils::create_genesis_config}, + solana_ledger::{ + blockstore::Blockstore, genesis_utils::create_genesis_config, + get_tmp_ledger_path_auto_delete, + }, solana_poh::poh_recorder::create_test_recorder, solana_runtime::bank::Bank, solana_sdk::clock::NUM_CONSECUTIVE_LEADER_SLOTS, std::{ - env::temp_dir, - sync::{atomic::Ordering, Arc}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, time::Instant, }, }; @@ -156,6 +175,7 @@ mod tests { fn test_buffered_packet_decision_bank_start() { let bank = Arc::new(Bank::default_for_tests()); let bank_start = BankStart { + contains_valid_certificate: Arc::new(AtomicBool::new(true)), working_bank: bank, bank_creation_time: Arc::new(Instant::now()), }; @@ -173,8 +193,8 @@ mod tests { fn test_make_consume_or_forward_decision() { let genesis_config = create_genesis_config(2).genesis_config; let (bank, _bank_forks) = Bank::new_no_wallclock_throttle_for_tests(&genesis_config); - let ledger_path = temp_dir(); - let blockstore = Arc::new(Blockstore::open(ledger_path.as_path()).unwrap()); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); let (exit, poh_recorder, poh_service, _entry_receiver) = create_test_recorder(bank.clone(), blockstore, None, None); // Drop the poh service immediately to avoid potential ticking @@ -239,15 +259,109 @@ mod tests { } } + #[test] + fn test_make_consume_or_forward_decision_alpenglow() { + let genesis_config = create_genesis_config(2).genesis_config; + let (bank, _bank_forks) = Bank::new_no_wallclock_throttle_for_tests(&genesis_config); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); + let (exit, poh_recorder, poh_service, _entry_receiver) = + create_test_recorder(bank.clone(), blockstore, None, None); + // Drop the poh service immediately to avoid potential ticking + exit.store(true, Ordering::Relaxed); + poh_service.join().unwrap(); + + let my_pubkey = Pubkey::new_unique(); + let decision_maker = DecisionMaker::new(my_pubkey, poh_recorder.clone()); + poh_recorder.write().unwrap().reset(bank.clone(), None); + let slot = bank.slot() + 1; + let mut bank = Bank::new_from_parent(bank, &my_pubkey, slot); + bank.activate_feature(&solana_feature_set::secp256k1_program_enabled::id()); + let bank = Arc::new(bank); + + // Currently Leader, with alpenglow enabled, no certificate - Hold + { + poh_recorder + .write() + .unwrap() + .set_bank_for_test(bank.clone()); + assert!(!poh_recorder + .write() + .unwrap() + .bank_start() + .unwrap() + .contains_valid_certificate + .load(Ordering::Relaxed)); + let decision = decision_maker.make_consume_or_forward_decision_no_cache(); + assert_matches!(decision, BufferedPacketsDecision::Hold); + } + + // Currently Leader, with alpenglow enabled, certificate valid - Consume + { + poh_recorder + .write() + .unwrap() + .bank_start() + .unwrap() + .contains_valid_certificate + .store(true, Ordering::Relaxed); + let decision = decision_maker.make_consume_or_forward_decision_no_cache(); + assert_matches!(decision, BufferedPacketsDecision::Consume(_)); + } + + // Will be leader shortly - Hold + for next_leader_slot_offset in [0, 1].into_iter() { + let next_leader_slot = bank.slot() + next_leader_slot_offset; + poh_recorder.write().unwrap().reset( + bank.clone(), + Some(( + next_leader_slot, + next_leader_slot + NUM_CONSECUTIVE_LEADER_SLOTS, + )), + ); + let decision = decision_maker.make_consume_or_forward_decision_no_cache(); + assert!( + matches!(decision, BufferedPacketsDecision::Hold), + "next_leader_slot_offset: {next_leader_slot_offset}", + ); + } + + // Will be leader - ForwardAndHold + for next_leader_slot_offset in [2, 19].into_iter() { + let next_leader_slot = bank.slot() + next_leader_slot_offset; + poh_recorder.write().unwrap().reset( + bank.clone(), + Some(( + next_leader_slot, + next_leader_slot + NUM_CONSECUTIVE_LEADER_SLOTS + 1, + )), + ); + let decision = decision_maker.make_consume_or_forward_decision_no_cache(); + assert!( + matches!(decision, BufferedPacketsDecision::ForwardAndHold), + "next_leader_slot_offset: {next_leader_slot_offset}", + ); + } + + // Known leader, not me - Forward + { + poh_recorder.write().unwrap().reset(bank, None); + let decision = decision_maker.make_consume_or_forward_decision_no_cache(); + assert_matches!(decision, BufferedPacketsDecision::Forward); + } + } + #[test] fn test_should_process_or_forward_packets() { let my_pubkey = solana_pubkey::new_rand(); let my_pubkey1 = solana_pubkey::new_rand(); let bank = Arc::new(Bank::default_for_tests()); let bank_start = Some(BankStart { + contains_valid_certificate: Arc::new(AtomicBool::new(true)), working_bank: bank, bank_creation_time: Arc::new(Instant::now()), }); + // having active bank allows to consume immediately assert_matches!( DecisionMaker::consume_or_forward_packets( diff --git a/core/src/banking_stage/leader_slot_metrics.rs b/core/src/banking_stage/leader_slot_metrics.rs index 605dea1ce8..0e0d07d67f 100644 --- a/core/src/banking_stage/leader_slot_metrics.rs +++ b/core/src/banking_stage/leader_slot_metrics.rs @@ -481,7 +481,7 @@ impl VotePacketCountMetrics { "id" => id, ("slot", slot, i64), ("dropped_gossip_votes", self.dropped_gossip_votes, i64), - ("dropped_tpu_votes", self.dropped_tpu_votes, i64) + ("dropped_tpu_votes", self.dropped_tpu_votes, i64), ); } } @@ -916,7 +916,10 @@ mod tests { super::*, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, genesis_utils::create_genesis_config}, - std::{mem, sync::Arc}, + std::{ + mem, + sync::{atomic::AtomicBool, Arc}, + }, }; struct TestSlotBoundaryComponents { @@ -931,6 +934,7 @@ mod tests { let genesis = create_genesis_config(10); let first_bank = Arc::new(Bank::new_for_tests(&genesis.genesis_config)); let first_poh_recorder_bank = BankStart { + contains_valid_certificate: Arc::new(AtomicBool::new(true)), working_bank: first_bank.clone(), bank_creation_time: Arc::new(Instant::now()), }; @@ -942,6 +946,7 @@ mod tests { first_bank.slot() + 1, )); let next_poh_recorder_bank = BankStart { + contains_valid_certificate: Arc::new(AtomicBool::new(true)), working_bank: next_bank.clone(), bank_creation_time: Arc::new(Instant::now()), }; diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs new file mode 100644 index 0000000000..d20d1125f6 --- /dev/null +++ b/core/src/block_creation_loop.rs @@ -0,0 +1,452 @@ +//! The Alpenglow block creation loop +//! When our leader window is reached, attempts to create our leader blocks +//! within the block timeouts. Responsible for inserting empty banks for +//! banking stage to fill, and clearing banks once the timeout has been reached. +use { + crate::{ + banking_trace::BankingTracer, + replay_stage::{Finalizer, ReplayStage}, + }, + crossbeam_channel::{Receiver, RecvTimeoutError}, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::{ + blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, + leader_schedule_utils::leader_slot_index, + }, + solana_poh::poh_recorder::{PohRecorder, Record, GRACE_TICKS_FACTOR, MAX_GRACE_SLOTS}, + solana_rpc::{rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier}, + solana_runtime::{ + bank::{Bank, NewBankOptions}, + bank_forks::BankForks, + }, + solana_sdk::{clock::Slot, pubkey::Pubkey}, + solana_votor::{block_timeout, event::LeaderWindowInfo, votor::LeaderWindowNotifier}, + std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Condvar, Mutex, RwLock, + }, + thread, + time::{Duration, Instant}, + }, + thiserror::Error, +}; + +pub struct BlockCreationLoopConfig { + pub exit: Arc, + pub track_transaction_indexes: bool, + + // Shared state + pub bank_forks: Arc>, + pub blockstore: Arc, + pub cluster_info: Arc, + pub poh_recorder: Arc>, + pub leader_schedule_cache: Arc, + pub rpc_subscriptions: Arc, + + // Notifiers + pub banking_tracer: Arc, + pub slot_status_notifier: Option, + + // Receivers / notifications from banking stage / replay / voting loop + pub record_receiver: Receiver, + pub leader_window_notifier: Arc, + pub replay_highest_frozen: Arc, +} + +struct LeaderContext { + my_pubkey: Pubkey, + blockstore: Arc, + poh_recorder: Arc>, + leader_schedule_cache: Arc, + bank_forks: Arc>, + rpc_subscriptions: Arc, + slot_status_notifier: Option, + banking_tracer: Arc, + track_transaction_indexes: bool, + replay_highest_frozen: Arc, +} + +#[derive(Default)] +pub struct ReplayHighestFrozen { + pub highest_frozen_slot: Mutex, + pub freeze_notification: Condvar, +} + +#[derive(Debug, Error)] +enum StartLeaderError { + /// Replay has not yet frozen the parent slot + #[error("Replay is behind for parent slot {0}")] + ReplayIsBehind(/* parent slot */ Slot), + + /// Startup verification is not yet complete + #[error("Startup verification is incomplete on parent bank {0}")] + StartupVerificationIncomplete(/* parent slot */ Slot), + + /// Bank forks already contains bank + #[error("Already contain bank for leader slot {0}")] + AlreadyHaveBank(/* leader slot */ Slot), + + /// Haven't landed a vote + #[error("Have not rooted a block with our vote")] + VoteNotRooted, +} + +fn start_receive_and_record_loop( + exit: Arc, + poh_recorder: Arc>, + record_receiver: Receiver, +) { + while !exit.load(Ordering::Relaxed) { + // We need a timeout here to check the exit flag, chose 400ms + // for now but can be longer if needed. + match record_receiver.recv_timeout(Duration::from_millis(400)) { + Ok(record) => { + if record + .sender + .send(poh_recorder.write().unwrap().record( + record.slot, + record.mixin, + record.transactions, + )) + .is_err() + { + panic!("Error returning mixin hash"); + } + } + Err(RecvTimeoutError::Disconnected) => { + info!("Record receiver disconnected"); + return; + } + Err(RecvTimeoutError::Timeout) => (), + } + } +} + +/// The block creation loop. +/// +/// The `votor::certificate_pool_service` tracks when it is our leader window, and populates +/// communicates the skip timer and parent slot for our window. This loop takes the responsibility +/// of creating our `NUM_CONSECUTIVE_LEADER_SLOTS` blocks and finishing them within the required timeout. +pub fn start_loop(config: BlockCreationLoopConfig) { + let BlockCreationLoopConfig { + exit, + track_transaction_indexes, + bank_forks, + blockstore, + cluster_info, + poh_recorder, + leader_schedule_cache, + rpc_subscriptions, + banking_tracer, + slot_status_notifier, + leader_window_notifier, + replay_highest_frozen, + record_receiver, + } = config; + + // Similar to the voting loop, if this loop dies kill the validator + let _exit = Finalizer::new(exit.clone()); + + // get latest identity pubkey during startup + let mut my_pubkey = cluster_info.id(); + let leader_bank_notifier = poh_recorder.read().unwrap().new_leader_bank_notifier(); + + let mut ctx = LeaderContext { + my_pubkey, + blockstore, + poh_recorder: poh_recorder.clone(), + leader_schedule_cache, + bank_forks, + rpc_subscriptions, + slot_status_notifier, + banking_tracer, + track_transaction_indexes, + replay_highest_frozen, + }; + + // Setup poh + reset_poh_recorder(&ctx.bank_forks.read().unwrap().working_bank(), &ctx); + + // Start receive and record loop + let exit_c = exit.clone(); + let p_rec = poh_recorder.clone(); + let receive_record_loop = thread::spawn(move || { + start_receive_and_record_loop(exit_c, p_rec, record_receiver); + }); + + while !exit.load(Ordering::Relaxed) { + // Check if set-identity was called at each leader window start + if my_pubkey != cluster_info.id() { + // set-identity cli has been called during runtime + let my_old_pubkey = my_pubkey; + my_pubkey = cluster_info.id(); + ctx.my_pubkey = my_pubkey; + + warn!( + "Identity changed from {} to {} during block creation loop", + my_old_pubkey, my_pubkey + ); + } + + // Wait for the voting loop to notify us + let LeaderWindowInfo { + start_slot, + end_slot, + // TODO: handle duplicate blocks by using the hash here + parent_block: (parent_slot, _), + skip_timer, + } = { + let window_info = leader_window_notifier.window_info.lock().unwrap(); + let (mut guard, timeout_res) = leader_window_notifier + .window_notification + .wait_timeout_while(window_info, Duration::from_secs(1), |wi| wi.is_none()) + .unwrap(); + if timeout_res.timed_out() { + continue; + } + guard.take().unwrap() + }; + + trace!( + "Received window notification for {start_slot} to {end_slot} \ + parent: {parent_slot}" + ); + + if let Err(e) = start_leader_retry_replay(start_slot, parent_slot, skip_timer, &ctx) { + // Give up on this leader window + error!( + "{my_pubkey}: Unable to produce first slot {start_slot}, skipping production of our entire leader window \ + {start_slot}-{end_slot}: {e:?}" + ); + continue; + } + + // Produce our window + let mut slot = start_slot; + // TODO(ashwin): Handle preemption of leader window during this loop + while !exit.load(Ordering::Relaxed) { + let leader_index = leader_slot_index(slot); + let timeout = block_timeout(leader_index); + + // Wait for either the block timeout or for the bank to be completed + // The receive and record loop will fill the bank + let remaining_slot_time = timeout.saturating_sub(skip_timer.elapsed()); + trace!( + "{my_pubkey}: waiting for leader bank {slot} to finish, remaining time: {}", + remaining_slot_time.as_millis(), + ); + leader_bank_notifier.wait_for_completed(remaining_slot_time); + + // Time to complete the bank, there are two possibilities: + // (1) We hit the block timeout, the bank is still present we must clear it + // (2) The bank has filled up and been cleared by banking stage + { + let mut w_poh_recorder = poh_recorder.write().unwrap(); + if let Some(bank) = w_poh_recorder.bank() { + assert_eq!(bank.slot(), slot); + trace!( + "{}: bank {} has reached block timeout, ticking", + bank.collector_id(), + bank.slot() + ); + let max_tick_height = bank.max_tick_height(); + // Set the tick height for the bank to max_tick_height - 1, so that PohRecorder::flush_cache() + // will properly increment the tick_height to max_tick_height. + bank.set_tick_height(max_tick_height - 1); + // Write the single tick for this slot + // TODO: handle migration slot because we need to provide the PoH + // for slots from the previous epoch, but `tick_alpenglow()` will + // delete those ticks from the cache + drop(bank); + w_poh_recorder.tick_alpenglow(max_tick_height); + } else { + trace!("{my_pubkey}: {slot} reached max tick height, moving to next block"); + } + } + + assert!(!poh_recorder.read().unwrap().has_bank()); + + // Produce our next slot + slot += 1; + if slot > end_slot { + trace!("{my_pubkey}: finished leader window {start_slot}-{end_slot}"); + break; + } + + // Although `slot - 1`has been cleared from `poh_recorder`, it might not have finished processing in + // `replay_stage`, which is why we use `start_leader_retry_replay` + if let Err(e) = start_leader_retry_replay(slot, slot - 1, skip_timer, &ctx) { + error!("{my_pubkey}: Unable to produce {slot}, skipping rest of leader window {slot} - {end_slot}: {e:?}"); + break; + } + } + } + + receive_record_loop.join().unwrap(); +} + +/// Resets poh recorder +fn reset_poh_recorder(bank: &Arc, ctx: &LeaderContext) { + trace!("{}: resetting poh to {}", ctx.my_pubkey, bank.slot()); + let next_leader_slot = ctx.leader_schedule_cache.next_leader_slot( + &ctx.my_pubkey, + bank.slot(), + bank, + Some(ctx.blockstore.as_ref()), + GRACE_TICKS_FACTOR * MAX_GRACE_SLOTS, + ); + + ctx.poh_recorder + .write() + .unwrap() + .reset(bank.clone(), next_leader_slot); +} + +/// Similar to `maybe_start_leader`, however if replay is lagging we retry +/// until either replay finishes or we hit the block timeout. +fn start_leader_retry_replay( + slot: Slot, + parent_slot: Slot, + skip_timer: Instant, + ctx: &LeaderContext, +) -> Result<(), StartLeaderError> { + let my_pubkey = ctx.my_pubkey; + let timeout = block_timeout(leader_slot_index(slot)); + while !timeout.saturating_sub(skip_timer.elapsed()).is_zero() { + match maybe_start_leader(slot, parent_slot, ctx) { + Ok(()) => { + return Ok(()); + } + Err(StartLeaderError::ReplayIsBehind(_)) => { + trace!( + "{my_pubkey}: Attempting to produce slot {slot}, however replay of the \ + the parent {parent_slot} is not yet finished, waiting. Skip timer {}", + skip_timer.elapsed().as_millis() + ); + let highest_frozen_slot = ctx + .replay_highest_frozen + .highest_frozen_slot + .lock() + .unwrap(); + // We wait until either we finish replay of the parent or the skip timer finishes + let _unused = ctx + .replay_highest_frozen + .freeze_notification + .wait_timeout_while( + highest_frozen_slot, + timeout.saturating_sub(skip_timer.elapsed()), + |hfs| *hfs < parent_slot, + ) + .unwrap(); + } + Err(e) => return Err(e), + } + } + + error!( + "{my_pubkey}: Skipping production of {slot}: \ + Unable to replay parent {parent_slot} in time" + ); + Err(StartLeaderError::ReplayIsBehind(parent_slot)) +} + +/// Checks if we are set to produce a leader block for `slot`: +/// - Is the highest notarization/finalized slot from `cert_pool` frozen +/// - Startup verification is complete +/// - Bank forks does not already contain a bank for `slot` +/// +/// If checks pass we return `Ok(())` and: +/// - Reset poh to the `parent_slot` +/// - Create a new bank for `slot` with parent `parent_slot` +/// - Insert into bank_forks and poh recorder +fn maybe_start_leader( + slot: Slot, + parent_slot: Slot, + ctx: &LeaderContext, +) -> Result<(), StartLeaderError> { + if ctx.bank_forks.read().unwrap().get(slot).is_some() { + return Err(StartLeaderError::AlreadyHaveBank(slot)); + } + + let Some(parent_bank) = ctx.bank_forks.read().unwrap().get(parent_slot) else { + return Err(StartLeaderError::ReplayIsBehind(parent_slot)); + }; + + if !parent_bank.is_frozen() { + return Err(StartLeaderError::ReplayIsBehind(parent_slot)); + } + + if !parent_bank.is_startup_verification_complete() { + return Err(StartLeaderError::StartupVerificationIncomplete(parent_slot)); + } + + // TODO(ashwin): plug this in from replay + let has_new_vote_been_rooted = true; + if !has_new_vote_been_rooted { + return Err(StartLeaderError::VoteNotRooted); + } + + // Create and insert the bank + create_and_insert_leader_bank(slot, parent_bank, ctx); + Ok(()) +} + +/// Creates and inserts the leader bank `slot` of this window with +/// parent `parent_bank` +fn create_and_insert_leader_bank(slot: Slot, parent_bank: Arc, ctx: &LeaderContext) { + let parent_slot = parent_bank.slot(); + let root_slot = ctx.bank_forks.read().unwrap().root(); + + if let Some(bank) = ctx.poh_recorder.read().unwrap().bank() { + panic!( + "{}: Attempting to produce a block for {slot}, however we still are in production of \ + {}. Something has gone wrong with the block creation loop. exiting", + ctx.my_pubkey, + bank.slot(), + ); + } + + if ctx.poh_recorder.read().unwrap().start_slot() != parent_slot { + // Important to keep Poh somewhat accurate for + // parts of the system relying on PohRecorder::would_be_leader() + // + // TODO: On migration need to keep the ticks around for parent slots in previous epoch + // because reset below will delete those ticks + reset_poh_recorder(&parent_bank, ctx); + } + + let tpu_bank = ReplayStage::new_bank_from_parent_with_notify( + parent_bank.clone(), + slot, + root_slot, + &ctx.my_pubkey, + &ctx.rpc_subscriptions, + &ctx.slot_status_notifier, + NewBankOptions::default(), + ); + // make sure parent is frozen for finalized hashes via the above + // new()-ing of its child bank + ctx.banking_tracer.hash_event( + parent_slot, + &parent_bank.last_blockhash(), + &parent_bank.hash(), + ); + + // Insert the bank + let tpu_bank = ctx.bank_forks.write().unwrap().insert(tpu_bank); + let poh_bank_start = ctx + .poh_recorder + .write() + .unwrap() + .set_bank(tpu_bank, ctx.track_transaction_indexes); + // TODO: cleanup, this is no longer needed + poh_bank_start + .contains_valid_certificate + .store(true, Ordering::Relaxed); + + info!( + "{}: new fork:{} parent:{} (leader) root:{}", + ctx.my_pubkey, slot, parent_slot, root_slot + ); +} diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index 500e741679..55fb8a570d 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -5,7 +5,7 @@ use { optimistic_confirmation_verifier::OptimisticConfirmationVerifier, replay_stage::DUPLICATE_THRESHOLD, result::{Error, Result}, - sigverify, + sigverifier::ed25519_sigverifier::ed25519_verify_cpu, }, agave_banking_stage_ingress_types::BankingPacketBatch, crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Select, Sender}, @@ -27,7 +27,6 @@ use { bank_forks::BankForks, bank_hash_cache::{BankHashCache, DumpedSlotSubscription}, commitment::VOTE_THRESHOLD_SIZE, - epoch_stakes::EpochStakes, root_bank_cache::RootBankCache, vote_sender_types::ReplayVoteReceiver, }, @@ -40,7 +39,7 @@ use { transaction::Transaction, }, solana_vote::{ - vote_parser::{self, ParsedVote}, + vote_parser::{self, ParsedVote, ParsedVoteTransaction}, vote_transaction::VoteTransaction, }, std::{ @@ -281,12 +280,16 @@ impl ClusterInfoVoteListener { let mut packet_batches = packet::to_packet_batches(&votes, 1); // Votes should already be filtered by this point. - sigverify::ed25519_verify_cpu( + ed25519_verify_cpu( &mut packet_batches, /*reject_non_vote=*/ false, votes.len(), ); let root_bank = root_bank_cache.root_bank(); + let first_alpenglow_slot = root_bank + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()) + .unwrap_or(Slot::MAX); let epoch_schedule = root_bank.epoch_schedule(); votes .into_iter() @@ -299,6 +302,9 @@ impl ClusterInfoVoteListener { .filter_map(|(tx, packet_batch)| { let (vote_account_key, vote, ..) = vote_parser::parse_vote_transaction(&tx)?; let slot = vote.last_voted_slot()?; + if (slot >= first_alpenglow_slot) ^ vote.is_alpenglow_vote() { + return None; + } let epoch = epoch_schedule.get_epoch(slot); let authorized_voter = root_bank .epoch_stakes(epoch)? @@ -499,11 +505,9 @@ impl ClusterInfoVoteListener { // if we don't have stake information, ignore it let epoch = root_bank.epoch_schedule().get_epoch(slot); - let epoch_stakes = root_bank.epoch_stakes(epoch); - if epoch_stakes.is_none() { + let Some(epoch_stakes) = root_bank.epoch_stakes(epoch) else { continue; - } - let epoch_stakes = epoch_stakes.unwrap(); + }; // We always track the last vote slot for optimistic confirmation. If we have replayed // the same version of last vote slot that is being voted on, then we also track the @@ -615,29 +619,45 @@ impl ClusterInfoVoteListener { // Process votes from gossip and ReplayStage let mut gossip_vote_txn_processing_time = Measure::start("gossip_vote_processing_time"); let votes = gossip_vote_txs - .iter() - .filter_map(vote_parser::parse_vote_transaction) - .zip(repeat(/*is_gossip:*/ true)) - .chain(replayed_votes.into_iter().zip(repeat(/*is_gossip:*/ false))); - for ((vote_pubkey, vote, _switch_proof, signature), is_gossip) in votes { - Self::track_new_votes_and_notify_confirmations( - vote, - &vote_pubkey, - signature, - vote_tracker, - root_bank, - subscriptions, - verified_vote_sender, - gossip_verified_vote_hash_sender, - &mut diff, - &mut new_optimistic_confirmed_slots, - is_gossip, - bank_notification_sender, - duplicate_confirmed_slot_sender, - latest_vote_slot_per_validator, - bank_hash_cache, - dumped_slot_subscription, - ); + .into_iter() + .filter_map(|tx| { + let parsed_vote = vote_parser::parse_vote_transaction(&tx)?; + Some((parsed_vote, Some(tx))) + }) + .chain(replayed_votes.into_iter().zip(repeat(/*is_gossip:*/ None))); + for ((vote_pubkey, vote, _switch_proof, signature), transaction) in votes { + match vote { + ParsedVoteTransaction::Alpenglow(_) => { + panic!("Will be removed soon"); + } + ParsedVoteTransaction::Tower(vote) => { + if root_bank + .feature_set + .is_active(&solana_feature_set::secp256k1_program_enabled::id()) + { + continue; + } + let is_gossip_vote = transaction.is_some(); + Self::track_new_votes_and_notify_confirmations( + vote, + &vote_pubkey, + signature, + vote_tracker, + root_bank, + subscriptions, + verified_vote_sender, + gossip_verified_vote_hash_sender, + &mut diff, + &mut new_optimistic_confirmed_slots, + is_gossip_vote, + bank_notification_sender, + duplicate_confirmed_slot_sender, + latest_vote_slot_per_validator, + bank_hash_cache, + dumped_slot_subscription, + ) + } + } } gossip_vote_txn_processing_time.stop(); let gossip_vote_txn_processing_time_us = gossip_vote_txn_processing_time.as_us(); @@ -674,7 +694,12 @@ impl ClusterInfoVoteListener { // in gossip in the past, `is_new` would be false and it would have // been filtered out above), so it's safe to increment the gossip-only // stake - Self::sum_stake(&mut gossip_only_stake, epoch_stakes, &pubkey); + if let Some(epoch_stakes) = epoch_stakes { + gossip_only_stake += epoch_stakes + .stakes() + .vote_accounts() + .get_delegated_stake(&pubkey); + } } // From the `slot_diff.retain` earlier, we know because there are @@ -721,12 +746,6 @@ impl ClusterInfoVoteListener { .get_or_insert_optimistic_votes_tracker(hash) .add_vote_pubkey(pubkey, stake, total_epoch_stake, &THRESHOLDS_TO_CHECK) } - - fn sum_stake(sum: &mut u64, epoch_stakes: Option<&EpochStakes>, pubkey: &Pubkey) { - if let Some(stakes) = epoch_stakes { - *sum += stakes.stakes().vote_accounts().get_delegated_stake(pubkey) - } - } } #[cfg(test)] @@ -749,7 +768,7 @@ mod tests { pubkey::Pubkey, signature::{Keypair, Signature, Signer}, }, - solana_vote::vote_transaction, + solana_vote::vote_transaction::{self, VoteTransaction}, solana_vote_program::vote_state::{TowerSync, Vote, MAX_LOCKOUT_HISTORY}, std::{ collections::BTreeSet, @@ -962,7 +981,7 @@ mod tests { replay_votes_sender .send(( vote_keypair.pubkey(), - VoteTransaction::from(replay_vote.clone()), + ParsedVoteTransaction::Tower(VoteTransaction::from(replay_vote.clone())), switch_proof_hash, Signature::default(), )) @@ -1285,7 +1304,10 @@ mod tests { replay_votes_sender .send(( vote_keypair.pubkey(), - VoteTransaction::from(Vote::new(vec![vote_slot], Hash::default())), + ParsedVoteTransaction::Tower(VoteTransaction::from(Vote::new( + vec![vote_slot], + Hash::default(), + ))), switch_proof_hash, Signature::default(), )) @@ -1334,6 +1356,7 @@ mod tests { run_test_process_votes3(Some(Hash::default())); } + // TODO: Add Alpenglow equivalent tests #[test] fn test_vote_tracker_references() { // Create some voters at genesis @@ -1388,7 +1411,10 @@ mod tests { // Add gossip vote for same slot, should not affect outcome vec![( validator0_keypairs.vote_keypair.pubkey(), - VoteTransaction::from(Vote::new(vec![voted_slot], Hash::default())), + ParsedVoteTransaction::Tower(VoteTransaction::from(Vote::new( + vec![voted_slot], + Hash::default(), + ))), None, Signature::default(), )], @@ -1437,7 +1463,10 @@ mod tests { vote_txs, vec![( validator_keypairs[1].vote_keypair.pubkey(), - VoteTransaction::from(Vote::new(vec![first_slot_in_new_epoch], Hash::default())), + ParsedVoteTransaction::Tower(VoteTransaction::from(Vote::new( + vec![first_slot_in_new_epoch], + Hash::default(), + ))), None, Signature::default(), )], @@ -1585,25 +1614,6 @@ mod tests { verify_packets_len(&packets, 2); } - #[test] - fn test_sum_stake() { - let SetupComponents { - bank, - validator_voting_keypairs, - .. - } = setup(); - let vote_keypair = &validator_voting_keypairs[0].vote_keypair; - let epoch_stakes = bank.epoch_stakes(bank.epoch()).unwrap(); - let mut gossip_only_stake = 0; - - ClusterInfoVoteListener::sum_stake( - &mut gossip_only_stake, - Some(epoch_stakes), - &vote_keypair.pubkey(), - ); - assert_eq!(gossip_only_stake, 100); - } - #[test] fn test_bad_vote() { run_test_bad_vote(None); @@ -1659,7 +1669,7 @@ mod tests { .unwrap(); ClusterInfoVoteListener::track_new_votes_and_notify_confirmations( - vote, + vote.as_tower_transaction().unwrap(), &vote_pubkey, signature, &vote_tracker, @@ -1692,7 +1702,7 @@ mod tests { .unwrap(); ClusterInfoVoteListener::track_new_votes_and_notify_confirmations( - vote, + vote.as_tower_transaction().unwrap(), &vote_pubkey, signature, &vote_tracker, diff --git a/core/src/commitment_service.rs b/core/src/commitment_service.rs index c9fd2dca45..86d37f05c6 100644 --- a/core/src/commitment_service.rs +++ b/core/src/commitment_service.rs @@ -1,6 +1,6 @@ use { crate::consensus::{tower_vote_state::TowerVoteState, Stake}, - crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender}, + crossbeam_channel::{bounded, select, unbounded, Receiver, RecvTimeoutError, Sender}, solana_measure::measure::Measure, solana_metrics::datapoint_info, solana_rpc::rpc_subscriptions::RpcSubscriptions, @@ -9,6 +9,7 @@ use { commitment::{BlockCommitment, BlockCommitmentCache, CommitmentSlots, VOTE_THRESHOLD_SIZE}, }, solana_sdk::{clock::Slot, pubkey::Pubkey}, + solana_votor::commitment::{AlpenglowCommitmentAggregationData, AlpenglowCommitmentType}, std::{ cmp::max, collections::HashMap, @@ -21,7 +22,7 @@ use { }, }; -pub struct CommitmentAggregationData { +pub struct TowerCommitmentAggregationData { bank: Arc, root: Slot, total_stake: Stake, @@ -30,7 +31,7 @@ pub struct CommitmentAggregationData { node_vote_state: (Pubkey, TowerVoteState), } -impl CommitmentAggregationData { +impl TowerCommitmentAggregationData { pub fn new( bank: Arc, root: Slot, @@ -67,13 +68,24 @@ impl AggregateCommitmentService { exit: Arc, block_commitment_cache: Arc>, subscriptions: Arc, - ) -> (Sender, Self) { + ) -> ( + Sender, + Sender, + Self, + ) { let (sender, receiver): ( - Sender, - Receiver, + Sender, + Receiver, ) = unbounded(); + // This channel should not grow unbounded, cap at 1000 messages for now + let (ag_sender, ag_receiver): ( + Sender, + Receiver, + ) = bounded(1000); + ( sender, + ag_sender, Self { t_commitment: Builder::new() .name("solAggCommitSvc".to_string()) @@ -82,9 +94,13 @@ impl AggregateCommitmentService { break; } - if let Err(RecvTimeoutError::Disconnected) = - Self::run(&receiver, &block_commitment_cache, &subscriptions, &exit) - { + if let Err(RecvTimeoutError::Disconnected) = Self::run( + &receiver, + &ag_receiver, + &block_commitment_cache, + &subscriptions, + &exit, + ) { break; } }) @@ -94,7 +110,8 @@ impl AggregateCommitmentService { } fn run( - receiver: &Receiver, + receiver: &Receiver, + ag_receiver: &Receiver, block_commitment_cache: &RwLock, subscriptions: &Arc, exit: &AtomicBool, @@ -104,18 +121,30 @@ impl AggregateCommitmentService { return Ok(()); } - let aggregation_data = receiver.recv_timeout(Duration::from_secs(1))?; - let aggregation_data = receiver.try_iter().last().unwrap_or(aggregation_data); - - let ancestors = aggregation_data.bank.status_cache_ancestors(); - if ancestors.is_empty() { - continue; - } - let mut aggregate_commitment_time = Measure::start("aggregate-commitment-ms"); - let update_commitment_slots = - Self::update_commitment_cache(block_commitment_cache, aggregation_data, ancestors); + let commitment_slots = select! { + recv(receiver) -> msg => { + let data = msg?; + let data = receiver.try_iter().last().unwrap_or(data); + let ancestors = data.bank.status_cache_ancestors(); + if ancestors.is_empty() { + continue; + } + Self::update_commitment_cache(block_commitment_cache, data, ancestors) + } + recv(ag_receiver) -> msg => { + let data = msg?; + let data = ag_receiver.try_iter().last().unwrap_or(data); + Self::alpenglow_update_commitment_cache( + block_commitment_cache, + data.commitment_type, + data.slot, + ) + } + default(Duration::from_secs(1)) => continue + }; aggregate_commitment_time.stop(); + datapoint_info!( "block-commitment-cache", ( @@ -125,26 +154,45 @@ impl AggregateCommitmentService { ), ( "highest-super-majority-root", - update_commitment_slots.highest_super_majority_root as i64, + commitment_slots.highest_super_majority_root as i64, i64 ), ( "highest-confirmed-slot", - update_commitment_slots.highest_confirmed_slot as i64, + commitment_slots.highest_confirmed_slot as i64, i64 ), ); - // Triggers rpc_subscription notifications as soon as new commitment data is available, // sending just the commitment cache slot information that the notifications thread // needs - subscriptions.notify_subscribers(update_commitment_slots); + subscriptions.notify_subscribers(commitment_slots); } } + fn alpenglow_update_commitment_cache( + block_commitment_cache: &RwLock, + update_type: AlpenglowCommitmentType, + slot: Slot, + ) -> CommitmentSlots { + let mut w_block_commitment_cache = block_commitment_cache.write().unwrap(); + + match update_type { + AlpenglowCommitmentType::Notarize => { + w_block_commitment_cache.set_slot(slot); + } + AlpenglowCommitmentType::Finalized => { + w_block_commitment_cache.set_highest_confirmed_slot(slot); + w_block_commitment_cache.set_root(slot); + w_block_commitment_cache.set_highest_super_majority_root(slot); + } + } + w_block_commitment_cache.commitment_slots() + } + fn update_commitment_cache( block_commitment_cache: &RwLock, - aggregation_data: CommitmentAggregationData, + aggregation_data: TowerCommitmentAggregationData, ancestors: Vec, ) -> CommitmentSlots { let (block_commitment, rooted_stake) = Self::aggregate_commitment( @@ -202,8 +250,11 @@ impl AggregateCommitmentService { let vote_state = if pubkey == node_vote_pubkey { // Override old vote_state in bank with latest one for my own vote pubkey node_vote_state.clone() + } else if let Some(vote_state_view) = account.vote_state_view() { + TowerVoteState::from(vote_state_view) } else { - TowerVoteState::from(account.vote_state().clone()) + // Alpenglow doesn't need to aggregate commitment. + continue; }; Self::aggregate_commitment_for_vote_account( &mut commitment, @@ -537,7 +588,7 @@ mod tests { fn test_highest_super_majority_root_advance() { fn get_vote_state(vote_pubkey: Pubkey, bank: &Bank) -> TowerVoteState { let vote_account = bank.get_vote_account(&vote_pubkey).unwrap(); - TowerVoteState::from(vote_account.vote_state().clone()) + TowerVoteState::from(vote_account.vote_state_view().unwrap()) } let block_commitment_cache = RwLock::new(BlockCommitmentCache::new_for_tests()); @@ -612,7 +663,7 @@ mod tests { let ancestors = working_bank.status_cache_ancestors(); let _ = AggregateCommitmentService::update_commitment_cache( &block_commitment_cache, - CommitmentAggregationData { + TowerCommitmentAggregationData { bank: working_bank, root: 0, total_stake: 100, @@ -651,7 +702,7 @@ mod tests { let ancestors = working_bank.status_cache_ancestors(); let _ = AggregateCommitmentService::update_commitment_cache( &block_commitment_cache, - CommitmentAggregationData { + TowerCommitmentAggregationData { bank: working_bank, root: 1, total_stake: 100, @@ -700,7 +751,7 @@ mod tests { let ancestors = working_bank.status_cache_ancestors(); let _ = AggregateCommitmentService::update_commitment_cache( &block_commitment_cache, - CommitmentAggregationData { + TowerCommitmentAggregationData { bank: working_bank, root: 0, total_stake: 100, diff --git a/core/src/consensus.rs b/core/src/consensus.rs index ac4ec65557..79815cc28a 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -406,7 +406,10 @@ impl Tower { continue; } trace!("{} {} with stake {}", vote_account_pubkey, key, voted_stake); - let mut vote_state = TowerVoteState::from(account.vote_state().clone()); + let Some(vote_state_view) = account.vote_state_view() else { + continue; // not relevant to Alpenglow. + }; + let mut vote_state = TowerVoteState::from(vote_state_view); for vote in &vote_state.votes { lockout_intervals .entry(vote.last_locked_out_slot()) @@ -608,8 +611,9 @@ impl Tower { pub fn last_voted_slot_in_bank(bank: &Bank, vote_account_pubkey: &Pubkey) -> Option { let vote_account = bank.get_vote_account(vote_account_pubkey)?; - let vote_state = vote_account.vote_state(); - vote_state.last_voted_slot() + // TODO(wen): make this work for Alpenglow. + let vote_state_view = vote_account.vote_state_view()?; + vote_state_view.last_voted_slot() } pub fn record_bank_vote(&mut self, bank: &Bank) -> Option { @@ -1618,7 +1622,11 @@ impl Tower { bank: &Bank, ) { if let Some(vote_account) = bank.get_vote_account(vote_account_pubkey) { - self.vote_state = TowerVoteState::from(vote_account.vote_state().clone()); + self.vote_state = TowerVoteState::from( + vote_account + .vote_state_view() + .expect("must be TowerBFT account"), + ); self.initialize_root(root); self.initialize_lockouts(|v| v.slot() > root); } else { @@ -1696,6 +1704,7 @@ impl TowerError { pub enum ExternalRootSource { Tower(Slot), HardFork(Slot), + VoteHistory(Slot), } impl ExternalRootSource { @@ -1703,15 +1712,16 @@ impl ExternalRootSource { match self { ExternalRootSource::Tower(slot) => *slot, ExternalRootSource::HardFork(slot) => *slot, + ExternalRootSource::VoteHistory(slot) => *slot, } } } -// Given an untimely crash, tower may have roots that are not reflected in blockstore, +// Given an untimely crash, tower/vote history may have roots that are not reflected in blockstore, // or the reverse of this. // That's because we don't impose any ordering guarantee or any kind of write barriers -// between tower (plain old POSIX fs calls) and blockstore (through RocksDB), when -// `ReplayState::handle_votable_bank()` saves tower before setting blockstore roots. +// between tower/vote history (plain old POSIX fs calls) and blockstore (through RocksDB), when +// replay or voting loop saves tower/vote history before setting blockstore roots. pub fn reconcile_blockstore_roots_with_external_source( external_source: ExternalRootSource, blockstore: &Blockstore, @@ -1986,6 +1996,7 @@ pub mod test { let duplicate_ancestor1 = 44; let duplicate_ancestor2 = 45; vote_simulator + .tbft_structs .heaviest_subtree_fork_choice .mark_fork_invalid_candidate(&( duplicate_ancestor1, @@ -1998,6 +2009,7 @@ pub mod test { .hash(), )); vote_simulator + .tbft_structs .heaviest_subtree_fork_choice .mark_fork_invalid_candidate(&( duplicate_ancestor2, @@ -2018,7 +2030,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchDuplicateRollback(duplicate_ancestor2) ); @@ -2032,6 +2044,7 @@ pub mod test { } for (i, duplicate_ancestor) in confirm_ancestors.into_iter().enumerate() { vote_simulator + .tbft_structs .heaviest_subtree_fork_choice .mark_fork_valid_candidate(&( duplicate_ancestor, @@ -2051,7 +2064,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ); if i == 0 { assert_eq!( @@ -2083,7 +2096,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SameFork ); @@ -2098,7 +2111,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2115,7 +2128,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2132,7 +2145,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2149,7 +2162,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2168,7 +2181,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2185,7 +2198,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -2203,7 +2216,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -2225,7 +2238,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2253,7 +2266,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, num_validators * 10000) ); @@ -2269,7 +2282,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2302,7 +2315,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -2322,7 +2335,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -2446,8 +2459,8 @@ pub mod test { .unwrap() .get_vote_account(&vote_pubkey) .unwrap(); - let state = observed.vote_state(); - info!("observed tower: {:#?}", state.votes); + let state = observed.vote_state_view().unwrap(); + info!("observed tower: {:#?}", state.votes_iter().collect_vec()); let num_slots_to_try = 200; cluster_votes @@ -3042,7 +3055,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SameFork ); @@ -3057,7 +3070,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -3073,7 +3086,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -3133,7 +3146,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -3149,7 +3162,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20000) ); @@ -3165,7 +3178,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -3770,7 +3783,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20_000) ); @@ -3788,7 +3801,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); @@ -3826,7 +3839,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::FailedSwitchThreshold(0, 20_000) ); @@ -3846,7 +3859,7 @@ pub mod test { total_stake, bank0.epoch_vote_accounts(0).unwrap(), &vote_simulator.latest_validator_votes_for_frozen_banks, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, ), SwitchForkDecision::SwitchProof(Hash::default()) ); diff --git a/core/src/consensus/tower_vote_state.rs b/core/src/consensus/tower_vote_state.rs index d21c6ac862..fd0796be53 100644 --- a/core/src/consensus/tower_vote_state.rs +++ b/core/src/consensus/tower_vote_state.rs @@ -1,5 +1,6 @@ use { solana_sdk::clock::Slot, + solana_vote::vote_state_view::VoteStateView, solana_vote_program::vote_state::{Lockout, VoteState, VoteState1_14_11, MAX_LOCKOUT_HISTORY}, std::collections::VecDeque, }; @@ -105,6 +106,15 @@ impl From for TowerVoteState { } } +impl From<&VoteStateView> for TowerVoteState { + fn from(vote_state: &VoteStateView) -> Self { + Self { + votes: vote_state.votes_iter().collect(), + root_slot: vote_state.root_slot(), + } + } +} + impl From for VoteState1_14_11 { fn from(vote_state: TowerVoteState) -> Self { let TowerVoteState { votes, root_slot } = vote_state; diff --git a/core/src/fetch_stage.rs b/core/src/fetch_stage.rs index 025451d337..17b679ffe3 100644 --- a/core/src/fetch_stage.rs +++ b/core/src/fetch_stage.rs @@ -1,8 +1,11 @@ //! The `fetch_stage` batches input from a UDP socket and sends it to a channel. use { - crate::result::{Error, Result}, - crossbeam_channel::{unbounded, RecvTimeoutError}, + crate::{ + result::{Error, Result}, + tpu::MAX_ALPENGLOW_PACKET_NUM, + }, + crossbeam_channel::{bounded, unbounded, RecvTimeoutError}, solana_metrics::{inc_new_counter_debug, inc_new_counter_info}, solana_perf::{packet::PacketBatchRecycler, recycler::Recycler}, solana_poh::poh_recorder::PohRecorder, @@ -34,21 +37,30 @@ impl FetchStage { sockets: Vec, tpu_forwards_sockets: Vec, tpu_vote_sockets: Vec, + alpenglow_socket: UdpSocket, exit: Arc, poh_recorder: &Arc>, coalesce: Option, - ) -> (Self, PacketBatchReceiver, PacketBatchReceiver) { + ) -> ( + Self, + PacketBatchReceiver, + PacketBatchReceiver, + PacketBatchReceiver, + ) { let (sender, receiver) = unbounded(); let (vote_sender, vote_receiver) = unbounded(); + let (alpenglow_sender, alpenglow_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let (forward_sender, forward_receiver) = unbounded(); ( Self::new_with_sender( sockets, tpu_forwards_sockets, tpu_vote_sockets, + alpenglow_socket, exit, &sender, &vote_sender, + &alpenglow_sender, &forward_sender, forward_receiver, poh_recorder, @@ -58,6 +70,7 @@ impl FetchStage { ), receiver, vote_receiver, + alpenglow_receiver, ) } @@ -66,9 +79,11 @@ impl FetchStage { sockets: Vec, tpu_forwards_sockets: Vec, tpu_vote_sockets: Vec, + alpenglow_socket: UdpSocket, exit: Arc, sender: &PacketBatchSender, vote_sender: &PacketBatchSender, + bls_packet_sender: &PacketBatchSender, forward_sender: &PacketBatchSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, @@ -83,9 +98,11 @@ impl FetchStage { tx_sockets, tpu_forwards_sockets, tpu_vote_sockets, + alpenglow_socket, exit, sender, vote_sender, + bls_packet_sender, forward_sender, forward_receiver, poh_recorder, @@ -142,9 +159,11 @@ impl FetchStage { tpu_sockets: Vec>, tpu_forwards_sockets: Vec>, tpu_vote_sockets: Vec>, + alpenglow_socket: UdpSocket, exit: Arc, sender: &PacketBatchSender, vote_sender: &PacketBatchSender, + bls_packet_sender: &PacketBatchSender, forward_sender: &PacketBatchSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, @@ -223,6 +242,20 @@ impl FetchStage { }) .collect(); + let bls_message_stats = Arc::new(StreamerReceiveStats::new("bls_message_receiver")); + let bls_message_threads: Vec<_> = vec![streamer::receiver( + "solRcvrAlpMsg".to_string(), + Arc::new(alpenglow_socket), + exit.clone(), + bls_packet_sender.clone(), + recycler.clone(), + bls_message_stats.clone(), + coalesce, + true, + None, + true, // only staked connections can send BLS messages + )]; + let sender = sender.clone(); let poh_recorder = poh_recorder.clone(); @@ -251,6 +284,7 @@ impl FetchStage { tpu_stats.report(); tpu_vote_stats.report(); tpu_forward_stats.report(); + bls_message_stats.report(); if exit.load(Ordering::Relaxed) { return; @@ -263,6 +297,7 @@ impl FetchStage { tpu_threads, tpu_forwards_threads, tpu_vote_threads, + bls_message_threads, vec![fwd_thread_hdl, metrics_thread_hdl], ] .into_iter() diff --git a/core/src/lib.rs b/core/src/lib.rs index 9ce541f186..ae3b54e635 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -13,6 +13,7 @@ pub mod admin_rpc_post_init; pub mod banking_simulation; pub mod banking_stage; pub mod banking_trace; +mod block_creation_loop; pub mod cluster_info_vote_listener; pub mod cluster_slots_service; pub mod commitment_service; @@ -32,10 +33,11 @@ pub mod replay_stage; mod result; pub mod sample_performance_service; mod shred_fetch_stage; -pub mod sigverify; +pub mod sigverifier; pub mod sigverify_stage; pub mod snapshot_packager_service; pub mod staked_nodes_updater_service; +pub mod staked_validators_cache; pub mod stats_reporter_service; pub mod system_monitor_service; pub mod tpu; diff --git a/core/src/repair/ancestor_hashes_service.rs b/core/src/repair/ancestor_hashes_service.rs index 4d1549b21a..cb9f4591c4 100644 --- a/core/src/repair/ancestor_hashes_service.rs +++ b/core/src/repair/ancestor_hashes_service.rs @@ -11,6 +11,7 @@ use { serve_repair::{ self, AncestorHashesRepairType, AncestorHashesResponse, RepairProtocol, ServeRepair, }, + standard_repair_handler::StandardRepairHandler, }, replay_stage::DUPLICATE_THRESHOLD, shred_fetch_stage::receive_quic_datagrams, @@ -206,7 +207,7 @@ impl AncestorHashesService { let t_ancestor_hashes_responses = Self::run_responses_listener( ancestor_hashes_request_statuses.clone(), response_receiver, - blockstore, + blockstore.clone(), outstanding_requests.clone(), exit.clone(), repair_info.ancestor_duplicate_slots_sender.clone(), @@ -217,6 +218,7 @@ impl AncestorHashesService { // Generate ancestor requests for dead slots that are repairable let t_ancestor_requests = Self::run_manage_ancestor_requests( + blockstore, ancestor_hashes_request_statuses, ancestor_hashes_request_socket, ancestor_hashes_request_quic_sender, @@ -590,6 +592,7 @@ impl AncestorHashesService { } fn run_manage_ancestor_requests( + blockstore: Arc, ancestor_hashes_request_statuses: Arc>, ancestor_hashes_request_socket: Arc, ancestor_hashes_request_quic_sender: AsyncSender<(SocketAddr, Bytes)>, @@ -599,11 +602,14 @@ impl AncestorHashesService { ancestor_hashes_replay_update_receiver: AncestorHashesReplayUpdateReceiver, retryable_slots_receiver: RetryableSlotsReceiver, ) -> JoinHandle<()> { - let serve_repair = ServeRepair::new( - repair_info.cluster_info.clone(), - repair_info.bank_forks.clone(), - repair_info.repair_whitelist.clone(), - ); + let serve_repair = { + ServeRepair::new( + repair_info.cluster_info.clone(), + repair_info.bank_forks.clone(), + repair_info.repair_whitelist.clone(), + Box::new(StandardRepairHandler::new(blockstore)), + ) + }; let mut repair_stats = AncestorRepairRequestsStats::default(); let mut dead_slot_pool = HashSet::new(); @@ -629,6 +635,16 @@ impl AncestorHashesService { if exit.load(Ordering::Relaxed) { return; } + if repair_info + .bank_forks + .read() + .unwrap() + .root_bank() + .feature_set + .is_active(&solana_feature_set::secp256k1_program_enabled::id()) + { + return; + } Self::manage_ancestor_requests( &ancestor_hashes_request_statuses, &ancestor_hashes_request_socket, @@ -1262,20 +1278,23 @@ mod test { Arc::new(keypair), SocketAddrSpace::Unspecified, ); - let responder_serve_repair = ServeRepair::new( - Arc::new(cluster_info), - vote_simulator.bank_forks, - Arc::>>::default(), // repair whitelist - ); + // Set up blockstore for responses + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Arc::new(Blockstore::open(&ledger_path).unwrap()); + let responder_serve_repair = { + ServeRepair::new( + Arc::new(cluster_info), + vote_simulator.bank_forks, + Arc::>>::default(), // repair whitelist + Box::new(StandardRepairHandler::new(blockstore.clone())), + ) + }; // Set up thread to give us responses - let ledger_path = get_tmp_ledger_path!(); let exit = Arc::new(AtomicBool::new(false)); let (requests_sender, requests_receiver) = unbounded(); let (response_sender, response_receiver) = unbounded(); - // Set up blockstore for responses - let blockstore = Arc::new(Blockstore::open(&ledger_path).unwrap()); // Create slots [slot - MAX_ANCESTOR_RESPONSES, slot) with 5 shreds apiece let (shreds, _) = make_many_slot_entries( slot_to_query - MAX_ANCESTOR_RESPONSES as Slot + 1, @@ -1313,7 +1332,6 @@ mod test { .unwrap(); let (repair_response_quic_sender, _) = tokio::sync::mpsc::channel(/*buffer:*/ 128); let t_listen = responder_serve_repair.listen( - blockstore, remote_request_receiver, response_sender, repair_response_quic_sender, @@ -1366,11 +1384,16 @@ mod test { SocketAddrSpace::Unspecified, )); let repair_whitelist = Arc::new(RwLock::new(HashSet::default())); - let requester_serve_repair = ServeRepair::new( - requester_cluster_info.clone(), - bank_forks.clone(), - repair_whitelist.clone(), - ); + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Arc::new(Blockstore::open(&ledger_path).unwrap()); + let requester_serve_repair = { + ServeRepair::new( + requester_cluster_info.clone(), + bank_forks.clone(), + repair_whitelist.clone(), + Box::new(StandardRepairHandler::new(blockstore)), + ) + }; let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = unbounded(); let repair_info = RepairInfo { bank_forks, diff --git a/core/src/repair/certificate_service.rs b/core/src/repair/certificate_service.rs new file mode 100644 index 0000000000..bd5143a458 --- /dev/null +++ b/core/src/repair/certificate_service.rs @@ -0,0 +1,124 @@ +//! The `certificate_service` handles critical certificate related activites: +//! - Storage of critical certificates in blockstore +//! - TODO: Broadcast of new critical certificates to the cluster +//! - TODO: Repair of missing critical certificates to enable progress + +use { + crate::result::{Error, Result}, + crossbeam_channel::{Receiver, RecvTimeoutError}, + solana_ledger::blockstore::Blockstore, + solana_votor_messages::bls_message::{Certificate, CertificateMessage}, + std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::{self, Builder, JoinHandle}, + time::Duration, + }, +}; + +pub(crate) type CertificateReceiver = Receiver<(Certificate, CertificateMessage)>; +pub struct CertificateService { + t_cert_insert: JoinHandle<()>, +} + +impl CertificateService { + pub(crate) fn new( + exit: Arc, + blockstore: Arc, + certificate_receiver: CertificateReceiver, + ) -> Self { + let t_cert_insert = + Self::start_certificate_insert_broadcast(exit, blockstore, certificate_receiver); + Self { t_cert_insert } + } + + fn start_certificate_insert_broadcast( + exit: Arc, + blockstore: Arc, + certificate_receiver: CertificateReceiver, + ) -> JoinHandle<()> { + let handle_error = || { + inc_new_counter_error!("solana-certificate-service-error", 1, 1); + }; + + Builder::new() + .name("solCertInsertBCast".to_string()) + .spawn(move || { + while !exit.load(Ordering::Relaxed) { + let certs = match Self::receive_new_certificates(&certificate_receiver) { + Ok(certs) => certs, + Err(e) if Self::should_exit_on_error(&e, &handle_error) => break, + Err(_e) => continue, + }; + + // TODO: broadcast to gossip / all-2-all + + // TODO: update highest cert local-state and potentially ask for repair for missing certificates + // e,g, our previous highest cert was 5, we now see certs for 7 & 8, notify repair to get the cert for 6 + + // Insert into blockstore + if let Err(e) = certs.into_iter().try_for_each(|(cert_id, cert)| { + Self::insert_certificate(blockstore.as_ref(), cert_id, cert) + }) { + if Self::should_exit_on_error(&e, &handle_error) { + break; + } + } + } + }) + .unwrap() + } + + fn receive_new_certificates( + certificate_receiver: &Receiver<(Certificate, CertificateMessage)>, + ) -> Result> { + const RECV_TIMEOUT: Duration = Duration::from_millis(200); + Ok( + std::iter::once(certificate_receiver.recv_timeout(RECV_TIMEOUT)?) + .chain(certificate_receiver.try_iter()) + .collect(), + ) + } + + fn insert_certificate( + blockstore: &Blockstore, + cert_id: Certificate, + vote_certificate: CertificateMessage, + ) -> Result<()> { + match cert_id { + Certificate::NotarizeFallback(slot, block_id) => blockstore + .insert_new_notarization_fallback_certificate(slot, block_id, vote_certificate)?, + Certificate::Skip(slot) => { + blockstore.insert_new_skip_certificate(slot, vote_certificate)? + } + Certificate::Finalize(_) + | Certificate::FinalizeFast(_, _) + | Certificate::Notarize(_, _) => { + panic!("Programmer error, certificate pool should not notify for {cert_id:?}") + } + } + Ok(()) + } + + fn should_exit_on_error(e: &Error, handle_error: &H) -> bool + where + H: Fn(), + { + match e { + Error::RecvTimeout(RecvTimeoutError::Disconnected) => true, + Error::RecvTimeout(RecvTimeoutError::Timeout) => false, + Error::Send => true, + _ => { + handle_error(); + error!("thread {:?} error {:?}", thread::current().name(), e); + false + } + } + } + + pub(crate) fn join(self) -> thread::Result<()> { + self.t_cert_insert.join() + } +} diff --git a/core/src/repair/cluster_slot_state_verifier.rs b/core/src/repair/cluster_slot_state_verifier.rs index 7b1e9d181b..b60e4f4b52 100644 --- a/core/src/repair/cluster_slot_state_verifier.rs +++ b/core/src/repair/cluster_slot_state_verifier.rs @@ -1582,7 +1582,7 @@ mod test { let (vote_simulator, blockstore) = setup_forks_from_tree(forks, 1, None); let descendants = vote_simulator.bank_forks.read().unwrap().descendants(); InitialState { - heaviest_subtree_fork_choice: vote_simulator.heaviest_subtree_fork_choice, + heaviest_subtree_fork_choice: vote_simulator.tbft_structs.heaviest_subtree_fork_choice, progress: vote_simulator.progress, descendants, bank_forks: vote_simulator.bank_forks, diff --git a/core/src/repair/malicious_repair_handler.rs b/core/src/repair/malicious_repair_handler.rs new file mode 100644 index 0000000000..783a2e7ba5 --- /dev/null +++ b/core/src/repair/malicious_repair_handler.rs @@ -0,0 +1,68 @@ +use { + super::{repair_handler::RepairHandler, repair_response::repair_response_packet_from_bytes}, + solana_ledger::{ + blockstore::Blockstore, + shred::{Nonce, SIZE_OF_DATA_SHRED_HEADERS}, + }, + solana_perf::packet::{Packet, PacketBatch, PacketBatchRecycler}, + solana_sdk::clock::Slot, + std::{net::SocketAddr, sync::Arc}, +}; + +#[derive(Copy, Clone, Debug, Default)] +pub struct MaliciousRepairConfig { + bad_shred_slot_frequency: Option, +} + +pub struct MaliciousRepairHandler { + blockstore: Arc, + config: MaliciousRepairConfig, +} + +impl MaliciousRepairHandler { + const BAD_DATA_INDEX: usize = SIZE_OF_DATA_SHRED_HEADERS + 5; + + pub fn new(blockstore: Arc, config: MaliciousRepairConfig) -> Self { + Self { blockstore, config } + } +} + +impl RepairHandler for MaliciousRepairHandler { + fn blockstore(&self) -> &Blockstore { + &self.blockstore + } + + fn repair_response_packet( + &self, + slot: Slot, + shred_index: u64, + dest: &SocketAddr, + nonce: Nonce, + ) -> Option { + let mut shred = self + .blockstore + .get_data_shred(slot, shred_index) + .expect("Blockstore could not get data shred")?; + if self + .config + .bad_shred_slot_frequency + .is_some_and(|freq| slot % freq == 0) + { + // Change some random piece of data + shred[Self::BAD_DATA_INDEX] = shred[Self::BAD_DATA_INDEX].wrapping_add(1); + } + repair_response_packet_from_bytes(shred, dest, nonce) + } + + fn run_orphan( + &self, + _recycler: &PacketBatchRecycler, + _from_addr: &SocketAddr, + _slot: Slot, + _max_responses: usize, + _nonce: Nonce, + ) -> Option { + // Don't respond to orphan repair + None + } +} diff --git a/core/src/repair/mod.rs b/core/src/repair/mod.rs index 8514527480..606015d778 100644 --- a/core/src/repair/mod.rs +++ b/core/src/repair/mod.rs @@ -1,10 +1,13 @@ pub mod ancestor_hashes_service; +pub mod certificate_service; pub mod cluster_slot_state_verifier; pub mod duplicate_repair_status; +pub(crate) mod malicious_repair_handler; pub mod outstanding_requests; pub mod packet_threshold; pub(crate) mod quic_endpoint; pub mod repair_generic_traversal; +pub mod repair_handler; pub mod repair_response; pub mod repair_service; pub mod repair_weight; @@ -13,3 +16,4 @@ pub mod request_response; pub mod result; pub mod serve_repair; pub mod serve_repair_service; +pub(crate) mod standard_repair_handler; diff --git a/core/src/repair/repair_handler.rs b/core/src/repair/repair_handler.rs new file mode 100644 index 0000000000..20a93857e7 --- /dev/null +++ b/core/src/repair/repair_handler.rs @@ -0,0 +1,154 @@ +use { + super::{ + malicious_repair_handler::{MaliciousRepairConfig, MaliciousRepairHandler}, + serve_repair::ServeRepair, + standard_repair_handler::StandardRepairHandler, + }, + crate::repair::{ + repair_response, + serve_repair::{AncestorHashesResponse, MAX_ANCESTOR_RESPONSES}, + }, + bincode::serialize, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::{ + ancestor_iterator::{AncestorIterator, AncestorIteratorWithHash}, + blockstore::Blockstore, + shred::Nonce, + }, + solana_perf::packet::{Packet, PacketBatch, PacketBatchRecycler}, + solana_pubkey::Pubkey, + solana_runtime::bank_forks::BankForks, + solana_sdk::clock::Slot, + std::{ + collections::HashSet, + net::SocketAddr, + sync::{Arc, RwLock}, + }, +}; + +pub trait RepairHandler { + fn blockstore(&self) -> &Blockstore; + + fn repair_response_packet( + &self, + slot: Slot, + shred_index: u64, + dest: &SocketAddr, + nonce: Nonce, + ) -> Option; + + fn run_window_request( + &self, + recycler: &PacketBatchRecycler, + from_addr: &SocketAddr, + slot: Slot, + shred_index: u64, + nonce: Nonce, + ) -> Option { + // Try to find the requested index in one of the slots + let packet = self.repair_response_packet(slot, shred_index, from_addr, nonce)?; + Some(PacketBatch::new_unpinned_with_recycler_data( + recycler, + "run_window_request", + vec![packet], + )) + } + + fn run_highest_window_request( + &self, + recycler: &PacketBatchRecycler, + from_addr: &SocketAddr, + slot: Slot, + highest_index: u64, + nonce: Nonce, + ) -> Option { + // Try to find the requested index in one of the slots + let meta = self.blockstore().meta(slot).ok()??; + if meta.received > highest_index { + // meta.received must be at least 1 by this point + let packet = self.repair_response_packet(slot, meta.received - 1, from_addr, nonce)?; + return Some(PacketBatch::new_unpinned_with_recycler_data( + recycler, + "run_highest_window_request", + vec![packet], + )); + } + None + } + + fn run_orphan( + &self, + recycler: &PacketBatchRecycler, + from_addr: &SocketAddr, + slot: Slot, + max_responses: usize, + nonce: Nonce, + ) -> Option; + + fn run_ancestor_hashes( + &self, + recycler: &PacketBatchRecycler, + from_addr: &SocketAddr, + slot: Slot, + nonce: Nonce, + ) -> Option { + let ancestor_slot_hashes = if self.blockstore().is_duplicate_confirmed(slot) { + let ancestor_iterator = AncestorIteratorWithHash::from( + AncestorIterator::new_inclusive(slot, self.blockstore()), + ); + ancestor_iterator.take(MAX_ANCESTOR_RESPONSES).collect() + } else { + // If this slot is not duplicate confirmed, return nothing + vec![] + }; + let response = AncestorHashesResponse::Hashes(ancestor_slot_hashes); + let serialized_response = serialize(&response).ok()?; + + // Could probably directly write response into packet via `serialize_into()` + // instead of incurring extra copy in `repair_response_packet_from_bytes`, but + // serialize_into doesn't return the written size... + let packet = repair_response::repair_response_packet_from_bytes( + serialized_response, + from_addr, + nonce, + )?; + Some(PacketBatch::new_unpinned_with_recycler_data( + recycler, + "run_ancestor_hashes", + vec![packet], + )) + } +} + +#[derive(Clone, Debug, Default)] +pub enum RepairHandlerType { + #[default] + Standard, + Malicious(MaliciousRepairConfig), +} + +impl RepairHandlerType { + pub fn to_handler(&self, blockstore: Arc) -> Box { + match self { + RepairHandlerType::Standard => Box::new(StandardRepairHandler::new(blockstore)), + RepairHandlerType::Malicious(config) => { + Box::new(MaliciousRepairHandler::new(blockstore, *config)) + } + } + } + + pub fn create_serve_repair( + &self, + blockstore: Arc, + cluster_info: Arc, + bank_forks: Arc>, + serve_repair_whitelist: Arc>>, + ) -> ServeRepair { + ServeRepair::new( + cluster_info, + bank_forks, + serve_repair_whitelist, + self.to_handler(blockstore), + ) + } +} diff --git a/core/src/repair/repair_service.rs b/core/src/repair/repair_service.rs index 5c12f28bbe..02f2907158 100644 --- a/core/src/repair/repair_service.rs +++ b/core/src/repair/repair_service.rs @@ -1,11 +1,7 @@ //! The `repair_service` module implements the tools necessary to generate a thread which //! regularly finds missing shreds in the ledger and sends repair requests for those shreds -#[cfg(test)] -use { - crate::repair::duplicate_repair_status::DuplicateSlotRepairStatus, - solana_sdk::{clock::DEFAULT_MS_PER_SLOT, signer::keypair::Keypair}, -}; use { + super::standard_repair_handler::StandardRepairHandler, crate::{ cluster_info_vote_listener::VerifiedVoteReceiver, cluster_slots_service::cluster_slots::ClusterSlots, @@ -55,6 +51,11 @@ use { }, tokio::sync::mpsc::Sender as AsyncSender, }; +#[cfg(test)] +use { + crate::repair::duplicate_repair_status::DuplicateSlotRepairStatus, + solana_sdk::clock::DEFAULT_MS_PER_SLOT, solana_sdk::signature::Keypair, +}; // Time to defer repair requests to allow for turbine propagation const DEFER_REPAIR_THRESHOLD: Duration = Duration::from_millis(250); @@ -451,7 +452,7 @@ impl RepairService { .name("solRepairSvc".to_string()) .spawn(move || { Self::run( - &blockstore, + blockstore, &exit, &repair_socket, repair_service_channels.repair_channels, @@ -721,13 +722,18 @@ impl RepairService { repair_metrics, ); - Self::handle_popular_pruned_forks( - root_bank.clone(), - repair_weight, - popular_pruned_forks_requests, - popular_pruned_forks_sender, - repair_metrics, - ); + if !root_bank + .feature_set + .is_active(&solana_feature_set::secp256k1_program_enabled::id()) + { + Self::handle_popular_pruned_forks( + root_bank.clone(), + repair_weight, + popular_pruned_forks_requests, + popular_pruned_forks_sender, + repair_metrics, + ); + } Self::build_and_send_repair_batch( serve_repair, @@ -743,7 +749,7 @@ impl RepairService { } fn run( - blockstore: &Blockstore, + blockstore: Arc, exit: &AtomicBool, repair_socket: &UdpSocket, repair_channels: RepairChannels, @@ -755,11 +761,14 @@ impl RepairService { let mut repair_tracker = RepairTracker { root_bank_cache, repair_weight: RepairWeight::new(root_bank_slot), - serve_repair: ServeRepair::new( - repair_info.cluster_info.clone(), - repair_info.bank_forks.clone(), - repair_info.repair_whitelist.clone(), - ), + serve_repair: { + ServeRepair::new( + repair_info.cluster_info.clone(), + repair_info.bank_forks.clone(), + repair_info.repair_whitelist.clone(), + Box::new(StandardRepairHandler::new(blockstore.clone())), + ) + }, repair_metrics: RepairMetrics::default(), peers_cache: LruCache::new(REPAIR_PEERS_CACHE_CAPACITY), popular_pruned_forks_requests: HashSet::new(), @@ -768,7 +777,7 @@ impl RepairService { while !exit.load(Ordering::Relaxed) { Self::run_repair_iteration( - blockstore, + blockstore.as_ref(), &repair_channels, &repair_info, &mut repair_tracker, @@ -1641,15 +1650,18 @@ mod test { let bank = Bank::new_for_tests(&genesis_config); let bank_forks = BankForks::new_rw_arc(bank); let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); let cluster_slots = ClusterSlots::default(); let cluster_info = Arc::new(new_test_cluster_info()); let identity_keypair = cluster_info.keypair().clone(); - let serve_repair = ServeRepair::new( - cluster_info, - bank_forks, - Arc::new(RwLock::new(HashSet::default())), - ); + let serve_repair = { + ServeRepair::new( + cluster_info, + bank_forks, + Arc::new(RwLock::new(HashSet::default())), + Box::new(StandardRepairHandler::new(blockstore.clone())), + ) + }; let mut duplicate_slot_repair_statuses = HashMap::new(); let dead_slot = 9; let receive_socket = &bind_to_unspecified().unwrap(); @@ -1744,11 +1756,16 @@ mod test { bind_to_unspecified().unwrap().local_addr().unwrap(), )); let cluster_info = Arc::new(new_test_cluster_info()); - let serve_repair = ServeRepair::new( - cluster_info.clone(), - bank_forks, - Arc::new(RwLock::new(HashSet::default())), - ); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); + let serve_repair = { + ServeRepair::new( + cluster_info.clone(), + bank_forks, + Arc::new(RwLock::new(HashSet::default())), + Box::new(StandardRepairHandler::new(blockstore)), + ) + }; let valid_repair_peer = Node::new_localhost().info; // Signal that this peer has confirmed the dead slot, and is thus diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index e10f93845c..ed0263645c 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -1,10 +1,15 @@ +#[cfg(test)] +use { + crate::repair::standard_repair_handler::StandardRepairHandler, + solana_ledger::{blockstore::Blockstore, get_tmp_ledger_path_auto_delete}, +}; use { crate::{ cluster_slots_service::cluster_slots::ClusterSlots, repair::{ duplicate_repair_status::get_ancestor_hash_repair_sample_size, quic_endpoint::RemoteRequest, - repair_response, + repair_handler::RepairHandler, repair_service::{OutstandingShredRepairs, RepairStats, REPAIR_MS}, request_response::RequestResponse, result::{Error, RepairVerifyError, Result}, @@ -24,11 +29,7 @@ use { ping_pong::{self, Pong}, weighted_shuffle::WeightedShuffle, }, - solana_ledger::{ - ancestor_iterator::{AncestorIterator, AncestorIteratorWithHash}, - blockstore::Blockstore, - shred::{self, Nonce, ShredFetchStats, SIZE_OF_NONCE}, - }, + solana_ledger::shred::{self, Nonce, ShredFetchStats, SIZE_OF_NONCE}, solana_perf::{ data_budget::DataBudget, packet::{Packet, PacketBatch, PacketBatchRecycler}, @@ -339,6 +340,7 @@ pub struct ServeRepair { cluster_info: Arc, root_bank_cache: RootBankCache, repair_whitelist: Arc>>, + repair_handler: Box, } // Cache entry for repair peers for a slot. @@ -401,22 +403,36 @@ impl ServeRepair { cluster_info: Arc, bank_forks: Arc>, repair_whitelist: Arc>>, + repair_handler: Box, ) -> Self { Self { cluster_info, root_bank_cache: RootBankCache::new(bank_forks), repair_whitelist, + repair_handler, } } + #[cfg(test)] + pub fn new_for_test( + cluster_info: Arc, + bank_forks: Arc>, + repair_whitelist: Arc>>, + ) -> Self { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); + let repair_handler = Box::new(StandardRepairHandler::new(blockstore)); + Self::new(cluster_info, bank_forks, repair_whitelist, repair_handler) + } + pub(crate) fn my_id(&self) -> Pubkey { self.cluster_info.id() } fn handle_repair( + &self, recycler: &PacketBatchRecycler, from_addr: &SocketAddr, - blockstore: &Blockstore, request: RepairProtocol, stats: &mut ServeRepairStats, ping_cache: &mut PingCache, @@ -430,10 +446,9 @@ impl ServeRepair { shred_index, } => { stats.window_index += 1; - let batch = Self::run_window_request( + let batch = self.repair_handler.run_window_request( recycler, from_addr, - blockstore, *slot, *shred_index, *nonce, @@ -450,10 +465,9 @@ impl ServeRepair { } => { stats.highest_window_index += 1; ( - Self::run_highest_window_request( + self.repair_handler.run_highest_window_request( recycler, from_addr, - blockstore, *slot, *highest_index, *nonce, @@ -467,10 +481,9 @@ impl ServeRepair { } => { stats.orphan += 1; ( - Self::run_orphan( + self.repair_handler.run_orphan( recycler, from_addr, - blockstore, *slot, MAX_ORPHAN_REPAIR_RESPONSES, *nonce, @@ -484,7 +497,8 @@ impl ServeRepair { } => { stats.ancestor_hashes += 1; ( - Self::run_ancestor_hashes(recycler, from_addr, blockstore, *slot, *nonce), + self.repair_handler + .run_ancestor_hashes(recycler, from_addr, *slot, *nonce), "AncestorHashes", ) } @@ -631,7 +645,6 @@ impl ServeRepair { &mut self, ping_cache: &mut PingCache, recycler: &PacketBatchRecycler, - blockstore: &Blockstore, requests_receiver: &Receiver, response_sender: &PacketBatchSender, repair_response_quic_sender: &AsyncSender<(SocketAddr, Bytes)>, @@ -706,7 +719,6 @@ impl ServeRepair { self.handle_requests( ping_cache, recycler, - blockstore, decoded_requests, response_sender, repair_response_quic_sender, @@ -808,7 +820,6 @@ impl ServeRepair { pub(crate) fn listen( mut self, - blockstore: Arc, requests_receiver: Receiver, response_sender: PacketBatchSender, repair_response_quic_sender: AsyncSender<(SocketAddr, Bytes)>, @@ -840,7 +851,6 @@ impl ServeRepair { let result = self.run_listen( &mut ping_cache, &recycler, - &blockstore, &requests_receiver, &response_sender, &repair_response_quic_sender, @@ -974,7 +984,6 @@ impl ServeRepair { &self, ping_cache: &mut PingCache, recycler: &PacketBatchRecycler, - blockstore: &Blockstore, requests: Vec, packet_batch_sender: &PacketBatchSender, repair_response_quic_sender: &AsyncSender<(SocketAddr, Bytes)>, @@ -1009,8 +1018,7 @@ impl ServeRepair { } } stats.processed += 1; - let Some(rsp) = - Self::handle_repair(recycler, &from_addr, blockstore, request, stats, ping_cache) + let Some(rsp) = self.handle_repair(recycler, &from_addr, request, stats, ping_cache) else { continue; }; @@ -1291,119 +1299,6 @@ impl ServeRepair { self.cluster_info.repair_peers(slot) } } - - fn run_window_request( - recycler: &PacketBatchRecycler, - from_addr: &SocketAddr, - blockstore: &Blockstore, - slot: Slot, - shred_index: u64, - nonce: Nonce, - ) -> Option { - // Try to find the requested index in one of the slots - let packet = repair_response::repair_response_packet( - blockstore, - slot, - shred_index, - from_addr, - nonce, - )?; - Some(PacketBatch::new_unpinned_with_recycler_data( - recycler, - "run_window_request", - vec![packet], - )) - } - - fn run_highest_window_request( - recycler: &PacketBatchRecycler, - from_addr: &SocketAddr, - blockstore: &Blockstore, - slot: Slot, - highest_index: u64, - nonce: Nonce, - ) -> Option { - // Try to find the requested index in one of the slots - let meta = blockstore.meta(slot).ok()??; - if meta.received > highest_index { - // meta.received must be at least 1 by this point - let packet = repair_response::repair_response_packet( - blockstore, - slot, - meta.received - 1, - from_addr, - nonce, - )?; - return Some(PacketBatch::new_unpinned_with_recycler_data( - recycler, - "run_highest_window_request", - vec![packet], - )); - } - None - } - - fn run_orphan( - recycler: &PacketBatchRecycler, - from_addr: &SocketAddr, - blockstore: &Blockstore, - slot: Slot, - max_responses: usize, - nonce: Nonce, - ) -> Option { - let mut res = - PacketBatch::new_unpinned_with_recycler(recycler, max_responses, "run_orphan"); - // Try to find the next "n" parent slots of the input slot - let packets = std::iter::successors(blockstore.meta(slot).ok()?, |meta| { - blockstore.meta(meta.parent_slot?).ok()? - }) - .map_while(|meta| { - repair_response::repair_response_packet( - blockstore, - meta.slot, - meta.received.checked_sub(1u64)?, - from_addr, - nonce, - ) - }); - for packet in packets.take(max_responses) { - res.push(packet); - } - (!res.is_empty()).then_some(res) - } - - fn run_ancestor_hashes( - recycler: &PacketBatchRecycler, - from_addr: &SocketAddr, - blockstore: &Blockstore, - slot: Slot, - nonce: Nonce, - ) -> Option { - let ancestor_slot_hashes = if blockstore.is_duplicate_confirmed(slot) { - let ancestor_iterator = - AncestorIteratorWithHash::from(AncestorIterator::new_inclusive(slot, blockstore)); - ancestor_iterator.take(MAX_ANCESTOR_RESPONSES).collect() - } else { - // If this slot is not duplicate confirmed, return nothing - vec![] - }; - let response = AncestorHashesResponse::Hashes(ancestor_slot_hashes); - let serialized_response = serialize(&response).ok()?; - - // Could probably directly write response into packet via `serialize_into()` - // instead of incurring extra copy in `repair_response_packet_from_bytes`, but - // serialize_into doesn't return the written size... - let packet = repair_response::repair_response_packet_from_bytes( - serialized_response, - from_addr, - nonce, - )?; - Some(PacketBatch::new_unpinned_with_recycler_data( - recycler, - "run_ancestor_hashes", - vec![packet], - )) - } } #[inline] @@ -1451,7 +1346,7 @@ mod tests { solana_feature_set::FeatureSet, solana_gossip::{contact_info::ContactInfo, socketaddr, socketaddr_any}, solana_ledger::{ - blockstore::make_many_slot_entries, + blockstore::{make_many_slot_entries, Blockstore}, blockstore_processor::fill_blockstore_slot_with_ticks, genesis_utils::{create_genesis_config, GenesisConfigInfo}, get_tmp_ledger_path_auto_delete, @@ -1593,7 +1488,7 @@ mod tests { let bank = Bank::new_for_tests(&genesis_config); let bank_forks = BankForks::new_rw_arc(bank); let cluster_info = Arc::new(new_test_cluster_info()); - let serve_repair = ServeRepair::new( + let serve_repair = ServeRepair::new_for_test( cluster_info.clone(), bank_forks, Arc::new(RwLock::new(HashSet::default())), @@ -1642,7 +1537,7 @@ mod tests { let mut bank = Bank::new_for_tests(&genesis_config); bank.feature_set = Arc::new(FeatureSet::all_enabled()); let bank_forks = BankForks::new_rw_arc(bank); - let serve_repair = ServeRepair::new( + let serve_repair = ServeRepair::new_for_test( cluster_info, bank_forks, Arc::new(RwLock::new(HashSet::default())), @@ -1679,7 +1574,7 @@ mod tests { let bank = Bank::new_for_tests(&genesis_config); let bank_forks = BankForks::new_rw_arc(bank); let cluster_info = Arc::new(new_test_cluster_info()); - let serve_repair = ServeRepair::new( + let serve_repair = ServeRepair::new_for_test( cluster_info.clone(), bank_forks, Arc::new(RwLock::new(HashSet::default())), @@ -1880,19 +1775,13 @@ mod tests { } /// test run_window_request responds with the right shred, and do not overrun - fn run_highest_window_request(slot: Slot, num_slots: u64, nonce: Nonce) { + pub fn run_highest_window_request(slot: Slot, num_slots: u64, nonce: Nonce) { let recycler = PacketBatchRecycler::default(); solana_logger::setup(); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let rv = ServeRepair::run_highest_window_request( - &recycler, - &socketaddr_any!(), - &blockstore, - 0, - 0, - nonce, - ); + let handler = StandardRepairHandler::new(blockstore.clone()); + let rv = handler.run_highest_window_request(&recycler, &socketaddr_any!(), 0, 0, nonce); assert!(rv.is_none()); let _ = fill_blockstore_slot_with_ticks( @@ -1904,15 +1793,9 @@ mod tests { ); let index = 1; - let mut rv = ServeRepair::run_highest_window_request( - &recycler, - &socketaddr_any!(), - &blockstore, - slot, - index, - nonce, - ) - .expect("packets"); + let mut rv = handler + .run_highest_window_request(&recycler, &socketaddr_any!(), slot, index, nonce) + .expect("packets"); let request = ShredRepairType::HighestShred(slot, index); verify_responses(&request, rv.iter()); @@ -1931,10 +1814,9 @@ mod tests { assert_eq!(rv[0].index(), index as u32); assert_eq!(rv[0].slot(), slot); - let rv = ServeRepair::run_highest_window_request( + let rv = handler.run_highest_window_request( &recycler, &socketaddr_any!(), - &blockstore, slot, index + 1, nonce, @@ -1948,19 +1830,13 @@ mod tests { } /// test window requests respond with the right shred, and do not overrun - fn run_window_request(slot: Slot, nonce: Nonce) { + pub fn run_window_request(slot: Slot, nonce: Nonce) { let recycler = PacketBatchRecycler::default(); solana_logger::setup(); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let rv = ServeRepair::run_window_request( - &recycler, - &socketaddr_any!(), - &blockstore, - slot, - 0, - nonce, - ); + let handler = StandardRepairHandler::new(blockstore.clone()); + let rv = handler.run_window_request(&recycler, &socketaddr_any!(), slot, 0, nonce); assert!(rv.is_none()); let shred = Shred::new_from_data(slot, 1, 1, &[], ShredFlags::empty(), 0, 2, 0); @@ -1969,15 +1845,9 @@ mod tests { .expect("Expect successful ledger write"); let index = 1; - let mut rv = ServeRepair::run_window_request( - &recycler, - &socketaddr_any!(), - &blockstore, - slot, - index, - nonce, - ) - .expect("packets"); + let mut rv = handler + .run_window_request(&recycler, &socketaddr_any!(), slot, index, nonce) + .expect("packets"); let request = ShredRepairType::Shred(slot, index); verify_responses(&request, rv.iter()); let rv: Vec = rv @@ -2008,7 +1878,7 @@ mod tests { let bank_forks = BankForks::new_rw_arc(bank); let cluster_slots = ClusterSlots::default(); let cluster_info = Arc::new(new_test_cluster_info()); - let serve_repair = ServeRepair::new( + let serve_repair = ServeRepair::new_for_test( cluster_info.clone(), bank_forks, Arc::new(RwLock::new(HashSet::default())), @@ -2115,13 +1985,13 @@ mod tests { run_orphan(2, 3, 9); } - fn run_orphan(slot: Slot, num_slots: u64, nonce: Nonce) { + pub fn run_orphan(slot: Slot, num_slots: u64, nonce: Nonce) { solana_logger::setup(); let recycler = PacketBatchRecycler::default(); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let rv = - ServeRepair::run_orphan(&recycler, &socketaddr_any!(), &blockstore, slot, 5, nonce); + let handler = StandardRepairHandler::new(blockstore.clone()); + let rv = handler.run_orphan(&recycler, &socketaddr_any!(), slot, 5, nonce); assert!(rv.is_none()); // Create slots [slot, slot + num_slots) with 5 shreds apiece @@ -2132,30 +2002,23 @@ mod tests { .expect("Expect successful ledger write"); // We don't have slot `slot + num_slots`, so we don't know how to service this request - let rv = ServeRepair::run_orphan( - &recycler, - &socketaddr_any!(), - &blockstore, - slot + num_slots, - 5, - nonce, - ); + let rv = handler.run_orphan(&recycler, &socketaddr_any!(), slot + num_slots, 5, nonce); assert!(rv.is_none()); // For a orphan request for `slot + num_slots - 1`, we should return the highest shreds // from slots in the range [slot, slot + num_slots - 1] - let rv: Vec<_> = ServeRepair::run_orphan( - &recycler, - &socketaddr_any!(), - &blockstore, - slot + num_slots - 1, - 5, - nonce, - ) - .expect("run_orphan packets") - .iter() - .cloned() - .collect(); + let rv: Vec<_> = handler + .run_orphan( + &recycler, + &socketaddr_any!(), + slot + num_slots - 1, + 5, + nonce, + ) + .expect("run_orphan packets") + .iter() + .cloned() + .collect(); // Verify responses let request = ShredRepairType::Orphan(slot + num_slots - 1); @@ -2209,12 +2072,13 @@ mod tests { // Orphan request for slot 2 should only return slot 1 since // calling `repair_response_packet` on slot 1's shred will // be corrupted - let rv: Vec<_> = - ServeRepair::run_orphan(&recycler, &socketaddr_any!(), &blockstore, 2, 5, nonce) - .expect("run_orphan packets") - .iter() - .cloned() - .collect(); + let handler = StandardRepairHandler::new(blockstore.clone()); + let rv: Vec<_> = handler + .run_orphan(&recycler, &socketaddr_any!(), 2, 5, nonce) + .expect("run_orphan packets") + .iter() + .cloned() + .collect(); // Verify responses let expected = vec![repair_response::repair_response_packet( @@ -2254,14 +2118,10 @@ mod tests { .expect("Expect successful ledger write"); // We don't have slot `slot + num_slots`, so we return empty - let rv = ServeRepair::run_ancestor_hashes( - &recycler, - &socketaddr_any!(), - &blockstore, - slot + num_slots, - nonce, - ) - .expect("run_ancestor_hashes packets"); + let handler = StandardRepairHandler::new(blockstore.clone()); + let rv = handler + .run_ancestor_hashes(&recycler, &socketaddr_any!(), slot + num_slots, nonce) + .expect("run_ancestor_hashes packets"); assert_eq!(rv.len(), 1); let packet = &rv[0]; let ancestor_hashes_response = deserialize_ancestor_hashes_response(packet); @@ -2276,14 +2136,9 @@ mod tests { // `slot + num_slots - 1` is not marked duplicate confirmed so nothing should return // empty - let rv = ServeRepair::run_ancestor_hashes( - &recycler, - &socketaddr_any!(), - &blockstore, - slot + num_slots - 1, - nonce, - ) - .expect("run_ancestor_hashes packets"); + let rv = handler + .run_ancestor_hashes(&recycler, &socketaddr_any!(), slot + num_slots - 1, nonce) + .expect("run_ancestor_hashes packets"); assert_eq!(rv.len(), 1); let packet = &rv[0]; let ancestor_hashes_response = deserialize_ancestor_hashes_response(packet); @@ -2305,14 +2160,9 @@ mod tests { (duplicate_confirmed_slot, frozen_hash); blockstore.insert_bank_hash(duplicate_confirmed_slot, frozen_hash, true); } - let rv = ServeRepair::run_ancestor_hashes( - &recycler, - &socketaddr_any!(), - &blockstore, - slot + num_slots - 1, - nonce, - ) - .expect("run_ancestor_hashes packets"); + let rv = handler + .run_ancestor_hashes(&recycler, &socketaddr_any!(), slot + num_slots - 1, nonce) + .expect("run_ancestor_hashes packets"); assert_eq!(rv.len(), 1); let packet = &rv[0]; let ancestor_hashes_response = deserialize_ancestor_hashes_response(packet); @@ -2341,7 +2191,7 @@ mod tests { cluster_info.insert_info(contact_info2.clone()); cluster_info.insert_info(contact_info3.clone()); let identity_keypair = cluster_info.keypair().clone(); - let serve_repair = ServeRepair::new( + let serve_repair = ServeRepair::new_for_test( cluster_info, bank_forks, Arc::new(RwLock::new(HashSet::default())), diff --git a/core/src/repair/serve_repair_service.rs b/core/src/repair/serve_repair_service.rs index 5fa115a456..d6300efe66 100644 --- a/core/src/repair/serve_repair_service.rs +++ b/core/src/repair/serve_repair_service.rs @@ -2,7 +2,6 @@ use { crate::repair::{quic_endpoint::RemoteRequest, serve_repair::ServeRepair}, bytes::Bytes, crossbeam_channel::{unbounded, Receiver, Sender}, - solana_ledger::blockstore::Blockstore, solana_perf::{packet::PacketBatch, recycler::Recycler}, solana_streamer::{ socket::SocketAddrSpace, @@ -27,7 +26,6 @@ impl ServeRepairService { remote_request_sender: Sender, remote_request_receiver: Receiver, repair_response_quic_sender: AsyncSender<(SocketAddr, Bytes)>, - blockstore: Arc, serve_repair_socket: UdpSocket, socket_addr_space: SocketAddrSpace, stats_reporter_sender: Sender>, @@ -65,7 +63,6 @@ impl ServeRepairService { Some(stats_reporter_sender), ); let t_listen = serve_repair.listen( - blockstore, remote_request_receiver, response_sender, repair_response_quic_sender, diff --git a/core/src/repair/standard_repair_handler.rs b/core/src/repair/standard_repair_handler.rs new file mode 100644 index 0000000000..22aef087a6 --- /dev/null +++ b/core/src/repair/standard_repair_handler.rs @@ -0,0 +1,68 @@ +use { + super::{repair_handler::RepairHandler, repair_response}, + solana_ledger::{blockstore::Blockstore, shred::Nonce}, + solana_perf::packet::{Packet, PacketBatch, PacketBatchRecycler}, + solana_sdk::clock::Slot, + std::{net::SocketAddr, sync::Arc}, +}; + +pub(crate) struct StandardRepairHandler { + blockstore: Arc, +} + +impl StandardRepairHandler { + pub(crate) fn new(blockstore: Arc) -> Self { + Self { blockstore } + } +} + +impl RepairHandler for StandardRepairHandler { + fn blockstore(&self) -> &Blockstore { + &self.blockstore + } + + fn repair_response_packet( + &self, + slot: Slot, + shred_index: u64, + dest: &SocketAddr, + nonce: Nonce, + ) -> Option { + repair_response::repair_response_packet( + self.blockstore.as_ref(), + slot, + shred_index, + dest, + nonce, + ) + } + + fn run_orphan( + &self, + recycler: &PacketBatchRecycler, + from_addr: &SocketAddr, + slot: Slot, + max_responses: usize, + nonce: Nonce, + ) -> Option { + let mut res = + PacketBatch::new_unpinned_with_recycler(recycler, max_responses, "run_orphan"); + // Try to find the next "n" parent slots of the input slot + let packets = std::iter::successors(self.blockstore.meta(slot).ok()?, |meta| { + self.blockstore.meta(meta.parent_slot?).ok()? + }) + .map_while(|meta| { + repair_response::repair_response_packet( + self.blockstore.as_ref(), + meta.slot, + meta.received.checked_sub(1u64)?, + from_addr, + nonce, + ) + }); + for packet in packets.take(max_responses) { + res.push(packet); + } + (!res.is_empty()).then_some(res) + } +} diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index bd5ef96aee..c0bd8edbb6 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1,14 +1,14 @@ //! The `replay_stage` replays transactions broadcast by the leader. - use { crate::{ banking_stage::update_bank_forks_and_poh_recorder_for_new_tpu_bank, banking_trace::BankingTracer, + block_creation_loop::ReplayHighestFrozen, cluster_info_vote_listener::{ DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, VoteTracker, }, cluster_slots_service::{cluster_slots::ClusterSlots, ClusterSlotsUpdateSender}, - commitment_service::{AggregateCommitmentService, CommitmentAggregationData}, + commitment_service::{AggregateCommitmentService, TowerCommitmentAggregationData}, consensus::{ fork_choice::{select_vote_and_reset_forks, ForkChoice, SelectVoteAndResetForkResult}, heaviest_subtree_fork_choice::HeaviestSubtreeForkChoice, @@ -33,7 +33,10 @@ use { window_service::DuplicateSlotReceiver, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, - rayon::{prelude::*, ThreadPool}, + rayon::{ + iter::{IntoParallelIterator, ParallelIterator}, + ThreadPool, + }, solana_accounts_db::contains::Contains, solana_entry::entry::VerifyRecyclers, solana_geyser_plugin_manager::block_metadata_notifier_interface::BlockMetadataNotifierArc, @@ -65,7 +68,9 @@ use { commitment::BlockCommitmentCache, installed_scheduler_pool::BankWithScheduler, prioritization_fee_cache::PrioritizationFeeCache, - vote_sender_types::ReplayVoteSender, + vote_sender_types::{ + BLSVerifiedMessageReceiver, BLSVerifiedMessageSender, ReplayVoteSender, + }, }, solana_sdk::{ clock::{BankId, Slot, NUM_CONSECUTIVE_LEADER_SLOTS}, @@ -78,6 +83,15 @@ use { }, solana_timings::ExecuteTimings, solana_vote::vote_transaction::VoteTransaction, + solana_votor::{ + event::{CompletedBlock, VotorEvent, VotorEventReceiver, VotorEventSender}, + root_utils, + vote_history::VoteHistory, + vote_history_storage::VoteHistoryStorage, + voting_utils::{BLSOp, GenerateVoteTxResult}, + votor::{LeaderWindowNotifier, Votor, VotorConfig}, + }, + solana_votor_messages::bls_message::{Certificate, CertificateMessage}, std::{ collections::{HashMap, HashSet}, num::NonZeroUsize, @@ -97,7 +111,7 @@ pub const MAX_UNCONFIRMED_SLOTS: usize = 5; pub const DUPLICATE_LIVENESS_THRESHOLD: f64 = 0.1; pub const DUPLICATE_THRESHOLD: f64 = 1.0 - SWITCH_FORK_THRESHOLD - DUPLICATE_LIVENESS_THRESHOLD; -const MAX_VOTE_SIGNATURES: usize = 200; +pub(crate) const MAX_VOTE_SIGNATURES: usize = 200; const MAX_VOTE_REFRESH_INTERVAL_MILLIS: usize = 5000; const MAX_REPAIR_RETRY_LOOP_ATTEMPTS: usize = 10; @@ -105,6 +119,7 @@ const MAX_REPAIR_RETRY_LOOP_ATTEMPTS: usize = 10; static_assertions::const_assert!(REFRESH_VOTE_BLOCKHEIGHT < solana_sdk::clock::MAX_PROCESSING_AGE); // Give at least 4 leaders the chance to pack our vote const REFRESH_VOTE_BLOCKHEIGHT: usize = 16; + #[derive(PartialEq, Eq, Debug)] pub enum HeaviestForkFailures { LockedOut(u64), @@ -131,36 +146,14 @@ enum ForkReplayMode { Parallel(ThreadPool), } -enum GenerateVoteTxResult { - // non voting validator, not eligible for refresh - // until authorized keypair is overriden - NonVoting, - // hot spare validator, not eligble for refresh - // until set identity is invoked - HotSpare, - // failed generation, eligible for refresh - Failed, - Tx(Transaction), -} - -impl GenerateVoteTxResult { - fn is_non_voting(&self) -> bool { - matches!(self, Self::NonVoting) - } - - fn is_hot_spare(&self) -> bool { - matches!(self, Self::HotSpare) - } -} - // Implement a destructor for the ReplayStage thread to signal it exited // even on panics -struct Finalizer { +pub(crate) struct Finalizer { exit_sender: Arc, } impl Finalizer { - fn new(exit_sender: Arc) -> Self { + pub(crate) fn new(exit_sender: Arc) -> Self { Finalizer { exit_sender } } } @@ -189,6 +182,14 @@ struct SkippedSlotsInfo { last_skipped_slot: u64, } +pub struct TowerBFTStructures { + pub heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice, + pub duplicate_slots_tracker: DuplicateSlotsTracker, + pub duplicate_confirmed_slots: DuplicateConfirmedSlots, + pub unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes, + pub epoch_slots_frozen_slots: EpochSlotsFrozenSlots, +} + struct PartitionInfo { partition_start_time: Option, } @@ -270,11 +271,15 @@ pub struct ReplayStageConfig { pub cluster_info: Arc, pub poh_recorder: Arc>, pub tower: Tower, + pub vote_history: VoteHistory, + pub vote_history_storage: Arc, pub vote_tracker: Arc, pub cluster_slots: Arc, pub log_messages_bytes_limit: Option, pub prioritization_fee_cache: Arc, pub banking_tracer: Arc, + pub replay_highest_frozen: Arc, + pub leader_window_notifier: Arc, } pub struct ReplaySenders { @@ -291,9 +296,13 @@ pub struct ReplaySenders { pub cluster_slots_update_sender: Sender>, pub cost_update_sender: Sender, pub voting_sender: Sender, + pub bls_sender: Sender, pub drop_bank_sender: Sender>, pub block_metadata_notifier: Option, pub dumped_slots_sender: Sender>, + pub certificate_sender: Sender<(Certificate, CertificateMessage)>, + pub votor_event_sender: VotorEventSender, + pub own_vote_sender: BLSVerifiedMessageSender, } pub struct ReplayReceivers { @@ -303,6 +312,8 @@ pub struct ReplayReceivers { pub duplicate_confirmed_slots_receiver: Receiver>, pub gossip_verified_vote_hash_receiver: Receiver<(Pubkey, u64, Hash)>, pub popular_pruned_forks_receiver: Receiver>, + pub bls_verified_message_receiver: BLSVerifiedMessageReceiver, + pub votor_event_receiver: VotorEventReceiver, } /// Timing information for the ReplayStage main processing loop @@ -341,19 +352,15 @@ struct ReplayLoopTiming { } impl ReplayLoopTiming { #[allow(clippy::too_many_arguments)] - fn update( + fn update_non_alpenglow( &mut self, collect_frozen_banks_elapsed_us: u64, compute_bank_stats_elapsed_us: u64, select_vote_and_reset_forks_elapsed_us: u64, - start_leader_elapsed_us: u64, reset_bank_elapsed_us: u64, voting_elapsed_us: u64, select_forks_elapsed_us: u64, compute_slot_stats_elapsed_us: u64, - generate_new_bank_forks_elapsed_us: u64, - replay_active_banks_elapsed_us: u64, - wait_receive_elapsed_us: u64, heaviest_fork_failures_elapsed_us: u64, bank_count: u64, process_ancestor_hashes_duplicate_slots_elapsed_us: u64, @@ -364,18 +371,13 @@ impl ReplayLoopTiming { repair_correct_slots_elapsed_us: u64, retransmit_not_propagated_elapsed_us: u64, ) { - self.loop_count += 1; self.collect_frozen_banks_elapsed_us += collect_frozen_banks_elapsed_us; self.compute_bank_stats_elapsed_us += compute_bank_stats_elapsed_us; self.select_vote_and_reset_forks_elapsed_us += select_vote_and_reset_forks_elapsed_us; - self.start_leader_elapsed_us += start_leader_elapsed_us; self.reset_bank_elapsed_us += reset_bank_elapsed_us; self.voting_elapsed_us += voting_elapsed_us; self.select_forks_elapsed_us += select_forks_elapsed_us; self.compute_slot_stats_elapsed_us += compute_slot_stats_elapsed_us; - self.generate_new_bank_forks_elapsed_us += generate_new_bank_forks_elapsed_us; - self.replay_active_banks_elapsed_us += replay_active_banks_elapsed_us; - self.wait_receive_elapsed_us += wait_receive_elapsed_us; self.heaviest_fork_failures_elapsed_us += heaviest_fork_failures_elapsed_us; self.bank_count += bank_count; self.process_ancestor_hashes_duplicate_slots_elapsed_us += @@ -388,6 +390,20 @@ impl ReplayLoopTiming { self.process_duplicate_slots_elapsed_us += process_duplicate_slots_elapsed_us; self.repair_correct_slots_elapsed_us += repair_correct_slots_elapsed_us; self.retransmit_not_propagated_elapsed_us += retransmit_not_propagated_elapsed_us; + } + + fn update_common( + &mut self, + generate_new_bank_forks_elapsed_us: u64, + replay_active_banks_elapsed_us: u64, + start_leader_elapsed_us: u64, + wait_receive_elapsed_us: u64, + ) { + self.loop_count += 1; + self.generate_new_bank_forks_elapsed_us += generate_new_bank_forks_elapsed_us; + self.replay_active_banks_elapsed_us += replay_active_banks_elapsed_us; + self.start_leader_elapsed_us += start_leader_elapsed_us; + self.wait_receive_elapsed_us += wait_receive_elapsed_us; self.maybe_submit(); } @@ -536,6 +552,7 @@ impl ReplayLoopTiming { pub struct ReplayStage { t_replay: JoinHandle<()>, + votor: Votor, commitment_service: AggregateCommitmentService, } @@ -561,11 +578,15 @@ impl ReplayStage { cluster_info, poh_recorder, mut tower, + vote_history, + vote_history_storage, vote_tracker, cluster_slots, log_messages_bytes_limit, prioritization_fee_cache, banking_tracer, + replay_highest_frozen, + leader_window_notifier, } = config; let ReplaySenders { @@ -582,9 +603,13 @@ impl ReplayStage { cluster_slots_update_sender, cost_update_sender, voting_sender, + bls_sender, drop_bank_sender, block_metadata_notifier, dumped_slots_sender, + certificate_sender, + votor_event_sender, + own_vote_sender, } = senders; let ReplayReceivers { @@ -594,47 +619,114 @@ impl ReplayStage { duplicate_confirmed_slots_receiver, gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, + bls_verified_message_receiver, + votor_event_receiver, } = receivers; trace!("replay stage"); + // Start the replay stage loop - let (lockouts_sender, commitment_service) = AggregateCommitmentService::new( - exit.clone(), - block_commitment_cache.clone(), - rpc_subscriptions.clone(), - ); + let (lockouts_sender, commitment_sender, commitment_service) = + AggregateCommitmentService::new( + exit.clone(), + block_commitment_cache.clone(), + rpc_subscriptions.clone(), + ); + + // Alpenglow specific objects + let votor_config = VotorConfig { + exit: exit.clone(), + vote_account, + wait_to_vote_slot, + wait_for_vote_to_start_leader, + vote_history, + vote_history_storage, + authorized_voter_keypairs: authorized_voter_keypairs.clone(), + blockstore: blockstore.clone(), + bank_forks: bank_forks.clone(), + cluster_info: cluster_info.clone(), + leader_schedule_cache: leader_schedule_cache.clone(), + rpc_subscriptions: rpc_subscriptions.clone(), + accounts_background_request_sender: accounts_background_request_sender.clone(), + bls_sender: bls_sender.clone(), + commitment_sender: commitment_sender.clone(), + drop_bank_sender: drop_bank_sender.clone(), + bank_notification_sender: bank_notification_sender.clone(), + leader_window_notifier, + certificate_sender, + event_sender: votor_event_sender.clone(), + event_receiver: votor_event_receiver.clone(), + own_vote_sender, + bls_receiver: bls_verified_message_receiver, + }; + let votor = Votor::new(votor_config); + + let mut first_alpenglow_slot = bank_forks + .read() + .unwrap() + .root_bank() + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()); + + let mut is_alpenglow_migration_complete = false; + if let Some(first_alpenglow_slot) = first_alpenglow_slot { + if bank_forks + .read() + .unwrap() + .frozen_banks() + .iter() + .any(|(slot, _bank)| *slot >= first_alpenglow_slot) + { + info!("initiating alpenglow migration on startup"); + Self::initiate_alpenglow_migration( + &poh_recorder, + &mut is_alpenglow_migration_complete, + ); + votor.start_migration(); + } + } + let mut highest_frozen_slot = bank_forks + .read() + .unwrap() + .highest_frozen_bank() + .map_or(0, |hfs| hfs.slot()); + *replay_highest_frozen.highest_frozen_slot.lock().unwrap() = highest_frozen_slot; + let run_replay = move || { let verify_recyclers = VerifyRecyclers::default(); let _exit = Finalizer::new(exit.clone()); let mut identity_keypair = cluster_info.keypair().clone(); let mut my_pubkey = identity_keypair.pubkey(); + if my_pubkey != tower.node_pubkey { // set-identity was called during the startup procedure, ensure the tower is consistent // before starting the loop. further calls to set-identity will reload the tower in the loop let my_old_pubkey = tower.node_pubkey; - tower = match Self::load_tower( - tower_storage.as_ref(), - &my_pubkey, - &vote_account, - &bank_forks, - ) { - Ok(tower) => tower, - Err(err) => { - error!( - "Unable to load new tower when attempting to change identity from {} \ - to {} on ReplayStage startup, Exiting: {}", - my_old_pubkey, my_pubkey, err - ); - // drop(_exit) will set the exit flag, eventually tearing down the entire process - return; - } - }; - warn!( - "Identity changed during startup from {} to {}", - my_old_pubkey, my_pubkey - ); + if !is_alpenglow_migration_complete { + tower = match Self::load_tower( + tower_storage.as_ref(), + &my_pubkey, + &vote_account, + &bank_forks, + ) { + Ok(tower) => tower, + Err(err) => { + error!( + "Unable to load new tower when attempting to change identity from {} \ + to {} on ReplayStage startup, Exiting: {}", + my_old_pubkey, my_pubkey, err + ); + // drop(_exit) will set the exit flag, eventually tearing down the entire process + return; + } + }; + warn!( + "Identity changed during startup from {} to {}", + my_old_pubkey, my_pubkey + ); + } } - let (mut progress, mut heaviest_subtree_fork_choice) = + let (mut progress, heaviest_subtree_fork_choice) = Self::initialize_progress_and_fork_choice_with_locked_bank_forks( &bank_forks, &my_pubkey, @@ -647,23 +739,30 @@ impl ReplayStage { let mut partition_info = PartitionInfo::new(); let mut skipped_slots_info = SkippedSlotsInfo::default(); let mut replay_timing = ReplayLoopTiming::default(); - let mut duplicate_slots_tracker = DuplicateSlotsTracker::default(); - let mut duplicate_confirmed_slots: DuplicateConfirmedSlots = + let duplicate_slots_tracker = DuplicateSlotsTracker::default(); + let duplicate_confirmed_slots: DuplicateConfirmedSlots = DuplicateConfirmedSlots::default(); - let mut epoch_slots_frozen_slots: EpochSlotsFrozenSlots = - EpochSlotsFrozenSlots::default(); + let epoch_slots_frozen_slots: EpochSlotsFrozenSlots = EpochSlotsFrozenSlots::default(); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let mut unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes = + let unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes = UnfrozenGossipVerifiedVoteHashes::default(); let mut latest_validator_votes_for_frozen_banks: LatestValidatorVotesForFrozenBanks = LatestValidatorVotesForFrozenBanks::default(); + let mut tbft_structs = TowerBFTStructures { + heaviest_subtree_fork_choice, + duplicate_slots_tracker, + duplicate_confirmed_slots, + unfrozen_gossip_verified_vote_hashes, + epoch_slots_frozen_slots, + }; let mut voted_signatures = Vec::new(); let mut has_new_vote_been_rooted = !wait_for_vote_to_start_leader; let mut last_vote_refresh_time = LastVoteRefreshTime { last_refresh_time: Instant::now(), last_print_time: Instant::now(), }; + let (working_bank, in_vote_only_mode) = { let r_bank_forks = bank_forks.read().unwrap(); ( @@ -690,13 +789,16 @@ impl ReplayStage { .build() .expect("new rayon threadpool"); - Self::reset_poh_recorder( - &my_pubkey, - &blockstore, - working_bank, - &poh_recorder, - &leader_schedule_cache, - ); + if !is_alpenglow_migration_complete { + // This reset is handled in block creation loop for alpenglow + Self::reset_poh_recorder( + &my_pubkey, + &blockstore, + working_bank, + &poh_recorder, + &leader_schedule_cache, + ); + } loop { // Stop getting entries if we get exit signal @@ -724,7 +826,7 @@ impl ReplayStage { let r_bank_forks = bank_forks.read().unwrap(); (r_bank_forks.ancestors(), r_bank_forks.descendants()) }; - let did_complete_bank = Self::replay_active_banks( + let new_frozen_slots = Self::replay_active_banks( &blockstore, &bank_forks, &my_pubkey, @@ -734,15 +836,10 @@ impl ReplayStage { block_meta_sender.as_ref(), entry_notification_sender.as_ref(), &verify_recyclers, - &mut heaviest_subtree_fork_choice, &replay_vote_sender, &bank_notification_sender, &rpc_subscriptions, &slot_status_notifier, - &mut duplicate_slots_tracker, - &duplicate_confirmed_slots, - &mut epoch_slots_frozen_slots, - &mut unfrozen_gossip_verified_vote_hashes, &mut latest_validator_votes_for_frozen_banks, &cluster_slots_update_sender, &cost_update_sender, @@ -755,429 +852,482 @@ impl ReplayStage { &replay_tx_thread_pool, &prioritization_fee_cache, &mut purge_repair_slot_counter, + &poh_recorder, + first_alpenglow_slot, + (!is_alpenglow_migration_complete).then_some(&mut tbft_structs), + &mut is_alpenglow_migration_complete, + &votor_event_sender, ); + let did_complete_bank = !new_frozen_slots.is_empty(); + if is_alpenglow_migration_complete { + if let Some(highest) = new_frozen_slots.iter().max() { + if *highest > highest_frozen_slot { + highest_frozen_slot = *highest; + let mut l_highest_frozen = + replay_highest_frozen.highest_frozen_slot.lock().unwrap(); + // Let the block creation loop know about this new frozen slot + *l_highest_frozen = *highest; + replay_highest_frozen.freeze_notification.notify_one(); + } + } + if did_complete_bank { + let bank_forks_r = bank_forks.read().unwrap(); + progress.handle_new_root(&bank_forks_r); + } + } replay_active_banks_time.stop(); let forks_root = bank_forks.read().unwrap().root(); - - // Process cluster-agreed versions of duplicate slots for which we potentially - // have the wrong version. Our version was dead or pruned. - // Signalled by ancestor_hashes_service. - let mut process_ancestor_hashes_duplicate_slots_time = - Measure::start("process_ancestor_hashes_duplicate_slots"); - Self::process_ancestor_hashes_duplicate_slots( - &my_pubkey, - &blockstore, - &ancestor_duplicate_slots_receiver, - &mut duplicate_slots_tracker, - &duplicate_confirmed_slots, - &mut epoch_slots_frozen_slots, - &progress, - &mut heaviest_subtree_fork_choice, - &bank_forks, - &mut duplicate_slots_to_repair, - &ancestor_hashes_replay_update_sender, - &mut purge_repair_slot_counter, - ); - process_ancestor_hashes_duplicate_slots_time.stop(); - - // Check for any newly duplicate confirmed slots detected from gossip / replay - // Note: since this is tracked using both gossip & replay votes, stake is not - // rolled up from descendants. - let mut process_duplicate_confirmed_slots_time = - Measure::start("process_duplicate_confirmed_slots"); - Self::process_duplicate_confirmed_slots( - &duplicate_confirmed_slots_receiver, - &blockstore, - &mut duplicate_slots_tracker, - &mut duplicate_confirmed_slots, - &mut epoch_slots_frozen_slots, - &bank_forks, - &progress, - &mut heaviest_subtree_fork_choice, - &mut duplicate_slots_to_repair, - &ancestor_hashes_replay_update_sender, - &mut purge_repair_slot_counter, - ); - process_duplicate_confirmed_slots_time.stop(); - - // Ingest any new verified votes from gossip. Important for fork choice - // and switching proofs because these may be votes that haven't yet been - // included in a block, so we may not have yet observed these votes just - // by replaying blocks. - let mut process_unfrozen_gossip_verified_vote_hashes_time = - Measure::start("process_gossip_verified_vote_hashes"); - Self::process_gossip_verified_vote_hashes( - &gossip_verified_vote_hash_receiver, - &mut unfrozen_gossip_verified_vote_hashes, - &heaviest_subtree_fork_choice, - &mut latest_validator_votes_for_frozen_banks, - ); - for _ in gossip_verified_vote_hash_receiver.try_iter() {} - process_unfrozen_gossip_verified_vote_hashes_time.stop(); - - let mut process_popular_pruned_forks_time = - Measure::start("process_popular_pruned_forks_time"); - // Check for "popular" (52+% stake aggregated across versions/descendants) forks - // that are pruned, which would not be detected by normal means. - // Signalled by `repair_service`. - Self::process_popular_pruned_forks( - &popular_pruned_forks_receiver, - &blockstore, - &mut duplicate_slots_tracker, - &mut epoch_slots_frozen_slots, - &bank_forks, - &mut heaviest_subtree_fork_choice, - &mut duplicate_slots_to_repair, - &ancestor_hashes_replay_update_sender, - &mut purge_repair_slot_counter, - ); - process_popular_pruned_forks_time.stop(); - - // Check to remove any duplicated slots from fork choice - let mut process_duplicate_slots_time = Measure::start("process_duplicate_slots"); - if !tpu_has_bank { - Self::process_duplicate_slots( + let start_leader_time = if !is_alpenglow_migration_complete { + // TODO(ashwin): This will be moved to the event coordinator once we figure out + // migration + for _ in votor_event_receiver.try_iter() {} + + // Process cluster-agreed versions of duplicate slots for which we potentially + // have the wrong version. Our version was dead or pruned. + // Signalled by ancestor_hashes_service. + let mut process_ancestor_hashes_duplicate_slots_time = + Measure::start("process_ancestor_hashes_duplicate_slots"); + Self::process_ancestor_hashes_duplicate_slots( + &my_pubkey, &blockstore, - &duplicate_slots_receiver, - &mut duplicate_slots_tracker, - &duplicate_confirmed_slots, - &mut epoch_slots_frozen_slots, - &bank_forks, + &ancestor_duplicate_slots_receiver, + &mut tbft_structs.duplicate_slots_tracker, + &tbft_structs.duplicate_confirmed_slots, + &mut tbft_structs.epoch_slots_frozen_slots, &progress, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, + &bank_forks, &mut duplicate_slots_to_repair, &ancestor_hashes_replay_update_sender, &mut purge_repair_slot_counter, ); - } - process_duplicate_slots_time.stop(); - - let mut collect_frozen_banks_time = Measure::start("frozen_banks"); - let mut frozen_banks: Vec<_> = bank_forks - .read() - .unwrap() - .frozen_banks() - .into_iter() - .filter(|(slot, _)| *slot >= forks_root) - .map(|(_, bank)| bank) - .collect(); - collect_frozen_banks_time.stop(); - - let mut compute_bank_stats_time = Measure::start("compute_bank_stats"); - let newly_computed_slot_stats = Self::compute_bank_stats( - &vote_account, - &ancestors, - &mut frozen_banks, - &mut tower, - &mut progress, - &vote_tracker, - &cluster_slots, - &bank_forks, - &mut heaviest_subtree_fork_choice, - &mut latest_validator_votes_for_frozen_banks, - ); - compute_bank_stats_time.stop(); - - let mut compute_slot_stats_time = Measure::start("compute_slot_stats_time"); - for slot in newly_computed_slot_stats { - let fork_stats = progress.get_fork_stats(slot).unwrap(); - let duplicate_confirmed_forks = Self::tower_duplicate_confirmed_forks( - &tower, - &fork_stats.voted_stakes, - fork_stats.total_stake, - &progress, - &bank_forks, - ); - - Self::mark_slots_duplicate_confirmed( - &duplicate_confirmed_forks, + process_ancestor_hashes_duplicate_slots_time.stop(); + + // Check for any newly duplicate confirmed slots detected from gossip / replay + // Note: since this is tracked using both gossip & replay votes, stake is not + // rolled up from descendants. + let mut process_duplicate_confirmed_slots_time = + Measure::start("process_duplicate_confirmed_slots"); + Self::process_duplicate_confirmed_slots( + &duplicate_confirmed_slots_receiver, &blockstore, + &mut tbft_structs.duplicate_slots_tracker, + &mut tbft_structs.duplicate_confirmed_slots, + &mut tbft_structs.epoch_slots_frozen_slots, &bank_forks, - &mut progress, - &mut duplicate_slots_tracker, - &mut heaviest_subtree_fork_choice, - &mut epoch_slots_frozen_slots, + &progress, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut duplicate_slots_to_repair, &ancestor_hashes_replay_update_sender, &mut purge_repair_slot_counter, - &mut duplicate_confirmed_slots, ); - } - compute_slot_stats_time.stop(); - - let mut select_forks_time = Measure::start("select_forks_time"); - let (heaviest_bank, heaviest_bank_on_same_voted_fork) = - heaviest_subtree_fork_choice.select_forks( - &frozen_banks, - &tower, - &progress, - &ancestors, + process_duplicate_confirmed_slots_time.stop(); + + // Ingest any new verified votes from gossip. Important for fork choice + // and switching proofs because these may be votes that haven't yet been + // included in a block, so we may not have yet observed these votes just + // by replaying blocks. + let mut process_unfrozen_gossip_verified_vote_hashes_time = + Measure::start("process_gossip_verified_vote_hashes"); + Self::process_gossip_verified_vote_hashes( + &gossip_verified_vote_hash_receiver, + &mut tbft_structs.unfrozen_gossip_verified_vote_hashes, + &tbft_structs.heaviest_subtree_fork_choice, + &mut latest_validator_votes_for_frozen_banks, + ); + for _ in gossip_verified_vote_hash_receiver.try_iter() {} + process_unfrozen_gossip_verified_vote_hashes_time.stop(); + + let mut process_popular_pruned_forks_time = + Measure::start("process_popular_pruned_forks_time"); + // Check for "popular" (52+% stake aggregated across versions/descendants) forks + // that are pruned, which would not be detected by normal means. + // Signalled by `repair_service`. + Self::process_popular_pruned_forks( + &popular_pruned_forks_receiver, + &blockstore, + &mut tbft_structs.duplicate_slots_tracker, + &mut tbft_structs.epoch_slots_frozen_slots, &bank_forks, + &mut tbft_structs.heaviest_subtree_fork_choice, + &mut duplicate_slots_to_repair, + &ancestor_hashes_replay_update_sender, + &mut purge_repair_slot_counter, ); - select_forks_time.stop(); + process_popular_pruned_forks_time.stop(); - Self::check_for_vote_only_mode( - heaviest_bank.slot(), - forks_root, - &in_vote_only_mode, - &bank_forks, - ); - - let mut select_vote_and_reset_forks_time = - Measure::start("select_vote_and_reset_forks"); - let SelectVoteAndResetForkResult { - vote_bank, - reset_bank, - heaviest_fork_failures, - } = select_vote_and_reset_forks( - &heaviest_bank, - heaviest_bank_on_same_voted_fork.as_ref(), - &ancestors, - &descendants, - &progress, - &mut tower, - &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, - ); - select_vote_and_reset_forks_time.stop(); + // Check to remove any duplicated slots from fork choice + let mut process_duplicate_slots_time = + Measure::start("process_duplicate_slots"); + if !tpu_has_bank { + Self::process_duplicate_slots( + &blockstore, + &duplicate_slots_receiver, + &mut tbft_structs.duplicate_slots_tracker, + &tbft_structs.duplicate_confirmed_slots, + &mut tbft_structs.epoch_slots_frozen_slots, + &bank_forks, + &progress, + &mut tbft_structs.heaviest_subtree_fork_choice, + &mut duplicate_slots_to_repair, + &ancestor_hashes_replay_update_sender, + &mut purge_repair_slot_counter, + ); + } + process_duplicate_slots_time.stop(); - if vote_bank.is_none() { - Self::maybe_refresh_last_vote( - &mut tower, - &progress, - heaviest_bank_on_same_voted_fork, + let mut collect_frozen_banks_time = Measure::start("frozen_banks"); + let mut frozen_banks: Vec<_> = bank_forks + .read() + .unwrap() + .frozen_banks() + .into_iter() + .filter(|(slot, _)| *slot >= forks_root) + .map(|(_, bank)| bank) + .collect(); + collect_frozen_banks_time.stop(); + + let mut compute_bank_stats_time = Measure::start("compute_bank_stats"); + let newly_computed_slot_stats = Self::compute_bank_stats( &vote_account, - &identity_keypair, - &authorized_voter_keypairs.read().unwrap(), - &mut voted_signatures, - has_new_vote_been_rooted, - &mut last_vote_refresh_time, - &voting_sender, - wait_to_vote_slot, - ); - } - - let mut heaviest_fork_failures_time = Measure::start("heaviest_fork_failures_time"); - if tower.is_recent(heaviest_bank.slot()) && !heaviest_fork_failures.is_empty() { - Self::log_heaviest_fork_failures( - &heaviest_fork_failures, - &bank_forks, - &tower, - &progress, &ancestors, - &heaviest_bank, - &mut last_threshold_failure_slot, + &mut frozen_banks, + &mut tower, + &mut progress, + &vote_tracker, + &cluster_slots, + &bank_forks, + &mut tbft_structs.heaviest_subtree_fork_choice, + &mut latest_validator_votes_for_frozen_banks, ); - } - heaviest_fork_failures_time.stop(); + compute_bank_stats_time.stop(); + + let mut compute_slot_stats_time = Measure::start("compute_slot_stats_time"); + for slot in newly_computed_slot_stats { + let fork_stats = progress.get_fork_stats(slot).unwrap(); + let duplicate_confirmed_forks = Self::tower_duplicate_confirmed_forks( + &tower, + &fork_stats.voted_stakes, + fork_stats.total_stake, + &progress, + &bank_forks, + ); - let mut voting_time = Measure::start("voting_time"); - // Vote on a fork - if let Some((ref vote_bank, ref switch_fork_decision)) = vote_bank { - if let Some(votable_leader) = - leader_schedule_cache.slot_leader_at(vote_bank.slot(), Some(vote_bank)) - { - Self::log_leader_change( - &my_pubkey, - vote_bank.slot(), - &mut current_leader, - &votable_leader, + Self::mark_slots_duplicate_confirmed( + &duplicate_confirmed_forks, + &blockstore, + &bank_forks, + &mut progress, + &mut tbft_structs.duplicate_slots_tracker, + &mut tbft_structs.heaviest_subtree_fork_choice, + &mut tbft_structs.epoch_slots_frozen_slots, + &mut duplicate_slots_to_repair, + &ancestor_hashes_replay_update_sender, + &mut purge_repair_slot_counter, + &mut tbft_structs.duplicate_confirmed_slots, ); } + compute_slot_stats_time.stop(); + + let mut select_forks_time = Measure::start("select_forks_time"); + let (heaviest_bank, heaviest_bank_on_same_voted_fork) = tbft_structs + .heaviest_subtree_fork_choice + .select_forks(&frozen_banks, &tower, &progress, &ancestors, &bank_forks); + select_forks_time.stop(); + + Self::check_for_vote_only_mode( + heaviest_bank.slot(), + forks_root, + &in_vote_only_mode, + &bank_forks, + ); - if let Err(e) = Self::handle_votable_bank( + let mut select_vote_and_reset_forks_time = + Measure::start("select_vote_and_reset_forks"); + let SelectVoteAndResetForkResult { vote_bank, - switch_fork_decision, - &bank_forks, + reset_bank, + heaviest_fork_failures, + } = select_vote_and_reset_forks( + &heaviest_bank, + heaviest_bank_on_same_voted_fork.as_ref(), + &ancestors, + &descendants, + &progress, &mut tower, - &mut progress, - &vote_account, - &identity_keypair, - &authorized_voter_keypairs.read().unwrap(), - &blockstore, - &leader_schedule_cache, - &lockouts_sender, - &accounts_background_request_sender, - &rpc_subscriptions, - &block_commitment_cache, - &mut heaviest_subtree_fork_choice, - &bank_notification_sender, - &mut duplicate_slots_tracker, - &mut duplicate_confirmed_slots, - &mut unfrozen_gossip_verified_vote_hashes, - &mut voted_signatures, - &mut has_new_vote_been_rooted, - &mut replay_timing, - &voting_sender, - &mut epoch_slots_frozen_slots, - &drop_bank_sender, - wait_to_vote_slot, - ) { - error!("Unable to set root: {e}"); - return; - } - } - voting_time.stop(); - - let mut reset_bank_time = Measure::start("reset_bank"); - // Reset onto a fork - if let Some(reset_bank) = reset_bank { - if last_reset == reset_bank.last_blockhash() { - let reset_bank_descendants = - Self::get_active_descendants(reset_bank.slot(), &progress, &blockstore); - if reset_bank_descendants != last_reset_bank_descendants { - last_reset_bank_descendants = reset_bank_descendants; - poh_recorder - .write() - .unwrap() - .update_start_bank_active_descendants(&last_reset_bank_descendants); - } - } else { - info!( - "vote bank: {:?} reset bank: {:?}", - vote_bank - .as_ref() - .map(|(b, switch_fork_decision)| (b.slot(), switch_fork_decision)), - reset_bank.slot(), - ); - let fork_progress = progress - .get(&reset_bank.slot()) - .expect("bank to reset to must exist in progress map"); - datapoint_info!( - "blocks_produced", - ("num_blocks_on_fork", fork_progress.num_blocks_on_fork, i64), - ( - "num_dropped_blocks_on_fork", - fork_progress.num_dropped_blocks_on_fork, - i64 - ), + &latest_validator_votes_for_frozen_banks, + &tbft_structs.heaviest_subtree_fork_choice, + ); + select_vote_and_reset_forks_time.stop(); + + if vote_bank.is_none() { + Self::maybe_refresh_last_vote( + &mut tower, + &progress, + heaviest_bank_on_same_voted_fork, + &vote_account, + &identity_keypair, + &authorized_voter_keypairs.read().unwrap(), + &mut voted_signatures, + has_new_vote_been_rooted, + &mut last_vote_refresh_time, + &voting_sender, + wait_to_vote_slot, ); + } - if my_pubkey != cluster_info.id() { - identity_keypair = cluster_info.keypair().clone(); - let my_old_pubkey = my_pubkey; - my_pubkey = identity_keypair.pubkey(); + let mut heaviest_fork_failures_time = + Measure::start("heaviest_fork_failures_time"); + if tower.is_recent(heaviest_bank.slot()) && !heaviest_fork_failures.is_empty() { + Self::log_heaviest_fork_failures( + &heaviest_fork_failures, + &bank_forks, + &tower, + &progress, + &ancestors, + &heaviest_bank, + &mut last_threshold_failure_slot, + ); + } + heaviest_fork_failures_time.stop(); - // Load the new identity's tower - tower = match Self::load_tower( - tower_storage.as_ref(), + let mut voting_time = Measure::start("voting_time"); + // Vote on a fork + if let Some((ref vote_bank, ref switch_fork_decision)) = vote_bank { + if let Some(votable_leader) = + leader_schedule_cache.slot_leader_at(vote_bank.slot(), Some(vote_bank)) + { + Self::log_leader_change( &my_pubkey, - &vote_account, - &bank_forks, - ) { - Ok(tower) => tower, - Err(err) => { - error!( - "Unable to load new tower when attempting to change \ - identity from {} to {} on set-identity, Exiting: {}", - my_old_pubkey, my_pubkey, err - ); - // drop(_exit) will set the exit flag, eventually tearing down the entire process - return; - } - }; - // Ensure the validator can land votes with the new identity before - // becoming leader - has_new_vote_been_rooted = !wait_for_vote_to_start_leader; - warn!("Identity changed from {} to {}", my_old_pubkey, my_pubkey); + vote_bank.slot(), + &mut current_leader, + &votable_leader, + ); } - Self::reset_poh_recorder( - &my_pubkey, + if let Err(e) = Self::handle_votable_bank( + vote_bank, + switch_fork_decision, + &bank_forks, + &mut tower, + &mut progress, + &vote_account, + &identity_keypair, + &authorized_voter_keypairs.read().unwrap(), &blockstore, - reset_bank.clone(), - &poh_recorder, &leader_schedule_cache, - ); - last_reset = reset_bank.last_blockhash(); - last_reset_bank_descendants = vec![]; - tpu_has_bank = false; - - if let Some(last_voted_slot) = tower.last_voted_slot() { - // If the current heaviest bank is not a descendant of the last voted slot, - // there must be a partition - partition_info.update( - Self::is_partition_detected( - &ancestors, - last_voted_slot, - heaviest_bank.slot(), - ), - heaviest_bank.slot(), - last_voted_slot, + &lockouts_sender, + &accounts_background_request_sender, + &rpc_subscriptions, + &block_commitment_cache, + &bank_notification_sender, + &mut voted_signatures, + &mut has_new_vote_been_rooted, + &mut replay_timing, + &voting_sender, + &drop_bank_sender, + wait_to_vote_slot, + &mut first_alpenglow_slot, + &mut tbft_structs, + ) { + error!("Unable to set root: {e}"); + return; + } + } + voting_time.stop(); + + let mut reset_bank_time = Measure::start("reset_bank"); + // Reset onto a fork + if let Some(reset_bank) = reset_bank { + if last_reset == reset_bank.last_blockhash() { + let reset_bank_descendants = Self::get_active_descendants( + reset_bank.slot(), + &progress, + &blockstore, + ); + if reset_bank_descendants != last_reset_bank_descendants { + last_reset_bank_descendants = reset_bank_descendants; + poh_recorder + .write() + .unwrap() + .update_start_bank_active_descendants( + &last_reset_bank_descendants, + ); + } + } else { + info!( + "vote bank: {:?} reset bank: {:?}", + vote_bank.as_ref().map(|(b, switch_fork_decision)| ( + b.slot(), + switch_fork_decision + )), reset_bank.slot(), - heaviest_fork_failures, ); + let fork_progress = progress + .get(&reset_bank.slot()) + .expect("bank to reset to must exist in progress map"); + datapoint_info!( + "blocks_produced", + ("num_blocks_on_fork", fork_progress.num_blocks_on_fork, i64), + ( + "num_dropped_blocks_on_fork", + fork_progress.num_dropped_blocks_on_fork, + i64 + ), + ); + + if my_pubkey != cluster_info.id() && !is_alpenglow_migration_complete { + identity_keypair = cluster_info.keypair().clone(); + let my_old_pubkey = my_pubkey; + my_pubkey = identity_keypair.pubkey(); + + // Load the new identity's tower + tower = match Self::load_tower( + tower_storage.as_ref(), + &my_pubkey, + &vote_account, + &bank_forks, + ) { + Ok(tower) => tower, + Err(err) => { + error!( + "Unable to load new tower when attempting to change \ + identity from {} to {} on set-identity, Exiting: {}", + my_old_pubkey, my_pubkey, err + ); + // drop(_exit) will set the exit flag, eventually tearing down the entire process + return; + } + }; + // Ensure the validator can land votes with the new identity before + // becoming leader + has_new_vote_been_rooted = !wait_for_vote_to_start_leader; + warn!("Identity changed from {} to {}", my_old_pubkey, my_pubkey); + } + + Self::reset_poh_recorder( + &my_pubkey, + &blockstore, + reset_bank.clone(), + &poh_recorder, + &leader_schedule_cache, + ); + last_reset = reset_bank.last_blockhash(); + last_reset_bank_descendants = vec![]; + tpu_has_bank = false; + + if let Some(last_voted_slot) = tower.last_voted_slot() { + // If the current heaviest bank is not a descendant of the last voted slot, + // there must be a partition + partition_info.update( + Self::is_partition_detected( + &ancestors, + last_voted_slot, + heaviest_bank.slot(), + ), + heaviest_bank.slot(), + last_voted_slot, + reset_bank.slot(), + heaviest_fork_failures, + ); + } } } - } - reset_bank_time.stop(); - - let mut start_leader_time = Measure::start("start_leader_time"); - let mut dump_then_repair_correct_slots_time = - Measure::start("dump_then_repair_correct_slots_time"); - // Used for correctness check - let poh_bank = poh_recorder.read().unwrap().bank(); - // Dump any duplicate slots that have been confirmed by the network in - // anticipation of repairing the confirmed version of the slot. - // - // Has to be before `maybe_start_leader()`. Otherwise, `ancestors` and `descendants` - // will be outdated, and we cannot assume `poh_bank` will be in either of these maps. - Self::dump_then_repair_correct_slots( - &mut duplicate_slots_to_repair, - &mut ancestors, - &mut descendants, - &mut progress, - &bank_forks, - &blockstore, - poh_bank.map(|bank| bank.slot()), - &mut purge_repair_slot_counter, - &dumped_slots_sender, - &my_pubkey, - &leader_schedule_cache, - ); - dump_then_repair_correct_slots_time.stop(); + reset_bank_time.stop(); - let mut retransmit_not_propagated_time = - Measure::start("retransmit_not_propagated_time"); - Self::retransmit_latest_unpropagated_leader_slot( - &poh_recorder, - &retransmit_slots_sender, - &mut progress, - ); - retransmit_not_propagated_time.stop(); - - // From this point on, its not safe to use ancestors/descendants since maybe_start_leader - // may add a bank that will not included in either of these maps. - drop(ancestors); - drop(descendants); - if !tpu_has_bank { - Self::maybe_start_leader( - &my_pubkey, + let mut dump_then_repair_correct_slots_time = + Measure::start("dump_then_repair_correct_slots_time"); + // Used for correctness check + let poh_bank = poh_recorder.read().unwrap().bank(); + // Dump any duplicate slots that have been confirmed by the network in + // anticipation of repairing the confirmed version of the slot. + // + // Has to be before `maybe_start_leader()`. Otherwise, `ancestors` and `descendants` + // will be outdated, and we cannot assume `poh_bank` will be in either of these maps. + Self::dump_then_repair_correct_slots( + &mut duplicate_slots_to_repair, + &mut ancestors, + &mut descendants, + &mut progress, &bank_forks, - &poh_recorder, + &blockstore, + poh_bank.map(|bank| bank.slot()), + &mut purge_repair_slot_counter, + &dumped_slots_sender, + &my_pubkey, &leader_schedule_cache, - &rpc_subscriptions, - &slot_status_notifier, - &mut progress, + ); + dump_then_repair_correct_slots_time.stop(); + + let mut retransmit_not_propagated_time = + Measure::start("retransmit_not_propagated_time"); + Self::retransmit_latest_unpropagated_leader_slot( + &poh_recorder, &retransmit_slots_sender, - &mut skipped_slots_info, - &banking_tracer, - has_new_vote_been_rooted, - transaction_status_sender.is_some(), + &mut progress, + ); + retransmit_not_propagated_time.stop(); + + // From this point on, its not safe to use ancestors/descendants since maybe_start_leader + // may add a bank that will not included in either of these maps. + drop(ancestors); + drop(descendants); + replay_timing.update_non_alpenglow( + collect_frozen_banks_time.as_us(), + compute_bank_stats_time.as_us(), + select_vote_and_reset_forks_time.as_us(), + reset_bank_time.as_us(), + voting_time.as_us(), + select_forks_time.as_us(), + compute_slot_stats_time.as_us(), + heaviest_fork_failures_time.as_us(), + u64::from(did_complete_bank), + process_ancestor_hashes_duplicate_slots_time.as_us(), + process_duplicate_confirmed_slots_time.as_us(), + process_unfrozen_gossip_verified_vote_hashes_time.as_us(), + process_popular_pruned_forks_time.as_us(), + process_duplicate_slots_time.as_us(), + dump_then_repair_correct_slots_time.as_us(), + retransmit_not_propagated_time.as_us(), ); - let poh_bank = poh_recorder.read().unwrap().bank(); - if let Some(bank) = poh_bank { - Self::log_leader_change( - &my_pubkey, - bank.slot(), - &mut current_leader, + let mut start_leader_time = Measure::start("start_leader_time"); + if !tpu_has_bank { + Self::maybe_start_leader( &my_pubkey, + &bank_forks, + &poh_recorder, + &leader_schedule_cache, + &rpc_subscriptions, + &slot_status_notifier, + &mut progress, + &retransmit_slots_sender, + &mut skipped_slots_info, + &banking_tracer, + has_new_vote_been_rooted, + transaction_status_sender.is_some(), + &first_alpenglow_slot, + &mut is_alpenglow_migration_complete, ); + + let poh_bank = poh_recorder.read().unwrap().bank(); + if let Some(bank) = poh_bank { + Self::log_leader_change( + &my_pubkey, + bank.slot(), + &mut current_leader, + &my_pubkey, + ); + } } - } - start_leader_time.stop(); + start_leader_time.stop(); + start_leader_time + } else { + let mut start_leader_time = Measure::start("start_leader_time"); + start_leader_time.stop(); + start_leader_time + }; let mut wait_receive_time = Measure::start("wait_receive_time"); if !did_complete_bank { @@ -1192,28 +1342,11 @@ impl ReplayStage { }; } wait_receive_time.stop(); - - replay_timing.update( - collect_frozen_banks_time.as_us(), - compute_bank_stats_time.as_us(), - select_vote_and_reset_forks_time.as_us(), - start_leader_time.as_us(), - reset_bank_time.as_us(), - voting_time.as_us(), - select_forks_time.as_us(), - compute_slot_stats_time.as_us(), + replay_timing.update_common( generate_new_bank_forks_time.as_us(), replay_active_banks_time.as_us(), + start_leader_time.as_us(), wait_receive_time.as_us(), - heaviest_fork_failures_time.as_us(), - u64::from(did_complete_bank), - process_ancestor_hashes_duplicate_slots_time.as_us(), - process_duplicate_confirmed_slots_time.as_us(), - process_unfrozen_gossip_verified_vote_hashes_time.as_us(), - process_popular_pruned_forks_time.as_us(), - process_duplicate_slots_time.as_us(), - dump_then_repair_correct_slots_time.as_us(), - retransmit_not_propagated_time.as_us(), ); } }; @@ -1224,6 +1357,7 @@ impl ReplayStage { Ok(Self { t_replay, + votor, commitment_service, }) } @@ -2004,62 +2138,211 @@ impl ReplayStage { bank_slot, new_leader, msg ); } - } - current_leader.replace(new_leader.to_owned()); - } - - fn check_propagation_for_start_leader( - poh_slot: Slot, - parent_slot: Slot, - progress_map: &ProgressMap, - ) -> bool { - // Assume `NUM_CONSECUTIVE_LEADER_SLOTS` = 4. Then `skip_propagated_check` - // below is true if `poh_slot` is within the same `NUM_CONSECUTIVE_LEADER_SLOTS` - // set of blocks as `latest_leader_slot`. - // - // Example 1 (`poh_slot` directly descended from `latest_leader_slot`): - // - // [B B B B] [B B B latest_leader_slot] poh_slot - // - // Example 2: - // - // [B latest_leader_slot B poh_slot] - // - // In this example, even if there's a block `B` on another fork between - // `poh_slot` and `parent_slot`, because they're in the same - // `NUM_CONSECUTIVE_LEADER_SLOTS` block, we still skip the propagated - // check because it's still within the propagation grace period. - // - // We've already checked in start_leader() that parent_slot hasn't been - // dumped, so we should get it in the progress map. - if let Some(latest_leader_slot) = - progress_map.get_latest_leader_slot_must_exist(parent_slot) - { - let skip_propagated_check = - poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS; - if skip_propagated_check { - return true; + } + current_leader.replace(new_leader.to_owned()); + } + + fn check_propagation_for_start_leader( + poh_slot: Slot, + parent_slot: Slot, + progress_map: &ProgressMap, + ) -> bool { + // Assume `NUM_CONSECUTIVE_LEADER_SLOTS` = 4. Then `skip_propagated_check` + // below is true if `poh_slot` is within the same `NUM_CONSECUTIVE_LEADER_SLOTS` + // set of blocks as `latest_leader_slot`. + // + // Example 1 (`poh_slot` directly descended from `latest_leader_slot`): + // + // [B B B B] [B B B latest_leader_slot] poh_slot + // + // Example 2: + // + // [B latest_leader_slot B poh_slot] + // + // In this example, even if there's a block `B` on another fork between + // `poh_slot` and `parent_slot`, because they're in the same + // `NUM_CONSECUTIVE_LEADER_SLOTS` block, we still skip the propagated + // check because it's still within the propagation grace period. + // + // We've already checked in start_leader() that parent_slot hasn't been + // dumped, so we should get it in the progress map. + if let Some(latest_leader_slot) = + progress_map.get_latest_leader_slot_must_exist(parent_slot) + { + let skip_propagated_check = + poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS; + if skip_propagated_check { + return true; + } + } + + // Note that `is_propagated(parent_slot)` doesn't necessarily check + // propagation of `parent_slot`, it checks propagation of the latest ancestor + // of `parent_slot` (hence the call to `get_latest_leader_slot()` in the + // check above) + progress_map + .get_leader_propagation_slot_must_exist(parent_slot) + .0 + } + + fn should_retransmit(poh_slot: Slot, last_retransmit_slot: &mut Slot) -> bool { + if poh_slot < *last_retransmit_slot + || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS + { + *last_retransmit_slot = poh_slot; + true + } else { + false + } + } + + fn common_maybe_start_leader_checks( + my_pubkey: &Pubkey, + leader_schedule_cache: &LeaderScheduleCache, + parent_bank: &Bank, + bank_forks: &RwLock, + maybe_my_leader_slot: Slot, + has_new_vote_been_rooted: bool, + ) -> bool { + if !parent_bank.is_startup_verification_complete() { + info!("startup verification incomplete, so skipping my leader slot"); + return false; + } + + if bank_forks + .read() + .unwrap() + .get(maybe_my_leader_slot) + .is_some() + { + warn!( + "{} already have bank in forks at {}?", + my_pubkey, maybe_my_leader_slot + ); + return false; + } + trace!( + "{} my_leader_slot {} parent_slot {}", + my_pubkey, + maybe_my_leader_slot, + parent_bank.slot(), + ); + + if let Some(next_leader) = + leader_schedule_cache.slot_leader_at(maybe_my_leader_slot, Some(parent_bank)) + { + if !has_new_vote_been_rooted { + info!("Haven't landed a vote, so skipping my leader slot"); + return false; + } + + trace!( + "{} leader {} at poh slot: {}", + my_pubkey, + next_leader, + maybe_my_leader_slot + ); + + // Poh: I guess I missed my slot + // Alpenglow: It's not my slot yet + if next_leader != *my_pubkey { + return false; + } + } else { + error!("{} No next leader found", my_pubkey); + return false; + } + true + } + + #[allow(clippy::too_many_arguments)] + fn poh_maybe_start_leader( + my_pubkey: &Pubkey, + my_leader_slot: Slot, + parent_bank: &Arc, + poh_recorder: &RwLock, + progress_map: &mut ProgressMap, + skipped_slots_info: &mut SkippedSlotsInfo, + bank_forks: &RwLock, + retransmit_slots_sender: &Sender, + slot_status_notifier: &Option, + rpc_subscriptions: &Arc, + track_transaction_indexes: bool, + banking_tracer: &BankingTracer, + ) -> bool { + let parent_slot = parent_bank.slot(); + if !Self::check_propagation_for_start_leader(my_leader_slot, parent_slot, progress_map) { + let latest_unconfirmed_leader_slot = progress_map + .get_latest_leader_slot_must_exist(parent_slot) + .expect( + "In order for propagated check to fail, latest leader must exist in \ + progress map", + ); + if my_leader_slot != skipped_slots_info.last_skipped_slot { + datapoint_info!( + "replay_stage-skip_leader_slot", + ("slot", my_leader_slot, i64), + ("parent_slot", parent_slot, i64), + ( + "latest_unconfirmed_leader_slot", + latest_unconfirmed_leader_slot, + i64 + ) + ); + progress_map.log_propagated_stats(latest_unconfirmed_leader_slot, bank_forks); + skipped_slots_info.last_skipped_slot = my_leader_slot; + } + if Self::should_retransmit(my_leader_slot, &mut skipped_slots_info.last_retransmit_slot) + { + Self::maybe_retransmit_unpropagated_slots( + "replay_stage-retransmit", + retransmit_slots_sender, + progress_map, + latest_unconfirmed_leader_slot, + ); } + return false; } - // Note that `is_propagated(parent_slot)` doesn't necessarily check - // propagation of `parent_slot`, it checks propagation of the latest ancestor - // of `parent_slot` (hence the call to `get_latest_leader_slot()` in the - // check above) - progress_map - .get_leader_propagation_slot_must_exist(parent_slot) - .0 - } + let root_slot = bank_forks.read().unwrap().root(); + datapoint_info!("replay_stage-my_leader_slot", ("slot", my_leader_slot, i64),); + info!( + "new fork:{} parent:{} (leader) root:{}", + my_leader_slot, parent_slot, root_slot + ); - fn should_retransmit(poh_slot: Slot, last_retransmit_slot: &mut Slot) -> bool { - if poh_slot < *last_retransmit_slot - || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS - { - *last_retransmit_slot = poh_slot; + let root_distance = my_leader_slot - root_slot; + let vote_only_bank = if root_distance > MAX_ROOT_DISTANCE_FOR_VOTE_ONLY { + datapoint_info!("vote-only-bank", ("slot", my_leader_slot, i64)); true } else { false - } + }; + + let tpu_bank = Self::new_bank_from_parent_with_notify( + parent_bank.clone(), + my_leader_slot, + root_slot, + my_pubkey, + rpc_subscriptions, + slot_status_notifier, + NewBankOptions { vote_only_bank }, + ); + // make sure parent is frozen for finalized hashes via the above + // new()-ing of its child bank + banking_tracer.hash_event( + parent_bank.slot(), + &parent_bank.last_blockhash(), + &parent_bank.hash(), + ); + + update_bank_forks_and_poh_recorder_for_new_tpu_bank( + bank_forks, + poh_recorder, + tpu_bank, + track_transaction_indexes, + ); + true } /// Checks if it is time for us to start producing a leader block. @@ -2085,147 +2368,119 @@ impl ReplayStage { banking_tracer: &Arc, has_new_vote_been_rooted: bool, track_transaction_indexes: bool, + first_alpenglow_slot: &Option, + is_alpenglow_migration_complete: &mut bool, ) -> bool { // all the individual calls to poh_recorder.read() are designed to // increase granularity, decrease contention - assert!(!poh_recorder.read().unwrap().has_bank()); - - let (poh_slot, parent_slot) = - match poh_recorder.read().unwrap().reached_leader_slot(my_pubkey) { - PohLeaderStatus::Reached { - poh_slot, - parent_slot, - } => (poh_slot, parent_slot), - PohLeaderStatus::NotReached => { - trace!("{} poh_recorder hasn't reached_leader_slot", my_pubkey); - return false; + let (parent_slot, maybe_my_leader_slot) = { + if !(*is_alpenglow_migration_complete) { + // We need to check regular Poh in these situations + match poh_recorder.read().unwrap().reached_leader_slot(my_pubkey) { + PohLeaderStatus::Reached { + poh_slot, + parent_slot, + } => (parent_slot, poh_slot), + PohLeaderStatus::NotReached => { + trace!("{} poh_recorder hasn't reached_leader_slot", my_pubkey); + return false; + } } - }; - - trace!("{} reached_leader_slot", my_pubkey); - - let Some(parent) = bank_forks.read().unwrap().get(parent_slot) else { - warn!( - "Poh recorder parent slot {parent_slot} is missing from bank_forks. This \ - indicates that we are in the middle of a dump and repair. Unable to start leader" - ); - return false; + } else { + // Migration is already complete voting loop will handle the rest + return false; + } }; - assert!(parent.is_frozen()); - - if !parent.is_startup_verification_complete() { - info!("startup verification incomplete, so skipping my leader slot"); - return false; + // Check if migration is necessary + if let Some(first_alpenglow_slot) = first_alpenglow_slot { + if !(*is_alpenglow_migration_complete) && maybe_my_leader_slot >= *first_alpenglow_slot + { + // Initiate migration + // TODO: need to keep the ticks around for parent slots in previous epoch + // because reset below will delete those ticks + info!( + "initiating alpenglow migration from maybe_start_leader() for slot {}", + maybe_my_leader_slot + ); + Self::initiate_alpenglow_migration(poh_recorder, is_alpenglow_migration_complete); + } } - if bank_forks.read().unwrap().get(poh_slot).is_some() { - warn!("{} already have bank in forks at {}?", my_pubkey, poh_slot); + if *is_alpenglow_migration_complete { + // Alpenglow voting loop will handle leader blocks from now on return false; } - trace!( - "{} poh_slot {} parent_slot {}", - my_pubkey, - poh_slot, - parent_slot - ); - - if let Some(next_leader) = leader_schedule_cache.slot_leader_at(poh_slot, Some(&parent)) { - if !has_new_vote_been_rooted { - info!("Haven't landed a vote, so skipping my leader slot"); - return false; - } - trace!( - "{} leader {} at poh slot: {}", - my_pubkey, - next_leader, - poh_slot - ); + trace!("{} reached_leader_slot", my_pubkey); - // I guess I missed my slot - if next_leader != *my_pubkey { - return false; + // TODO(ashwin): remove alpenglow stuff below + let Some(parent_bank) = bank_forks.read().unwrap().get(parent_slot) else { + if parent_slot >= first_alpenglow_slot.unwrap_or(u64::MAX) { + warn!( + "We have a certificate for {parent_slot} that is not in bank_forks, we are running behind!" + ); + } else { + warn!( + "Poh recorder parent slot {parent_slot} is missing from bank_forks. This \ + indicates that we are in the middle of a dump and repair. Unable to start leader" + ); } + return false; + }; - datapoint_info!( - "replay_stage-new_leader", - ("slot", poh_slot, i64), - ("leader", next_leader.to_string(), String), - ); - - if !Self::check_propagation_for_start_leader(poh_slot, parent_slot, progress_map) { - let latest_unconfirmed_leader_slot = progress_map - .get_latest_leader_slot_must_exist(parent_slot) - .expect( - "In order for propagated check to fail, latest leader must exist in \ - progress map", - ); - if poh_slot != skipped_slots_info.last_skipped_slot { - datapoint_info!( - "replay_stage-skip_leader_slot", - ("slot", poh_slot, i64), - ("parent_slot", parent_slot, i64), - ( - "latest_unconfirmed_leader_slot", - latest_unconfirmed_leader_slot, - i64 - ) - ); - progress_map.log_propagated_stats(latest_unconfirmed_leader_slot, bank_forks); - skipped_slots_info.last_skipped_slot = poh_slot; - } - if Self::should_retransmit(poh_slot, &mut skipped_slots_info.last_retransmit_slot) { - Self::maybe_retransmit_unpropagated_slots( - "replay_stage-retransmit", - retransmit_slots_sender, - progress_map, - latest_unconfirmed_leader_slot, - ); - } - return false; - } + if parent_slot < *first_alpenglow_slot.as_ref().unwrap_or(&u64::MAX) { + assert!(parent_bank.is_frozen()); + } - let root_slot = bank_forks.read().unwrap().root(); - datapoint_info!("replay_stage-my_leader_slot", ("slot", poh_slot, i64),); - info!( - "new fork:{} parent:{} (leader) root:{}", - poh_slot, parent_slot, root_slot - ); + // In Alpenglow we can potentially get a notarization certificate for a slot + // we haven't yet replayed + if !parent_bank.is_frozen() { + return false; + } - let root_distance = poh_slot - root_slot; - let vote_only_bank = if root_distance > MAX_ROOT_DISTANCE_FOR_VOTE_ONLY { - datapoint_info!("vote-only-bank", ("slot", poh_slot, i64)); - true - } else { - false - }; + if !Self::common_maybe_start_leader_checks( + my_pubkey, + leader_schedule_cache, + &parent_bank, + bank_forks, + maybe_my_leader_slot, + has_new_vote_been_rooted, + ) { + return false; + } - let tpu_bank = Self::new_bank_from_parent_with_notify( - parent.clone(), - poh_slot, - root_slot, + let my_leader_slot = maybe_my_leader_slot; + if my_leader_slot < *first_alpenglow_slot.as_ref().unwrap_or(&u64::MAX) { + // Alpenglow is not enabled yet for my leader slot, do the regular checks + if !Self::poh_maybe_start_leader( my_pubkey, - rpc_subscriptions, - slot_status_notifier, - NewBankOptions { vote_only_bank }, - ); - // make sure parent is frozen for finalized hashes via the above - // new()-ing of its child bank - banking_tracer.hash_event(parent.slot(), &parent.last_blockhash(), &parent.hash()); - - update_bank_forks_and_poh_recorder_for_new_tpu_bank( - bank_forks, + my_leader_slot, + &parent_bank, poh_recorder, - tpu_bank, + progress_map, + skipped_slots_info, + bank_forks, + retransmit_slots_sender, + slot_status_notifier, + rpc_subscriptions, track_transaction_indexes, - ); - true + banking_tracer, + ) { + return false; + } } else { - error!("{} No next leader found", my_pubkey); - false + return false; } + + // Starting my leader slot was a success + datapoint_info!( + "replay_stage-new_leader", + ("slot", my_leader_slot, i64), + ("leader", my_pubkey.to_string(), String), + ); + true } #[allow(clippy::too_many_arguments)] @@ -2276,14 +2531,11 @@ impl ReplayStage { err: &BlockstoreProcessorError, rpc_subscriptions: &Arc, slot_status_notifier: &Option, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &DuplicateConfirmedSlots, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, progress: &mut ProgressMap, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, duplicate_slots_to_repair: &mut DuplicateSlotsToRepair, ancestor_hashes_replay_update_sender: &AncestorHashesReplayUpdateSender, purge_repair_slot_counter: &mut PurgeRepairSlotCounter, + tbft_structs: &mut Option<&mut TowerBFTStructures>, ) { // Do not remove from progress map when marking dead! Needed by // `process_duplicate_confirmed_slots()` @@ -2331,35 +2583,20 @@ impl ReplayStage { timestamp: timestamp(), }); - let dead_state = DeadState::new_from_state( - slot, - duplicate_slots_tracker, - duplicate_confirmed_slots, + if let Some(TowerBFTStructures { heaviest_subtree_fork_choice, - epoch_slots_frozen_slots, - ); - check_slot_agrees_with_cluster( - slot, - root, - blockstore, duplicate_slots_tracker, + duplicate_confirmed_slots, epoch_slots_frozen_slots, - heaviest_subtree_fork_choice, - duplicate_slots_to_repair, - ancestor_hashes_replay_update_sender, - purge_repair_slot_counter, - SlotStateUpdate::Dead(dead_state), - ); - - // If we previously marked this slot as duplicate in blockstore, let the state machine know - if !duplicate_slots_tracker.contains(&slot) && blockstore.get_duplicate_slot(slot).is_some() + .. + }) = tbft_structs { - let duplicate_state = DuplicateState::new_from_state( + let dead_state = DeadState::new_from_state( slot, + duplicate_slots_tracker, duplicate_confirmed_slots, heaviest_subtree_fork_choice, - || true, - || None, + epoch_slots_frozen_slots, ); check_slot_agrees_with_cluster( slot, @@ -2371,8 +2608,33 @@ impl ReplayStage { duplicate_slots_to_repair, ancestor_hashes_replay_update_sender, purge_repair_slot_counter, - SlotStateUpdate::Duplicate(duplicate_state), + SlotStateUpdate::Dead(dead_state), ); + + // If we previously marked this slot as duplicate in blockstore, let the state machine know + if !duplicate_slots_tracker.contains(&slot) + && blockstore.get_duplicate_slot(slot).is_some() + { + let duplicate_state = DuplicateState::new_from_state( + slot, + duplicate_confirmed_slots, + heaviest_subtree_fork_choice, + || true, + || None, + ); + check_slot_agrees_with_cluster( + slot, + root, + blockstore, + duplicate_slots_tracker, + epoch_slots_frozen_slots, + heaviest_subtree_fork_choice, + duplicate_slots_to_repair, + ancestor_hashes_replay_update_sender, + purge_repair_slot_counter, + SlotStateUpdate::Duplicate(duplicate_state), + ); + } } } @@ -2388,22 +2650,19 @@ impl ReplayStage { authorized_voter_keypairs: &[Arc], blockstore: &Blockstore, leader_schedule_cache: &Arc, - lockouts_sender: &Sender, + lockouts_sender: &Sender, accounts_background_request_sender: &AbsRequestSender, rpc_subscriptions: &Arc, block_commitment_cache: &Arc>, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, bank_notification_sender: &Option, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &mut DuplicateConfirmedSlots, - unfrozen_gossip_verified_vote_hashes: &mut UnfrozenGossipVerifiedVoteHashes, vote_signatures: &mut Vec, has_new_vote_been_rooted: &mut bool, replay_timing: &mut ReplayLoopTiming, voting_sender: &Sender, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, drop_bank_sender: &Sender>, wait_to_vote_slot: Option, + first_alpenglow_slot: &mut Option, + tbft_structs: &mut TowerBFTStructures, ) -> Result<(), SetRootError> { if bank.is_empty() { datapoint_info!("replay_stage-voted_empty_bank", ("slot", bank.slot(), i64)); @@ -2412,8 +2671,29 @@ impl ReplayStage { let new_root = tower.record_bank_vote(bank); if let Some(new_root) = new_root { + if first_alpenglow_slot.is_none() { + *first_alpenglow_slot = bank_forks + .read() + .unwrap() + .root_bank() + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()); + if let Some(first_alpenglow_slot) = first_alpenglow_slot { + info!( + "alpenglow feature detected in root bank {}, to be enabled on slot {}", + new_root, first_alpenglow_slot + ); + } + } + let highest_super_majority_root = Some( + block_commitment_cache + .read() + .unwrap() + .highest_super_majority_root(), + ); Self::check_and_handle_new_root( - bank, + &identity_keypair.pubkey(), + bank.parent_slot(), new_root, bank_forks, progress, @@ -2421,16 +2701,12 @@ impl ReplayStage { leader_schedule_cache, accounts_background_request_sender, rpc_subscriptions, - block_commitment_cache, - heaviest_subtree_fork_choice, + highest_super_majority_root, bank_notification_sender, - duplicate_slots_tracker, - duplicate_confirmed_slots, - unfrozen_gossip_verified_vote_hashes, has_new_vote_been_rooted, vote_signatures, - epoch_slots_frozen_slots, drop_bank_sender, + tbft_structs, )?; } @@ -2512,17 +2788,27 @@ impl ReplayStage { } Some(vote_account) => vote_account, }; - let vote_state = vote_account.vote_state(); - if vote_state.node_pubkey != node_keypair.pubkey() { + let vote_state_view = match vote_account.vote_state_view() { + None => { + warn!( + "Vote account {} does not have a vote state. Unable to vote", + vote_account_pubkey, + ); + return GenerateVoteTxResult::Failed; + } + Some(vote_state_view) => vote_state_view, + }; + if vote_state_view.node_pubkey() != &node_keypair.pubkey() { info!( "Vote account node_pubkey mismatch: {} (expected: {}). Unable to vote", - vote_state.node_pubkey, + vote_state_view.node_pubkey(), node_keypair.pubkey() ); return GenerateVoteTxResult::HotSpare; } - let Some(authorized_voter_pubkey) = vote_state.get_authorized_voter(bank.epoch()) else { + let Some(authorized_voter_pubkey) = vote_state_view.get_authorized_voter(bank.epoch()) + else { warn!( "Vote account {} has no authorized voter for epoch {}. Unable to vote", vote_account_pubkey, @@ -2533,7 +2819,7 @@ impl ReplayStage { let authorized_voter_keypair = match authorized_voter_keypairs .iter() - .find(|keypair| keypair.pubkey() == authorized_voter_pubkey) + .find(|keypair| &keypair.pubkey() == authorized_voter_pubkey) { None => { warn!( @@ -2816,9 +3102,9 @@ impl ReplayStage { root: Slot, total_stake: Stake, node_vote_state: (Pubkey, TowerVoteState), - lockouts_sender: &Sender, + lockouts_sender: &Sender, ) { - if let Err(e) = lockouts_sender.send(CommitmentAggregationData::new( + if let Err(e) = lockouts_sender.send(TowerCommitmentAggregationData::new( bank, root, total_stake, @@ -3003,17 +3289,17 @@ impl ReplayStage { debug!("bank_slot {:?} is marked dead", bank_slot); replay_result.is_slot_dead = true; } else { - let bank = bank_forks - .read() - .unwrap() - .get_with_scheduler(bank_slot) - .unwrap(); + let Some(bank) = bank_forks.read().unwrap().get_with_scheduler(bank_slot) else { + info!("Abandoning replay of unrooted slot {bank_slot}"); + return replay_result; + }; let parent_slot = bank.parent_slot(); let prev_leader_slot = progress.get_bank_prev_leader_slot(&bank); let (num_blocks_on_fork, num_dropped_blocks_on_fork) = { - let stats = progress - .get(&parent_slot) - .expect("parent of active bank must exist in progress map"); + let Some(stats) = progress.get(&parent_slot) else { + info!("Abandoning replay of unrooted slot {bank_slot}"); + return replay_result; + }; let num_blocks_on_fork = stats.num_blocks_on_fork + 1; let new_dropped_blocks = bank.slot() - parent_slot - 1; let num_dropped_blocks_on_fork = @@ -3055,6 +3341,20 @@ impl ReplayStage { replay_result } + fn initiate_alpenglow_migration( + poh_recorder: &RwLock, + is_alpenglow_migration_complete: &mut bool, + ) { + info!("initiating alpenglow migration from replay"); + poh_recorder.write().unwrap().is_alpenglow_enabled = true; + while !poh_recorder.read().unwrap().use_alpenglow_tick_producer { + // Wait for PohService to migrate to alpenglow tick producer + thread::sleep(Duration::from_millis(10)); + } + *is_alpenglow_migration_complete = true; + info!("alpenglow migration complete!"); + } + #[allow(clippy::too_many_arguments)] fn process_replay_results( blockstore: &Blockstore, @@ -3062,14 +3362,9 @@ impl ReplayStage { progress: &mut ProgressMap, transaction_status_sender: Option<&TransactionStatusSender>, block_meta_sender: Option<&BlockMetaSender>, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, bank_notification_sender: &Option, rpc_subscriptions: &Arc, slot_status_notifier: &Option, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &DuplicateConfirmedSlots, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, - unfrozen_gossip_verified_vote_hashes: &mut UnfrozenGossipVerifiedVoteHashes, latest_validator_votes_for_frozen_banks: &mut LatestValidatorVotesForFrozenBanks, cluster_slots_update_sender: &ClusterSlotsUpdateSender, cost_update_sender: &Sender, @@ -3079,22 +3374,26 @@ impl ReplayStage { replay_result_vec: &[ReplaySlotFromBlockstore], purge_repair_slot_counter: &mut PurgeRepairSlotCounter, my_pubkey: &Pubkey, - ) -> bool { + first_alpenglow_slot: Option, + poh_recorder: &RwLock, + is_alpenglow_migration_complete: &mut bool, + mut tbft_structs: Option<&mut TowerBFTStructures>, + votor_event_sender: &VotorEventSender, + ) -> Vec { // TODO: See if processing of blockstore replay results and bank completion can be made thread safe. - let mut did_complete_bank = false; let mut tx_count = 0; let mut execute_timings = ExecuteTimings::default(); + let mut new_frozen_slots = vec![]; for replay_result in replay_result_vec { if replay_result.is_slot_dead { continue; } let bank_slot = replay_result.bank_slot; - let bank = &bank_forks - .read() - .unwrap() - .get_with_scheduler(bank_slot) - .unwrap(); + let Some(bank) = &bank_forks.read().unwrap().get_with_scheduler(bank_slot) else { + info!("Abandoning replay of unrooted slot {bank_slot}"); + continue; + }; if let Some(replay_result) = &replay_result.replay_result { match replay_result { Ok(replay_tx_count) => tx_count += replay_tx_count, @@ -3107,14 +3406,11 @@ impl ReplayStage { err, rpc_subscriptions, slot_status_notifier, - duplicate_slots_tracker, - duplicate_confirmed_slots, - epoch_slots_frozen_slots, progress, - heaviest_subtree_fork_choice, duplicate_slots_to_repair, ancestor_hashes_replay_update_sender, purge_repair_slot_counter, + &mut tbft_structs, ); // don't try to run the below logic to check if the bank is completed continue; @@ -3124,6 +3420,18 @@ impl ReplayStage { assert_eq!(bank_slot, bank.slot()); if bank.is_complete() { + if let Some(first_alpenglow_slot) = first_alpenglow_slot { + if !*is_alpenglow_migration_complete && bank.slot() >= first_alpenglow_slot { + info!( + "initiating alpenglow migration from replaying bank {}", + bank.slot() + ); + Self::initiate_alpenglow_migration( + poh_recorder, + is_alpenglow_migration_complete, + ); + } + } let mut bank_complete_time = Measure::start("bank_complete_time"); let bank_progress = progress .get_mut(&bank.slot()) @@ -3156,30 +3464,31 @@ impl ReplayStage { &BlockstoreProcessorError::InvalidTransaction(err), rpc_subscriptions, slot_status_notifier, - duplicate_slots_tracker, - duplicate_confirmed_slots, - epoch_slots_frozen_slots, progress, - heaviest_subtree_fork_choice, duplicate_slots_to_repair, ancestor_hashes_replay_update_sender, purge_repair_slot_counter, + &mut tbft_structs, ); // don't try to run the remaining normal processing for the completed bank continue; } } - let block_id = if bank.collector_id() != my_pubkey { - // If the block does not have at least DATA_SHREDS_PER_FEC_BLOCK correctly retransmitted - // shreds in the last FEC set, mark it dead. No reason to perform this check on our leader block. - match blockstore.check_last_fec_set_and_get_block_id( - bank.slot(), - bank.hash(), - &bank.feature_set, - ) { - Ok(block_id) => block_id, - Err(result_err) => { + // If the block does not have at least DATA_SHREDS_PER_FEC_BLOCK correctly retransmitted + // shreds in the last FEC set, mark it dead. + let block_id = match blockstore.check_last_fec_set_and_get_block_id( + bank.slot(), + bank.hash(), + false, + &bank.feature_set, + ) { + Ok(block_id) => block_id, + Err(result_err) => { + if bank.collector_id() == my_pubkey { + // Our leader block has not finished shredding + None + } else { let root = bank_forks.read().unwrap().root(); Self::mark_dead_slot( blockstore, @@ -3188,22 +3497,20 @@ impl ReplayStage { &result_err, rpc_subscriptions, slot_status_notifier, - duplicate_slots_tracker, - duplicate_confirmed_slots, - epoch_slots_frozen_slots, progress, - heaviest_subtree_fork_choice, duplicate_slots_to_repair, ancestor_hashes_replay_update_sender, purge_repair_slot_counter, + &mut tbft_structs, ); continue; } } - } else { - None }; - bank.set_block_id(block_id); + + if bank.block_id().is_none() { + bank.set_block_id(block_id); + } let r_replay_stats = replay_stats.read().unwrap(); let replay_progress = bank_progress.replay_progress.clone(); @@ -3214,7 +3521,7 @@ impl ReplayStage { bank.slot(), r_replay_stats.batch_execute.totals ); - did_complete_bank = true; + new_frozen_slots.push(bank.slot()); let _ = cluster_slots_update_sender.send(vec![bank_slot]); if let Some(transaction_status_sender) = transaction_status_sender { transaction_status_sender.send_transaction_status_freeze_message(bank); @@ -3238,42 +3545,28 @@ impl ReplayStage { // Needs to be updated before `check_slot_agrees_with_cluster()` so that // any updates in `check_slot_agrees_with_cluster()` on fork choice take // effect - heaviest_subtree_fork_choice.add_new_leaf_slot( - (bank.slot(), bank.hash()), - Some((bank.parent_slot(), bank.parent_hash())), - ); - heaviest_subtree_fork_choice.maybe_print_state(); + bank_progress.fork_stats.bank_hash = Some(bank.hash()); - let bank_frozen_state = BankFrozenState::new_from_state( - bank.slot(), - bank.hash(), - duplicate_slots_tracker, - duplicate_confirmed_slots, + if let Some(TowerBFTStructures { heaviest_subtree_fork_choice, - epoch_slots_frozen_slots, - ); - check_slot_agrees_with_cluster( - bank.slot(), - bank_forks.read().unwrap().root(), - blockstore, duplicate_slots_tracker, + duplicate_confirmed_slots, epoch_slots_frozen_slots, - heaviest_subtree_fork_choice, - duplicate_slots_to_repair, - ancestor_hashes_replay_update_sender, - purge_repair_slot_counter, - SlotStateUpdate::BankFrozen(bank_frozen_state), - ); - // If we previously marked this slot as duplicate in blockstore, let the state machine know - if !duplicate_slots_tracker.contains(&bank.slot()) - && blockstore.get_duplicate_slot(bank.slot()).is_some() + .. + }) = &mut tbft_structs { - let duplicate_state = DuplicateState::new_from_state( + heaviest_subtree_fork_choice.add_new_leaf_slot( + (bank.slot(), bank.hash()), + Some((bank.parent_slot(), bank.parent_hash())), + ); + heaviest_subtree_fork_choice.maybe_print_state(); + let bank_frozen_state = BankFrozenState::new_from_state( bank.slot(), + bank.hash(), + duplicate_slots_tracker, duplicate_confirmed_slots, heaviest_subtree_fork_choice, - || false, - || Some(bank.hash()), + epoch_slots_frozen_slots, ); check_slot_agrees_with_cluster( bank.slot(), @@ -3285,9 +3578,49 @@ impl ReplayStage { duplicate_slots_to_repair, ancestor_hashes_replay_update_sender, purge_repair_slot_counter, - SlotStateUpdate::Duplicate(duplicate_state), + SlotStateUpdate::BankFrozen(bank_frozen_state), ); + // If we previously marked this slot as duplicate in blockstore, let the state machine know + if !duplicate_slots_tracker.contains(&bank.slot()) + && blockstore.get_duplicate_slot(bank.slot()).is_some() + { + let duplicate_state = DuplicateState::new_from_state( + bank.slot(), + duplicate_confirmed_slots, + heaviest_subtree_fork_choice, + || false, + || Some(bank.hash()), + ); + check_slot_agrees_with_cluster( + bank.slot(), + bank_forks.read().unwrap().root(), + blockstore, + duplicate_slots_tracker, + epoch_slots_frozen_slots, + heaviest_subtree_fork_choice, + duplicate_slots_to_repair, + ancestor_hashes_replay_update_sender, + purge_repair_slot_counter, + SlotStateUpdate::Duplicate(duplicate_state), + ); + } + } + + // For leader banks: + // 1) Replay finishes before shredding, broadcast_stage will take care of + // notifying votor + // 2) Shredding finishes before replay, we notify here + // + // For non leader banks (2) is always true, so notify here + if *is_alpenglow_migration_complete && bank.block_id().is_some() { + // Leader blocks will not have a block id, broadcast stage will + // take care of notifying the voting loop + let _ = votor_event_sender.send(VotorEvent::Block(CompletedBlock { + slot: bank.slot(), + bank: bank.clone_without_scheduler(), + })); } + if let Some(sender) = bank_notification_sender { sender .sender @@ -3297,9 +3630,10 @@ impl ReplayStage { blockstore_processor::send_block_meta(bank, block_meta_sender); let bank_hash = bank.hash(); - if let Some(new_frozen_voters) = - unfrozen_gossip_verified_vote_hashes.remove_slot_hash(bank.slot(), &bank_hash) - { + if let Some(new_frozen_voters) = tbft_structs.as_mut().and_then(|tbft| { + tbft.unfrozen_gossip_verified_vote_hashes + .remove_slot_hash(bank.slot(), &bank_hash) + }) { for pubkey in new_frozen_voters { latest_validator_votes_for_frozen_banks.check_add_vote( pubkey, @@ -3348,7 +3682,7 @@ impl ReplayStage { } } - did_complete_bank + new_frozen_slots } #[allow(clippy::too_many_arguments)] @@ -3362,15 +3696,10 @@ impl ReplayStage { block_meta_sender: Option<&BlockMetaSender>, entry_notification_sender: Option<&EntryNotifierSender>, verify_recyclers: &VerifyRecyclers, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, replay_vote_sender: &ReplayVoteSender, bank_notification_sender: &Option, rpc_subscriptions: &Arc, slot_status_notifier: &Option, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &DuplicateConfirmedSlots, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, - unfrozen_gossip_verified_vote_hashes: &mut UnfrozenGossipVerifiedVoteHashes, latest_validator_votes_for_frozen_banks: &mut LatestValidatorVotesForFrozenBanks, cluster_slots_update_sender: &ClusterSlotsUpdateSender, cost_update_sender: &Sender, @@ -3383,7 +3712,12 @@ impl ReplayStage { replay_tx_thread_pool: &ThreadPool, prioritization_fee_cache: &PrioritizationFeeCache, purge_repair_slot_counter: &mut PurgeRepairSlotCounter, - ) -> bool /* completed a bank */ { + poh_recorder: &RwLock, + first_alpenglow_slot: Option, + tbft_structs: Option<&mut TowerBFTStructures>, + is_alpenglow_migration_complete: &mut bool, + votor_event_sender: &VotorEventSender, + ) -> Vec /* completed slots */ { let active_bank_slots = bank_forks.read().unwrap().active_bank_slots(); let num_active_banks = active_bank_slots.len(); trace!( @@ -3392,7 +3726,7 @@ impl ReplayStage { active_bank_slots ); if active_bank_slots.is_empty() { - return false; + return vec![]; } let replay_result_vec = match replay_mode { @@ -3445,14 +3779,9 @@ impl ReplayStage { progress, transaction_status_sender, block_meta_sender, - heaviest_subtree_fork_choice, bank_notification_sender, rpc_subscriptions, slot_status_notifier, - duplicate_slots_tracker, - duplicate_confirmed_slots, - epoch_slots_frozen_slots, - unfrozen_gossip_verified_vote_hashes, latest_validator_votes_for_frozen_banks, cluster_slots_update_sender, cost_update_sender, @@ -3462,6 +3791,11 @@ impl ReplayStage { &replay_result_vec, purge_repair_slot_counter, my_pubkey, + first_alpenglow_slot, + poh_recorder, + is_alpenglow_migration_complete, + tbft_structs, + votor_event_sender, ) } @@ -3577,7 +3911,10 @@ impl ReplayStage { let Some(vote_account) = bank.get_vote_account(my_vote_pubkey) else { return; }; - let mut bank_vote_state = TowerVoteState::from(vote_account.vote_state().clone()); + let Some(vote_state_view) = vote_account.vote_state_view() else { + return; + }; + let mut bank_vote_state = TowerVoteState::from(vote_state_view); if bank_vote_state.last_voted_slot() <= tower.vote_state.last_voted_slot() { return; } @@ -3951,11 +4288,10 @@ impl ReplayStage { if prog.fork_stats.duplicate_confirmed_hash.is_some() { continue; } - let bank = bank_forks - .read() - .unwrap() - .get(*slot) - .expect("bank in progress must exist in BankForks"); + // TODO(ashwin): expect once we share progress + let Some(bank) = bank_forks.read().unwrap().get(*slot) else { + continue; + }; let duration = prog .replay_stats .read() @@ -3989,8 +4325,12 @@ impl ReplayStage { } #[allow(clippy::too_many_arguments)] + /// A wrapper around `root_utils::check_and_handle_new_root` which: + /// - calls into `root_utils::set_bank_forks_root` + /// - Executes `set_progress_and_tower_bft_root` to cleanup tower bft structs and the progress map fn check_and_handle_new_root( - vote_bank: &Bank, + my_pubkey: &Pubkey, + parent_slot: Slot, new_root: Slot, bank_forks: &RwLock, progress: &mut ProgressMap, @@ -3998,139 +4338,89 @@ impl ReplayStage { leader_schedule_cache: &Arc, accounts_background_request_sender: &AbsRequestSender, rpc_subscriptions: &Arc, - block_commitment_cache: &Arc>, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, + highest_super_majority_root: Option, bank_notification_sender: &Option, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &mut DuplicateConfirmedSlots, - unfrozen_gossip_verified_vote_hashes: &mut UnfrozenGossipVerifiedVoteHashes, has_new_vote_been_rooted: &mut bool, voted_signatures: &mut Vec, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, drop_bank_sender: &Sender>, + tbft_structs: &mut TowerBFTStructures, ) -> Result<(), SetRootError> { - // get the root bank before squash - let root_bank = bank_forks - .read() - .unwrap() - .get(new_root) - .expect("Root bank doesn't exist"); - let mut rooted_banks = root_bank.parents(); - let oldest_parent = rooted_banks.last().map(|last| last.parent_slot()); - rooted_banks.push(root_bank.clone()); - let rooted_slots: Vec<_> = rooted_banks.iter().map(|bank| bank.slot()).collect(); - // The following differs from rooted_slots by including the parent slot of the oldest parent bank. - let rooted_slots_with_parents = bank_notification_sender - .as_ref() - .is_some_and(|sender| sender.should_send_parents) - .then(|| { - let mut new_chain = rooted_slots.clone(); - new_chain.push(oldest_parent.unwrap_or_else(|| vote_bank.parent_slot())); - new_chain - }); - - // Call leader schedule_cache.set_root() before blockstore.set_root() because - // bank_forks.root is consumed by repair_service to update gossip, so we don't want to - // get shreds for repair on gossip before we update leader schedule, otherwise they may - // get dropped. - leader_schedule_cache.set_root(rooted_banks.last().unwrap()); - blockstore - .set_roots(rooted_slots.iter()) - .expect("Ledger set roots failed"); - let highest_super_majority_root = Some( - block_commitment_cache - .read() - .unwrap() - .highest_super_majority_root(), - ); - Self::handle_new_root( + root_utils::check_and_handle_new_root( + parent_slot, new_root, - bank_forks, - progress, accounts_background_request_sender, highest_super_majority_root, + bank_notification_sender, + drop_bank_sender, + blockstore, + leader_schedule_cache, + bank_forks, + rpc_subscriptions, + my_pubkey, + has_new_vote_been_rooted, + voted_signatures, + move |bank_forks| { + Self::set_progress_and_tower_bft_root(new_root, bank_forks, progress, tbft_structs) + }, + ) + } + + // To avoid code duplication and keep compatibility with alpenglow, we add this + // extra callback in the rooting path. This happens immediately after setting the bank forks root + fn set_progress_and_tower_bft_root( + new_root: Slot, + bank_forks: &BankForks, + progress: &mut ProgressMap, + tbft_structs: &mut TowerBFTStructures, + ) { + progress.handle_new_root(bank_forks); + let TowerBFTStructures { heaviest_subtree_fork_choice, duplicate_slots_tracker, duplicate_confirmed_slots, unfrozen_gossip_verified_vote_hashes, - has_new_vote_been_rooted, - voted_signatures, epoch_slots_frozen_slots, - drop_bank_sender, - )?; - blockstore.slots_stats.mark_rooted(new_root); - rpc_subscriptions.notify_roots(rooted_slots); - if let Some(sender) = bank_notification_sender { - sender - .sender - .send(BankNotification::NewRootBank(root_bank)) - .unwrap_or_else(|err| warn!("bank_notification_sender failed: {:?}", err)); - - if let Some(new_chain) = rooted_slots_with_parents { - sender - .sender - .send(BankNotification::NewRootedChain(new_chain)) - .unwrap_or_else(|err| warn!("bank_notification_sender failed: {:?}", err)); - } - } - info!("new root {}", new_root); - Ok(()) + .. + } = tbft_structs; + heaviest_subtree_fork_choice.set_tree_root((new_root, bank_forks.root_bank().hash())); + *duplicate_slots_tracker = duplicate_slots_tracker.split_off(&new_root); + // duplicate_slots_tracker now only contains entries >= `new_root` + + *duplicate_confirmed_slots = duplicate_confirmed_slots.split_off(&new_root); + // gossip_confirmed_slots now only contains entries >= `new_root` + + unfrozen_gossip_verified_vote_hashes.set_root(new_root); + *epoch_slots_frozen_slots = epoch_slots_frozen_slots.split_off(&new_root); + // epoch_slots_frozen_slots now only contains entries >= `new_root` } #[allow(clippy::too_many_arguments)] + /// A wrapper around `root_utils::set_bank_forks_root` which additionally: + /// - Executes `set_progress_and_tower_bft_root` to cleanup tower bft structs and the progress map pub fn handle_new_root( new_root: Slot, bank_forks: &RwLock, progress: &mut ProgressMap, accounts_background_request_sender: &AbsRequestSender, highest_super_majority_root: Option, - heaviest_subtree_fork_choice: &mut HeaviestSubtreeForkChoice, - duplicate_slots_tracker: &mut DuplicateSlotsTracker, - duplicate_confirmed_slots: &mut DuplicateConfirmedSlots, - unfrozen_gossip_verified_vote_hashes: &mut UnfrozenGossipVerifiedVoteHashes, has_new_vote_been_rooted: &mut bool, voted_signatures: &mut Vec, - epoch_slots_frozen_slots: &mut EpochSlotsFrozenSlots, drop_bank_sender: &Sender>, + tbft_structs: &mut TowerBFTStructures, ) -> Result<(), SetRootError> { - bank_forks.read().unwrap().prune_program_cache(new_root); - let removed_banks = bank_forks.write().unwrap().set_root( + root_utils::set_bank_forks_root( new_root, + bank_forks, accounts_background_request_sender, highest_super_majority_root, + has_new_vote_been_rooted, + voted_signatures, + drop_bank_sender, + move |bank_forks| { + Self::set_progress_and_tower_bft_root(new_root, bank_forks, progress, tbft_structs) + }, )?; - - drop_bank_sender - .send(removed_banks) - .unwrap_or_else(|err| warn!("bank drop failed: {:?}", err)); - - // Dropping the bank_forks write lock and reacquiring as a read lock is - // safe because updates to bank_forks are only made by a single thread. - let r_bank_forks = bank_forks.read().unwrap(); - let new_root_bank = &r_bank_forks[new_root]; - if !*has_new_vote_been_rooted { - for signature in voted_signatures.iter() { - if new_root_bank.get_signature_status(signature).is_some() { - *has_new_vote_been_rooted = true; - break; - } - } - if *has_new_vote_been_rooted { - std::mem::take(voted_signatures); - } - } - progress.handle_new_root(&r_bank_forks); - heaviest_subtree_fork_choice.set_tree_root((new_root, r_bank_forks.root_bank().hash())); - *duplicate_slots_tracker = duplicate_slots_tracker.split_off(&new_root); - // duplicate_slots_tracker now only contains entries >= `new_root` - - *duplicate_confirmed_slots = duplicate_confirmed_slots.split_off(&new_root); - // gossip_confirmed_slots now only contains entries >= `new_root` - - unfrozen_gossip_verified_vote_hashes.set_root(new_root); - *epoch_slots_frozen_slots = epoch_slots_frozen_slots.split_off(&new_root); Ok(()) - // epoch_slots_frozen_slots now only contains entries >= `new_root` } fn generate_new_bank_forks( @@ -4196,6 +4486,8 @@ impl ReplayStage { slot_status_notifier, NewBankOptions::default(), ); + // Set ticks for received banks, block creation loop will take care of leader banks + blockstore_processor::set_alpenglow_ticks(&child_bank); let empty: Vec = vec![]; Self::update_fork_propagated_threshold_from_votes( progress, @@ -4212,9 +4504,18 @@ impl ReplayStage { let mut generate_new_bank_forks_write_lock = Measure::start("generate_new_bank_forks_write_lock"); - let mut forks = bank_forks.write().unwrap(); - for (_, bank) in new_banks { - forks.insert(bank); + if !new_banks.is_empty() { + let mut forks = bank_forks.write().unwrap(); + let root = forks.root(); + for (slot, bank) in new_banks { + if slot < root { + continue; + } + if forks.get(bank.parent_slot()).is_none() { + continue; + } + forks.insert(bank); + } } generate_new_bank_forks_write_lock.stop(); saturating_add_assign!( @@ -4235,7 +4536,7 @@ impl ReplayStage { ); } - fn new_bank_from_parent_with_notify( + pub(crate) fn new_bank_from_parent_with_notify( parent: Arc, slot: u64, root_slot: u64, @@ -4317,6 +4618,7 @@ impl ReplayStage { pub fn join(self) -> thread::Result<()> { self.commitment_service.join()?; + self.votor.join()?; self.t_replay.join().map(|_| ()) } } @@ -4664,45 +4966,46 @@ pub(crate) mod tests { let root_hash = root_bank.hash(); bank_forks.write().unwrap().insert(root_bank); - let mut heaviest_subtree_fork_choice = HeaviestSubtreeForkChoice::new((root, root_hash)); - let mut progress = ProgressMap::default(); for i in 0..=root { progress.insert(i, ForkProgress::new(Hash::default(), None, None, 0, 0)); } - let mut duplicate_slots_tracker: DuplicateSlotsTracker = + let duplicate_slots_tracker: DuplicateSlotsTracker = vec![root - 1, root, root + 1].into_iter().collect(); - let mut duplicate_confirmed_slots: DuplicateConfirmedSlots = vec![root - 1, root, root + 1] + let duplicate_confirmed_slots: DuplicateConfirmedSlots = vec![root - 1, root, root + 1] .into_iter() .map(|s| (s, Hash::default())) .collect(); - let mut unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes = + let unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes = UnfrozenGossipVerifiedVoteHashes { votes_per_slot: vec![root - 1, root, root + 1] .into_iter() .map(|s| (s, HashMap::new())) .collect(), }; - let mut epoch_slots_frozen_slots: EpochSlotsFrozenSlots = vec![root - 1, root, root + 1] + let epoch_slots_frozen_slots: EpochSlotsFrozenSlots = vec![root - 1, root, root + 1] .into_iter() .map(|slot| (slot, Hash::default())) .collect(); let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let mut tbft_structs = TowerBFTStructures { + heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice::new((root, root_hash)), + duplicate_slots_tracker, + duplicate_confirmed_slots, + unfrozen_gossip_verified_vote_hashes, + epoch_slots_frozen_slots, + }; ReplayStage::handle_new_root( root, &bank_forks, &mut progress, &AbsRequestSender::default(), None, - &mut heaviest_subtree_fork_choice, - &mut duplicate_slots_tracker, - &mut duplicate_confirmed_slots, - &mut unfrozen_gossip_verified_vote_hashes, &mut true, &mut Vec::new(), - &mut epoch_slots_frozen_slots, &drop_bank_sender, + &mut tbft_structs, ) .unwrap(); assert_eq!(bank_forks.read().unwrap().root(), root); @@ -4710,18 +5013,23 @@ pub(crate) mod tests { assert!(progress.get(&root).is_some()); // root - 1 is filtered out assert_eq!( - duplicate_slots_tracker.into_iter().collect::>(), + tbft_structs + .duplicate_slots_tracker + .into_iter() + .collect::>(), vec![root, root + 1] ); assert_eq!( - duplicate_confirmed_slots + tbft_structs + .duplicate_confirmed_slots .keys() .cloned() .collect::>(), vec![root, root + 1] ); assert_eq!( - unfrozen_gossip_verified_vote_hashes + tbft_structs + .unfrozen_gossip_verified_vote_hashes .votes_per_slot .keys() .cloned() @@ -4729,7 +5037,10 @@ pub(crate) mod tests { vec![root, root + 1] ); assert_eq!( - epoch_slots_frozen_slots.into_keys().collect::>(), + tbft_structs + .epoch_slots_frozen_slots + .into_keys() + .collect::>(), vec![root, root + 1] ); } @@ -4762,26 +5073,28 @@ pub(crate) mod tests { root_bank.freeze(); let root_hash = root_bank.hash(); bank_forks.write().unwrap().insert(root_bank); - let mut heaviest_subtree_fork_choice = HeaviestSubtreeForkChoice::new((root, root_hash)); let mut progress = ProgressMap::default(); for i in 0..=root { progress.insert(i, ForkProgress::new(Hash::default(), None, None, 0, 0)); } let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let mut tbft_structs = TowerBFTStructures { + heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice::new((root, root_hash)), + duplicate_slots_tracker: DuplicateSlotsTracker::default(), + duplicate_confirmed_slots: DuplicateConfirmedSlots::default(), + unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes::default(), + epoch_slots_frozen_slots: EpochSlotsFrozenSlots::default(), + }; ReplayStage::handle_new_root( root, &bank_forks, &mut progress, &AbsRequestSender::default(), Some(confirmed_root), - &mut heaviest_subtree_fork_choice, - &mut DuplicateSlotsTracker::default(), - &mut DuplicateConfirmedSlots::default(), - &mut UnfrozenGossipVerifiedVoteHashes::default(), &mut true, &mut Vec::new(), - &mut EpochSlotsFrozenSlots::default(), &drop_bank_sender, + &mut tbft_structs, ) .unwrap(); assert_eq!(bank_forks.read().unwrap().root(), root); @@ -5068,7 +5381,7 @@ pub(crate) mod tests { let VoteSimulator { mut progress, bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, validator_keypairs, .. } = vote_simulator; @@ -5133,14 +5446,11 @@ pub(crate) mod tests { err, &rpc_subscriptions, &slot_status_notifier, - &mut DuplicateSlotsTracker::default(), - &DuplicateConfirmedSlots::new(), - &mut EpochSlotsFrozenSlots::default(), &mut progress, - &mut heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), + &mut Some(&mut tbft_structs), ); } assert!(dead_slots.lock().unwrap().contains(&bank1.slot())); @@ -5197,7 +5507,7 @@ pub(crate) mod tests { block_commitment_cache.clone(), OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks), )); - let (lockouts_sender, _) = AggregateCommitmentService::new( + let (lockouts_sender, _commitment_sender, _) = AggregateCommitmentService::new( exit, block_commitment_cache.clone(), rpc_subscriptions, @@ -5515,7 +5825,8 @@ pub(crate) mod tests { .values() .cloned() .collect(); - let heaviest_subtree_fork_choice = &mut vote_simulator.heaviest_subtree_fork_choice; + let heaviest_subtree_fork_choice = + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice; let mut latest_validator_votes_for_frozen_banks = LatestValidatorVotesForFrozenBanks::default(); let ancestors = vote_simulator.bank_forks.read().unwrap().ancestors(); @@ -5599,7 +5910,7 @@ pub(crate) mod tests { &VoteTracker::default(), &ClusterSlots::default(), &vote_simulator.bank_forks, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, ); @@ -5621,6 +5932,7 @@ pub(crate) mod tests { // The only leaf should always be chosen over parents assert_eq!( vote_simulator + .tbft_structs .heaviest_subtree_fork_choice .best_slot(&(bank.slot(), bank.hash())) .unwrap() @@ -6780,7 +7092,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -6792,16 +7104,23 @@ pub(crate) mod tests { // 4 should be the heaviest slot, but should not be votable // because of lockout. 5 is the heaviest slot on the same fork as the last vote. - let (vote_fork, reset_fork, _) = run_compute_and_select_forks( + let (vote_fork, reset_fork, heaviest_fork_failures) = run_compute_and_select_forks( &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); assert!(vote_fork.is_none()); assert_eq!(reset_fork, Some(5)); + assert_eq!( + heaviest_fork_failures, + vec![ + HeaviestForkFailures::FailedSwitchThreshold(4, 0, 10000), + HeaviestForkFailures::LockedOut(4) + ] + ); // Mark 5 as duplicate blockstore.store_duplicate_slot(5, vec![], vec![]).unwrap(); @@ -6814,7 +7133,7 @@ pub(crate) mod tests { let duplicate_state = DuplicateState::new_from_state( 5, &duplicate_confirmed_slots, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, || progress.is_dead(5).unwrap_or(false), || Some(bank5_hash), ); @@ -6826,7 +7145,7 @@ pub(crate) mod tests { &blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut purge_repair_slot_counter, @@ -6840,7 +7159,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -6867,7 +7186,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -6895,7 +7214,7 @@ pub(crate) mod tests { &blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut duplicate_slots_to_repair, &ancestor_hashes_replay_update_sender, &mut purge_repair_slot_counter, @@ -6914,7 +7233,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -6932,6 +7251,7 @@ pub(crate) mod tests { // last vote which was previously marked as invalid and now duplicate confirmed let bank6_hash = bank_forks.read().unwrap().bank_hash(6).unwrap(); let _ = vote_simulator + .tbft_structs .heaviest_subtree_fork_choice .split_off(&(6, bank6_hash)); // Should now pick 5 as the heaviest fork from last vote again. @@ -6939,7 +7259,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -6998,7 +7318,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -7019,7 +7339,7 @@ pub(crate) mod tests { let duplicate_state = DuplicateState::new_from_state( 4, &duplicate_confirmed_slots, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, || progress.is_dead(4).unwrap_or(false), || Some(bank4_hash), ); @@ -7031,7 +7351,7 @@ pub(crate) mod tests { &blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), @@ -7042,7 +7362,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -7056,7 +7376,7 @@ pub(crate) mod tests { let duplicate_state = DuplicateState::new_from_state( 2, &duplicate_confirmed_slots, - &vote_simulator.heaviest_subtree_fork_choice, + &vote_simulator.tbft_structs.heaviest_subtree_fork_choice, || progress.is_dead(2).unwrap_or(false), || Some(bank2_hash), ); @@ -7066,7 +7386,7 @@ pub(crate) mod tests { &blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), @@ -7077,7 +7397,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -7102,7 +7422,7 @@ pub(crate) mod tests { &blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut duplicate_slots_to_repair, &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), @@ -7117,7 +7437,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, None, ); @@ -7236,7 +7556,7 @@ pub(crate) mod tests { let VoteSimulator { ref mut progress, ref bank_forks, - ref mut heaviest_subtree_fork_choice, + ref mut tbft_structs, .. } = vote_simulator; @@ -7266,7 +7586,7 @@ pub(crate) mod tests { blockstore, &mut duplicate_slots_tracker, &mut epoch_slots_frozen_slots, - heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut duplicate_slots_to_repair, &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), @@ -7324,7 +7644,7 @@ pub(crate) mod tests { let VoteSimulator { mut progress, bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, mut latest_validator_votes_for_frozen_banks, .. } = vote_simulator; @@ -7349,12 +7669,13 @@ pub(crate) mod tests { &VoteTracker::default(), &ClusterSlots::default(), &bank_forks, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut latest_validator_votes_for_frozen_banks, ); // Try to switch to vote to the heaviest slot 6, then return the vote results - let (heaviest_bank, heaviest_bank_on_same_fork) = heaviest_subtree_fork_choice + let (heaviest_bank, heaviest_bank_on_same_fork) = tbft_structs + .heaviest_subtree_fork_choice .select_forks(&frozen_banks, &tower, &progress, &ancestors, &bank_forks); assert_eq!(heaviest_bank.slot(), 7); assert!(heaviest_bank_on_same_fork.is_none()); @@ -7366,7 +7687,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ) } @@ -7440,14 +7761,14 @@ pub(crate) mod tests { let VoteSimulator { mut progress, bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, mut latest_validator_votes_for_frozen_banks, .. } = vote_simulator; // Check that the new branch with slot 2 is different than the original version. let bank_1_hash = bank_forks.read().unwrap().bank_hash(1).unwrap(); - let children_of_1 = (&heaviest_subtree_fork_choice) + let children_of_1 = (&tbft_structs.heaviest_subtree_fork_choice) .children(&(1, bank_1_hash)) .unwrap(); let duplicate_versions_of_2 = children_of_1.filter(|(slot, _hash)| *slot == 2).count(); @@ -7473,11 +7794,12 @@ pub(crate) mod tests { &VoteTracker::default(), &ClusterSlots::default(), &bank_forks, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut latest_validator_votes_for_frozen_banks, ); // Try to switch to vote to the heaviest slot 5, then return the vote results - let (heaviest_bank, heaviest_bank_on_same_fork) = heaviest_subtree_fork_choice + let (heaviest_bank, heaviest_bank_on_same_fork) = tbft_structs + .heaviest_subtree_fork_choice .select_forks(&frozen_banks, &tower, &progress, &ancestors, &bank_forks); assert_eq!(heaviest_bank.slot(), 5); assert!(heaviest_bank_on_same_fork.is_none()); @@ -7489,7 +7811,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ) } @@ -7554,7 +7876,7 @@ pub(crate) mod tests { let ( VoteSimulator { bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, mut latest_validator_votes_for_frozen_banks, vote_pubkeys, .. @@ -7567,7 +7889,13 @@ pub(crate) mod tests { let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); // Best slot is 4 - assert_eq!(heaviest_subtree_fork_choice.best_overall_slot().0, 4); + assert_eq!( + tbft_structs + .heaviest_subtree_fork_choice + .best_overall_slot() + .0, + 4 + ); // Cast a vote for slot 3 on one fork let vote_slot = 3; @@ -7578,19 +7906,27 @@ pub(crate) mod tests { ReplayStage::process_gossip_verified_vote_hashes( &gossip_verified_vote_hash_receiver, &mut unfrozen_gossip_verified_vote_hashes, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, &mut latest_validator_votes_for_frozen_banks, ); // Pick the best fork. Gossip votes shouldn't affect fork choice - heaviest_subtree_fork_choice.compute_bank_stats( - &vote_bank, - &Tower::default(), - &mut latest_validator_votes_for_frozen_banks, - ); + tbft_structs + .heaviest_subtree_fork_choice + .compute_bank_stats( + &vote_bank, + &Tower::default(), + &mut latest_validator_votes_for_frozen_banks, + ); // Best slot is still 4 - assert_eq!(heaviest_subtree_fork_choice.best_overall_slot().0, 4); + assert_eq!( + tbft_structs + .heaviest_subtree_fork_choice + .best_overall_slot() + .0, + 4 + ); } #[test] @@ -7814,7 +8150,7 @@ pub(crate) mod tests { assert_eq!(tower.last_voted_slot().unwrap(), 1); // Trying to refresh the vote for bank 1 in bank 2 won't succeed because - // the blockheight has not increased enough + // the blockheight is not increased enough progress .get_fork_stats_mut(bank1.slot()) .unwrap() @@ -7957,7 +8293,15 @@ pub(crate) mod tests { let vote_account = expired_bank_child .get_vote_account(&my_vote_pubkey) .unwrap(); - assert_eq!(vote_account.vote_state().tower(), vec![0, 1]); + assert_eq!( + vote_account + .vote_state_view() + .unwrap() + .votes_iter() + .map(|lockout| lockout.slot()) + .collect_vec(), + vec![0, 1] + ); expired_bank_child.fill_bank_with_ticks_for_tests(); expired_bank_child.freeze(); @@ -8025,7 +8369,7 @@ pub(crate) mod tests { tower_storage: &dyn TowerStorage, make_it_landing: bool, cursor: &mut Cursor, - bank_forks: &RwLock, + bank_forks: Arc>, progress: &mut ProgressMap, ) -> Arc { let my_vote_pubkey = &my_vote_keypair[0].pubkey(); @@ -8079,7 +8423,7 @@ pub(crate) mod tests { ); assert_eq!(tower.last_voted_slot().unwrap(), parent_bank.slot()); let bank = new_bank_from_parent_with_bank_forks( - bank_forks, + &bank_forks, parent_bank, &Pubkey::default(), my_slot, @@ -8118,7 +8462,7 @@ pub(crate) mod tests { let VoteSimulator { mut validator_keypairs, bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, mut latest_validator_votes_for_frozen_banks, mut progress, .. @@ -8176,7 +8520,7 @@ pub(crate) mod tests { &tower_storage, true, &mut cursor, - &bank_forks, + bank_forks.clone(), &mut progress, ); new_bank = send_vote_in_new_bank( @@ -8194,7 +8538,7 @@ pub(crate) mod tests { &tower_storage, false, &mut cursor, - &bank_forks, + bank_forks.clone(), &mut progress, ); // Create enough banks on the fork so last vote is outside SlotHash, make sure @@ -8240,7 +8584,7 @@ pub(crate) mod tests { &VoteTracker::default(), &ClusterSlots::default(), &bank_forks, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut latest_validator_votes_for_frozen_banks, ); assert_eq!(tower.last_voted_slot(), Some(last_voted_slot)); @@ -8254,7 +8598,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ); assert!(vote_bank.is_some()); assert_eq!(vote_bank.unwrap().0.slot(), tip_of_voted_fork); @@ -8270,7 +8614,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ); assert!(vote_bank.is_none()); @@ -8285,7 +8629,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ); assert!(vote_bank.is_none()); @@ -8302,7 +8646,7 @@ pub(crate) mod tests { &progress, &mut tower, &latest_validator_votes_for_frozen_banks, - &heaviest_subtree_fork_choice, + &tbft_structs.heaviest_subtree_fork_choice, ); assert!(vote_bank.is_none()); } @@ -8400,7 +8744,7 @@ pub(crate) mod tests { let res = retransmit_slots_receiver.recv_timeout(Duration::from_millis(10)); assert!( res.is_err(), - "retry_iteration=1, elapsed < 2^1 * RETRY_BASE_DELAY_MS" + "retry_iteration=1, elapsed < 2^1 * RETRANSMIT_BASE_DELAY_MS" ); progress.get_retransmit_info_mut(0).unwrap().retry_time = Instant::now() @@ -8684,6 +9028,8 @@ pub(crate) mod tests { &banking_tracer, has_new_vote_been_rooted, track_transaction_indexes, + &None, + &mut false, )); } @@ -8898,7 +9244,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, Some(my_vote_pubkey), ); @@ -8917,7 +9263,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, Some(my_vote_pubkey), ); @@ -8981,7 +9327,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, Some(my_vote_pubkey), ); @@ -8994,7 +9340,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut tower, - &mut vote_simulator.heaviest_subtree_fork_choice, + &mut vote_simulator.tbft_structs.heaviest_subtree_fork_choice, &mut vote_simulator.latest_validator_votes_for_frozen_banks, Some(my_vote_pubkey), ); @@ -9284,6 +9630,7 @@ pub(crate) mod tests { rpc_subscriptions, .. } = replay_blockstore_components(None, 1, None); + let VoteSimulator { bank_forks, mut progress, @@ -9342,6 +9689,7 @@ pub(crate) mod tests { poh_recorder.read().unwrap().reached_leader_slot(&my_pubkey), PohLeaderStatus::NotReached ); + assert!(!ReplayStage::maybe_start_leader( &my_pubkey, &bank_forks, @@ -9355,6 +9703,8 @@ pub(crate) mod tests { &banking_tracer, has_new_vote_been_rooted, track_transaction_indexes, + &None, + &mut false, )); // Register another slots worth of ticks with PoH recorder @@ -9382,6 +9732,8 @@ pub(crate) mod tests { &banking_tracer, has_new_vote_been_rooted, track_transaction_indexes, + &None, + &mut false, )); // Get the new working bank, which is also the new leader bank/slot let working_bank = bank_forks.read().unwrap().working_bank(); @@ -9407,7 +9759,7 @@ pub(crate) mod tests { setup_forks_from_tree(tree, 3, Some(Box::new(generate_votes))); let VoteSimulator { bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, mut progress, .. } = vote_simulator; @@ -9429,7 +9781,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut DuplicateSlotsTracker::default(), - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut EpochSlotsFrozenSlots::default(), &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, @@ -9449,7 +9801,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut DuplicateSlotsTracker::default(), - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut EpochSlotsFrozenSlots::default(), &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, @@ -9458,7 +9810,8 @@ pub(crate) mod tests { ); assert_eq!(*duplicate_confirmed_slots.get(&5).unwrap(), bank_hash_5); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(5, bank_hash_5)) .unwrap_or(false)); @@ -9472,7 +9825,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut DuplicateSlotsTracker::default(), - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut EpochSlotsFrozenSlots::default(), &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, @@ -9481,11 +9834,13 @@ pub(crate) mod tests { ); assert_eq!(*duplicate_confirmed_slots.get(&5).unwrap(), bank_hash_5); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(5, bank_hash_5)) .unwrap_or(false)); assert_eq!(*duplicate_confirmed_slots.get(&6).unwrap(), bank_hash_6); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(6, bank_hash_6)) .unwrap_or(false)); @@ -9497,7 +9852,7 @@ pub(crate) mod tests { &bank_forks, &mut progress, &mut DuplicateSlotsTracker::default(), - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut EpochSlotsFrozenSlots::default(), &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, @@ -9521,7 +9876,7 @@ pub(crate) mod tests { setup_forks_from_tree(tree, 3, Some(Box::new(generate_votes))); let VoteSimulator { bank_forks, - mut heaviest_subtree_fork_choice, + mut tbft_structs, progress, .. } = vote_simulator; @@ -9547,7 +9902,7 @@ pub(crate) mod tests { &mut EpochSlotsFrozenSlots::default(), &bank_forks, &progress, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), @@ -9567,14 +9922,15 @@ pub(crate) mod tests { &mut EpochSlotsFrozenSlots::default(), &bank_forks, &progress, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), ); assert_eq!(*duplicate_confirmed_slots.get(&5).unwrap(), bank_hash_5); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(5, bank_hash_5)) .unwrap_or(false)); @@ -9597,18 +9953,20 @@ pub(crate) mod tests { &mut EpochSlotsFrozenSlots::default(), &bank_forks, &progress, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), ); assert_eq!(*duplicate_confirmed_slots.get(&5).unwrap(), bank_hash_5); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(5, bank_hash_5)) .unwrap_or(false)); assert_eq!(*duplicate_confirmed_slots.get(&6).unwrap(), bank_hash_6); - assert!(heaviest_subtree_fork_choice + assert!(tbft_structs + .heaviest_subtree_fork_choice .is_duplicate_confirmed(&(6, bank_hash_6)) .unwrap_or(false)); @@ -9623,7 +9981,7 @@ pub(crate) mod tests { &mut EpochSlotsFrozenSlots::default(), &bank_forks, &progress, - &mut heaviest_subtree_fork_choice, + &mut tbft_structs.heaviest_subtree_fork_choice, &mut DuplicateSlotsToRepair::default(), &ancestor_hashes_replay_update_sender, &mut PurgeRepairSlotCounter::default(), diff --git a/core/src/sigverifier/bls_sigverifier.rs b/core/src/sigverifier/bls_sigverifier.rs new file mode 100644 index 0000000000..03ee50a87f --- /dev/null +++ b/core/src/sigverifier/bls_sigverifier.rs @@ -0,0 +1,381 @@ +//! The BLS signature verifier. +//! This is just a placeholder for now, until we have a real implementation. + +mod stats; + +use { + crate::{ + cluster_info_vote_listener::VerifiedVoteSender, + sigverify_stage::{SigVerifier, SigVerifyServiceError}, + }, + crossbeam_channel::{Sender, TrySendError}, + solana_pubkey::Pubkey, + solana_runtime::epoch_stakes_service::EpochStakesService, + solana_sdk::clock::Slot, + solana_streamer::packet::PacketBatch, + solana_votor_messages::bls_message::BLSMessage, + stats::{BLSSigVerifierStats, StatsUpdater}, + std::{collections::HashMap, sync::Arc}, +}; + +pub struct BLSSigVerifier { + verified_votes_sender: VerifiedVoteSender, + message_sender: Sender, + epoch_stakes_service: Arc, + stats: BLSSigVerifierStats, +} + +impl SigVerifier for BLSSigVerifier { + type SendType = BLSMessage; + + // TODO(wen): just a placeholder without any verification. + fn verify_batches(&self, batches: Vec, _valid_packets: usize) -> Vec { + batches + } + + fn send_packets( + &mut self, + packet_batches: Vec, + ) -> Result<(), SigVerifyServiceError> { + // TODO(wen): just a placeholder without any batching. + let mut verified_votes = HashMap::new(); + let mut stats_updater = StatsUpdater::default(); + + for packet in packet_batches.iter().flatten() { + stats_updater.received += 1; + + let message = match packet.deserialize_slice(..) { + Ok(msg) => msg, + Err(e) => { + trace!("Failed to deserialize BLS message: {}", e); + stats_updater.received_malformed += 1; + continue; + } + }; + + let slot = match &message { + BLSMessage::Vote(vote_message) => vote_message.vote.slot(), + BLSMessage::Certificate(certificate_message) => { + certificate_message.certificate.slot() + } + }; + + let Some(rank_to_pubkey_map) = self.epoch_stakes_service.get_key_to_rank_map(slot) + else { + stats_updater.received_no_epoch_stakes += 1; + continue; + }; + + if let BLSMessage::Vote(vote_message) = &message { + let vote = &vote_message.vote; + stats_updater.received_votes += 1; + if vote.is_notarization_or_finalization() || vote.is_notarize_fallback() { + let Some((pubkey, _)) = rank_to_pubkey_map.get_pubkey(vote_message.rank.into()) + else { + stats_updater.received_malformed += 1; + continue; + }; + let cur_slots: &mut Vec = verified_votes.entry(*pubkey).or_default(); + if !cur_slots.contains(&slot) { + cur_slots.push(slot); + } + } + } + + // Now send the BLS message to certificate pool. + match self.message_sender.try_send(message) { + Ok(()) => stats_updater.sent += 1, + Err(TrySendError::Full(_)) => { + stats_updater.sent_failed += 1; + } + Err(e @ TrySendError::Disconnected(_)) => { + return Err(e.into()); + } + } + } + self.send_verified_votes(verified_votes); + self.stats.update(stats_updater); + self.stats.maybe_report_stats(); + Ok(()) + } +} + +impl BLSSigVerifier { + pub fn new( + epoch_stakes_service: Arc, + verified_votes_sender: VerifiedVoteSender, + message_sender: Sender, + ) -> Self { + Self { + epoch_stakes_service, + verified_votes_sender, + message_sender, + stats: BLSSigVerifierStats::new(), + } + } + + fn send_verified_votes(&mut self, verified_votes: HashMap>) { + let mut stats_updater = StatsUpdater::default(); + for (pubkey, slots) in verified_votes { + match self.verified_votes_sender.try_send((pubkey, slots)) { + Ok(()) => { + stats_updater.verified_votes_sent += 1; + } + Err(e) => { + trace!("Failed to send verified vote: {}", e); + stats_updater.verified_votes_sent_failed += 1; + } + } + } + self.stats.update(stats_updater); + } +} + +// Add tests for the BLS signature verifier +#[cfg(test)] +mod tests { + use { + super::*, + bitvec::prelude::*, + crossbeam_channel::{unbounded, Receiver}, + solana_bls_signatures::Signature, + solana_perf::packet::Packet, + solana_runtime::{ + bank::Bank, + genesis_utils::{ + create_genesis_config_with_alpenglow_vote_accounts_no_program, + ValidatorVoteKeypairs, + }, + }, + solana_sdk::{hash::Hash, signer::Signer}, + solana_votor_messages::{ + bls_message::{ + BLSMessage, Certificate, CertificateMessage, CertificateType, VoteMessage, + }, + vote::Vote, + }, + stats::STATS_INTERVAL_DURATION, + std::time::{Duration, Instant}, + }; + + fn create_keypairs_and_bls_sig_verifier( + verified_vote_sender: VerifiedVoteSender, + message_sender: Sender, + ) -> (Vec, BLSSigVerifier) { + // Create 10 node validatorvotekeypairs vec + let validator_keypairs = (0..10) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let stakes_vec = (0..validator_keypairs.len()) + .map(|i| 1_000 - i as u64) + .collect::>(); + let genesis = create_genesis_config_with_alpenglow_vote_accounts_no_program( + 1_000_000_000, + &validator_keypairs, + stakes_vec, + ); + let bank = Arc::new(Bank::new_for_tests(&genesis.genesis_config)); + let epoch = bank.epoch(); + let (_tx, rx) = unbounded(); + let epoch_stakes_service = Arc::new(EpochStakesService::new(bank, epoch, rx)); + ( + validator_keypairs, + BLSSigVerifier::new(epoch_stakes_service, verified_vote_sender, message_sender), + ) + } + + fn test_bls_message_transmission( + verifier: &mut BLSSigVerifier, + receiver: Option<&Receiver>, + messages: &[BLSMessage], + expect_send_packets_ok: bool, + ) { + let packets = messages + .iter() + .map(|msg| { + let mut packet = Packet::default(); + packet + .populate_packet(None, msg) + .expect("Failed to populate packet"); + packet + }) + .collect::>(); + let packet_batches = vec![PacketBatch::new(packets)]; + if expect_send_packets_ok { + assert!(verifier.send_packets(packet_batches).is_ok()); + if let Some(receiver) = receiver { + for msg in messages { + match receiver.recv_timeout(Duration::from_secs(1)) { + Ok(received_msg) => assert_eq!(received_msg, *msg), + Err(e) => warn!("Failed to receive BLS message: {}", e), + } + } + } + } else { + assert!(verifier.send_packets(packet_batches).is_err()); + } + } + + #[test] + fn test_blssigverifier_send_packets() { + let (sender, receiver) = crossbeam_channel::unbounded(); + let (verified_vote_sender, verfied_vote_receiver) = crossbeam_channel::unbounded(); + // Create bank forks and epoch stakes + + let (validator_keypairs, mut verifier) = + create_keypairs_and_bls_sig_verifier(verified_vote_sender, sender); + + let mut bitmap = BitVec::::repeat(false, 8); + bitmap.set(3, true); + bitmap.set(5, true); + let vote_rank: usize = 2; + + let certificate = Certificate::new(CertificateType::Finalize, 4, None); + + let messages = vec![ + BLSMessage::Vote(VoteMessage { + vote: Vote::new_finalization_vote(5), + signature: Signature::default(), + rank: vote_rank as u16, + }), + BLSMessage::Certificate(CertificateMessage { + certificate, + signature: Signature::default(), + bitmap, + }), + ]; + test_bls_message_transmission(&mut verifier, Some(&receiver), &messages, true); + assert_eq!(verifier.stats.sent, 2); + assert_eq!(verifier.stats.received, 2); + assert_eq!(verifier.stats.received_malformed, 0); + let received_verified_votes = verfied_vote_receiver.try_recv().unwrap(); + assert_eq!( + received_verified_votes, + (validator_keypairs[vote_rank].vote_keypair.pubkey(), vec![5]) + ); + + let vote_rank: usize = 3; + let messages = vec![BLSMessage::Vote(VoteMessage { + vote: Vote::new_notarization_vote(6, Hash::new_unique()), + signature: Signature::default(), + rank: vote_rank as u16, + })]; + test_bls_message_transmission(&mut verifier, Some(&receiver), &messages, true); + assert_eq!(verifier.stats.sent, 3); + assert_eq!(verifier.stats.received, 3); + assert_eq!(verifier.stats.received_malformed, 0); + let received_verified_votes = verfied_vote_receiver.try_recv().unwrap(); + assert_eq!( + received_verified_votes, + (validator_keypairs[vote_rank].vote_keypair.pubkey(), vec![6]) + ); + + // Pretend 10 seconds have passed, make sure stats are reset + verifier.stats.last_stats_logged = Instant::now() - STATS_INTERVAL_DURATION; + let vote_rank: usize = 9; + let messages = vec![BLSMessage::Vote(VoteMessage { + vote: Vote::new_notarization_fallback_vote(7, Hash::new_unique()), + signature: Signature::default(), + rank: vote_rank as u16, + })]; + test_bls_message_transmission(&mut verifier, Some(&receiver), &messages, true); + // Since we just logged all stats (including the packet just sent), stats should be reset + assert_eq!(verifier.stats.sent, 0); + assert_eq!(verifier.stats.received, 0); + assert_eq!(verifier.stats.received_malformed, 0); + let received_verified_votes = verfied_vote_receiver.try_recv().unwrap(); + assert_eq!( + received_verified_votes, + (validator_keypairs[vote_rank].vote_keypair.pubkey(), vec![7]) + ); + } + + #[test] + fn test_blssigverifier_send_packets_malformed() { + let (sender, receiver) = crossbeam_channel::unbounded(); + let (verified_vote_sender, _) = crossbeam_channel::unbounded(); + let (_, mut verifier) = create_keypairs_and_bls_sig_verifier(verified_vote_sender, sender); + + let packets = vec![Packet::default()]; + let packet_batches = vec![PacketBatch::new(packets)]; + assert!(verifier.send_packets(packet_batches).is_ok()); + assert_eq!(verifier.stats.sent, 0); + assert_eq!(verifier.stats.received, 1); + assert_eq!(verifier.stats.received_malformed, 1); + assert_eq!(verifier.stats.received_no_epoch_stakes, 0); + + // Expect no messages since the packet was malformed + assert!(receiver.is_empty()); + + // Send a packet with no epoch stakes + let messages = vec![BLSMessage::Vote(VoteMessage { + vote: Vote::new_finalization_vote(5_000_000_000), + signature: Signature::default(), + rank: 0, + })]; + test_bls_message_transmission(&mut verifier, None, &messages, true); + assert_eq!(verifier.stats.sent, 0); + assert_eq!(verifier.stats.received, 2); + assert_eq!(verifier.stats.received_malformed, 1); + assert_eq!(verifier.stats.received_no_epoch_stakes, 1); + + // Expect no messages since the packet was malformed + assert!(receiver.is_empty()); + + // Send a packet with invalid rank + let messages = vec![BLSMessage::Vote(VoteMessage { + vote: Vote::new_finalization_vote(5), + signature: Signature::default(), + rank: 1000, // Invalid rank + })]; + test_bls_message_transmission(&mut verifier, None, &messages, true); + assert_eq!(verifier.stats.sent, 0); + assert_eq!(verifier.stats.received, 3); + assert_eq!(verifier.stats.received_malformed, 2); + assert_eq!(verifier.stats.received_no_epoch_stakes, 1); + + // Expect no messages since the packet was malformed + assert!(receiver.is_empty()); + } + + #[test] + fn test_blssigverifier_send_packets_channel_full() { + solana_logger::setup(); + let (sender, receiver) = crossbeam_channel::bounded(1); + let (verified_vote_sender, _) = crossbeam_channel::unbounded(); + let (_, mut verifier) = create_keypairs_and_bls_sig_verifier(verified_vote_sender, sender); + let messages = vec![ + BLSMessage::Vote(VoteMessage { + vote: Vote::new_finalization_vote(5), + signature: Signature::default(), + rank: 0, + }), + BLSMessage::Vote(VoteMessage { + vote: Vote::new_notarization_fallback_vote(6, Hash::new_unique()), + signature: Signature::default(), + rank: 2, + }), + ]; + test_bls_message_transmission(&mut verifier, Some(&receiver), &messages, true); + + // We failed to send the second message because the channel is full. + assert_eq!(verifier.stats.sent, 1); + assert_eq!(verifier.stats.received, 2); + assert_eq!(verifier.stats.received_malformed, 0); + } + + #[test] + fn test_blssigverifier_send_packets_receiver_closed() { + let (sender, receiver) = crossbeam_channel::bounded(1); + let (verified_vote_sender, _) = crossbeam_channel::unbounded(); + let (_, mut verifier) = create_keypairs_and_bls_sig_verifier(verified_vote_sender, sender); + // Close the receiver, should get panic on next send + drop(receiver); + let messages = vec![BLSMessage::Vote(VoteMessage { + vote: Vote::new_finalization_vote(5), + signature: Signature::default(), + rank: 0, + })]; + test_bls_message_transmission(&mut verifier, None, &messages, false); + } +} diff --git a/core/src/sigverifier/bls_sigverifier/stats.rs b/core/src/sigverifier/bls_sigverifier/stats.rs new file mode 100644 index 0000000000..635c6e6572 --- /dev/null +++ b/core/src/sigverifier/bls_sigverifier/stats.rs @@ -0,0 +1,101 @@ +use std::time::{Duration, Instant}; + +pub(super) const STATS_INTERVAL_DURATION: Duration = Duration::from_secs(1); + +#[derive(Debug, Default)] +pub(super) struct StatsUpdater { + pub(super) sent: u64, + pub(super) sent_failed: u64, + pub(super) verified_votes_sent: u64, + pub(super) verified_votes_sent_failed: u64, + pub(super) received: u64, + pub(super) received_malformed: u64, + pub(super) received_no_epoch_stakes: u64, + pub(super) received_votes: u64, +} + +// We are adding our own stats because we do BLS decoding in batch verification, +// and we send one BLS message at a time. So it makes sense to have finer-grained stats +// +// The fields are visible to support testing and should not be accessed +// directly in production code. Use `StatsUpdater` instead. +#[derive(Debug)] +pub(super) struct BLSSigVerifierStats { + pub(super) sent: u64, + pub(super) sent_failed: u64, + pub(super) verified_votes_sent: u64, + pub(super) verified_votes_sent_failed: u64, + pub(super) received: u64, + pub(super) received_malformed: u64, + pub(super) received_no_epoch_stakes: u64, + pub(super) received_votes: u64, + pub(super) last_stats_logged: Instant, +} + +impl BLSSigVerifierStats { + pub(super) fn new() -> Self { + Self { + sent: 0, + sent_failed: 0, + verified_votes_sent: 0, + verified_votes_sent_failed: 0, + received: 0, + received_malformed: 0, + received_no_epoch_stakes: 0, + received_votes: 0, + last_stats_logged: Instant::now(), + } + } + + /// If sufficient time has passed since last report, report stats. + pub(super) fn maybe_report_stats(&mut self) { + let now = Instant::now(); + let time_since_last_log = now.duration_since(self.last_stats_logged); + if time_since_last_log < STATS_INTERVAL_DURATION { + return; + } + datapoint_info!( + "bls_sig_verifier_stats", + ("sent", self.sent as i64, i64), + ("sent_failed", self.sent_failed as i64, i64), + ("verified_votes_sent", self.verified_votes_sent as i64, i64), + ( + "verified_votes_sent_failed", + self.verified_votes_sent_failed as i64, + i64 + ), + ("received", self.received as i64, i64), + ("received_votes", self.received_votes as i64, i64), + ( + "received_no_epoch_stakes", + self.received_no_epoch_stakes as i64, + i64 + ), + ("received_malformed", self.received_malformed as i64, i64), + ); + *self = BLSSigVerifierStats::new(); + } + + pub(super) fn update( + &mut self, + StatsUpdater { + sent, + sent_failed, + verified_votes_sent, + verified_votes_sent_failed, + received, + received_malformed, + received_no_epoch_stakes, + received_votes, + }: StatsUpdater, + ) { + self.sent += sent; + self.sent_failed += sent_failed; + self.verified_votes_sent += verified_votes_sent; + self.verified_votes_sent_failed += verified_votes_sent_failed; + self.received += received; + self.received_malformed += received_malformed; + self.received_no_epoch_stakes += received_no_epoch_stakes; + self.received_votes += received_votes; + } +} diff --git a/core/src/sigverify.rs b/core/src/sigverifier/ed25519_sigverifier.rs similarity index 100% rename from core/src/sigverify.rs rename to core/src/sigverifier/ed25519_sigverifier.rs diff --git a/core/src/sigverifier/mod.rs b/core/src/sigverifier/mod.rs new file mode 100644 index 0000000000..20af3b7f03 --- /dev/null +++ b/core/src/sigverifier/mod.rs @@ -0,0 +1,2 @@ +pub mod bls_sigverifier; +pub mod ed25519_sigverifier; diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index 7dbf819f6f..5806eb5ee5 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -6,9 +6,9 @@ //! if perf-libs are available use { - crate::sigverify, + crate::sigverifier::ed25519_sigverifier::ed25519_verify_disabled, core::time::Duration, - crossbeam_channel::{Receiver, RecvTimeoutError, SendError}, + crossbeam_channel::{Receiver, RecvTimeoutError, SendError, TrySendError}, itertools::Itertools, solana_measure::measure::Measure, solana_perf::{ @@ -44,6 +44,9 @@ pub enum SigVerifyServiceError { #[error("send packets batch error")] Send(#[from] SendError), + #[error("try_send packet errror")] + TrySend(#[from] TrySendError), + #[error("streamer error")] Streamer(#[from] StreamerError), } @@ -221,7 +224,7 @@ impl SigVerifier for DisabledSigVerifier { mut batches: Vec, _valid_packets: usize, ) -> Vec { - sigverify::ed25519_verify_disabled(&mut batches); + ed25519_verify_disabled(&mut batches); batches } @@ -408,7 +411,7 @@ impl SigVerifyStage { SigVerifyServiceError::Streamer(StreamerError::RecvTimeout( RecvTimeoutError::Timeout, )) => (), - SigVerifyServiceError::Send(_) => { + SigVerifyServiceError::Send(_) | SigVerifyServiceError::TrySend(_) => { break; } _ => error!("{:?}", e), @@ -433,7 +436,9 @@ impl SigVerifyStage { mod tests { use { super::*, - crate::{banking_trace::BankingTracer, sigverify::TransactionSigVerifier}, + crate::{ + banking_trace::BankingTracer, sigverifier::ed25519_sigverifier::TransactionSigVerifier, + }, crossbeam_channel::unbounded, solana_perf::{ packet::{to_packet_batches, Packet}, diff --git a/core/src/staked_validators_cache.rs b/core/src/staked_validators_cache.rs new file mode 100644 index 0000000000..8ca75450c3 --- /dev/null +++ b/core/src/staked_validators_cache.rs @@ -0,0 +1,889 @@ +use { + crate::voting_service::AlpenglowPortOverride, + lru::LruCache, + solana_gossip::{cluster_info::ClusterInfo, contact_info::Protocol}, + solana_pubkey::Pubkey, + solana_runtime::bank_forks::BankForks, + solana_sdk::clock::{Epoch, Slot}, + std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, RwLock}, + time::{Duration, Instant}, + }, +}; + +struct StakedValidatorsCacheEntry { + /// TPU Vote Sockets associated with the staked validators + validator_sockets: Vec, + + /// Alpenglow Sockets associated with the staked validators + alpenglow_sockets: Vec, + + /// The time at which this entry was created + creation_time: Instant, +} + +/// Maintain `SocketAddr`s associated with all staked validators for a particular protocol (e.g., +/// UDP, QUIC) over number of epochs. +/// +/// We employ an LRU cache with capped size, mapping Epoch to cache entries that store the socket +/// information. We also track cache entry times, forcing recalculations of cache entries that are +/// accessed after a specified TTL. +pub struct StakedValidatorsCache { + /// key: the epoch for which we have cached our stake validators list + /// value: the cache entry + cache: LruCache, + + /// Time to live for cache entries + ttl: Duration, + + /// Bank forks + bank_forks: Arc>, + + /// Protocol + protocol: Protocol, + + /// Whether to include the running validator's socket address in cache entries + include_self: bool, + + /// Optional override for Alpenglow port, used for testing purposes + alpenglow_port_override: Option, + + /// timestamp of the last alpenglow port override we read + alpenglow_port_override_last_modified: Instant, +} + +enum PortsToUse { + TpuVote, + Alpenglow, +} + +impl StakedValidatorsCache { + pub fn new( + bank_forks: Arc>, + protocol: Protocol, + ttl: Duration, + max_cache_size: usize, + include_self: bool, + alpenglow_port_override: Option, + ) -> Self { + Self { + cache: LruCache::new(max_cache_size), + ttl, + bank_forks, + protocol, + include_self, + alpenglow_port_override, + alpenglow_port_override_last_modified: Instant::now(), + } + } + + #[inline] + fn cur_epoch(&self, slot: Slot) -> Epoch { + self.bank_forks + .read() + .unwrap() + .working_bank() + .epoch_schedule() + .get_epoch(slot) + } + + fn refresh_cache_entry( + &mut self, + epoch: Epoch, + cluster_info: &ClusterInfo, + update_time: Instant, + ) { + let banks = { + let bank_forks = self.bank_forks.read().unwrap(); + [bank_forks.root_bank(), bank_forks.working_bank()] + }; + + let epoch_staked_nodes = banks.iter().find_map(|bank| bank.epoch_staked_nodes(epoch)).unwrap_or_else(|| { + error!("StakedValidatorsCache::get: unknown Bank::epoch_staked_nodes for epoch: {epoch}"); + Arc::>::default() + }); + + struct Node { + pubkey: Pubkey, + stake: u64, + tpu_socket: SocketAddr, + // TODO(wen): this should not be an Option after BLS all-to-all is submitted. + alpenglow_socket: Option, + } + + let mut nodes: Vec<_> = epoch_staked_nodes + .iter() + .filter(|(pubkey, stake)| { + let positive_stake = **stake > 0; + let not_self = pubkey != &&cluster_info.id(); + + positive_stake && (self.include_self || not_self) + }) + .filter_map(|(pubkey, stake)| { + cluster_info.lookup_contact_info(pubkey, |node| { + let tpu_socket = node.tpu_vote(self.protocol); + let alpenglow_socket = node.alpenglow(); + // To not change current behavior, we only consider nodes that have a + // TPU socket, and ignore nodes that only have an Alpenglow socket. + // TODO(wen): tpu_socket is no longer needed after Alpenglow migration. + tpu_socket.map(|tpu_socket| Node { + pubkey: *pubkey, + stake: *stake, + tpu_socket, + alpenglow_socket, + }) + })? + }) + .collect(); + + // TODO(wen): After Alpenglow vote is no longer transaction, we dedup by alpenglow socket. + nodes.dedup_by_key(|node| node.tpu_socket); + nodes.sort_unstable_by(|a, b| a.stake.cmp(&b.stake)); + + let mut validator_sockets = Vec::new(); + let mut alpenglow_sockets = Vec::new(); + let override_map = self + .alpenglow_port_override + .as_ref() + .map(|x| x.get_override_map()); + for node in nodes { + validator_sockets.push(node.tpu_socket); + + if let Some(alpenglow_socket) = node.alpenglow_socket { + let socket = if let Some(override_map) = &override_map { + // If we have an override, use it. + override_map + .get(&node.pubkey) + .cloned() + .unwrap_or(alpenglow_socket) + } else { + alpenglow_socket + }; + alpenglow_sockets.push(socket); + } + } + self.cache.push( + epoch, + StakedValidatorsCacheEntry { + validator_sockets, + alpenglow_sockets, + creation_time: update_time, + }, + ); + } + + pub fn get_staked_validators_by_slot_with_tpu_vote_ports( + &mut self, + slot: Slot, + cluster_info: &ClusterInfo, + access_time: Instant, + ) -> (&[SocketAddr], bool) { + self.get_staked_validators_by_epoch( + self.cur_epoch(slot), + cluster_info, + access_time, + PortsToUse::TpuVote, + ) + } + + pub fn get_staked_validators_by_slot_with_alpenglow_ports( + &mut self, + slot: Slot, + cluster_info: &ClusterInfo, + access_time: Instant, + ) -> (&[SocketAddr], bool) { + // Check if self.alpenglow_port_override has a different last_modified. + // Immediately refresh the cache if it does. + if let Some(alpenglow_port_override) = &self.alpenglow_port_override { + if alpenglow_port_override.has_new_override(self.alpenglow_port_override_last_modified) + { + self.alpenglow_port_override_last_modified = + alpenglow_port_override.last_modified(); + trace!( + "refreshing cache entry for epoch {} due to alpenglow port override last_modified change", + self.cur_epoch(slot) + ); + self.refresh_cache_entry(self.cur_epoch(slot), cluster_info, access_time); + } + } + + self.get_staked_validators_by_epoch( + self.cur_epoch(slot), + cluster_info, + access_time, + PortsToUse::Alpenglow, + ) + } + + fn get_staked_validators_by_epoch( + &mut self, + epoch: Epoch, + cluster_info: &ClusterInfo, + access_time: Instant, + ports_to_use: PortsToUse, + ) -> (&[SocketAddr], bool) { + // For a given epoch, if we either: + // + // (1) have a cache entry that has expired + // (2) have no existing cache entry + // + // then update the cache. + let refresh_cache = self + .cache + .get(&epoch) + .map(|v| access_time > v.creation_time + self.ttl) + .unwrap_or(true); + + if refresh_cache { + self.refresh_cache_entry(epoch, cluster_info, access_time); + } + + ( + // Unwrapping is fine here, since update_cache guarantees that we push a cache entry to + // self.cache[epoch]. + self.cache + .get(&epoch) + .map(|v| match ports_to_use { + PortsToUse::TpuVote => &*v.validator_sockets, + PortsToUse::Alpenglow => &*v.alpenglow_sockets, + }) + .unwrap(), + refresh_cache, + ) + } + + pub fn len(&self) -> usize { + self.cache.len() + } + + pub fn is_empty(&self) -> bool { + self.cache.is_empty() + } +} + +#[cfg(test)] +mod tests { + use { + super::StakedValidatorsCache, + crate::voting_service::AlpenglowPortOverride, + solana_gossip::{ + cluster_info::{ClusterInfo, Node}, + contact_info::{ContactInfo, Protocol}, + crds::GossipRoute, + crds_data::CrdsData, + crds_value::CrdsValue, + }, + solana_ledger::genesis_utils::{create_genesis_config, GenesisConfigInfo}, + solana_pubkey::Pubkey, + solana_runtime::{bank::Bank, bank_forks::BankForks, epoch_stakes::EpochStakes}, + solana_sdk::{ + account::AccountSharedData, + clock::{Clock, Slot}, + genesis_config::GenesisConfig, + signature::Keypair, + signer::Signer, + timing::timestamp, + }, + solana_streamer::socket::SocketAddrSpace, + solana_vote::vote_account::{VoteAccount, VoteAccountsHashMap}, + solana_vote_program::vote_state::{VoteInit, VoteState, VoteStateVersions}, + std::{ + collections::HashMap, + iter::{repeat, repeat_with}, + net::{Ipv4Addr, SocketAddr}, + sync::{Arc, RwLock}, + time::{Duration, Instant}, + }, + test_case::{test_case, test_matrix}, + }; + + fn new_rand_vote_account( + rng: &mut R, + node_pubkey: Option, + ) -> (AccountSharedData, VoteState) { + let vote_init = VoteInit { + node_pubkey: node_pubkey.unwrap_or_else(Pubkey::new_unique), + authorized_voter: Pubkey::new_unique(), + authorized_withdrawer: Pubkey::new_unique(), + commission: rng.gen(), + }; + let clock = Clock { + slot: rng.gen(), + epoch_start_timestamp: rng.gen(), + epoch: rng.gen(), + leader_schedule_epoch: rng.gen(), + unix_timestamp: rng.gen(), + }; + let vote_state = VoteState::new(&vote_init, &clock); + let account = AccountSharedData::new_data( + rng.gen(), // lamports + &VoteStateVersions::new_current(vote_state.clone()), + &solana_sdk_ids::vote::id(), // owner + ) + .unwrap(); + (account, vote_state) + } + + fn new_rand_vote_accounts( + rng: &mut R, + num_nodes: usize, + num_zero_stake_nodes: usize, + ) -> impl Iterator + '_ { + let node_keypairs: Vec<_> = repeat_with(Keypair::new).take(num_nodes).collect(); + + repeat(0..num_nodes).flatten().map(move |node_ix| { + let node_keypair = node_keypairs[node_ix].insecure_clone(); + let vote_account_keypair = Keypair::new(); + + let (account, _) = new_rand_vote_account(rng, Some(node_keypair.pubkey())); + let stake = if node_ix < num_zero_stake_nodes { + 0 + } else { + rng.gen_range(1..997) + }; + let vote_account = VoteAccount::try_from(account).unwrap(); + (vote_account_keypair, node_keypair, stake, vote_account) + }) + } + + struct StakedValidatorsCacheHarness { + bank: Bank, + cluster_info: ClusterInfo, + } + + impl StakedValidatorsCacheHarness { + pub fn new(genesis_config: &GenesisConfig, keypair: Keypair) -> Self { + let bank = Bank::new_for_tests(genesis_config); + + let cluster_info = ClusterInfo::new( + Node::new_localhost_with_pubkey(&keypair.pubkey()).info, + Arc::new(keypair), + SocketAddrSpace::Unspecified, + ); + + Self { bank, cluster_info } + } + + pub fn with_vote_accounts( + mut self, + slot: Slot, + node_keypair_map: HashMap, + vote_accounts: VoteAccountsHashMap, + protocol: Protocol, + ) -> Self { + // Update cluster info + { + let node_contact_info = + node_keypair_map + .keys() + .enumerate() + .map(|(node_ix, pubkey)| { + let mut contact_info = ContactInfo::new(*pubkey, 0_u64, 0_u16); + + assert!(contact_info + .set_tpu_vote( + protocol, + (Ipv4Addr::LOCALHOST, 8005 + node_ix as u16), + ) + .is_ok()); + + assert!(contact_info + .set_alpenglow((Ipv4Addr::LOCALHOST, 8080 + node_ix as u16)) + .is_ok()); + + contact_info + }); + + for contact_info in node_contact_info { + let node_pubkey = *contact_info.pubkey(); + + let entry = CrdsValue::new( + CrdsData::ContactInfo(contact_info), + &node_keypair_map[&node_pubkey], + ); + + assert_eq!(node_pubkey, entry.label().pubkey()); + + { + let mut gossip_crds = self.cluster_info.gossip.crds.write().unwrap(); + + gossip_crds + .insert(entry, timestamp(), GossipRoute::LocalMessage) + .unwrap(); + } + } + } + + // Update bank + let epoch_num = self.bank.epoch_schedule().get_epoch(slot); + let epoch_stakes = EpochStakes::new_for_tests(vote_accounts, epoch_num); + + self.bank.set_epoch_stakes_for_test(epoch_num, epoch_stakes); + + self + } + + pub fn bank_forks(self) -> (Arc>, ClusterInfo) { + let bank_forks = self.bank.wrap_with_bank_forks_for_tests().1; + (bank_forks, self.cluster_info) + } + } + + /// Create a number of nodes; each node will have one or more vote accounts. Each vote account + /// will have random stake in [1, 997), with the exception of the first few vote accounts + /// having exactly 0 stake. + fn build_epoch_stakes( + num_nodes: usize, + num_zero_stake_vote_accounts: usize, + num_vote_accounts: usize, + ) -> (HashMap, VoteAccountsHashMap) { + let mut rng = rand::thread_rng(); + + let vote_accounts: Vec<_> = + new_rand_vote_accounts(&mut rng, num_nodes, num_zero_stake_vote_accounts) + .take(num_vote_accounts) + .collect(); + + let node_keypair_map: HashMap = vote_accounts + .iter() + .map(|(_, node_keypair, _, _)| (node_keypair.pubkey(), node_keypair.insecure_clone())) + .collect(); + + let vahm = vote_accounts + .into_iter() + .map(|(vote_keypair, _, stake, vote_account)| { + (vote_keypair.pubkey(), (stake, vote_account)) + }) + .collect(); + + (node_keypair_map, vahm) + } + + #[test_case(1_usize, 0_usize, 10_usize, Protocol::UDP, false)] + #[test_case(1_usize, 0_usize, 10_usize, Protocol::UDP, true)] + #[test_case(3_usize, 0_usize, 10_usize, Protocol::QUIC, false)] + #[test_case(10_usize, 2_usize, 10_usize, Protocol::UDP, false)] + #[test_case(10_usize, 2_usize, 10_usize, Protocol::UDP, true)] + #[test_case(10_usize, 10_usize, 10_usize, Protocol::QUIC, false)] + #[test_case(50_usize, 7_usize, 60_usize, Protocol::UDP, false)] + #[test_case(50_usize, 7_usize, 60_usize, Protocol::UDP, true)] + fn test_detect_only_staked_nodes_and_refresh_after_ttl( + num_nodes: usize, + num_zero_stake_nodes: usize, + num_vote_accounts: usize, + protocol: Protocol, + use_alpenglow_socket: bool, + ) { + let slot_num = 325_000_000_u64; + let genesis_lamports = 123_u64; + // Create our harness + let (keypair_map, vahm) = + build_epoch_stakes(num_nodes, num_zero_stake_nodes, num_vote_accounts); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(genesis_lamports); + + let (bank_forks, cluster_info) = + StakedValidatorsCacheHarness::new(&genesis_config, Keypair::new()) + .with_vote_accounts(slot_num, keypair_map, vahm, protocol) + .bank_forks(); + + // Create our staked validators cache + let mut svc = + StakedValidatorsCache::new(bank_forks, protocol, Duration::from_secs(5), 5, true, None); + + let now = Instant::now(); + + let (sockets, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports(slot_num, &cluster_info, now) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports(slot_num, &cluster_info, now) + }; + + assert!(refreshed); + assert_eq!(num_nodes - num_zero_stake_nodes, sockets.len()); + assert_eq!(1, svc.len()); + + // Re-fetch from the cache right before the 5-second deadline + let (sockets, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + now + Duration::from_secs_f64(4.999), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + now + Duration::from_secs_f64(4.999), + ) + }; + + assert!(!refreshed); + assert_eq!(num_nodes - num_zero_stake_nodes, sockets.len()); + assert_eq!(1, svc.len()); + + // Re-fetch from the cache right at the 5-second deadline - we still shouldn't refresh. + let (sockets, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + now + Duration::from_secs(5), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + now + Duration::from_secs(5), + ) + }; + + assert!(!refreshed); + assert_eq!(num_nodes - num_zero_stake_nodes, sockets.len()); + assert_eq!(1, svc.len()); + + // Re-fetch from the cache right after the 5-second deadline - now we should refresh. + let (sockets, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + now + Duration::from_secs_f64(5.001), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + now + Duration::from_secs_f64(5.001), + ) + }; + + assert!(refreshed); + assert_eq!(num_nodes - num_zero_stake_nodes, sockets.len()); + assert_eq!(1, svc.len()); + + // Re-fetch from the cache well after the 5-second deadline - we should refresh. + let (sockets, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + now + Duration::from_secs(100), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + now + Duration::from_secs(100), + ) + }; + + assert!(refreshed); + assert_eq!(num_nodes - num_zero_stake_nodes, sockets.len()); + assert_eq!(1, svc.len()); + } + + #[test_case(true)] + #[test_case(false)] + fn test_cache_eviction(use_alpenglow_socket: bool) { + // Create our harness + let (keypair_map, vahm) = build_epoch_stakes(50, 7, 60); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(123); + + let base_slot = 325_000_000_000; + let (bank_forks, cluster_info) = + StakedValidatorsCacheHarness::new(&genesis_config, Keypair::new()) + .with_vote_accounts(base_slot, keypair_map, vahm, Protocol::UDP) + .bank_forks(); + + // Create our staked validators cache + let mut svc = StakedValidatorsCache::new( + bank_forks, + Protocol::UDP, + Duration::from_secs(5), + 5, + true, + None, + ); + + assert_eq!(0, svc.len()); + assert!(svc.is_empty()); + + let now = Instant::now(); + + // Populate the first five entries; accessing the cache once again shouldn't trigger any + // refreshes. + for entry_ix in 1..=5 { + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + entry_ix * base_slot, + &cluster_info, + now, + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + entry_ix * base_slot, + &cluster_info, + now, + ) + }; + assert!(refreshed); + assert_eq!(entry_ix as usize, svc.len()); + + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + entry_ix * base_slot, + &cluster_info, + now, + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + entry_ix * base_slot, + &cluster_info, + now, + ) + }; + assert!(!refreshed); + assert_eq!(entry_ix as usize, svc.len()); + } + + // Entry 6 - this shouldn't increase the cache length. + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + 6 * base_slot, + &cluster_info, + now, + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports(6 * base_slot, &cluster_info, now) + }; + assert!(refreshed); + assert_eq!(5, svc.len()); + + // Epoch 1 should have been evicted + assert!(!svc.cache.contains(&svc.cur_epoch(base_slot))); + + // Epochs 2 - 6 should have entries + for entry_ix in 2..=6 { + assert!(svc.cache.contains(&svc.cur_epoch(entry_ix * base_slot))); + } + + // Accessing the cache after TTL should recalculate everything; the size remains 5, since + // we only ever lazily evict cache entries. + for entry_ix in 1..=5 { + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + entry_ix * base_slot, + &cluster_info, + now + Duration::from_secs(10), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + entry_ix * base_slot, + &cluster_info, + now + Duration::from_secs(10), + ) + }; + assert!(refreshed); + assert_eq!(5, svc.len()); + } + } + + #[test_case(true)] + #[test_case(false)] + fn test_only_update_once_per_epoch(use_alpenglow_socket: bool) { + let slot_num = 325_000_000_u64; + let num_nodes = 10_usize; + let num_zero_stake_nodes = 2_usize; + let num_vote_accounts = 10_usize; + let genesis_lamports = 123_u64; + let protocol = Protocol::UDP; + + // Create our harness + let (keypair_map, vahm) = + build_epoch_stakes(num_nodes, num_zero_stake_nodes, num_vote_accounts); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(genesis_lamports); + + let (bank_forks, cluster_info) = + StakedValidatorsCacheHarness::new(&genesis_config, Keypair::new()) + .with_vote_accounts(slot_num, keypair_map, vahm, protocol) + .bank_forks(); + + // Create our staked validators cache + let mut svc = + StakedValidatorsCache::new(bank_forks, protocol, Duration::from_secs(5), 5, true, None); + + let now = Instant::now(); + + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports(slot_num, &cluster_info, now) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports(slot_num, &cluster_info, now) + }; + assert!(refreshed); + + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports(slot_num, &cluster_info, now) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports(slot_num, &cluster_info, now) + }; + assert!(!refreshed); + + let (_, refreshed) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports(2 * slot_num, &cluster_info, now) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports(2 * slot_num, &cluster_info, now) + }; + assert!(refreshed); + } + + #[test_matrix( + [1_usize, 10_usize], + [Protocol::UDP, Protocol::QUIC], + [false, true] +)] + fn test_exclude_self_from_cache( + num_nodes: usize, + protocol: Protocol, + use_alpenglow_socket: bool, + ) { + let slot_num = 325_000_000_u64; + let num_vote_accounts = 10_usize; + let genesis_lamports = 123_u64; + + // Create our harness + let (keypair_map, vahm) = build_epoch_stakes(num_nodes, 0, num_vote_accounts); + + // Fetch some keypair from the keypair map + let keypair = keypair_map.values().next().unwrap().insecure_clone(); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(genesis_lamports); + + let (bank_forks, cluster_info) = + StakedValidatorsCacheHarness::new(&genesis_config, keypair.insecure_clone()) + .with_vote_accounts(slot_num, keypair_map, vahm, protocol) + .bank_forks(); + + let my_socket_addr = cluster_info + .lookup_contact_info(&keypair.pubkey(), |node| { + if use_alpenglow_socket { + node.alpenglow().unwrap() + } else { + node.tpu_vote(protocol).unwrap() + } + }) + .unwrap(); + + // Create our staked validators cache - set include_self to true + let mut svc = StakedValidatorsCache::new( + bank_forks.clone(), + protocol, + Duration::from_secs(5), + 5, + true, + None, + ); + + let (sockets, _) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + Instant::now(), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + Instant::now(), + ) + }; + assert_eq!(sockets.len(), num_nodes); + assert!(sockets.contains(&my_socket_addr)); + + // Create our staked validators cache - set include_self to false + let mut svc = StakedValidatorsCache::new( + bank_forks.clone(), + protocol, + Duration::from_secs(5), + 5, + false, + None, + ); + + let (sockets, _) = if use_alpenglow_socket { + svc.get_staked_validators_by_slot_with_alpenglow_ports( + slot_num, + &cluster_info, + Instant::now(), + ) + } else { + svc.get_staked_validators_by_slot_with_tpu_vote_ports( + slot_num, + &cluster_info, + Instant::now(), + ) + }; + // We should have num_nodes - 1 sockets, since we exclude our own socket address. + assert_eq!(sockets.len(), num_nodes - 1); + assert!(!sockets.contains(&my_socket_addr)); + } + + #[test] + fn test_alpenglow_port_override() { + let (keypair_map, vahm) = build_epoch_stakes(3, 0, 3); + let pubkey_b = *keypair_map.keys().nth(1).unwrap(); + let keypair = keypair_map.values().next().unwrap().insecure_clone(); + + let alpenglow_port_override = AlpenglowPortOverride::default(); + let blackhole_addr: SocketAddr = "0.0.0.0:0".parse().unwrap(); + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(100); + + let (bank_forks, cluster_info) = + StakedValidatorsCacheHarness::new(&genesis_config, keypair.insecure_clone()) + .with_vote_accounts(0, keypair_map, vahm, Protocol::UDP) + .bank_forks(); + + // Create our staked validators cache - set include_self to false + let mut svc = StakedValidatorsCache::new( + bank_forks.clone(), + Protocol::UDP, + Duration::from_secs(5), + 5, + false, + Some(alpenglow_port_override.clone()), + ); + // Nothing in the override, so we should get the original socket addresses. + let (sockets, _) = svc.get_staked_validators_by_slot_with_alpenglow_ports( + 0, + &cluster_info, + Instant::now(), + ); + assert_eq!(sockets.len(), 2); + assert!(!sockets.contains(&blackhole_addr)); + + // Add an override for pubkey_B, and check that we get the overridden socket address. + alpenglow_port_override.update_override(HashMap::from([(pubkey_b, blackhole_addr)])); + let (sockets, _) = svc.get_staked_validators_by_slot_with_alpenglow_ports( + 0, + &cluster_info, + Instant::now(), + ); + assert_eq!(sockets.len(), 2); + // Sort sockets to ensure the blackhole address is at index 0. + let mut sockets: Vec<_> = sockets.to_vec(); + sockets.sort(); + assert_eq!(sockets[0], blackhole_addr); + assert_ne!(sockets[1], blackhole_addr); + + // Now clear the override, and check that we get the original socket addresses. + alpenglow_port_override.clear(); + let (sockets, _) = svc.get_staked_validators_by_slot_with_alpenglow_ports( + 0, + &cluster_info, + Instant::now(), + ); + assert_eq!(sockets.len(), 2); + assert!(!sockets.contains(&blackhole_addr)); + } +} diff --git a/core/src/tpu.rs b/core/src/tpu.rs index b5c3b08dc2..672fdef0c0 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -18,7 +18,9 @@ use { }, fetch_stage::FetchStage, forwarding_stage::ForwardingStage, - sigverify::TransactionSigVerifier, + sigverifier::{ + bls_sigverifier::BLSSigVerifier, ed25519_sigverifier::TransactionSigVerifier, + }, sigverify_stage::SigVerifyStage, staked_nodes_updater_service::StakedNodesUpdaterService, tpu_entry_notifier::TpuEntryNotifier, @@ -40,9 +42,10 @@ use { }, solana_runtime::{ bank_forks::BankForks, + epoch_stakes_service::EpochStakesService, prioritization_fee_cache::PrioritizationFeeCache, root_bank_cache::RootBankCache, - vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender}, + vote_sender_types::{BLSVerifiedMessageSender, ReplayVoteReceiver, ReplayVoteSender}, }, solana_sdk::{clock::Slot, pubkey::Pubkey, quic::NotifyKeyUpdate, signature::Keypair}, solana_streamer::{ @@ -50,6 +53,7 @@ use { streamer::StakedNodes, }, solana_turbine::broadcast_stage::{BroadcastStage, BroadcastStageType}, + solana_votor::event::VotorEventSender, std::{ collections::HashMap, net::{SocketAddr, UdpSocket}, @@ -60,6 +64,9 @@ use { tokio::sync::mpsc::Sender as AsyncSender, }; +// The maximum number of alpenglow packets that can be processed in a single batch +pub const MAX_ALPENGLOW_PACKET_NUM: usize = 10000; + pub struct TpuSockets { pub transactions: Vec, pub transaction_forwards: Vec, @@ -68,12 +75,14 @@ pub struct TpuSockets { pub transactions_quic: Vec, pub transactions_forwards_quic: Vec, pub vote_quic: Vec, + pub alpenglow: UdpSocket, } pub struct Tpu { fetch_stage: FetchStage, sigverify_stage: SigVerifyStage, vote_sigverify_stage: SigVerifyStage, + alpenglow_sigverify_stage: SigVerifyStage, banking_stage: BankingStage, forwarding_stage: JoinHandle<()>, cluster_info_vote_listener: ClusterInfoVoteListener, @@ -110,8 +119,10 @@ impl Tpu { bank_notification_sender: Option, tpu_coalesce: Duration, duplicate_confirmed_slot_sender: DuplicateConfirmedSlotsSender, + bls_verified_message_sender: BLSVerifiedMessageSender, connection_cache: &Arc, turbine_quic_endpoint_sender: AsyncSender<(SocketAddr, Bytes)>, + votor_event_sender: VotorEventSender, keypair: &Keypair, log_messages_bytes_limit: Option, staked_nodes: &Arc>, @@ -136,18 +147,22 @@ impl Tpu { transactions_quic: transactions_quic_sockets, transactions_forwards_quic: transactions_forwards_quic_sockets, vote_quic: tpu_vote_quic_sockets, + alpenglow: alpenglow_socket, } = sockets; let (packet_sender, packet_receiver) = unbounded(); let (vote_packet_sender, vote_packet_receiver) = unbounded(); let (forwarded_packet_sender, forwarded_packet_receiver) = unbounded(); + let (bls_packet_sender, bls_packet_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let fetch_stage = FetchStage::new_with_sender( transactions_sockets, tpu_forwards_sockets, tpu_vote_sockets, + alpenglow_socket, exit.clone(), &packet_sender, &vote_packet_sender, + &bls_packet_sender, &forwarded_packet_sender, forwarded_packet_receiver, poh_recorder, @@ -245,6 +260,25 @@ impl Tpu { ) }; + let alpenglow_sigverify_stage = { + let (tx, rx) = unbounded(); + bank_forks.write().unwrap().register_new_bank_subscriber(tx); + let bank = bank_forks.read().unwrap().root_bank(); + let epoch = bank.epoch(); + let epoch_stakes_service = Arc::new(EpochStakesService::new(bank, epoch, rx)); + let verifier = BLSSigVerifier::new( + epoch_stakes_service, + verified_vote_sender.clone(), + bls_verified_message_sender, + ); + SigVerifyStage::new( + bls_packet_receiver, + verifier, + "solSigVerAlpenglow", + "tpu-alpenglow-verifier", + ) + }; + let cluster_info_vote_listener = ClusterInfoVoteListener::new( exit.clone(), cluster_info.clone(), @@ -307,6 +341,7 @@ impl Tpu { bank_forks, shred_version, turbine_quic_endpoint_sender, + votor_event_sender, ); ( @@ -314,6 +349,7 @@ impl Tpu { fetch_stage, sigverify_stage, vote_sigverify_stage, + alpenglow_sigverify_stage, banking_stage, forwarding_stage, cluster_info_vote_listener, @@ -334,6 +370,7 @@ impl Tpu { self.fetch_stage.join(), self.sigverify_stage.join(), self.vote_sigverify_stage.join(), + self.alpenglow_sigverify_stage.join(), self.cluster_info_vote_listener.join(), self.banking_stage.join(), self.forwarding_stage.join(), diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 4416e41532..f5703f10b6 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -4,6 +4,7 @@ use { crate::{ banking_trace::BankingTracer, + block_creation_loop::ReplayHighestFrozen, cluster_info_vote_listener::{ DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, VerifiedVoteReceiver, VoteTracker, @@ -16,12 +17,12 @@ use { repair::repair_service::{OutstandingShredRepairs, RepairInfo, RepairServiceChannels}, replay_stage::{ReplayReceivers, ReplaySenders, ReplayStage, ReplayStageConfig}, shred_fetch_stage::ShredFetchStage, - voting_service::VotingService, + voting_service::{VotingService, VotingServiceOverride}, warm_quic_cache_service::WarmQuicCacheService, window_service::{WindowService, WindowServiceChannels}, }, bytes::Bytes, - crossbeam_channel::{unbounded, Receiver, Sender}, + crossbeam_channel::{bounded, unbounded, Receiver, Sender}, solana_client::connection_cache::ConnectionCache, solana_geyser_plugin_manager::block_metadata_notifier_interface::BlockMetadataNotifierArc, solana_gossip::{ @@ -40,12 +41,22 @@ use { rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier, }, solana_runtime::{ - accounts_background_service::AbsRequestSender, bank_forks::BankForks, - commitment::BlockCommitmentCache, prioritization_fee_cache::PrioritizationFeeCache, - vote_sender_types::ReplayVoteSender, + accounts_background_service::AbsRequestSender, + bank_forks::BankForks, + commitment::BlockCommitmentCache, + prioritization_fee_cache::PrioritizationFeeCache, + vote_sender_types::{ + BLSVerifiedMessageReceiver, BLSVerifiedMessageSender, ReplayVoteSender, + }, }, solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Keypair}, solana_turbine::retransmit_stage::RetransmitStage, + solana_votor::{ + event::{VotorEventReceiver, VotorEventSender}, + vote_history::VoteHistory, + vote_history_storage::VoteHistoryStorage, + votor::LeaderWindowNotifier, + }, std::{ collections::HashSet, net::{SocketAddr, UdpSocket}, @@ -126,6 +137,8 @@ impl Tvu { poh_recorder: &Arc>, tower: Tower, tower_storage: Arc, + vote_history: VoteHistory, + vote_history_storage: Arc, leader_schedule_cache: &Arc, exit: Arc, block_commitment_cache: Arc>, @@ -141,6 +154,8 @@ impl Tvu { completed_data_sets_sender: Option, bank_notification_sender: Option, duplicate_confirmed_slots_receiver: DuplicateConfirmedSlotsReceiver, + own_vote_sender: BLSVerifiedMessageSender, + bls_verified_message_receiver: BLSVerifiedMessageReceiver, tvu_config: TvuConfig, max_slots: &Arc, block_metadata_notifier: Option, @@ -161,6 +176,11 @@ impl Tvu { wen_restart_repair_slots: Option>>>, slot_status_notifier: Option, vote_connection_cache: Arc, + replay_highest_frozen: Arc, + leader_window_notifier: Arc, + voting_service_test_override: Option, + votor_event_sender: VotorEventSender, + votor_event_receiver: VotorEventReceiver, ) -> Result { let in_wen_restart = wen_restart_repair_slots.is_some(); @@ -220,6 +240,7 @@ impl Tvu { unbounded(); let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); let (popular_pruned_forks_sender, popular_pruned_forks_receiver) = unbounded(); + let (certificate_sender, certificate_receiver) = unbounded(); let window_service = { let epoch_schedule = bank_forks .read() @@ -262,6 +283,7 @@ impl Tvu { window_service_channels, leader_schedule_cache.clone(), outstanding_repair_requests, + certificate_receiver, ) }; @@ -278,6 +300,10 @@ impl Tvu { let (cost_update_sender, cost_update_receiver) = unbounded(); let (drop_bank_sender, drop_bank_receiver) = unbounded(); let (voting_sender, voting_receiver) = unbounded(); + // The BLS sender channel should be mostly used during standstill handling, + // there could be 10s/400ms = 25 slots, <=5 votes and <=5 certificates per slot, + // we cap the channel at 512 to give some headroom. + let (bls_sender, bls_receiver) = bounded(512); let replay_senders = ReplaySenders { rpc_subscriptions: rpc_subscriptions.clone(), @@ -293,9 +319,13 @@ impl Tvu { cluster_slots_update_sender, cost_update_sender, voting_sender, + bls_sender, drop_bank_sender, block_metadata_notifier, dumped_slots_sender, + certificate_sender, + votor_event_sender, + own_vote_sender, }; let replay_receivers = ReplayReceivers { @@ -305,6 +335,8 @@ impl Tvu { duplicate_confirmed_slots_receiver, gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, + bls_verified_message_receiver, + votor_event_receiver, }; let replay_stage_config = ReplayStageConfig { @@ -323,19 +355,27 @@ impl Tvu { cluster_info: cluster_info.clone(), poh_recorder: poh_recorder.clone(), tower, + vote_history, + vote_history_storage: vote_history_storage.clone(), vote_tracker, cluster_slots, log_messages_bytes_limit, prioritization_fee_cache: prioritization_fee_cache.clone(), banking_tracer, + replay_highest_frozen, + leader_window_notifier, }; let voting_service = VotingService::new( voting_receiver, + bls_receiver, cluster_info.clone(), poh_recorder.clone(), tower_storage, + vote_history_storage.clone(), vote_connection_cache.clone(), + bank_forks.clone(), + voting_service_test_override, ); let warm_quic_cache_service = create_cache_warmer_if_needed( @@ -458,6 +498,7 @@ pub mod tests { solana_sdk::signature::{Keypair, Signer}, solana_streamer::socket::SocketAddrSpace, solana_tpu_client::tpu_client::{DEFAULT_TPU_CONNECTION_POOL_SIZE, DEFAULT_VOTE_USE_QUIC}, + solana_votor::vote_history_storage::FileVoteHistoryStorage, std::sync::atomic::{AtomicU64, Ordering}, }; @@ -506,6 +547,7 @@ pub mod tests { let (_verified_vote_sender, verified_vote_receiver) = unbounded(); let (replay_vote_sender, _replay_vote_receiver) = unbounded(); let (_, gossip_confirmed_slots_receiver) = unbounded(); + let (bls_verified_message_sender, bls_verified_message_receiver) = unbounded(); let max_complete_transaction_status_slot = Arc::new(AtomicU64::default()); let max_complete_rewards_slot = Arc::new(AtomicU64::default()); let ignored_prioritization_fee_cache = Arc::new(PrioritizationFeeCache::new(0u64)); @@ -527,6 +569,7 @@ pub mod tests { DEFAULT_TPU_CONNECTION_POOL_SIZE, ) }; + let (votor_event_sender, votor_event_receiver) = unbounded(); let tvu = Tvu::new( &vote_keypair.pubkey(), @@ -554,6 +597,8 @@ pub mod tests { &poh_recorder, Tower::default(), Arc::new(FileTowerStorage::default()), + VoteHistory::default(), + Arc::new(FileVoteHistoryStorage::default()), &leader_schedule_cache, exit.clone(), block_commitment_cache, @@ -569,6 +614,8 @@ pub mod tests { /*completed_data_sets_sender:*/ None, None, gossip_confirmed_slots_receiver, + bls_verified_message_sender, + bls_verified_message_receiver, TvuConfig::default(), &Arc::new(MaxSlots::default()), None, @@ -589,6 +636,11 @@ pub mod tests { wen_restart_repair_slots, None, Arc::new(connection_cache), + Arc::new(ReplayHighestFrozen::default()), + Arc::new(LeaderWindowNotifier::default()), + None, + votor_event_sender, + votor_event_receiver, ) .expect("assume success"); if enable_wen_restart { diff --git a/core/src/validator.rs b/core/src/validator.rs index 372a373400..7906d998e0 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -6,6 +6,7 @@ use { accounts_hash_verifier::AccountsHashVerifier, admin_rpc_post_init::AdminRpcRequestMetadataPostInit, banking_trace::{self, BankingTracer, TraceError}, + block_creation_loop::{self, BlockCreationLoopConfig, ReplayHighestFrozen}, cluster_info_vote_listener::VoteTracker, completed_data_sets_service::CompletedDataSetsService, consensus::{ @@ -17,18 +18,19 @@ use { repair::{ self, quic_endpoint::{RepairQuicAsyncSenders, RepairQuicSenders, RepairQuicSockets}, - serve_repair::ServeRepair, + repair_handler::RepairHandlerType, serve_repair_service::ServeRepairService, }, sample_performance_service::SamplePerformanceService, - sigverify, + sigverifier::ed25519_sigverifier, snapshot_packager_service::{PendingSnapshotPackages, SnapshotPackagerService}, stats_reporter_service::StatsReporterService, system_monitor_service::{ verify_net_stats_access, SystemMonitorService, SystemMonitorStatsReportConfig, }, - tpu::{Tpu, TpuSockets, DEFAULT_TPU_COALESCE}, + tpu::{Tpu, TpuSockets, DEFAULT_TPU_COALESCE, MAX_ALPENGLOW_PACKET_NUM}, tvu::{Tvu, TvuConfig, TvuSockets}, + voting_service::VotingServiceOverride, }, anyhow::{anyhow, Context, Result}, crossbeam_channel::{bounded, unbounded, Receiver}, @@ -131,6 +133,11 @@ use { solana_turbine::{self, broadcast_stage::BroadcastStageType}, solana_unified_scheduler_pool::DefaultSchedulerPool, solana_vote_program::vote_state, + solana_votor::{ + vote_history::{VoteHistory, VoteHistoryError}, + vote_history_storage::{NullVoteHistoryStorage, VoteHistoryStorage}, + votor::LeaderWindowNotifier, + }, solana_wen_restart::wen_restart::{wait_for_wen_restart, WenRestartConfig}, std::{ borrow::Cow, @@ -276,6 +283,7 @@ pub struct ValidatorConfig { pub run_verification: bool, pub require_tower: bool, pub tower_storage: Arc, + pub vote_history_storage: Arc, pub debug_keys: Option>>, pub contact_debug_interval: u64, pub contact_save_interval: u64, @@ -315,6 +323,8 @@ pub struct ValidatorConfig { pub replay_transactions_threads: NonZeroUsize, pub tvu_shred_sigverify_threads: NonZeroUsize, pub delay_leader_block_for_pending_fork: bool, + pub voting_service_test_override: Option, + pub repair_handler_type: RepairHandlerType, } impl Default for ValidatorConfig { @@ -349,6 +359,7 @@ impl Default for ValidatorConfig { run_verification: true, require_tower: false, tower_storage: Arc::new(NullTowerStorage::default()), + vote_history_storage: Arc::new(NullVoteHistoryStorage::default()), debug_keys: None, contact_debug_interval: DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS, contact_save_interval: DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS, @@ -388,6 +399,8 @@ impl Default for ValidatorConfig { replay_transactions_threads: NonZeroUsize::new(1).expect("1 is non-zero"), tvu_shred_sigverify_threads: NonZeroUsize::new(1).expect("1 is non-zero"), delay_leader_block_for_pending_fork: false, + voting_service_test_override: None, + repair_handler_type: RepairHandlerType::default(), } } } @@ -696,7 +709,7 @@ impl Validator { } else { info!("Initializing sigverify..."); } - sigverify::init(); + ed25519_sigverifier::init(); info!("Initializing sigverify done."); if !ledger_path.is_dir() { @@ -943,6 +956,8 @@ impl Validator { ); let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (bls_verified_message_sender, bls_verified_message_receiver) = + bounded(MAX_ALPENGLOW_PACKET_NUM); // block min prioritization fee cache should be readable by RPC, and writable by validator // (by both replay stage and banking stage) @@ -952,7 +967,15 @@ impl Validator { let startup_verification_complete; let (poh_recorder, entry_receiver, record_receiver) = { let bank = &bank_forks.read().unwrap().working_bank(); + let highest_frozen_bank = bank_forks.read().unwrap().highest_frozen_bank(); startup_verification_complete = Arc::clone(bank.get_startup_verification_complete()); + let first_alpenglow_slot = highest_frozen_bank.as_ref().and_then(|hfb| { + hfb.feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()) + }); + let is_alpenglow_enabled = highest_frozen_bank + .zip(first_alpenglow_slot) + .is_some_and(|(hfs, fas)| hfs.slot() >= fas); PohRecorder::new_with_clear_signal( bank.tick_height(), bank.last_blockhash(), @@ -966,6 +989,7 @@ impl Validator { &genesis_config.poh_config, Some(poh_timing_point_sender), exit.clone(), + is_alpenglow_enabled, ) }; let poh_recorder = Arc::new(RwLock::new(poh_recorder)); @@ -1014,6 +1038,11 @@ impl Validator { let entry_notification_sender = entry_notifier_service .as_ref() .map(|service| service.sender()); + + let is_alpenglow = genesis_config + .accounts + .contains_key(&solana_feature_set::secp256k1_program_enabled::id()); + let mut process_blockstore = ProcessBlockStore::new( &id, vote_account, @@ -1029,6 +1058,7 @@ impl Validator { blockstore_root_scan, accounts_background_request_sender.clone(), config, + is_alpenglow, ); maybe_warp_slot( @@ -1300,7 +1330,8 @@ impl Validator { Some(stats_reporter_sender.clone()), exit.clone(), ); - let serve_repair = ServeRepair::new( + let serve_repair = config.repair_handler_type.create_serve_repair( + blockstore.clone(), cluster_info.clone(), bank_forks.clone(), config.repair_whitelist.clone(), @@ -1325,6 +1356,27 @@ impl Validator { let wait_for_vote_to_start_leader = !waited_for_supermajority && !config.no_wait_for_vote_to_start_leader; + let replay_highest_frozen = Arc::new(ReplayHighestFrozen::default()); + let leader_window_notifier = Arc::new(LeaderWindowNotifier::default()); + let block_creation_loop_config = BlockCreationLoopConfig { + exit: exit.clone(), + track_transaction_indexes: transaction_status_sender.is_some(), + bank_forks: bank_forks.clone(), + blockstore: blockstore.clone(), + cluster_info: cluster_info.clone(), + poh_recorder: poh_recorder.clone(), + leader_schedule_cache: leader_schedule_cache.clone(), + rpc_subscriptions: rpc_subscriptions.clone(), + banking_tracer: banking_tracer.clone(), + slot_status_notifier: slot_status_notifier.clone(), + record_receiver: record_receiver.clone(), + leader_window_notifier: leader_window_notifier.clone(), + replay_highest_frozen: replay_highest_frozen.clone(), + }; + let block_creation_loop = || { + block_creation_loop::start_loop(block_creation_loop_config); + }; + let poh_service = PohService::new( poh_recorder.clone(), &genesis_config.poh_config, @@ -1333,6 +1385,7 @@ impl Validator { config.poh_pinned_cpu_core, config.poh_hashes_per_batch, record_receiver, + block_creation_loop, ); assert_eq!( blockstore.get_new_shred_signals_len(), @@ -1346,7 +1399,6 @@ impl Validator { let (verified_vote_sender, verified_vote_receiver) = unbounded(); let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); let (duplicate_confirmed_slot_sender, duplicate_confirmed_slots_receiver) = unbounded(); - let entry_notification_sender = entry_notifier_service .as_ref() .map(|service| service.sender_cloned()); @@ -1435,7 +1487,6 @@ impl Validator { repair_request_quic_sender, repair_request_quic_receiver, repair_quic_async_senders.repair_response_quic_sender, - blockstore.clone(), node.sockets.serve_repair, socket_addr_space, stats_reporter_sender, @@ -1448,25 +1499,50 @@ impl Validator { } else { None }; - let tower = match process_blockstore.process_to_create_tower() { - Ok(tower) => { - info!("Tower state: {:?}", tower); - tower - } - Err(e) => { - warn!( - "Unable to retrieve tower: {:?} creating default tower....", - e - ); - Tower::default() - } + let (tower, vote_history) = if genesis_config + .accounts + .contains_key(&solana_feature_set::secp256k1_program_enabled::id()) + { + let vote_history = match process_blockstore.process_to_create_vote_history() { + Ok(vote_history) => { + info!("Vote history: {:?}", vote_history); + vote_history + } + Err(e) => { + warn!( + "Unable to retrieve vote history: {:?} creating default vote history....", + e + ); + VoteHistory::default() + } + }; + (Tower::default(), vote_history) + } else { + let tower = match process_blockstore.process_to_create_tower() { + Ok(tower) => { + info!("Tower state: {:?}", tower); + tower + } + Err(e) => { + warn!( + "Unable to retrieve tower: {:?} creating default tower....", + e + ); + Tower::default() + } + }; + (tower, VoteHistory::default()) }; + let last_vote = tower.last_vote(); let outstanding_repair_requests = Arc::>::default(); let cluster_slots = Arc::new(crate::cluster_slots_service::cluster_slots::ClusterSlots::default()); + // This channel backing up indicates a serious problem in the voting loop + // Capping at 1000 for now, TODO: add metrics for channel len + let (votor_event_sender, votor_event_receiver) = bounded(1000); let tvu = Tvu::new( vote_account, @@ -1485,6 +1561,8 @@ impl Validator { &poh_recorder, tower, config.tower_storage.clone(), + vote_history, + config.vote_history_storage.clone(), &leader_schedule_cache, exit.clone(), block_commitment_cache, @@ -1500,6 +1578,8 @@ impl Validator { completed_data_sets_sender, bank_notification_sender.clone(), duplicate_confirmed_slots_receiver, + bls_verified_message_sender.clone(), + bls_verified_message_receiver, TvuConfig { max_ledger_shreds: config.max_ledger_shreds, shred_version: node.info.shred_version(), @@ -1529,6 +1609,11 @@ impl Validator { wen_restart_repair_slots.clone(), slot_status_notifier, vote_connection_cache, + replay_highest_frozen, + leader_window_notifier, + config.voting_service_test_override.clone(), + votor_event_sender.clone(), + votor_event_receiver, ) .map_err(ValidatorError::Other)?; @@ -1566,6 +1651,7 @@ impl Validator { transactions_quic: node.sockets.tpu_quic, transactions_forwards_quic: node.sockets.tpu_forwards_quic, vote_quic: node.sockets.tpu_vote_quic, + alpenglow: node.sockets.alpenglow, }, &rpc_subscriptions, transaction_status_sender, @@ -1583,8 +1669,10 @@ impl Validator { bank_notification_sender.map(|sender| sender.sender), config.tpu_coalesce, duplicate_confirmed_slot_sender, + bls_verified_message_sender, &connection_cache, turbine_quic_endpoint_sender, + votor_event_sender, &identity_keypair, config.runtime_config.log_messages_bytes_limit, &staked_nodes, @@ -1700,6 +1788,10 @@ impl Validator { "local retransmit address: {}", node.sockets.retransmit_sockets[0].local_addr().unwrap() ); + info!( + "local alpenglow address: {}", + node.sockets.alpenglow.local_addr().unwrap() + ); } pub fn join(self) { @@ -1953,6 +2045,80 @@ fn post_process_restored_tower( Ok(restored_tower) } +fn post_process_restored_vote_history( + restored_vote_history: solana_votor::vote_history_storage::Result, + validator_identity: &Pubkey, + config: &ValidatorConfig, + bank_forks: &BankForks, +) -> Result { + let mut should_require_vote_history = config.require_tower; + + let restored_vote_history = restored_vote_history.and_then(|mut vote_history| { + let root_bank = bank_forks.root_bank(); + + if vote_history.root() < root_bank.slot() { + // Vote history is old, update + vote_history.set_root(root_bank.slot()); + } + + if let Some(hard_fork_restart_slot) = + maybe_cluster_restart_with_hard_fork(config, root_bank.slot()) + { + // intentionally fail to restore vote_history; we're supposedly in a new hard fork; past + // out-of-chain votor state doesn't make sense at all + // what if --wait-for-supermajority again if the validator restarted? + let message = + format!("Hard fork is detected; discarding vote_history restoration result: {vote_history:?}"); + datapoint_error!("vote_history_error", ("error", message, String),); + error!("{}", message); + + // unconditionally relax vote_history requirement + should_require_vote_history = false; + return Err(VoteHistoryError::HardFork( + hard_fork_restart_slot, + )); + } + + if let Some(warp_slot) = config.warp_slot { + // unconditionally relax vote_history requirement + should_require_vote_history = false; + return Err(VoteHistoryError::HardFork(warp_slot)); + } + + Ok(vote_history) + }); + + let restored_vote_history = match restored_vote_history { + Ok(vote_history) => vote_history, + Err(err) => { + if !err.is_file_missing() { + datapoint_error!( + "vote_history_error", + ( + "error", + format!("Unable to restore vote_history: {err}"), + String + ), + ); + } + if should_require_vote_history { + return Err(format!( + "Requested mandatory vote_history restore failed: {err}. Ensure that the vote history \ + storage file has been copied to the correct directory. Aborting" + )); + } + error!( + "Rebuilding an empty vote_history from root slot due to failed restore: {}", + err + ); + + VoteHistory::new(*validator_identity, bank_forks.root()) + } + }; + + Ok(restored_vote_history) +} + fn load_genesis( config: &ValidatorConfig, ledger_path: &Path, @@ -2125,6 +2291,8 @@ pub struct ProcessBlockStore<'a> { accounts_background_request_sender: AbsRequestSender, config: &'a ValidatorConfig, tower: Option, + vote_history: Option, + is_alpenglow: bool, } impl<'a> ProcessBlockStore<'a> { @@ -2144,6 +2312,7 @@ impl<'a> ProcessBlockStore<'a> { blockstore_root_scan: BlockstoreRootScan, accounts_background_request_sender: AbsRequestSender, config: &'a ValidatorConfig, + is_alpenglow: bool, ) -> Self { Self { id, @@ -2161,52 +2330,60 @@ impl<'a> ProcessBlockStore<'a> { accounts_background_request_sender, config, tower: None, + vote_history: None, + is_alpenglow, } } pub(crate) fn process(&mut self) -> Result<(), String> { - if self.tower.is_none() { - let previous_start_process = *self.start_progress.read().unwrap(); - *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; - - let exit = Arc::new(AtomicBool::new(false)); - if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { - let bank_forks = self.bank_forks.clone(); - let exit = exit.clone(); - let start_progress = self.start_progress.clone(); - - let _ = Builder::new() - .name("solRptLdgrStat".to_string()) - .spawn(move || { - while !exit.load(Ordering::Relaxed) { - let slot = bank_forks.read().unwrap().working_bank().slot(); - *start_progress.write().unwrap() = - ValidatorStartProgress::ProcessingLedger { slot, max_slot }; - sleep(Duration::from_secs(2)); - } - }) - .unwrap(); - } - blockstore_processor::process_blockstore_from_root( - self.blockstore, - self.bank_forks, - self.leader_schedule_cache, - self.process_options, - self.transaction_status_sender, - self.block_meta_sender.as_ref(), - self.entry_notification_sender, - &self.accounts_background_request_sender, - ) - .map_err(|err| { - exit.store(true, Ordering::Relaxed); - format!("Failed to load ledger: {err:?}") - })?; + if self.is_alpenglow && self.vote_history.is_some() + || !self.is_alpenglow && self.tower.is_some() + { + return Ok(()); + } + let previous_start_process = *self.start_progress.read().unwrap(); + *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; + + let exit = Arc::new(AtomicBool::new(false)); + if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { + let bank_forks = self.bank_forks.clone(); + let exit = exit.clone(); + let start_progress = self.start_progress.clone(); + + let _ = Builder::new() + .name("solRptLdgrStat".to_string()) + .spawn(move || { + while !exit.load(Ordering::Relaxed) { + let slot = bank_forks.read().unwrap().working_bank().slot(); + *start_progress.write().unwrap() = + ValidatorStartProgress::ProcessingLedger { slot, max_slot }; + sleep(Duration::from_secs(2)); + } + }) + .unwrap(); + } + blockstore_processor::process_blockstore_from_root( + self.blockstore, + self.bank_forks, + self.leader_schedule_cache, + self.process_options, + self.transaction_status_sender, + self.block_meta_sender.as_ref(), + self.entry_notification_sender, + &self.accounts_background_request_sender, + ) + .map_err(|err| { exit.store(true, Ordering::Relaxed); + format!("Failed to load ledger: {err:?}") + })?; + exit.store(true, Ordering::Relaxed); - if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { - blockstore_root_scan.join(); - } + if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { + blockstore_root_scan.join(); + } + if !self.is_alpenglow { + // Load and post process tower self.tower = Some({ let restored_tower = Tower::restore(self.config.tower_storage.as_ref(), self.id); if let Ok(tower) = &restored_tower { @@ -2227,23 +2404,47 @@ impl<'a> ProcessBlockStore<'a> { &self.bank_forks.read().unwrap(), )? }); + } else { + // Load and post process vote history + self.vote_history = Some({ + let restored_vote_history = + VoteHistory::restore(self.config.vote_history_storage.as_ref(), self.id); + if let Ok(vote_history) = &restored_vote_history { + // reconciliation attempt 1 of 2 with vote history + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::VoteHistory(vote_history.root()), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| { + format!("Failed to reconcile blockstore with vote history: {err:?}") + })?; + } - if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( - self.config, - self.bank_forks.read().unwrap().root(), - ) { - // reconciliation attempt 2 of 2 with hard fork - // this should be #2 because hard fork root > tower root in almost all cases - reconcile_blockstore_roots_with_external_source( - ExternalRootSource::HardFork(hard_fork_restart_slot), - self.blockstore, - &mut self.original_blockstore_root, - ) - .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; - } + post_process_restored_vote_history( + restored_vote_history, + self.id, + self.config, + &self.bank_forks.read().unwrap(), + )? + }); + } - *self.start_progress.write().unwrap() = previous_start_process; + if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( + self.config, + self.bank_forks.read().unwrap().root(), + ) { + // reconciliation attempt 2 of 2 with hard fork + // this should be #2 because hard fork root > tower root in almost all cases + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::HardFork(hard_fork_restart_slot), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; } + + *self.start_progress.write().unwrap() = previous_start_process; Ok(()) } @@ -2251,6 +2452,11 @@ impl<'a> ProcessBlockStore<'a> { self.process()?; Ok(self.tower.unwrap()) } + + pub(crate) fn process_to_create_vote_history(mut self) -> Result { + self.process()?; + Ok(self.vote_history.unwrap()) + } } fn maybe_warp_slot( diff --git a/core/src/vote_simulator.rs b/core/src/vote_simulator.rs index 27f42c5924..dc3b61a8b8 100644 --- a/core/src/vote_simulator.rs +++ b/core/src/vote_simulator.rs @@ -14,7 +14,7 @@ use { repair::cluster_slot_state_verifier::{ DuplicateConfirmedSlots, DuplicateSlotsTracker, EpochSlotsFrozenSlots, }, - replay_stage::{HeaviestForkFailures, ReplayStage}, + replay_stage::{HeaviestForkFailures, ReplayStage, TowerBFTStructures}, unfrozen_gossip_verified_vote_hashes::UnfrozenGossipVerifiedVoteHashes, }, crossbeam_channel::unbounded, @@ -42,8 +42,8 @@ pub struct VoteSimulator { pub vote_pubkeys: Vec, pub bank_forks: Arc>, pub progress: ProgressMap, - pub heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice, pub latest_validator_votes_for_frozen_banks: LatestValidatorVotesForFrozenBanks, + pub tbft_structs: TowerBFTStructures, } impl VoteSimulator { @@ -62,8 +62,14 @@ impl VoteSimulator { vote_pubkeys, bank_forks, progress, - heaviest_subtree_fork_choice, latest_validator_votes_for_frozen_banks: LatestValidatorVotesForFrozenBanks::default(), + tbft_structs: TowerBFTStructures { + heaviest_subtree_fork_choice, + duplicate_slots_tracker: DuplicateSlotsTracker::default(), + duplicate_confirmed_slots: DuplicateConfirmedSlots::default(), + unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes::default(), + epoch_slots_frozen_slots: EpochSlotsFrozenSlots::default(), + }, } } @@ -103,8 +109,9 @@ impl VoteSimulator { let tower_sync = if let Some(vote_account) = parent_bank.get_vote_account(&keypairs.vote_keypair.pubkey()) { - let mut vote_state = - TowerVoteState::from(vote_account.vote_state().clone()); + let mut vote_state = TowerVoteState::from( + vote_account.vote_state_view().expect("must be TowerBFT"), + ); vote_state.process_next_vote_slot(parent); TowerSync::new( vote_state.votes, @@ -135,8 +142,10 @@ impl VoteSimulator { let vote_account = new_bank .get_vote_account(&keypairs.vote_keypair.pubkey()) .unwrap(); - let state = vote_account.vote_state(); - assert!(state.votes.iter().any(|lockout| lockout.slot() == parent)); + let vote_state_view = vote_account.vote_state_view().unwrap(); + assert!(vote_state_view + .votes_iter() + .any(|lockout| lockout.slot() == parent)); } } while new_bank.tick_height() < new_bank.max_tick_height() { @@ -149,10 +158,12 @@ impl VoteSimulator { .get_fork_stats_mut(new_bank.slot()) .expect("All frozen banks must exist in the Progress map") .bank_hash = Some(new_bank.hash()); - self.heaviest_subtree_fork_choice.add_new_leaf_slot( - (new_bank.slot(), new_bank.hash()), - Some((new_bank.parent_slot(), new_bank.parent_hash())), - ); + self.tbft_structs + .heaviest_subtree_fork_choice + .add_new_leaf_slot( + (new_bank.slot(), new_bank.hash()), + Some((new_bank.parent_slot(), new_bank.parent_hash())), + ); } walk.forward(); @@ -185,7 +196,7 @@ impl VoteSimulator { &VoteTracker::default(), &ClusterSlots::default(), &self.bank_forks, - &mut self.heaviest_subtree_fork_choice, + &mut self.tbft_structs.heaviest_subtree_fork_choice, &mut self.latest_validator_votes_for_frozen_banks, ); @@ -209,7 +220,7 @@ impl VoteSimulator { &self.progress, tower, &self.latest_validator_votes_for_frozen_banks, - &self.heaviest_subtree_fork_choice, + &self.tbft_structs.heaviest_subtree_fork_choice, ); // Make sure this slot isn't locked out or failing threshold @@ -234,16 +245,12 @@ impl VoteSimulator { &mut self.progress, &AbsRequestSender::default(), None, - &mut self.heaviest_subtree_fork_choice, - &mut DuplicateSlotsTracker::default(), - &mut DuplicateConfirmedSlots::default(), - &mut UnfrozenGossipVerifiedVoteHashes::default(), &mut true, &mut Vec::new(), - &mut EpochSlotsFrozenSlots::default(), &drop_bank_sender, + &mut self.tbft_structs, ) - .unwrap() + .unwrap(); } pub fn create_and_vote_new_branch( diff --git a/core/src/voting_service.rs b/core/src/voting_service.rs index 1871316f6a..ecba7beb4d 100644 --- a/core/src/voting_service.rs +++ b/core/src/voting_service.rs @@ -2,27 +2,37 @@ use { crate::{ consensus::tower_storage::{SavedTowerVersions, TowerStorage}, next_leader::upcoming_leader_tpu_vote_sockets, + staked_validators_cache::StakedValidatorsCache, }, bincode::serialize, - crossbeam_channel::Receiver, + crossbeam_channel::{select, Receiver}, solana_client::connection_cache::ConnectionCache, solana_connection_cache::client_connection::ClientConnection, solana_gossip::cluster_info::ClusterInfo, solana_measure::measure::Measure, solana_poh::poh_recorder::PohRecorder, + solana_runtime::bank_forks::BankForks, solana_sdk::{ clock::{Slot, FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET}, + pubkey::Pubkey, transaction::Transaction, transport::TransportError, }, + solana_votor::{vote_history_storage::VoteHistoryStorage, voting_utils::BLSOp}, + solana_votor_messages::bls_message::BLSMessage, std::{ + collections::HashMap, net::SocketAddr, sync::{Arc, RwLock}, thread::{self, Builder, JoinHandle}, + time::{Duration, Instant}, }, thiserror::Error, }; +const STAKED_VALIDATORS_CACHE_TTL_S: u64 = 5; +const STAKED_VALIDATORS_CACHE_NUM_EPOCH_CAP: usize = 5; + pub enum VoteOp { PushVote { tx: Transaction, @@ -35,15 +45,6 @@ pub enum VoteOp { }, } -impl VoteOp { - fn tx(&self) -> &Transaction { - match self { - VoteOp::PushVote { tx, .. } => tx, - VoteOp::RefreshVote { tx, .. } => tx, - } - } -} - #[derive(Debug, Error)] enum SendVoteError { #[error(transparent)] @@ -54,6 +55,16 @@ enum SendVoteError { TransportError(#[from] TransportError), } +fn send_message( + buf: Vec, + socket: &SocketAddr, + connection_cache: &Arc, +) -> Result<(), TransportError> { + let client = connection_cache.get_connection(socket); + + client.send_data_async(buf) +} + fn send_vote_transaction( cluster_info: &ClusterInfo, transaction: &Transaction, @@ -80,48 +91,147 @@ pub struct VotingService { thread_hdl: JoinHandle<()>, } +/// Override for Alpenglow ports to allow testing with different ports +/// The last_modified is used to determine if the override has changed so +/// StakedValidatorsCache can refresh its cache. +/// Inside the map, the key is the validator's vote pubkey and the value +/// is the overridden socket address. +/// For example, if you want validator A to send messages for validator B's +/// Alpenglow port to a new_address, you would insert an entry into the A's +/// map like this: (B will not get the message as a result): +/// `override_map.insert(validator_b_pubkey, new_address);` +#[derive(Clone, Default)] +pub struct AlpenglowPortOverride { + inner: Arc>, +} + +#[derive(Clone)] +struct AlpenglowPortOverrideInner { + override_map: HashMap, + last_modified: Instant, +} + +impl Default for AlpenglowPortOverrideInner { + fn default() -> Self { + Self { + override_map: HashMap::new(), + last_modified: Instant::now(), + } + } +} + +impl AlpenglowPortOverride { + pub fn update_override(&self, new_override: HashMap) { + let mut inner = self.inner.write().unwrap(); + inner.override_map = new_override; + inner.last_modified = Instant::now(); + } + + pub fn has_new_override(&self, previous: Instant) -> bool { + self.inner.read().unwrap().last_modified != previous + } + + pub fn last_modified(&self) -> Instant { + self.inner.read().unwrap().last_modified + } + + pub fn clear(&self) { + let mut inner = self.inner.write().unwrap(); + inner.override_map.clear(); + inner.last_modified = Instant::now(); + } + + pub fn get_override_map(&self) -> HashMap { + self.inner.read().unwrap().override_map.clone() + } +} + +#[derive(Clone)] +pub struct VotingServiceOverride { + pub additional_listeners: Vec, + pub alpenglow_port_override: AlpenglowPortOverride, +} + impl VotingService { pub fn new( vote_receiver: Receiver, + bls_receiver: Receiver, cluster_info: Arc, poh_recorder: Arc>, tower_storage: Arc, + vote_history_storage: Arc, connection_cache: Arc, + bank_forks: Arc>, + test_override: Option, ) -> Self { + let (additional_listeners, alpenglow_port_override) = test_override + .map(|test_override| { + ( + Some(test_override.additional_listeners), + Some(test_override.alpenglow_port_override), + ) + }) + .unwrap_or((None, None)); let thread_hdl = Builder::new() .name("solVoteService".to_string()) .spawn(move || { - for vote_op in vote_receiver.iter() { - Self::handle_vote( - &cluster_info, - &poh_recorder, - tower_storage.as_ref(), - vote_op, - connection_cache.clone(), - ); + let mut staked_validators_cache = StakedValidatorsCache::new( + bank_forks.clone(), + connection_cache.protocol(), + Duration::from_secs(STAKED_VALIDATORS_CACHE_TTL_S), + STAKED_VALIDATORS_CACHE_NUM_EPOCH_CAP, + false, + alpenglow_port_override, + ); + + loop { + select! { + recv(vote_receiver) -> vote_op => { + match vote_op { + Ok(vote_op) => { + Self::handle_vote( + &cluster_info, + &poh_recorder, + tower_storage.as_ref(), + vote_op, + connection_cache.clone(), + ); + } + Err(_) => { + break; + } + } + } + recv(bls_receiver) -> bls_op => { + match bls_op { + Ok(bls_op) => { + Self::handle_bls_vote( + &cluster_info, + vote_history_storage.as_ref(), + bls_op, + connection_cache.clone(), + additional_listeners.as_ref(), + &mut staked_validators_cache, + ); + } + Err(_) => { + break; + } + } + } + } } }) .unwrap(); Self { thread_hdl } } - pub fn handle_vote( + fn broadcast_tower_vote( cluster_info: &ClusterInfo, poh_recorder: &RwLock, - tower_storage: &dyn TowerStorage, - vote_op: VoteOp, - connection_cache: Arc, + tx: &Transaction, + connection_cache: &Arc, ) { - if let VoteOp::PushVote { saved_tower, .. } = &vote_op { - let mut measure = Measure::start("tower storage save"); - if let Err(err) = tower_storage.store(saved_tower) { - error!("Unable to save tower to storage: {:?}", err); - std::process::exit(1); - } - measure.stop(); - trace!("{measure}"); - } - // Attempt to send our vote transaction to the leaders for the next few // slots. From the current slot to the forwarding slot offset // (inclusive). @@ -129,10 +239,13 @@ impl VotingService { FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET.saturating_add(1); #[cfg(test)] static_assertions::const_assert_eq!(UPCOMING_LEADER_FANOUT_SLOTS, 3); + + let leader_fanout = UPCOMING_LEADER_FANOUT_SLOTS; + let upcoming_leader_sockets = upcoming_leader_tpu_vote_sockets( cluster_info, poh_recorder, - UPCOMING_LEADER_FANOUT_SLOTS, + leader_fanout, connection_cache.protocol(), ); @@ -140,20 +253,123 @@ impl VotingService { for tpu_vote_socket in upcoming_leader_sockets { let _ = send_vote_transaction( cluster_info, - vote_op.tx(), + tx, Some(tpu_vote_socket), - &connection_cache, + connection_cache, ); } } else { // Send to our own tpu vote socket if we cannot find a leader to send to - let _ = send_vote_transaction(cluster_info, vote_op.tx(), None, &connection_cache); + let _ = send_vote_transaction(cluster_info, tx, None, connection_cache); } + } + + fn broadcast_alpenglow_message( + slot: Slot, + cluster_info: &ClusterInfo, + bls_message: &BLSMessage, + connection_cache: Arc, + additional_listeners: Option<&Vec>, + staked_validators_cache: &mut StakedValidatorsCache, + ) { + let (staked_validator_alpenglow_sockets, _) = staked_validators_cache + .get_staked_validators_by_slot_with_alpenglow_ports(slot, cluster_info, Instant::now()); + let sockets = additional_listeners + .map(|v| v.as_slice()) + .unwrap_or(&[]) + .iter() + .chain(staked_validator_alpenglow_sockets.iter()); + let buf = match serialize(bls_message) { + Ok(buf) => buf, + Err(err) => { + error!("Failed to serialize alpenglow message: {:?}", err); + return; + } + }; + + // We use send_message in a loop right now because we worry that sending packets too fast + // will cause a packet spike and overwhelm the network. If we later find out that this is + // not an issue, we can optimize this by using multi_targret_send or similar methods. + for alpenglow_socket in sockets { + if let Err(e) = send_message(buf.clone(), alpenglow_socket, &connection_cache) { + warn!( + "Failed to send alpenglow message to {}: {:?}", + alpenglow_socket, e + ); + } + } + } + + pub fn handle_bls_vote( + cluster_info: &ClusterInfo, + vote_history_storage: &dyn VoteHistoryStorage, + bls_op: BLSOp, + connection_cache: Arc, + additional_listeners: Option<&Vec>, + staked_validators_cache: &mut StakedValidatorsCache, + ) { + match bls_op { + BLSOp::PushVote { + bls_message, + slot, + saved_vote_history, + } => { + let mut measure = Measure::start("alpenglow vote history save"); + if let Err(err) = vote_history_storage.store(&saved_vote_history) { + error!("Unable to save vote history to storage: {:?}", err); + std::process::exit(1); + } + measure.stop(); + trace!("{measure}"); + + Self::broadcast_alpenglow_message( + slot, + cluster_info, + &bls_message, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + } + BLSOp::PushCertificate { certificate } => { + let vote_slot = certificate.certificate.slot(); + let bls_message = BLSMessage::Certificate((*certificate).clone()); + Self::broadcast_alpenglow_message( + vote_slot, + cluster_info, + &bls_message, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + } + } + } + + pub fn handle_vote( + cluster_info: &ClusterInfo, + poh_recorder: &RwLock, + tower_storage: &dyn TowerStorage, + vote_op: VoteOp, + connection_cache: Arc, + ) { match vote_op { VoteOp::PushVote { - tx, tower_slots, .. + tx, + tower_slots, + saved_tower, } => { + let mut measure = Measure::start("tower storage save"); + if let Err(err) = tower_storage.store(&saved_tower) { + error!("Unable to save tower to storage: {:?}", err); + std::process::exit(1); + } + measure.stop(); + trace!("{measure}"); + + Self::broadcast_tower_vote(cluster_info, poh_recorder, &tx, &connection_cache); + cluster_info.push_vote(&tower_slots, tx); } VoteOp::RefreshVote { @@ -169,3 +385,162 @@ impl VotingService { self.thread_hdl.join() } } + +#[cfg(test)] +mod tests { + use { + super::*, + crate::consensus::tower_storage::NullTowerStorage, + bitvec::prelude::*, + solana_bls_signatures::Signature as BLSSignature, + solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo}, + solana_ledger::{ + blockstore::Blockstore, get_tmp_ledger_path_auto_delete, + leader_schedule_cache::LeaderScheduleCache, + }, + solana_poh_config::PohConfig, + solana_runtime::{ + bank::Bank, + bank_forks::BankForks, + genesis_utils::{ + create_genesis_config_with_alpenglow_vote_accounts_no_program, + ValidatorVoteKeypairs, + }, + }, + solana_sdk::signer::{keypair::Keypair, Signer}, + solana_streamer::{ + packet::{Packet, PacketBatch}, + recvmmsg::recv_mmsg, + socket::SocketAddrSpace, + }, + solana_votor::vote_history_storage::{ + NullVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, + }, + solana_votor_messages::{ + bls_message::{ + BLSMessage, Certificate, CertificateMessage, CertificateType, VoteMessage, + }, + vote::Vote, + }, + std::{ + net::SocketAddr, + sync::{atomic::AtomicBool, Arc, RwLock}, + }, + test_case::test_case, + }; + + fn create_voting_service( + vote_receiver: Receiver, + bls_receiver: Receiver, + listener: SocketAddr, + ) -> VotingService { + // Create 10 node validatorvotekeypairs vec + let validator_keypairs = (0..10) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let genesis = create_genesis_config_with_alpenglow_vote_accounts_no_program( + 1_000_000_000, + &validator_keypairs, + vec![100; validator_keypairs.len()], + ); + let bank0 = Bank::new_for_tests(&genesis.genesis_config); + let bank_forks = BankForks::new_rw_arc(bank0); + let keypair = Keypair::new(); + let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0); + let cluster_info = ClusterInfo::new( + contact_info, + Arc::new(keypair), + SocketAddrSpace::Unspecified, + ); + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()) + .expect("Expected to be able to open database ledger"); + let working_bank = bank_forks.read().unwrap().working_bank(); + let poh_recorder = PohRecorder::new( + working_bank.tick_height(), + working_bank.last_blockhash(), + working_bank.clone(), + None, + working_bank.ticks_per_slot(), + Arc::new(blockstore), + &Arc::new(LeaderScheduleCache::new_from_bank(&working_bank)), + &PohConfig::default(), + Arc::new(AtomicBool::new(false)), + ) + .0; + + VotingService::new( + vote_receiver, + bls_receiver, + Arc::new(cluster_info), + Arc::new(RwLock::new(poh_recorder)), + Arc::new(NullTowerStorage::default()), + Arc::new(NullVoteHistoryStorage::default()), + Arc::new(ConnectionCache::with_udp("TestConnectionCache", 10)), + bank_forks, + Some(VotingServiceOverride { + additional_listeners: vec![listener], + alpenglow_port_override: AlpenglowPortOverride::default(), + }), + ) + } + + #[test_case(BLSOp::PushVote { + bls_message: Arc::new(BLSMessage::Vote(VoteMessage { + vote: Vote::new_skip_vote(5), + signature: BLSSignature::default(), + rank: 1, + })), + slot: 5, + saved_vote_history: SavedVoteHistoryVersions::Current(SavedVoteHistory::default()), + }, BLSMessage::Vote(VoteMessage { + vote: Vote::new_skip_vote(5), + signature: BLSSignature::default(), + rank: 1, + }))] + #[test_case(BLSOp::PushCertificate { + certificate: Arc::new(CertificateMessage { + certificate: Certificate::new(CertificateType::Skip, 5, None), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }), + }, BLSMessage::Certificate(CertificateMessage { + certificate: Certificate::new(CertificateType::Skip, 5, None), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }))] + fn test_send_bls_message(bls_op: BLSOp, expected_bls_message: BLSMessage) { + solana_logger::setup(); + let (_vote_sender, vote_receiver) = crossbeam_channel::unbounded(); + let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); + // Create listener thread on a random port we allocated and return SocketAddr to create VotingService + + // Bind to a random UDP port + let socket = solana_net_utils::bind_to_localhost().unwrap(); + let listener_addr = socket.local_addr().unwrap(); + + // Create VotingService with the listener address + let _ = create_voting_service(vote_receiver, bls_receiver, listener_addr); + + // Send a BLS message via the VotingService + assert!(bls_sender.send(bls_op).is_ok()); + + // Wait for the listener to receive the message + let mut packet_batch = PacketBatch::new(vec![Packet::default()]); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + assert!(recv_mmsg(&socket, &mut packet_batch[..]).is_ok()); + let packet = packet_batch.iter().next().expect("No packets received"); + let received_bls_message = packet + .deserialize_slice::(..) + .unwrap_or_else(|err| { + panic!( + "Failed to deserialize BLSMessage: {:?} {:?}", + size_of::(), + err + ) + }); + assert_eq!(received_bls_message, expected_bls_message); + } +} diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 4bb3f57e3f..c61beec822 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -5,8 +5,11 @@ use { crate::{ completed_data_sets_service::CompletedDataSetsSender, - repair::repair_service::{ - OutstandingShredRepairs, RepairInfo, RepairService, RepairServiceChannels, + repair::{ + certificate_service::{CertificateReceiver, CertificateService}, + repair_service::{ + OutstandingShredRepairs, RepairInfo, RepairService, RepairServiceChannels, + }, }, result::{Error, Result}, }, @@ -164,6 +167,13 @@ fn run_check_duplicate( } }; + if root_bank + .feature_set + .is_active(&solana_feature_set::secp256k1_program_enabled::id()) + { + return Ok(()); + } + // Propagate duplicate proof through gossip cluster_info.push_duplicate_shred(&shred1, &shred2)?; // Notify duplicate consensus state machine @@ -271,6 +281,7 @@ pub(crate) struct WindowService { t_insert: JoinHandle<()>, t_check_duplicate: JoinHandle<()>, repair_service: RepairService, + certificate_service: CertificateService, } impl WindowService { @@ -283,6 +294,7 @@ impl WindowService { window_service_channels: WindowServiceChannels, leader_schedule_cache: Arc, outstanding_repair_requests: Arc>, + certificate_receiver: CertificateReceiver, ) -> WindowService { let cluster_info = repair_info.cluster_info.clone(); let bank_forks = repair_info.bank_forks.clone(); @@ -309,6 +321,9 @@ impl WindowService { repair_service_channels, ); + let certificate_service = + CertificateService::new(exit.clone(), blockstore.clone(), certificate_receiver); + let (duplicate_sender, duplicate_receiver) = unbounded(); let t_check_duplicate = Self::start_check_duplicate_thread( @@ -335,6 +350,7 @@ impl WindowService { t_insert, t_check_duplicate, repair_service, + certificate_service, } } @@ -452,7 +468,8 @@ impl WindowService { pub(crate) fn join(self) -> thread::Result<()> { self.t_insert.join()?; self.t_check_duplicate.join()?; - self.repair_service.join() + self.repair_service.join()?; + self.certificate_service.join() } } diff --git a/core/tests/unified_scheduler.rs b/core/tests/unified_scheduler.rs index 5565110f9a..479c94b5f8 100644 --- a/core/tests/unified_scheduler.rs +++ b/core/tests/unified_scheduler.rs @@ -12,10 +12,7 @@ use { progress_map::{ForkProgress, ProgressMap}, }, drop_bank_service::DropBankService, - repair::cluster_slot_state_verifier::{ - DuplicateConfirmedSlots, DuplicateSlotsTracker, EpochSlotsFrozenSlots, - }, - replay_stage::ReplayStage, + replay_stage::{ReplayStage, TowerBFTStructures}, unfrozen_gossip_verified_vote_hashes::UnfrozenGossipVerifiedVoteHashes, }, solana_entry::entry::Entry, @@ -137,44 +134,45 @@ fn test_scheduler_waited_by_drop_bank_service() { info!("calling handle_new_root()..."); // Mostly copied from: test_handle_new_root() { - let mut heaviest_subtree_fork_choice = HeaviestSubtreeForkChoice::new((root, root_hash)); + let heaviest_subtree_fork_choice = HeaviestSubtreeForkChoice::new((root, root_hash)); let mut progress = ProgressMap::default(); for i in genesis..=root { progress.insert(i, ForkProgress::new(Hash::default(), None, None, 0, 0)); } - let mut duplicate_slots_tracker: DuplicateSlotsTracker = - vec![root - 1, root, root + 1].into_iter().collect(); - let mut duplicate_confirmed_slots: DuplicateConfirmedSlots = vec![root - 1, root, root + 1] + let duplicate_slots_tracker = vec![root - 1, root, root + 1].into_iter().collect(); + let duplicate_confirmed_slots = vec![root - 1, root, root + 1] .into_iter() .map(|s| (s, Hash::default())) .collect(); - let mut unfrozen_gossip_verified_vote_hashes: UnfrozenGossipVerifiedVoteHashes = - UnfrozenGossipVerifiedVoteHashes { - votes_per_slot: vec![root - 1, root, root + 1] - .into_iter() - .map(|s| (s, HashMap::new())) - .collect(), - }; - let mut epoch_slots_frozen_slots: EpochSlotsFrozenSlots = vec![root - 1, root, root + 1] + let unfrozen_gossip_verified_vote_hashes = UnfrozenGossipVerifiedVoteHashes { + votes_per_slot: vec![root - 1, root, root + 1] + .into_iter() + .map(|s| (s, HashMap::new())) + .collect(), + }; + let epoch_slots_frozen_slots = vec![root - 1, root, root + 1] .into_iter() .map(|slot| (slot, Hash::default())) .collect(); + let mut tbft_structs = TowerBFTStructures { + heaviest_subtree_fork_choice, + duplicate_slots_tracker, + duplicate_confirmed_slots, + unfrozen_gossip_verified_vote_hashes, + epoch_slots_frozen_slots, + }; ReplayStage::handle_new_root( root, &bank_forks, &mut progress, &AbsRequestSender::default(), None, - &mut heaviest_subtree_fork_choice, - &mut duplicate_slots_tracker, - &mut duplicate_confirmed_slots, - &mut unfrozen_gossip_verified_vote_hashes, &mut true, &mut Vec::new(), - &mut epoch_slots_frozen_slots, &drop_bank_sender1, + &mut tbft_structs, ) .unwrap(); } diff --git a/curves/bls12-381/Cargo.toml b/curves/bls12-381/Cargo.toml new file mode 100644 index 0000000000..83d786f790 --- /dev/null +++ b/curves/bls12-381/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "solana-bls12-381" +description = "Solana Bls12-381" +documentation = "https://docs.rs/solana-bls12-381" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +bytemuck = { workspace = true } +bytemuck_derive = { workspace = true } +solana-curve-traits = { workspace = true } +thiserror = { workspace = true } + +[target.'cfg(target_os = "solana")'.dependencies] +solana-define-syscall = { workspace = true } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +blst = { workspace = true } + +[lints] +workspace = true diff --git a/curves/bls12-381/src/errors.rs b/curves/bls12-381/src/errors.rs new file mode 100644 index 0000000000..49f4a98a52 --- /dev/null +++ b/curves/bls12-381/src/errors.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Clone, Debug, Eq, PartialEq)] +pub enum BlsError { + #[error("encoding failed")] + BadEncoding, + #[error("point is not on curve")] + PointNotOnCurve, + #[error("point is not in group")] + PointNotInGroup, + #[error("scalar failed")] + BadScalar, +} diff --git a/curves/bls12-381/src/g1.rs b/curves/bls12-381/src/g1.rs new file mode 100644 index 0000000000..38caff38f8 --- /dev/null +++ b/curves/bls12-381/src/g1.rs @@ -0,0 +1,370 @@ +pub use target_arch::*; +use { + crate::scalar::PodScalar, + bytemuck_derive::{Pod, Zeroable}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodG1Compressed(pub [u8; 48]); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodG1Affine(pub [u8; 96]); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct PodG1Projective(pub [u8; 144]); + +unsafe impl bytemuck::Zeroable for PodG1Projective {} +unsafe impl bytemuck::Pod for PodG1Projective {} + +#[cfg(not(target_os = "solana"))] +mod target_arch { + use { + super::*, + blst::{ + blst_fp, blst_fp_from_lendian, blst_lendian_from_fp, blst_p1, blst_p1_add, + blst_p1_cneg, blst_p1_mult, + }, + solana_curve_traits::GroupOperations, + }; + + pub fn add( + left_point: &PodG1Projective, + right_point: &PodG1Projective, + ) -> Option { + PodG1Projective::add(left_point, right_point) + } + + pub fn subtract( + left_point: &PodG1Projective, + right_point: &PodG1Projective, + ) -> Option { + PodG1Projective::subtract(left_point, right_point) + } + + pub fn multiply(scalar: &PodScalar, point: &PodG1Projective) -> Option { + PodG1Projective::multiply(scalar, point) + } + + impl GroupOperations for PodG1Projective { + type Scalar = PodScalar; + type Point = Self; + + fn add(left_point: &Self, right_point: &Self) -> Option { + let mut result = blst_p1::default(); + // TODO: this conversion makes a copy of bytes + // see if it is possible to make zero-copy conversion + let left_point: blst_p1 = left_point.into(); + let right_point: blst_p1 = right_point.into(); + + unsafe { + blst_p1_add( + &mut result as *mut blst_p1, + &left_point as *const blst_p1, + &right_point as *const blst_p1, + ); + } + Some(result.into()) + } + + fn subtract(left_point: &Self, right_point: &Self) -> Option { + let mut result = blst_p1::default(); + let left_point: blst_p1 = left_point.into(); + let right_point: blst_p1 = right_point.into(); + unsafe { + let mut right_point_negated = right_point; + blst_p1_cneg(&mut right_point_negated as *mut blst_p1, true); + blst_p1_add( + &mut result as *mut blst_p1, + &left_point as *const blst_p1, + &right_point_negated as *const blst_p1, + ); + } + Some(result.into()) + } + + fn multiply(scalar: &PodScalar, point: &Self) -> Option { + let mut result = blst_p1::default(); + let point: blst_p1 = point.into(); + unsafe { + blst_p1_mult( + &mut result as *mut blst_p1, + &point as *const blst_p1, + scalar.0.as_ptr(), + 256, + ); + } + Some(result.into()) + } + } + + impl From for PodG1Projective { + fn from(point: blst_p1) -> Self { + let mut bytes = [0u8; 144]; + // TODO: this is unchecked; check if on curve and in the correct coset + unsafe { + blst_lendian_from_fp(bytes[0..48].as_mut_ptr(), &point.x as *const blst_fp); + blst_lendian_from_fp(bytes[48..96].as_mut_ptr(), &point.y as *const blst_fp); + blst_lendian_from_fp(bytes[96..144].as_mut_ptr(), &point.z as *const blst_fp); + } + Self(bytes) + } + } + + impl From for blst_p1 { + fn from(point: PodG1Projective) -> Self { + let mut x = blst_fp::default(); + let mut y = blst_fp::default(); + let mut z = blst_fp::default(); + unsafe { + blst_fp_from_lendian(&mut x as *mut blst_fp, point.0[0..48].as_ptr()); + blst_fp_from_lendian(&mut y as *mut blst_fp, point.0[48..96].as_ptr()); + blst_fp_from_lendian(&mut z as *mut blst_fp, point.0[96..144].as_ptr()); + } + blst_p1 { x, y, z } + } + } + + impl From<&PodG1Projective> for blst_p1 { + fn from(point: &PodG1Projective) -> Self { + let mut x = blst_fp::default(); + let mut y = blst_fp::default(); + let mut z = blst_fp::default(); + unsafe { + blst_fp_from_lendian(&mut x as *mut blst_fp, point.0[0..48].as_ptr()); + blst_fp_from_lendian(&mut y as *mut blst_fp, point.0[48..96].as_ptr()); + blst_fp_from_lendian(&mut z as *mut blst_fp, point.0[96..144].as_ptr()); + } + blst_p1 { x, y, z } + } + } +} + +#[cfg(target_os = "solana")] +mod target_arch { + use { + super::*, + bytemuck::Zeroable, + solana_curve_traits::{ADD, BLS12_381_G1_PROJECTIVE, MUL, SUB}, + }; + + pub fn add( + left_point: &PodG1Projective, + right_point: &PodG1Projective, + ) -> Option { + let mut result_point = PodG1Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + ADD, + &left_point.0 as *const u8, + &right_point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } + + pub fn subtract( + left_point: &PodG1Projective, + right_point: &PodG1Projective, + ) -> Option { + let mut result_point = PodG1Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + SUB, + &left_point.0 as *const u8, + &right_point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } + + pub fn multiply(scalar: &PodScalar, point: &PodG1Projective) -> Option { + let mut result_point = PodG1Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + MUL, + &scalar.0 as *const u8, + &point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::scalar::PodScalar, + blst::{blst_p1, blst_p1_affine}, + solana_curve_traits::GroupOperations, + }; + + unsafe fn decompress(compressed: &PodG1Compressed) -> PodG1Projective { + let point_ptr = &compressed.0 as *const u8; + + let mut point_affine = blst_p1_affine::default(); + let point_affine_ptr = &mut point_affine as *mut blst_p1_affine; + blst::blst_p1_uncompress(point_affine_ptr, point_ptr); + + let mut point_full = blst_p1::default(); + let point_full_ptr = &mut point_full as *mut blst_p1; + blst::blst_p1_from_affine(point_full_ptr, point_affine_ptr); + + point_full.into() + } + + unsafe fn compress(projective: &PodG1Projective) -> PodG1Compressed { + let mut compressed = [0u8; 48]; + let point_ptr = &projective.0 as *const u8 as *mut blst_p1; + blst::blst_p1_compress(compressed.as_mut_ptr(), point_ptr); + PodG1Compressed(compressed) + } + + #[test] + fn test_add_subtract_bls_12_381() { + let identity: PodG1Projective = blst_p1::default().into(); + + let point_a_compressed = PodG1Compressed([ + 140, 112, 74, 2, 254, 123, 212, 72, 73, 122, 106, 93, 64, 7, 172, 236, 36, 227, 96, + 130, 121, 240, 41, 205, 62, 7, 207, 15, 94, 159, 7, 91, 99, 57, 241, 162, 136, 81, 90, + 5, 179, 98, 6, 98, 41, 146, 195, 14, + ]); + + let point_b_compressed = PodG1Compressed([ + 149, 247, 195, 10, 243, 121, 148, 92, 212, 118, 110, 34, 133, 35, 193, 161, 225, 85, + 122, 150, 192, 175, 136, 69, 63, 0, 146, 159, 103, 117, 89, 145, 171, 184, 105, 135, + 75, 231, 97, 247, 162, 101, 208, 175, 198, 222, 35, 102, + ]); + + let point_c_compressed = PodG1Compressed([ + 137, 46, 171, 236, 48, 64, 85, 76, 96, 91, 201, 87, 53, 133, 184, 211, 4, 113, 227, + 145, 17, 134, 71, 182, 72, 39, 55, 230, 145, 29, 216, 20, 52, 247, 57, 191, 255, 53, + 57, 150, 221, 59, 52, 78, 171, 240, 129, 39, + ]); + + let point_a = unsafe { decompress(&point_a_compressed) }; + let point_b = unsafe { decompress(&point_b_compressed) }; + let point_c = unsafe { decompress(&point_c_compressed) }; + + // identity + assert_eq!(PodG1Projective::add(&point_a, &identity).unwrap(), point_a); + + // associativity + unsafe { + assert_eq!( + compress( + &PodG1Projective::add( + &PodG1Projective::add(&point_a, &point_b).unwrap(), + &point_c + ) + .unwrap() + ), + compress( + &PodG1Projective::add( + &point_a, + &PodG1Projective::add(&point_b, &point_c).unwrap() + ) + .unwrap() + ), + ) + }; + + unsafe { + assert_eq!( + compress( + &PodG1Projective::subtract( + &PodG1Projective::subtract(&point_a, &point_b).unwrap(), + &point_c + ) + .unwrap() + ), + compress( + &PodG1Projective::subtract( + &point_a, + &PodG1Projective::add(&point_b, &point_c).unwrap() + ) + .unwrap() + ), + ) + }; + + // commutativity + unsafe { + assert_eq!( + compress(&PodG1Projective::add(&point_a, &point_b).unwrap()), + compress(&PodG1Projective::add(&point_b, &point_a).unwrap()) + ) + }; + + // subtraction + unsafe { + assert_eq!( + compress(&PodG1Projective::subtract(&point_a, &point_a).unwrap()), + compress(&identity) + ) + }; + } + + #[test] + fn test_multiply_bls12_381() { + let scalar = PodScalar([ + 107, 15, 13, 77, 216, 207, 117, 144, 252, 166, 162, 81, 107, 12, 249, 164, 242, 212, + 76, 68, 144, 198, 72, 233, 76, 116, 60, 179, 0, 32, 86, 93, + ]); + + let point_a_compressed = PodG1Compressed([ + 140, 112, 74, 2, 254, 123, 212, 72, 73, 122, 106, 93, 64, 7, 172, 236, 36, 227, 96, + 130, 121, 240, 41, 205, 62, 7, 207, 15, 94, 159, 7, 91, 99, 57, 241, 162, 136, 81, 90, + 5, 179, 98, 6, 98, 41, 146, 195, 14, + ]); + + let point_b_compressed = PodG1Compressed([ + 149, 247, 195, 10, 243, 121, 148, 92, 212, 118, 110, 34, 133, 35, 193, 161, 225, 85, + 122, 150, 192, 175, 136, 69, 63, 0, 146, 159, 103, 117, 89, 145, 171, 184, 105, 135, + 75, 231, 97, 247, 162, 101, 208, 175, 198, 222, 35, 102, + ]); + + let point_a = unsafe { decompress(&point_a_compressed) }; + let point_b = unsafe { decompress(&point_b_compressed) }; + + let ax = PodG1Projective::multiply(&scalar, &point_a).unwrap(); + let bx = PodG1Projective::multiply(&scalar, &point_b).unwrap(); + + unsafe { + assert_eq!( + compress(&PodG1Projective::add(&ax, &bx).unwrap()), + compress( + &PodG1Projective::multiply( + &scalar, + &PodG1Projective::add(&point_a, &point_b).unwrap() + ) + .unwrap() + ), + ) + }; + } +} diff --git a/curves/bls12-381/src/g2.rs b/curves/bls12-381/src/g2.rs new file mode 100644 index 0000000000..0d4a05dbd4 --- /dev/null +++ b/curves/bls12-381/src/g2.rs @@ -0,0 +1,406 @@ +pub use target_arch::*; +use { + crate::scalar::PodScalar, + bytemuck_derive::{Pod, Zeroable}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodG2Compressed(pub [u8; 96]); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct PodG2Affine(pub [u8; 192]); + +unsafe impl bytemuck::Zeroable for PodG2Affine {} +unsafe impl bytemuck::Pod for PodG2Affine {} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct PodG2Projective(pub [u8; 288]); + +unsafe impl bytemuck::Zeroable for PodG2Projective {} +unsafe impl bytemuck::Pod for PodG2Projective {} + +#[cfg(not(target_os = "solana"))] +mod target_arch { + use { + super::*, + blst::{ + blst_fp, blst_fp2, blst_fp_from_lendian, blst_lendian_from_fp, blst_p2, blst_p2_add, + blst_p2_cneg, blst_p2_mult, + }, + solana_curve_traits::GroupOperations, + }; + + pub fn add( + left_point: &PodG2Projective, + right_point: &PodG2Projective, + ) -> Option { + PodG2Projective::add(left_point, right_point) + } + + pub fn subtract( + left_point: &PodG2Projective, + right_point: &PodG2Projective, + ) -> Option { + PodG2Projective::subtract(left_point, right_point) + } + + pub fn multiply(scalar: &PodScalar, point: &PodG2Projective) -> Option { + PodG2Projective::multiply(scalar, point) + } + + impl GroupOperations for PodG2Projective { + type Scalar = PodScalar; + type Point = Self; + + fn add(left_point: &Self, right_point: &Self) -> Option { + let mut result = blst_p2::default(); + // TODO: this conversion makes a copy of bytes + // see if it is possible to make zero-copy conversion + let left_point: blst_p2 = left_point.into(); + let right_point: blst_p2 = right_point.into(); + + unsafe { + blst_p2_add( + &mut result as *mut blst_p2, + &left_point as *const blst_p2, + &right_point as *const blst_p2, + ); + } + Some(result.into()) + } + + fn subtract(left_point: &Self, right_point: &Self) -> Option { + let mut result = blst_p2::default(); + let left_point: blst_p2 = left_point.into(); + let right_point: blst_p2 = right_point.into(); + unsafe { + let mut right_point_negated = right_point; + blst_p2_cneg(&mut right_point_negated as *mut blst_p2, true); + blst_p2_add( + &mut result as *mut blst_p2, + &left_point as *const blst_p2, + &right_point_negated as *const blst_p2, + ); + } + Some(result.into()) + } + + fn multiply(scalar: &PodScalar, point: &Self) -> Option { + let mut result = blst_p2::default(); + let point: blst_p2 = point.into(); + unsafe { + blst_p2_mult( + &mut result as *mut blst_p2, + &point as *const blst_p2, + scalar.0.as_ptr(), + 256, + ); + } + Some(result.into()) + } + } + + impl From for PodG2Projective { + fn from(point: blst_p2) -> Self { + let mut bytes = [0u8; 288]; + unsafe { + blst_lendian_from_fp(bytes[0..48].as_mut_ptr(), &point.x.fp[0] as *const blst_fp); + blst_lendian_from_fp(bytes[48..96].as_mut_ptr(), &point.x.fp[1] as *const blst_fp); + blst_lendian_from_fp( + bytes[96..144].as_mut_ptr(), + &point.y.fp[0] as *const blst_fp, + ); + blst_lendian_from_fp( + bytes[144..192].as_mut_ptr(), + &point.y.fp[1] as *const blst_fp, + ); + blst_lendian_from_fp( + bytes[192..240].as_mut_ptr(), + &point.z.fp[0] as *const blst_fp, + ); + blst_lendian_from_fp( + bytes[240..288].as_mut_ptr(), + &point.z.fp[1] as *const blst_fp, + ); + } + Self(bytes) + } + } + + impl From for blst_p2 { + fn from(point: PodG2Projective) -> Self { + let mut x = blst_fp2::default(); + let mut y = blst_fp2::default(); + let mut z = blst_fp2::default(); + unsafe { + blst_fp_from_lendian(&mut x.fp[0] as *mut blst_fp, point.0[0..48].as_ptr()); + blst_fp_from_lendian(&mut x.fp[1] as *mut blst_fp, point.0[48..96].as_ptr()); + blst_fp_from_lendian(&mut y.fp[0] as *mut blst_fp, point.0[96..144].as_ptr()); + blst_fp_from_lendian(&mut y.fp[1] as *mut blst_fp, point.0[144..192].as_ptr()); + blst_fp_from_lendian(&mut z.fp[0] as *mut blst_fp, point.0[192..240].as_ptr()); + blst_fp_from_lendian(&mut z.fp[1] as *mut blst_fp, point.0[240..288].as_ptr()); + } + blst_p2 { x, y, z } + } + } + + impl From<&PodG2Projective> for blst_p2 { + fn from(point: &PodG2Projective) -> Self { + let mut x = blst_fp2::default(); + let mut y = blst_fp2::default(); + let mut z = blst_fp2::default(); + unsafe { + blst_fp_from_lendian(&mut x.fp[0] as *mut blst_fp, point.0[0..48].as_ptr()); + blst_fp_from_lendian(&mut x.fp[1] as *mut blst_fp, point.0[48..96].as_ptr()); + blst_fp_from_lendian(&mut y.fp[0] as *mut blst_fp, point.0[96..144].as_ptr()); + blst_fp_from_lendian(&mut y.fp[1] as *mut blst_fp, point.0[144..192].as_ptr()); + blst_fp_from_lendian(&mut z.fp[0] as *mut blst_fp, point.0[192..240].as_ptr()); + blst_fp_from_lendian(&mut z.fp[1] as *mut blst_fp, point.0[240..288].as_ptr()); + } + blst_p2 { x, y, z } + } + } +} + +#[cfg(target_os = "solana")] +mod target_arch { + use { + super::*, + bytemuck::Zeroable, + solana_curve_traits::{ADD, BLS12_381_G1_PROJECTIVE, MUL, SUB}, + }; + + pub fn add( + left_point: &PodG2Projective, + right_point: &PodG2Projective, + ) -> Option { + let mut result_point = PodG2Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + ADD, + &left_point.0 as *const u8, + &right_point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } + + pub fn subtract( + left_point: &PodG2Projective, + right_point: &PodG2Projective, + ) -> Option { + let mut result_point = PodG2Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + SUB, + &left_point.0 as *const u8, + &right_point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } + + pub fn multiply(scalar: &PodScalar, point: &PodG2Projective) -> Option { + let mut result_point = PodG2Projective::zeroed(); + let result = unsafe { + solana_define_syscall::definitions::sol_curve_group_op( + BLS12_381_G1_PROJECTIVE, + MUL, + &scalar.0 as *const u8, + &point.0 as *const u8, + &mut result_point.0 as *mut u8, + ) + }; + + if result == 0 { + Some(result_point) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::scalar::PodScalar, + blst::{blst_p2, blst_p2_affine}, + solana_curve_traits::GroupOperations, + }; + + unsafe fn decompress(compressed: &PodG2Compressed) -> PodG2Projective { + let point_ptr = &compressed.0 as *const u8; + + let mut point_affine = blst_p2_affine::default(); + let point_affine_ptr = &mut point_affine as *mut blst_p2_affine; + blst::blst_p2_uncompress(point_affine_ptr, point_ptr); + + let mut point_full = blst_p2::default(); + let point_full_ptr = &mut point_full as *mut blst_p2; + blst::blst_p2_from_affine(point_full_ptr, point_affine_ptr); + + point_full.into() + } + + unsafe fn compress(projective: &PodG2Projective) -> PodG2Compressed { + let mut compressed = [0u8; 96]; + let point_ptr = &projective.0 as *const u8 as *mut blst_p2; + blst::blst_p2_compress(compressed.as_mut_ptr(), point_ptr); + PodG2Compressed(compressed) + } + + #[test] + fn test_add_subtract_bls_12_381() { + let identity: PodG2Projective = blst_p2::default().into(); + + let point_a_compressed = PodG2Compressed([ + 164, 206, 80, 113, 43, 158, 131, 37, 93, 106, 231, 75, 147, 161, 185, 106, 81, 151, 33, + 215, 119, 212, 236, 144, 255, 79, 164, 84, 156, 164, 121, 86, 19, 207, 42, 161, 95, 32, + 22, 141, 21, 250, 100, 154, 134, 50, 186, 209, 12, 208, 242, 49, 189, 146, 166, 202, + 120, 136, 221, 182, 244, 18, 95, 15, 95, 85, 3, 216, 6, 37, 199, 101, 109, 31, 213, 20, + 68, 69, 19, 79, 126, 19, 60, 71, 114, 17, 78, 220, 142, 37, 33, 157, 252, 2, 18, 182, + ]); + + let point_b_compressed = PodG2Compressed([ + 183, 42, 8, 225, 237, 101, 184, 130, 73, 9, 104, 128, 181, 122, 114, 248, 38, 145, 28, + 175, 76, 168, 219, 102, 168, 17, 1, 163, 145, 33, 127, 101, 159, 1, 108, 7, 56, 68, + 142, 7, 151, 2, 220, 149, 227, 134, 194, 231, 9, 6, 86, 227, 163, 72, 228, 151, 235, + 97, 51, 218, 156, 244, 234, 108, 157, 71, 90, 247, 143, 215, 224, 44, 68, 20, 155, 178, + 155, 29, 183, 167, 10, 244, 56, 19, 49, 169, 90, 8, 100, 86, 172, 14, 119, 200, 205, + 193, + ]); + + let point_c_compressed = PodG2Compressed([ + 139, 35, 111, 111, 138, 15, 121, 99, 87, 180, 83, 67, 5, 100, 162, 78, 79, 114, 138, + 150, 244, 249, 138, 213, 44, 122, 179, 155, 36, 156, 121, 98, 76, 57, 109, 116, 219, + 227, 54, 177, 90, 19, 147, 215, 145, 4, 231, 175, 1, 144, 102, 168, 64, 217, 60, 234, + 32, 38, 115, 250, 43, 47, 227, 138, 249, 195, 141, 231, 226, 207, 122, 246, 147, 50, + 72, 230, 22, 215, 146, 161, 209, 111, 221, 185, 53, 103, 4, 224, 151, 54, 60, 94, 65, + 34, 66, 247, + ]); + + let point_a = unsafe { decompress(&point_a_compressed) }; + let point_b = unsafe { decompress(&point_b_compressed) }; + let point_c = unsafe { decompress(&point_c_compressed) }; + + // identity + assert_eq!(PodG2Projective::add(&point_a, &identity).unwrap(), point_a); + + // associativity + unsafe { + assert_eq!( + compress( + &PodG2Projective::add( + &PodG2Projective::add(&point_a, &point_b).unwrap(), + &point_c + ) + .unwrap() + ), + compress( + &PodG2Projective::add( + &point_a, + &PodG2Projective::add(&point_b, &point_c).unwrap() + ) + .unwrap() + ), + ) + }; + + unsafe { + assert_eq!( + compress( + &PodG2Projective::subtract( + &PodG2Projective::subtract(&point_a, &point_b).unwrap(), + &point_c + ) + .unwrap() + ), + compress( + &PodG2Projective::subtract( + &point_a, + &PodG2Projective::add(&point_b, &point_c).unwrap() + ) + .unwrap() + ), + ) + }; + + // commutativity + unsafe { + assert_eq!( + compress(&PodG2Projective::add(&point_a, &point_b).unwrap()), + compress(&PodG2Projective::add(&point_b, &point_a).unwrap()) + ) + }; + + // subtraction + unsafe { + assert_eq!( + compress(&PodG2Projective::subtract(&point_a, &point_a).unwrap()), + compress(&identity) + ) + }; + } + + #[test] + fn test_multiply_bls12_381() { + let scalar = PodScalar([ + 107, 15, 13, 77, 216, 207, 117, 144, 252, 166, 162, 81, 107, 12, 249, 164, 242, 212, + 76, 68, 144, 198, 72, 233, 76, 116, 60, 179, 0, 32, 86, 93, + ]); + + let point_a_compressed = PodG2Compressed([ + 164, 206, 80, 113, 43, 158, 131, 37, 93, 106, 231, 75, 147, 161, 185, 106, 81, 151, 33, + 215, 119, 212, 236, 144, 255, 79, 164, 84, 156, 164, 121, 86, 19, 207, 42, 161, 95, 32, + 22, 141, 21, 250, 100, 154, 134, 50, 186, 209, 12, 208, 242, 49, 189, 146, 166, 202, + 120, 136, 221, 182, 244, 18, 95, 15, 95, 85, 3, 216, 6, 37, 199, 101, 109, 31, 213, 20, + 68, 69, 19, 79, 126, 19, 60, 71, 114, 17, 78, 220, 142, 37, 33, 157, 252, 2, 18, 182, + ]); + + let point_b_compressed = PodG2Compressed([ + 183, 42, 8, 225, 237, 101, 184, 130, 73, 9, 104, 128, 181, 122, 114, 248, 38, 145, 28, + 175, 76, 168, 219, 102, 168, 17, 1, 163, 145, 33, 127, 101, 159, 1, 108, 7, 56, 68, + 142, 7, 151, 2, 220, 149, 227, 134, 194, 231, 9, 6, 86, 227, 163, 72, 228, 151, 235, + 97, 51, 218, 156, 244, 234, 108, 157, 71, 90, 247, 143, 215, 224, 44, 68, 20, 155, 178, + 155, 29, 183, 167, 10, 244, 56, 19, 49, 169, 90, 8, 100, 86, 172, 14, 119, 200, 205, + 193, + ]); + + let point_a = unsafe { decompress(&point_a_compressed) }; + let point_b = unsafe { decompress(&point_b_compressed) }; + + let ax = PodG2Projective::multiply(&scalar, &point_a).unwrap(); + let bx = PodG2Projective::multiply(&scalar, &point_b).unwrap(); + + unsafe { + assert_eq!( + compress(&PodG2Projective::add(&ax, &bx).unwrap()), + compress( + &PodG2Projective::multiply( + &scalar, + &PodG2Projective::add(&point_a, &point_b).unwrap() + ) + .unwrap() + ), + ) + }; + } +} diff --git a/curves/bls12-381/src/lib.rs b/curves/bls12-381/src/lib.rs new file mode 100644 index 0000000000..5501913d43 --- /dev/null +++ b/curves/bls12-381/src/lib.rs @@ -0,0 +1,7 @@ +#![allow(clippy::arithmetic_side_effects, clippy::op_ref)] +//! Syscall operations for bls12-381 + +pub mod errors; +pub mod g1; +pub mod g2; +pub mod scalar; diff --git a/curves/bls12-381/src/scalar.rs b/curves/bls12-381/src/scalar.rs new file mode 100644 index 0000000000..76b274aa24 --- /dev/null +++ b/curves/bls12-381/src/scalar.rs @@ -0,0 +1,5 @@ +use bytemuck_derive::{Pod, Zeroable}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PodScalar(pub [u8; 32]); diff --git a/curves/curve-traits/Cargo.toml b/curves/curve-traits/Cargo.toml new file mode 100644 index 0000000000..4ffd7bdc4b --- /dev/null +++ b/curves/curve-traits/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "solana-curve-traits" +description = "Solana Curve Traits" +documentation = "https://docs.rs/solana-curve-traits" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] + +[lints] +workspace = true diff --git a/curves/curve25519/src/curve_syscall_traits.rs b/curves/curve-traits/src/lib.rs similarity index 84% rename from curves/curve25519/src/curve_syscall_traits.rs rename to curves/curve-traits/src/lib.rs index 547f6c948d..b599e157c5 100644 --- a/curves/curve25519/src/curve_syscall_traits.rs +++ b/curves/curve-traits/src/lib.rs @@ -22,6 +22,13 @@ pub trait PointValidation { fn validate_point(&self) -> bool; } +pub trait HashToCurve { + type Point; + + /// Hash a sequence of bytes to a curve point. + fn hash_to_curve(bytes: &[u8], dst: &[u8], aug: &[u8]) -> Self::Point; +} + pub trait GroupOperations { type Point; type Scalar; @@ -72,13 +79,24 @@ pub trait Pairing { /// Applies the bilinear pairing operation to two curve points P1, P2 -> e(P1, P2). This trait /// is only relevant for "pairing-friendly" curves such as BN254 and BLS12-381. fn pairing_map( - left_point: &Self::G1Point, - right_point: &Self::G2Point, + left_point: &[Self::G1Point], + right_point: &[Self::G2Point], + n: usize, ) -> Option; } pub const CURVE25519_EDWARDS: u64 = 0; pub const CURVE25519_RISTRETTO: u64 = 1; +pub const BN254_G1: u64 = 2; +pub const BN254_G2: u64 = 3; +pub const BN254_GT: u64 = 4; +pub const BLS12_381_G1_COMPRESSED: u64 = 5; +pub const BLS12_381_G2_COMPRESSED: u64 = 6; +pub const BLS12_381_G1_AFFINE: u64 = 7; +pub const BLS12_381_G2_AFFINE: u64 = 8; +pub const BLS12_381_G1_PROJECTIVE: u64 = 9; +pub const BLS12_381_G2_PROJECTIVE: u64 = 10; +pub const BLS12_381_GT: u64 = 11; pub const ADD: u64 = 0; pub const SUB: u64 = 1; diff --git a/curves/curve25519/Cargo.toml b/curves/curve25519/Cargo.toml index 597e8f2560..18cac319a3 100644 --- a/curves/curve25519/Cargo.toml +++ b/curves/curve25519/Cargo.toml @@ -12,6 +12,7 @@ edition = { workspace = true } [dependencies] bytemuck = { workspace = true } bytemuck_derive = { workspace = true } +solana-curve-traits = { workspace = true } # this crate uses `subtle::CtOption::into_option` via curve25519-dalek, # which requires subtle v2.6.1, but curve25519-dalek only requires v2.3.0 # The line below help users of this crate obtain correct subtle version. diff --git a/curves/curve25519/src/edwards.rs b/curves/curve25519/src/edwards.rs index 69fe8f3186..3a854f2d82 100644 --- a/curves/curve25519/src/edwards.rs +++ b/curves/curve25519/src/edwards.rs @@ -9,16 +9,13 @@ pub struct PodEdwardsPoint(pub [u8; 32]); mod target_arch { use { super::*, - crate::{ - curve_syscall_traits::{GroupOperations, MultiScalarMultiplication, PointValidation}, - errors::Curve25519Error, - scalar::PodScalar, - }, + crate::{errors::Curve25519Error, scalar::PodScalar}, curve25519_dalek::{ edwards::{CompressedEdwardsY, EdwardsPoint}, scalar::Scalar, traits::VartimeMultiscalarMul, }, + solana_curve_traits::{GroupOperations, MultiScalarMultiplication, PointValidation}, }; pub fn validate_edwards(point: &PodEdwardsPoint) -> bool { diff --git a/curves/curve25519/src/lib.rs b/curves/curve25519/src/lib.rs index d0ab9d4709..9757e6d5d8 100644 --- a/curves/curve25519/src/lib.rs +++ b/curves/curve25519/src/lib.rs @@ -1,7 +1,6 @@ #![allow(clippy::arithmetic_side_effects, clippy::op_ref)] //! Syscall operations for curve25519 -pub mod curve_syscall_traits; pub mod edwards; pub mod errors; pub mod ristretto; diff --git a/curves/curve25519/src/ristretto.rs b/curves/curve25519/src/ristretto.rs index 019390b5e6..c75cba27a5 100644 --- a/curves/curve25519/src/ristretto.rs +++ b/curves/curve25519/src/ristretto.rs @@ -9,16 +9,13 @@ pub struct PodRistrettoPoint(pub [u8; 32]); mod target_arch { use { super::*, - crate::{ - curve_syscall_traits::{GroupOperations, MultiScalarMultiplication, PointValidation}, - errors::Curve25519Error, - scalar::PodScalar, - }, + crate::{errors::Curve25519Error, scalar::PodScalar}, curve25519_dalek::{ ristretto::{CompressedRistretto, RistrettoPoint}, scalar::Scalar, traits::VartimeMultiscalarMul, }, + solana_curve_traits::{GroupOperations, MultiScalarMultiplication, PointValidation}, }; pub fn validate_ristretto(point: &PodRistrettoPoint) -> bool { diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 9339c84ab6..5ed4fbd55e 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -19,6 +19,7 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } solana-account = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-clock = { workspace = true } @@ -47,7 +48,9 @@ solana-stake-interface = { workspace = true } solana-stake-program = { workspace = true } solana-time-utils = { workspace = true } solana-version = { workspace = true } +solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor-messages = { workspace = true } tempfile = { workspace = true } [dev-dependencies] diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index ef54f1660f..6fc42b71d3 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -29,4 +29,5 @@ pub struct StakedValidatorAccountInfo { pub identity_account: String, pub vote_account: String, pub stake_account: String, + pub bls_pubkey: Option, } diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 8f47cb22b4..2cd7094051 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -7,9 +7,11 @@ use { itertools::Itertools, solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, solana_accounts_db::hardened_unpack::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, + solana_bls_signatures::Pubkey as BLSPubkey, solana_clap_utils::{ input_parsers::{ - cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime, + bls_pubkeys_of, cluster_type_of, pubkey_of, pubkeys_of, + unix_timestamp_from_rfc3339_datetime, }, input_validators::{ is_pubkey, is_pubkey_or_keypair, is_rfc3339_datetime, is_slot, is_url_or_moniker, @@ -38,11 +40,13 @@ use { solana_rent::Rent, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, + solana_runtime::genesis_utils::include_alpenglow_bpf_program, solana_sdk_ids::system_program, solana_signer::Signer, solana_stake_interface::state::StakeStateV2, solana_stake_program::stake_state, solana_vote_program::vote_state::{self, VoteState}, + solana_votor_messages::state::VoteState as AlpenglowVoteState, std::{ collections::HashMap, error, @@ -119,6 +123,7 @@ pub fn load_validator_accounts( commission: u8, rent: &Rent, genesis_config: &mut GenesisConfig, + is_alpenglow: bool, ) -> io::Result<()> { let accounts_file = File::open(file)?; let validator_genesis_accounts: Vec = @@ -156,15 +161,22 @@ pub fn load_validator_accounts( ) })?, ]; + let bls_pubkeys: Vec = account_details.bls_pubkey.map_or(Ok(vec![]), |s| { + BLSPubkey::from_str(&s).map(|pk| vec![pk]).map_err(|err| { + io::Error::new(io::ErrorKind::Other, format!("Invalid BLS pubkey: {err}")) + }) + })?; add_validator_accounts( genesis_config, &mut pubkeys.iter(), + bls_pubkeys, account_details.balance_lamports, account_details.stake_lamports, commission, rent, None, + is_alpenglow, )?; } @@ -238,17 +250,20 @@ fn features_to_deactivate_for_cluster( fn add_validator_accounts( genesis_config: &mut GenesisConfig, pubkeys_iter: &mut Iter, + bls_pubkeys: Vec, lamports: u64, stake_lamports: u64, commission: u8, rent: &Rent, authorized_pubkey: Option<&Pubkey>, + is_alpenglow: bool, ) -> io::Result<()> { rent_exempt_check( stake_lamports, rent.minimum_balance(StakeStateV2::size_of()), )?; + let mut bls_pubkeys_iter = bls_pubkeys.iter(); loop { let Some(identity_pubkey) = pubkeys_iter.next() else { break; @@ -261,13 +276,27 @@ fn add_validator_accounts( AccountSharedData::new(lamports, 0, &system_program::id()), ); - let vote_account = vote_state::create_account_with_authorized( - identity_pubkey, - identity_pubkey, - identity_pubkey, - commission, - VoteState::get_rent_exempt_reserve(rent).max(1), - ); + let vote_account = if is_alpenglow { + let bls_pubkey = bls_pubkeys_iter + .next() + .expect("Missing BLS pubkey for {identity_pubkey}"); + AlpenglowVoteState::create_account_with_authorized( + identity_pubkey, + identity_pubkey, + identity_pubkey, + commission, + AlpenglowVoteState::get_rent_exempt_reserve(rent).max(1), + *bls_pubkey, + ) + } else { + vote_state::create_account_with_authorized( + identity_pubkey, + identity_pubkey, + identity_pubkey, + commission, + VoteState::get_rent_exempt_reserve(rent).max(1), + ) + }; genesis_config.add_account( *stake_pubkey, @@ -329,7 +358,9 @@ fn main() -> Result<(), Box> { // vote account let default_bootstrap_validator_lamports = &sol_to_lamports(500.0) .max(VoteState::get_rent_exempt_reserve(&rent)) + .max(AlpenglowVoteState::get_rent_exempt_reserve(&rent)) .to_string(); + // stake account let default_bootstrap_validator_stake_lamports = &sol_to_lamports(0.5) .max(rent.minimum_balance(StakeStateV2::size_of())) @@ -363,6 +394,15 @@ fn main() -> Result<(), Box> { .required(true) .help("The bootstrap validator's identity, vote and stake pubkeys"), ) + .arg( + Arg::with_name("bootstrap_validator_bls_pubkey") + .long("bootstrap-validator-bls-pubkey") + .value_name("BLS_PUBKEY") + .multiple(true) + .takes_value(true) + .required(false) + .help("The bootstrap validator's bls pubkey"), + ) .arg( Arg::with_name("ledger_path") .short("l") @@ -620,6 +660,12 @@ fn main() -> Result<(), Box> { feature sets", ), ) + .arg( + Arg::with_name("alpenglow") + .long("alpenglow") + .takes_value(true) + .help("Path to spl-alpenglow_vote.so. When specified, we use Alpenglow consensus; when not specified, we use POH."), + ) .get_matches(); let ledger_path = PathBuf::from(matches.value_of("ledger_path").unwrap()); @@ -633,6 +679,16 @@ fn main() -> Result<(), Box> { let bootstrap_validator_pubkeys = pubkeys_of(&matches, "bootstrap_validator").unwrap(); assert_eq!(bootstrap_validator_pubkeys.len() % 3, 0); + let bootstrap_validator_bls_pubkeys = + bls_pubkeys_of(&matches, "bootstrap_validator_bls_pubkey"); + if let Some(pubkeys) = &bootstrap_validator_bls_pubkeys { + assert_eq!( + pubkeys.len() * 3, + bootstrap_validator_pubkeys.len(), + "Number of BLS pubkeys must match the number of bootstrap validator identities" + ); + } + // Ensure there are no duplicated pubkeys in the --bootstrap-validator list { let mut v = bootstrap_validator_pubkeys.clone(); @@ -740,14 +796,18 @@ fn main() -> Result<(), Box> { let commission = value_t_or_exit!(matches, "vote_commission_percentage", u8); let rent = genesis_config.rent.clone(); + let alpenglow_so_path = matches.value_of("alpenglow"); + add_validator_accounts( &mut genesis_config, &mut bootstrap_validator_pubkeys.iter(), + bootstrap_validator_bls_pubkeys.unwrap_or_default(), bootstrap_validator_lamports, bootstrap_validator_stake_lamports, commission, &rent, bootstrap_stake_authorized_pubkey.as_ref(), + alpenglow_so_path.is_some(), )?; if let Some(creation_time) = unix_timestamp_from_rfc3339_datetime(&matches, "creation_time") { @@ -762,7 +822,13 @@ fn main() -> Result<(), Box> { } solana_stake_program::add_genesis_accounts(&mut genesis_config); - solana_runtime::genesis_utils::activate_all_features(&mut genesis_config); + + if alpenglow_so_path.is_some() { + solana_runtime::genesis_utils::activate_all_features_alpenglow(&mut genesis_config); + } else { + solana_runtime::genesis_utils::activate_all_features(&mut genesis_config); + } + if !features_to_deactivate.is_empty() { solana_runtime::genesis_utils::deactivate_features( &mut genesis_config, @@ -778,7 +844,13 @@ fn main() -> Result<(), Box> { if let Some(files) = matches.values_of("validator_accounts_file") { for file in files { - load_validator_accounts(file, commission, &rent, &mut genesis_config)?; + load_validator_accounts( + file, + commission, + &rent, + &mut genesis_config, + alpenglow_so_path.is_some(), + )?; } } @@ -829,6 +901,10 @@ fn main() -> Result<(), Box> { } } + if let Some(alpenglow_so_path) = alpenglow_so_path { + include_alpenglow_bpf_program(&mut genesis_config, alpenglow_so_path); + } + if let Some(values) = matches.values_of("upgradeable_program") { for (address, loader, program, upgrade_authority) in values.tuples() { let address = parse_address(address, "address"); @@ -901,6 +977,7 @@ fn main() -> Result<(), Box> { mod tests { use { super::*, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_borsh::v1 as borsh1, solana_genesis_config::GenesisConfig, solana_stake_interface as stake, @@ -1251,17 +1328,20 @@ mod tests { "unknownfile", 100, &Rent::default(), - &mut GenesisConfig::default() + &mut GenesisConfig::default(), + false, ) .is_err()); let mut genesis_config = GenesisConfig::default(); + let bls_pubkey: BLSPubkey = BLSKeypair::new().public.into(); let validator_accounts = vec![ StakedValidatorAccountInfo { identity_account: solana_pubkey::new_rand().to_string(), vote_account: solana_pubkey::new_rand().to_string(), stake_account: solana_pubkey::new_rand().to_string(), + bls_pubkey: None, balance_lamports: 100000000000, stake_lamports: 10000000000, }, @@ -1269,6 +1349,7 @@ mod tests { identity_account: solana_pubkey::new_rand().to_string(), vote_account: solana_pubkey::new_rand().to_string(), stake_account: solana_pubkey::new_rand().to_string(), + bls_pubkey: Some(bls_pubkey.to_string()), balance_lamports: 200000000000, stake_lamports: 20000000000, }, @@ -1276,6 +1357,7 @@ mod tests { identity_account: solana_pubkey::new_rand().to_string(), vote_account: solana_pubkey::new_rand().to_string(), stake_account: solana_pubkey::new_rand().to_string(), + bls_pubkey: Some(bls_pubkey.to_string()), balance_lamports: 300000000000, stake_lamports: 30000000000, }, @@ -1294,6 +1376,7 @@ mod tests { 100, &Rent::default(), &mut genesis_config, + false, ) .expect("Failed to load validator accounts"); @@ -1305,7 +1388,7 @@ mod tests { assert_eq!(genesis_config.accounts.len(), expected_accounts_len); // test account data matches - for b64_account in validator_accounts.iter() { + for (i, b64_account) in validator_accounts.iter().enumerate() { // check identity let identity_pk = b64_account.identity_account.parse().unwrap(); assert_eq!( @@ -1333,6 +1416,13 @@ mod tests { genesis_config.accounts[&stake_pk].lamports ); + // check BLS pubkey + if i == 0 { + assert!(b64_account.bls_pubkey.is_none()); + } else { + assert_eq!(b64_account.bls_pubkey, Some(bls_pubkey.to_string())); + } + let stake_data = genesis_config.accounts[&stake_pk].data.clone(); let stake_state = borsh1::try_from_slice_unchecked::(&stake_data).unwrap(); diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index de52bb0976..66390e8396 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -569,7 +569,7 @@ impl ClusterInfo { } let ip_addr = node.gossip().as_ref().map(SocketAddr::ip); Some(format!( - "{:15} {:2}| {:5} | {:44} |{:^9}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", + "{:15} {:2}| {:5} | {:44} |{:^9}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {:5}| {}\n", node.gossip() .filter(|addr| self.socket_addr_space.check(addr)) .as_ref() @@ -592,6 +592,7 @@ impl ClusterInfo { self.addr_to_string(&ip_addr, &node.tvu(contact_info::Protocol::UDP)), self.addr_to_string(&ip_addr, &node.tvu(contact_info::Protocol::QUIC)), self.addr_to_string(&ip_addr, &node.serve_repair(contact_info::Protocol::UDP)), + self.addr_to_string(&ip_addr, &node.alpenglow()), node.shred_version(), )) } @@ -600,9 +601,9 @@ impl ClusterInfo { format!( "IP Address |Age(ms)| Node identifier \ - | Version |Gossip|TPUvote| TPU |TPUfwd| TVU |TVU Q |ServeR|ShredVer\n\ + | Version |Gossip|TPUvote| TPU |TPUfwd| TVU |TVU Q |ServeR|Alpeng|ShredVer\n\ ------------------+-------+----------------------------------------------\ - +---------+------+-------+------+------+------+------+------+--------\n\ + +---------+------+-------+------+------+------+------+------+------+--------\n\ {}\ Nodes: {}{}{}", nodes.join(""), @@ -797,8 +798,8 @@ impl ClusterInfo { } } - /// If there are less than `MAX_LOCKOUT_HISTORY` votes present, returns the next index - /// without a vote. If there are `MAX_LOCKOUT_HISTORY` votes: + /// If there are less than `MAX_VOTES` votes present, returns the next index + /// without a vote. If there are `MAX_VOTES` votes: /// - Finds the oldest wallclock vote and returns its index /// - Otherwise returns the total amount of observed votes /// @@ -2411,6 +2412,7 @@ pub struct Sockets { pub tpu_quic: Vec, pub tpu_forwards_quic: Vec, pub tpu_vote_quic: Vec, + pub alpenglow: UdpSocket, } pub struct NodeConfig { @@ -2480,6 +2482,7 @@ impl Node { let tpu_vote_quic = bind_to_localhost().unwrap(); let tpu_vote_quic = bind_more_with_config(tpu_vote_quic, num_quic_endpoints, quic_config).unwrap(); + let alpenglow = bind_to_localhost().unwrap(); let repair = bind_to_localhost().unwrap(); let repair_quic = bind_to_localhost().unwrap(); @@ -2535,6 +2538,7 @@ impl Node { tpu_vote_quic[0].local_addr().unwrap(), "TPU-vote QUIC" ); + set_socket!(set_alpenglow, alpenglow.local_addr().unwrap(), "Alpenglow"); set_socket!(set_rpc, rpc_addr, "RPC"); set_socket!(set_rpc_pubsub, rpc_pubsub_addr, "RPC-pubsub"); set_socket!( @@ -2570,6 +2574,7 @@ impl Node { tpu_quic, tpu_forwards_quic, tpu_vote_quic, + alpenglow, }, } } @@ -2654,6 +2659,8 @@ impl Node { socket_config_reuseport, ) .unwrap(); + let (alpenglow_port, alpenglow) = + Self::bind_with_config(bind_ip_addr, port_range, socket_config); let (_, retransmit_socket) = Self::bind_with_config(bind_ip_addr, port_range, socket_config); @@ -2700,6 +2707,7 @@ impl Node { set_socket!(set_tpu_forwards, tpu_forwards_port, "TPU-forwards"); set_socket!(set_tpu_vote, UDP, tpu_vote_port, "TPU-vote"); set_socket!(set_tpu_vote, QUIC, tpu_vote_quic_port, "TPU-vote QUIC"); + set_socket!(set_alpenglow, alpenglow_port, "Alpenglow"); set_socket!(set_rpc, rpc_port, "RPC"); set_socket!(set_rpc_pubsub, rpc_pubsub_port, "RPC-pubsub"); set_socket!(set_serve_repair, UDP, serve_repair_port, "serve-repair"); @@ -2733,6 +2741,7 @@ impl Node { tpu_quic, tpu_forwards_quic, tpu_vote_quic, + alpenglow, }, } } @@ -2811,6 +2820,9 @@ impl Node { ) .unwrap(); + let (alpenglow_port, alpenglow) = + Self::bind_with_config(bind_ip_addr, port_range, socket_config); + let (_, retransmit_sockets) = multi_bind_in_range_with_config(bind_ip_addr, port_range, socket_config_reuseport, 8) .expect("retransmit multi_bind"); @@ -2854,6 +2866,7 @@ impl Node { .unwrap(); info.set_serve_repair(QUIC, (addr, serve_repair_quic_port)) .unwrap(); + info.set_alpenglow((addr, alpenglow_port)).unwrap(); trace!("new ContactInfo: {:?}", info); @@ -2878,6 +2891,7 @@ impl Node { tpu_quic, tpu_forwards_quic, tpu_vote_quic, + alpenglow, }, } } @@ -3335,6 +3349,7 @@ mod tests { check_socket(&node.sockets.gossip, ip, range); check_socket(&node.sockets.repair, ip, range); check_socket(&node.sockets.tvu_quic, ip, range); + check_socket(&node.sockets.alpenglow, ip, range); check_sockets(&node.sockets.tvu, ip, range); check_sockets(&node.sockets.tpu, ip, range); @@ -4316,6 +4331,10 @@ mod tests { #[test] fn test_contact_trace() { solana_logger::setup(); + // If you change the format of cluster_info_trace or rpc_info_trace, please make sure + // you read the actual output so the headers lign up with the output. + const CLUSTER_INFO_TRACE_LENGTH: usize = 452; + const RPC_INFO_TRACE_LENGTH: usize = 335; let keypair43 = Arc::new( Keypair::from_bytes(&[ 198, 203, 8, 178, 196, 71, 119, 152, 31, 96, 221, 142, 115, 224, 45, 34, 173, 138, @@ -4356,18 +4375,18 @@ mod tests { let trace = cluster_info44.contact_info_trace(); info!("cluster:\n{}", trace); - assert_eq!(trace.len(), 431); + assert_eq!(trace.len(), CLUSTER_INFO_TRACE_LENGTH); let trace = cluster_info44.rpc_info_trace(); info!("rpc:\n{}", trace); - assert_eq!(trace.len(), 335); + assert_eq!(trace.len(), RPC_INFO_TRACE_LENGTH); let trace = cluster_info43.contact_info_trace(); info!("cluster:\n{}", trace); - assert_eq!(trace.len(), 431); + assert_eq!(trace.len(), CLUSTER_INFO_TRACE_LENGTH); let trace = cluster_info43.rpc_info_trace(); info!("rpc:\n{}", trace); - assert_eq!(trace.len(), 335); + assert_eq!(trace.len(), RPC_INFO_TRACE_LENGTH); } } diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs index 6720b0fc18..9e197e3b86 100644 --- a/gossip/src/contact_info.rs +++ b/gossip/src/contact_info.rs @@ -47,8 +47,9 @@ const SOCKET_TAG_TPU_VOTE: u8 = 9; const SOCKET_TAG_TPU_VOTE_QUIC: u8 = 12; const SOCKET_TAG_TVU: u8 = 10; const SOCKET_TAG_TVU_QUIC: u8 = 11; -const_assert_eq!(SOCKET_CACHE_SIZE, 13); -const SOCKET_CACHE_SIZE: usize = SOCKET_TAG_TPU_VOTE_QUIC as usize + 1usize; +const SOCKET_TAG_ALPENGLOW: u8 = 13; +const_assert_eq!(SOCKET_CACHE_SIZE, 14); +const SOCKET_CACHE_SIZE: usize = SOCKET_TAG_ALPENGLOW as usize + 1usize; // An alias for a function that reads data from a ContactInfo entry stored in // the gossip CRDS table. @@ -276,6 +277,7 @@ impl ContactInfo { ); get_socket!(tpu_vote, SOCKET_TAG_TPU_VOTE, SOCKET_TAG_TPU_VOTE_QUIC); get_socket!(tvu, SOCKET_TAG_TVU, SOCKET_TAG_TVU_QUIC); + get_socket!(alpenglow, SOCKET_TAG_ALPENGLOW); set_socket!(set_gossip, SOCKET_TAG_GOSSIP); set_socket!(set_rpc, SOCKET_TAG_RPC); @@ -289,6 +291,7 @@ impl ContactInfo { set_socket!(@multi set_serve_repair, SOCKET_TAG_SERVE_REPAIR, SOCKET_TAG_SERVE_REPAIR_QUIC); set_socket!(@multi set_tpu_vote, SOCKET_TAG_TPU_VOTE, SOCKET_TAG_TPU_VOTE_QUIC); set_socket!(@multi set_tvu, SOCKET_TAG_TVU, SOCKET_TAG_TVU_QUIC); + set_socket!(set_alpenglow, SOCKET_TAG_ALPENGLOW); remove_socket!( remove_serve_repair, @@ -302,6 +305,7 @@ impl ContactInfo { SOCKET_TAG_TPU_FORWARDS_QUIC ); remove_socket!(remove_tvu, SOCKET_TAG_TVU, SOCKET_TAG_TVU_QUIC); + remove_socket!(remove_alpenglow, SOCKET_TAG_ALPENGLOW); #[cfg(test)] fn get_socket(&self, key: u8) -> Result { @@ -745,6 +749,7 @@ mod tests { assert_matches!(ci.tpu_vote(Protocol::QUIC), None); assert_matches!(ci.tvu(Protocol::QUIC), None); assert_matches!(ci.tvu(Protocol::UDP), None); + assert_matches!(ci.alpenglow(), None); } #[test] @@ -872,6 +877,10 @@ mod tests { } assert_eq!(node.gossip().as_ref(), sockets.get(&SOCKET_TAG_GOSSIP)); assert_eq!(node.rpc().as_ref(), sockets.get(&SOCKET_TAG_RPC)); + assert_eq!( + node.alpenglow().as_ref(), + sockets.get(&SOCKET_TAG_ALPENGLOW) + ); assert_eq!( node.rpc_pubsub().as_ref(), sockets.get(&SOCKET_TAG_RPC_PUBSUB) @@ -1087,6 +1096,23 @@ mod tests { assert_matches!(node.tpu_forwards(Protocol::QUIC), None); } + #[test] + fn test_set_and_remove_alpenglow() { + let mut rng = rand::thread_rng(); + let mut node = ContactInfo::new( + Keypair::new().pubkey(), + rng.gen(), // wallclock + rng.gen(), // shred_version + ); + let socket = repeat_with(|| new_rand_socket(&mut rng)) + .find(|socket| matches!(sanitize_socket(socket), Ok(()))) + .unwrap(); + node.set_alpenglow(socket).unwrap(); + assert_eq!(node.alpenglow().unwrap(), socket); + node.remove_alpenglow(); + assert_matches!(node.alpenglow(), None); + } + #[test] fn test_check_duplicate() { let mut rng = rand::thread_rng(); diff --git a/gossip/src/main.rs b/gossip/src/main.rs index 900b128e08..162062df97 100644 --- a/gossip/src/main.rs +++ b/gossip/src/main.rs @@ -5,7 +5,7 @@ use { crate_description, crate_name, value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand, }, - log::{error, info}, + log::{error, info, warn}, solana_clap_utils::{ hidden_unless_forced, input_parsers::{keypair_of, pubkeys_of}, @@ -42,10 +42,14 @@ fn parse_matches() -> ArgMatches<'static> { .value_name("HOST") .takes_value(true) .validator(solana_net_utils::is_host) - .help( - "Gossip DNS name or IP address for the node to advertise in gossip \ - [default: ask --entrypoint, or 127.0.0.1 when --entrypoint is not provided]", - ); + .help("DEPRECATED: --gossip-host is no longer supported. Use --bind-address instead."); + + let bind_address_arg = clap::Arg::with_name("bind_address") + .long("bind-address") + .value_name("HOST") + .takes_value(true) + .validator(solana_net_utils::is_host) + .help("IP address to bind the node to for gossip (replaces --gossip-host)"); App::new(crate_name!()) .about(crate_description!()) @@ -95,6 +99,7 @@ fn parse_matches() -> ArgMatches<'static> { .arg(&shred_version_arg) .arg(&gossip_port_arg) .arg(&gossip_host_arg) + .arg(&bind_address_arg) .setting(AppSettings::DisableVersion), ) .subcommand( @@ -149,6 +154,7 @@ fn parse_matches() -> ArgMatches<'static> { .arg(&shred_version_arg) .arg(&gossip_port_arg) .arg(&gossip_host_arg) + .arg(&bind_address_arg) .arg( Arg::with_name("timeout") .long("timeout") @@ -160,25 +166,30 @@ fn parse_matches() -> ArgMatches<'static> { .get_matches() } -fn parse_gossip_host(matches: &ArgMatches, entrypoint_addr: Option) -> IpAddr { - matches - .value_of("gossip_host") - .map(|gossip_host| { - solana_net_utils::parse_host(gossip_host).unwrap_or_else(|e| { - eprintln!("failed to parse gossip-host: {e}"); - exit(1); - }) +fn parse_bind_address(matches: &ArgMatches, entrypoint_addr: Option) -> IpAddr { + if let Some(bind_address) = matches.value_of("bind_address") { + solana_net_utils::parse_host(bind_address).unwrap_or_else(|e| { + eprintln!("failed to parse bind-address: {e}"); + exit(1); }) - .unwrap_or_else(|| { - if let Some(entrypoint_addr) = entrypoint_addr { - solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap_or_else(|err| { - eprintln!("Failed to contact cluster entrypoint {entrypoint_addr}: {err}"); - exit(1); - }) - } else { - IpAddr::V4(Ipv4Addr::LOCALHOST) - } + } else if let Some(gossip_host) = matches.value_of("gossip_host") { + warn!("--gossip-host is deprecated. Use --bind-address instead."); + solana_net_utils::parse_host(gossip_host).unwrap_or_else(|e| { + eprintln!("failed to parse gossip-host: {e}"); + exit(1); + }) + } else if let Some(entrypoint_addr) = entrypoint_addr { + solana_net_utils::get_public_ip_addr_with_binding( + &entrypoint_addr, + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + ) + .unwrap_or_else(|err| { + eprintln!("Failed to contact cluster entrypoint {entrypoint_addr}: {err}"); + exit(1); }) + } else { + IpAddr::V4(Ipv4Addr::LOCALHOST) + } } fn process_spy_results( @@ -350,9 +361,9 @@ fn process_rpc_url( } fn get_gossip_address(matches: &ArgMatches, entrypoint_addr: Option) -> SocketAddr { - let gossip_host = parse_gossip_host(matches, entrypoint_addr); + let bind_address = parse_bind_address(matches, entrypoint_addr); SocketAddr::new( - gossip_host, + bind_address, value_t!(matches, "gossip_port", u16).unwrap_or_else(|_| { solana_net_utils::find_available_port_in_range( IpAddr::V4(Ipv4Addr::UNSPECIFIED), diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index f75374cc6f..9a78042c7b 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -10,11 +10,13 @@ license = { workspace = true } edition = { workspace = true } [dependencies] +alpenglow-vote = { workspace = true } bs58 = { workspace = true } clap = { version = "3.1.5", features = ["cargo"] } dirs-next = { workspace = true } num_cpus = { workspace = true } serde_json = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["solana-signer-derive"] } solana-clap-v3-utils = { workspace = true } solana-cli-config = { workspace = true } solana-derivation-path = { workspace = true } @@ -26,6 +28,8 @@ solana-remote-wallet = { workspace = true, features = ["default"] } solana-seed-derivable = { workspace = true } solana-signer = { workspace = true } solana-version = { workspace = true } +solana-vote = { workspace = true } +solana-votor-messages = { workspace = true } tiny-bip39 = { workspace = true } [dev-dependencies] diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index 1450a13f44..fc771f163d 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -5,6 +5,7 @@ use { builder::ValueParser, crate_description, crate_name, value_parser, Arg, ArgAction, ArgMatches, Command, }, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey}, solana_clap_v3_utils::{ input_parsers::{ signer::{SignerSource, SignerSourceParserBuilder}, @@ -35,6 +36,7 @@ use { solana_pubkey::Pubkey, solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_signer::Signer, + solana_votor_messages::bls_message::BLS_KEYPAIR_DERIVE_SEED, std::{ collections::HashSet, error, @@ -388,6 +390,40 @@ fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> { .help("Overwrite the output file if it exists"), ) ) + .subcommand( + Command::new("bls_pubkey") + .about("Display the BLS pubkey derived from given ed25519 keypair file") + .disable_version_flag(true) + .arg( + Arg::new("keypair") + .index(1) + .value_name("KEYPAIR") + .takes_value(true) + .value_parser( + SignerSourceParserBuilder::default().allow_all().build() + ) + .help("Filepath or URL to a keypair"), + ) + .arg( + Arg::new(SKIP_SEED_PHRASE_VALIDATION_ARG.name) + .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) + .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help), + ) + .arg( + Arg::new("outfile") + .short('o') + .long("outfile") + .value_name("FILEPATH") + .takes_value(true) + .help("Path to generated file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .help("Overwrite the output file if it exists"), + ) + ) .subcommand( Command::new("recover") .about("Recover keypair from seed phrase and optional BIP39 passphrase") @@ -438,6 +474,24 @@ fn write_pubkey_file(outfile: &str, pubkey: Pubkey) -> Result<(), Box Result<(), Box> { + use std::io::Write; + + let printable = format!("{bls_pubkey}"); + let serialized = serde_json::to_string(&printable)?; + + if let Some(outdir) = std::path::Path::new(&outfile).parent() { + std::fs::create_dir_all(outdir)?; + } + let mut f = std::fs::File::create(outfile)?; + f.write_all(&serialized.into_bytes())?; + + Ok(()) +} + fn main() -> Result<(), Box> { let default_num_threads = num_cpus::get().to_string(); let matches = app(&default_num_threads, solana_version::version!()) @@ -470,6 +524,19 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { println!("{pubkey}"); } } + ("bls_pubkey", matches) => { + let keypair = get_keypair_from_matches(matches, config, &mut wallet_manager)?; + let bls_keypair = BLSKeypair::derive_from_signer(&keypair, BLS_KEYPAIR_DERIVE_SEED)?; + let bls_pubkey: BLSPubkey = bls_keypair.public.into(); + + if matches.try_contains_id("outfile")? { + let outfile = matches.get_one::("outfile").unwrap(); + check_for_overwrite(outfile, matches)?; + write_bls_pubkey_file(outfile, bls_pubkey)?; + } else { + println!("{bls_pubkey}"); + } + } ("new", matches) => { let mut path = dirs_next::home_dir().expect("home directory"); let outfile = if matches.try_contains_id("outfile")? { diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index 65129b53d6..38e1cc97b8 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -57,6 +57,7 @@ solana-transaction-status = { workspace = true } solana-type-overrides = { workspace = true } solana-unified-scheduler-pool = { workspace = true } solana-version = { workspace = true } +solana-vote = { workspace = true } solana-vote-program = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index e6fc98013d..eaf56bebd0 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -83,6 +83,7 @@ use { solana_stake_program::stake_state, solana_transaction_status::parse_ui_instruction, solana_unified_scheduler_pool::DefaultSchedulerPool, + solana_vote::vote_state_view::VoteStateView, solana_vote_program::{ self, vote_state::{self, VoteState}, @@ -221,16 +222,19 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { .map(|(_, (stake, _))| stake) .sum(); for (stake, vote_account) in bank.vote_accounts().values() { - let vote_state = vote_account.vote_state(); - if let Some(last_vote) = vote_state.votes.iter().last() { - let entry = last_votes.entry(vote_state.node_pubkey).or_insert(( - last_vote.slot(), - vote_state.clone(), + // TODO(wen): make this work for Alpenglow + let Some(vote_state_view) = vote_account.vote_state_view() else { + continue; + }; + if let Some(last_vote) = vote_state_view.last_voted_slot() { + let entry = last_votes.entry(*vote_state_view.node_pubkey()).or_insert(( + last_vote, + vote_state_view.clone(), *stake, total_stake, )); - if entry.0 < last_vote.slot() { - *entry = (last_vote.slot(), vote_state.clone(), *stake, total_stake); + if entry.0 < last_vote { + *entry = (last_vote, vote_state_view.clone(), *stake, total_stake); } } } @@ -254,19 +258,23 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { dot.push(" subgraph cluster_banks {".to_string()); dot.push(" style=invis".to_string()); let mut styled_slots = HashSet::new(); - let mut all_votes: HashMap> = HashMap::new(); + let mut all_votes: HashMap> = HashMap::new(); for fork_slot in &fork_slots { let mut bank = bank_forks[*fork_slot].clone(); let mut first = true; loop { for (_, vote_account) in bank.vote_accounts().values() { - let vote_state = vote_account.vote_state(); - if let Some(last_vote) = vote_state.votes.iter().last() { - let validator_votes = all_votes.entry(vote_state.node_pubkey).or_default(); + // TODO(wen): make this work for Alpenglow + let Some(vote_state_view) = vote_account.vote_state_view() else { + continue; + }; + if let Some(last_vote) = vote_state_view.last_voted_slot() { + let validator_votes = + all_votes.entry(*vote_state_view.node_pubkey()).or_default(); validator_votes - .entry(last_vote.slot()) - .or_insert_with(|| vote_state.clone()); + .entry(last_vote) + .or_insert_with(|| vote_state_view.clone()); } } @@ -344,7 +352,7 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { let mut absent_votes = 0; let mut lowest_last_vote_slot = u64::MAX; let mut lowest_total_stake = 0; - for (node_pubkey, (last_vote_slot, vote_state, stake, total_stake)) in &last_votes { + for (node_pubkey, (last_vote_slot, vote_state_view, stake, total_stake)) in &last_votes { all_votes.entry(*node_pubkey).and_modify(|validator_votes| { validator_votes.remove(last_vote_slot); }); @@ -364,9 +372,8 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { if matches!(config.vote_account_mode, GraphVoteAccountMode::WithHistory) { format!( "vote history:\n{}", - vote_state - .votes - .iter() + vote_state_view + .votes_iter() .map(|vote| format!( "slot {} (conf={})", vote.slot(), @@ -378,10 +385,9 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { } else { format!( "last vote slot: {}", - vote_state - .votes - .back() - .map(|vote| vote.slot().to_string()) + vote_state_view + .last_voted_slot() + .map(|vote_slot| vote_slot.to_string()) .unwrap_or_else(|| "none".to_string()) ) }; @@ -390,7 +396,7 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { node_pubkey, node_pubkey, lamports_to_sol(*stake), - vote_state.root_slot.unwrap_or(0), + vote_state_view.root_slot().unwrap_or(0), vote_history, )); @@ -419,16 +425,15 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String { // Add for vote information from all banks. if config.include_all_votes { for (node_pubkey, validator_votes) in &all_votes { - for (vote_slot, vote_state) in validator_votes { + for (vote_slot, vote_state_view) in validator_votes { dot.push(format!( r#" "{} vote {}"[shape=box,style=dotted,label="validator vote: {}\nroot slot: {}\nvote history:\n{}"];"#, node_pubkey, vote_slot, node_pubkey, - vote_state.root_slot.unwrap_or(0), - vote_state - .votes - .iter() + vote_state_view.root_slot().unwrap_or(0), + vote_state_view + .votes_iter() .map(|vote| format!("slot {} (conf={})", vote.slot(), vote.confirmation_count())) .collect::>() .join("\n") diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index 41d30a2360..6ebf98b27c 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -72,6 +72,7 @@ solana-transaction-context = { workspace = true } solana-transaction-status = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor-messages = { workspace = true } spl-token = { workspace = true, features = ["no-entrypoint"] } spl-token-2022 = { workspace = true, features = ["no-entrypoint"] } static_assertions = { workspace = true } diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index af74cedb9d..42f7e7a83e 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -58,6 +58,7 @@ use { VersionedConfirmedBlock, VersionedConfirmedBlockWithEntries, VersionedTransactionWithStatusMeta, }, + solana_votor_messages::bls_message::CertificateMessage, std::{ borrow::Cow, cell::RefCell, @@ -268,6 +269,7 @@ pub struct Blockstore { program_costs_cf: LedgerColumn, rewards_cf: LedgerColumn, roots_cf: LedgerColumn, + slot_certificates_cf: LedgerColumn, transaction_memos_cf: LedgerColumn, transaction_status_cf: LedgerColumn, transaction_status_index_cf: LedgerColumn, @@ -417,6 +419,7 @@ impl Blockstore { let program_costs_cf = db.column(); let rewards_cf = db.column(); let roots_cf = db.column(); + let slot_certificates_cf = db.column(); let transaction_memos_cf = db.column(); let transaction_status_cf = db.column(); let transaction_status_index_cf = db.column(); @@ -452,6 +455,7 @@ impl Blockstore { program_costs_cf, rewards_cf, roots_cf, + slot_certificates_cf, transaction_memos_cf, transaction_status_cf, transaction_status_index_cf, @@ -875,6 +879,7 @@ impl Blockstore { self.bank_hash_cf.submit_rocksdb_cf_metrics(); self.optimistic_slots_cf.submit_rocksdb_cf_metrics(); self.merkle_root_meta_cf.submit_rocksdb_cf_metrics(); + self.slot_certificates_cf.submit_rocksdb_cf_metrics(); } /// Report the accumulated RPC API metrics @@ -3828,14 +3833,17 @@ impl Blockstore { &self, slot: Slot, bank_hash: Hash, + is_leader: bool, feature_set: &FeatureSet, ) -> std::result::Result, BlockstoreProcessorError> { let results = self.check_last_fec_set(slot); let Ok(results) = results else { - warn!( - "Unable to check the last fec set for slot {slot} {bank_hash}, \ + if !is_leader { + warn!( + "Unable to check the last fec set for slot {slot} {bank_hash}, \ marking as dead: {results:?}", - ); + ); + } if feature_set.is_active(&solana_sdk::feature_set::vote_only_full_fec_sets::id()) { return Err(BlockstoreProcessorError::IncompleteFinalFecSet); } @@ -4012,6 +4020,54 @@ impl Blockstore { .map(|meta| (meta.hash(), meta.timestamp()))) } + /// Insert newly completed notarization fallback certificate for `slot` + /// If already present, this will overwrite the old certificate + pub fn insert_new_notarization_fallback_certificate( + &self, + slot: Slot, + block_id: Hash, + certificate: CertificateMessage, + ) -> Result<()> { + let mut certificates = self + .slot_certificates(slot)? + .unwrap_or(SlotCertificates::default()); + certificates.add_notarization_fallback_certificate(block_id, certificate); + self.slot_certificates_cf.put(slot, &certificates) + } + + /// Insert newly completed skip certificate for `slot` + /// If already present, this will overwrite the old certificate + pub fn insert_new_skip_certificate( + &self, + slot: Slot, + certificate: CertificateMessage, + ) -> Result<()> { + let mut certificates = self + .slot_certificates(slot)? + .unwrap_or(SlotCertificates::default()); + certificates.set_skip_certificate(certificate); + self.slot_certificates_cf.put(slot, &certificates) + } + + /// Returns all completed certificates for `slot` + pub fn slot_certificates(&self, slot: Slot) -> Result> { + self.slot_certificates_cf.get(slot) + } + + /// Returns all certificates from `slot` onwards + pub fn slot_certificates_iterator( + &self, + slot: Slot, + ) -> Result + '_> { + let iter = self + .slot_certificates_cf + .iter(IteratorMode::From(slot, IteratorDirection::Forward))?; + Ok(iter.map(|(slot, bytes)| { + let certs: SlotCertificates = deserialize(&bytes).unwrap(); + (slot, certs) + })) + } + /// Returns information about the `num` latest optimistically confirmed slot pub fn get_latest_optimistic_slots( &self, diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index 441d24eff9..e982800492 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -315,6 +315,10 @@ impl Blockstore { & self .merkle_root_meta_cf .delete_range_in_batch(write_batch, from_slot, to_slot) + .is_ok() + & self + .slot_certificates_cf + .delete_range_in_batch(write_batch, from_slot, to_slot) .is_ok(); match purge_type { @@ -396,6 +400,10 @@ impl Blockstore { .merkle_root_meta_cf .delete_file_in_range(from_slot, to_slot) .is_ok() + & self + .slot_certificates_cf + .delete_file_in_range(from_slot, to_slot) + .is_ok() } /// Returns true if the special columns, TransactionStatus and diff --git a/ledger/src/blockstore/column.rs b/ledger/src/blockstore/column.rs index 53e8c43032..a4b4e5f19a 100644 --- a/ledger/src/blockstore/column.rs +++ b/ledger/src/blockstore/column.rs @@ -215,6 +215,16 @@ pub mod columns { /// * index type: `crate::shred::ErasureSetId` `(Slot, fec_set_index: u32)` /// * value type: [`blockstore_meta::MerkleRootMeta`]` pub struct MerkleRootMeta; + + #[derive(Debug)] + /// The vote certificate column + /// + /// Stores the `NotarizeFallback` and `Skip` certificates for each column + /// for use during catch up and to serve repair + /// + /// * index type: `u64` (see [`SlotColumn`]) + /// * value type: [`blockstore_meta::SlotCertificates`] + pub struct SlotCertificates; } macro_rules! convert_column_index_to_key_bytes { @@ -848,3 +858,11 @@ impl ColumnName for columns::MerkleRootMeta { impl TypedColumn for columns::MerkleRootMeta { type Type = blockstore_meta::MerkleRootMeta; } + +impl SlotColumn for columns::SlotCertificates {} +impl ColumnName for columns::SlotCertificates { + const NAME: &'static str = "slot_certificates"; +} +impl TypedColumn for columns::SlotCertificates { + type Type = blockstore_meta::SlotCertificates; +} diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index 50527c71cb..f4a3f0a234 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -188,6 +188,7 @@ impl Rocks { new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), new_cf_descriptor::(options, oldest_slot), + new_cf_descriptor::(options, oldest_slot), ]; // If the access type is Secondary, we don't need to open all of the @@ -236,7 +237,7 @@ impl Rocks { cf_descriptors } - const fn columns() -> [&'static str; 21] { + const fn columns() -> [&'static str; 22] { [ columns::ErasureMeta::NAME, columns::DeadSlots::NAME, @@ -259,6 +260,7 @@ impl Rocks { columns::ProgramCosts::NAME, columns::OptimisticSlots::NAME, columns::MerkleRootMeta::NAME, + columns::SlotCertificates::NAME, ] } diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 6e01b80598..a6cfaeeda4 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -9,8 +9,9 @@ use { clock::{Slot, UnixTimestamp}, hash::Hash, }, + solana_votor_messages::bls_message::CertificateMessage, std::{ - collections::BTreeSet, + collections::{BTreeSet, HashMap}, ops::{Bound, Range, RangeBounds}, }, }; @@ -872,6 +873,42 @@ impl OptimisticSlotMetaVersioned { } } +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +/// Holds the certificates for this slot in blockstore +/// Under normal operation there will only be *one* certificate, +/// either `notarize_fallback` or `skip` +/// In the worse case (duplicate blocks) there can be at most: +/// - 3 `notarize_fallback certificates` +/// - plus 1 `skip_certificate` +/// +/// Note: Currently these are pre BLS `CertificateMessage`, but post BLS +/// the certificate will be one transaction / similar, roughly 800 bytes in size +/// +/// This will normally be written to once per slot, but in the worst case 4 times per slot +/// It will be read to serve repair to other nodes. +pub struct SlotCertificates { + /// The notarization fallback certificates keyed by block_id + pub notarize_fallback_certificates: HashMap, + /// The skip certificate + pub skip_certificate: Option, +} + +impl SlotCertificates { + /// Insert a new notarization fallback certificate for this slot. + /// Overwrites an existing one if it exists + pub fn add_notarization_fallback_certificate( + &mut self, + block_id: Hash, + cert: CertificateMessage, + ) { + self.notarize_fallback_certificates.insert(block_id, cert); + } + + pub fn set_skip_certificate(&mut self, cert: CertificateMessage) { + self.skip_certificate.replace(cert); + } +} + #[cfg(test)] mod test { use { diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index ead09698bf..03eded8a55 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -807,6 +807,15 @@ pub enum BlockstoreProcessorError { #[error("invalid retransmitter signature final fec set")] InvalidRetransmitterSignatureFinalFecSet, + + #[error("invalid notarization certificate in bank {0} for slot {1}")] + InvalidNotarizationCertificate(Slot, Slot), + + #[error("invalid skip certificate in bank {0} for slot range {1} - {2}")] + InvalidSkipCertificate(Slot, Slot, Slot), + + #[error("non consecutive leader slot for bank {0} parent {1}")] + NonConsecutiveLeaderSlot(Slot, Slot), } /// Callback for accessing bank state after each slot is confirmed while @@ -1089,7 +1098,7 @@ pub fn process_blockstore_from_root( /// Verify that a segment of entries has the correct number of ticks and hashes fn verify_ticks( bank: &Bank, - entries: &[Entry], + mut entries: &[Entry], slot_full: bool, tick_hash_count: &mut u64, ) -> std::result::Result<(), BlockError> { @@ -1119,6 +1128,28 @@ fn verify_ticks( } } + if let Some(first_alpenglow_slot) = bank + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()) + { + if bank.parent_slot() >= first_alpenglow_slot { + return Ok(()); + } + + if bank.slot() >= first_alpenglow_slot && next_bank_tick_height == max_bank_tick_height { + if entries.is_empty() { + // This shouldn't happen, but good to double check + error!("Processing empty entries in verify_ticks()"); + return Ok(()); + } + // last entry must be a tick, as verified by the `has_trailing_entry` + // check above. Because in Alpenglow the last tick does not have any + // hashing guarantees, we pass everything but that last tick to the + // entry verification. + entries = &entries[..entries.len() - 1]; + } + } + let hashes_per_tick = bank.hashes_per_tick().unwrap_or(0); if !entries.verify_tick_hash_count(tick_hash_count, hashes_per_tick) { warn!( @@ -1816,6 +1847,7 @@ fn process_next_slots( .unwrap(), *next_slot, ); + set_alpenglow_ticks(&next_bank); trace!( "New bank for slot {}, parent slot is {}", next_slot, @@ -1830,6 +1862,59 @@ fn process_next_slots( Ok(()) } +/// Set alpenglow bank tick height. +/// For alpenglow banks this tick height is `max_tick_height` - 1, +/// For a bank on the boundary of feature activation, we need ticks_per_slot for +/// TowerBFT ticks, and one extra tick for the alpenglow bank +pub fn set_alpenglow_ticks(bank: &Bank) { + let Some(first_alpenglow_slot) = bank + .feature_set + .activated_slot(&solana_feature_set::secp256k1_program_enabled::id()) + else { + return; + }; + + let Some(alpenglow_ticks) = calculate_alpenglow_ticks( + bank.slot(), + first_alpenglow_slot, + bank.parent_slot(), + bank.ticks_per_slot(), + ) else { + return; + }; + + info!( + "Setting tick height for slot {} to {}", + bank.slot(), + bank.max_tick_height() - alpenglow_ticks + ); + bank.set_tick_height(bank.max_tick_height() - alpenglow_ticks); +} + +fn calculate_alpenglow_ticks( + slot: Slot, + first_alpenglow_slot: Slot, + parent_slot: Slot, + ticks_per_slot: u64, +) -> Option { + // Slots before alpenglow shouldn't have alpenglow ticks + if slot < first_alpenglow_slot { + return None; + } + + let alpenglow_ticks = if parent_slot < first_alpenglow_slot && slot >= first_alpenglow_slot { + // 1. All slots between the parent and the first alpenglow slot need to + // have `ticks_per_slot` ticks + // 2. One extra tick for the actual alpenglow slot + // 3. There are no ticks for any skipped alpenglow slots + (first_alpenglow_slot - parent_slot - 1) * ticks_per_slot + 1 + } else { + 1 + }; + + Some(alpenglow_ticks) +} + /// Starting with the root slot corresponding to `start_slot_meta`, iteratively /// find and process children slots from the blockstore. /// @@ -2098,7 +2183,11 @@ fn supermajority_root_from_vote_accounts( return None; } - Some((account.vote_state().root_slot?, *stake)) + if let Some(vote_state_view) = account.vote_state_view() { + Some((vote_state_view.root_slot()?, *stake)) + } else { + None + } }) .collect(); @@ -2166,7 +2255,7 @@ pub fn process_single_slot( result? } - let block_id = blockstore.check_last_fec_set_and_get_block_id(slot, bank.hash(), &bank.feature_set) + let block_id = blockstore.check_last_fec_set_and_get_block_id(slot, bank.hash(), false, &bank.feature_set) .inspect_err(|err| { warn!("slot {} failed last fec set checks: {}", slot, err); if blockstore.is_primary_access() { @@ -5345,4 +5434,60 @@ pub mod tests { check_block_cost_limits(&bank, &processing_results, &txs) ); } + + #[test] + fn test_calculate_alpenglow_ticks() { + let first_alpenglow_slot = 10; + let ticks_per_slot = 2; + + // Slots before alpenglow don't have alpenglow ticks + let slot = 9; + let parent_slot = 8; + assert!( + calculate_alpenglow_ticks(slot, first_alpenglow_slot, parent_slot, ticks_per_slot) + .is_none() + ); + + // First alpenglow slot should only have 1 tick + let slot = first_alpenglow_slot; + let parent_slot = first_alpenglow_slot - 1; + assert_eq!( + calculate_alpenglow_ticks(slot, first_alpenglow_slot, parent_slot, ticks_per_slot) + .unwrap(), + 1 + ); + + // First alpenglow slot with skipped non-alpenglow slots + // need to have `ticks_per_slot` ticks per skipped slot and + // then one additional tick for the first alpenglow slot + let slot = first_alpenglow_slot; + let num_skipped_slots = 3; + let parent_slot = first_alpenglow_slot - num_skipped_slots - 1; + assert_eq!( + calculate_alpenglow_ticks(slot, first_alpenglow_slot, parent_slot, ticks_per_slot) + .unwrap(), + num_skipped_slots * ticks_per_slot + 1 + ); + + // Skipped alpenglow slots don't need any additional ticks + let slot = first_alpenglow_slot + 2; + let parent_slot = first_alpenglow_slot; + assert_eq!( + calculate_alpenglow_ticks(slot, first_alpenglow_slot, parent_slot, ticks_per_slot) + .unwrap(), + 1 + ); + + // Skipped alpenglow slots along skipped non-alpenglow slots + // need to have `ticks_per_slot` ticks per skipped non-alpenglow + // slot only and then one additional tick for the alpenglow slot + let slot = first_alpenglow_slot + 2; + let num_skipped_non_alpenglow_slots = 4; + let parent_slot = first_alpenglow_slot - num_skipped_non_alpenglow_slots - 1; + assert_eq!( + calculate_alpenglow_ticks(slot, first_alpenglow_slot, parent_slot, ticks_per_slot) + .unwrap(), + num_skipped_non_alpenglow_slots * ticks_per_slot + 1 + ); + } } diff --git a/ledger/src/genesis_utils.rs b/ledger/src/genesis_utils.rs index 6844278082..5a334fe15a 100644 --- a/ledger/src/genesis_utils.rs +++ b/ledger/src/genesis_utils.rs @@ -25,5 +25,6 @@ pub fn create_genesis_config_with_mint_keypair( mint_lamports, &Pubkey::new_unique(), bootstrap_validator_stake_lamports(), + None, ) } diff --git a/ledger/src/leader_schedule_utils.rs b/ledger/src/leader_schedule_utils.rs index 187c2bc414..ce86bebcf0 100644 --- a/ledger/src/leader_schedule_utils.rs +++ b/ledger/src/leader_schedule_utils.rs @@ -59,6 +59,27 @@ pub fn first_of_consecutive_leader_slots(slot: Slot) -> Slot { (slot / NUM_CONSECUTIVE_LEADER_SLOTS) * NUM_CONSECUTIVE_LEADER_SLOTS } +/// Returns the last slot in the leader window that contains `slot` +#[inline] +pub fn last_of_consecutive_leader_slots(slot: Slot) -> Slot { + first_of_consecutive_leader_slots(slot) + NUM_CONSECUTIVE_LEADER_SLOTS - 1 +} + +/// Returns the index within the leader slot range that contains `slot` +#[inline] +pub fn leader_slot_index(slot: Slot) -> usize { + (slot % NUM_CONSECUTIVE_LEADER_SLOTS) as usize +} + +/// Returns the number of slots left after `slot` in the leader window +/// that contains `slot` +#[inline] +pub fn remaining_slots_in_window(slot: Slot) -> u64 { + NUM_CONSECUTIVE_LEADER_SLOTS + .checked_sub(leader_slot_index(slot) as u64) + .unwrap() +} + #[cfg(test)] mod tests { use { diff --git a/ledger/src/shred.rs b/ledger/src/shred.rs index 12d870f8fc..636e0ac7a0 100644 --- a/ledger/src/shred.rs +++ b/ledger/src/shred.rs @@ -105,7 +105,7 @@ pub const SIZE_OF_NONCE: usize = std::mem::size_of::(); /// `test_shred_constants` ensures that the values are correct. /// Constants are used over lazy_static for performance reasons. const SIZE_OF_COMMON_SHRED_HEADER: usize = 83; -const SIZE_OF_DATA_SHRED_HEADERS: usize = 88; +pub const SIZE_OF_DATA_SHRED_HEADERS: usize = 88; const SIZE_OF_CODING_SHRED_HEADERS: usize = 89; const SIZE_OF_SIGNATURE: usize = SIGNATURE_BYTES; diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index 82104121a2..ab74630663 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -10,17 +10,23 @@ license = { workspace = true } edition = { workspace = true } [dependencies] +bincode = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } log = { workspace = true } rand = { workspace = true } rayon = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true } +solana-build-alpenglow-vote = { workspace = true } solana-client = { workspace = true } +solana-clock = { workspace = true } solana-config-program = { workspace = true } +solana-connection-cache = { workspace = true } solana-core = { workspace = true } solana-entry = { workspace = true } solana-gossip = { workspace = true } +solana-keypair = { workspace = true } solana-ledger = { workspace = true } solana-logger = { workspace = true } solana-net-utils = { workspace = true } @@ -34,9 +40,12 @@ solana-stake-program = { workspace = true } solana-streamer = { workspace = true } solana-thin-client = { workspace = true } solana-tpu-client = { workspace = true } +solana-transaction = { workspace = true, features = ["bincode"] } solana-turbine = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor = { workspace = true } +solana-votor-messages = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } tempfile = { workspace = true } diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 2b05ad35b1..2483b5ce60 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -37,11 +37,15 @@ use { }, solana_streamer::socket::SocketAddrSpace, solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig, TpuSenderError}, - solana_vote::vote_transaction::{self, VoteTransaction}, + solana_vote::{ + vote_parser::ParsedVoteTransaction, + vote_transaction::{self}, + }, solana_vote_program::vote_state::TowerSync, + solana_votor_messages::bls_message::BLSMessage, std::{ collections::{HashMap, HashSet, VecDeque}, - net::{SocketAddr, TcpListener}, + net::{SocketAddr, TcpListener, UdpSocket}, path::Path, sync::{ atomic::{AtomicBool, Ordering}, @@ -397,12 +401,46 @@ pub fn check_for_new_roots( connection_cache: &Arc, test_name: &str, ) { - let mut roots = vec![HashSet::new(); contact_infos.len()]; + check_for_new_commitment_slots( + num_new_roots, + contact_infos, + connection_cache, + test_name, + CommitmentConfig::finalized(), + ); +} + +/// For alpenglow, CommitmentConfig::processed() refers to the current voting loop slot, +/// so this is more accurate for determining that each node is voting when stake distribution is +/// uneven +pub fn check_for_new_processed( + num_new_processed: usize, + contact_infos: &[ContactInfo], + connection_cache: &Arc, + test_name: &str, +) { + check_for_new_commitment_slots( + num_new_processed, + contact_infos, + connection_cache, + test_name, + CommitmentConfig::processed(), + ); +} + +fn check_for_new_commitment_slots( + num_new_slots: usize, + contact_infos: &[ContactInfo], + connection_cache: &Arc, + test_name: &str, + commitment: CommitmentConfig, +) { + let mut slots = vec![HashSet::new(); contact_infos.len()]; let mut done = false; let mut last_print = Instant::now(); let loop_start = Instant::now(); let loop_timeout = Duration::from_secs(180); - let mut num_roots_map = HashMap::new(); + let mut num_slots_map = HashMap::new(); while !done { assert!(loop_start.elapsed() < loop_timeout); @@ -410,16 +448,16 @@ pub fn check_for_new_roots( let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); let root_slot = client .rpc_client() - .get_slot_with_commitment(CommitmentConfig::finalized()) + .get_slot_with_commitment(commitment) .unwrap_or(0); - roots[i].insert(root_slot); - num_roots_map.insert(*ingress_node.pubkey(), roots[i].len()); - let num_roots = roots.iter().map(|r| r.len()).min().unwrap(); - done = num_roots >= num_new_roots; + slots[i].insert(root_slot); + num_slots_map.insert(*ingress_node.pubkey(), slots[i].len()); + let num_slots = slots.iter().map(|r| r.len()).min().unwrap(); + done = num_slots >= num_new_slots; if done || last_print.elapsed().as_secs() > 3 { info!( - "{} waiting for {} new roots.. observed: {:?}", - test_name, num_new_roots, num_roots_map + "{} waiting for {} new {:?} slots.. observed: {:?}", + test_name, num_new_slots, commitment.commitment, num_slots_map ); last_print = Instant::now(); } @@ -497,6 +535,90 @@ pub fn check_no_new_roots( } } +pub fn check_for_new_notarized_votes( + num_new_votes: usize, + contact_infos: &[ContactInfo], + connection_cache: &Arc, + test_name: &str, + vote_listener: UdpSocket, +) { + let loop_start = Instant::now(); + let loop_timeout = Duration::from_secs(180); + // First get the current max root. + let Some(current_root) = contact_infos + .iter() + .map(|ingress_node| { + let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); + let root_slot = client + .rpc_client() + .get_slot_with_commitment(CommitmentConfig::processed()) + .unwrap_or(0); + root_slot + }) + .max() + else { + panic!("No nodes found to get current root"); + }; + + // Clone data for thread + let contact_infos_owned: Vec = contact_infos.to_vec(); + let test_name_owned = test_name.to_string(); + + // Now start vote listener and wait for new notarized votes. + let vote_listener = std::thread::spawn({ + let mut buf = [0_u8; 65_535]; + let mut num_new_notarized_votes = contact_infos_owned.iter().map(|_| 0).collect::>(); + let mut last_notarized = contact_infos_owned + .iter() + .map(|_| current_root) + .collect::>(); + let mut last_print = Instant::now(); + let mut done = false; + + move || { + while !done { + assert!(loop_start.elapsed() < loop_timeout); + let n_bytes = vote_listener + .recv_from(&mut buf) + .expect("Failed to receive vote message") + .0; + let bls_message = bincode::deserialize::(&buf[0..n_bytes]).unwrap(); + let BLSMessage::Vote(vote_message) = bls_message else { + continue; + }; + let vote = vote_message.vote; + if !vote.is_notarization() { + continue; + } + let rank = vote_message.rank; + if rank >= contact_infos_owned.len() as u16 { + warn!( + "Received vote with rank {} which is greater than number of nodes {}", + rank, + contact_infos_owned.len() + ); + continue; + } + let slot = vote.slot(); + if slot <= last_notarized[rank as usize] { + continue; + } + last_notarized[rank as usize] = slot; + num_new_notarized_votes[rank as usize] += 1; + done = num_new_notarized_votes.iter().all(|&x| x > num_new_votes); + if done || last_print.elapsed().as_secs() > 3 { + info!( + "{} waiting for {} new notarized votes.. observed: {:?}", + test_name_owned, num_new_votes, num_new_notarized_votes + ); + last_print = Instant::now(); + } + } + } + }); + vote_listener.join().expect("Vote listener thread panicked"); +} + fn poll_all_nodes_for_signature( entry_point_info: &ContactInfo, cluster_nodes: &[ContactInfo], @@ -517,6 +639,9 @@ fn poll_all_nodes_for_signature( Ok(()) } +/// Represents a service that monitors the gossip network for votes, processes them according to +/// provided filters and callbacks, and maintains a connection to the gossip network. Often used as +/// a "spy" representing a Byzantine node in a cluster. pub struct GossipVoter { pub gossip_service: GossipService, pub tcp_listener: Option, @@ -533,16 +658,22 @@ impl GossipVoter { } } -/// Reads votes from gossip and runs them through `vote_filter` to filter votes that then -/// get passed to `generate_vote_tx` to create votes that are then pushed into gossip as if -/// sent by a node with identity `node_keypair`. +/// Creates and starts a gossip voter service that monitors the gossip network for votes. +/// This service: +/// 1. Connects to the gossip network at the specified address using the node's keypair +/// 2. Waits for a specified number of peers to join before becoming active +/// 3. Continuously polls for new votes in the network +/// 4. Filters incoming votes through the provided `vote_filter` function +/// 5. Processes filtered votes using the `process_vote_tx` callback +/// 6. Maintains a queue of recent votes and periodically refreshes them +/// 7. Returns a GossipVoter struct that can be used to control and shut down the service pub fn start_gossip_voter( gossip_addr: &SocketAddr, node_keypair: &Keypair, - vote_filter: impl Fn((CrdsValueLabel, Transaction)) -> Option<(VoteTransaction, Transaction)> + vote_filter: impl Fn((CrdsValueLabel, Transaction)) -> Option<(ParsedVoteTransaction, Transaction)> + std::marker::Send + 'static, - mut process_vote_tx: impl FnMut(Slot, &Transaction, &VoteTransaction, &ClusterInfo) + mut process_vote_tx: impl FnMut(Slot, &Transaction, &ParsedVoteTransaction, &ClusterInfo) + std::marker::Send + 'static, sleep_ms: u64, @@ -569,7 +700,7 @@ pub fn start_gossip_voter( } let mut latest_voted_slot = 0; - let mut refreshable_votes: VecDeque<(Transaction, VoteTransaction)> = VecDeque::new(); + let mut refreshable_votes: VecDeque<(Transaction, ParsedVoteTransaction)> = VecDeque::new(); let mut latest_push_attempt = Instant::now(); let t_voter = { diff --git a/local-cluster/src/integration_tests.rs b/local-cluster/src/integration_tests.rs index 4c35d1bea9..0bac76485a 100644 --- a/local-cluster/src/integration_tests.rs +++ b/local-cluster/src/integration_tests.rs @@ -18,9 +18,11 @@ use { }, log::*, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, + solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_core::{ consensus::{tower_storage::FileTowerStorage, Tower, SWITCH_FORK_THRESHOLD}, validator::{is_snapshot_config_valid, ValidatorConfig}, + voting_service::{AlpenglowPortOverride, VotingServiceOverride}, }, solana_gossip::gossip_service::discover_cluster, solana_ledger::{ @@ -46,8 +48,9 @@ use { solana_turbine::broadcast_stage::BroadcastStageType, static_assertions, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fs, iter, + net::SocketAddr, num::NonZeroUsize, path::{Path, PathBuf}, sync::{ @@ -63,6 +66,12 @@ use { pub const RUST_LOG_FILTER: &str = "error,solana_core::replay_stage=warn,solana_local_cluster=info,local_cluster=info"; +pub const AG_DEBUG_LOG_FILTER: &str = "error,solana_core::replay_stage=info,\ + solana_local_cluster=info,local_cluster=info,\ + solana_core::block_creation_loop=trace,\ + solana_votor=trace,\ + solana_votor::vote_history_storage=info,\ + solana_core::validator=info"; pub const DEFAULT_NODE_STAKE: u64 = 10 * LAMPORTS_PER_SOL; pub fn last_vote_in_tower(tower_path: &Path, node_pubkey: &Pubkey) -> Option<(Slot, Hash)> { @@ -192,7 +201,26 @@ pub fn ms_for_n_slots(num_blocks: u64, ticks_per_slot: u64) -> u64 { (ticks_per_slot * DEFAULT_MS_PER_SLOT * num_blocks).div_ceil(DEFAULT_TICKS_PER_SLOT) } -pub fn run_kill_partition_switch_threshold( +/// Implements a test scenario that creates a network partition by killing validator nodes. +/// +/// # Arguments +/// * `stakes_to_kill` - Validators to remove from the network, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `alive_stakes` - Validators to keep alive, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `ticks_per_slot` - Optional override for the default ticks per slot +/// * `partition_context` - Test-specific context object that will be passed to callbacks +/// * `on_partition_start` - Callback executed when the partition begins +/// * `on_before_partition_resolved` - Callback executed right before the partition is resolved +/// * `on_partition_resolved` - Callback executed after the partition is resolved +/// +/// This function simulates a network partition by killing specified validator nodes, +/// waiting for a period, resolving the partition, and then verifying the network +/// can recover and reach consensus. The IS_ALPENGLOW parameter determines whether +/// to use Alpenglow-specific cluster initialization. +fn run_kill_partition_switch_threshold_impl( stakes_to_kill: &[(usize, usize)], alive_stakes: &[(usize, usize)], ticks_per_slot: Option, @@ -250,7 +278,7 @@ pub fn run_kill_partition_switch_threshold( partition_context, ); }; - run_cluster_partition( + run_cluster_partition::( &stake_partitions, Some((leader_schedule, validator_keys)), partition_context, @@ -259,18 +287,65 @@ pub fn run_kill_partition_switch_threshold( on_partition_resolved, ticks_per_slot, vec![], + IS_ALPENGLOW, + ) +} + +pub fn run_kill_partition_switch_threshold_alpenglow( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, + ) +} + +pub fn run_kill_partition_switch_threshold( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, ) } pub fn create_custom_leader_schedule( validator_key_to_slots: impl Iterator, ) -> LeaderSchedule { - let mut leader_schedule = vec![]; - for (k, num_slots) in validator_key_to_slots { - for _ in 0..num_slots { - leader_schedule.push(k) - } - } + let leader_schedule: Vec<_> = validator_key_to_slots + .flat_map(|(pubkey, num_slots)| { + // Ensure that the number of slots is a multiple of NUM_CONSECUTIVE_LEADER_SLOTS + // Because we only check leadership every NUM_CONSECUTIVE_LEADER_SLOTS slots, for + // example, you can have [(pubkey_A, 70), (pubkey_B, 30)], A will happily produce + // block 70 and 71 because it is the leader for block 68, but when B gets the shred + // it check leadership for block 70 and 71, it will see that it is the leader, so the + // shreds from A will be ignored. + assert!(num_slots % (NUM_CONSECUTIVE_LEADER_SLOTS as usize) == 0); + std::iter::repeat(pubkey).take(num_slots) + }) + .collect(); info!("leader_schedule: {}", leader_schedule.len()); Box::new(IdentityKeyedLeaderSchedule::new_from_schedule( @@ -293,14 +368,35 @@ pub fn create_custom_leader_schedule_with_random_keys( (leader_schedule, validator_keys) } -/// This function runs a network, initiates a partition based on a -/// configuration, resolve the partition, then checks that the network -/// continues to achieve consensus +/// Simulates a network partition test scenario by creating a cluster, triggering a partition, +/// allowing the partition to heal, and then verifying the network's ability to recover and +/// achieve consensus after the partition is resolved. +/// +/// This function: +/// 1. Creates a local cluster with nodes configured according to the provided stakes +/// 2. Induces a network partition by disabling communication between validators +/// 3. Runs the partition for a predetermined duration +/// 4. Resolves the partition by re-enabling communication +/// 5. Verifies the network can recover and continue to make progress +/// /// # Arguments -/// * `partitions` - A slice of partition configurations, where each partition -/// configuration is a usize representing a node's stake -/// * `leader_schedule` - An option that specifies whether the cluster should -/// run with a fixed, predetermined leader schedule +/// * `partitions` - A slice of partition configurations, where each usize represents a validator's +/// stake weight. This determines the relative voting power of each node in the network. +/// * `leader_schedule` - An option that specifies whether the cluster should run with a fixed, +/// predetermined leader schedule. If provided, the partition will last for one complete +/// iteration of the leader schedule. +/// * `context` - A user-defined context object that is passed to the callback functions. +/// * `on_partition_start` - Callback function that runs when the partition begins. Can be used +/// to perform custom actions or checks at the start of the partition. +/// * `on_before_partition_resolved` - Callback function that runs just before the partition +/// is resolved. Can be used to verify partition state or prepare for resolution. +/// * `on_partition_resolved` - Callback function that runs after the partition is resolved and +/// the network has had time to recover. Can be used to verify recovery. +/// * `ticks_per_slot` - Optional override for the default ticks per slot. Controls the +/// rate at which slots advance in the cluster. +/// * `additional_accounts` - Additional accounts to be added to the genesis configuration. +/// * `is_alpenglow` - Boolean flag indicating whether to initialize the `LocalCluster` in Alpenglow +/// mode. #[allow(clippy::cognitive_complexity)] pub fn run_cluster_partition( partitions: &[usize], @@ -311,6 +407,7 @@ pub fn run_cluster_partition( on_partition_resolved: impl FnOnce(&mut LocalCluster, &mut C), ticks_per_slot: Option, additional_accounts: Vec<(Pubkey, AccountSharedData)>, + is_alpenglow: bool, ) { solana_logger::setup_with_default(RUST_LOG_FILTER); info!("PARTITION_TEST!"); @@ -351,10 +448,21 @@ pub fn run_cluster_partition( }; let slots_per_epoch = 2048; + let alpenglow_port_override = AlpenglowPortOverride::default(); + let validator_configs = make_identical_validator_configs(&validator_config, num_nodes) + .into_iter() + .map(|mut config| { + config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![], + alpenglow_port_override: alpenglow_port_override.clone(), + }); + config + }) + .collect(); let mut config = ClusterConfig { mint_lamports, node_stakes, - validator_configs: make_identical_validator_configs(&validator_config, num_nodes), + validator_configs, validator_keys: Some( validator_keys .into_iter() @@ -374,7 +482,12 @@ pub fn run_cluster_partition( "PARTITION_TEST starting cluster with {:?} partitions slots_per_epoch: {}", partitions, config.slots_per_epoch, ); - let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); + + let mut cluster = if is_alpenglow { + LocalCluster::new_alpenglow(&mut config, SocketAddrSpace::Unspecified) + } else { + LocalCluster::new(&mut config, SocketAddrSpace::Unspecified) + }; info!("PARTITION_TEST spend_and_verify_all_nodes(), ensure all nodes are caught up"); cluster_tests::spend_and_verify_all_nodes( @@ -404,12 +517,25 @@ pub fn run_cluster_partition( info!("PARTITION_TEST start partition"); on_partition_start(&mut cluster, &mut context); turbine_disabled.store(true, Ordering::Relaxed); - + // Make all to all votes/certs not able to reach each other by overriding the + // alpenglow port override to SocketAddr which no one is listening on. + let blackhole_addr: SocketAddr = solana_net_utils::bind_to_localhost() + .unwrap() + .local_addr() + .unwrap(); + let new_override = HashMap::from_iter( + cluster_nodes + .iter() + .map(|node| (*node.pubkey(), blackhole_addr)), + ); + alpenglow_port_override.update_override(new_override); sleep(partition_duration); on_before_partition_resolved(&mut cluster, &mut context); info!("PARTITION_TEST remove partition"); turbine_disabled.store(false, Ordering::Relaxed); + // Restore the alpenglow port override to the default, so that the nodes can communicate again. + alpenglow_port_override.clear(); // Give partitions time to propagate their blocks from during the partition // after the partition resolves diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 249899dffd..bf27f99c69 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -58,11 +58,12 @@ use { vote_instruction, vote_state::{self, VoteInit}, }, + solana_votor::vote_history_storage::FileVoteHistoryStorage, std::{ collections::HashMap, io::{Error, ErrorKind, Result}, iter, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Instant, @@ -184,6 +185,8 @@ impl LocalCluster { .0, ]; config.tower_storage = Arc::new(FileTowerStorage::new(ledger_path.to_path_buf())); + config.vote_history_storage = + Arc::new(FileVoteHistoryStorage::new(ledger_path.to_path_buf())); let snapshot_config = &mut config.snapshot_config; let dummy: PathBuf = DUMMY_SNAPSHOT_CONFIG_PATH_MARKER.into(); @@ -196,6 +199,22 @@ impl LocalCluster { } pub fn new(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self { + Self::init(config, socket_addr_space, None) + } + + pub fn new_alpenglow(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self { + Self::init( + config, + socket_addr_space, + Some(build_alpenglow_vote::ALPENGLOW_VOTE_SO_PATH), + ) + } + + pub fn init( + config: &mut ClusterConfig, + socket_addr_space: SocketAddrSpace, + alpenglow_so_path: Option<&str>, + ) -> Self { assert_eq!(config.validator_configs.len(), config.node_stakes.len()); let quic_connection_cache_config = config.tpu_use_quic.then(|| { @@ -271,11 +290,11 @@ impl LocalCluster { ); if *in_genesis { Some(( - ValidatorVoteKeypairs { - node_keypair: node_keypair.insecure_clone(), - vote_keypair: vote_keypair.insecure_clone(), - stake_keypair: Keypair::new(), - }, + ValidatorVoteKeypairs::new( + node_keypair.insecure_clone(), + vote_keypair.insecure_clone(), + Keypair::new(), + ), stake, )) } else { @@ -305,6 +324,7 @@ impl LocalCluster { &keys_in_genesis, stakes_in_genesis, config.cluster_type, + alpenglow_so_path, ); genesis_config.accounts.extend( config @@ -663,6 +683,23 @@ impl LocalCluster { info!("{} done waiting for roots", test_name); } + pub fn check_for_new_processed( + &self, + num_new_processed: usize, + test_name: &str, + socket_addr_space: SocketAddrSpace, + ) { + let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); + info!("{} looking for new processed slots on all nodes", test_name); + cluster_tests::check_for_new_processed( + num_new_processed, + &alive_node_contact_infos, + &self.connection_cache, + test_name, + ); + info!("{} done waiting for processed slots", test_name); + } + pub fn check_no_new_roots( &self, num_slots_to_wait: usize, @@ -693,6 +730,25 @@ impl LocalCluster { info!("{} done waiting for roots", test_name); } + pub fn check_for_new_notarized_votes( + &self, + num_new_notarized_votes: usize, + test_name: &str, + socket_addr_space: SocketAddrSpace, + vote_listener_addr: UdpSocket, + ) { + let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); + info!("{} looking for new notarized votes on all nodes", test_name); + cluster_tests::check_for_new_notarized_votes( + num_new_notarized_votes, + &alive_node_contact_infos, + &self.connection_cache, + test_name, + vote_listener_addr, + ); + info!("{} done waiting for notarized votes", test_name); + } + /// Attempt to send and confirm tx "attempts" times /// Wait for signature confirmation before returning /// Return the transaction signature diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs index 50199be9e7..db76a7aca2 100644 --- a/local-cluster/src/validator_configs.rs +++ b/local-cluster/src/validator_configs.rs @@ -35,6 +35,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { run_verification: config.run_verification, require_tower: config.require_tower, tower_storage: config.tower_storage.clone(), + vote_history_storage: config.vote_history_storage.clone(), debug_keys: config.debug_keys.clone(), contact_debug_interval: config.contact_debug_interval, contact_save_interval: config.contact_save_interval, @@ -74,6 +75,8 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { replay_transactions_threads: config.replay_transactions_threads, tvu_shred_sigverify_threads: config.tvu_shred_sigverify_threads, delay_leader_block_for_pending_fork: config.delay_leader_block_for_pending_fork, + voting_service_test_override: config.voting_service_test_override.clone(), + repair_handler_type: config.repair_handler_type.clone(), } } @@ -81,9 +84,7 @@ pub fn make_identical_validator_configs( config: &ValidatorConfig, num: usize, ) -> Vec { - let mut configs = vec![]; - for _ in 0..num { - configs.push(safe_clone_config(config)); - } - configs + std::iter::repeat_with(|| safe_clone_config(config)) + .take(num) + .collect() } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 3754c64bb4..93e4bdf69d 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -10,6 +10,10 @@ use { solana_accounts_db::{ hardened_unpack::open_genesis_config, utils::create_accounts_run_and_snapshot_dirs, }, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Signature as BLSSignature}, + solana_client::connection_cache::ConnectionCache, + solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, + solana_connection_cache::client_connection::ClientConnection, solana_core::{ consensus::{ tower_storage::FileTowerStorage, Tower, SWITCH_FORK_THRESHOLD, VOTE_THRESHOLD_DEPTH, @@ -17,10 +21,12 @@ use { optimistic_confirmation_verifier::OptimisticConfirmationVerifier, replay_stage::DUPLICATE_THRESHOLD, validator::{BlockVerificationMethod, ValidatorConfig}, + voting_service::{AlpenglowPortOverride, VotingServiceOverride}, }, solana_download_utils::download_snapshot_archive, solana_entry::entry::create_ticks, solana_gossip::{crds_data::MAX_VOTES, gossip_service::discover_cluster}, + solana_keypair::keypair_from_seed, solana_ledger::{ ancestor_iterator::AncestorIterator, bank_forks_utils, @@ -41,7 +47,7 @@ use { run_cluster_partition, run_kill_partition_switch_threshold, save_tower, setup_snapshot_validator_config, test_faulty_node, wait_for_duplicate_proof, wait_for_last_vote_in_tower_to_land_in_ledger, SnapshotValidatorConfig, - ValidatorTestConfig, DEFAULT_NODE_STAKE, RUST_LOG_FILTER, + ValidatorTestConfig, AG_DEBUG_LOG_FILTER, DEFAULT_NODE_STAKE, RUST_LOG_FILTER, }, local_cluster::{ClusterConfig, LocalCluster, DEFAULT_MINT_LAMPORTS}, validator_configs::*, @@ -83,8 +89,15 @@ use { broadcast_duplicates_run::{BroadcastDuplicatesConfig, ClusterPartition}, BroadcastStageType, }, - solana_vote::{vote_parser, vote_transaction}, + solana_vote::{ + vote_parser::{self}, + vote_transaction, + }, solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY, + solana_votor_messages::{ + bls_message::{BLSMessage, CertificateType, VoteMessage, BLS_KEYPAIR_DERIVE_SEED}, + vote::Vote, + }, std::{ collections::{BTreeSet, HashMap, HashSet}, fs, @@ -135,6 +148,87 @@ fn test_local_cluster_start_and_exit_with_config() { assert_eq!(cluster.validators.len(), NUM_NODES); } +fn test_alpenglow_nodes_basic(num_nodes: usize, num_offline_nodes: usize) { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + let validator_keys = (0..num_nodes) + .map(|i| (Arc::new(keypair_from_seed(&[i as u8; 32]).unwrap()), true)) + .collect::>(); + + let mut config = ClusterConfig { + validator_configs: make_identical_validator_configs( + &ValidatorConfig::default_for_test(), + num_nodes, + ), + validator_keys: Some(validator_keys.clone()), + node_stakes: vec![DEFAULT_NODE_STAKE; num_nodes], + ticks_per_slot: 8, + slots_per_epoch: MINIMUM_SLOTS_PER_EPOCH * 2, + stakers_slot_offset: MINIMUM_SLOTS_PER_EPOCH * 2, + poh_config: PohConfig { + target_tick_duration: PohConfig::default().target_tick_duration, + hashes_per_tick: Some(clock::DEFAULT_HASHES_PER_TICK), + target_tick_count: None, + }, + ..ClusterConfig::default() + }; + let mut cluster = LocalCluster::new_alpenglow(&mut config, SocketAddrSpace::Unspecified); + assert_eq!(cluster.validators.len(), num_nodes); + + // Check transactions land + cluster_tests::spend_and_verify_all_nodes( + &cluster.entry_point_info, + &cluster.funding_keypair, + num_nodes, + HashSet::new(), + SocketAddrSpace::Unspecified, + &cluster.connection_cache, + ); + + if num_offline_nodes > 0 { + // Bring nodes offline + info!("Shutting down {num_offline_nodes} nodes"); + for (key, _) in validator_keys.iter().take(num_offline_nodes) { + cluster.exit_node(&key.pubkey()); + } + } + + // Check for new roots + cluster.check_for_new_roots( + 16, + &format!("test_{}_nodes_alpenglow", num_nodes), + SocketAddrSpace::Unspecified, + ); +} + +#[test] +#[serial] +fn test_1_node_alpenglow() { + const NUM_NODES: usize = 1; + test_alpenglow_nodes_basic(NUM_NODES, 0); +} + +#[test] +#[serial] +fn test_2_nodes_alpenglow() { + const NUM_NODES: usize = 2; + test_alpenglow_nodes_basic(NUM_NODES, 0); +} + +#[test] +#[serial] +fn test_4_nodes_alpenglow() { + const NUM_NODES: usize = 4; + test_alpenglow_nodes_basic(NUM_NODES, 0); +} + +#[test] +#[serial] +fn test_4_nodes_with_1_offline_alpenglow() { + const NUM_NODES: usize = 4; + const NUM_OFFLINE: usize = 1; + test_alpenglow_nodes_basic(NUM_NODES, NUM_OFFLINE); +} + #[test] #[serial] fn test_spend_and_verify_all_nodes_1() { @@ -1295,6 +1389,7 @@ fn test_snapshot_restart_tower() { #[test] #[serial] +#[ignore] fn test_snapshots_blockstore_floor() { solana_logger::setup_with_default(RUST_LOG_FILTER); // First set up the cluster with 1 snapshotting leader @@ -1536,6 +1631,7 @@ fn test_fake_shreds_broadcast_leader() { #[test] #[serial] +#[ignore] fn test_wait_for_max_stake() { solana_logger::setup_with_default(RUST_LOG_FILTER); let validator_config = ValidatorConfig::default_for_test(); @@ -2621,10 +2717,16 @@ fn test_restart_tower_rollback() { #[test] #[serial] fn test_run_test_load_program_accounts_partition_root() { - run_test_load_program_accounts_partition(CommitmentConfig::finalized()); + run_test_load_program_accounts_partition(CommitmentConfig::finalized(), false); +} + +#[test] +#[serial] +fn test_alpenglow_run_test_load_program_accounts_partition_root() { + run_test_load_program_accounts_partition(CommitmentConfig::finalized(), true); } -fn run_test_load_program_accounts_partition(scan_commitment: CommitmentConfig) { +fn run_test_load_program_accounts_partition(scan_commitment: CommitmentConfig, is_alpenglow: bool) { let num_slots_per_validator = 8; let partitions: [usize; 2] = [1, 1]; let (leader_schedule, validator_keys) = create_custom_leader_schedule_with_random_keys(&[ @@ -2659,7 +2761,7 @@ fn run_test_load_program_accounts_partition(scan_commitment: CommitmentConfig) { let on_partition_resolved = |cluster: &mut LocalCluster, _: &mut ()| { cluster.check_for_new_roots( - 20, + 16, "run_test_load_program_accounts_partition", SocketAddrSpace::Unspecified, ); @@ -2677,6 +2779,7 @@ fn run_test_load_program_accounts_partition(scan_commitment: CommitmentConfig) { on_partition_resolved, None, additional_accounts, + is_alpenglow, ); } @@ -2815,10 +2918,12 @@ fn test_oc_bad_signatures() { |(_label, leader_vote_tx)| { let vote = vote_parser::parse_vote_transaction(&leader_vote_tx) .map(|(_, vote, ..)| vote) + .unwrap() + .as_tower_transaction() .unwrap(); // Filter out empty votes if !vote.is_empty() { - Some((vote, leader_vote_tx)) + Some((vote.into(), leader_vote_tx)) } else { None } @@ -2829,6 +2934,7 @@ fn test_oc_bad_signatures() { let num_votes_simulated = num_votes_simulated.clone(); move |vote_slot, leader_vote_tx, parsed_vote, _cluster_info| { info!("received vote for {}", vote_slot); + let parsed_vote = parsed_vote.as_tower_transaction_ref().unwrap(); let vote_hash = parsed_vote.hash(); info!( "Simulating vote from our node on slot {}, hash {}", @@ -3282,7 +3388,7 @@ fn do_test_lockout_violation_with_or_without_tower(with_tower: bool) { let validator_to_slots = vec![ ( validator_b_pubkey, - validator_b_last_leader_slot as usize + 1, + (validator_b_last_leader_slot + NUM_CONSECUTIVE_LEADER_SLOTS) as usize, ), (validator_c_pubkey, DEFAULT_SLOTS_PER_EPOCH as usize), ]; @@ -3819,11 +3925,14 @@ fn test_kill_heaviest_partition() { on_partition_resolved, None, vec![], + // TODO: make Alpenglow equivalent when skips are available + false, ) } #[test] #[serial] +#[ignore] fn test_kill_partition_switch_threshold_no_progress() { let max_switch_threshold_failure_pct = 1.0 - 2.0 * SWITCH_FORK_THRESHOLD; let total_stake = 10_000 * DEFAULT_NODE_STAKE; @@ -3858,6 +3967,7 @@ fn test_kill_partition_switch_threshold_no_progress() { #[test] #[serial] +#[ignore] fn test_kill_partition_switch_threshold_progress() { let max_switch_threshold_failure_pct = 1.0 - 2.0 * SWITCH_FORK_THRESHOLD; let total_stake = 10_000 * DEFAULT_NODE_STAKE; @@ -4010,10 +4120,12 @@ fn run_duplicate_shreds_broadcast_leader(vote_on_duplicate: bool) { if label.pubkey() == bad_leader_id { let vote = vote_parser::parse_vote_transaction(&leader_vote_tx) .map(|(_, vote, ..)| vote) + .unwrap() + .as_tower_transaction() .unwrap(); // Filter out empty votes if !vote.is_empty() { - Some((vote, leader_vote_tx)) + Some((vote.into(), leader_vote_tx)) } else { None } @@ -4039,6 +4151,7 @@ fn run_duplicate_shreds_broadcast_leader(vote_on_duplicate: bool) { for slot in duplicate_slot_receiver.try_iter() { duplicate_slots.push(slot); } + let parsed_vote = parsed_vote.as_tower_transaction_ref().unwrap(); let vote_hash = parsed_vote.hash(); if vote_on_duplicate || !duplicate_slots.contains(&latest_vote_slot) { info!( @@ -4401,31 +4514,35 @@ fn find_latest_replayed_slot_from_ledger( #[test] #[serial] fn test_cluster_partition_1_1() { - let empty = |_: &mut LocalCluster, _: &mut ()| {}; - let on_partition_resolved = |cluster: &mut LocalCluster, _: &mut ()| { - cluster.check_for_new_roots(16, "PARTITION_TEST", SocketAddrSpace::Unspecified); - }; - run_cluster_partition( - &[1, 1], - None, - (), - empty, - empty, - on_partition_resolved, - None, - vec![], - ) + run_test_cluster_partition(2, false); +} + +#[test] +#[serial] +fn test_alpenglow_cluster_partition_1_1() { + run_test_cluster_partition(2, true); } #[test] #[serial] fn test_cluster_partition_1_1_1() { + run_test_cluster_partition(3, false); +} + +#[test] +#[serial] +fn test_alpenglow_cluster_partition_1_1_1() { + run_test_cluster_partition(3, true); +} + +fn run_test_cluster_partition(num_partitions: usize, is_alpenglow: bool) { let empty = |_: &mut LocalCluster, _: &mut ()| {}; let on_partition_resolved = |cluster: &mut LocalCluster, _: &mut ()| { cluster.check_for_new_roots(16, "PARTITION_TEST", SocketAddrSpace::Unspecified); }; + let partition_sizes = vec![1; num_partitions]; run_cluster_partition( - &[1, 1, 1], + &partition_sizes, None, (), empty, @@ -4433,6 +4550,7 @@ fn test_cluster_partition_1_1_1() { on_partition_resolved, None, vec![], + is_alpenglow, ) } @@ -4738,7 +4856,7 @@ fn test_duplicate_with_pruned_ancestor() { let observer_stake = DEFAULT_NODE_STAKE; let slots_per_epoch = 2048; - let fork_slot: u64 = 10; + let fork_slot: u64 = 12; let fork_length: u64 = 20; let majority_fork_buffer = 5; @@ -5530,8 +5648,8 @@ fn test_duplicate_shreds_switch_failure() { ); let validator_to_slots = vec![ - (duplicate_leader_validator_pubkey, 50), - (target_switch_fork_validator_pubkey, 5), + (duplicate_leader_validator_pubkey, 52), + (target_switch_fork_validator_pubkey, 8), // The ideal sequence of events for the `duplicate_fork_validator1_pubkey` validator would go: // 1. Vote for duplicate block `D` // 2. See `D` is duplicate, remove from fork choice and reset to ancestor `A`, potentially generating a fork off that ancestor @@ -5861,7 +5979,7 @@ fn test_invalid_forks_persisted_on_restart() { let (target_pubkey, majority_pubkey) = (validators[0], validators[1]); // Need majority validator to make the dup_slot let validator_to_slots = vec![ - (majority_pubkey, dup_slot as usize + 5), + (majority_pubkey, dup_slot as usize + 6), (target_pubkey, DEFAULT_SLOTS_PER_EPOCH as usize), ]; let leader_schedule = create_custom_leader_schedule(validator_to_slots.into_iter()); @@ -5989,3 +6107,1239 @@ fn test_invalid_forks_persisted_on_restart() { sleep(Duration::from_millis(100)); } } + +#[test] +#[serial] +fn test_restart_node_alpenglow() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + let slots_per_epoch = MINIMUM_SLOTS_PER_EPOCH * 2; + let ticks_per_slot = 16; + let validator_config = ValidatorConfig::default_for_test(); + let mut cluster = LocalCluster::new_alpenglow( + &mut ClusterConfig { + node_stakes: vec![DEFAULT_NODE_STAKE], + validator_configs: vec![safe_clone_config(&validator_config)], + ticks_per_slot, + slots_per_epoch, + stakers_slot_offset: slots_per_epoch, + skip_warmup_slots: true, + ..ClusterConfig::default() + }, + SocketAddrSpace::Unspecified, + ); + let nodes = cluster.get_node_pubkeys(); + cluster_tests::sleep_n_epochs( + 1.0, + &cluster.genesis_config.poh_config, + clock::DEFAULT_TICKS_PER_SLOT, + slots_per_epoch, + ); + info!("Restarting node"); + cluster.exit_restart_node(&nodes[0], validator_config, SocketAddrSpace::Unspecified); + cluster_tests::sleep_n_epochs( + 0.5, + &cluster.genesis_config.poh_config, + clock::DEFAULT_TICKS_PER_SLOT, + slots_per_epoch, + ); + cluster_tests::send_many_transactions( + &cluster.entry_point_info, + &cluster.funding_keypair, + &cluster.connection_cache, + 10, + 1, + ); +} + +/// We start 2 nodes, where the first node A holds 90% of the stake +/// +/// We let A run by itself, and ensure that B can join and rejoin the network +/// through fast forwarding their slot on receiving A's finalization certificate +#[test] +#[serial] +fn test_alpenglow_imbalanced_stakes_catchup() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + // Create node stakes + let slots_per_epoch = 512; + + let total_stake = 2 * DEFAULT_NODE_STAKE; + let tenth_stake = total_stake / 10; + let node_a_stake = 9 * tenth_stake; + let node_b_stake = total_stake - node_a_stake; + + let node_stakes = vec![node_a_stake, node_b_stake]; + let num_nodes = node_stakes.len(); + + // Create leader schedule with A and B as leader 72/28 + let (leader_schedule, validator_keys) = + create_custom_leader_schedule_with_random_keys(&[72, 28]); + + let leader_schedule = FixedSchedule { + leader_schedule: Arc::new(leader_schedule), + }; + + // Create our UDP socket to listen to votes + let vote_listener_addr = solana_net_utils::bind_to_localhost().unwrap(); + + let mut validator_config = ValidatorConfig::default_for_test(); + validator_config.fixed_leader_schedule = Some(leader_schedule); + validator_config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![vote_listener_addr.local_addr().unwrap()], + alpenglow_port_override: AlpenglowPortOverride::default(), + }); + + // Collect node pubkeys + let node_pubkeys = validator_keys + .iter() + .map(|key| key.pubkey()) + .collect::>(); + + // Cluster config + let mut cluster_config = ClusterConfig { + mint_lamports: total_stake, + node_stakes, + validator_configs: make_identical_validator_configs(&validator_config, num_nodes), + validator_keys: Some( + validator_keys + .iter() + .cloned() + .zip(iter::repeat_with(|| true)) + .collect(), + ), + slots_per_epoch, + stakers_slot_offset: slots_per_epoch, + ticks_per_slot: DEFAULT_TICKS_PER_SLOT, + skip_warmup_slots: true, + ..ClusterConfig::default() + }; + + // Create local cluster + let mut cluster = + LocalCluster::new_alpenglow(&mut cluster_config, SocketAddrSpace::Unspecified); + + // Ensure all nodes are voting + cluster.check_for_new_processed( + 8, + "test_alpenglow_imbalanced_stakes_catchup", + SocketAddrSpace::Unspecified, + ); + + info!("exiting node B"); + let b_info = cluster.exit_node(&node_pubkeys[1]); + + // Let A make roots by itself + cluster.check_for_new_roots( + 8, + "test_alpenglow_imbalanced_stakes_catchup", + SocketAddrSpace::Unspecified, + ); + + info!("restarting node B"); + cluster.restart_node(&node_pubkeys[1], b_info, SocketAddrSpace::Unspecified); + + // Ensure all nodes are voting + cluster.check_for_new_notarized_votes( + 16, + "test_alpenglow_imbalanced_stakes_catchup", + SocketAddrSpace::Unspecified, + vote_listener_addr, + ); +} + +fn broadcast_vote( + bls_message: BLSMessage, + tpu_socket_addrs: &[std::net::SocketAddr], + additional_listeners: Option<&Vec>, + connection_cache: Arc, +) { + for tpu_socket_addr in tpu_socket_addrs + .iter() + .chain(additional_listeners.unwrap_or(&vec![]).iter()) + { + let buf = bincode::serialize(&bls_message).unwrap(); + let client = connection_cache.get_connection(tpu_socket_addr); + client.send_data_async(buf).unwrap_or_else(|_| { + panic!("Failed to broadcast vote to {}", tpu_socket_addr); + }); + } +} + +fn _vote_to_tuple(vote: &Vote) -> (u64, u8) { + let discriminant = if vote.is_notarization() { + 0 + } else if vote.is_finalize() { + 1 + } else if vote.is_skip() { + 2 + } else if vote.is_notarize_fallback() { + 3 + } else if vote.is_skip_fallback() { + 4 + } else { + panic!("Invalid vote type: {:?}", vote) + }; + + let slot = vote.slot(); + + (slot, discriminant) +} + +/// This test validates the Alpenglow consensus protocol's ability to maintain liveness when a node +/// needs to issue a NotarizeFallback vote. The test sets up a two-node cluster with a specific +/// stake distribution to create a scenario where: +/// +/// - Node A has 60% of stake minus a small amount (epsilon) +/// - Node B has 40% of stake plus a small amount (epsilon) +/// +/// The test simulates the following sequence: +/// 1. Node B (as leader) proposes a block for slot 32 +/// 2. Node A is unable to receive the block (simulated via turbine disconnection) +/// 3. Node A sends Skip votes to both nodes for slot 32 +/// 4. Node B sends Notarize votes to both nodes for slot 32 +/// 5. Node A receives both votes and its certificate pool determines: +/// - Skip has (60% - epsilon) votes +/// - Notarize has (40% + epsilon) votes +/// - Protocol determines it's "SafeToNotar" and issues a NotarizeFallback vote +/// 6. Node B doesn't issue NotarizeFallback because it already submitted a Notarize +/// 7. Node B receives Node A's NotarizeFallback vote +/// 8. Network progresses and maintains liveness after this fallback scenario +#[test] +#[serial] +fn test_alpenglow_ensure_liveness_after_single_notar_fallback() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + // Configure total stake and stake distribution + let total_stake = 2 * DEFAULT_NODE_STAKE; + let slots_per_epoch = MINIMUM_SLOTS_PER_EPOCH; + + let node_a_stake = total_stake * 6 / 10 - 1; + let node_b_stake = total_stake * 4 / 10 + 1; + + let node_stakes = vec![node_a_stake, node_b_stake]; + let num_nodes = node_stakes.len(); + + assert_eq!(total_stake, node_a_stake + node_b_stake); + + // Control components + let node_a_turbine_disabled = Arc::new(AtomicBool::new(false)); + + // Create leader schedule + let (leader_schedule, validator_keys) = create_custom_leader_schedule_with_random_keys(&[0, 4]); + + let leader_schedule = FixedSchedule { + leader_schedule: Arc::new(leader_schedule), + }; + + // Create our UDP socket to listen to votes + let vote_listener = solana_net_utils::bind_to_localhost().unwrap(); + + // Create validator configs + let mut validator_config = ValidatorConfig::default_for_test(); + validator_config.fixed_leader_schedule = Some(leader_schedule); + validator_config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![vote_listener.local_addr().unwrap()], + alpenglow_port_override: AlpenglowPortOverride::default(), + }); + + let mut validator_configs = make_identical_validator_configs(&validator_config, num_nodes); + validator_configs[0].turbine_disabled = node_a_turbine_disabled.clone(); + + assert_eq!(num_nodes, validator_keys.len()); + + // Cluster config + let mut cluster_config = ClusterConfig { + mint_lamports: total_stake, + node_stakes, + validator_configs, + validator_keys: Some( + validator_keys + .iter() + .cloned() + .zip(iter::repeat_with(|| true)) + .collect(), + ), + slots_per_epoch, + stakers_slot_offset: slots_per_epoch, + ticks_per_slot: DEFAULT_TICKS_PER_SLOT, + ..ClusterConfig::default() + }; + + // Create local cluster + let cluster = LocalCluster::new_alpenglow(&mut cluster_config, SocketAddrSpace::Unspecified); + + assert_eq!(cluster.validators.len(), num_nodes); + + // Track Node A's votes and when the test can conclude + let mut post_experiment_votes = HashMap::new(); + let mut post_experiment_roots = HashSet::new(); + + // Start vote listener thread to monitor and control the experiment + let vote_listener = std::thread::spawn({ + let mut buf = [0_u8; 65_535]; + let mut check_for_roots = false; + let mut slots_with_skip = HashSet::new(); + + move || loop { + let n_bytes = vote_listener.recv(&mut buf).unwrap(); + let bls_message = bincode::deserialize::(&buf[0..n_bytes]).unwrap(); + let BLSMessage::Vote(vote_message) = bls_message else { + continue; + }; + let vote = vote_message.vote; + + // Since A has 60% of the stake, it will be node 0, and B will be node 1 + let node_index = vote_message.rank; + + // Once we've received a vote from node B at slot 31, we can start the experiment. + if vote.slot() == 31 && node_index == 1 { + node_a_turbine_disabled.store(true, Ordering::Relaxed); + } + + if vote.slot() >= 32 && node_index == 0 { + if vote.is_skip() { + slots_with_skip.insert(vote.slot()); + } + + if !check_for_roots && vote.slot() == 32 && vote.is_notarize_fallback() { + check_for_roots = true; + assert!(slots_with_skip.contains(&32)); // skip on slot 32 + } + } + + // We should see a skip followed by a notar fallback. Once we do, the experiment is + // complete. + if check_for_roots { + node_a_turbine_disabled.store(false, Ordering::Relaxed); + + if vote.is_finalize() { + let value = post_experiment_votes.entry(vote.slot()).or_insert(vec![]); + + value.push(node_index); + + if value.len() == 2 { + post_experiment_roots.insert(vote.slot()); + + if post_experiment_roots.len() >= 10 { + break; + } + } + } + } + } + }); + + vote_listener.join().unwrap(); +} + +/// Test to validate the Alpenglow consensus protocol's ability to maintain liveness when a node +/// needs to issue multiple NotarizeFallback votes due to Byzantine behavior and network partitioning. +/// +/// This test simulates a complex Byzantine scenario with four nodes having the following stake distribution: +/// - Node A (Leader): 20% - ε (small epsilon) +/// - Node B: 40% +/// - Node C: 20% +/// - Node D: 20% + ε +/// +/// The test validates the protocol's behavior through the following phases: +/// +/// ## Phase 1: Initial Network Partition +/// - Node C's turbine is disabled at slot 50, causing it to miss blocks and vote Skip +/// - Node A (leader) proposes blocks normally +/// - Node B initially copies Node A's votes +/// - Node D copies Node A's votes +/// - Node C accumulates 10 NotarizeFallback votes while in this steady state +/// +/// ## Phase 2: Byzantine Equivocation +/// After Node C has issued sufficient NotarizeFallback votes, Node A begins equivocating: +/// - Node A votes for block b1 (original block) +/// - Node B votes for block b2 (equivocated block with different block_id and bank_hash) +/// - Node C continues voting Skip but observes conflicting votes +/// - Node D votes for block b1 (same as Node A) +/// +/// This creates a voting distribution where: +/// - b1 has 40% stake (A: 20%-ε + D: 20%+ε) +/// - b2 has 40% stake (B: 40%) +/// - Skip has 20% stake (C: 20%) +/// +/// ## Phase 3: Double NotarizeFallback +/// Node C, observing the conflicting votes, triggers SafeToNotar for both blocks: +/// - Issues NotarizeFallback for b1 (A's block) +/// - Issues NotarizeFallback for b2 (B's equivocated block) +/// - Verifies the block IDs are different due to equivocation +/// - Continues this pattern until 3 slots have double NotarizeFallback votes +/// +/// ## Phase 4: Recovery and Liveness +/// After confirming the double NotarizeFallback behavior: +/// - Node A stops equivocating +/// - Node C's turbine is re-enabled +/// - Network returns to normal operation +/// - Test verifies 10+ new roots are created, ensuring liveness is maintained +/// +/// ## Key Validation Points +/// - SafeToNotar triggers correctly when conflicting blocks have sufficient stake +/// - NotarizeFallback votes are issued for both equivocated blocks +/// - Network maintains liveness despite Byzantine behavior and temporary partitions +/// - Protocol correctly handles the edge case where multiple blocks have equal stake +/// - Recovery is possible once Byzantine behavior stops +/// +/// NOTE: we could get away with just three nodes in this test, assigning A a total of 40% stake, +/// since node D *always* copy votes node A. But, doing so technically makes all nodes have >= 20% +/// stake, meaning that none of them is allowed to be Byzantine. We opt to be a bit more explicit in +/// this test. +#[test] +#[serial] +#[ignore] +fn test_alpenglow_ensure_liveness_after_double_notar_fallback() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + + // Configure total stake and stake distribution + const TOTAL_STAKE: u64 = 10 * DEFAULT_NODE_STAKE; + const SLOTS_PER_EPOCH: u64 = MINIMUM_SLOTS_PER_EPOCH; + + // Node stakes with slight imbalance to trigger fallback behavior + let node_stakes = [ + TOTAL_STAKE * 2 / 10 - 1, // Node A (Leader): 20% - ε + TOTAL_STAKE * 4 / 10, // Node B: 40% + TOTAL_STAKE * 2 / 10, // Node C: 20% + TOTAL_STAKE * 2 / 10 + 1, // Node D: 20% + ε + ]; + + assert_eq!(TOTAL_STAKE, node_stakes.iter().sum::()); + + // Control components + let node_c_turbine_disabled = Arc::new(AtomicBool::new(false)); + + // Create leader schedule with Node A as primary leader + let (leader_schedule, validator_keys) = + create_custom_leader_schedule_with_random_keys(&[4, 0, 0, 0]); + + let leader_schedule = FixedSchedule { + leader_schedule: Arc::new(leader_schedule), + }; + + // Create UDP socket to listen to votes + let vote_listener_socket = solana_net_utils::bind_to_localhost().unwrap(); + + // Create validator configs + let mut validator_config = ValidatorConfig::default_for_test(); + validator_config.fixed_leader_schedule = Some(leader_schedule); + validator_config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![vote_listener_socket.local_addr().unwrap()], + alpenglow_port_override: AlpenglowPortOverride::default(), + }); + + let mut validator_configs = + make_identical_validator_configs(&validator_config, node_stakes.len()); + validator_configs[2].turbine_disabled = node_c_turbine_disabled.clone(); + + // Cluster config + let mut cluster_config = ClusterConfig { + mint_lamports: TOTAL_STAKE, + node_stakes: node_stakes.to_vec(), + validator_configs, + validator_keys: Some( + validator_keys + .iter() + .cloned() + .zip(std::iter::repeat(true)) + .collect(), + ), + slots_per_epoch: SLOTS_PER_EPOCH, + stakers_slot_offset: SLOTS_PER_EPOCH, + ticks_per_slot: DEFAULT_TICKS_PER_SLOT, + ..ClusterConfig::default() + }; + + // Create local cluster + let mut cluster = + LocalCluster::new_alpenglow(&mut cluster_config, SocketAddrSpace::Unspecified); + + // Create mapping from vote pubkeys to node indices + let vote_pubkeys: HashMap<_, _> = validator_keys + .iter() + .enumerate() + .filter_map(|(index, keypair)| { + cluster + .validators + .get(&keypair.pubkey()) + .map(|validator| (validator.info.voting_keypair.pubkey(), index)) + }) + .collect(); + + assert_eq!(vote_pubkeys.len(), node_stakes.len()); + + // Collect node pubkeys and TPU addresses + let node_pubkeys: Vec<_> = validator_keys.iter().map(|key| key.pubkey()).collect(); + + let tpu_socket_addrs: Vec<_> = node_pubkeys + .iter() + .map(|pubkey| { + cluster + .get_contact_info(pubkey) + .unwrap() + .tpu_vote(cluster.connection_cache.protocol()) + .unwrap_or_else(|| panic!("Failed to get TPU address for {}", pubkey)) + }) + .collect(); + + // Exit nodes B and D to control their voting behavior + let node_b_info = cluster.exit_node(&validator_keys[1].pubkey()); + let node_b_vote_keypair = node_b_info.info.voting_keypair.clone(); + + let node_d_info = cluster.exit_node(&validator_keys[3].pubkey()); + let node_d_vote_keypair = node_d_info.info.voting_keypair.clone(); + + // Vote listener state + #[derive(Debug)] + struct VoteListenerState { + num_notar_fallback_votes: u32, + a_equivocates: bool, + notar_fallback_map: HashMap>, + double_notar_fallback_slots: Vec, + check_for_roots: bool, + post_experiment_votes: HashMap>, + post_experiment_roots: HashSet, + } + + impl VoteListenerState { + fn new() -> Self { + Self { + num_notar_fallback_votes: 0, + a_equivocates: false, + notar_fallback_map: HashMap::new(), + double_notar_fallback_slots: Vec::new(), + check_for_roots: false, + post_experiment_votes: HashMap::new(), + post_experiment_roots: HashSet::new(), + } + } + + fn sign_and_construct_vote_message( + &self, + vote: Vote, + keypair: &Keypair, + rank: u16, + ) -> BLSMessage { + let bls_keypair = + BLSKeypair::derive_from_signer(keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); + let signature: BLSSignature = bls_keypair + .sign(bincode::serialize(&vote).unwrap().as_slice()) + .into(); + BLSMessage::new_vote(vote, signature, rank) + } + + fn handle_node_a_vote( + &self, + vote_message: &VoteMessage, + node_b_keypair: &Keypair, + node_d_keypair: &Keypair, + tpu_socket_addrs: &[std::net::SocketAddr], + connection_cache: Arc, + ) { + // Create vote for Node B (potentially equivocated) + let vote = &vote_message.vote; + let vote_b = if self.a_equivocates && vote.is_notarization() { + let new_block_id = Hash::new_unique(); + Vote::new_notarization_vote(vote.slot(), new_block_id) + } else { + *vote + }; + + broadcast_vote( + self.sign_and_construct_vote_message( + vote_b, + node_b_keypair, + 1, // Node B's rank is 1 + ), + tpu_socket_addrs, + None, + connection_cache.clone(), + ); + + // Create vote for Node D (always copies Node A) + broadcast_vote( + self.sign_and_construct_vote_message( + *vote, + node_d_keypair, + 3, // Node D's rank is 3 + ), + tpu_socket_addrs, + None, + connection_cache, + ); + } + + fn handle_node_c_vote( + &mut self, + vote: &Vote, + node_c_turbine_disabled: &Arc, + ) -> bool { + let turbine_disabled = node_c_turbine_disabled.load(Ordering::Acquire); + + // Count NotarizeFallback votes while turbine is disabled + if turbine_disabled && vote.is_notarize_fallback() { + self.num_notar_fallback_votes += 1; + } + + // Handle double NotarizeFallback during equivocation + if self.a_equivocates && vote.is_notarize_fallback() { + let block_id = vote.block_id().copied().unwrap(); + + let entry = self.notar_fallback_map.entry(vote.slot()).or_default(); + entry.push(block_id); + + assert!( + entry.len() <= 2, + "More than 2 NotarizeFallback votes for slot {}", + vote.slot() + ); + + if entry.len() == 2 { + // Verify equivocation: different block IDs + assert_ne!( + entry[0], entry[1], + "Block IDs should differ due to equivocation" + ); + + self.double_notar_fallback_slots.push(vote.slot()); + + // End experiment after 3 double NotarizeFallback slots + if self.double_notar_fallback_slots.len() == 3 { + info!("Phase 4, checking for 10 roots"); + self.a_equivocates = false; + node_c_turbine_disabled.store(false, Ordering::Release); + self.check_for_roots = true; + } + } + } + + // Start equivocation after stable NotarizeFallback behavior + if turbine_disabled && self.num_notar_fallback_votes == 10 { + info!("Phase 2, checking for 3 double notarize fallback votes from C"); + self.a_equivocates = true; + } + + // Disable turbine at slot 50 to start the experiment + if vote.slot() == 50 { + info!("Phase 1, checking for 10 notarize fallback votes from C"); + node_c_turbine_disabled.store(true, Ordering::Release); + } + + false + } + + fn handle_finalize_vote(&mut self, vote_message: &VoteMessage) -> bool { + if !self.check_for_roots { + return false; + } + + let slot = vote_message.vote.slot(); + let slot_votes = self.post_experiment_votes.entry(slot).or_default(); + slot_votes.push(vote_message.rank); + + // We expect votes from 2 nodes (A and C) since B and D are copy-voting + if slot_votes.len() == 2 { + self.post_experiment_roots.insert(slot); + + // End test after 10 new roots + if self.post_experiment_roots.len() >= 10 { + return true; + } + } + + false + } + } + + // Start vote listener thread to monitor and control the experiment + let vote_listener_thread = std::thread::spawn({ + let mut buf = [0u8; 65_535]; + let mut state = VoteListenerState::new(); + + move || { + loop { + let n_bytes = vote_listener_socket.recv(&mut buf).unwrap(); + let BLSMessage::Vote(vote_message) = + bincode::deserialize::(&buf[0..n_bytes]).unwrap() + else { + continue; + }; + + match vote_message.rank { + 0 => { + // Node A: Handle vote broadcasting to B and D + state.handle_node_a_vote( + &vote_message, + &node_b_vote_keypair, + &node_d_vote_keypair, + &tpu_socket_addrs, + cluster.connection_cache.clone(), + ); + } + 2 => { + // Node C: Handle experiment state transitions + state.handle_node_c_vote(&vote_message.vote, &node_c_turbine_disabled); + } + _ => {} + } + + // Check for finalization votes to determine test completion + if vote_message.vote.is_finalize() && state.handle_finalize_vote(&vote_message) { + break; + } + } + } + }); + + vote_listener_thread.join().unwrap(); +} + +/// Test to validate Alpenglow's ability to maintain liveness when nodes issue both NotarizeFallback +/// and SkipFallback votes in an intertwined manner. +/// +/// This test simulates a consensus scenario with four nodes having specific stake distributions: +/// - Node A: 40% + epsilon stake +/// - Node B: 40% - epsilon stake +/// - Node C: 20% - epsilon stake +/// - Node D: epsilon stake (minimal, acts as perpetual leader) +/// +/// The test proceeds through two main stages: +/// +/// ## Stage 1: Stable Network Operation +/// All nodes are voting normally for leader D's proposals, with notarization votes going through +/// successfully and the network maintaining consensus. +/// +/// ## Stage 2: Network Partition and Fallback Scenario +/// At slot 50, Node A's turbine is disabled, creating a network partition. This triggers the +/// following sequence: +/// 1. Node D (leader) proposes a block b1 +/// 2. Nodes B, C, and D can communicate and vote to notarize b1 +/// 3. Node A is partitioned and cannot receive b1, so it issues a skip vote +/// 4. The vote distribution creates a complex fallback scenario: +/// - Nodes B, C, D: Issue notarize votes initially, then skip fallback votes +/// - Node A: Issues skip vote initially, then notarize fallback vote +/// 5. This creates the specific vote pattern: +/// - B, C, D: notarize + skip_fallback +/// - A: skip + notarize_fallback +/// +/// The test validates that: +/// - The network can handle intertwined fallback scenarios +/// - Consensus is maintained despite complex vote patterns +/// - The network continues to make progress and create new roots after the partition is resolved +/// - At least 10 new roots are created post-experiment to ensure sustained liveness +#[test] +#[serial] +fn test_alpenglow_ensure_liveness_after_intertwined_notar_and_skip_fallbacks() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + + // Configure stake distribution for the four-node cluster + const TOTAL_STAKE: u64 = 10 * DEFAULT_NODE_STAKE; + const EPSILON: u64 = 1; + const NUM_NODES: usize = 4; + + // Ensure that node stakes are in decreasing order, so node_index can directly be set as + // vote_message.rank. + let node_stakes = [ + TOTAL_STAKE * 4 / 10 + EPSILON, // Node A: 40% + epsilon + TOTAL_STAKE * 4 / 10 - EPSILON, // Node B: 40% - epsilon + TOTAL_STAKE * 2 / 10 - EPSILON, // Node C: 20% - epsilon + EPSILON, // Node D: epsilon + ]; + + assert_eq!(NUM_NODES, node_stakes.len()); + + // Verify stake distribution adds up correctly + assert_eq!(TOTAL_STAKE, node_stakes.iter().sum::()); + + // Control mechanism for network partition + let node_a_turbine_disabled = Arc::new(AtomicBool::new(false)); + + // Create leader schedule with A as perpetual leader + let (leader_schedule, validator_keys) = + create_custom_leader_schedule_with_random_keys(&[0, 0, 0, 4]); + + let leader_schedule = FixedSchedule { + leader_schedule: Arc::new(leader_schedule), + }; + + // Set up vote monitoring + let vote_listener_socket = + solana_net_utils::bind_to_localhost().expect("Failed to bind vote listener socket"); + + // Configure validators + let mut validator_config = ValidatorConfig::default_for_test(); + validator_config.fixed_leader_schedule = Some(leader_schedule); + validator_config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![vote_listener_socket.local_addr().unwrap()], + alpenglow_port_override: AlpenglowPortOverride::default(), + }); + + let mut validator_configs = make_identical_validator_configs(&validator_config, NUM_NODES); + // Node A (index 0) will have its turbine disabled during the experiment + validator_configs[0].turbine_disabled = node_a_turbine_disabled.clone(); + + assert_eq!(NUM_NODES, validator_keys.len()); + + // Set up cluster configuration + let mut cluster_config = ClusterConfig { + mint_lamports: TOTAL_STAKE, + node_stakes: node_stakes.to_vec(), + validator_configs, + validator_keys: Some( + validator_keys + .iter() + .cloned() + .zip(std::iter::repeat(true)) + .collect(), + ), + ..ClusterConfig::default() + }; + + // Initialize the cluster + let cluster = LocalCluster::new_alpenglow(&mut cluster_config, SocketAddrSpace::Unspecified); + assert_eq!(NUM_NODES, cluster.validators.len()); + + /// Helper struct to manage experiment state and vote pattern tracking + #[derive(Debug, PartialEq, Eq)] + enum Stage { + Stability, + ObserveSkipFallbacks, + ObserveLiveness, + } + + impl Stage { + fn timeout(&self) -> Duration { + match self { + Stage::Stability => Duration::from_secs(60), + Stage::ObserveSkipFallbacks => Duration::from_secs(120), + Stage::ObserveLiveness => Duration::from_secs(180), + } + } + + fn all() -> Vec { + vec![ + Stage::Stability, + Stage::ObserveSkipFallbacks, + Stage::ObserveLiveness, + ] + } + } + + #[derive(Debug)] + struct ExperimentState { + stage: Stage, + vote_type_bitmap: HashMap, // slot -> [node_vote_pattern; 4] + consecutive_pattern_matches: usize, + post_experiment_roots: HashSet, + } + + impl ExperimentState { + fn new() -> Self { + Self { + stage: Stage::Stability, + vote_type_bitmap: HashMap::new(), + consecutive_pattern_matches: 0, + post_experiment_roots: HashSet::new(), + } + } + + fn record_vote_bitmap(&mut self, slot: u64, node_index: usize, vote: &Vote) { + let (_, vote_type) = _vote_to_tuple(vote); + let slot_pattern = self.vote_type_bitmap.entry(slot).or_insert([0u8; 4]); + + assert!(node_index < NUM_NODES, "Invalid node index: {}", node_index); + slot_pattern[node_index] |= 1 << vote_type; + } + + fn matches_expected_pattern(&mut self) -> bool { + // Expected patterns: + // Nodes 1, 2, 3: notarize + skip_fallback = (1 << 0) | (1 << 4) = 17 + // Node 0: skip + notarize_fallback = (1 << 2) | (1 << 3) = 12 + const EXPECTED_PATTERN_MAJORITY: u8 = 17; // notarize + skip_fallback + const EXPECTED_PATTERN_MINORITY: u8 = 12; // skip + notarize_fallback + + for pattern in self.vote_type_bitmap.values() { + if pattern[0] == EXPECTED_PATTERN_MINORITY + && pattern[1] == EXPECTED_PATTERN_MAJORITY + && pattern[2] == EXPECTED_PATTERN_MAJORITY + && pattern[3] == EXPECTED_PATTERN_MAJORITY + { + self.consecutive_pattern_matches += 1; + if self.consecutive_pattern_matches >= 3 { + return true; + } + } + } + false + } + + fn record_certificate(&mut self, slot: u64) { + self.post_experiment_roots.insert(slot); + } + + fn sufficient_roots_created(&self) -> bool { + self.post_experiment_roots.len() >= 8 + } + } + + // Start vote monitoring thread + let vote_listener_thread = std::thread::spawn({ + let node_c_turbine_disabled = node_a_turbine_disabled.clone(); + + move || { + let mut buffer = [0u8; 65_535]; + let mut experiment_state = ExperimentState::new(); + + let timer = std::time::Instant::now(); + + loop { + let bytes_received = vote_listener_socket + .recv(&mut buffer) + .expect("Failed to receive vote data"); + + let bls_message = bincode::deserialize::(&buffer[..bytes_received]) + .expect("Failed to deserialize BLS message"); + + match bls_message { + BLSMessage::Vote(vote_message) => { + let vote = &vote_message.vote; + let node_index = vote_message.rank as usize; + + // Stage timeouts + let elapsed_time = timer.elapsed(); + + for stage in Stage::all() { + if elapsed_time > stage.timeout() { + panic!( + "Timeout during {:?}. node_c_turbine_disabled: {:#?}. Latest vote: {:#?}. Experiment state: {:#?}", + stage, + node_c_turbine_disabled.load(Ordering::Acquire), + vote, + experiment_state + ); + } + } + + // Stage 1: Wait for stability, then introduce partition at slot 20 + if vote.slot() == 20 && !node_c_turbine_disabled.load(Ordering::Acquire) { + node_c_turbine_disabled.store(true, Ordering::Release); + experiment_state.stage = Stage::ObserveSkipFallbacks; + } + + // Stage 2: Monitor for expected fallback vote patterns + if experiment_state.stage == Stage::ObserveSkipFallbacks { + experiment_state.record_vote_bitmap(vote.slot(), node_index, vote); + + // Check if we've observed the expected pattern for 3 consecutive slots + if experiment_state.matches_expected_pattern() { + node_c_turbine_disabled.store(false, Ordering::Release); + experiment_state.stage = Stage::ObserveLiveness; + } + } + } + BLSMessage::Certificate(cert_message) => { + // Stage 3: Verify continued liveness after partition resolution + if experiment_state.stage == Stage::ObserveLiveness + && [CertificateType::Finalize, CertificateType::FinalizeFast] + .contains(&cert_message.certificate.certificate_type()) + { + experiment_state.record_certificate(cert_message.certificate.slot()); + + if experiment_state.sufficient_roots_created() { + break; + } + } + } + } + } + } + }); + + vote_listener_thread + .join() + .expect("Vote listener thread panicked"); +} + +/// Test to validate the Alpenglow consensus protocol's ability to maintain liveness when a node +/// needs to issue NotarizeFallback votes due to the second fallback condition. +/// +/// This test simulates a scenario with three nodes having the following stake distribution: +/// - Node A: 40% - ε (small epsilon) +/// - Node B (Leader): 30% + ε +/// - Node C: 30% +/// +/// The test validates the protocol's behavior through two main phases: +/// +/// ## Phase 1: Node A Goes Offline (Byzantine + Offline Stake) +/// - Node A (40% - ε stake) is taken offline, representing combined Byzantine and offline stake +/// - This leaves Node B (30% + ε) and Node C (30%) as the active validators +/// - Despite the significant offline stake, the remaining nodes can still achieve consensus +/// - Network continues to fast finalize blocks with the remaining 60% + ε stake +/// +/// ## Phase 2: Network Partition Triggers NotarizeFallback +/// - Node C's turbine is disabled at slot 20, causing it to miss incoming blocks +/// - Node B (as leader) proposes blocks and votes Notarize for them +/// - Node C, unable to receive blocks, votes Skip for the same slots +/// - This creates a voting scenario where: +/// - Notarize votes: 30% + ε (Node B only) +/// - Skip votes: 30% (Node C only) +/// - Offline: 40% - ε (Node A) +/// +/// ## NotarizeFallback Condition 2 Trigger +/// Node C observes that: +/// - There are insufficient notarization votes for the current block (30% + ε < 40%) +/// - But the combination of notarize + skip votes represents >= 60% participation while there is +/// sufficient notarize stake (>= 20%). +/// - Protocol determines it's "SafeToNotar" under condition 2 and issues NotarizeFallback +/// +/// ## Phase 3: Recovery and Liveness Verification +/// After observing 5 NotarizeFallback votes from Node C: +/// - Node C's turbine is re-enabled to restore normal block reception +/// - Network returns to normal operation with both active nodes +/// - Test verifies 10+ new roots are created, ensuring liveness is maintained +/// +/// ## Key Validation Points +/// - Protocol handles significant offline stake (40%) gracefully +/// - NotarizeFallback condition 2 triggers correctly with insufficient notarization +/// - Network maintains liveness despite temporary partitioning +/// - Recovery is seamless once partition is resolved +#[test] +#[serial] +fn test_alpenglow_ensure_liveness_after_second_notar_fallback_condition() { + solana_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + + // Configure total stake and stake distribution + const TOTAL_STAKE: u64 = 10 * DEFAULT_NODE_STAKE; + const SLOTS_PER_EPOCH: u64 = MINIMUM_SLOTS_PER_EPOCH; + + // Node stakes designed to trigger NotarizeFallback condition 2 + let node_stakes = [ + TOTAL_STAKE * 4 / 10 - 1, // Node A: 40% - ε (will go offline) + TOTAL_STAKE * 3 / 10 + 1, // Node B: 30% + ε (leader, stays online) + TOTAL_STAKE * 3 / 10, // Node C: 30% (will be partitioned) + ]; + + assert_eq!(TOTAL_STAKE, node_stakes.iter().sum::()); + + // Control component for network partition simulation + let node_c_turbine_disabled = Arc::new(AtomicBool::new(false)); + + // Create leader schedule with Node B as primary leader (Node A will go offline) + let (leader_schedule, validator_keys) = + create_custom_leader_schedule_with_random_keys(&[0, 4, 0]); + + let leader_schedule = FixedSchedule { + leader_schedule: Arc::new(leader_schedule), + }; + + // Create UDP socket to listen to votes for experiment control + let vote_listener_socket = solana_net_utils::bind_to_localhost().unwrap(); + + // Create validator configs + let mut validator_config = ValidatorConfig::default_for_test(); + validator_config.fixed_leader_schedule = Some(leader_schedule); + validator_config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![vote_listener_socket.local_addr().unwrap()], + alpenglow_port_override: AlpenglowPortOverride::default(), + }); + + let mut validator_configs = + make_identical_validator_configs(&validator_config, node_stakes.len()); + + // Node C will have its turbine disabled during the experiment + validator_configs[2].turbine_disabled = node_c_turbine_disabled.clone(); + + // Cluster configuration + let mut cluster_config = ClusterConfig { + mint_lamports: TOTAL_STAKE, + node_stakes: node_stakes.to_vec(), + validator_configs, + validator_keys: Some( + validator_keys + .iter() + .cloned() + .zip(std::iter::repeat(true)) + .collect(), + ), + slots_per_epoch: SLOTS_PER_EPOCH, + stakers_slot_offset: SLOTS_PER_EPOCH, + ticks_per_slot: DEFAULT_TICKS_PER_SLOT, + ..ClusterConfig::default() + }; + + // Create local cluster + let mut cluster = + LocalCluster::new_alpenglow(&mut cluster_config, SocketAddrSpace::Unspecified); + + // Create mapping from vote pubkeys to node indices for vote identification + let vote_pubkeys: HashMap<_, _> = validator_keys + .iter() + .enumerate() + .filter_map(|(index, keypair)| { + cluster + .validators + .get(&keypair.pubkey()) + .map(|validator| (validator.info.voting_keypair.pubkey(), index)) + }) + .collect(); + + assert_eq!(vote_pubkeys.len(), node_stakes.len()); + + // Phase 1: Take Node A offline to simulate Byzantine + offline stake + // This represents 40% - ε of total stake going offline + cluster.exit_node(&validator_keys[0].pubkey()); + + // Vote listener state management + #[derive(Debug, PartialEq, Eq)] + enum Stage { + Stability, + ObserveNotarFallbacks, + ObserveLiveness, + } + + impl Stage { + fn timeout(&self) -> Duration { + match self { + Stage::Stability => Duration::from_secs(60), + Stage::ObserveNotarFallbacks => Duration::from_secs(120), + Stage::ObserveLiveness => Duration::from_secs(180), + } + } + + fn all() -> Vec { + vec![ + Stage::Stability, + Stage::ObserveNotarFallbacks, + Stage::ObserveLiveness, + ] + } + } + + #[derive(Debug)] + struct ExperimentState { + stage: Stage, + notar_fallbacks: HashSet, + post_experiment_roots: HashSet, + } + + impl ExperimentState { + fn new() -> Self { + Self { + stage: Stage::Stability, + notar_fallbacks: HashSet::new(), + post_experiment_roots: HashSet::new(), + } + } + + fn handle_experiment_start( + &mut self, + vote: &Vote, + node_c_turbine_disabled: &Arc, + ) { + // Phase 2: Start network partition experiment at slot 20 + if vote.slot() >= 20 && self.stage == Stage::Stability { + info!( + "Starting network partition experiment at slot {}", + vote.slot() + ); + node_c_turbine_disabled.store(true, Ordering::Relaxed); + self.stage = Stage::ObserveNotarFallbacks; + } + } + + fn handle_notar_fallback( + &mut self, + vote: &Vote, + node_name: usize, + node_c_turbine_disabled: &Arc, + ) { + // Track NotarizeFallback votes from Node C + if self.stage == Stage::ObserveNotarFallbacks + && node_name == 2 + && vote.is_notarize_fallback() + { + self.notar_fallbacks.insert(vote.slot()); + info!( + "Node C issued NotarizeFallback for slot {}, total fallbacks: {}", + vote.slot(), + self.notar_fallbacks.len() + ); + + // Phase 3: End partition after observing sufficient NotarizeFallback votes + if self.notar_fallbacks.len() >= 5 { + info!("Sufficient NotarizeFallback votes observed, ending partition"); + node_c_turbine_disabled.store(false, Ordering::Relaxed); + self.stage = Stage::ObserveLiveness; + } + } + } + + fn record_certificate(&mut self, slot: u64) { + self.post_experiment_roots.insert(slot); + } + + fn sufficient_roots_created(&self) -> bool { + self.post_experiment_roots.len() >= 8 + } + } + + // Start vote listener thread to monitor and control the experiment + let vote_listener_thread = std::thread::spawn({ + let mut buf = [0u8; 65_535]; + let node_c_turbine_disabled = node_c_turbine_disabled.clone(); + let mut experiment_state = ExperimentState::new(); + let timer = std::time::Instant::now(); + + move || { + loop { + let n_bytes = vote_listener_socket.recv(&mut buf).unwrap(); + + let bls_message = bincode::deserialize::(&buf[0..n_bytes]).unwrap(); + + match bls_message { + BLSMessage::Vote(vote_message) => { + let vote = &vote_message.vote; + let node_name = vote_message.rank as usize; + + // Stage timeouts + let elapsed_time = timer.elapsed(); + + for stage in Stage::all() { + if elapsed_time > stage.timeout() { + panic!( + "Timeout during {:?}. node_c_turbine_disabled: {:#?}. Latest vote: {:#?}. Experiment state: {:#?}", + stage, + node_c_turbine_disabled.load(Ordering::Acquire), + vote, + experiment_state + ); + } + } + + // Handle experiment phase transitions + experiment_state.handle_experiment_start(vote, &node_c_turbine_disabled); + experiment_state.handle_notar_fallback( + vote, + node_name, + &node_c_turbine_disabled, + ); + } + + BLSMessage::Certificate(cert_message) => { + // Check for finalization certificates to determine test completion + if [CertificateType::Finalize, CertificateType::FinalizeFast] + .contains(&cert_message.certificate.certificate_type()) + { + experiment_state.record_certificate(cert_message.certificate.slot()); + + if experiment_state.sufficient_roots_created() { + break; + } + } + } + } + } + } + }); + + vote_listener_thread.join().unwrap(); +} diff --git a/metrics/src/datapoint.rs b/metrics/src/datapoint.rs index e2740ce3ae..49cf3317c4 100644 --- a/metrics/src/datapoint.rs +++ b/metrics/src/datapoint.rs @@ -81,6 +81,11 @@ impl DataPoint { self } + pub fn add_field_u64(&mut self, name: &'static str, value: u64) -> &mut Self { + self.fields.push((name, value.to_string() + "u")); + self + } + pub fn add_field_f64(&mut self, name: &'static str, value: f64) -> &mut Self { self.fields.push((name, value.to_string())); self @@ -108,6 +113,9 @@ macro_rules! create_datapoint { (@field $point:ident $name:expr, $value:expr, i64) => { $point.add_field_i64($name, $value as i64); }; + (@field $point:ident $name:expr, $value:expr, u64) => { + $point.add_field_u64($name, $value as u64); + }; (@field $point:ident $name:expr, $value:expr, f64) => { $point.add_field_f64($name, $value as f64); }; diff --git a/multinode-demo/bootstrap-validator.sh b/multinode-demo/bootstrap-validator.sh index 4285be6cac..2a49e50da2 100755 --- a/multinode-demo/bootstrap-validator.sh +++ b/multinode-demo/bootstrap-validator.sh @@ -28,7 +28,7 @@ while [[ -n $1 ]]; do if [[ $1 = --init-complete-file ]]; then args+=("$1" "$2") shift 2 - elif [[ $1 = --gossip-host ]]; then + elif [[ $1 = --bind-address ]]; then args+=("$1" "$2") shift 2 elif [[ $1 = --gossip-port ]]; then diff --git a/multinode-demo/setup.sh b/multinode-demo/setup.sh index fbb2185724..b845f84233 100755 --- a/multinode-demo/setup.sh +++ b/multinode-demo/setup.sh @@ -32,6 +32,8 @@ else $solana_keygen new --no-passphrase -so "$SOLANA_CONFIG_DIR"/bootstrap-validator/vote-account.json fi +BLS_PUBKEY=$($solana_keygen bls_pubkey "$SOLANA_CONFIG_DIR"/bootstrap-validator/identity.json) + args=( "$@" --max-genesis-archive-unpacked-size 1073741824 @@ -39,6 +41,7 @@ args=( --bootstrap-validator "$SOLANA_CONFIG_DIR"/bootstrap-validator/identity.json "$SOLANA_CONFIG_DIR"/bootstrap-validator/vote-account.json "$SOLANA_CONFIG_DIR"/bootstrap-validator/stake-account.json + --bootstrap-validator-bls-pubkey "$BLS_PUBKEY" ) "$SOLANA_ROOT"/fetch-core-bpf.sh diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index 800b4ce9d1..10b545e50b 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -19,6 +19,7 @@ vote_account= no_restart=0 gossip_entrypoint= ledger_dir= +alpenglow= usage() { if [[ -n $1 ]]; then @@ -191,6 +192,9 @@ while [[ -n $1 ]]; do elif [[ $1 == --wen-restart-coordinator ]]; then args+=("$1" "$2") shift 2 + elif [[ $1 == --alpenglow ]]; then + alpenglow=(--alpenglow) + shift elif [[ $1 = -h ]]; then usage "$@" else @@ -329,8 +333,13 @@ setup_validator_accounts() { ) || return $? fi - echo "Creating validator vote account" - wallet create-vote-account "$vote_account" "$identity" "$authorized_withdrawer" || return $? + if [[ -n "$alpenglow" ]]; then + echo "Creating Alpenglow validator vote account" + wallet create-vote-account "$alpenglow" "$vote_account" "$identity" "$authorized_withdrawer" || return $? + else + echo "Creating POH validator vote account" + wallet create-vote-account "$vote_account" "$identity" "$authorized_withdrawer" || return $? + fi fi echo "Validator vote account configured" diff --git a/net/net.sh b/net/net.sh index 235d485555..e8ecff539d 100755 --- a/net/net.sh +++ b/net/net.sh @@ -317,6 +317,7 @@ startBootstrapLeader() { declare ipAddress=$1 declare nodeIndex="$2" declare logFile="$3" + declare alpenglow="$4" echo "--- Starting bootstrap validator: $ipAddress" echo "start log: $logFile" @@ -328,6 +329,21 @@ startBootstrapLeader() { deployBootstrapValidator "$ipAddress" + # TODO: once we cut a public release of alpenglow-vote, we can eliminate this block + # below. For now though, as we're developing alpenglow in tandem with alpenglow-vote, + # we auto-generate spl_alpenglow-vote.so while building alpenglow. This block here + # copies over this auto-generated spl_alpenglow-vote.so over to the bootstrap + # validator. + if $alpenglow; then + declare remoteHome + remoteHome=$(remoteHomeDir "$ipAddress") + local remoteSolanaHome="${remoteHome}/solana" + + rsync -vPrc -e "ssh ${sshOptions[*]}" \ + "$SOLANA_ROOT"/target/alpenglow-vote-so/spl_alpenglow-vote.so \ + "$ipAddress":"$remoteSolanaHome"/ > /dev/null + fi + ssh "${sshOptions[@]}" -n "$ipAddress" \ "./solana/net/remote/remote-node.sh \ $deployMethod \ @@ -354,6 +370,7 @@ startBootstrapLeader() { \"$disableQuic\" \ \"$enableUdp\" \ \"$maybeWenRestart\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 || { @@ -367,6 +384,7 @@ startNode() { declare ipAddress=$1 declare nodeType=$2 declare nodeIndex="$3" + declare alpenglow="$4" initLogDir declare logFile="$netLogDir/validator-$ipAddress.log" @@ -429,6 +447,7 @@ startNode() { \"$disableQuic\" \ \"$enableUdp\" \ \"$maybeWenRestart\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 & declare pid=$! @@ -633,7 +652,7 @@ deploy() { if $bootstrapLeader; then SECONDS=0 declare bootstrapNodeDeployTime= - startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" + startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" "$alpenglow" bootstrapNodeDeployTime=$SECONDS $metricsWriteDatapoint "testnet-deploy net-bootnode-leader-started=1" @@ -641,7 +660,7 @@ deploy() { SECONDS=0 pids=() else - startNode "$ipAddress" "$nodeType" "$nodeIndex" + startNode "$ipAddress" "$nodeType" "$nodeIndex" "$alpenglow" # Stagger additional node start time. If too many nodes start simultaneously # the bootstrap node gets more rsync requests from the additional nodes than @@ -842,6 +861,7 @@ enableUdp=false clientType=tpu-client maybeUseUnstakedConnection="" maybeWenRestart="" +alpenglow=false command=$1 [[ -n $command ]] || usage @@ -995,6 +1015,9 @@ while [[ -n $1 ]]; do skipSetup=true maybeWenRestart="$2" shift 2 + elif [[ $1 = --alpenglow ]]; then + alpenglow=true + shift 1 else usage "Unknown long option: $1" fi @@ -1104,7 +1127,7 @@ if [[ "$numClientsRequested" -eq 0 ]]; then numClientsRequested=$numClients else if [[ "$numClientsRequested" -gt "$numClients" ]]; then - echo "Error: More clients requested ($numClientsRequested) then available ($numClients)" + echo "Error: More clients requested ($numClientsRequested) than available ($numClients)" exit 1 fi fi diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index edd21ba731..ac2eb34ddd 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -31,6 +31,7 @@ tmpfsAccounts="${22:false}" disableQuic="${23}" enableUdp="${24}" maybeWenRestart="${25}" +alpenglow="${26}" set +x @@ -236,9 +237,17 @@ EOF "$(solana-keygen pubkey "config/validator-vote-$i.json")" "$(solana-keygen pubkey "config/validator-stake-$i.json")" ) + args+=(--bootstrap-validator-bls-pubkey "$(solana-keygen bls_pubkey "config/validator-identity-$i.json")") done fi + if $alpenglow; then + echo "Consensus method: Alpenglow" + args+=(--alpenglow "$HOME"/solana/spl_alpenglow-vote.so) + else + echo "Consensus method: POH" + fi + multinode-demo/setup.sh "${args[@]}" maybeWaitForSupermajority= @@ -271,7 +280,7 @@ EOF fi fi args=( - --gossip-host "$entrypointIp" + --bind-address "$entrypointIp" --gossip-port 8001 --init-complete-file "$initCompleteFile" ) @@ -440,6 +449,13 @@ EOF args+=(--wen-restart-coordinator "$maybeWenRestart") fi + if $alpenglow; then + echo "Consensus method: Alpenglow" + args+=(--alpenglow) + else + echo "Consensus method: POH" + fi + cat >> ~/solana/on-reboot < validator.log.\$now 2>&1 & diff --git a/perf/Cargo.toml b/perf/Cargo.toml index c1a1293b8d..a98216b0e0 100644 --- a/perf/Cargo.toml +++ b/perf/Cargo.toml @@ -43,8 +43,9 @@ solana-system-interface = { workspace = true, optional = true } solana-system-transaction = { workspace = true, optional = true } solana-time-utils = { workspace = true } solana-transaction = { workspace = true, optional = true } -solana-vote = { workspace = true, optional = true } +solana-vote = { workspace = true } solana-vote-program = { workspace = true, optional = true } +solana-votor-messages = { workspace = true } [target."cfg(target_os = \"linux\")".dependencies] caps = { workspace = true } @@ -70,7 +71,6 @@ dev-context-only-utils = [ "dep:solana-system-transaction", "dep:solana-transaction", "dep:solana-vote-program", - "dep:solana-vote", ] frozen-abi = [ "dep:solana-frozen-abi", diff --git a/perf/src/sigverify.rs b/perf/src/sigverify.rs index 89ba49c59a..c6f576a9c9 100644 --- a/perf/src/sigverify.rs +++ b/perf/src/sigverify.rs @@ -376,10 +376,12 @@ fn check_for_simple_vote_transaction( .checked_add(size_of::()) .ok_or(PacketError::InvalidLen)?; - if packet + let program_id = packet .data(instruction_program_id_start..instruction_program_id_end) - .ok_or(PacketError::InvalidLen)? - == solana_sdk_ids::vote::id().as_ref() + .ok_or(PacketError::InvalidLen)?; + + if program_id == solana_sdk_ids::vote::id().as_ref() + || program_id == solana_votor_messages::id().as_ref() { packet.meta_mut().flags |= PacketFlags::SIMPLE_VOTE_TX; } diff --git a/poh/Cargo.toml b/poh/Cargo.toml index 3b37ead60c..b0281b209a 100644 --- a/poh/Cargo.toml +++ b/poh/Cargo.toml @@ -22,6 +22,7 @@ solana-metrics = { workspace = true } solana-poh-config = { workspace = true } solana-pubkey = { workspace = true } solana-runtime = { workspace = true } +solana-sdk = { workspace = true } solana-time-utils = { workspace = true } solana-transaction = { workspace = true } thiserror = { workspace = true } diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index d633018958..2466501be6 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -10,6 +10,8 @@ //! For Entries: //! * recorded entry must be >= WorkingBank::min_tick_height && entry must be < WorkingBank::max_tick_height //! +#[cfg(feature = "dev-context-only-utils")] +use std::sync::RwLock; use { crate::{leader_bank_notifier::LeaderBankNotifier, poh_service::PohService}, crossbeam_channel::{ @@ -34,7 +36,7 @@ use { num::Saturating, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Mutex, RwLock, + Arc, Mutex, }, time::{Duration, Instant}, }, @@ -64,6 +66,7 @@ pub type WorkingBankEntry = (Arc, (Entry, u64)); pub struct BankStart { pub working_bank: Arc, pub bank_creation_time: Arc, + pub contains_valid_certificate: Arc, } impl BankStart { @@ -75,6 +78,16 @@ impl BankStart { } } +impl From<&WorkingBank> for BankStart { + fn from(w: &WorkingBank) -> Self { + Self { + working_bank: w.bank.clone(), + bank_creation_time: w.start.clone(), + contains_valid_certificate: w.contains_valid_certificate.clone(), + } + } +} + // Sends the Result of the record operation, including the index in the slot of the first // transaction, if being tracked by WorkingBank type RecordResultSender = Sender>>; @@ -234,6 +247,7 @@ pub struct WorkingBank { pub min_tick_height: u64, pub max_tick_height: u64, pub transaction_index: Option, + pub contains_valid_certificate: Arc, } #[derive(Debug, PartialEq, Eq)] @@ -322,6 +336,8 @@ pub struct PohRecorder { delay_leader_block_for_pending_fork: bool, last_reported_slot_for_pending_fork: Arc>, pub is_exited: Arc, + pub is_alpenglow_enabled: bool, + pub use_alpenglow_tick_producer: bool, } impl PohRecorder { @@ -329,6 +345,7 @@ impl PohRecorder { /// * bank - the LastId's queue is updated on `tick` and `record` events /// * sender - the Entry channel that outputs to the ledger #[allow(clippy::too_many_arguments)] + #[cfg(feature = "dev-context-only-utils")] pub fn new( tick_height: u64, last_entry_hash: Hash, @@ -354,6 +371,7 @@ impl PohRecorder { poh_config, None, is_exited, + false, ) } @@ -371,6 +389,7 @@ impl PohRecorder { poh_config: &PohConfig, poh_timing_point_sender: Option, is_exited: Arc, + is_alpenglow_enabled: bool, ) -> (Self, Receiver, Receiver) { let tick_number = 0; let poh = Arc::new(Mutex::new(Poh::new_with_slot_info( @@ -412,6 +431,8 @@ impl PohRecorder { delay_leader_block_for_pending_fork, last_reported_slot_for_pending_fork: Arc::default(), is_exited, + is_alpenglow_enabled, + use_alpenglow_tick_producer: is_alpenglow_enabled, }, working_bank_receiver, record_receiver, @@ -554,7 +575,11 @@ impl PohRecorder { } } - pub fn set_bank(&mut self, bank: BankWithScheduler, track_transaction_indexes: bool) { + pub fn set_bank( + &mut self, + bank: BankWithScheduler, + track_transaction_indexes: bool, + ) -> BankStart { assert!(self.working_bank.is_none()); self.leader_bank_notifier.set_in_progress(&bank); let working_bank = WorkingBank { @@ -563,7 +588,9 @@ impl PohRecorder { bank, start: Arc::new(Instant::now()), transaction_index: track_transaction_indexes.then_some(0), + contains_valid_certificate: Arc::new(AtomicBool::new(false)), }; + let bank_start = BankStart::from(&working_bank); trace!("new working bank"); assert_eq!(working_bank.bank.ticks_per_slot(), self.ticks_per_slot()); if let Some(hashes_per_tick) = *working_bank.bank.hashes_per_tick() { @@ -598,6 +625,7 @@ impl PohRecorder { // TODO: adjust the working_bank.start time based on number of ticks // that have already elapsed based on current tick height. let _ = self.flush_cache(false); + bank_start } fn clear_bank(&mut self) { @@ -639,9 +667,14 @@ impl PohRecorder { fn reset_poh(&mut self, reset_bank: Arc, reset_start_bank: bool) { let blockhash = reset_bank.last_blockhash(); + let hashes_per_tick = if self.use_alpenglow_tick_producer { + None + } else { + *reset_bank.hashes_per_tick() + }; let poh_hash = { let mut poh = self.poh.lock().unwrap(); - poh.reset(blockhash, *reset_bank.hashes_per_tick()); + poh.reset(blockhash, hashes_per_tick); poh.hash }; info!( @@ -778,10 +811,7 @@ impl PohRecorder { } pub fn bank_start(&self) -> Option { - self.working_bank.as_ref().map(|w| BankStart { - working_bank: w.bank.clone(), - bank_creation_time: w.start.clone(), - }) + self.working_bank.as_ref().map(BankStart::from) } fn working_bank_end_slot(&self) -> Option { @@ -1028,12 +1058,12 @@ impl PohRecorder { #[cfg(feature = "dev-context-only-utils")] pub fn set_bank_for_test(&mut self, bank: Arc) { - self.set_bank(BankWithScheduler::new_without_scheduler(bank), false) + self.set_bank(BankWithScheduler::new_without_scheduler(bank), false); } #[cfg(feature = "dev-context-only-utils")] pub fn set_bank_with_transaction_index_for_test(&mut self, bank: Arc) { - self.set_bank(BankWithScheduler::new_without_scheduler(bank), true) + self.set_bank(BankWithScheduler::new_without_scheduler(bank), true); } #[cfg(feature = "dev-context-only-utils")] @@ -1096,8 +1126,48 @@ impl PohRecorder { self.report_poh_timing_point_by_tick() } } + + pub fn tick_alpenglow(&mut self, slot_max_tick_height: u64) { + let (poh_entry, tick_lock_contention_us) = measure_us!({ + let mut poh_l = self.poh.lock().unwrap(); + poh_l.tick() + }); + self.metrics.tick_lock_contention_us += tick_lock_contention_us; + + if let Some(poh_entry) = poh_entry { + self.tick_height = slot_max_tick_height; + self.report_poh_timing_point(); + + // Should be empty in most cases, but reset just to be safe + self.tick_cache = vec![]; + self.tick_cache.push(( + Entry { + num_hashes: poh_entry.num_hashes, + hash: poh_entry.hash, + transactions: vec![], + }, + self.tick_height, + )); + + let (_flush_res, flush_cache_and_tick_us) = measure_us!(self.flush_cache(true)); + self.metrics.flush_cache_tick_us += flush_cache_and_tick_us; + } + } + + pub fn migrate_to_alpenglow_poh(&mut self) { + self.tick_cache = vec![]; + { + let mut poh = self.poh.lock().unwrap(); + // sets PoH to low power mode + let hashes_per_tick = None; + let current_hash = poh.hash; + info!("migrating poh to low power mode"); + poh.reset(current_hash, hashes_per_tick); + } + } } +#[cfg(feature = "dev-context-only-utils")] fn do_create_test_recorder( bank: Arc, blockstore: Arc, @@ -1142,11 +1212,13 @@ fn do_create_test_recorder( crate::poh_service::DEFAULT_PINNED_CPU_CORE, crate::poh_service::DEFAULT_HASHES_PER_BATCH, record_receiver, + || {}, ); (exit, poh_recorder, poh_service, entry_receiver) } +#[cfg(feature = "dev-context-only-utils")] pub fn create_test_recorder( bank: Arc, blockstore: Arc, @@ -1161,6 +1233,7 @@ pub fn create_test_recorder( do_create_test_recorder(bank, blockstore, poh_config, leader_schedule_cache, false) } +#[cfg(feature = "dev-context-only-utils")] pub fn create_test_recorder_with_index_tracking( bank: Arc, blockstore: Arc, @@ -1813,6 +1886,7 @@ mod tests { &PohConfig::default(), None, Arc::new(AtomicBool::default()), + false, ); poh_recorder.set_bank_for_test(bank); poh_recorder.clear_bank(); diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs index a8b7229a76..cc6433a917 100644 --- a/poh/src/poh_service.rs +++ b/poh/src/poh_service.rs @@ -94,7 +94,8 @@ impl PohTiming { } impl PohService { - pub fn new( + #[allow(clippy::too_many_arguments)] + pub fn new( poh_recorder: Arc>, poh_config: &PohConfig, poh_exit: Arc, @@ -102,46 +103,84 @@ impl PohService { pinned_cpu_core: usize, hashes_per_batch: u64, record_receiver: Receiver, - ) -> Self { + block_creation_loop: F, + ) -> Self + where + // TODO: this weirdness is because solana_poh can't depend on solana_core + // Once we cleanup the prototype and separate alpenglow into it's own crate this + // can be fixed. + F: FnOnce() + std::marker::Send + 'static, + { let poh_config = poh_config.clone(); + let is_alpenglow_enabled = poh_recorder.read().unwrap().is_alpenglow_enabled; let tick_producer = Builder::new() .name("solPohTickProd".to_string()) .spawn(move || { - if poh_config.hashes_per_tick.is_none() { - if poh_config.target_tick_count.is_none() { - Self::low_power_tick_producer( - poh_recorder, - &poh_config, - &poh_exit, - record_receiver, - ); + if !is_alpenglow_enabled { + if poh_config.hashes_per_tick.is_none() { + if poh_config.target_tick_count.is_none() { + Self::low_power_tick_producer( + poh_recorder.clone(), + &poh_config, + &poh_exit, + record_receiver.clone(), + ); + } else { + Self::short_lived_low_power_tick_producer( + poh_recorder.clone(), + &poh_config, + &poh_exit, + record_receiver.clone(), + ); + } } else { - Self::short_lived_low_power_tick_producer( - poh_recorder, - &poh_config, + // PoH service runs in a tight loop, generating hashes as fast as possible. + // Let's dedicate one of the CPU cores to this thread so that it can gain + // from cache performance. + if let Some(cores) = core_affinity::get_core_ids() { + core_affinity::set_for_current(cores[pinned_cpu_core]); + } + Self::tick_producer( + poh_recorder.clone(), &poh_exit, - record_receiver, + ticks_per_slot, + hashes_per_batch, + record_receiver.clone(), + Self::target_ns_per_tick( + ticks_per_slot, + poh_config.target_tick_duration.as_nanos() as u64, + ), ); } - } else { - // PoH service runs in a tight loop, generating hashes as fast as possible. - // Let's dedicate one of the CPU cores to this thread so that it can gain - // from cache performance. - if let Some(cores) = core_affinity::get_core_ids() { - core_affinity::set_for_current(cores[pinned_cpu_core]); + + // Migrate to alpenglow PoH + if !poh_exit.load(Ordering::Relaxed) + // Should be set by replay_stage after it sees a notarized + // block in the new alpenglow epoch + && poh_recorder.read().unwrap().is_alpenglow_enabled + { + info!("Migrating poh service to alpenglow tick producer"); + } else { + poh_exit.store(true, Ordering::Relaxed); + return; } - Self::tick_producer( - poh_recorder, - &poh_exit, - ticks_per_slot, - hashes_per_batch, - record_receiver, - Self::target_ns_per_tick( - ticks_per_slot, - poh_config.target_tick_duration.as_nanos() as u64, - ), - ); } + + // Start alpenglow + // + // Important this is called *before* any new alpenglow + // leaders call `set_bank()`, otherwise, the old PoH + // tick producer will still tick in that alpenglow bank + // + // TODO: Can essentailly replace this with no ticks + // once we properly remove poh/entry verification in replay + { + let mut w_poh_recorder = poh_recorder.write().unwrap(); + w_poh_recorder.migrate_to_alpenglow_poh(); + w_poh_recorder.use_alpenglow_tick_producer = true; + } + info!("Starting alpenglow block creation loop"); + block_creation_loop(); poh_exit.store(true, Ordering::Relaxed); }) .unwrap(); @@ -178,7 +217,12 @@ impl PohService { ); if remaining_tick_time.is_zero() { last_tick = Instant::now(); - poh_recorder.write().unwrap().tick(); + let mut w_poh_recorder = poh_recorder.write().unwrap(); + w_poh_recorder.tick(); + if w_poh_recorder.is_alpenglow_enabled { + info!("exiting tick_producer because alpenglow enabled"); + break; + } } } } @@ -352,6 +396,10 @@ impl PohService { let mut poh_recorder_l = poh_recorder.write().unwrap(); lock_time.stop(); timing.total_lock_time_ns += lock_time.as_ns(); + if poh_recorder_l.is_alpenglow_enabled { + info!("exiting tick_producer because alpenglow enabled"); + break; + } let mut tick_time = Measure::start("tick"); poh_recorder_l.tick(); tick_time.stop(); @@ -490,6 +538,7 @@ mod tests { DEFAULT_PINNED_CPU_CORE, hashes_per_batch, record_receiver, + || {}, ); poh_recorder.write().unwrap().set_bank_for_test(bank); diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 53a577cf1d..108e952231 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -952,6 +952,10 @@ impl ProgramCache { error!("Failed to lock fork graph for reading."); return; }; + self.prune_locked(new_root_slot, new_root_epoch, &fork_graph); + } + + pub fn prune_locked(&mut self, new_root_slot: Slot, new_root_epoch: Epoch, fork_graph: &FG) { let mut preparation_phase_ends = false; if self.latest_root_epoch != new_root_epoch { self.latest_root_epoch = new_root_epoch; diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index ed01c8a22e..1932851d66 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -812,12 +812,14 @@ impl ProgramTest { &bootstrap_validator_pubkey, &voting_keypair.pubkey(), &Pubkey::new_unique(), + None, bootstrap_validator_stake_lamports, 42, fee_rate_governor, rent.clone(), ClusterType::Development, std::mem::take(&mut self.genesis_accounts), + None, ); // Remove features tagged to deactivate diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index b25df1761e..caba985399 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -20,9 +20,11 @@ solana-account-info = { workspace = true } solana-big-mod-exp = { workspace = true } solana-bincode = { workspace = true } solana-blake3-hasher = { workspace = true } +solana-bls12-381 = { workspace = true } solana-bn254 = { workspace = true } solana-clock = { workspace = true } solana-cpi = { workspace = true } +solana-curve-traits = { workspace = true } solana-curve25519 = { workspace = true } solana-feature-set = { workspace = true } solana-hash = { workspace = true } @@ -64,7 +66,9 @@ solana-program = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-rent = { workspace = true } solana-slot-hashes = { workspace = true } -solana-transaction-context = { workspace = true, features = ["dev-context-only-utils"] } +solana-transaction-context = { workspace = true, features = [ + "dev-context-only-utils", +] } static_assertions = { workspace = true } test-case = { workspace = true } @@ -89,6 +93,6 @@ metrics = ["solana-program-runtime/metrics"] shuttle-test = [ "solana-type-overrides/shuttle-test", "solana-program-runtime/shuttle-test", - "solana-sbpf/shuttle-test" + "solana-sbpf/shuttle-test", ] svm-internal = [] diff --git a/programs/bpf_loader/src/syscalls/mod.rs b/programs/bpf_loader/src/syscalls/mod.rs index 76164a0eb1..c178178629 100644 --- a/programs/bpf_loader/src/syscalls/mod.rs +++ b/programs/bpf_loader/src/syscalls/mod.rs @@ -1053,7 +1053,10 @@ declare_builtin_function!( _arg5: u64, memory_mapping: &mut MemoryMapping, ) -> Result { - use solana_curve25519::{curve_syscall_traits::*, edwards, ristretto}; + use { + solana_curve25519::{edwards, ristretto}, + solana_curve_traits::*, + }; match curve_id { CURVE25519_EDWARDS => { let cost = invoke_context @@ -1119,7 +1122,11 @@ declare_builtin_function!( result_point_addr: u64, memory_mapping: &mut MemoryMapping, ) -> Result { - use solana_curve25519::{curve_syscall_traits::*, edwards, ristretto, scalar}; + use { + solana_bls12_381::{g1, g2, scalar as bls_scalar}, + solana_curve25519::{edwards, ristretto, scalar}, + solana_curve_traits::*, + }; match curve_id { CURVE25519_EDWARDS => match group_op { ADD => { @@ -1317,6 +1324,182 @@ declare_builtin_function!( } }, + BLS12_381_G1_PROJECTIVE => match group_op { + ADD => { + // TODO: add compute costs + + let left_point = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let right_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g1::add(left_point, right_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + SUB => { + // TODO: add compute costs + + let left_point = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let right_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g1::subtract(left_point, right_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + MUL => { + // TODO: add compute costs + + let scalar = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let input_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g1::multiply(scalar, input_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + _ => { + if invoke_context + .get_feature_set() + .is_active(&abort_on_invalid_curve::id()) + { + Err(SyscallError::InvalidAttribute.into()) + } else { + Ok(1) + } + } + }, + + BLS12_381_G2_PROJECTIVE => match group_op { + ADD => { + // TODO: add compute costs + + let left_point = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let right_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g2::add(left_point, right_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + SUB => { + // TODO: add compute costs + + let left_point = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let right_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g2::subtract(left_point, right_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + MUL => { + // TODO: add compute costs + + let scalar = translate_type::( + memory_mapping, + left_input_addr, + invoke_context.get_check_aligned(), + )?; + let input_point = translate_type::( + memory_mapping, + right_input_addr, + invoke_context.get_check_aligned(), + )?; + + if let Some(result_point) = g2::multiply(scalar, input_point) { + *translate_type_mut::( + memory_mapping, + result_point_addr, + invoke_context.get_check_aligned(), + )? = result_point; + Ok(0) + } else { + Ok(1) + } + } + _ => { + if invoke_context + .get_feature_set() + .is_active(&abort_on_invalid_curve::id()) + { + Err(SyscallError::InvalidAttribute.into()) + } else { + Ok(1) + } + } + }, + _ => { if invoke_context .get_feature_set() @@ -1345,7 +1528,10 @@ declare_builtin_function!( result_point_addr: u64, memory_mapping: &mut MemoryMapping, ) -> Result { - use solana_curve25519::{curve_syscall_traits::*, edwards, ristretto, scalar}; + use { + solana_curve25519::{edwards, ristretto, scalar}, + solana_curve_traits::*, + }; if points_len > 512 { return Err(Box::new(SyscallError::InvalidLength)); @@ -2903,7 +3089,7 @@ mod tests { #[test] fn test_syscall_edwards_curve_point_validation() { - use solana_curve25519::curve_syscall_traits::CURVE25519_EDWARDS; + use solana_curve_traits::CURVE25519_EDWARDS; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); @@ -2976,7 +3162,7 @@ mod tests { #[test] fn test_syscall_ristretto_curve_point_validation() { - use solana_curve25519::curve_syscall_traits::CURVE25519_RISTRETTO; + use solana_curve_traits::CURVE25519_RISTRETTO; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); @@ -3049,7 +3235,7 @@ mod tests { #[test] fn test_syscall_edwards_curve_group_ops() { - use solana_curve25519::curve_syscall_traits::{ADD, CURVE25519_EDWARDS, MUL, SUB}; + use solana_curve_traits::{ADD, CURVE25519_EDWARDS, MUL, SUB}; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); @@ -3204,7 +3390,7 @@ mod tests { #[test] fn test_syscall_ristretto_curve_group_ops() { - use solana_curve25519::curve_syscall_traits::{ADD, CURVE25519_RISTRETTO, MUL, SUB}; + use solana_curve_traits::{ADD, CURVE25519_RISTRETTO, MUL, SUB}; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); @@ -3361,7 +3547,7 @@ mod tests { #[test] fn test_syscall_multiscalar_multiplication() { - use solana_curve25519::curve_syscall_traits::{CURVE25519_EDWARDS, CURVE25519_RISTRETTO}; + use solana_curve_traits::{CURVE25519_EDWARDS, CURVE25519_RISTRETTO}; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); @@ -3467,7 +3653,7 @@ mod tests { #[test] fn test_syscall_multiscalar_multiplication_maximum_length_exceeded() { - use solana_curve25519::curve_syscall_traits::{CURVE25519_EDWARDS, CURVE25519_RISTRETTO}; + use solana_curve_traits::{CURVE25519_EDWARDS, CURVE25519_RISTRETTO}; let config = Config::default(); prepare_mockup!(invoke_context, program_id, bpf_loader::id()); diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 9ee34df151..8c60648fb5 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -157,6 +157,7 @@ dependencies = [ "solana-unified-scheduler-pool", "solana-version", "solana-vote-program", + "solana-votor", "symlink", "thiserror 2.0.12", "tikv-jemallocator", @@ -723,6 +724,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.6.1" @@ -755,6 +769,34 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "borsh" version = "0.10.3" @@ -863,6 +905,12 @@ dependencies = [ "serde", ] +[[package]] +name = "build-print" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2128d00b7061b82b72844a351e80acd29e05afc60e9261e2ac90dca9ecc2ac" + [[package]] name = "bumpalo" version = "3.12.0" @@ -879,6 +927,12 @@ dependencies = [ "serde", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.22.0" @@ -1196,9 +1250,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1789,6 +1843,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1807,6 +1872,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + [[package]] name = "five8_const" version = "0.1.3" @@ -1818,9 +1892,9 @@ dependencies = [ [[package]] name = "five8_core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a72055cd9cffc40c9f75f1e5810c80559e158796cf2202292ce4745889588" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" [[package]] name = "fixedbitset" @@ -1889,6 +1963,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -2057,9 +2137,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" @@ -2113,6 +2193,19 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand 0.8.5", + "rand_core 0.6.4", + "rand_xorshift", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -3604,9 +3697,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.9.0", "cfg-if 1.0.0", @@ -3645,9 +3738,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -3675,6 +3768,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parity-tokio-ipc" version = "0.9.0" @@ -3981,7 +4083,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -3990,7 +4092,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -4208,6 +4310,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4889,6 +4997,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5406,6 +5523,41 @@ dependencies = [ "solana-time-utils", ] +[[package]] +name = "solana-bls-signatures" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af089f712fb5cbef2d73ac7ffee9cad05ada6cd1fd5c812338df040bcd3c410b" +dependencies = [ + "base64 0.22.1", + "blst", + "blstrs", + "bytemuck", + "cfg_eval", + "ff", + "group", + "rand 0.8.5", + "serde", + "serde_json", + "serde_with", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-bls12-381" +version = "2.3.0" +dependencies = [ + "blst", + "bytemuck", + "bytemuck_derive", + "solana-curve-traits", + "solana-define-syscall", + "thiserror 2.0.12", +] + [[package]] name = "solana-bn254" version = "2.2.1" @@ -5445,9 +5597,11 @@ dependencies = [ "solana-big-mod-exp", "solana-bincode", "solana-blake3-hasher", + "solana-bls12-381", "solana-bn254", "solana-clock", "solana-cpi", + "solana-curve-traits", "solana-curve25519", "solana-feature-set", "solana-hash", @@ -5495,6 +5649,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "solana-build-alpenglow-vote" +version = "2.3.0" +dependencies = [ + "build-print", + "glob", + "serde", + "toml 0.8.12", +] + [[package]] name = "solana-builtins" version = "2.3.0" @@ -5543,6 +5707,7 @@ dependencies = [ "chrono", "clap", "rpassword", + "solana-bls-signatures", "solana-clock", "solana-cluster-type", "solana-commitment-config", @@ -5821,6 +5986,7 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bs58", "bytes", "chrono", @@ -5849,6 +6015,7 @@ dependencies = [ "slab", "solana-accounts-db", "solana-bloom", + "solana-bls-signatures", "solana-builtins-default-costs", "solana-client", "solana-compute-budget", @@ -5890,6 +6057,8 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-votor", + "solana-votor-messages", "solana-wen-restart", "strum", "strum_macros", @@ -5943,6 +6112,10 @@ dependencies = [ "solana-stable-layout", ] +[[package]] +name = "solana-curve-traits" +version = "2.3.0" + [[package]] name = "solana-curve25519" version = "2.3.0" @@ -5950,6 +6123,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", + "solana-curve-traits", "solana-define-syscall", "subtle", "thiserror 2.0.12", @@ -6540,6 +6714,7 @@ dependencies = [ "solana-transaction-status", "solana-vote", "solana-vote-program", + "solana-votor-messages", "spl-token", "spl-token-2022 7.0.0", "static_assertions", @@ -6816,6 +6991,8 @@ dependencies = [ "solana-short-vec", "solana-signature", "solana-time-utils", + "solana-vote", + "solana-votor-messages", ] [[package]] @@ -6834,6 +7011,7 @@ dependencies = [ "solana-poh-config", "solana-pubkey", "solana-runtime", + "solana-sdk", "solana-time-utils", "solana-transaction", "thiserror 2.0.12", @@ -7473,6 +7651,7 @@ dependencies = [ "num-traits", "num_cpus", "num_enum", + "parking_lot 0.12.2", "percentage", "qualifier_attr", "rand 0.8.5", @@ -7483,8 +7662,10 @@ dependencies = [ "serde_json", "serde_with", "solana-accounts-db", + "solana-bls-signatures", "solana-bpf-loader-program", "solana-bucket-map", + "solana-build-alpenglow-vote", "solana-builtins", "solana-compute-budget", "solana-compute-budget-instruction", @@ -7494,6 +7675,7 @@ dependencies = [ "solana-fee", "solana-inline-spl", "solana-lattice-hash", + "solana-loader-v3-interface", "solana-measure", "solana-metrics", "solana-nohash-hasher", @@ -7517,6 +7699,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-votor-messages", "static_assertions", "strum", "strum_macros", @@ -7543,6 +7726,8 @@ dependencies = [ "solana-svm-transaction", "solana-transaction", "solana-transaction-error", + "solana-vote", + "solana-votor-messages", "thiserror 2.0.12", ] @@ -8313,12 +8498,12 @@ dependencies = [ [[package]] name = "solana-signature" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d251c8f3dc015f320b4161daac7f108156c837428e5a8cc61136d25beb11d6" +checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" dependencies = [ - "bs58", "ed25519-dalek", + "five8", "rand 0.8.5", "serde", "serde-big-array", @@ -8418,7 +8603,9 @@ dependencies = [ "solana-sysvar", "solana-transaction-context 2.3.0", "solana-type-overrides", + "solana-vote", "solana-vote-interface", + "solana-votor-messages", ] [[package]] @@ -8704,6 +8891,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-compute-budget", "solana-core", @@ -9013,6 +9201,7 @@ dependencies = [ "solana-sdk", "solana-streamer", "solana-tls-utils", + "solana-votor", "static_assertions", "thiserror 2.0.12", "tokio", @@ -9103,24 +9292,36 @@ dependencies = [ name = "solana-vote" version = "2.3.0" dependencies = [ + "bincode", + "bitvec", + "bytemuck", "itertools 0.12.1", "log", + "num-derive", + "num-traits", + "num_enum", + "rand 0.8.5", "serde", "serde_derive", "solana-account", "solana-bincode", + "solana-bls-signatures", "solana-clock", "solana-hash", "solana-instruction", "solana-keypair", "solana-packet", + "solana-program", "solana-pubkey", "solana-sdk-ids", + "solana-serialize-utils", "solana-signature", "solana-signer", "solana-svm-transaction", "solana-transaction", "solana-vote-interface", + "solana-votor-messages", + "spl-pod", "thiserror 2.0.12", ] @@ -9180,6 +9381,61 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "solana-votor" +version = "2.3.0" +dependencies = [ + "anyhow", + "bincode", + "bitvec", + "bs58", + "crossbeam-channel", + "dashmap", + "etcd-client", + "itertools 0.12.1", + "log", + "qualifier_attr", + "rayon", + "serde", + "serde_bytes", + "serde_derive", + "solana-accounts-db", + "solana-bloom", + "solana-bls-signatures", + "solana-entry", + "solana-gossip", + "solana-ledger", + "solana-logger", + "solana-measure", + "solana-metrics", + "solana-pubkey", + "solana-rpc", + "solana-runtime", + "solana-sdk", + "solana-vote", + "solana-vote-program", + "solana-votor-messages", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-votor-messages" +version = "2.3.0" +dependencies = [ + "bitvec", + "bytemuck", + "num_enum", + "serde", + "solana-account", + "solana-bls-signatures", + "solana-hash", + "solana-logger", + "solana-program", + "solana-sdk", + "solana-vote-interface", + "spl-pod", +] + [[package]] name = "solana-wen-restart" version = "2.3.0" @@ -9850,6 +10106,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -9983,6 +10245,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tikv-jemalloc-sys" version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" @@ -10227,11 +10498,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.12", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -10241,7 +10527,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap 2.8.0", "toml_datetime", - "winnow", + "winnow 0.5.25", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap 2.8.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.26", ] [[package]] @@ -11116,6 +11415,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -11147,6 +11455,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index a6435910dd..69c46a90df 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -1431,12 +1431,14 @@ fn get_stable_genesis_config() -> GenesisConfigInfo { &validator_pubkey, &voting_keypair.pubkey(), &stake_pubkey, + None, bootstrap_validator_stake_lamports(), 42, FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees Rent::free(), // most tests don't expect rent ClusterType::Development, vec![], + None, ); genesis_config.creation_time = Duration::ZERO.as_secs() as UnixTimestamp; diff --git a/programs/stake/Cargo.toml b/programs/stake/Cargo.toml index fd61854ad7..b164c7e64d 100644 --- a/programs/stake/Cargo.toml +++ b/programs/stake/Cargo.toml @@ -30,7 +30,9 @@ solana-stake-interface = { workspace = true } solana-sysvar = { workspace = true } solana-transaction-context = { workspace = true, features = ["bincode"] } solana-type-overrides = { workspace = true } +solana-vote = { workspace = true } solana-vote-interface = { workspace = true, features = ["bincode"] } +solana-votor-messages = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 5d0550886e..94efd8f1d1 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -29,6 +29,7 @@ use { BorrowedAccount, IndexOfAccount, InstructionContext, TransactionContext, }, solana_vote_interface::state::{VoteState, VoteStateVersions}, + solana_votor_messages::state::VoteState as AlpenglowVoteState, std::{collections::HashSet, convert::TryFrom}, }; @@ -213,6 +214,18 @@ pub(crate) fn new_stake( } } +pub(crate) fn new_stake_with_credits( + stake: u64, + voter_pubkey: &Pubkey, + credits: u64, + activation_epoch: Epoch, +) -> Stake { + Stake { + delegation: Delegation::new(voter_pubkey, stake, activation_epoch), + credits_observed: credits, + } +} + pub fn initialize( stake_account: &mut BorrowedAccount, authorized: &Authorized, @@ -1416,7 +1429,16 @@ fn do_create_account( ) -> AccountSharedData { let mut stake_account = AccountSharedData::new(lamports, StakeStateV2::size_of(), &id()); - let vote_state = VoteState::deserialize(vote_account.data()).expect("vote_state"); + let credits = if solana_votor_messages::check_id(vote_account.owner()) { + AlpenglowVoteState::deserialize(vote_account.data()) + .expect("alpenglow_vote_state") + .epoch_credits() + .credits() + } else { + VoteState::deserialize(vote_account.data()) + .expect("vote_state") + .credits() + }; let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len()); @@ -1427,10 +1449,10 @@ fn do_create_account( rent_exempt_reserve, ..Meta::default() }, - new_stake( + new_stake_with_credits( lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve); voter_pubkey, - &vote_state, + credits, activation_epoch, ), StakeFlags::empty(), diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 7d509fc80f..4cfb5df7fd 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -4521,7 +4521,7 @@ impl RpcClient { let result: Response>> = self .send( RpcRequest::GetSignatureStatuses, - json!([[signature.to_string()]]), + json!([[signature.to_string()], {"searchTransactionHistory": true}]), ) .await?; diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index f4abce3b08..c68ed75290 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -206,6 +206,8 @@ impl JsonRpcConfig { Self { full_api: true, disable_health_check: true, + // Alpenglow requires this to serve transaction signatures + enable_rpc_transaction_history: true, ..Self::default() } } @@ -1181,32 +1183,25 @@ impl JsonRpcRequestProcessor { } } - let vote_state = account.vote_state(); - let last_vote = if let Some(vote) = vote_state.votes.iter().last() { - vote.slot() - } else { - 0 - }; - - let epoch_credits = vote_state.epoch_credits(); - let epoch_credits = if epoch_credits.len() - > MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY - { - epoch_credits - .iter() - .skip(epoch_credits.len() - MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY) - .cloned() - .collect() - } else { - epoch_credits.clone() - }; + // TODO(wen): make this work for Alpenglow + let vote_state_view = account.vote_state_view()?; + let last_vote = vote_state_view.last_voted_slot().unwrap_or(0); + let num_epoch_credits = vote_state_view.num_epoch_credits(); + let epoch_credits = vote_state_view + .epoch_credits_iter() + .skip( + num_epoch_credits + .saturating_sub(MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY), + ) + .map(Into::into) + .collect(); Some(RpcVoteAccountInfo { vote_pubkey: vote_pubkey.to_string(), - node_pubkey: vote_state.node_pubkey.to_string(), + node_pubkey: vote_state_view.node_pubkey().to_string(), activated_stake: *activated_stake, - commission: vote_state.commission, - root_slot: vote_state.root_slot.unwrap_or(0), + commission: vote_state_view.commission(), + root_slot: vote_state_view.root_slot().unwrap_or(0), epoch_credits, epoch_vote_account: epoch_vote_accounts.contains_key(vote_pubkey), last_vote, diff --git a/runtime-transaction/Cargo.toml b/runtime-transaction/Cargo.toml index 438f53dce4..705c0ace82 100644 --- a/runtime-transaction/Cargo.toml +++ b/runtime-transaction/Cargo.toml @@ -22,6 +22,8 @@ solana-signature = { workspace = true } solana-svm-transaction = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } +solana-vote = { workspace = true } +solana-votor-messages = { workspace = true } thiserror = { workspace = true } [lib] diff --git a/runtime-transaction/src/runtime_transaction/transaction_view.rs b/runtime-transaction/src/runtime_transaction/transaction_view.rs index 0a09fc2c31..0eeee89144 100644 --- a/runtime-transaction/src/runtime_transaction/transaction_view.rs +++ b/runtime-transaction/src/runtime_transaction/transaction_view.rs @@ -38,6 +38,24 @@ fn is_simple_vote_transaction( is_simple_vote_transaction_impl(signatures, is_legacy_message, instruction_programs) } +fn is_alpenglow_simple_vote_transaction( + transaction: &SanitizedTransactionView, +) -> bool { + let signatures = transaction.signatures(); + let is_legacy_message = matches!(transaction.version(), TransactionVersion::Legacy); + let mut instruction_programs = transaction + .program_instructions_iter() + .map(|(program_id, _ix)| program_id); + + signatures.len() < 3 + && is_legacy_message + && instruction_programs + .next() + .xor(instruction_programs.next()) + .map(|program_id| program_id == &solana_votor_messages::ID) + .unwrap_or(false) +} + impl RuntimeTransaction> { pub fn try_from( transaction: SanitizedTransactionView, @@ -48,8 +66,10 @@ impl RuntimeTransaction> { MessageHash::Precomputed(hash) => hash, MessageHash::Compute => VersionedMessage::hash_raw_message(transaction.message_data()), }; - let is_simple_vote_tx = - is_simple_vote_tx.unwrap_or_else(|| is_simple_vote_transaction(&transaction)); + let is_simple_vote_tx = is_simple_vote_tx.unwrap_or_else(|| { + is_simple_vote_transaction(&transaction) + || is_alpenglow_simple_vote_transaction(&transaction) + }); let precompile_signature_details = get_precompile_signature_details(transaction.program_instructions_iter()); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b66cc53fb0..12f26431f9 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -37,6 +37,7 @@ num-derive = { workspace = true } num-traits = { workspace = true } num_cpus = { workspace = true } num_enum = { workspace = true } +parking_lot = { workspace = true } percentage = { workspace = true } qualifier_attr = { workspace = true } rand = { workspace = true } @@ -47,8 +48,10 @@ serde_derive = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["serde"] } solana-bpf-loader-program = { workspace = true } solana-bucket-map = { workspace = true } +solana-build-alpenglow-vote = { workspace = true } solana-builtins = { workspace = true } solana-compute-budget = { workspace = true } solana-compute-budget-instruction = { workspace = true } @@ -64,6 +67,7 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-inline-spl = { workspace = true } solana-lattice-hash = { workspace = true } +solana-loader-v3-interface = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } solana-nohash-hasher = { workspace = true } @@ -87,6 +91,7 @@ solana-unified-scheduler-logic = { workspace = true } solana-version = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-votor-messages = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } @@ -132,11 +137,13 @@ dev-context-only-utils = [ "dep:solana-system-program", "solana-svm/dev-context-only-utils", "solana-runtime-transaction/dev-context-only-utils", + "solana-vote/dev-context-only-utils", ] frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", "solana-accounts-db/frozen-abi", + "solana-bls-signatures/frozen-abi", "solana-compute-budget/frozen-abi", "solana-cost-model/frozen-abi", "solana-perf/frozen-abi", diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index ab96447650..902cf6f2bd 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -33,6 +33,7 @@ //! It offers a high-level API that signs transactions //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. + use { crate::{ account_saver::collect_accounts_to_store, @@ -1542,6 +1543,19 @@ impl Bank { .prune(new_root_slot, new_root_epoch); } + pub fn prune_program_cache_locked( + &self, + new_root_slot: Slot, + new_root_epoch: Epoch, + bank_forks: &BankForks, + ) { + self.transaction_processor + .program_cache + .write() + .unwrap() + .prune_locked(new_root_slot, new_root_epoch, bank_forks); + } + pub fn prune_program_cache_by_deployment_slot(&self, deployment_slot: Slot) { self.transaction_processor .program_cache @@ -2503,17 +2517,10 @@ impl Bank { let slots_per_epoch = self.epoch_schedule().slots_per_epoch; let vote_accounts = self.vote_accounts(); let recent_timestamps = vote_accounts.iter().filter_map(|(pubkey, (_, account))| { - let vote_state = account.vote_state(); - let slot_delta = self.slot().checked_sub(vote_state.last_timestamp.slot)?; - (slot_delta <= slots_per_epoch).then_some({ - ( - *pubkey, - ( - vote_state.last_timestamp.slot, - vote_state.last_timestamp.timestamp, - ), - ) - }) + let last_timestamp = account.last_timestamp(); + let slot_delta = self.slot().checked_sub(last_timestamp.slot)?; + (slot_delta <= slots_per_epoch) + .then_some((*pubkey, (last_timestamp.slot, last_timestamp.timestamp))) }); let slot_duration = Duration::from_nanos(self.ns_per_slot as u64); let epoch = self.epoch_schedule().get_epoch(self.slot()); @@ -6051,6 +6058,10 @@ impl Bank { self.tick_height.load(Relaxed) } + pub fn set_tick_height(&self, tick_height: u64) { + self.tick_height.store(tick_height, Relaxed) + } + /// Return the inflation parameters of the Bank pub fn inflation(&self) -> Inflation { *self.inflation.read().unwrap() @@ -6815,6 +6826,10 @@ impl Bank { &self.fee_structure } + pub fn parent_block_id(&self) -> Option { + self.parent().and_then(|p| p.block_id()) + } + pub fn block_id(&self) -> Option { *self.block_id.read().unwrap() } diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index 8b28844ffd..f51c5718f7 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -369,16 +369,17 @@ impl Bank { let stake_pubkey = **stake_pubkey; let vote_pubkey = stake_account.delegation().voter_pubkey; let vote_account = get_vote_account(&vote_pubkey)?; - if vote_account.owner() != &solana_vote_program { + if vote_account.owner() != &solana_vote_program + && !solana_votor_messages::check_id(vote_account.owner()) + { return None; } - let vote_state = vote_account.vote_state(); let mut stake_state = *stake_account.stake_state(); let redeemed = redeem_rewards( rewarded_epoch, &mut stake_state, - vote_state, + &vote_account, &point_value, stake_history, reward_calc_tracer.as_ref(), @@ -386,7 +387,7 @@ impl Bank { ); if let Ok((stakers_reward, voters_reward)) = redeemed { - let commission = vote_state.commission; + let commission = vote_account.commission(); // track voter rewards let mut voters_reward_entry = vote_account_rewards @@ -478,13 +479,14 @@ impl Bank { let Some(vote_account) = get_vote_account(&vote_pubkey) else { return 0; }; - if vote_account.owner() != &solana_vote_program { + if vote_account.owner() != &solana_vote_program + && !solana_votor_messages::check_id(vote_account.owner()) + { return 0; } - calculate_points( stake_account.stake_state(), - vote_account.vote_state(), + &vote_account, stake_history, new_warmup_cooldown_rate_epoch, ) diff --git a/runtime/src/bank/serde_snapshot.rs b/runtime/src/bank/serde_snapshot.rs index c359de7561..9bd131f250 100644 --- a/runtime/src/bank/serde_snapshot.rs +++ b/runtime/src/bank/serde_snapshot.rs @@ -568,7 +568,7 @@ mod tests { #[cfg_attr( feature = "frozen-abi", derive(AbiExample), - frozen_abi(digest = "3PsrjAtyWBU3KPopGoM1UK1sa8HjVzehjBi7M2v6wW1Q") + frozen_abi(digest = "3zNnweAk98vCV4H3YcdWGfXBvGGxFenn61vFxHrCSVUh") )] #[derive(Serialize)] pub struct BankAbiTestWrapper { diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index af966aa80c..9451f1dcf5 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -1906,12 +1906,14 @@ fn test_rent_eager_collect_rent_zero_lamport_deterministic() { assert_ne!(hash2_with_zero, Hash::default()); } -#[test] -fn test_bank_update_vote_stake_rewards() { +#[test_case(true; "alpenglow")] +#[test_case(false; "towerbft")] +fn test_bank_update_vote_stake_rewards(is_alpenglow: bool) { let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); - check_bank_update_vote_stake_rewards(|bank: &Bank| { - bank._load_vote_and_stake_accounts(&thread_pool, null_tracer()) - }); + check_bank_update_vote_stake_rewards( + |bank: &Bank| bank._load_vote_and_stake_accounts(&thread_pool, null_tracer()), + is_alpenglow, + ); } impl Bank { @@ -2011,7 +2013,7 @@ type StakeDelegations = Vec<(Pubkey, StakeAccount)>; type StakeDelegationsMap = DashMap; #[cfg(test)] -fn check_bank_update_vote_stake_rewards(load_vote_and_stake_accounts: F) +fn check_bank_update_vote_stake_rewards(load_vote_and_stake_accounts: F, is_alpenglow: bool) where F: Fn(&Bank) -> StakeDelegationsMap, { @@ -2050,27 +2052,39 @@ where ); let ((vote_id, mut vote_account), (stake_id, stake_account)) = - crate::stakes::tests::create_staked_node_accounts(10_000); + crate::stakes::tests::create_staked_node_accounts(10_000, is_alpenglow); let starting_vote_and_stake_balance = 10_000 + 1; // set up accounts bank0.store_account_and_update_capitalization(&stake_id, &stake_account); // generate some rewards - let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); - for i in 0..MAX_LOCKOUT_HISTORY + 42 { - if let Some(v) = vote_state.as_mut() { - vote_state::process_slot_vote_unchecked(v, i as u64) + if is_alpenglow { + let mut vote_state = + *solana_votor_messages::state::VoteState::deserialize(vote_account.data()).unwrap(); + for _ in 0..MAX_LOCKOUT_HISTORY + 42 { + let mut epoch_credits = *vote_state.epoch_credits(); + epoch_credits.set_credits(epoch_credits.credits() + 16); + vote_state.set_epoch_credits(epoch_credits); + vote_state.serialize_into(vote_account.data_as_mut_slice()); + bank0.store_account_and_update_capitalization(&vote_id, &vote_account); } - let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - vote_state::to(&versioned, &mut vote_account).unwrap(); - bank0.store_account_and_update_capitalization(&vote_id, &vote_account); - match versioned { - VoteStateVersions::Current(v) => { - vote_state = Some(*v); + } else { + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); + for i in 0..MAX_LOCKOUT_HISTORY + 42 { + if let Some(v) = vote_state.as_mut() { + vote_state::process_slot_vote_unchecked(v, i as u64) } - _ => panic!("Has to be of type Current"), - }; + let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); + vote_state::to(&versioned, &mut vote_account).unwrap(); + bank0.store_account_and_update_capitalization(&vote_id, &vote_account); + match versioned { + VoteStateVersions::Current(v) => { + vote_state = Some(*v); + } + _ => panic!("Has to be of type Current"), + }; + } } bank0.store_account_and_update_capitalization(&vote_id, &vote_account); bank0.freeze(); @@ -4897,8 +4911,7 @@ fn test_add_duplicate_static_program() { ); } -#[test] -fn test_add_instruction_processor_for_existing_unrelated_accounts() { +fn test_add_instruction_processor_for_existing_unrelated_accounts(is_alpenglow: bool) { for pass in 0..5 { let mut bank = create_simple_test_bank(500); @@ -4919,7 +4932,7 @@ fn test_add_instruction_processor_for_existing_unrelated_accounts() { } let ((vote_id, vote_account), (stake_id, stake_account)) = - crate::stakes::tests::create_staked_node_accounts(1_0000); + crate::stakes::tests::create_staked_node_accounts(1_0000, is_alpenglow); bank.capitalization .fetch_add(vote_account.lamports() + stake_account.lamports(), Relaxed); bank.store_account(&vote_id, &vote_account); @@ -4992,6 +5005,12 @@ fn test_add_instruction_processor_for_existing_unrelated_accounts() { } } +#[test] +fn test_add_instruction_processor_for_existing_unrelated_accounts_tests() { + test_add_instruction_processor_for_existing_unrelated_accounts(false); + test_add_instruction_processor_for_existing_unrelated_accounts(true); +} + #[allow(deprecated)] #[test] fn test_recent_blockhashes_sysvar() { @@ -12935,7 +12954,7 @@ fn test_last_restart_slot() { let GenesisConfigInfo { mut genesis_config, .. } = create_genesis_config_with_leader(mint_lamports, &leader_pubkey, validator_stake_lamports); - // Remove last restart slot account so we can simluate its' activation + // Remove last restart slot account so we can simulate its' activation genesis_config .accounts .remove(&feature_set::last_restart_slot_sysvar::id()) diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index 4270fab7ec..16fbe65fbf 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -10,7 +10,7 @@ use { }, snapshot_config::SnapshotConfig, }, - crossbeam_channel::SendError, + crossbeam_channel::{SendError, Sender}, log::*, solana_measure::measure::Measure, solana_program_runtime::loaded_programs::{BlockRelation, ForkGraph}, @@ -86,6 +86,8 @@ pub struct BankForks { scheduler_pool: Option, dumped_slot_subscribers: Vec, + /// Tracks subscribers interested in hearing about new `Bank`s. + new_bank_subscribers: Vec>>, } impl Index for BankForks { @@ -137,6 +139,7 @@ impl BankForks { highest_slot_at_startup: 0, scheduler_pool: None, dumped_slot_subscribers: vec![], + new_bank_subscribers: vec![], })); root_bank.set_fork_graph_in_program_cache(Arc::downgrade(&bank_forks)); @@ -216,6 +219,10 @@ impl BankForks { self.get(slot).map(|bank| bank.hash()) } + pub fn is_frozen(&self, slot: Slot) -> bool { + self.get(slot).map(|bank| bank.is_frozen()).unwrap_or(false) + } + pub fn root_bank(&self) -> Arc { self[self.root()].clone() } @@ -293,6 +300,20 @@ impl BankForks { Some(bank) } + pub fn highest_frozen_bank(&self) -> Option> { + self.banks + .values() + .filter_map(|bank| { + if bank.is_frozen() { + Some(bank.slot()) + } else { + None + } + }) + .max() + .and_then(|slot| self.get(slot)) + } + pub fn highest_slot(&self) -> Slot { self.banks.values().map(|bank| bank.slot()).max().unwrap() } @@ -311,6 +332,24 @@ impl BankForks { self.dumped_slot_subscribers.push(notifier); } + /// Register a new subscriber interested in hearing about new `Bank`s. + pub fn register_new_bank_subscriber(&mut self, tx: Sender>) { + self.new_bank_subscribers.push(tx); + } + + /// Call to notify subscribers of new `Bank`s. + fn notify_new_bank_subscribers(&mut self, root_bank: &Arc) { + let mut channels_to_drop = vec![]; + for (ind, tx) in self.new_bank_subscribers.iter().enumerate() { + if let Err(SendError(_)) = tx.send(root_bank.clone()) { + channels_to_drop.push(ind); + } + } + for ind in channels_to_drop { + self.new_bank_subscribers.remove(ind); + } + } + /// Clears associated banks from BankForks and notifies subscribers that a dump has occured. pub fn dump_slots<'a, I>(&mut self, slots: I) -> (Vec<(Slot, BankId)>, Vec) where @@ -426,6 +465,7 @@ impl BankForks { .unwrap() .node_id_to_vote_accounts() ); + self.notify_new_bank_subscribers(root_bank); } let root_tx_count = root_bank .parents() @@ -524,7 +564,7 @@ impl BankForks { pub fn prune_program_cache(&self, root: Slot) { if let Some(root_bank) = self.banks.get(&root) { - root_bank.prune_program_cache(root, root_bank.epoch()); + root_bank.prune_program_cache_locked(root, root_bank.epoch(), self); } } diff --git a/runtime/src/bank_utils.rs b/runtime/src/bank_utils.rs index 837eae0205..cd636a56d8 100644 --- a/runtime/src/bank_utils.rs +++ b/runtime/src/bank_utils.rs @@ -44,15 +44,18 @@ pub fn find_and_send_votes( commit_results: &[TransactionCommitResult], vote_sender: Option<&ReplayVoteSender>, ) { - if let Some(vote_sender) = vote_sender { + if vote_sender.is_some() { sanitized_txs .iter() .zip(commit_results.iter()) .for_each(|(tx, commit_result)| { if tx.is_simple_vote_transaction() && commit_result.was_executed_successfully() { - if let Some(parsed_vote) = vote_parser::parse_sanitized_vote_transaction(tx) { - if parsed_vote.1.last_voted_slot().is_some() { - let _ = vote_sender.send(parsed_vote); + if let Some(vote_sender) = vote_sender { + if let Some(parsed_vote) = vote_parser::parse_sanitized_vote_transaction(tx) + { + if parsed_vote.1.last_voted_slot().is_some() { + let _ = vote_sender.send(parsed_vote); + } } } } diff --git a/runtime/src/commitment.rs b/runtime/src/commitment.rs index 632e4bda3d..fac3465244 100644 --- a/runtime/src/commitment.rs +++ b/runtime/src/commitment.rs @@ -180,12 +180,22 @@ impl BlockCommitmentCache { } } + pub fn set_slot(&mut self, slot: Slot) { + self.commitment_slots.slot = std::cmp::max(self.commitment_slots.slot, slot); + } + pub fn set_highest_confirmed_slot(&mut self, slot: Slot) { - self.commitment_slots.highest_confirmed_slot = slot; + self.commitment_slots.highest_confirmed_slot = + std::cmp::max(self.commitment_slots.highest_confirmed_slot, slot); } - pub fn set_highest_super_majority_root(&mut self, root: Slot) { - self.commitment_slots.highest_super_majority_root = root; + pub fn set_root(&mut self, slot: Slot) { + self.commitment_slots.root = std::cmp::max(self.commitment_slots.root, slot); + } + + pub fn set_highest_super_majority_root(&mut self, slot: Slot) { + self.commitment_slots.highest_super_majority_root = + std::cmp::max(self.commitment_slots.highest_super_majority_root, slot); } pub fn initialize_slots(&mut self, slot: Slot, root: Slot) { @@ -337,4 +347,28 @@ mod tests { assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 0); } + + #[test] + fn test_setters_getters() { + let mut block_commitment_cache = BlockCommitmentCache::default(); + // Setting bigger slots should be ok + block_commitment_cache.set_slot(1); + assert_eq!(block_commitment_cache.slot(), 1); + block_commitment_cache.set_highest_confirmed_slot(2); + assert_eq!(block_commitment_cache.highest_confirmed_slot(), 2); + block_commitment_cache.set_root(3); + assert_eq!(block_commitment_cache.root(), 3); + block_commitment_cache.set_highest_super_majority_root(4); + assert_eq!(block_commitment_cache.highest_super_majority_root(), 4); + + // Setting smaller slots shuold be ignored + block_commitment_cache.set_slot(0); + assert_eq!(block_commitment_cache.slot(), 1); + block_commitment_cache.set_highest_confirmed_slot(1); + assert_eq!(block_commitment_cache.highest_confirmed_slot(), 2); + block_commitment_cache.set_root(2); + assert_eq!(block_commitment_cache.root(), 3); + block_commitment_cache.set_highest_super_majority_root(3); + assert_eq!(block_commitment_cache.highest_super_majority_root(), 4); + } } diff --git a/runtime/src/epoch_stakes.rs b/runtime/src/epoch_stakes.rs index 8a5438712c..350da97ca3 100644 --- a/runtime/src/epoch_stakes.rs +++ b/runtime/src/epoch_stakes.rs @@ -1,6 +1,7 @@ use { crate::stakes::{serde_stakes_to_delegation_format, SerdeStakesToStakeFormat, StakesEnum}, serde::{Deserialize, Serialize}, + solana_bls_signatures::Pubkey as BLSPubkey, solana_sdk::{clock::Epoch, pubkey::Pubkey}, solana_vote::vote_account::VoteAccountsHashMap, std::{collections::HashMap, sync::Arc}, @@ -8,6 +9,63 @@ use { pub type NodeIdToVoteAccounts = HashMap; pub type EpochAuthorizedVoters = HashMap; +pub type SortedPubkeys = Vec<(Pubkey, BLSPubkey)>; + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] +pub struct BLSPubkeyToRankMap { + rank_map: HashMap, + //TODO(wen): We can make SortedPubkeys a Vec after we remove ed25519 + // pubkey from certificate pool. + sorted_pubkeys: Vec<(Pubkey, BLSPubkey)>, +} + +impl BLSPubkeyToRankMap { + pub fn new(epoch_vote_accounts_hash_map: &VoteAccountsHashMap) -> Self { + let mut pubkey_stake_pair_vec: Vec<(Pubkey, BLSPubkey, u64)> = epoch_vote_accounts_hash_map + .iter() + .filter_map(|(pubkey, (stake, account))| { + if *stake > 0 { + account + .bls_pubkey() + .map(|bls_pubkey| (*pubkey, *bls_pubkey, *stake)) + } else { + None + } + }) + .collect(); + pubkey_stake_pair_vec.sort_by(|(_, a_pubkey, a_stake), (_, b_pubkey, b_stake)| { + b_stake.cmp(a_stake).then(a_pubkey.cmp(b_pubkey)) + }); + let mut sorted_pubkeys = Vec::new(); + let mut bls_pubkey_to_rank_map = HashMap::new(); + for (rank, (pubkey, bls_pubkey, _stake)) in pubkey_stake_pair_vec.into_iter().enumerate() { + sorted_pubkeys.push((pubkey, bls_pubkey)); + bls_pubkey_to_rank_map.insert(bls_pubkey, rank as u16); + } + Self { + rank_map: bls_pubkey_to_rank_map, + sorted_pubkeys, + } + } + + pub fn is_empty(&self) -> bool { + self.rank_map.is_empty() + } + + pub fn len(&self) -> usize { + self.rank_map.len() + } + + pub fn get_rank(&self, bls_pubkey: &BLSPubkey) -> Option<&u16> { + self.rank_map.get(bls_pubkey) + } + + pub fn get_pubkey(&self, index: usize) -> Option<&(Pubkey, BLSPubkey)> { + self.sorted_pubkeys.get(index) + } +} #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone, Serialize, Debug, Deserialize, Default, PartialEq, Eq)] @@ -25,6 +83,7 @@ pub struct EpochStakes { total_stake: u64, node_id_to_vote_accounts: Arc, epoch_authorized_voters: Arc, + bls_pubkey_to_rank_map: Arc, } impl EpochStakes { @@ -32,11 +91,13 @@ impl EpochStakes { let epoch_vote_accounts = stakes.vote_accounts(); let (total_stake, node_id_to_vote_accounts, epoch_authorized_voters) = Self::parse_epoch_vote_accounts(epoch_vote_accounts.as_ref(), leader_schedule_epoch); + let bls_pubkey_to_rank_map = BLSPubkeyToRankMap::new(epoch_vote_accounts.as_ref()); Self { stakes, total_stake, node_id_to_vote_accounts: Arc::new(node_id_to_vote_accounts), epoch_authorized_voters: Arc::new(epoch_authorized_voters), + bls_pubkey_to_rank_map: Arc::new(bls_pubkey_to_rank_map), } } @@ -82,6 +143,10 @@ impl EpochStakes { &self.epoch_authorized_voters } + pub fn bls_pubkey_to_rank_map(&self) -> &Arc { + &self.bls_pubkey_to_rank_map + } + pub fn vote_account_stake(&self, vote_account: &Pubkey) -> u64 { self.stakes .vote_accounts() @@ -100,15 +165,12 @@ impl EpochStakes { let epoch_authorized_voters = epoch_vote_accounts .iter() .filter_map(|(key, (stake, account))| { - let vote_state = account.vote_state(); - if *stake > 0 { - if let Some(authorized_voter) = vote_state - .authorized_voters() - .get_authorized_voter(leader_schedule_epoch) + if let Some(authorized_voter) = + account.get_authorized_voter(leader_schedule_epoch) { let node_vote_accounts = node_id_to_vote_accounts - .entry(vote_state.node_pubkey) + .entry(*account.node_pubkey()) .or_default(); node_vote_accounts.total_stake += stake; @@ -152,11 +214,14 @@ impl From for EpochStakes { epoch_authorized_voters, } = versioned; + let stakes: Arc = Arc::new(stakes.into()); + let bls_pubkey_to_rank_map = BLSPubkeyToRankMap::new(stakes.vote_accounts().as_ref()); Self { - stakes: Arc::new(stakes.into()), + stakes, total_stake, node_id_to_vote_accounts, epoch_authorized_voters, + bls_pubkey_to_rank_map: Arc::new(bls_pubkey_to_rank_map), } } } @@ -181,6 +246,7 @@ pub(crate) fn split_epoch_stakes( total_stake, node_id_to_vote_accounts, epoch_authorized_voters, + bls_pubkey_to_rank_map, } = epoch_stakes; match stakes.as_ref() { StakesEnum::Delegations(_) => { @@ -191,6 +257,7 @@ pub(crate) fn split_epoch_stakes( total_stake, node_id_to_vote_accounts, epoch_authorized_voters, + bls_pubkey_to_rank_map, }, ); } @@ -229,11 +296,14 @@ pub(crate) mod tests { stake_account::StakeAccount, stakes::{Stakes, StakesCache}, }, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_sdk::{account::AccountSharedData, rent::Rent}, solana_stake_program::stake_state::{self, Delegation, Stake}, solana_vote::vote_account::VoteAccount, solana_vote_program::vote_state::{self, create_account_with_authorized}, + solana_votor_messages::state::VoteState as AlpenglowVoteState, std::iter, + test_case::test_case, }; struct VoteAccountInfo { @@ -245,6 +315,7 @@ pub(crate) mod tests { fn new_vote_accounts( num_nodes: usize, num_vote_accounts_per_node: usize, + is_alpenglow: bool, ) -> HashMap> { // Create some vote accounts for each pubkey (0..num_nodes) @@ -254,15 +325,28 @@ pub(crate) mod tests { node_id, iter::repeat_with(|| { let authorized_voter = solana_pubkey::new_rand(); - VoteAccountInfo { - vote_account: solana_pubkey::new_rand(), - account: create_account_with_authorized( + let bls_keypair = BLSKeypair::new(); + let account = if is_alpenglow { + AlpenglowVoteState::create_account_with_authorized( + &node_id, + &authorized_voter, + &node_id, + 0, + 100, + bls_keypair.public.into(), + ) + } else { + create_account_with_authorized( &node_id, &authorized_voter, &node_id, 0, 100, - ), + ) + }; + VoteAccountInfo { + vote_account: solana_pubkey::new_rand(), + account, authorized_voter, } }) @@ -289,13 +373,15 @@ pub(crate) mod tests { .collect() } - #[test] - fn test_parse_epoch_vote_accounts() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_parse_epoch_vote_accounts(is_alpenglow: bool) { let stake_per_account = 100; let num_vote_accounts_per_node = 2; let num_nodes = 10; - let vote_accounts_map = new_vote_accounts(num_nodes, num_vote_accounts_per_node); + let vote_accounts_map = + new_vote_accounts(num_nodes, num_vote_accounts_per_node, is_alpenglow); let expected_authorized_voters: HashMap<_, _> = vote_accounts_map .iter() @@ -399,6 +485,7 @@ pub(crate) mod tests { total_stake: 100, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch, epoch_stakes.clone()); @@ -420,6 +507,7 @@ pub(crate) mod tests { total_stake: 100, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch, epoch_stakes.clone()); @@ -449,6 +537,7 @@ pub(crate) mod tests { total_stake: 100, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch, epoch_stakes.clone()); @@ -479,6 +568,7 @@ pub(crate) mod tests { total_stake: 100, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch1, epoch_stakes1); @@ -490,6 +580,7 @@ pub(crate) mod tests { total_stake: 200, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch2, epoch_stakes2); @@ -501,6 +592,7 @@ pub(crate) mod tests { total_stake: 300, node_id_to_vote_accounts: Arc::new(HashMap::new()), epoch_authorized_voters: Arc::new(HashMap::new()), + bls_pubkey_to_rank_map: Arc::new(BLSPubkeyToRankMap::default()), }; bank_epoch_stakes.insert(epoch3, epoch_stakes3); @@ -530,12 +622,14 @@ pub(crate) mod tests { ); } - #[test] - fn test_node_id_to_stake() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_node_id_to_stake(is_alpenglow: bool) { let num_nodes = 10; let num_vote_accounts_per_node = 2; - let vote_accounts_map = new_vote_accounts(num_nodes, num_vote_accounts_per_node); + let vote_accounts_map = + new_vote_accounts(num_nodes, num_vote_accounts_per_node, is_alpenglow); let node_id_to_stake_map = vote_accounts_map .keys() .enumerate() @@ -554,4 +648,42 @@ pub(crate) mod tests { ); } } + + #[test_case(1; "single_vote_account")] + #[test_case(2; "multiple_vote_accounts")] + fn test_bls_pubkey_rank_map(num_vote_accounts_per_node: usize) { + let num_nodes = 10; + let num_vote_accounts = num_nodes * num_vote_accounts_per_node; + + let vote_accounts_map = new_vote_accounts(num_nodes, num_vote_accounts_per_node, true); + let node_id_to_stake_map = vote_accounts_map + .keys() + .enumerate() + .map(|(index, node_id)| (*node_id, ((index + 1) * 100) as u64)) + .collect::>(); + let epoch_vote_accounts = new_epoch_vote_accounts(&vote_accounts_map, |node_id| { + *node_id_to_stake_map.get(node_id).unwrap() + }); + let epoch_stakes = EpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0); + let bls_pubkey_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); + assert_eq!(bls_pubkey_to_rank_map.len(), num_vote_accounts); + for (pubkey, (_, vote_account)) in epoch_vote_accounts { + let index = bls_pubkey_to_rank_map + .get_rank(vote_account.bls_pubkey().unwrap()) + .unwrap(); + assert!(index >= &0 && index < &(num_vote_accounts as u16)); + assert_eq!( + bls_pubkey_to_rank_map.get_pubkey(*index as usize), + Some(&(pubkey, *vote_account.bls_pubkey().unwrap())) + ); + } + + // Convert it to versioned and back, we should get the same rank map + let mut bank_epoch_stakes = HashMap::new(); + bank_epoch_stakes.insert(0, epoch_stakes.clone()); + let (_, versioned_epoch_stakes) = split_epoch_stakes(bank_epoch_stakes); + let epoch_stakes = EpochStakes::from(versioned_epoch_stakes.get(&0).unwrap().clone()); + let bls_pubkey_to_rank_map2 = epoch_stakes.bls_pubkey_to_rank_map(); + assert_eq!(bls_pubkey_to_rank_map2, bls_pubkey_to_rank_map); + } } diff --git a/runtime/src/epoch_stakes_service.rs b/runtime/src/epoch_stakes_service.rs new file mode 100644 index 0000000000..94d630479a --- /dev/null +++ b/runtime/src/epoch_stakes_service.rs @@ -0,0 +1,68 @@ +use { + crate::{ + bank::Bank, + epoch_stakes::{BLSPubkeyToRankMap, EpochStakes}, + }, + crossbeam_channel::Receiver, + log::warn, + parking_lot::RwLock as PlRwLock, + solana_sdk::{ + clock::{Epoch, Slot}, + epoch_schedule::EpochSchedule, + }, + std::{collections::HashMap, sync::Arc, thread}, +}; + +struct State { + stakes: HashMap, + epoch_schedule: EpochSchedule, +} + +impl State { + fn new(bank: Arc) -> Self { + Self { + stakes: bank.epoch_stakes_map().clone(), + epoch_schedule: bank.epoch_schedule().clone(), + } + } +} + +/// A service that regularly updates the epoch stakes state from `Bank`s +/// and exposes various methods to access the state. +pub struct EpochStakesService { + state: Arc>, +} + +impl EpochStakesService { + pub fn new(bank: Arc, epoch: Epoch, new_bank_receiver: Receiver>) -> Self { + let mut prev_epoch = epoch; + let state = Arc::new(PlRwLock::new(State::new(bank))); + { + let state = state.clone(); + thread::spawn(move || loop { + let bank = match new_bank_receiver.recv() { + Ok(b) => b, + Err(e) => { + warn!("recv() returned {e:?}. Exiting."); + break; + } + }; + let new_epoch = bank.epoch(); + if new_epoch > prev_epoch { + prev_epoch = new_epoch; + *state.write() = State::new(bank) + } + }); + } + Self { state } + } + + pub fn get_key_to_rank_map(&self, slot: Slot) -> Option> { + let guard = self.state.read(); + let epoch = guard.epoch_schedule.get_epoch(slot); + guard + .stakes + .get(&epoch) + .map(|stake| Arc::clone(stake.bls_pubkey_to_rank_map())) + } +} diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 161a7f07db..ca4db7978a 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -1,6 +1,8 @@ use { log::*, - solana_feature_set::{FeatureSet, FEATURE_NAMES}, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Pubkey as BLSPubkey}, + solana_feature_set::{self, FeatureSet, FEATURE_NAMES}, + solana_loader_v3_interface::state::UpgradeableLoaderState, solana_sdk::{ account::{Account, AccountSharedData}, feature::{self, Feature}, @@ -16,7 +18,10 @@ use { }, solana_stake_program::stake_state, solana_vote_program::vote_state, - std::borrow::Borrow, + solana_votor_messages::{ + self, bls_message::BLS_KEYPAIR_DERIVE_SEED, state::VoteState as AlpenglowVoteState, + }, + std::{borrow::Borrow, fs::File, io::Read}, }; // Default amount received by the validator @@ -50,14 +55,18 @@ pub struct ValidatorVoteKeypairs { pub node_keypair: Keypair, pub vote_keypair: Keypair, pub stake_keypair: Keypair, + pub bls_keypair: BLSKeypair, } impl ValidatorVoteKeypairs { pub fn new(node_keypair: Keypair, vote_keypair: Keypair, stake_keypair: Keypair) -> Self { + let bls_keypair = + BLSKeypair::derive_from_signer(&vote_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); Self { node_keypair, vote_keypair, stake_keypair, + bls_keypair, } } @@ -66,6 +75,7 @@ impl ValidatorVoteKeypairs { node_keypair: Keypair::new(), vote_keypair: Keypair::new(), stake_keypair: Keypair::new(), + bls_keypair: BLSKeypair::new(), } } } @@ -99,6 +109,22 @@ pub fn create_genesis_config_with_vote_accounts( voting_keypairs, stakes, ClusterType::Development, + None, + ) +} + +pub fn create_genesis_config_with_alpenglow_vote_accounts( + mint_lamports: u64, + voting_keypairs: &[impl Borrow], + stakes: Vec, + alpenglow_so_path: &str, +) -> GenesisConfigInfo { + create_genesis_config_with_vote_accounts_and_cluster_type( + mint_lamports, + voting_keypairs, + stakes, + ClusterType::Development, + Some(alpenglow_so_path), ) } @@ -107,6 +133,7 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type( voting_keypairs: &[impl Borrow], stakes: Vec, cluster_type: ClusterType, + alpenglow_so_path: Option<&str>, ) -> GenesisConfigInfo { assert!(!voting_keypairs.is_empty()); assert_eq!(voting_keypairs.len(), stakes.len()); @@ -121,12 +148,14 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type( &validator_pubkey, &voting_keypairs[0].borrow().vote_keypair.pubkey(), &voting_keypairs[0].borrow().stake_keypair.pubkey(), + Some(&voting_keypairs[0].borrow().bls_keypair.public.into()), stakes[0], VALIDATOR_LAMPORTS, FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees Rent::free(), // most tests don't expect rent cluster_type, vec![], + alpenglow_so_path, ); let mut genesis_config_info = GenesisConfigInfo { @@ -140,10 +169,22 @@ pub fn create_genesis_config_with_vote_accounts_and_cluster_type( let node_pubkey = validator_voting_keypairs.borrow().node_keypair.pubkey(); let vote_pubkey = validator_voting_keypairs.borrow().vote_keypair.pubkey(); let stake_pubkey = validator_voting_keypairs.borrow().stake_keypair.pubkey(); + let bls_pubkey = validator_voting_keypairs.borrow().bls_keypair.public.into(); // Create accounts let node_account = Account::new(VALIDATOR_LAMPORTS, 0, &system_program::id()); - let vote_account = vote_state::create_account(&vote_pubkey, &node_pubkey, 0, *stake); + let vote_account = if alpenglow_so_path.is_some() { + AlpenglowVoteState::create_account_with_authorized( + &node_pubkey, + &vote_pubkey, + &vote_pubkey, + 0, + *stake, + bls_pubkey, + ) + } else { + vote_state::create_account(&vote_pubkey, &node_pubkey, 0, *stake) + }; let stake_account = Account::from(stake_state::create_account( &stake_pubkey, &vote_pubkey, @@ -182,6 +223,44 @@ pub fn create_genesis_config_with_leader( mint_lamports, validator_pubkey, validator_stake_lamports, + None, + ) +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn create_genesis_config_with_alpenglow_vote_accounts_no_program( + mint_lamports: u64, + voting_keypairs: &[impl Borrow], + stakes: Vec, +) -> GenesisConfigInfo { + create_genesis_config_with_alpenglow_vote_accounts( + mint_lamports, + voting_keypairs, + stakes, + build_alpenglow_vote::ALPENGLOW_VOTE_SO_PATH, + ) +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn create_genesis_config_with_leader_enable_alpenglow( + mint_lamports: u64, + validator_pubkey: &Pubkey, + validator_stake_lamports: u64, + alpenglow_so_path: Option<&str>, +) -> GenesisConfigInfo { + // Use deterministic keypair so we don't get confused by randomness in tests + let mint_keypair = Keypair::from_seed(&[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, + ]) + .unwrap(); + + create_genesis_config_with_leader_with_mint_keypair( + mint_keypair, + mint_lamports, + validator_pubkey, + validator_stake_lamports, + alpenglow_so_path, ) } @@ -190,6 +269,7 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( mint_lamports: u64, validator_pubkey: &Pubkey, validator_stake_lamports: u64, + alpenglow_so_path: Option<&str>, ) -> GenesisConfigInfo { // Use deterministic keypair so we don't get confused by randomness in tests let voting_keypair = Keypair::from_seed(&[ @@ -198,18 +278,23 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( ]) .unwrap(); + let bls_keypair = + BLSKeypair::derive_from_signer(&voting_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); + let bls_pubkey: BLSPubkey = bls_keypair.public.into(); let genesis_config = create_genesis_config_with_leader_ex( mint_lamports, &mint_keypair.pubkey(), validator_pubkey, &voting_keypair.pubkey(), &Pubkey::new_unique(), + Some(&bls_pubkey), validator_stake_lamports, VALIDATOR_LAMPORTS, FeeRateGovernor::new(0, 0), // most tests can't handle transaction fees Rent::free(), // most tests don't expect rent ClusterType::Development, vec![], + alpenglow_so_path, ); GenesisConfigInfo { @@ -220,10 +305,20 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( } } +pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { + do_activate_all_features::(genesis_config); +} + pub fn activate_all_features(genesis_config: &mut GenesisConfig) { + do_activate_all_features::(genesis_config); +} + +pub fn do_activate_all_features(genesis_config: &mut GenesisConfig) { // Activate all features at genesis in development mode for feature_id in FeatureSet::default().inactive { - activate_feature(genesis_config, feature_id); + if IS_ALPENGLOW || feature_id != solana_feature_set::secp256k1_program_enabled::id() { + activate_feature(genesis_config, feature_id); + } } } @@ -256,6 +351,68 @@ pub fn activate_feature(genesis_config: &mut GenesisConfig, feature_id: Pubkey) ); } +pub fn include_alpenglow_bpf_program(genesis_config: &mut GenesisConfig, alpenglow_so_path: &str) { + // Parse out the elf + let mut program_data_elf: Vec = vec![]; + File::open(alpenglow_so_path) + .and_then(|mut file| file.read_to_end(&mut program_data_elf)) + .unwrap_or_else(|err| { + panic!( + "Error: failed to read alpenglow-vote program from path {}: {}", + alpenglow_so_path, err + ) + }); + + // Derive the address for the program data account + let address = solana_votor_messages::id(); + let loader = solana_program::bpf_loader_upgradeable::id(); + let programdata_address = + solana_program::bpf_loader_upgradeable::get_program_data_address(&address); + + // Generate the data for the program data account + let upgrade_authority_address = system_program::id(); + let mut program_data = bincode::serialize(&UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: Some(upgrade_authority_address), + }) + .unwrap(); + program_data.extend_from_slice(&program_data_elf); + + // Store the program data account into genesis + genesis_config.add_account( + programdata_address, + AccountSharedData::from(Account { + lamports: genesis_config + .rent + .minimum_balance(program_data.len()) + .max(1u64), + data: program_data, + owner: loader, + executable: false, + rent_epoch: 0, + }), + ); + + // Add the program acccount to genesis + let program_data = bincode::serialize(&UpgradeableLoaderState::Program { + programdata_address, + }) + .unwrap(); + genesis_config.add_account( + address, + AccountSharedData::from(Account { + lamports: genesis_config + .rent + .minimum_balance(program_data.len()) + .max(1u64), + data: program_data, + owner: loader, + executable: true, + rent_epoch: 0, + }), + ); +} + #[allow(clippy::too_many_arguments)] pub fn create_genesis_config_with_leader_ex_no_features( mint_lamports: u64, @@ -263,19 +420,32 @@ pub fn create_genesis_config_with_leader_ex_no_features( validator_pubkey: &Pubkey, validator_vote_account_pubkey: &Pubkey, validator_stake_account_pubkey: &Pubkey, + validator_bls_pubkey: Option<&BLSPubkey>, validator_stake_lamports: u64, validator_lamports: u64, fee_rate_governor: FeeRateGovernor, rent: Rent, cluster_type: ClusterType, mut initial_accounts: Vec<(Pubkey, AccountSharedData)>, + alpenglow_so_path: Option<&str>, ) -> GenesisConfig { - let validator_vote_account = vote_state::create_account( - validator_vote_account_pubkey, - validator_pubkey, - 0, - validator_stake_lamports, - ); + let validator_vote_account = if alpenglow_so_path.is_some() { + AlpenglowVoteState::create_account_with_authorized( + validator_pubkey, + validator_vote_account_pubkey, + validator_vote_account_pubkey, + 0, + validator_stake_lamports, + *validator_bls_pubkey.unwrap(), + ) + } else { + vote_state::create_account( + validator_vote_account_pubkey, + validator_pubkey, + 0, + validator_stake_lamports, + ) + }; let validator_stake_account = stake_state::create_account( validator_stake_account_pubkey, @@ -322,6 +492,10 @@ pub fn create_genesis_config_with_leader_ex_no_features( solana_stake_program::add_genesis_accounts(&mut genesis_config); + if let Some(alpenglow_so_path) = alpenglow_so_path { + include_alpenglow_bpf_program(&mut genesis_config, alpenglow_so_path); + } + genesis_config } @@ -332,12 +506,14 @@ pub fn create_genesis_config_with_leader_ex( validator_pubkey: &Pubkey, validator_vote_account_pubkey: &Pubkey, validator_stake_account_pubkey: &Pubkey, + validator_bls_pubkey: Option<&BLSPubkey>, validator_stake_lamports: u64, validator_lamports: u64, fee_rate_governor: FeeRateGovernor, rent: Rent, cluster_type: ClusterType, initial_accounts: Vec<(Pubkey, AccountSharedData)>, + alpenglow_so_path: Option<&str>, ) -> GenesisConfig { let mut genesis_config = create_genesis_config_with_leader_ex_no_features( mint_lamports, @@ -345,16 +521,22 @@ pub fn create_genesis_config_with_leader_ex( validator_pubkey, validator_vote_account_pubkey, validator_stake_account_pubkey, + validator_bls_pubkey, validator_stake_lamports, validator_lamports, fee_rate_governor, rent, cluster_type, initial_accounts, + alpenglow_so_path, ); if genesis_config.cluster_type == ClusterType::Development { - activate_all_features(&mut genesis_config); + if alpenglow_so_path.is_some() { + activate_all_features_alpenglow(&mut genesis_config); + } else { + activate_all_features(&mut genesis_config); + } } genesis_config diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 5487c37d43..7f9dc1b816 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -10,7 +10,7 @@ use { sysvar::stake_history::StakeHistory, }, solana_stake_program::stake_state::{Stake, StakeStateV2}, - solana_vote_program::vote_state::VoteState, + solana_vote::vote_account::VoteAccount, }; pub mod points; @@ -27,7 +27,7 @@ struct CalculatedStakeRewards { pub fn redeem_rewards( rewarded_epoch: Epoch, stake_state: &mut StakeStateV2, - vote_state: &VoteState, + vote_account: &VoteAccount, point_value: &PointValue, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, @@ -46,7 +46,7 @@ pub fn redeem_rewards( meta.rent_exempt_reserve, )); inflation_point_calc_tracer(&InflationPointCalculationEvent::Commission( - vote_state.commission, + vote_account.commission(), )); } @@ -54,7 +54,7 @@ pub fn redeem_rewards( rewarded_epoch, stake, point_value, - vote_state, + vote_account, stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, @@ -72,7 +72,7 @@ fn redeem_stake_rewards( rewarded_epoch: Epoch, stake: &mut Stake, point_value: &PointValue, - vote_state: &VoteState, + vote_account: &VoteAccount, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, @@ -87,7 +87,7 @@ fn redeem_stake_rewards( rewarded_epoch, stake, point_value, - vote_state, + vote_account, stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, @@ -119,7 +119,7 @@ fn calculate_stake_rewards( rewarded_epoch: Epoch, stake: &Stake, point_value: &PointValue, - vote_state: &VoteState, + vote_account: &VoteAccount, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, @@ -131,7 +131,7 @@ fn calculate_stake_rewards( mut force_credits_update_with_skipped_reward, } = calculate_stake_points_and_credits( stake, - vote_state, + vote_account, stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, @@ -190,7 +190,7 @@ fn calculate_stake_rewards( return None; } let (voter_rewards, staker_rewards, is_split) = - commission_split(vote_state.commission, rewards); + commission_split(vote_account.commission(), rewards); if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer(&InflationPointCalculationEvent::SplitRewards( rewards, @@ -256,18 +256,20 @@ fn commission_split(commission: u8, on: u64) -> (u64, u64, bool) { mod tests { use { self::points::null_tracer, super::*, solana_program::stake::state::Delegation, - solana_pubkey::Pubkey, solana_sdk::native_token::sol_to_lamports, test_case::test_case, + solana_pubkey::Pubkey, solana_sdk::native_token::sol_to_lamports, + solana_vote_program::vote_state::VoteState, + solana_votor_messages::state::VoteState as AlpenglowVoteState, test_case::test_case, }; fn new_stake( stake: u64, voter_pubkey: &Pubkey, - vote_state: &VoteState, + credits_observed: u64, activation_epoch: Epoch, ) -> Stake { Stake { delegation: Delegation::new(voter_pubkey, stake, activation_epoch), - credits_observed: vote_state.credits(), + credits_observed, } } @@ -277,8 +279,9 @@ mod tests { // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 let stake_lamports = 1; - let mut stake = new_stake(stake_lamports, &Pubkey::default(), &vote_state, u64::MAX); + let mut stake = new_stake(stake_lamports, &Pubkey::default(), 0, u64::MAX); + let vote_account = { VoteAccount::new_from_vote_state(&vote_state) }; // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( None, @@ -289,7 +292,7 @@ mod tests { rewards: 1_000_000_000, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -297,8 +300,11 @@ mod tests { ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + let vote_account = { + vote_state.increment_credits(0, 1); + vote_state.increment_credits(0, 1); + VoteAccount::new_from_vote_state(&vote_state) + }; // this one should be able to collect exactly 2 assert_eq!( @@ -310,7 +316,7 @@ mod tests { rewards: 1, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -327,9 +333,12 @@ mod tests { #[test] fn test_stake_state_calculate_rewards() { let mut vote_state = VoteState::default(); + // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 - let mut stake = new_stake(1, &Pubkey::default(), &vote_state, u64::MAX); + let mut stake = new_stake(1, &Pubkey::default(), 0, u64::MAX); + + let vote_account = VoteAccount::new_from_vote_state(&vote_state); // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( @@ -341,7 +350,7 @@ mod tests { rewards: 1_000_000_000, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -349,8 +358,11 @@ mod tests { ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + let vote_account = { + vote_state.increment_credits(0, 1); + vote_state.increment_credits(0, 1); + VoteAccount::new_from_vote_state(&vote_state) + }; // this one should be able to collect exactly 2 assert_eq!( @@ -366,7 +378,7 @@ mod tests { rewards: 2, points: 2 // all his }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -388,7 +400,7 @@ mod tests { rewards: 1, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -396,7 +408,10 @@ mod tests { ); // put 1 credit in epoch 1 - vote_state.increment_credits(1, 1); + let vote_account = { + vote_state.increment_credits(1, 1); + VoteAccount::new_from_vote_state(&vote_state) + }; stake.credits_observed = 2; // this one should be able to collect the one just added @@ -413,7 +428,7 @@ mod tests { rewards: 2, points: 2 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -421,7 +436,10 @@ mod tests { ); // put 1 credit in epoch 2 - vote_state.increment_credits(2, 1); + let vote_account = { + vote_state.increment_credits(2, 1); + VoteAccount::new_from_vote_state(&vote_state) + }; // this one should be able to collect 2 now assert_eq!( Some(CalculatedStakeRewards { @@ -436,7 +454,7 @@ mod tests { rewards: 2, points: 2 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -461,7 +479,7 @@ mod tests { rewards: 4, points: 4 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -470,7 +488,10 @@ mod tests { // same as above, but is a really small commission out of 32 bits, // verify that None comes back on small redemptions where no one gets paid - vote_state.commission = 1; + let vote_account = { + vote_state.commission = 1; + VoteAccount::new_from_vote_state(&vote_state) + }; assert_eq!( None, // would be Some((0, 2 * 1 + 1 * 2, 4)), calculate_stake_rewards( @@ -480,13 +501,16 @@ mod tests { rewards: 4, points: 4 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, ) ); - vote_state.commission = 99; + let vote_account = { + vote_state.commission = 99; + VoteAccount::new_from_vote_state(&vote_state) + }; assert_eq!( None, // would be Some((0, 2 * 1 + 1 * 2, 4)), calculate_stake_rewards( @@ -496,7 +520,7 @@ mod tests { rewards: 4, points: 4 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -519,7 +543,7 @@ mod tests { rewards: 0, points: 4 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -542,7 +566,7 @@ mod tests { rewards: 0, points: 4 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -557,7 +581,7 @@ mod tests { }, calculate_stake_points_and_credits( &stake, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None @@ -576,7 +600,7 @@ mod tests { }, calculate_stake_points_and_credits( &stake, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None @@ -592,7 +616,7 @@ mod tests { }, calculate_stake_points_and_credits( &stake, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None @@ -600,7 +624,10 @@ mod tests { ); // get rewards and credits observed when not the activation epoch - vote_state.commission = 0; + let vote_account = { + vote_state.commission = 0; + VoteAccount::new_from_vote_state(&vote_state) + }; stake.credits_observed = 3; stake.delegation.activation_epoch = 1; assert_eq!( @@ -616,7 +643,7 @@ mod tests { rewards: 1, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -640,7 +667,7 @@ mod tests { rewards: 1, points: 1 }, - &vote_state, + &vote_account, &StakeHistory::default(), null_tracer(), None, @@ -653,7 +680,7 @@ mod tests { fn calculate_rewards_tests(stake: u64, rewards: u64, credits: u64) { let mut vote_state = VoteState::default(); - let stake = new_stake(stake, &Pubkey::default(), &vote_state, u64::MAX); + let stake = new_stake(stake, &Pubkey::default(), 0, u64::MAX); vote_state.increment_credits(0, credits); @@ -661,7 +688,7 @@ mod tests { 0, &stake, &PointValue { rewards, points: 1 }, - &vote_state, + &VoteAccount::new_from_vote_state(&vote_state), &StakeHistory::default(), null_tracer(), None, @@ -671,13 +698,14 @@ mod tests { #[test] fn test_stake_state_calculate_points_with_typical_values() { let vote_state = VoteState::default(); + let alpenglow_vote_state = AlpenglowVoteState::default(); // bootstrap means fully-vested stake at epoch 0 with // 10_000_000 SOL is a big but not unreasaonable stake let stake = new_stake( sol_to_lamports(10_000_000f64), &Pubkey::default(), - &vote_state, + 0, u64::MAX, ); @@ -691,7 +719,22 @@ mod tests { rewards: 1_000_000_000, points: 1 }, - &vote_state, + &VoteAccount::new_from_vote_state(&vote_state), + &StakeHistory::default(), + null_tracer(), + None, + ) + ); + assert_eq!( + None, + calculate_stake_rewards( + 0, + &stake, + &PointValue { + rewards: 1_000_000_000, + points: 1 + }, + &VoteAccount::new_from_alpenglow_vote_state(&alpenglow_vote_state), &StakeHistory::default(), null_tracer(), None, diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 6d2ee95558..ffbb16eaaa 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -6,7 +6,7 @@ use { clock::Epoch, instruction::InstructionError, sysvar::stake_history::StakeHistory, }, solana_stake_program::stake_state::{Delegation, Stake, StakeStateV2}, - solana_vote_program::vote_state::VoteState, + solana_vote::vote_account::VoteAccount, std::cmp::Ordering, }; @@ -64,14 +64,14 @@ impl From for InflationPointCalculationEvent { pub fn calculate_points( stake_state: &StakeStateV2, - vote_state: &VoteState, + vote_account: &VoteAccount, stake_history: &StakeHistory, new_rate_activation_epoch: Option, ) -> Result { if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state { Ok(calculate_stake_points( stake, - vote_state, + vote_account, stake_history, null_tracer(), new_rate_activation_epoch, @@ -83,14 +83,14 @@ pub fn calculate_points( fn calculate_stake_points( stake: &Stake, - vote_state: &VoteState, + vote_account: &VoteAccount, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, ) -> u128 { calculate_stake_points_and_credits( stake, - vote_state, + vote_account, stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, @@ -98,18 +98,18 @@ fn calculate_stake_points( .points } -/// for a given stake and vote_state, calculate how many +/// for a given stake and vote_account, calculate how many /// points were earned (credits * stake) and new value /// for credits_observed were the points paid pub(crate) fn calculate_stake_points_and_credits( stake: &Stake, - new_vote_state: &VoteState, + new_vote_account: &VoteAccount, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, ) -> CalculatedStakePoints { let credits_in_stake = stake.credits_observed; - let credits_in_vote = new_vote_state.credits(); + let credits_in_vote = new_vote_account.credits(); // if there is no newer credits since observed, return no point match credits_in_vote.cmp(&credits_in_stake) { Ordering::Less => { @@ -157,9 +157,7 @@ pub(crate) fn calculate_stake_points_and_credits( let mut points = 0; let mut new_credits_observed = credits_in_stake; - for (epoch, final_epoch_credits, initial_epoch_credits) in - new_vote_state.epoch_credits().iter().copied() - { + for (epoch, final_epoch_credits, initial_epoch_credits) in new_vote_account.epoch_credits() { let stake_amount = u128::from(stake.delegation.stake( epoch, stake_history, @@ -207,17 +205,20 @@ pub(crate) fn calculate_stake_points_and_credits( #[cfg(test)] mod tests { - use {super::*, solana_sdk::native_token::sol_to_lamports}; + use { + super::*, solana_sdk::native_token::sol_to_lamports, + solana_vote_program::vote_state::VoteState, + }; fn new_stake( stake: u64, voter_pubkey: &Pubkey, - vote_state: &VoteState, + credits_observed: u64, activation_epoch: Epoch, ) -> Stake { Stake { delegation: Delegation::new(voter_pubkey, stake, activation_epoch), - credits_observed: vote_state.credits(), + credits_observed, } } @@ -226,11 +227,11 @@ mod tests { let mut vote_state = VoteState::default(); // bootstrap means fully-vested stake at epoch 0 with - // 10_000_000 SOL is a big but not unreasaonable stake + // 10_000_000 SOL is a big but not unreasonable stake let stake = new_stake( sol_to_lamports(10_000_000f64), &Pubkey::default(), - &vote_state, + vote_state.credits(), u64::MAX, ); @@ -246,7 +247,7 @@ mod tests { u128::from(stake.delegation.stake) * epoch_slots, calculate_stake_points( &stake, - &vote_state, + &VoteAccount::new_from_vote_state(&vote_state), &StakeHistory::default(), null_tracer(), None diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b5cd9ddf5d..51e36bc32b 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,6 +13,7 @@ pub mod bank_hash_cache; pub mod bank_utils; pub mod commitment; pub mod epoch_stakes; +pub mod epoch_stakes_service; pub mod genesis_utils; pub mod inflation_rewards; pub mod installed_scheduler_pool; diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 3755b7e99d..6653ff062b 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -79,7 +79,7 @@ impl StakesCache { // Zero lamport accounts are not stored in accounts-db // and so should be removed from cache as well. if account.lamports() == 0 { - if solana_vote_program::check_id(owner) { + if solana_vote_program::check_id(owner) || solana_votor_messages::check_id(owner) { let _old_vote_account = { let mut stakes = self.0.write().unwrap(); stakes.remove_vote_account(pubkey) @@ -120,6 +120,39 @@ impl StakesCache { stakes.remove_vote_account(pubkey) }; }; + } else if solana_votor_messages::check_id(owner) { + match VoteAccount::try_from(account.to_account_shared_data()) { + Ok(vote_account) => { + if vote_account + .alpenglow_vote_state() + .unwrap() + .is_initialized() + { + // drop the old account after releasing the lock + let _old_vote_account = { + let mut stakes = self.0.write().unwrap(); + stakes.upsert_vote_account( + pubkey, + vote_account, + new_rate_activation_epoch, + ) + }; + } else { + // drop the old account after releasing the lock + let _old_vote_account = { + let mut stakes = self.0.write().unwrap(); + stakes.remove_vote_account(pubkey) + }; + } + } + Err(_) => { + // drop the old account after releasing the lock + let _old_vote_account = { + let mut stakes = self.0.write().unwrap(); + stakes.remove_vote_account(pubkey) + }; + } + } } else if solana_stake_program::check_id(owner) { match StakeAccount::try_from(account.to_account_shared_data()) { Ok(stake_account) => { @@ -230,9 +263,19 @@ impl Stakes { let voter_pubkey = &delegation.voter_pubkey; if stakes.vote_accounts.get(voter_pubkey).is_none() { if let Some(account) = get_account(voter_pubkey) { - if VoteStateVersions::is_correct_size_and_initialized(account.data()) - && VoteAccount::try_from(account.clone()).is_ok() - { + let is_valid_account = if solana_votor_messages::check_id(account.owner()) { + match VoteAccount::try_from(account.clone()) { + Ok(vote_account) => vote_account + .alpenglow_vote_state() + .unwrap() + .is_initialized(), + Err(_) => false, + } + } else { + VoteStateVersions::is_correct_size_and_initialized(account.data()) + && VoteAccount::try_from(account.clone()).is_ok() + }; + if is_valid_account { error!("vote account not cached: {voter_pubkey}, {account:?}"); return Err(Error::VoteAccountNotCached(*voter_pubkey)); } @@ -604,18 +647,33 @@ pub(crate) mod tests { use { super::*, rayon::ThreadPoolBuilder, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_sdk::{account::WritableAccount, pubkey::Pubkey, rent::Rent, stake}, solana_stake_program::stake_state, solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, + solana_votor_messages::state::VoteState as AlpenglowVoteState, + test_case::test_case, }; // set up some dummies for a staked node (( vote ) ( stake )) pub(crate) fn create_staked_node_accounts( stake: u64, + is_alpenglow: bool, ) -> ((Pubkey, AccountSharedData), (Pubkey, AccountSharedData)) { let vote_pubkey = solana_pubkey::new_rand(); - let vote_account = - vote_state::create_account(&vote_pubkey, &solana_pubkey::new_rand(), 0, 1); + let bls_keypair = BLSKeypair::new(); + let vote_account = if is_alpenglow { + AlpenglowVoteState::create_account_with_authorized( + &vote_pubkey, + &vote_pubkey, + &vote_pubkey, + 0, + 1, + bls_keypair.public.into(), + ) + } else { + vote_state::create_account(&vote_pubkey, &solana_pubkey::new_rand(), 0, 1) + }; let stake_pubkey = solana_pubkey::new_rand(); ( (vote_pubkey, vote_account), @@ -644,10 +702,22 @@ pub(crate) mod tests { fn create_warming_staked_node_accounts( stake: u64, epoch: Epoch, + is_alpenglow: bool, ) -> ((Pubkey, AccountSharedData), (Pubkey, AccountSharedData)) { let vote_pubkey = solana_pubkey::new_rand(); - let vote_account = - vote_state::create_account(&vote_pubkey, &solana_pubkey::new_rand(), 0, 1); + let bls_keypair = BLSKeypair::new(); + let vote_account = if is_alpenglow { + AlpenglowVoteState::create_account_with_authorized( + &vote_pubkey, + &vote_pubkey, + &vote_pubkey, + 0, + 1, + bls_keypair.public.into(), + ) + } else { + vote_state::create_account(&vote_pubkey, &solana_pubkey::new_rand(), 0, 1) + }; ( (vote_pubkey, vote_account), create_warming_stake_account(stake, epoch, &vote_pubkey), @@ -674,8 +744,9 @@ pub(crate) mod tests { ) } - #[test] - fn test_stakes_basic() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_basic(is_alpenglow: bool) { for i in 0..4 { let stakes_cache = StakesCache::new(Stakes { epoch: i, @@ -683,7 +754,7 @@ pub(crate) mod tests { }); let ((vote_pubkey, vote_account), (stake_pubkey, mut stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); @@ -736,39 +807,47 @@ pub(crate) mod tests { } } - #[test] - fn test_stakes_highest() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_highest(is_alpenglow: bool) { let stakes_cache = StakesCache::default(); assert_eq!(stakes_cache.stakes().highest_staked_node(), None); let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); let ((vote11_pubkey, vote11_account), (stake11_pubkey, stake11_account)) = - create_staked_node_accounts(20); + create_staked_node_accounts(20, is_alpenglow); stakes_cache.check_and_store(&vote11_pubkey, &vote11_account, None); stakes_cache.check_and_store(&stake11_pubkey, &stake11_account, None); - let vote11_node_pubkey = vote_state::from(&vote11_account).unwrap().node_pubkey; + let vote11_node_pubkey = if is_alpenglow { + *AlpenglowVoteState::deserialize(vote11_account.data()) + .unwrap() + .node_pubkey() + } else { + vote_state::from(&vote11_account).unwrap().node_pubkey + }; let highest_staked_node = stakes_cache.stakes().highest_staked_node().copied(); assert_eq!(highest_staked_node, Some(vote11_node_pubkey)); } - #[test] - fn test_stakes_vote_account_disappear_reappear() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_vote_account_disappear_reappear(is_alpenglow: bool) { let stakes_cache = StakesCache::new(Stakes { epoch: 4, ..Stakes::default() }); let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); @@ -815,9 +894,15 @@ pub(crate) mod tests { } // Vote account uninitialized - let default_vote_state = VoteState::default(); - let versioned = VoteStateVersions::new_current(default_vote_state); - vote_state::to(&versioned, &mut vote_account).unwrap(); + if is_alpenglow { + vote_account.set_data(cache_data.clone()); + let default_vote_state = AlpenglowVoteState::default(); + default_vote_state.serialize_into(vote_account.data_as_mut_slice()); + } else { + let default_vote_state = VoteState::default(); + let versioned = VoteStateVersions::new_current(default_vote_state); + vote_state::to(&versioned, &mut vote_account).unwrap(); + } stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); { @@ -838,18 +923,19 @@ pub(crate) mod tests { } } - #[test] - fn test_stakes_change_delegate() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_change_delegate(is_alpenglow: bool) { let stakes_cache = StakesCache::new(Stakes { epoch: 4, ..Stakes::default() }); let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); let ((vote_pubkey2, vote_account2), (_stake_pubkey2, stake_account2)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&vote_pubkey2, &vote_account2, None); @@ -886,15 +972,17 @@ pub(crate) mod tests { ); } } - #[test] - fn test_stakes_multiple_stakers() { + + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_multiple_stakers(is_alpenglow: bool) { let stakes_cache = StakesCache::new(Stakes { epoch: 4, ..Stakes::default() }); let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); let stake_pubkey2 = solana_pubkey::new_rand(); let stake_account2 = create_stake_account(10, &vote_pubkey, &stake_pubkey2); @@ -913,12 +1001,13 @@ pub(crate) mod tests { } } - #[test] - fn test_activate_epoch() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_activate_epoch(is_alpenglow: bool) { let stakes_cache = StakesCache::default(); let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); @@ -944,15 +1033,16 @@ pub(crate) mod tests { } } - #[test] - fn test_stakes_not_delegate() { + #[test_case(true; "alpenglow")] + #[test_case(false; "towerbft")] + fn test_stakes_not_delegate(is_alpenglow: bool) { let stakes_cache = StakesCache::new(Stakes { epoch: 4, ..Stakes::default() }); let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_staked_node_accounts(10); + create_staked_node_accounts(10, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); @@ -984,8 +1074,7 @@ pub(crate) mod tests { assert_eq!(stakes.vote_balance_and_staked(), 0); } - #[test] - fn test_vote_balance_and_staked_normal() { + fn test_vote_balance_and_staked_normal(is_alpenglow: bool) { let stakes_cache = StakesCache::default(); #[allow(non_local_definitions)] impl Stakes { @@ -1006,7 +1095,7 @@ pub(crate) mod tests { let genesis_epoch = 0; let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = - create_warming_staked_node_accounts(10, genesis_epoch); + create_warming_staked_node_accounts(10, genesis_epoch, is_alpenglow); stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); @@ -1029,4 +1118,10 @@ pub(crate) mod tests { ); } } + + #[test] + fn test_vote_balance_and_staked_normal_tests() { + test_vote_balance_and_staked_normal(false); + test_vote_balance_and_staked_normal(true); + } } diff --git a/runtime/src/vote_sender_types.rs b/runtime/src/vote_sender_types.rs index 3d02c961ca..7a91d781a0 100644 --- a/runtime/src/vote_sender_types.rs +++ b/runtime/src/vote_sender_types.rs @@ -1,7 +1,11 @@ use { crossbeam_channel::{Receiver, Sender}, solana_vote::vote_parser::ParsedVote, + solana_votor_messages::bls_message::BLSMessage, }; pub type ReplayVoteSender = Sender; pub type ReplayVoteReceiver = Receiver; + +pub type BLSVerifiedMessageSender = Sender; +pub type BLSVerifiedMessageReceiver = Receiver; diff --git a/sdk/frozen-abi/src/abi_example.rs b/sdk/frozen-abi/src/abi_example.rs new file mode 100644 index 0000000000..1c63cf13e9 --- /dev/null +++ b/sdk/frozen-abi/src/abi_example.rs @@ -0,0 +1,629 @@ +use { + crate::abi_digester::{AbiDigester, DigestError, DigestResult}, + log::*, + serde::Serialize, + std::any::type_name, +}; + +// The most important trait for the abi digesting. This trait is used to create any complexities of +// object graph to generate the abi digest. The frozen abi test harness calls T::example() to +// instantiate the tested root type and traverses its fields recursively, abusing the +// serde::serialize(). +// +// This trait applicability is similar to the Default trait. That means all referenced types must +// implement this trait. AbiExample is implemented for almost all common types in this file. +// +// When implementing AbiExample manually, you need to return a _minimally-populated_ value +// from it to actually generate a meaningful digest. This impl semantics is unlike Default, which +// usually returns something empty. See actual impls for inspiration. +// +// The requirement of AbiExample impls even applies to those types of `#[serde(skip)]`-ed fields. +// That's because the abi digesting needs a properly initialized object to enter into the +// serde::serialize() to begin with, even knowning they aren't used for serialization and thus abi +// digest. Luckily, `#[serde(skip)]`-ed fields' AbiExample impls can just delegate to T::default(), +// exploiting the nature of this artificial impl requirement as an exception from the usual +// AbiExample semantics. +pub trait AbiExample: Sized { + fn example() -> Self; +} + +// Following code snippets are copied and adapted from the official rustc implementation to +// implement AbiExample trait for most of basic types. +// These are licensed under Apache-2.0 + MIT (compatible because we're Apache-2.0) + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/tuple.rs#L7 +macro_rules! tuple_example_impls { + ($( + $Tuple:ident { + $(($idx:tt) -> $T:ident)+ + } + )+) => { + $( + impl<$($T:AbiExample),+> AbiExample for ($($T,)+) { + fn example() -> Self { + ($({ let x: $T = AbiExample::example(); x},)+) + } + } + )+ + } +} + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/tuple.rs#L110 +tuple_example_impls! { + Tuple1 { + (0) -> A + } + Tuple2 { + (0) -> A + (1) -> B + } + Tuple3 { + (0) -> A + (1) -> B + (2) -> C + } + Tuple4 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + } + Tuple5 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + } + Tuple6 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + } + Tuple7 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + } + Tuple8 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + } + Tuple9 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + } + Tuple10 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + } + Tuple11 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + } + Tuple12 { + (0) -> A + (1) -> B + (2) -> C + (3) -> D + (4) -> E + (5) -> F + (6) -> G + (7) -> H + (8) -> I + (9) -> J + (10) -> K + (11) -> L + } +} + +impl AbiExample for [T; N] { + fn example() -> Self { + std::array::from_fn(|_| T::example()) + } +} + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/default.rs#L137 +macro_rules! example_impls { + ($t:ty, $v:expr) => { + impl AbiExample for $t { + fn example() -> Self { + $v + } + } + }; +} + +example_impls! { (), () } +example_impls! { bool, false } +example_impls! { char, '\x00' } + +example_impls! { usize, 0 } +example_impls! { u8, 0 } +example_impls! { u16, 0 } +example_impls! { u32, 0 } +example_impls! { u64, 0 } +example_impls! { u128, 0 } + +example_impls! { isize, 0 } +example_impls! { i8, 0 } +example_impls! { i16, 0 } +example_impls! { i32, 0 } +example_impls! { i64, 0 } +example_impls! { i128, 0 } + +example_impls! { f32, 0.0f32 } +example_impls! { f64, 0.0f64 } +example_impls! { String, String::new() } +example_impls! { std::time::Duration, std::time::Duration::from_secs(0) } +example_impls! { std::sync::Once, std::sync::Once::new() } + +use std::sync::atomic::*; + +// Source: https://github.com/rust-lang/rust/blob/ba18875557aabffe386a2534a1aa6118efb6ab88/src/libcore/sync/atomic.rs#L1199 +macro_rules! atomic_example_impls { + ($atomic_type: ident) => { + impl AbiExample for $atomic_type { + fn example() -> Self { + Self::new(AbiExample::example()) + } + } + }; +} +atomic_example_impls! { AtomicU8 } +atomic_example_impls! { AtomicU16 } +atomic_example_impls! { AtomicU32 } +atomic_example_impls! { AtomicU64 } +atomic_example_impls! { AtomicUsize } +atomic_example_impls! { AtomicI8 } +atomic_example_impls! { AtomicI16 } +atomic_example_impls! { AtomicI32 } +atomic_example_impls! { AtomicI64 } +atomic_example_impls! { AtomicIsize } +atomic_example_impls! { AtomicBool } + +use bv::{BitVec, BlockType}; +impl AbiExample for BitVec { + fn example() -> Self { + Self::default() + } +} + +impl TransparentAsHelper for BitVec {} +// This (EvenAsOpaque) marker trait is needed for BitVec because we can't impl AbiExample for its +// private type: +// thread '...TestBitVec_frozen_abi...' panicked at ...: +// derive or implement AbiExample/AbiEnumVisitor for +// bv::bit_vec::inner::Inner +impl EvenAsOpaque for BitVec { + const TYPE_NAME_MATCHER: &'static str = "bv::bit_vec::inner::"; +} + +use serde_with::ser::SerializeAsWrap; +impl TransparentAsHelper for SerializeAsWrap<'_, T, U> {} +// This (EvenAsOpaque) marker trait is needed for serde_with's serde_as(...) because this struct is +// basically a wrapper struct. +impl EvenAsOpaque for SerializeAsWrap<'_, T, U> { + const TYPE_NAME_MATCHER: &'static str = "serde_with::ser::SerializeAsWrap<"; +} + +pub(crate) fn normalize_type_name(type_name: &str) -> String { + type_name.chars().filter(|c| *c != '&').collect() +} + +type Placeholder = (); + +impl AbiExample for T { + default fn example() -> Self { + ::type_erased_example() + } +} + +// this works like a type erasure and a hatch to escape type error to runtime error +trait TypeErasedExample { + fn type_erased_example() -> T; +} + +impl TypeErasedExample for Placeholder { + default fn type_erased_example() -> T { + panic!( + "derive or implement AbiExample/AbiEnumVisitor for {}", + type_name::() + ); + } +} + +impl TypeErasedExample for Placeholder { + default fn type_erased_example() -> T { + let original_type_name = type_name::(); + let normalized_type_name = normalize_type_name(original_type_name); + + if normalized_type_name.starts_with("solana") { + panic!("derive or implement AbiExample/AbiEnumVisitor for {original_type_name}"); + } else { + panic!("new unrecognized type for ABI digest!: {original_type_name}") + } + } +} + +impl AbiExample for Option { + fn example() -> Self { + info!("AbiExample for (Option): {}", type_name::()); + Some(T::example()) + } +} + +impl AbiExample for Result { + fn example() -> Self { + info!("AbiExample for (Result): {}", type_name::()); + Ok(O::example()) + } +} + +impl AbiExample for Box { + fn example() -> Self { + info!("AbiExample for (Box): {}", type_name::()); + Box::new(T::example()) + } +} + +impl AbiExample for Box { + fn example() -> Self { + info!("AbiExample for (Box): {}", type_name::()); + Box::new(move |_t: &mut T| {}) + } +} + +impl AbiExample for Box { + fn example() -> Self { + info!("AbiExample for (Box): {}", type_name::()); + Box::new(move |_t: &mut T, _u: U| {}) + } +} + +impl AbiExample for Box<[T]> { + fn example() -> Self { + info!("AbiExample for (Box<[T]>): {}", type_name::()); + Box::new([T::example()]) + } +} + +impl AbiExample for std::marker::PhantomData { + fn example() -> Self { + info!("AbiExample for (PhantomData): {}", type_name::()); + std::marker::PhantomData:: + } +} + +impl AbiExample for std::sync::Arc { + fn example() -> Self { + info!("AbiExample for (Arc): {}", type_name::()); + std::sync::Arc::new(T::example()) + } +} + +// When T is weakly owned by the likes of `std::{sync, rc}::Weak`s, we need to uphold the ownership +// of T in some way at least during abi digesting... However, there's no easy way. Stashing them +// into static is confronted with Send/Sync issue. Stashing them into thread_local is confronted +// with not enough (T + 'static) lifetime bound.. So, just leak the examples. This should be +// tolerated, considering ::example() should ever be called inside tests, not in production code... +fn leak_and_inhibit_drop<'a, T>(t: T) -> &'a mut T { + Box::leak(Box::new(t)) +} + +impl AbiExample for &T { + fn example() -> Self { + info!("AbiExample for (&T): {}", type_name::()); + leak_and_inhibit_drop(T::example()) + } +} + +impl AbiExample for &[T] { + fn example() -> Self { + info!("AbiExample for (&[T]): {}", type_name::()); + leak_and_inhibit_drop(vec![T::example()]) + } +} + +impl AbiExample for std::sync::Weak { + fn example() -> Self { + info!("AbiExample for (Arc's Weak): {}", type_name::()); + // leaking is needed otherwise Arc::upgrade() will always return None... + std::sync::Arc::downgrade(leak_and_inhibit_drop(std::sync::Arc::new(T::example()))) + } +} + +impl AbiExample for std::rc::Rc { + fn example() -> Self { + info!("AbiExample for (Rc): {}", type_name::()); + std::rc::Rc::new(T::example()) + } +} + +impl AbiExample for std::rc::Weak { + fn example() -> Self { + info!("AbiExample for (Rc's Weak): {}", type_name::()); + // leaking is needed otherwise Rc::upgrade() will always return None... + std::rc::Rc::downgrade(leak_and_inhibit_drop(std::rc::Rc::new(T::example()))) + } +} + +impl AbiExample for std::sync::Mutex { + fn example() -> Self { + info!("AbiExample for (Mutex): {}", type_name::()); + std::sync::Mutex::new(T::example()) + } +} + +impl AbiExample for std::sync::RwLock { + fn example() -> Self { + info!("AbiExample for (RwLock): {}", type_name::()); + std::sync::RwLock::new(T::example()) + } +} + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; + +impl< + T: std::cmp::Eq + std::hash::Hash + AbiExample, + S: AbiExample, + H: std::hash::BuildHasher + Default, + > AbiExample for HashMap +{ + fn example() -> Self { + info!("AbiExample for (HashMap): {}", type_name::()); + let mut map = HashMap::default(); + map.insert(T::example(), S::example()); + map + } +} + +#[cfg(not(target_os = "solana"))] +impl< + T: Clone + std::cmp::Eq + std::hash::Hash + AbiExample, + S: Clone + AbiExample, + H: std::hash::BuildHasher + Default, + > AbiExample for im::HashMap +{ + fn example() -> Self { + info!("AbiExample for (HashMap): {}", type_name::()); + let mut map = im::HashMap::default(); + map.insert(T::example(), S::example()); + map + } +} + +impl AbiExample for BTreeMap { + fn example() -> Self { + info!("AbiExample for (BTreeMap): {}", type_name::()); + let mut map = BTreeMap::default(); + map.insert(T::example(), S::example()); + map + } +} + +impl AbiExample for Vec { + fn example() -> Self { + info!("AbiExample for (Vec): {}", type_name::()); + vec![T::example()] + } +} + +impl AbiExample for VecDeque { + fn example() -> Self { + info!("AbiExample for (Vec): {}", type_name::()); + VecDeque::from(vec![T::example()]) + } +} + +impl AbiExample + for HashSet +{ + fn example() -> Self { + info!("AbiExample for (HashSet): {}", type_name::()); + let mut set: HashSet = HashSet::default(); + set.insert(T::example()); + set + } +} + +impl AbiExample for BTreeSet { + fn example() -> Self { + info!("AbiExample for (BTreeSet): {}", type_name::()); + let mut set: BTreeSet = BTreeSet::default(); + set.insert(T::example()); + set + } +} + +#[cfg(not(target_os = "solana"))] +impl AbiExample for memmap2::MmapMut { + fn example() -> Self { + memmap2::MmapMut::map_anon(1).expect("failed to map the data file") + } +} + +#[cfg(not(target_os = "solana"))] +impl AbiExample for std::path::PathBuf { + fn example() -> Self { + std::path::PathBuf::from(String::example()) + } +} + +#[cfg(not(target_os = "solana"))] +impl AbiExample for std::time::SystemTime { + fn example() -> Self { + std::time::SystemTime::UNIX_EPOCH + } +} + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +impl AbiExample for SocketAddr { + fn example() -> Self { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } +} + +impl AbiExample for IpAddr { + fn example() -> Self { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + } +} + +// This is a control flow indirection needed for digesting all variants of an enum. +// +// All of types (including non-enums) will be processed by this trait, albeit the +// name of this trait. +// User-defined enums usually just need to impl this with namesake derive macro (AbiEnumVisitor). +// +// Note that sometimes this indirection doesn't work for various reasons. For that end, there are +// hacks with marker traits (TransparentAsHelper/EvenAsOpaque). +pub trait AbiEnumVisitor: Serialize { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult; +} + +pub trait TransparentAsHelper {} +pub trait EvenAsOpaque { + const TYPE_NAME_MATCHER: &'static str; +} + +impl AbiEnumVisitor for T { + default fn visit_for_abi(&self, _digester: &mut AbiDigester) -> DigestResult { + unreachable!( + "AbiEnumVisitor must be implemented for {}", + type_name::() + ); + } +} + +impl AbiEnumVisitor for T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for T: {}", type_name::()); + // not calling self.serialize(...) is intentional here as the most generic impl + // consider TransparentAsHelper and EvenAsOpaque if you're stuck on this.... + T::example() + .serialize(digester.create_new()) + .map_err(DigestError::wrap_by_type::) + } +} + +// even (experimental) rust specialization isn't enough for us, resort to +// the autoref hack: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md +// relevant test: TestVecEnum +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for &T: {}", type_name::()); + // Don't call self.visit_for_abi(...) to avoid the infinite recursion! + T::visit_for_abi(self, digester) + } +} + +// force to call self.serialize instead of T::visit_for_abi() for serialization +// helper structs like ad-hoc iterator `struct`s +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!( + "AbiEnumVisitor for (TransparentAsHelper): {}", + type_name::() + ); + self.serialize(digester.create_new()) + .map_err(DigestError::wrap_by_type::) + } +} + +// force to call self.serialize instead of T::visit_for_abi() to work around the +// inability of implementing AbiExample for private structs from other crates +impl AbiEnumVisitor for &T { + default fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + let type_name = type_name::(); + let matcher = T::TYPE_NAME_MATCHER; + info!( + "AbiEnumVisitor for (EvenAsOpaque): {}: matcher: {}", + type_name, matcher + ); + self.serialize(digester.create_new_opaque(matcher)) + .map_err(DigestError::wrap_by_type::) + } +} + +// Because Option and Result enums are so common enums, provide generic trait implementations +// The digesting pattern must match with what is derived from #[derive(AbiEnumVisitor)] +impl AbiEnumVisitor for Option { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (Option): {}", type_name::()); + + let variant: Self = Option::Some(T::example()); + // serde calls serialize_some(); not serialize_variant(); + // so create_new is correct, not create_enum_child or create_enum_new + variant.serialize(digester.create_new()) + } +} + +impl AbiEnumVisitor for Result { + fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult { + info!("AbiEnumVisitor for (Result): {}", type_name::()); + + digester.update(&["enum Result (variants = 2)"]); + let variant: Self = Result::Ok(O::example()); + variant.serialize(digester.create_enum_child()?)?; + + let variant: Self = Result::Err(E::example()); + variant.serialize(digester.create_enum_child()?)?; + + digester.create_child() + } +} + +#[cfg(not(target_os = "solana"))] +impl AbiExample for std::sync::OnceLock { + fn example() -> Self { + Self::from(T::example()) + } +} + +impl AbiExample for std::ops::RangeInclusive { + fn example() -> Self { + info!( + "AbiExample for (RangeInclusive): {}", + type_name::() + ); + T::example()..=T::example() + } +} diff --git a/svm/examples/Cargo.lock b/svm/examples/Cargo.lock index 36e30daed3..3a2936e2d1 100644 --- a/svm/examples/Cargo.lock +++ b/svm/examples/Cargo.lock @@ -642,6 +642,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.6.1" @@ -674,6 +687,34 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "borsh" version = "0.10.4" @@ -782,6 +823,12 @@ dependencies = [ "serde", ] +[[package]] +name = "build-print" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2128d00b7061b82b72844a351e80acd29e05afc60e9261e2ac90dca9ecc2ac" + [[package]] name = "bumpalo" version = "3.16.0" @@ -798,6 +845,12 @@ dependencies = [ "serde", ] +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.22.0" @@ -1109,9 +1162,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1674,6 +1727,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1692,6 +1756,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + [[package]] name = "five8_const" version = "0.1.3" @@ -1780,6 +1853,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -2005,6 +2084,19 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand 0.8.5", + "rand_core 0.6.4", + "rand_xorshift", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -3485,9 +3577,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.9.0", "cfg-if 1.0.0", @@ -3526,9 +3618,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -3556,6 +3648,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parking" version = "2.2.1" @@ -3851,7 +3952,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -4079,6 +4180,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4748,6 +4855,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5264,6 +5380,41 @@ dependencies = [ "solana-time-utils", ] +[[package]] +name = "solana-bls-signatures" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af089f712fb5cbef2d73ac7ffee9cad05ada6cd1fd5c812338df040bcd3c410b" +dependencies = [ + "base64 0.22.1", + "blst", + "blstrs", + "bytemuck", + "cfg_eval", + "ff", + "group", + "rand 0.8.5", + "serde", + "serde_json", + "serde_with", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-bls12-381" +version = "2.3.0" +dependencies = [ + "blst", + "bytemuck", + "bytemuck_derive", + "solana-curve-traits", + "solana-define-syscall", + "thiserror 2.0.12", +] + [[package]] name = "solana-bn254" version = "2.2.1" @@ -5303,9 +5454,11 @@ dependencies = [ "solana-big-mod-exp", "solana-bincode", "solana-blake3-hasher", + "solana-bls12-381", "solana-bn254", "solana-clock", "solana-cpi", + "solana-curve-traits", "solana-curve25519", "solana-feature-set", "solana-hash", @@ -5353,6 +5506,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "solana-build-alpenglow-vote" +version = "2.3.0" +dependencies = [ + "build-print", + "glob", + "serde", + "toml 0.8.19", +] + [[package]] name = "solana-builtins" version = "2.3.0" @@ -5401,6 +5564,7 @@ dependencies = [ "chrono", "clap", "rpassword", + "solana-bls-signatures", "solana-clock", "solana-cluster-type", "solana-commitment-config", @@ -5679,6 +5843,7 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bs58", "bytes", "chrono", @@ -5707,6 +5872,7 @@ dependencies = [ "slab", "solana-accounts-db", "solana-bloom", + "solana-bls-signatures", "solana-builtins-default-costs", "solana-client", "solana-compute-budget", @@ -5748,6 +5914,8 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-votor", + "solana-votor-messages", "solana-wen-restart", "strum", "strum_macros", @@ -5801,6 +5969,10 @@ dependencies = [ "solana-stable-layout", ] +[[package]] +name = "solana-curve-traits" +version = "2.3.0" + [[package]] name = "solana-curve25519" version = "2.3.0" @@ -5808,6 +5980,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", + "solana-curve-traits", "solana-define-syscall", "subtle", "thiserror 2.0.12", @@ -6364,6 +6537,7 @@ dependencies = [ "solana-transaction-status", "solana-vote", "solana-vote-program", + "solana-votor-messages", "spl-token", "spl-token-2022 7.0.0", "static_assertions", @@ -6640,6 +6814,8 @@ dependencies = [ "solana-short-vec", "solana-signature", "solana-time-utils", + "solana-vote", + "solana-votor-messages", ] [[package]] @@ -6658,6 +6834,7 @@ dependencies = [ "solana-poh-config", "solana-pubkey", "solana-runtime", + "solana-sdk", "solana-time-utils", "solana-transaction", "thiserror 2.0.12", @@ -7297,6 +7474,7 @@ dependencies = [ "num-traits", "num_cpus", "num_enum", + "parking_lot 0.12.3", "percentage", "qualifier_attr", "rand 0.8.5", @@ -7307,8 +7485,10 @@ dependencies = [ "serde_json", "serde_with", "solana-accounts-db", + "solana-bls-signatures", "solana-bpf-loader-program", "solana-bucket-map", + "solana-build-alpenglow-vote", "solana-builtins", "solana-compute-budget", "solana-compute-budget-instruction", @@ -7318,6 +7498,7 @@ dependencies = [ "solana-fee", "solana-inline-spl", "solana-lattice-hash", + "solana-loader-v3-interface", "solana-measure", "solana-metrics", "solana-nohash-hasher", @@ -7340,6 +7521,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-votor-messages", "static_assertions", "strum", "strum_macros", @@ -7366,6 +7548,8 @@ dependencies = [ "solana-svm-transaction", "solana-transaction", "solana-transaction-error", + "solana-vote", + "solana-votor-messages", "thiserror 2.0.12", ] @@ -7636,12 +7820,12 @@ dependencies = [ [[package]] name = "solana-signature" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d251c8f3dc015f320b4161daac7f108156c837428e5a8cc61136d25beb11d6" +checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" dependencies = [ - "bs58", "ed25519-dalek", + "five8", "rand 0.8.5", "serde", "serde-big-array", @@ -7741,7 +7925,9 @@ dependencies = [ "solana-sysvar", "solana-transaction-context 2.3.0", "solana-type-overrides", + "solana-vote", "solana-vote-interface", + "solana-votor-messages", ] [[package]] @@ -8044,6 +8230,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-compute-budget", "solana-core", @@ -8353,6 +8540,7 @@ dependencies = [ "solana-sdk", "solana-streamer", "solana-tls-utils", + "solana-votor", "static_assertions", "thiserror 2.0.12", "tokio", @@ -8443,24 +8631,34 @@ dependencies = [ name = "solana-vote" version = "2.3.0" dependencies = [ + "bitvec", + "bytemuck", "itertools 0.12.1", "log", + "num-derive", + "num-traits", + "num_enum", "serde", "serde_derive", "solana-account", "solana-bincode", + "solana-bls-signatures", "solana-clock", "solana-hash", "solana-instruction", "solana-keypair", "solana-packet", + "solana-program", "solana-pubkey", "solana-sdk-ids", + "solana-serialize-utils", "solana-signature", "solana-signer", "solana-svm-transaction", "solana-transaction", "solana-vote-interface", + "solana-votor-messages", + "spl-pod", "thiserror 2.0.12", ] @@ -8519,6 +8717,61 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "solana-votor" +version = "2.3.0" +dependencies = [ + "anyhow", + "bincode", + "bitvec", + "bs58", + "crossbeam-channel", + "dashmap", + "etcd-client", + "itertools 0.12.1", + "log", + "qualifier_attr", + "rayon", + "serde", + "serde_bytes", + "serde_derive", + "solana-accounts-db", + "solana-bloom", + "solana-bls-signatures", + "solana-entry", + "solana-gossip", + "solana-ledger", + "solana-logger", + "solana-measure", + "solana-metrics", + "solana-pubkey", + "solana-rpc", + "solana-runtime", + "solana-sdk", + "solana-vote", + "solana-vote-program", + "solana-votor-messages", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-votor-messages" +version = "2.3.0" +dependencies = [ + "bitvec", + "bytemuck", + "num_enum", + "serde", + "solana-account", + "solana-bls-signatures", + "solana-hash", + "solana-logger", + "solana-program", + "solana-sdk", + "solana-vote-interface", + "spl-pod", +] + [[package]] name = "solana-wen-restart" version = "2.3.0" @@ -9189,6 +9442,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -9322,6 +9581,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.3.37" @@ -9546,11 +9814,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -9559,6 +9842,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.8.0", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -10471,6 +10756,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index d24401c13c..a02c3d4a8a 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -17,6 +17,7 @@ log = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true } solana-cli-output = { workspace = true } solana-compute-budget = { workspace = true } solana-core = { workspace = true } diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index 98ad356ff8..2d9b078c03 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -15,7 +15,7 @@ use { consensus::tower_storage::TowerStorage, validator::{Validator, ValidatorConfig, ValidatorStartProgress, ValidatorTpuConfig}, }, - solana_feature_set::FEATURE_NAMES, + solana_feature_set::{self, FEATURE_NAMES}, solana_geyser_plugin_manager::{ geyser_plugin_manager::GeyserPluginManager, GeyserPluginManagerRequest, }, @@ -856,12 +856,14 @@ impl TestValidator { &validator_identity.pubkey(), &validator_vote_account.pubkey(), &validator_stake_account.pubkey(), + None, validator_stake_lamports, validator_identity_lamports, config.fee_rate_governor.clone(), config.rent.clone(), solana_sdk::genesis_config::ClusterType::Development, accounts.into_iter().collect(), + None, ); genesis_config.epoch_schedule = config .epoch_schedule @@ -874,7 +876,10 @@ impl TestValidator { } for feature in feature_set { - genesis_utils::activate_feature(&mut genesis_config, feature); + // TODO remove this + if feature != solana_feature_set::secp256k1_program_enabled::id() { + genesis_utils::activate_feature(&mut genesis_config, feature); + } } let ledger_path = match &config.ledger_path { @@ -1229,6 +1234,8 @@ mod test { [ solana_sdk::feature_set::deprecate_rewards_sysvar::id(), solana_sdk::feature_set::disable_fees_sysvar::id(), + // TODO: remove this + solana_sdk::feature_set::secp256k1_program_enabled::id(), ] .into_iter() .for_each(|feature| { diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index 798d39062d..dd7e1f99d9 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -41,6 +41,7 @@ solana-runtime = { workspace = true } solana-sdk = { workspace = true } solana-streamer = { workspace = true } solana-tls-utils = { workspace = true } +solana-votor = { workspace = true } static_assertions = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/turbine/src/broadcast_stage.rs b/turbine/src/broadcast_stage.rs index 3be9cbc7c4..651f888269 100644 --- a/turbine/src/broadcast_stage.rs +++ b/turbine/src/broadcast_stage.rs @@ -34,6 +34,7 @@ use { sendmmsg::{batch_send, SendPktsError}, socket::SocketAddrSpace, }, + solana_votor::event::VotorEventSender, static_assertions::const_assert_eq, std::{ collections::{HashMap, HashSet}, @@ -121,6 +122,7 @@ impl BroadcastStageType { bank_forks: Arc>, shred_version: u16, quic_endpoint_sender: AsyncSender<(SocketAddr, Bytes)>, + votor_event_sender: VotorEventSender, ) -> BroadcastStage { match self { BroadcastStageType::Standard => BroadcastStage::new( @@ -132,6 +134,7 @@ impl BroadcastStageType { blockstore, bank_forks, quic_endpoint_sender, + votor_event_sender.clone(), StandardBroadcastRun::new(shred_version), ), @@ -144,6 +147,7 @@ impl BroadcastStageType { blockstore, bank_forks, quic_endpoint_sender, + votor_event_sender.clone(), FailEntryVerificationBroadcastRun::new(shred_version), ), @@ -156,6 +160,7 @@ impl BroadcastStageType { blockstore, bank_forks, quic_endpoint_sender, + votor_event_sender.clone(), BroadcastFakeShredsRun::new(0, shred_version), ), @@ -168,6 +173,7 @@ impl BroadcastStageType { blockstore, bank_forks, quic_endpoint_sender, + votor_event_sender.clone(), BroadcastDuplicatesRun::new(shred_version, config.clone()), ), } @@ -182,6 +188,7 @@ trait BroadcastRun { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + votor_event_sender: &VotorEventSender, ) -> Result<()>; fn transmit( &mut self, @@ -224,6 +231,7 @@ impl BroadcastStage { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + votor_event_sender: &VotorEventSender, mut broadcast_stage_run: impl BroadcastRun, ) -> BroadcastStageReturnType { loop { @@ -233,6 +241,7 @@ impl BroadcastStage { receiver, socket_sender, blockstore_sender, + votor_event_sender, ); let res = Self::handle_error(res, "run"); if let Some(res) = res { @@ -285,6 +294,7 @@ impl BroadcastStage { blockstore: Arc, bank_forks: Arc>, quic_endpoint_sender: AsyncSender<(SocketAddr, Bytes)>, + votor_event_sender: VotorEventSender, mut broadcast_stage_run: impl BroadcastRun + Send + 'static + Clone, ) -> Self { let (socket_sender, socket_receiver) = unbounded(); @@ -305,6 +315,7 @@ impl BroadcastStage { &receiver, &socket_sender_, &blockstore_sender, + &votor_event_sender, bs_run, ) }) @@ -693,6 +704,8 @@ pub mod test { let bank_forks = BankForks::new_rw_arc(bank); let bank = bank_forks.read().unwrap().root_bank(); + let (votor_event_sender, _) = unbounded(); + // Start up the broadcast stage let broadcast_service = BroadcastStage::new( leader_info.sockets.broadcast, @@ -703,6 +716,7 @@ pub mod test { blockstore.clone(), bank_forks, quic_endpoint_sender, + votor_event_sender, StandardBroadcastRun::new(0), ); diff --git a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs index f5d5c0dce0..6770ab9147 100644 --- a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs +++ b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs @@ -10,6 +10,7 @@ use { signature::{Keypair, Signature, Signer}, system_transaction, }, + solana_votor::event::VotorEventSender, std::collections::HashSet, }; @@ -82,6 +83,7 @@ impl BroadcastRun for BroadcastDuplicatesRun { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + _votor_event_sender: &VotorEventSender, ) -> Result<()> { // 1) Pull entries from banking stage let mut receive_results = broadcast_utils::recv_slot_entries(receiver)?; diff --git a/turbine/src/broadcast_stage/broadcast_fake_shreds_run.rs b/turbine/src/broadcast_stage/broadcast_fake_shreds_run.rs index 89485d6b5c..660b62de1d 100644 --- a/turbine/src/broadcast_stage/broadcast_fake_shreds_run.rs +++ b/turbine/src/broadcast_stage/broadcast_fake_shreds_run.rs @@ -4,6 +4,7 @@ use { solana_gossip::contact_info::ContactInfo, solana_ledger::shred::{self, ProcessShredsStats, ReedSolomonCache, Shredder}, solana_sdk::{hash::Hash, signature::Keypair}, + solana_votor::event::VotorEventSender, }; #[derive(Clone)] @@ -35,6 +36,7 @@ impl BroadcastRun for BroadcastFakeShredsRun { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + _votor_event_sender: &VotorEventSender, ) -> Result<()> { // 1) Pull entries from banking stage let receive_results = broadcast_utils::recv_slot_entries(receiver)?; diff --git a/turbine/src/broadcast_stage/broadcast_utils.rs b/turbine/src/broadcast_stage/broadcast_utils.rs index eeec94467f..2584f5db82 100644 --- a/turbine/src/broadcast_stage/broadcast_utils.rs +++ b/turbine/src/broadcast_stage/broadcast_utils.rs @@ -10,6 +10,7 @@ use { solana_poh::poh_recorder::WorkingBankEntry, solana_runtime::bank::Bank, solana_sdk::{clock::Slot, hash::Hash}, + solana_votor::event::{CompletedBlock, VotorEvent, VotorEventSender}, std::{ sync::Arc, time::{Duration, Instant}, @@ -119,6 +120,22 @@ pub(super) fn get_chained_merkle_root_from_parent( }) } +/// Set the block id on the bank and send it for consideration in voting +pub(super) fn set_block_id_and_send( + votor_event_sender: &VotorEventSender, + bank: Arc, + block_id: Hash, +) -> Result<()> { + bank.set_block_id(Some(block_id)); + if bank.is_frozen() { + votor_event_sender.send(VotorEvent::Block(CompletedBlock { + slot: bank.slot(), + bank, + }))?; + } + Ok(()) +} + #[cfg(test)] mod tests { use { diff --git a/turbine/src/broadcast_stage/fail_entry_verification_broadcast_run.rs b/turbine/src/broadcast_stage/fail_entry_verification_broadcast_run.rs index e9ed6a1a6e..e11b3f9cf6 100644 --- a/turbine/src/broadcast_stage/fail_entry_verification_broadcast_run.rs +++ b/turbine/src/broadcast_stage/fail_entry_verification_broadcast_run.rs @@ -3,6 +3,7 @@ use { crate::cluster_nodes::ClusterNodesCache, solana_ledger::shred::{ProcessShredsStats, ReedSolomonCache, Shredder}, solana_sdk::{hash::Hash, signature::Keypair}, + solana_votor::event::VotorEventSender, std::{thread::sleep, time::Duration}, tokio::sync::mpsc::Sender as AsyncSender, }; @@ -49,6 +50,7 @@ impl BroadcastRun for FailEntryVerificationBroadcastRun { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + _votor_event_sender: &VotorEventSender, ) -> Result<()> { // 1) Pull entries from banking stage let mut receive_results = broadcast_utils::recv_slot_entries(receiver)?; diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 654eed425c..86e8e8a115 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -6,12 +6,14 @@ use { *, }, crate::cluster_nodes::ClusterNodesCache, + crossbeam_channel::Sender, solana_entry::entry::Entry, solana_ledger::{ - blockstore, + blockstore::{self}, shred::{shred_code, ProcessShredsStats, ReedSolomonCache, Shred, ShredType, Shredder}, }, solana_sdk::{hash::Hash, signature::Keypair, timing::AtomicInterval}, + solana_votor::event::VotorEventSender, std::{borrow::Cow, sync::RwLock, time::Duration}, tokio::sync::mpsc::Sender as AsyncSender, }; @@ -162,7 +164,15 @@ impl StandardBroadcastRun { ) -> Result<()> { let (bsend, brecv) = unbounded(); let (ssend, srecv) = unbounded(); - self.process_receive_results(keypair, blockstore, &ssend, &bsend, receive_results)?; + let (cbsend, _cbrecv) = unbounded(); + self.process_receive_results( + keypair, + blockstore, + &ssend, + &bsend, + &cbsend, + receive_results, + )?; // Data and coding shreds are sent in a single batch. let _ = self.transmit(&srecv, cluster_info, sock, bank_forks, quic_endpoint_sender); let _ = self.record(&brecv, blockstore); @@ -175,6 +185,7 @@ impl StandardBroadcastRun { blockstore: &Blockstore, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + votor_event_sender: &VotorEventSender, receive_results: ReceiveResults, ) -> Result<()> { let mut receive_elapsed = receive_results.time_elapsed; @@ -318,6 +329,14 @@ impl StandardBroadcastRun { if last_tick_height == bank.max_tick_height() { self.report_and_reset_stats(false); self.completed = true; + + // Populate the block id and send for voting + // The block id is the merkle root of the last FEC set which is now the chained merkle root + broadcast_utils::set_block_id_and_send( + votor_event_sender, + bank.clone(), + self.chained_merkle_root, + )?; } Ok(()) @@ -434,6 +453,7 @@ impl BroadcastRun for StandardBroadcastRun { receiver: &Receiver, socket_sender: &Sender<(Arc>, Option)>, blockstore_sender: &Sender<(Arc>, Option)>, + votor_event_sender: &VotorEventSender, ) -> Result<()> { let receive_results = broadcast_utils::recv_slot_entries(receiver)?; // TODO: Confirm that last chunk of coding shreds @@ -443,6 +463,7 @@ impl BroadcastRun for StandardBroadcastRun { blockstore, socket_sender, blockstore_sender, + votor_event_sender, receive_results, ) } @@ -716,6 +737,7 @@ mod test { setup(num_shreds_per_slot); let (bsend, brecv) = unbounded(); let (ssend, _srecv) = unbounded(); + let (cbsend, _) = unbounded(); let mut last_tick_height = 0; let mut standard_broadcast_run = StandardBroadcastRun::new(0); let mut process_ticks = |num_ticks| { @@ -734,6 +756,7 @@ mod test { &blockstore, &ssend, &bsend, + &cbsend, receive_results, ) .unwrap(); diff --git a/unified-scheduler-pool/Cargo.toml b/unified-scheduler-pool/Cargo.toml index 091191229e..ff906d3574 100644 --- a/unified-scheduler-pool/Cargo.toml +++ b/unified-scheduler-pool/Cargo.toml @@ -42,6 +42,7 @@ solana-entry = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } solana-logger = { workspace = true } +solana-poh = { workspace = true, features = ["dev-context-only-utils"] } solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } solana-system-transaction = { workspace = true } test-case = { workspace = true } diff --git a/unified-scheduler-pool/src/lib.rs b/unified-scheduler-pool/src/lib.rs index a7bd294971..a3ad702f3b 100644 --- a/unified-scheduler-pool/src/lib.rs +++ b/unified-scheduler-pool/src/lib.rs @@ -328,6 +328,7 @@ where ) } + #[allow(clippy::too_many_arguments)] fn do_new( handler_count: Option, log_messages_bytes_limit: Option, diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 1ac4fa9aaf..b10f3507e5 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -65,6 +65,7 @@ solana-tpu-client = { workspace = true } solana-unified-scheduler-pool = { workspace = true } solana-version = { workspace = true } solana-vote-program = { workspace = true } +solana-votor = { workspace = true } symlink = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 6aa7b75eb6..13def9cf5f 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -160,6 +160,7 @@ fn main() { let ticks_per_slot = value_t!(matches, "ticks_per_slot", u64).ok(); let slots_per_epoch = value_t!(matches, "slots_per_epoch", Slot).ok(); let gossip_host = matches.value_of("gossip_host").map(|gossip_host| { + warn!("--gossip-host is deprecated. Use --bind-address instead."); solana_net_utils::parse_host(gossip_host).unwrap_or_else(|err| { eprintln!("Failed to parse --gossip-host: {err}"); exit(1); @@ -172,12 +173,24 @@ fn main() { exit(1); }) }); - let bind_address = matches.value_of("bind_address").map(|bind_address| { - solana_net_utils::parse_host(bind_address).unwrap_or_else(|err| { - eprintln!("Failed to parse --bind-address: {err}"); - exit(1); - }) + let bind_address = solana_net_utils::parse_host( + matches + .value_of("bind_address") + .expect("Bind address has default value"), + ) + .unwrap_or_else(|err| { + eprintln!("Failed to parse --bind-address: {err}"); + exit(1); }); + + let advertised_ip = if let Some(ip) = gossip_host { + ip + } else if !bind_address.is_unspecified() && !bind_address.is_loopback() { + bind_address + } else { + IpAddr::V4(Ipv4Addr::LOCALHOST) + }; + let compute_unit_limit = value_t!(matches, "compute_unit_limit", u64).ok(); let faucet_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), faucet_port); @@ -540,9 +553,7 @@ fn main() { genesis.rent = Rent::with_slots_per_epoch(slots_per_epoch); } - if let Some(gossip_host) = gossip_host { - genesis.gossip_host(gossip_host); - } + genesis.gossip_host(advertised_ip); if let Some(gossip_port) = gossip_port { genesis.gossip_port(gossip_port); @@ -552,9 +563,7 @@ fn main() { genesis.port_range(dynamic_port_range); } - if let Some(bind_address) = bind_address { - genesis.bind_ip_addr(bind_address); - } + genesis.bind_ip_addr(bind_address); if matches.is_present("geyser_plugin_config") { genesis.geyser_plugin_config_files = Some( diff --git a/validator/src/cli.rs b/validator/src/cli.rs index ad1469b4c2..82196c8a3a 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -844,10 +844,8 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .value_name("HOST") .takes_value(true) .validator(solana_net_utils::is_host) - .help( - "Gossip DNS name or IP address for the validator to advertise in gossip \ - [default: 127.0.0.1]", - ), + .hidden(hidden_unless_forced()) + .help("DEPRECATED: Use --bind-address instead."), ) .arg( Arg::with_name("dynamic_port_range") @@ -863,8 +861,8 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .value_name("HOST") .takes_value(true) .validator(solana_net_utils::is_host) - .default_value("0.0.0.0") - .help("IP address to bind the validator ports [default: 0.0.0.0]"), + .default_value("127.0.0.1") + .help("IP address to bind the validator ports [default: 127.0.0.1]"), ) .arg( Arg::with_name("clone_account") diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index f4284cfbdd..fde0dc1d04 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -344,10 +344,8 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .value_name("HOST") .takes_value(true) .validator(solana_net_utils::is_host) - .help( - "Gossip DNS name or IP address for the validator to advertise in gossip \ - [default: ask --entrypoint, or 127.0.0.1 when --entrypoint is not provided]", - ), + .hidden(hidden_unless_forced()) + .help("DEPRECATED: Use --bind-address instead."), ) .arg( Arg::with_name("public_tpu_addr") diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index ebadb90b91..fcc08754f1 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -67,6 +67,7 @@ use { solana_send_transaction_service::send_transaction_service, solana_streamer::{quic::QuicServerParams, socket::SocketAddrSpace}, solana_tpu_client::tpu_client::DEFAULT_TPU_ENABLE_UDP, + solana_votor::vote_history_storage, std::{ collections::HashSet, fs::{self, File}, @@ -396,6 +397,16 @@ pub fn execute( _ => unreachable!(), }; + let vote_history_storage: Arc = { + let vote_history_path = value_t!(matches, "vote_history_path", PathBuf) + .ok() + .unwrap_or_else(|| ledger_path.clone()); + + Arc::new(vote_history_storage::FileVoteHistoryStorage::new( + vote_history_path, + )) + }; + let mut accounts_index_config = AccountsIndexConfig { started_from_validator: true, // this is the only place this is set num_flush_threads: Some(accounts_index_flush_threads), @@ -628,6 +639,7 @@ pub fn execute( let mut validator_config = ValidatorConfig { require_tower: matches.is_present("require_tower"), tower_storage, + vote_history_storage, halt_at_slot: value_t!(matches, "dev_halt_at_slot", Slot).ok(), expected_genesis_hash: matches .value_of("expected_genesis_hash") @@ -1096,45 +1108,49 @@ pub fn execute( }, ); - let gossip_host: IpAddr = matches + let gossip_host = matches .value_of("gossip_host") .map(|gossip_host| { + warn!("--gossip-host is deprecated. Use --bind-address or rely on automatic public IP discovery instead."); solana_net_utils::parse_host(gossip_host) .map_err(|err| format!("failed to parse --gossip-host: {err}")) }) - .transpose()? - .or_else(|| { - if !entrypoint_addrs.is_empty() { - let mut order: Vec<_> = (0..entrypoint_addrs.len()).collect(); - order.shuffle(&mut thread_rng()); - // Return once we determine our IP from an entrypoint - order.into_iter().find_map(|i| { - let entrypoint_addr = &entrypoint_addrs[i]; - info!( - "Contacting {} to determine the validator's public IP address", - entrypoint_addr - ); - solana_net_utils::get_public_ip_addr_with_binding(entrypoint_addr, bind_address) - .map_or_else( - |err| { - warn!( - "Failed to contact cluster entrypoint {entrypoint_addr}: {err}" - ); - None - }, - Some, - ) - }) - } else { - Some(IpAddr::V4(Ipv4Addr::LOCALHOST)) - } - }) - .ok_or_else(|| "unable to determine the validator's public IP address".to_string())?; + .transpose()?; + + let advertised_ip = if let Some(ip) = gossip_host { + ip + } else if !bind_address.is_unspecified() && !bind_address.is_loopback() { + bind_address + } else if !entrypoint_addrs.is_empty() { + let mut order: Vec<_> = (0..entrypoint_addrs.len()).collect(); + order.shuffle(&mut thread_rng()); + + order + .into_iter() + .find_map(|i| { + let entrypoint_addr = &entrypoint_addrs[i]; + info!( + "Contacting {} to determine the validator's public IP address", + entrypoint_addr + ); + solana_net_utils::get_public_ip_addr_with_binding(entrypoint_addr, bind_address) + .map_or_else( + |err| { + warn!("Failed to contact cluster entrypoint {entrypoint_addr}: {err}"); + None + }, + Some, + ) + }) + .ok_or_else(|| "unable to determine the validator's public IP address".to_string())? + } else { + IpAddr::V4(Ipv4Addr::LOCALHOST) + }; let gossip_port = value_t!(matches, "gossip_port", u16).or_else(|_| { solana_net_utils::find_available_port_in_range(bind_address, (0, 1)) .map_err(|err| format!("unable to find an available gossip port: {err}")) })?; - let gossip_addr = SocketAddr::new(gossip_host, gossip_port); + let gossip_addr = SocketAddr::new(advertised_ip, gossip_port); let public_tpu_addr = matches .value_of("public_tpu_addr") @@ -1198,6 +1214,7 @@ pub fn execute( node.info.remove_tpu_forwards(); node.info.remove_tvu(); node.info.remove_serve_repair(); + node.info.remove_alpenglow(); // A node in this configuration shouldn't be an entrypoint to other nodes node.sockets.ip_echo = None; diff --git a/vortexor/src/vortexor.rs b/vortexor/src/vortexor.rs index b2230bc191..aca31eaf02 100644 --- a/vortexor/src/vortexor.rs +++ b/vortexor/src/vortexor.rs @@ -1,7 +1,7 @@ use { crossbeam_channel::{Receiver, Sender}, solana_core::{ - banking_trace::TracedSender, sigverify::TransactionSigVerifier, + banking_trace::TracedSender, sigverifier::ed25519_sigverifier::TransactionSigVerifier, sigverify_stage::SigVerifyStage, }, solana_net_utils::{bind_in_range_with_config, bind_more_with_config, SocketConfig}, diff --git a/vote/Cargo.toml b/vote/Cargo.toml index bd29d3080b..4b225b363c 100644 --- a/vote/Cargo.toml +++ b/vote/Cargo.toml @@ -10,13 +10,20 @@ license = { workspace = true } edition = { workspace = true } [dependencies] +bincode = { workspace = true, optional = true } +bitvec = { workspace = true } +bytemuck = { workspace = true } itertools = { workspace = true } log = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +num_enum = { workspace = true } rand = { workspace = true, optional = true } serde = { workspace = true, features = ["rc"] } serde_derive = { workspace = true } solana-account = { workspace = true, features = ["bincode"] } solana-bincode = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["bytemuck"] } solana-clock = { workspace = true } solana-frozen-abi = { workspace = true, optional = true, features = [ "frozen-abi", @@ -28,13 +35,17 @@ solana-hash = { workspace = true } solana-instruction = { workspace = true } solana-keypair = { workspace = true } solana-packet = { workspace = true } +solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-sdk-ids = { workspace = true } +solana-serialize-utils = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-svm-transaction = { workspace = true } solana-transaction = { workspace = true, features = ["bincode"] } solana-vote-interface = { workspace = true, features = ["bincode"] } +solana-votor-messages = { workspace = true } +spl-pod = { workspace = true } thiserror = { workspace = true } [lib] @@ -42,19 +53,25 @@ crate-type = ["lib"] name = "solana_vote" [dev-dependencies] +arbitrary = { workspace = true } bincode = { workspace = true } rand = { workspace = true } +serial_test = { workspace = true } solana-keypair = { workspace = true } solana-logger = { workspace = true } +solana-sdk = { workspace = true } solana-sha256-hasher = { workspace = true } solana-signer = { workspace = true } solana-transaction = { workspace = true, features = ["bincode"] } +solana-vote-interface = { workspace = true, features = ["bincode", "dev-context-only-utils"] } +static_assertions = { workspace = true } +test-case = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] [features] -dev-context-only-utils = ["dep:rand"] +dev-context-only-utils = ["dep:rand", "dep:bincode"] frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"] [lints] diff --git a/vote/benches/vote_account.rs b/vote/benches/vote_account.rs index 47f7c8a788..ea576c6f96 100644 --- a/vote/benches/vote_account.rs +++ b/vote/benches/vote_account.rs @@ -27,7 +27,8 @@ fn new_rand_vote_account( leader_schedule_epoch: rng.gen(), unix_timestamp: rng.gen(), }; - let vote_state = VoteState::new(&vote_init, &clock); + let mut vote_state = VoteState::new(&vote_init, &clock); + vote_state.process_next_vote_slot(0, 0, 1); let account = AccountSharedData::new_data( rng.gen(), // lamports &VoteStateVersions::new_current(vote_state.clone()), @@ -44,7 +45,11 @@ fn bench_vote_account_try_from(b: &mut Bencher) { b.iter(|| { let vote_account = VoteAccount::try_from(account.clone()).unwrap(); - let state = vote_account.vote_state(); - assert_eq!(state, &vote_state); + let vote_state_view = vote_account.vote_state_view().unwrap(); + assert_eq!(&vote_state.node_pubkey, vote_state_view.node_pubkey()); + assert_eq!(vote_state.commission, vote_state_view.commission()); + assert_eq!(vote_state.credits(), vote_state_view.credits()); + assert_eq!(vote_state.last_timestamp, vote_state_view.last_timestamp()); + assert_eq!(vote_state.root_slot, vote_state_view.root_slot()); }); } diff --git a/vote/src/alpenglow/certificate.rs b/vote/src/alpenglow/certificate.rs new file mode 100644 index 0000000000..43a41151aa --- /dev/null +++ b/vote/src/alpenglow/certificate.rs @@ -0,0 +1,32 @@ +//! Define BLS certificate to be sent all to all in Alpenglow +use { + serde::{Deserialize, Serialize}, + solana_hash::Hash, + solana_program::clock::Slot, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +/// Certificate Type in Alpenglow +pub enum CertificateType { + /// Finalize slow: at least 60 percent Finalize + Finalize, + /// Finalize fast: at least 80 percent Notarize + FinalizeFast, + /// Notarize: at least 60 percent Notarize + Notarize, + /// Notarize fallback: at least 60 percent Notarize or NotarizeFallback + NotarizeFallback, + /// Skip: at least 60 percent Skip or SkipFallback + Skip, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +/// Certificate Type in Alpenglow +pub struct Certificate { + /// Certificate type + pub certificate_type: CertificateType, + /// The slot of the block + pub slot: Slot, + /// The block id of the block + pub block_id: Option, +} diff --git a/vote/src/lib.rs b/vote/src/lib.rs index b496dde973..5daa6e7d3a 100644 --- a/vote/src/lib.rs +++ b/vote/src/lib.rs @@ -3,6 +3,7 @@ pub mod vote_account; pub mod vote_parser; +pub mod vote_state_view; pub mod vote_transaction; #[cfg_attr(feature = "frozen-abi", macro_use)] diff --git a/vote/src/vote_account.rs b/vote/src/vote_account.rs index b119778b23..820bcdd914 100644 --- a/vote/src/vote_account.rs +++ b/vote/src/vote_account.rs @@ -1,24 +1,33 @@ use { + crate::vote_state_view::VoteStateView, itertools::Itertools, serde::{ de::{MapAccess, Visitor}, ser::{Serialize, Serializer}, }, solana_account::{AccountSharedData, ReadableAccount}, + solana_bls_signatures::Pubkey as BLSPubkey, solana_instruction::error::InstructionError, + solana_program::program_error::ProgramError, solana_pubkey::Pubkey, - solana_vote_interface::state::VoteState, + solana_vote_interface::state::BlockTimestamp, + solana_votor_messages::state::VoteState as AlpenglowVoteState, std::{ cmp::Ordering, collections::{hash_map::Entry, HashMap}, fmt, iter::FromIterator, - mem::{self, MaybeUninit}, - ptr::addr_of_mut, + mem, sync::{Arc, OnceLock}, }, thiserror::Error, }; +// The following imports are only needed for dev-context-only-utils. +#[cfg(feature = "dev-context-only-utils")] +use { + solana_account::WritableAccount, + solana_vote_interface::state::{VoteState, VoteStateVersions}, +}; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone, Debug, PartialEq)] @@ -30,13 +39,22 @@ pub enum Error { InstructionError(#[from] InstructionError), #[error("Invalid vote account owner: {0}")] InvalidOwner(/*owner:*/ Pubkey), + #[error(transparent)] + ProgramError(#[from] ProgramError), +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Debug)] +enum VoteAccountState { + TowerBFT(VoteStateView), + Alpenglow, } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Debug)] struct VoteAccountInner { account: AccountSharedData, - vote_state: VoteState, + vote_state_view: VoteAccountState, } pub type VoteAccountsHashMap = HashMap; @@ -83,13 +101,108 @@ impl VoteAccount { self.0.account.owner() } - pub fn vote_state(&self) -> &VoteState { - &self.0.vote_state + pub fn vote_state_view(&self) -> Option<&VoteStateView> { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => Some(vote_state_view), + _ => None, + } + } + + pub fn alpenglow_vote_state(&self) -> Option<&AlpenglowVoteState> { + match &self.0.vote_state_view { + VoteAccountState::Alpenglow => { + AlpenglowVoteState::deserialize(self.0.account.data()).ok() + } + _ => None, + } } /// VoteState.node_pubkey of this vote-account. pub fn node_pubkey(&self) -> &Pubkey { - &self.0.vote_state.node_pubkey + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => vote_state_view.node_pubkey(), + VoteAccountState::Alpenglow => AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .node_pubkey(), + } + } + + pub fn commission(&self) -> u8 { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => vote_state_view.commission(), + VoteAccountState::Alpenglow => AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .commission(), + } + } + + pub fn credits(&self) -> u64 { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => vote_state_view.credits(), + VoteAccountState::Alpenglow => AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .epoch_credits() + .credits(), + } + } + + pub fn epoch_credits(&self) -> Box + '_> { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => { + Box::new(vote_state_view.epoch_credits_iter().map(|epoch_credits| { + ( + epoch_credits.epoch(), + epoch_credits.credits(), + epoch_credits.prev_credits(), + ) + })) + } + VoteAccountState::Alpenglow => Box::new( + std::iter::once( + AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .epoch_credits(), + ) + .map(|epoch_credits| { + ( + epoch_credits.epoch(), + epoch_credits.credits(), + epoch_credits.prev_credits(), + ) + }), + ), + } + } + + pub fn get_authorized_voter(&self, epoch: u64) -> Option { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => { + vote_state_view.get_authorized_voter(epoch).copied() + } + VoteAccountState::Alpenglow => AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .get_authorized_voter(epoch), + } + } + + pub fn last_timestamp(&self) -> BlockTimestamp { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(vote_state_view) => vote_state_view.last_timestamp(), + VoteAccountState::Alpenglow => AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .latest_timestamp_legacy_format(), + } + } + + pub fn bls_pubkey(&self) -> Option<&BLSPubkey> { + match &self.0.vote_state_view { + VoteAccountState::TowerBFT(_) => None, + VoteAccountState::Alpenglow => Some( + AlpenglowVoteState::deserialize(self.0.account.data()) + .unwrap() + .bls_pubkey(), + ), + } } #[cfg(feature = "dev-context-only-utils")] @@ -97,7 +210,7 @@ impl VoteAccount { use { rand::Rng as _, solana_clock::Clock, - solana_vote_interface::state::{VoteInit, VoteStateVersions}, + solana_vote_interface::state::{VoteInit, VoteState, VoteStateVersions}, }; let mut rng = rand::thread_rng(); @@ -125,6 +238,30 @@ impl VoteAccount { VoteAccount::try_from(account).unwrap() } + + #[cfg(feature = "dev-context-only-utils")] + pub fn new_from_vote_state(vote_state: &VoteState) -> VoteAccount { + let account = AccountSharedData::new_data( + 100, // lamports + &VoteStateVersions::new_current(vote_state.clone()), + &solana_sdk_ids::vote::id(), // owner + ) + .unwrap(); + + VoteAccount::try_from(account).unwrap() + } + + #[cfg(feature = "dev-context-only-utils")] + pub fn new_from_alpenglow_vote_state(vote_state: &AlpenglowVoteState) -> VoteAccount { + let mut account = AccountSharedData::new( + 100, // lamports + AlpenglowVoteState::size(), + &solana_votor_messages::id(), + ); + vote_state.serialize_into(account.data_as_mut_slice()); + + VoteAccount::try_from(account).unwrap() + } } impl VoteAccounts { @@ -321,50 +458,24 @@ impl From for AccountSharedData { impl TryFrom for VoteAccount { type Error = Error; fn try_from(account: AccountSharedData) -> Result { - if !solana_sdk_ids::vote::check_id(account.owner()) { - return Err(Error::InvalidOwner(*account.owner())); - } - - // Allocate as Arc> so we can initialize in place. - let mut inner = Arc::new(MaybeUninit::::uninit()); - let inner_ptr = Arc::get_mut(&mut inner) - .expect("we're the only ref") - .as_mut_ptr(); - - // Safety: - // - All the addr_of_mut!(...).write(...) calls are valid since we just allocated and so - // the field pointers are valid. - // - We use write() so that the old values aren't dropped since they're still - // uninitialized. - unsafe { - let vote_state = addr_of_mut!((*inner_ptr).vote_state); - // Safety: - // - vote_state is non-null and MaybeUninit is guaranteed to have same layout - // and alignment as VoteState. - // - Here it is safe to create a reference to MaybeUninit since the value is - // aligned and MaybeUninit is valid for all possible bit values. - let vote_state = &mut *(vote_state as *mut MaybeUninit); - - // Try to deserialize in place - if let Err(e) = VoteState::deserialize_into_uninit(account.data(), vote_state) { - // Safety: - // - Deserialization failed so at this point vote_state is uninitialized and must - // not be dropped. We're ok since `vote_state` is a subfield of `inner` which is - // still MaybeUninit - which isn't dropped by definition - and so neither are its - // subfields. - return Err(e.into()); - } - - // Write the account field which completes the initialization of VoteAccountInner. - addr_of_mut!((*inner_ptr).account).write(account); - - // Safety: - // - At this point both `inner.vote_state` and `inner.account`` are initialized, so it's safe to - // transmute the MaybeUninit to VoteAccountInner. - Ok(VoteAccount(mem::transmute::< - Arc>, - Arc, - >(inner))) + if solana_sdk_ids::vote::check_id(account.owner()) { + Ok(Self(Arc::new(VoteAccountInner { + vote_state_view: VoteAccountState::TowerBFT( + VoteStateView::try_new(account.data_clone()).map_err(|_| { + Error::InstructionError(InstructionError::InvalidAccountData) + })?, + ), + account, + }))) + } else if solana_votor_messages::check_id(account.owner()) { + // Even though we don't copy data, should verify we can successfully deserialize. + let _ = AlpenglowVoteState::deserialize(account.data())?; + Ok(Self(Arc::new(VoteAccountInner { + vote_state_view: VoteAccountState::Alpenglow, + account, + }))) + } else { + Err(Error::InvalidOwner(*account.owner())) } } } @@ -373,7 +484,7 @@ impl PartialEq for VoteAccountInner { fn eq(&self, other: &Self) -> bool { let Self { account, - vote_state: _, + vote_state_view: _, } = self; account == &other.account } @@ -482,16 +593,17 @@ mod tests { bincode::Options, rand::Rng, solana_account::WritableAccount, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_clock::Clock, solana_pubkey::Pubkey, - solana_vote_interface::state::{VoteInit, VoteStateVersions}, + solana_vote_interface::state::{VoteInit, VoteState, VoteStateVersions}, std::iter::repeat_with, }; fn new_rand_vote_account( rng: &mut R, node_pubkey: Option, - ) -> (AccountSharedData, VoteState) { + ) -> AccountSharedData { let vote_init = VoteInit { node_pubkey: node_pubkey.unwrap_or_else(Pubkey::new_unique), authorized_voter: Pubkey::new_unique(), @@ -506,23 +618,49 @@ mod tests { unix_timestamp: rng.gen(), }; let vote_state = VoteState::new(&vote_init, &clock); - let account = AccountSharedData::new_data( + AccountSharedData::new_data( rng.gen(), // lamports &VoteStateVersions::new_current(vote_state.clone()), &solana_sdk_ids::vote::id(), // owner ) - .unwrap(); - (account, vote_state) + .unwrap() + } + + fn new_rand_alpenglow_vote_account( + rng: &mut R, + node_pubkey: Option, + ) -> (AccountSharedData, AlpenglowVoteState) { + let bls_keypair = BLSKeypair::new(); + let alpenglow_vote_state = AlpenglowVoteState::new_for_tests( + node_pubkey.unwrap_or_else(Pubkey::new_unique), + Pubkey::new_unique(), + rng.gen(), + Pubkey::new_unique(), + rng.gen(), + bls_keypair.public.into(), + ); + let mut account = AccountSharedData::new( + rng.gen(), // lamports + AlpenglowVoteState::size(), + &solana_votor_messages::id(), + ); + alpenglow_vote_state.serialize_into(account.data_as_mut_slice()); + (account, alpenglow_vote_state) } fn new_rand_vote_accounts( rng: &mut R, num_nodes: usize, + num_alpenglow_nodes: usize, ) -> impl Iterator + '_ { let nodes: Vec<_> = repeat_with(Pubkey::new_unique).take(num_nodes).collect(); repeat_with(move || { let node = nodes[rng.gen_range(0..nodes.len())]; - let (account, _) = new_rand_vote_account(rng, Some(node)); + let account = if rng.gen_ratio(num_alpenglow_nodes as u32, num_nodes as u32) { + new_rand_alpenglow_vote_account(rng, Some(node)).0 + } else { + new_rand_vote_account(rng, Some(node)) + }; let stake = rng.gen_range(0..997); let vote_account = VoteAccount::try_from(account).unwrap(); (Pubkey::new_unique(), (stake, vote_account)) @@ -549,11 +687,24 @@ mod tests { #[test] fn test_vote_account_try_from() { let mut rng = rand::thread_rng(); - let (account, vote_state) = new_rand_vote_account(&mut rng, None); + let account = new_rand_vote_account(&mut rng, None); let lamports = account.lamports(); let vote_account = VoteAccount::try_from(account.clone()).unwrap(); assert_eq!(lamports, vote_account.lamports()); - assert_eq!(vote_state, *vote_account.vote_state()); + assert_eq!(&account, vote_account.account()); + } + + #[test] + fn test_alpenglow_vote_account_try_from() { + let mut rng = rand::thread_rng(); + let (account, alpenglow_vote_state) = new_rand_alpenglow_vote_account(&mut rng, None); + let lamports = account.lamports(); + let vote_account = VoteAccount::try_from(account.clone()).unwrap(); + assert_eq!(lamports, vote_account.lamports()); + assert_eq!( + alpenglow_vote_state, + *vote_account.alpenglow_vote_state().unwrap() + ); assert_eq!(&account, vote_account.account()); } @@ -561,7 +712,7 @@ mod tests { #[should_panic(expected = "InvalidOwner")] fn test_vote_account_try_from_invalid_owner() { let mut rng = rand::thread_rng(); - let (mut account, _) = new_rand_vote_account(&mut rng, None); + let mut account = new_rand_vote_account(&mut rng, None); account.set_owner(Pubkey::new_unique()); VoteAccount::try_from(account).unwrap(); } @@ -574,12 +725,35 @@ mod tests { VoteAccount::try_from(account).unwrap(); } + #[test] + #[should_panic(expected = "InvalidArgument")] + fn test_vote_account_try_from_invalid_alpenglow_account() { + let mut account = AccountSharedData::default(); + account.set_owner(solana_votor_messages::id()); + VoteAccount::try_from(account).unwrap(); + } + #[test] fn test_vote_account_serialize() { let mut rng = rand::thread_rng(); - let (account, vote_state) = new_rand_vote_account(&mut rng, None); + let account = new_rand_vote_account(&mut rng, None); let vote_account = VoteAccount::try_from(account.clone()).unwrap(); - assert_eq!(vote_state, *vote_account.vote_state()); + // Assert that VoteAccount has the same wire format as Account. + assert_eq!( + bincode::serialize(&account).unwrap(), + bincode::serialize(&vote_account).unwrap() + ); + } + + #[test] + fn test_alpenglow_vote_account_serialize() { + let mut rng = rand::thread_rng(); + let (account, alpenglow_vote_state) = new_rand_alpenglow_vote_account(&mut rng, None); + let vote_account = VoteAccount::try_from(account.clone()).unwrap(); + assert_eq!( + alpenglow_vote_state, + *vote_account.alpenglow_vote_state().unwrap() + ); // Assert that VoteAccount has the same wire format as Account. assert_eq!( bincode::serialize(&account).unwrap(), @@ -590,8 +764,9 @@ mod tests { #[test] fn test_vote_accounts_serialize() { let mut rng = rand::thread_rng(); - let vote_accounts_hash_map: VoteAccountsHashMap = - new_rand_vote_accounts(&mut rng, 64).take(1024).collect(); + let vote_accounts_hash_map: VoteAccountsHashMap = new_rand_vote_accounts(&mut rng, 64, 32) + .take(1024) + .collect(); let vote_accounts = VoteAccounts::from(Arc::new(vote_accounts_hash_map.clone())); assert!(vote_accounts.staked_nodes().len() > 32); assert_eq!( @@ -609,8 +784,9 @@ mod tests { #[test] fn test_vote_accounts_deserialize() { let mut rng = rand::thread_rng(); - let vote_accounts_hash_map: VoteAccountsHashMap = - new_rand_vote_accounts(&mut rng, 64).take(1024).collect(); + let vote_accounts_hash_map: VoteAccountsHashMap = new_rand_vote_accounts(&mut rng, 64, 32) + .take(1024) + .collect(); let data = bincode::serialize(&vote_accounts_hash_map).unwrap(); let vote_accounts: VoteAccounts = bincode::deserialize(&data).unwrap(); assert!(vote_accounts.staked_nodes().len() > 32); @@ -629,7 +805,7 @@ mod tests { // the valid one after deserialiation let mut vote_accounts_hash_map = HashMap::::new(); - let (valid_account, _) = new_rand_vote_account(&mut rng, None); + let valid_account = new_rand_vote_account(&mut rng, None); vote_accounts_hash_map.insert(Pubkey::new_unique(), (0xAA, valid_account.clone())); // bad data @@ -658,7 +834,9 @@ mod tests { #[test] fn test_staked_nodes() { let mut rng = rand::thread_rng(); - let mut accounts: Vec<_> = new_rand_vote_accounts(&mut rng, 64).take(1024).collect(); + let mut accounts: Vec<_> = new_rand_vote_accounts(&mut rng, 64, 32) + .take(1024) + .collect(); let mut vote_accounts = VoteAccounts::default(); // Add vote accounts. for (k, (pubkey, (stake, vote_account))) in accounts.iter().enumerate() { @@ -706,14 +884,17 @@ mod tests { assert!(vote_accounts.staked_nodes.get().unwrap().is_empty()); } - #[test] - fn test_staked_nodes_update() { + fn test_staked_nodes_update(is_alpenglow: bool) { let mut vote_accounts = VoteAccounts::default(); let mut rng = rand::thread_rng(); let pubkey = Pubkey::new_unique(); let node_pubkey = Pubkey::new_unique(); - let (account1, _) = new_rand_vote_account(&mut rng, Some(node_pubkey)); + let account1 = if is_alpenglow { + new_rand_alpenglow_vote_account(&mut rng, Some(node_pubkey)).0 + } else { + new_rand_vote_account(&mut rng, Some(node_pubkey)) + }; let vote_account1 = VoteAccount::try_from(account1).unwrap(); // first insert @@ -733,7 +914,11 @@ mod tests { assert_eq!(vote_accounts.staked_nodes().get(&node_pubkey), Some(&42)); // update with changed state, same node pubkey - let (account2, _) = new_rand_vote_account(&mut rng, Some(node_pubkey)); + let account2 = if is_alpenglow { + new_rand_alpenglow_vote_account(&mut rng, Some(node_pubkey)).0 + } else { + new_rand_vote_account(&mut rng, Some(node_pubkey)) + }; let vote_account2 = VoteAccount::try_from(account2).unwrap(); let ret = vote_accounts.insert(pubkey, vote_account2.clone(), || { panic!("should not be called") @@ -746,7 +931,11 @@ mod tests { // update with new node pubkey, stake must be moved let new_node_pubkey = Pubkey::new_unique(); - let (account3, _) = new_rand_vote_account(&mut rng, Some(new_node_pubkey)); + let account3 = if is_alpenglow { + new_rand_alpenglow_vote_account(&mut rng, Some(new_node_pubkey)).0 + } else { + new_rand_vote_account(&mut rng, Some(new_node_pubkey)) + }; let vote_account3 = VoteAccount::try_from(account3).unwrap(); let ret = vote_accounts.insert(pubkey, vote_account3.clone(), || { panic!("should not be called") @@ -760,13 +949,22 @@ mod tests { } #[test] - fn test_staked_nodes_zero_stake() { + fn test_staked_nodes_updates() { + test_staked_nodes_update(false); + test_staked_nodes_update(true); + } + + fn test_staked_nodes_zero_stake(is_alpenglow: bool) { let mut vote_accounts = VoteAccounts::default(); let mut rng = rand::thread_rng(); let pubkey = Pubkey::new_unique(); let node_pubkey = Pubkey::new_unique(); - let (account1, _) = new_rand_vote_account(&mut rng, Some(node_pubkey)); + let account1 = if is_alpenglow { + new_rand_alpenglow_vote_account(&mut rng, Some(node_pubkey)).0 + } else { + new_rand_vote_account(&mut rng, Some(node_pubkey)) + }; let vote_account1 = VoteAccount::try_from(account1).unwrap(); // we call this here to initialize VoteAccounts::staked_nodes which is a OnceLock @@ -779,7 +977,11 @@ mod tests { // update with new node pubkey, stake is 0 and should remain 0 let new_node_pubkey = Pubkey::new_unique(); - let (account2, _) = new_rand_vote_account(&mut rng, Some(new_node_pubkey)); + let account2 = if is_alpenglow { + new_rand_alpenglow_vote_account(&mut rng, Some(new_node_pubkey)).0 + } else { + new_rand_vote_account(&mut rng, Some(new_node_pubkey)) + }; let vote_account2 = VoteAccount::try_from(account2).unwrap(); let ret = vote_accounts.insert(pubkey, vote_account2.clone(), || { panic!("should not be called") @@ -790,11 +992,17 @@ mod tests { assert_eq!(vote_accounts.staked_nodes().get(&new_node_pubkey), None); } + #[test] + fn test_staked_nodes_zero_stakes() { + test_staked_nodes_zero_stake(false); + test_staked_nodes_zero_stake(true); + } + // Asserts that returned staked-nodes are copy-on-write references. #[test] fn test_staked_nodes_cow() { let mut rng = rand::thread_rng(); - let mut accounts = new_rand_vote_accounts(&mut rng, 64); + let mut accounts = new_rand_vote_accounts(&mut rng, 64, 32); // Add vote accounts. let mut vote_accounts = VoteAccounts::default(); for (pubkey, (stake, vote_account)) in (&mut accounts).take(1024) { @@ -826,7 +1034,7 @@ mod tests { #[test] fn test_vote_accounts_cow() { let mut rng = rand::thread_rng(); - let mut accounts = new_rand_vote_accounts(&mut rng, 64); + let mut accounts = new_rand_vote_accounts(&mut rng, 64, 32); // Add vote accounts. let mut vote_accounts = VoteAccounts::default(); for (pubkey, (stake, vote_account)) in (&mut accounts).take(1024) { diff --git a/vote/src/vote_parser.rs b/vote/src/vote_parser.rs index 55fc07222c..0eaaf453e8 100644 --- a/vote/src/vote_parser.rs +++ b/vote/src/vote_parser.rs @@ -1,11 +1,99 @@ use { crate::vote_transaction::VoteTransaction, solana_bincode::limited_deserialize, - solana_hash::Hash, solana_pubkey::Pubkey, solana_signature::Signature, + solana_clock::Slot, solana_hash::Hash, solana_pubkey::Pubkey, solana_signature::Signature, solana_svm_transaction::svm_transaction::SVMTransaction, solana_transaction::Transaction, solana_vote_interface::instruction::VoteInstruction, + solana_votor_messages::vote::Vote as AlpenglowVote, }; -pub type ParsedVote = (Pubkey, VoteTransaction, Option, Signature); +/// Represents a parsed vote transaction, which can be either a traditional Tower +/// vote or an Alpenglow vote. This enum allows unified handling of different vote types. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum ParsedVoteTransaction { + Tower(VoteTransaction), + Alpenglow(AlpenglowVote), +} + +impl ParsedVoteTransaction { + pub fn slots(&self) -> Vec { + match self { + ParsedVoteTransaction::Tower(tx) => tx.slots(), + ParsedVoteTransaction::Alpenglow(tx) => vec![tx.slot()], + } + } + pub fn last_voted_slot(&self) -> Option { + match self { + ParsedVoteTransaction::Tower(tx) => tx.last_voted_slot(), + ParsedVoteTransaction::Alpenglow(tx) => Some(tx.slot()), + } + } + + pub fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)> { + match self { + ParsedVoteTransaction::Tower(tx) => tx.last_voted_slot_hash(), + ParsedVoteTransaction::Alpenglow(tx) => match tx { + AlpenglowVote::Notarize(vote) => Some((vote.slot(), Hash::default())), + AlpenglowVote::Finalize(_vote) => None, + AlpenglowVote::NotarizeFallback(vote) => Some((vote.slot(), Hash::default())), + AlpenglowVote::Skip(_vote) => None, + AlpenglowVote::SkipFallback(_vote) => None, + }, + } + } + + pub fn is_alpenglow_vote(&self) -> bool { + matches!(self, ParsedVoteTransaction::Alpenglow(_)) + } + + pub fn is_full_tower_vote(&self) -> bool { + match self { + ParsedVoteTransaction::Tower(tx) => tx.is_full_tower_vote(), + ParsedVoteTransaction::Alpenglow(_tx) => false, + } + } + + pub fn as_tower_transaction(self) -> Option { + match self { + ParsedVoteTransaction::Tower(tx) => Some(tx), + ParsedVoteTransaction::Alpenglow(_tx) => None, + } + } + + pub fn as_alpenglow_transaction(self) -> Option { + match self { + ParsedVoteTransaction::Tower(_tx) => None, + ParsedVoteTransaction::Alpenglow(tx) => Some(tx), + } + } + + pub fn as_tower_transaction_ref(&self) -> Option<&VoteTransaction> { + match self { + ParsedVoteTransaction::Tower(tx) => Some(tx), + ParsedVoteTransaction::Alpenglow(_tx) => None, + } + } + + pub fn as_alpenglow_transaction_ref(&self) -> Option<&AlpenglowVote> { + match self { + ParsedVoteTransaction::Tower(_tx) => None, + ParsedVoteTransaction::Alpenglow(tx) => Some(tx), + } + } +} + +impl From for ParsedVoteTransaction { + fn from(value: VoteTransaction) -> Self { + ParsedVoteTransaction::Tower(value) + } +} + +impl From for ParsedVoteTransaction { + fn from(value: solana_votor_messages::vote::Vote) -> Self { + ParsedVoteTransaction::Alpenglow(value) + } +} + +pub type ParsedVote = (Pubkey, ParsedVoteTransaction, Option, Signature); // Used for locally forwarding processed vote transactions to consensus pub fn parse_sanitized_vote_transaction(tx: &impl SVMTransaction) -> Option { @@ -18,7 +106,12 @@ pub fn parse_sanitized_vote_transaction(tx: &impl SVMTransaction) -> Option Option { let key = message.account_keys.get(first_account)?; let (vote, switch_proof_hash) = parse_vote_instruction_data(&first_instruction.data)?; let signature = tx.signatures.first().cloned().unwrap_or_default(); - Some((*key, vote, switch_proof_hash, signature)) + Some(( + *key, + ParsedVoteTransaction::Tower(vote), + switch_proof_hash, + signature, + )) } fn parse_vote_instruction_data( @@ -136,7 +234,10 @@ mod test { ); let (key, vote, hash, signature) = parse_vote_transaction(&vote_tx).unwrap(); assert_eq!(hash, input_hash); - assert_eq!(vote, VoteTransaction::from(Vote::new(vec![42], bank_hash))); + assert_eq!( + vote, + ParsedVoteTransaction::Tower(VoteTransaction::from(Vote::new(vec![42], bank_hash))) + ); assert_eq!(key, vote_keypair.pubkey()); assert_eq!(signature, vote_tx.signatures[0]); diff --git a/vote/src/vote_state_view.rs b/vote/src/vote_state_view.rs new file mode 100644 index 0000000000..b4c6dc313a --- /dev/null +++ b/vote/src/vote_state_view.rs @@ -0,0 +1,506 @@ +use { + self::{ + field_frames::{ + AuthorizedVotersListFrame, EpochCreditsItem, EpochCreditsListFrame, RootSlotFrame, + RootSlotView, VotesFrame, + }, + frame_v1_14_11::VoteStateFrameV1_14_11, + frame_v3::VoteStateFrameV3, + list_view::ListView, + }, + core::fmt::Debug, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + solana_vote_interface::state::{BlockTimestamp, Lockout}, + std::sync::Arc, +}; +#[cfg(feature = "dev-context-only-utils")] +use { + bincode, + solana_vote_interface::state::{VoteState, VoteStateVersions}, +}; + +mod field_frames; +mod frame_v1_14_11; +mod frame_v3; +mod list_view; + +#[derive(Debug, PartialEq, Eq)] +pub enum VoteStateViewError { + AccountDataTooSmall, + InvalidVotesLength, + InvalidRootSlotOption, + InvalidAuthorizedVotersLength, + InvalidEpochCreditsLength, + OldVersion, + UnsupportedVersion, +} + +pub type Result = core::result::Result; + +enum Field { + NodePubkey, + Commission, + Votes, + RootSlot, + AuthorizedVoters, + EpochCredits, + LastTimestamp, +} + +/// A view into a serialized VoteState. +/// +/// This struct provides access to the VoteState data without +/// deserializing it. This is done by parsing and caching metadata +/// about the layout of the serialized VoteState. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub struct VoteStateView { + data: Arc>, + frame: VoteStateFrame, +} + +impl VoteStateView { + pub fn try_new(data: Arc>) -> Result { + let frame = VoteStateFrame::try_new(data.as_ref())?; + Ok(Self { data, frame }) + } + + pub fn node_pubkey(&self) -> &Pubkey { + let offset = self.frame.offset(Field::NodePubkey); + // SAFETY: `frame` was created from `data`. + unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) } + } + + pub fn commission(&self) -> u8 { + let offset = self.frame.offset(Field::Commission); + // SAFETY: `frame` was created from `data`. + self.data[offset] + } + + pub fn votes_iter(&self) -> impl Iterator + '_ { + self.votes_view().into_iter().map(|vote| { + Lockout::new_with_confirmation_count(vote.slot(), vote.confirmation_count()) + }) + } + + pub fn last_lockout(&self) -> Option { + self.votes_view().last().map(|item| { + Lockout::new_with_confirmation_count(item.slot(), item.confirmation_count()) + }) + } + + pub fn last_voted_slot(&self) -> Option { + self.votes_view().last().map(|item| item.slot()) + } + + pub fn root_slot(&self) -> Option { + self.root_slot_view().root_slot() + } + + pub fn get_authorized_voter(&self, epoch: Epoch) -> Option<&Pubkey> { + self.authorized_voters_view().get_authorized_voter(epoch) + } + + pub fn num_epoch_credits(&self) -> usize { + self.epoch_credits_view().len() + } + + pub fn epoch_credits_iter(&self) -> impl Iterator + '_ { + self.epoch_credits_view().into_iter() + } + + pub fn credits(&self) -> u64 { + self.epoch_credits_view() + .last() + .map(|item| item.credits()) + .unwrap_or(0) + } + + pub fn last_timestamp(&self) -> BlockTimestamp { + let offset = self.frame.offset(Field::LastTimestamp); + // SAFETY: `frame` was created from `data`. + let buffer = &self.data[offset..]; + let mut cursor = std::io::Cursor::new(buffer); + BlockTimestamp { + slot: solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap(), + timestamp: solana_serialize_utils::cursor::read_i64(&mut cursor).unwrap(), + } + } + + fn votes_view(&self) -> ListView { + let offset = self.frame.offset(Field::Votes); + // SAFETY: `frame` was created from `data`. + ListView::new(self.frame.votes_frame(), &self.data[offset..]) + } + + fn root_slot_view(&self) -> RootSlotView { + let offset = self.frame.offset(Field::RootSlot); + // SAFETY: `frame` was created from `data`. + RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..]) + } + + fn authorized_voters_view(&self) -> ListView { + let offset = self.frame.offset(Field::AuthorizedVoters); + // SAFETY: `frame` was created from `data`. + ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..]) + } + + fn epoch_credits_view(&self) -> ListView { + let offset = self.frame.offset(Field::EpochCredits); + // SAFETY: `frame` was created from `data`. + ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..]) + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl From for VoteStateView { + fn from(vote_state: VoteState) -> Self { + let vote_account_data = + bincode::serialize(&VoteStateVersions::new_current(vote_state)).unwrap(); + VoteStateView::try_new(Arc::new(vote_account_data)).unwrap() + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +enum VoteStateFrame { + V1_14_11(VoteStateFrameV1_14_11), + V3(VoteStateFrameV3), +} + +impl VoteStateFrame { + /// Parse a serialized vote state and verify structure. + fn try_new(bytes: &[u8]) -> Result { + let version = { + let mut cursor = std::io::Cursor::new(bytes); + solana_serialize_utils::cursor::read_u32(&mut cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? + }; + + Ok(match version { + 0 => return Err(VoteStateViewError::OldVersion), + 1 => Self::V1_14_11(VoteStateFrameV1_14_11::try_new(bytes)?), + 2 => Self::V3(VoteStateFrameV3::try_new(bytes)?), + _ => return Err(VoteStateViewError::UnsupportedVersion), + }) + } + + fn offset(&self, field: Field) -> usize { + match &self { + Self::V1_14_11(frame) => frame.field_offset(field), + Self::V3(frame) => frame.field_offset(field), + } + } + + fn votes_frame(&self) -> VotesFrame { + match &self { + Self::V1_14_11(frame) => VotesFrame::Lockout(frame.votes_frame), + Self::V3(frame) => VotesFrame::Landed(frame.votes_frame), + } + } + + fn root_slot_frame(&self) -> RootSlotFrame { + match &self { + Self::V1_14_11(vote_frame) => vote_frame.root_slot_frame, + Self::V3(vote_frame) => vote_frame.root_slot_frame, + } + } + + fn authorized_voters_frame(&self) -> AuthorizedVotersListFrame { + match &self { + Self::V1_14_11(frame) => frame.authorized_voters_frame, + Self::V3(frame) => frame.authorized_voters_frame, + } + } + + fn epoch_credits_frame(&self) -> EpochCreditsListFrame { + match &self { + Self::V1_14_11(frame) => frame.epoch_credits_frame, + Self::V3(frame) => frame.epoch_credits_frame, + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + arbitrary::{Arbitrary, Unstructured}, + solana_clock::Clock, + solana_vote_interface::{ + authorized_voters::AuthorizedVoters, + state::{ + vote_state_1_14_11::VoteState1_14_11, LandedVote, VoteInit, VoteState, + VoteStateVersions, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, + }, + }, + std::collections::VecDeque, + }; + + fn new_test_vote_state() -> VoteState { + let mut target_vote_state = VoteState::new( + &VoteInit { + node_pubkey: Pubkey::new_unique(), + authorized_voter: Pubkey::new_unique(), + authorized_withdrawer: Pubkey::new_unique(), + commission: 42, + }, + &Clock::default(), + ); + + target_vote_state + .set_new_authorized_voter( + &Pubkey::new_unique(), // authorized_pubkey + 0, // current_epoch + 1, // target_epoch + |_| Ok(()), + ) + .unwrap(); + + target_vote_state.root_slot = Some(42); + target_vote_state.epoch_credits.push((42, 42, 42)); + target_vote_state.last_timestamp = BlockTimestamp { + slot: 42, + timestamp: 42, + }; + for i in 0..MAX_LOCKOUT_HISTORY { + target_vote_state.votes.push_back(LandedVote { + latency: i as u8, + lockout: Lockout::new_with_confirmation_count(i as u64, i as u32), + }); + } + + target_vote_state + } + + #[test] + fn test_vote_state_view_v3() { + let target_vote_state = new_test_vote_state(); + let target_vote_state_versions = + VoteStateVersions::Current(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_v3(&vote_state_view, &target_vote_state); + } + + #[test] + fn test_vote_state_view_v3_default() { + let target_vote_state = VoteState::default(); + let target_vote_state_versions = + VoteStateVersions::Current(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_v3(&vote_state_view, &target_vote_state); + } + + #[test] + fn test_vote_state_view_v3_arbitrary() { + // variant + // provide 4x the minimum struct size in bytes to ensure we typically touch every field + let struct_bytes_x4 = VoteState::size_of() * 4; + for _ in 0..100 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let mut target_vote_state = VoteState::arbitrary(&mut unstructured).unwrap(); + target_vote_state.votes.truncate(MAX_LOCKOUT_HISTORY); + target_vote_state + .epoch_credits + .truncate(MAX_EPOCH_CREDITS_HISTORY); + if target_vote_state.authorized_voters().len() >= u8::MAX as usize { + continue; + } + + let target_vote_state_versions = + VoteStateVersions::Current(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_v3(&vote_state_view, &target_vote_state); + } + } + + #[test] + fn test_vote_state_view_1_14_11() { + let target_vote_state: VoteState1_14_11 = new_test_vote_state().into(); + let target_vote_state_versions = + VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state); + } + + #[test] + fn test_vote_state_view_1_14_11_default() { + let target_vote_state = VoteState1_14_11::default(); + let target_vote_state_versions = + VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state); + } + + #[test] + fn test_vote_state_view_1_14_11_arbitrary() { + // variant + // provide 4x the minimum struct size in bytes to ensure we typically touch every field + let struct_bytes_x4 = std::mem::size_of::() * 4; + for _ in 0..100 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).collect(); + let mut unstructured = Unstructured::new(&raw_data); + + let mut target_vote_state = VoteState1_14_11::arbitrary(&mut unstructured).unwrap(); + target_vote_state.votes.truncate(MAX_LOCKOUT_HISTORY); + target_vote_state + .epoch_credits + .truncate(MAX_EPOCH_CREDITS_HISTORY); + if target_vote_state.authorized_voters.len() >= u8::MAX as usize { + let (&first, &voter) = target_vote_state.authorized_voters.first().unwrap(); + let mut authorized_voters = AuthorizedVoters::new(first, voter); + for (epoch, pubkey) in target_vote_state.authorized_voters.iter().skip(1).take(10) { + authorized_voters.insert(*epoch, *pubkey); + } + target_vote_state.authorized_voters = authorized_voters; + } + + let target_vote_state_versions = + VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone())); + let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap(); + let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap(); + assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state); + } + } + + fn assert_eq_vote_state_v3(vote_state_view: &VoteStateView, vote_state: &VoteState) { + assert_eq!(vote_state_view.node_pubkey(), &vote_state.node_pubkey); + assert_eq!(vote_state_view.commission(), vote_state.commission); + let view_votes = vote_state_view.votes_iter().collect::>(); + let state_votes = vote_state + .votes + .iter() + .map(|vote| vote.lockout) + .collect::>(); + assert_eq!(view_votes, state_votes); + assert_eq!( + vote_state_view.last_lockout(), + vote_state.last_lockout().copied() + ); + assert_eq!( + vote_state_view.last_voted_slot(), + vote_state.last_voted_slot(), + ); + assert_eq!(vote_state_view.root_slot(), vote_state.root_slot); + + if let Some((first_voter_epoch, first_voter)) = vote_state.authorized_voters().first() { + assert_eq!( + vote_state_view.get_authorized_voter(*first_voter_epoch), + Some(first_voter) + ); + + let (last_voter_epoch, last_voter) = vote_state.authorized_voters().last().unwrap(); + assert_eq!( + vote_state_view.get_authorized_voter(*last_voter_epoch), + Some(last_voter) + ); + assert_eq!( + vote_state_view.get_authorized_voter(u64::MAX), + Some(last_voter) + ); + } else { + assert_eq!(vote_state_view.get_authorized_voter(u64::MAX), None); + } + + assert_eq!( + vote_state_view.num_epoch_credits(), + vote_state.epoch_credits.len() + ); + let view_credits: Vec<(Epoch, u64, u64)> = vote_state_view + .epoch_credits_iter() + .map(Into::into) + .collect::>(); + assert_eq!(view_credits, vote_state.epoch_credits); + + assert_eq!( + vote_state_view.credits(), + vote_state.epoch_credits.last().map(|x| x.1).unwrap_or(0) + ); + assert_eq!(vote_state_view.last_timestamp(), vote_state.last_timestamp); + } + + fn assert_eq_vote_state_1_14_11( + vote_state_view: &VoteStateView, + vote_state: &VoteState1_14_11, + ) { + assert_eq!(vote_state_view.node_pubkey(), &vote_state.node_pubkey); + assert_eq!(vote_state_view.commission(), vote_state.commission); + let view_votes = vote_state_view.votes_iter().collect::>(); + assert_eq!(view_votes, vote_state.votes); + assert_eq!( + vote_state_view.last_lockout(), + vote_state.votes.back().copied() + ); + assert_eq!( + vote_state_view.last_voted_slot(), + vote_state.votes.back().map(|lockout| lockout.slot()), + ); + assert_eq!(vote_state_view.root_slot(), vote_state.root_slot); + + if let Some((first_voter_epoch, first_voter)) = vote_state.authorized_voters.first() { + assert_eq!( + vote_state_view.get_authorized_voter(*first_voter_epoch), + Some(first_voter) + ); + + let (last_voter_epoch, last_voter) = vote_state.authorized_voters.last().unwrap(); + assert_eq!( + vote_state_view.get_authorized_voter(*last_voter_epoch), + Some(last_voter) + ); + assert_eq!( + vote_state_view.get_authorized_voter(u64::MAX), + Some(last_voter) + ); + } else { + assert_eq!(vote_state_view.get_authorized_voter(u64::MAX), None); + } + + assert_eq!( + vote_state_view.num_epoch_credits(), + vote_state.epoch_credits.len() + ); + let view_credits: Vec<(Epoch, u64, u64)> = vote_state_view + .epoch_credits_iter() + .map(Into::into) + .collect::>(); + assert_eq!(view_credits, vote_state.epoch_credits); + + assert_eq!( + vote_state_view.credits(), + vote_state.epoch_credits.last().map(|x| x.1).unwrap_or(0) + ); + assert_eq!(vote_state_view.last_timestamp(), vote_state.last_timestamp); + } + + #[test] + fn test_vote_state_view_too_small() { + for i in 0..4 { + let vote_data = Arc::new(vec![0; i]); + let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err(); + assert_eq!(vote_state_view_err, VoteStateViewError::AccountDataTooSmall); + } + } + + #[test] + fn test_vote_state_view_old_version() { + let vote_data = Arc::new(0u32.to_le_bytes().to_vec()); + let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err(); + assert_eq!(vote_state_view_err, VoteStateViewError::OldVersion); + } + + #[test] + fn test_vote_state_view_unsupported_version() { + let vote_data = Arc::new(3u32.to_le_bytes().to_vec()); + let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err(); + assert_eq!(vote_state_view_err, VoteStateViewError::UnsupportedVersion); + } +} diff --git a/vote/src/vote_state_view/field_frames.rs b/vote/src/vote_state_view/field_frames.rs new file mode 100644 index 0000000000..d25bdf57a6 --- /dev/null +++ b/vote/src/vote_state_view/field_frames.rs @@ -0,0 +1,365 @@ +use { + super::{list_view::ListView, Result, VoteStateViewError}, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + std::io::BufRead, +}; + +pub(super) trait ListFrame { + type Item; + + // SAFETY: Each implementor MUST enforce that `Self::Item` is alignment 1 to + // ensure that after casting it won't have alignment issues, any heap + // allocated fields, or any assumptions about endianness. + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: (); + + fn len(&self) -> usize; + fn item_size(&self) -> usize { + core::mem::size_of::() + } + + /// This function is safe under the following conditions: + /// SAFETY: + /// - `Self::Item` is alignment 1 + /// - The passed `item_data` slice is large enough for the type `Self::Item` + /// - `Self::Item` is valid for any sequence of bytes + unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item { + &*(item_data.as_ptr() as *const Self::Item) + } + + fn total_size(&self) -> usize { + core::mem::size_of::() /* len */ + self.total_item_size() + } + + fn total_item_size(&self) -> usize { + self.len() * self.item_size() + } +} + +pub(super) enum VotesFrame { + Lockout(LockoutListFrame), + Landed(LandedVotesListFrame), +} + +impl ListFrame for VotesFrame { + type Item = LockoutItem; + + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: () = { + static_assertions::const_assert!(core::mem::align_of::() == 1); + }; + + fn len(&self) -> usize { + match self { + Self::Lockout(frame) => frame.len(), + Self::Landed(frame) => frame.len(), + } + } + + fn item_size(&self) -> usize { + match self { + Self::Lockout(frame) => frame.item_size(), + Self::Landed(frame) => frame.item_size(), + } + } + + unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item { + match self { + Self::Lockout(frame) => frame.read_item(item_data), + Self::Landed(frame) => frame.read_item(item_data), + } + } +} + +#[repr(C)] +pub(super) struct LockoutItem { + slot: [u8; 8], + confirmation_count: [u8; 4], +} + +impl LockoutItem { + #[inline] + pub(super) fn slot(&self) -> Slot { + u64::from_le_bytes(self.slot) + } + #[inline] + pub(super) fn confirmation_count(&self) -> u32 { + u32::from_le_bytes(self.confirmation_count) + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct LockoutListFrame { + pub(super) len: u8, +} + +impl LockoutListFrame { + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let len = solana_serialize_utils::cursor::read_u64(cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize; + let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidVotesLength)?; + let frame = Self { len }; + cursor.consume(frame.total_item_size()); + Ok(frame) + } +} + +impl ListFrame for LockoutListFrame { + type Item = LockoutItem; + + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: () = { + static_assertions::const_assert!(core::mem::align_of::() == 1); + }; + + fn len(&self) -> usize { + self.len as usize + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct LandedVotesListFrame { + pub(super) len: u8, +} + +impl LandedVotesListFrame { + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let len = solana_serialize_utils::cursor::read_u64(cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize; + let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidVotesLength)?; + let frame = Self { len }; + cursor.consume(frame.total_item_size()); + Ok(frame) + } +} + +#[repr(C)] +pub(super) struct LandedVoteItem { + latency: u8, + slot: [u8; 8], + confirmation_count: [u8; 4], +} + +impl ListFrame for LandedVotesListFrame { + type Item = LockoutItem; + + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: () = { + static_assertions::const_assert!(core::mem::align_of::() == 1); + }; + + fn len(&self) -> usize { + self.len as usize + } + + fn item_size(&self) -> usize { + core::mem::size_of::() + } + + unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item { + &*(item_data[1..].as_ptr() as *const LockoutItem) + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct AuthorizedVotersListFrame { + pub(super) len: u8, +} + +impl AuthorizedVotersListFrame { + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let len = solana_serialize_utils::cursor::read_u64(cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize; + let len = + u8::try_from(len).map_err(|_| VoteStateViewError::InvalidAuthorizedVotersLength)?; + let frame = Self { len }; + cursor.consume(frame.total_item_size()); + Ok(frame) + } +} + +#[repr(C)] +pub(super) struct AuthorizedVoterItem { + epoch: [u8; 8], + voter: Pubkey, +} + +impl ListFrame for AuthorizedVotersListFrame { + type Item = AuthorizedVoterItem; + + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: () = { + static_assertions::const_assert!(core::mem::align_of::() == 1); + }; + + fn len(&self) -> usize { + self.len as usize + } +} + +impl<'a> ListView<'a, AuthorizedVotersListFrame> { + pub(super) fn get_authorized_voter(self, epoch: Epoch) -> Option<&'a Pubkey> { + for item in self.into_iter().rev() { + let voter_epoch = u64::from_le_bytes(item.epoch); + if voter_epoch <= epoch { + return Some(&item.voter); + } + } + + None + } +} + +#[repr(C)] +pub struct EpochCreditsItem { + epoch: [u8; 8], + credits: [u8; 8], + prev_credits: [u8; 8], +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct EpochCreditsListFrame { + pub(super) len: u8, +} + +impl EpochCreditsListFrame { + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let len = solana_serialize_utils::cursor::read_u64(cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize; + let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidEpochCreditsLength)?; + let frame = Self { len }; + cursor.consume(frame.total_item_size()); + Ok(frame) + } +} + +impl ListFrame for EpochCreditsListFrame { + type Item = EpochCreditsItem; + + #[cfg(test)] + const ASSERT_ITEM_ALIGNMENT: () = { + static_assertions::const_assert!(core::mem::align_of::() == 1); + }; + + fn len(&self) -> usize { + self.len as usize + } +} + +impl EpochCreditsItem { + #[inline] + pub fn epoch(&self) -> u64 { + u64::from_le_bytes(self.epoch) + } + #[inline] + pub fn credits(&self) -> u64 { + u64::from_le_bytes(self.credits) + } + #[inline] + pub fn prev_credits(&self) -> u64 { + u64::from_le_bytes(self.prev_credits) + } +} + +impl From<&EpochCreditsItem> for (Epoch, u64, u64) { + fn from(item: &EpochCreditsItem) -> Self { + (item.epoch(), item.credits(), item.prev_credits()) + } +} + +pub(super) struct RootSlotView<'a> { + frame: RootSlotFrame, + buffer: &'a [u8], +} + +impl<'a> RootSlotView<'a> { + pub(super) fn new(frame: RootSlotFrame, buffer: &'a [u8]) -> Self { + Self { frame, buffer } + } +} + +impl RootSlotView<'_> { + pub(super) fn root_slot(&self) -> Option { + if !self.frame.has_root_slot { + None + } else { + let root_slot = { + let mut cursor = std::io::Cursor::new(self.buffer); + cursor.consume(1); + solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap() + }; + Some(root_slot) + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct RootSlotFrame { + pub(super) has_root_slot: bool, +} + +impl RootSlotFrame { + pub(super) fn total_size(&self) -> usize { + 1 + self.size() + } + + pub(super) fn size(&self) -> usize { + if self.has_root_slot { + core::mem::size_of::() + } else { + 0 + } + } + + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + let byte = solana_serialize_utils::cursor::read_u8(cursor) + .map_err(|_err| VoteStateViewError::AccountDataTooSmall)?; + let has_root_slot = match byte { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(VoteStateViewError::InvalidRootSlotOption), + }?; + + let frame = Self { has_root_slot }; + cursor.consume(frame.size()); + Ok(frame) + } +} + +pub(super) struct PriorVotersFrame; +impl PriorVotersFrame { + pub(super) const fn total_size() -> usize { + 1545 // see test_prior_voters_total_size + } + + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) { + cursor.consume(PriorVotersFrame::total_size()); + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_vote_interface::state::CircBuf}; + + #[test] + fn test_prior_voters_total_size() { + #[repr(C)] + pub(super) struct PriorVotersItem { + voter: Pubkey, + start_epoch_inclusive: [u8; 8], + end_epoch_exclusive: [u8; 8], + } + + let prior_voters_len = CircBuf::<()>::default().buf().len(); + let expected_total_size = prior_voters_len * core::mem::size_of::() + + core::mem::size_of::() /* idx */ + + core::mem::size_of::() /* is_empty */; + assert_eq!(PriorVotersFrame::total_size(), expected_total_size); + } +} diff --git a/vote/src/vote_state_view/frame_v1_14_11.rs b/vote/src/vote_state_view/frame_v1_14_11.rs new file mode 100644 index 0000000000..d35b1b4260 --- /dev/null +++ b/vote/src/vote_state_view/frame_v1_14_11.rs @@ -0,0 +1,230 @@ +use { + super::{ + field_frames::{ + AuthorizedVotersListFrame, ListFrame, LockoutListFrame, PriorVotersFrame, RootSlotFrame, + }, + EpochCreditsListFrame, Field, Result, VoteStateViewError, + }, + solana_pubkey::Pubkey, + solana_vote_interface::state::BlockTimestamp, + std::io::BufRead, +}; + +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct VoteStateFrameV1_14_11 { + pub(super) votes_frame: LockoutListFrame, + pub(super) root_slot_frame: RootSlotFrame, + pub(super) authorized_voters_frame: AuthorizedVotersListFrame, + pub(super) epoch_credits_frame: EpochCreditsListFrame, +} + +impl VoteStateFrameV1_14_11 { + pub(super) fn try_new(bytes: &[u8]) -> Result { + let votes_offset = Self::votes_offset(); + let mut cursor = std::io::Cursor::new(bytes); + cursor.set_position(votes_offset as u64); + + let votes_frame = LockoutListFrame::read(&mut cursor)?; + let root_slot_frame = RootSlotFrame::read(&mut cursor)?; + let authorized_voters_frame = AuthorizedVotersListFrame::read(&mut cursor)?; + PriorVotersFrame::read(&mut cursor); + let epoch_credits_frame = EpochCreditsListFrame::read(&mut cursor)?; + cursor.consume(core::mem::size_of::()); + // trailing bytes are allowed. consistent with default behavior of + // function bincode::deserialize + if cursor.position() as usize <= bytes.len() { + Ok(Self { + votes_frame, + root_slot_frame, + authorized_voters_frame, + epoch_credits_frame, + }) + } else { + Err(VoteStateViewError::AccountDataTooSmall) + } + } + + pub(super) fn field_offset(&self, field: Field) -> usize { + match field { + Field::NodePubkey => Self::node_pubkey_offset(), + Field::Commission => Self::commission_offset(), + Field::Votes => Self::votes_offset(), + Field::RootSlot => self.root_slot_offset(), + Field::AuthorizedVoters => self.authorized_voters_offset(), + Field::EpochCredits => self.epoch_credits_offset(), + Field::LastTimestamp => self.last_timestamp_offset(), + } + } + + const fn node_pubkey_offset() -> usize { + core::mem::size_of::() // version + } + + const fn authorized_withdrawer_offset() -> usize { + Self::node_pubkey_offset() + core::mem::size_of::() + } + + const fn commission_offset() -> usize { + Self::authorized_withdrawer_offset() + core::mem::size_of::() + } + + const fn votes_offset() -> usize { + Self::commission_offset() + core::mem::size_of::() + } + + fn root_slot_offset(&self) -> usize { + Self::votes_offset() + self.votes_frame.total_size() + } + + fn authorized_voters_offset(&self) -> usize { + self.root_slot_offset() + self.root_slot_frame.total_size() + } + + fn prior_voters_offset(&self) -> usize { + self.authorized_voters_offset() + self.authorized_voters_frame.total_size() + } + + fn epoch_credits_offset(&self) -> usize { + self.prior_voters_offset() + PriorVotersFrame::total_size() + } + + fn last_timestamp_offset(&self) -> usize { + self.epoch_credits_offset() + self.epoch_credits_frame.total_size() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_clock::Clock, + solana_vote_interface::state::{ + LandedVote, Lockout, VoteInit, VoteState, VoteState1_14_11, VoteStateVersions, + }, + }; + + #[test] + fn test_try_new_zeroed() { + let target_vote_state = VoteState1_14_11::default(); + let target_vote_state_versions = VoteStateVersions::V1_14_11(Box::new(target_vote_state)); + let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap(); + + for i in 0..bytes.len() { + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes[..i]); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::AccountDataTooSmall) + ); + } + + for has_trailing_bytes in [false, true] { + if has_trailing_bytes { + bytes.extend_from_slice(&[0; 42]); + } + assert_eq!( + VoteStateFrameV1_14_11::try_new(&bytes), + Ok(VoteStateFrameV1_14_11 { + votes_frame: LockoutListFrame { len: 0 }, + root_slot_frame: RootSlotFrame { + has_root_slot: false, + }, + authorized_voters_frame: AuthorizedVotersListFrame { len: 0 }, + epoch_credits_frame: EpochCreditsListFrame { len: 0 }, + }) + ); + } + } + + #[test] + fn test_try_new_simple() { + let mut target_vote_state = VoteState::new(&VoteInit::default(), &Clock::default()); + target_vote_state.root_slot = Some(42); + target_vote_state.epoch_credits.push((1, 2, 3)); + target_vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::default(), + }); + + let target_vote_state_versions = + VoteStateVersions::V1_14_11(Box::new(target_vote_state.into())); + let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap(); + + for i in 0..bytes.len() { + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes[..i]); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::AccountDataTooSmall) + ); + } + + for has_trailing_bytes in [false, true] { + if has_trailing_bytes { + bytes.extend_from_slice(&[0; 42]); + } + assert_eq!( + VoteStateFrameV1_14_11::try_new(&bytes), + Ok(VoteStateFrameV1_14_11 { + votes_frame: LockoutListFrame { len: 1 }, + root_slot_frame: RootSlotFrame { + has_root_slot: true, + }, + authorized_voters_frame: AuthorizedVotersListFrame { len: 1 }, + epoch_credits_frame: EpochCreditsListFrame { len: 1 }, + }) + ); + } + } + + #[test] + fn test_try_new_invalid_values() { + let mut bytes = vec![0; VoteStateFrameV1_14_11::votes_offset()]; + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidVotesLength) + ); + } + + bytes.extend_from_slice(&[0; core::mem::size_of::()]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(2u8.to_le_bytes())); + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidRootSlotOption) + ); + } + + bytes.extend_from_slice(&[0; 1]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidAuthorizedVotersLength) + ); + } + + bytes.extend_from_slice(&[0; core::mem::size_of::()]); + bytes.extend_from_slice(&[0; PriorVotersFrame::total_size()]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidEpochCreditsLength) + ); + } + } +} diff --git a/vote/src/vote_state_view/frame_v3.rs b/vote/src/vote_state_view/frame_v3.rs new file mode 100644 index 0000000000..69fe1b434b --- /dev/null +++ b/vote/src/vote_state_view/frame_v3.rs @@ -0,0 +1,230 @@ +use { + super::{ + field_frames::{ + AuthorizedVotersListFrame, EpochCreditsListFrame, LandedVotesListFrame, ListFrame, + PriorVotersFrame, RootSlotFrame, + }, + Field, Result, VoteStateViewError, + }, + solana_pubkey::Pubkey, + solana_vote_interface::state::BlockTimestamp, + std::io::BufRead, +}; + +#[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +pub(super) struct VoteStateFrameV3 { + pub(super) votes_frame: LandedVotesListFrame, + pub(super) root_slot_frame: RootSlotFrame, + pub(super) authorized_voters_frame: AuthorizedVotersListFrame, + pub(super) epoch_credits_frame: EpochCreditsListFrame, +} + +impl VoteStateFrameV3 { + pub(super) fn try_new(bytes: &[u8]) -> Result { + let votes_offset = Self::votes_offset(); + let mut cursor = std::io::Cursor::new(bytes); + cursor.set_position(votes_offset as u64); + + let votes_frame = LandedVotesListFrame::read(&mut cursor)?; + let root_slot_frame = RootSlotFrame::read(&mut cursor)?; + let authorized_voters_frame = AuthorizedVotersListFrame::read(&mut cursor)?; + PriorVotersFrame::read(&mut cursor); + let epoch_credits_frame = EpochCreditsListFrame::read(&mut cursor)?; + cursor.consume(core::mem::size_of::()); + // trailing bytes are allowed. consistent with default behavior of + // function bincode::deserialize + if cursor.position() as usize <= bytes.len() { + Ok(Self { + votes_frame, + root_slot_frame, + authorized_voters_frame, + epoch_credits_frame, + }) + } else { + Err(VoteStateViewError::AccountDataTooSmall) + } + } + + pub(super) fn field_offset(&self, field: Field) -> usize { + match field { + Field::NodePubkey => Self::node_pubkey_offset(), + Field::Commission => Self::commission_offset(), + Field::Votes => Self::votes_offset(), + Field::RootSlot => self.root_slot_offset(), + Field::AuthorizedVoters => self.authorized_voters_offset(), + Field::EpochCredits => self.epoch_credits_offset(), + Field::LastTimestamp => self.last_timestamp_offset(), + } + } + + const fn node_pubkey_offset() -> usize { + core::mem::size_of::() // version + } + + const fn authorized_withdrawer_offset() -> usize { + Self::node_pubkey_offset() + core::mem::size_of::() + } + + const fn commission_offset() -> usize { + Self::authorized_withdrawer_offset() + core::mem::size_of::() + } + + const fn votes_offset() -> usize { + Self::commission_offset() + core::mem::size_of::() + } + + fn root_slot_offset(&self) -> usize { + Self::votes_offset() + self.votes_frame.total_size() + } + + fn authorized_voters_offset(&self) -> usize { + self.root_slot_offset() + self.root_slot_frame.total_size() + } + + fn prior_voters_offset(&self) -> usize { + self.authorized_voters_offset() + self.authorized_voters_frame.total_size() + } + + fn epoch_credits_offset(&self) -> usize { + self.prior_voters_offset() + PriorVotersFrame::total_size() + } + + fn last_timestamp_offset(&self) -> usize { + self.epoch_credits_offset() + self.epoch_credits_frame.total_size() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_clock::Clock, + solana_vote_interface::state::{ + LandedVote, Lockout, VoteInit, VoteState, VoteStateVersions, + }, + }; + + #[test] + fn test_try_new_zeroed() { + let target_vote_state = VoteState::default(); + let target_vote_state_versions = VoteStateVersions::Current(Box::new(target_vote_state)); + let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap(); + + for i in 0..bytes.len() { + let vote_state_frame = VoteStateFrameV3::try_new(&bytes[..i]); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::AccountDataTooSmall) + ); + } + + for has_trailing_bytes in [false, true] { + if has_trailing_bytes { + bytes.extend_from_slice(&[0; 42]); + } + assert_eq!( + VoteStateFrameV3::try_new(&bytes), + Ok(VoteStateFrameV3 { + votes_frame: LandedVotesListFrame { len: 0 }, + root_slot_frame: RootSlotFrame { + has_root_slot: false, + }, + authorized_voters_frame: AuthorizedVotersListFrame { len: 0 }, + epoch_credits_frame: EpochCreditsListFrame { len: 0 }, + }) + ); + } + } + + #[test] + fn test_try_new_simple() { + let mut target_vote_state = VoteState::new(&VoteInit::default(), &Clock::default()); + target_vote_state.root_slot = Some(42); + target_vote_state.epoch_credits.push((1, 2, 3)); + target_vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::default(), + }); + + let target_vote_state_versions = VoteStateVersions::Current(Box::new(target_vote_state)); + let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap(); + + for i in 0..bytes.len() { + let vote_state_frame = VoteStateFrameV3::try_new(&bytes[..i]); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::AccountDataTooSmall) + ); + } + + for has_trailing_bytes in [false, true] { + if has_trailing_bytes { + bytes.extend_from_slice(&[0; 42]); + } + assert_eq!( + VoteStateFrameV3::try_new(&bytes), + Ok(VoteStateFrameV3 { + votes_frame: LandedVotesListFrame { len: 1 }, + root_slot_frame: RootSlotFrame { + has_root_slot: true, + }, + authorized_voters_frame: AuthorizedVotersListFrame { len: 1 }, + epoch_credits_frame: EpochCreditsListFrame { len: 1 }, + }) + ); + } + } + + #[test] + fn test_try_new_invalid_values() { + let mut bytes = vec![0; VoteStateFrameV3::votes_offset()]; + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV3::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidVotesLength) + ); + } + + bytes.extend_from_slice(&[0; core::mem::size_of::()]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(2u8.to_le_bytes())); + let vote_state_frame = VoteStateFrameV3::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidRootSlotOption) + ); + } + + bytes.extend_from_slice(&[0; 1]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV3::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidAuthorizedVotersLength) + ); + } + + bytes.extend_from_slice(&[0; core::mem::size_of::()]); + bytes.extend_from_slice(&[0; PriorVotersFrame::total_size()]); + + { + let mut bytes = bytes.clone(); + bytes.extend_from_slice(&(256u64.to_le_bytes())); + let vote_state_frame = VoteStateFrameV3::try_new(&bytes); + assert_eq!( + vote_state_frame, + Err(VoteStateViewError::InvalidEpochCreditsLength) + ); + } + } +} diff --git a/vote/src/vote_state_view/list_view.rs b/vote/src/vote_state_view/list_view.rs new file mode 100644 index 0000000000..51d84dc327 --- /dev/null +++ b/vote/src/vote_state_view/list_view.rs @@ -0,0 +1,86 @@ +use super::field_frames::ListFrame; + +pub(super) struct ListView<'a, F> { + frame: F, + item_buffer: &'a [u8], +} + +impl<'a, F: ListFrame> ListView<'a, F> { + pub(super) fn new(frame: F, buffer: &'a [u8]) -> Self { + let len_offset = core::mem::size_of::(); + let item_buffer = &buffer[len_offset..]; + Self { frame, item_buffer } + } + + pub(super) fn len(&self) -> usize { + self.frame.len() + } + + pub(super) fn into_iter(self) -> ListViewIter<'a, F> + where + Self: Sized, + { + ListViewIter { + index: 0, + rev_index: 0, + view: self, + } + } + + pub(super) fn last(&self) -> Option<&F::Item> { + let len = self.len(); + if len == 0 { + return None; + } + self.item(len - 1) + } + + fn item(&self, index: usize) -> Option<&'a F::Item> { + if index >= self.len() { + return None; + } + + let offset = index * self.frame.item_size(); + // SAFETY: `item_buffer` is long enough to contain all items + let item_data = &self.item_buffer[offset..offset + self.frame.item_size()]; + // SAFETY: `item_data` is long enough to contain an item + Some(unsafe { self.frame.read_item(item_data) }) + } +} + +pub(super) struct ListViewIter<'a, F> { + index: usize, + rev_index: usize, + view: ListView<'a, F>, +} + +impl<'a, F: ListFrame> Iterator for ListViewIter<'a, F> +where + F::Item: 'a, +{ + type Item = &'a F::Item; + fn next(&mut self) -> Option { + if self.index < self.view.len() { + let item = self.view.item(self.index); + self.index += 1; + item + } else { + None + } + } +} + +impl<'a, F: ListFrame> DoubleEndedIterator for ListViewIter<'a, F> +where + F::Item: 'a, +{ + fn next_back(&mut self) -> Option { + if self.rev_index < self.view.len() { + let item = self.view.item(self.view.len() - self.rev_index - 1); + self.rev_index += 1; + item + } else { + None + } + } +} diff --git a/votor-messages/Cargo.toml b/votor-messages/Cargo.toml new file mode 100644 index 0000000000..ba1f1e0b76 --- /dev/null +++ b/votor-messages/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "solana-votor-messages" +description = "Blockchain, Rebuilt for Scale" +documentation = "https://docs.rs/solana-votor-messages" +readme = "../README.md" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[features] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-sdk/frozen-abi", +] + +[dependencies] +bitvec = { workspace = true } +bytemuck = { workspace = true } +num_enum = { workspace = true } +serde = { workspace = true } +solana-account = { workspace = true } +solana-bls-signatures = { workspace = true, features = [ + "solana-signer-derive", +] } +solana-frozen-abi = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-hash = { workspace = true } +solana-logger = { workspace = true } +solana-program = { workspace = true } +solana-sdk = { workspace = true } +solana-vote-interface = { workspace = true } +spl-pod = { workspace = true } + +[lints] +workspace = true diff --git a/votor-messages/src/accounting.rs b/votor-messages/src/accounting.rs new file mode 100644 index 0000000000..9b34e25047 --- /dev/null +++ b/votor-messages/src/accounting.rs @@ -0,0 +1,99 @@ +//! Accounting related operations on the Vote Account +use { + crate::state::PodEpoch, + bytemuck::{Pod, PodInOption, Zeroable, ZeroableInOption}, + solana_program::{clock::Epoch, pubkey::Pubkey}, + spl_pod::primitives::PodU64, +}; + +/// Authorized Signer for vote instructions +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable, Default, PartialEq)] +pub struct AuthorizedVoter { + pub(crate) epoch: PodEpoch, + pub(crate) voter: Pubkey, +} + +impl AuthorizedVoter { + /// Create a new authorized voter for `epoch` + pub fn new(epoch: Epoch, voter: Pubkey) -> Self { + Self { + epoch: PodEpoch::from(epoch), + voter, + } + } + + /// Get the authorization epoch + pub fn epoch(&self) -> Epoch { + Epoch::from(self.epoch) + } + + /// Get the voter that is authorized + pub fn voter(&self) -> &Pubkey { + &self.voter + } + + /// Set the authorization epoch + pub fn set_epoch(&mut self, epoch: Epoch) { + self.epoch = PodEpoch::from(epoch); + } + + /// Set the voter that is authorized + pub fn set_voter(&mut self, voter: Pubkey) { + self.voter = voter; + } +} + +// UNSAFE: we require that `epoch > 0` so this is safe +unsafe impl ZeroableInOption for AuthorizedVoter {} +unsafe impl PodInOption for AuthorizedVoter {} + +/// The credits information for an epoch +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable, Default, PartialEq)] +pub struct EpochCredit { + pub(crate) epoch: PodEpoch, + pub(crate) credits: PodU64, + pub(crate) prev_credits: PodU64, +} + +impl EpochCredit { + /// Create a new epoch credit + pub fn new(epoch: Epoch, credits: u64, prev_credits: u64) -> Self { + Self { + epoch: PodEpoch::from(epoch), + credits: PodU64::from(credits), + prev_credits: PodU64::from(prev_credits), + } + } + + /// Get epoch in which credits were earned + pub fn epoch(&self) -> u64 { + u64::from(self.epoch) + } + + /// Get the credits earned + pub fn credits(&self) -> u64 { + u64::from(self.credits) + } + + /// Get the credits earned in the previous epoch + pub fn prev_credits(&self) -> u64 { + u64::from(self.prev_credits) + } + + /// Set epoch in which credits were earned + pub fn set_epoch(&mut self, epoch: Epoch) { + self.epoch = PodEpoch::from(epoch); + } + + /// Set the credits earned + pub fn set_credits(&mut self, credits: u64) { + self.credits = PodU64::from(credits); + } + + /// Set the credits earned in the previous epoch + pub fn set_prev_credits(&mut self, prev_credits: u64) { + self.prev_credits = PodU64::from(prev_credits); + } +} diff --git a/votor-messages/src/bls_message.rs b/votor-messages/src/bls_message.rs new file mode 100644 index 0000000000..63b268f3dd --- /dev/null +++ b/votor-messages/src/bls_message.rs @@ -0,0 +1,178 @@ +//! Put BLS message here so all clients can agree on the format +use { + crate::vote::Vote, + bitvec::prelude::*, + serde::{Deserialize, Serialize}, + solana_bls_signatures::Signature as BLSSignature, + solana_sdk::{clock::Slot, hash::Hash}, +}; + +/// The seed used to derive the BLS keypair +pub const BLS_KEYPAIR_DERIVE_SEED: &[u8; 9] = b"alpenglow"; + +/// Block, a (slot, hash) tuple +pub type Block = (Slot, Hash); + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +/// BLS vote message, we need rank to look up pubkey +pub struct VoteMessage { + /// The vote + pub vote: Vote, + /// The signature + pub signature: BLSSignature, + /// The rank of the validator + pub rank: u16, +} + +/// Certificate details +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +pub enum Certificate { + /// Finalize certificate + Finalize(Slot), + /// Fast finalize certificate + FinalizeFast(Slot, Hash), + /// Notarize certificate + Notarize(Slot, Hash), + /// Notarize fallback certificate + NotarizeFallback(Slot, Hash), + /// Skip certificate + Skip(Slot), +} + +/// Certificate type +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +pub enum CertificateType { + /// Finalize certificate + Finalize, + /// Fast finalize certificate + FinalizeFast, + /// Notarize certificate + Notarize, + /// Notarize fallback certificate + NotarizeFallback, + /// Skip certificate + Skip, +} + +impl Certificate { + /// Create a new certificate ID from a CertificateType, Option, and Option + pub fn new(certificate_type: CertificateType, slot: Slot, hash: Option) -> Self { + match (certificate_type, hash) { + (CertificateType::Finalize, None) => Certificate::Finalize(slot), + (CertificateType::FinalizeFast, Some(hash)) => Certificate::FinalizeFast(slot, hash), + (CertificateType::Notarize, Some(hash)) => Certificate::Notarize(slot, hash), + (CertificateType::NotarizeFallback, Some(hash)) => { + Certificate::NotarizeFallback(slot, hash) + } + (CertificateType::Skip, None) => Certificate::Skip(slot), + _ => panic!("Invalid certificate type and hash combination"), + } + } + + /// Get the certificate type + pub fn certificate_type(&self) -> CertificateType { + match self { + Certificate::Finalize(_) => CertificateType::Finalize, + Certificate::FinalizeFast(_, _) => CertificateType::FinalizeFast, + Certificate::Notarize(_, _) => CertificateType::Notarize, + Certificate::NotarizeFallback(_, _) => CertificateType::NotarizeFallback, + Certificate::Skip(_) => CertificateType::Skip, + } + } + + /// Get the slot of the certificate + pub fn slot(&self) -> Slot { + match self { + Certificate::Finalize(slot) + | Certificate::FinalizeFast(slot, _) + | Certificate::Notarize(slot, _) + | Certificate::NotarizeFallback(slot, _) + | Certificate::Skip(slot) => *slot, + } + } + + /// Is this a fast finalize certificate? + pub fn is_fast_finalization(&self) -> bool { + matches!(self, Self::FinalizeFast(_, _)) + } + + /// Is this a finalize / fast finalize certificate? + pub fn is_finalization(&self) -> bool { + matches!(self, Self::Finalize(_) | Self::FinalizeFast(_, _)) + } + + /// Is this a notarize fallback certificate? + pub fn is_notarize_fallback(&self) -> bool { + matches!(self, Self::NotarizeFallback(_, _)) + } + + /// Is this a skip certificate? + pub fn is_skip(&self) -> bool { + matches!(self, Self::Skip(_)) + } + + /// Gets the block associated with this certificate, if present + pub fn to_block(self) -> Option { + match self { + Certificate::Finalize(_) | Certificate::Skip(_) => None, + Certificate::Notarize(slot, block_id) + | Certificate::NotarizeFallback(slot, block_id) + | Certificate::FinalizeFast(slot, block_id) => Some((slot, block_id)), + } + } + + /// "Critical" certs are the certificates necessary to make progress + /// We do not consider the next slot for voting until we've seen either + /// a Skip certificate or a NotarizeFallback certificate for ParentReady + /// + /// Note: Notarization certificates necessarily generate a + /// NotarizeFallback certificate as well + pub fn is_critical(&self) -> bool { + matches!(self, Self::NotarizeFallback(_, _) | Self::Skip(_)) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +/// BLS vote message, we need rank to look up pubkey +pub struct CertificateMessage { + /// The certificate + pub certificate: Certificate, + /// The signature + pub signature: BLSSignature, + /// The bitmap for validators, little endian byte order + pub bitmap: BitVec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] +/// BLS message data in Alpenglow +pub enum BLSMessage { + /// Vote message, with the vote and the rank of the validator. + Vote(VoteMessage), + /// Certificate message + Certificate(CertificateMessage), +} + +impl BLSMessage { + /// Create a new vote message + pub fn new_vote(vote: Vote, signature: BLSSignature, rank: u16) -> Self { + Self::Vote(VoteMessage { + vote, + signature, + rank, + }) + } + + /// Create a new certificate message + pub fn new_certificate( + certificate: Certificate, + bitmap: BitVec, + signature: BLSSignature, + ) -> Self { + Self::Certificate(CertificateMessage { + certificate, + signature, + bitmap, + }) + } +} diff --git a/votor-messages/src/instruction.rs b/votor-messages/src/instruction.rs new file mode 100644 index 0000000000..ad2bd02fb5 --- /dev/null +++ b/votor-messages/src/instruction.rs @@ -0,0 +1,96 @@ +//! Program instructions +use { + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_bls_signatures::Pubkey as BlsPubkey, + solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }, + spl_pod::{bytemuck::pod_bytes_of, primitives::PodU32}, +}; + +/// Instructions supported by the program +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] +pub enum VoteInstruction { + /// Initialize a vote account + /// + /// # Account references + /// 0. `[WRITE]` Uninitialized vote account + /// 1. `[SIGNER]` New validator identity (node_pubkey) + /// + /// Data expected by this instruction: + /// `InitializeAccountInstructionData` + InitializeAccount, +} + +/// Data expected by +/// `VoteInstruction::InitializeAccount` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)] +pub struct InitializeAccountInstructionData { + /// The node that votes in this account + pub node_pubkey: Pubkey, + /// The signer for vote transactions + pub authorized_voter: Pubkey, + /// The signer for withdrawals + pub authorized_withdrawer: Pubkey, + /// The commission percentage for this vote account + pub commission: u8, + /// BLS public key + pub bls_pubkey: BlsPubkey, +} + +/// Instruction builder to initialize a new vote account with a valid VoteState: +/// - `vote_pubkey` the vote account +/// - `instruction_data` the vote account's account creation metadata +pub fn initialize_account( + vote_pubkey: Pubkey, + instruction_data: &InitializeAccountInstructionData, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(vote_pubkey, false), + AccountMeta::new_readonly(instruction_data.node_pubkey, true), + ]; + + encode_instruction( + accounts, + VoteInstruction::InitializeAccount, + instruction_data, + ) +} + +/// Utility function for encoding instruction data +pub(crate) fn encode_instruction( + accounts: Vec, + instruction: VoteInstruction, + instruction_data: &D, +) -> Instruction { + encode_instruction_with_seed(accounts, instruction, instruction_data, None) +} + +/// Utility function for encoding instruction data +/// with a seed. +/// +/// Some accounting instructions have a variable length +/// `seed`, we serialize this as a pod slice at the end +/// of the instruction data +pub(crate) fn encode_instruction_with_seed( + accounts: Vec, + instruction: VoteInstruction, + instruction_data: &D, + seed: Option<&str>, +) -> Instruction { + let mut data = vec![u8::from(instruction)]; + data.extend_from_slice(bytemuck::bytes_of(instruction_data)); + if let Some(seed) = seed { + let seed_len = PodU32::from(seed.len() as u32); + data.extend_from_slice(&[pod_bytes_of(&seed_len), seed.as_bytes()].concat()); + } + Instruction { + program_id: crate::id(), + accounts, + data, + } +} diff --git a/votor-messages/src/lib.rs b/votor-messages/src/lib.rs new file mode 100644 index 0000000000..1626124cab --- /dev/null +++ b/votor-messages/src/lib.rs @@ -0,0 +1,19 @@ +//! Alpenglow Vote program +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![deny(missing_docs)] + +pub mod accounting; +pub mod bls_message; +pub mod instruction; +pub mod state; +pub mod vote; + +#[cfg_attr(feature = "frozen-abi", macro_use)] +#[cfg(feature = "frozen-abi")] +extern crate solana_frozen_abi_macro; + +// Export current SDK types for downstream users building with a different SDK +// version +pub use solana_program; + +solana_program::declare_id!("Vote222222222222222222222222222222222222222"); diff --git a/votor-messages/src/state.rs b/votor-messages/src/state.rs new file mode 100644 index 0000000000..75b607949d --- /dev/null +++ b/votor-messages/src/state.rs @@ -0,0 +1,256 @@ +//! Program state +use { + // crate::alpenglow::accounting::{AuthorizedVoter, EpochCredit}, + crate::accounting::{AuthorizedVoter, EpochCredit}, + bytemuck::{Pod, Zeroable}, + solana_account::{AccountSharedData, WritableAccount}, + solana_bls_signatures::Pubkey as BlsPubkey, + solana_program::{ + clock::{Epoch, Slot, UnixTimestamp}, + hash::Hash, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + }, + solana_vote_interface::state::BlockTimestamp as LegacyBlockTimestamp, + spl_pod::primitives::{PodI64, PodU64}, +}; + +pub(crate) type PodEpoch = PodU64; +pub(crate) type PodSlot = PodU64; +pub(crate) type PodUnixTimestamp = PodI64; + +/// The accounting and vote information associated with +/// this vote account +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable, Default, PartialEq)] +pub struct VoteState { + /// The current vote state version + pub(crate) version: u8, + + /// The node that votes in this account + pub(crate) node_pubkey: Pubkey, + + /// The signer for withdrawals + pub(crate) authorized_withdrawer: Pubkey, + + /// Percentage (0-100) that represents what part of a rewards + /// payout should be given to this VoteAccount + pub(crate) commission: u8, + + /// The signer for vote transactions in this epoch + pub(crate) authorized_voter: AuthorizedVoter, + + /// The signer for vote transaction in an upcoming epoch + pub(crate) next_authorized_voter: Option, + + /// How many credits this validator is earning in this Epoch + pub(crate) epoch_credits: EpochCredit, + + /// The slot of the latest replayed block + /// Only relevant after APE + pub(crate) _replayed_slot: PodSlot, + + /// The bank hash of the latest replayed block + /// Only relevant after APE + pub(crate) _replayed_bank_hash: Hash, + + /// Associated BLS public key + pub(crate) bls_pubkey: BlsPubkey, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable, Default, PartialEq)] +/// The most recent timestamp submitted with a notarization vote +pub struct BlockTimestamp { + pub(crate) slot: PodSlot, + pub(crate) timestamp: PodUnixTimestamp, +} + +impl BlockTimestamp { + /// The slot that was voted on + pub fn slot(&self) -> Slot { + Slot::from(self.slot) + } + + /// The timestamp + pub fn timestamp(&self) -> UnixTimestamp { + UnixTimestamp::from(self.timestamp) + } +} + +impl From<&BlockTimestamp> for LegacyBlockTimestamp { + fn from(ts: &BlockTimestamp) -> Self { + LegacyBlockTimestamp { + slot: ts.slot(), + timestamp: ts.timestamp(), + } + } +} + +impl VoteState { + const VOTE_STATE_VERSION: u8 = 1; + + /// Create a new vote state for tests + pub fn new_for_tests( + node_pubkey: Pubkey, + authorized_voter: Pubkey, + epoch: Epoch, + authorized_withdrawer: Pubkey, + commission: u8, + bls_pubkey: BlsPubkey, + ) -> Self { + Self { + version: Self::VOTE_STATE_VERSION, + node_pubkey, + authorized_voter: AuthorizedVoter { + epoch: PodU64::from(epoch), + voter: authorized_voter, + }, + authorized_withdrawer, + commission, + bls_pubkey, + ..VoteState::default() + } + } + + /// Create a new vote state and wrap it in an account + pub fn create_account_with_authorized( + node_pubkey: &Pubkey, + authorized_voter: &Pubkey, + authorized_withdrawer: &Pubkey, + commission: u8, + lamports: u64, + bls_pubkey: BlsPubkey, + ) -> AccountSharedData { + let mut account = AccountSharedData::new(lamports, Self::size(), &crate::id()); + let vote_state = Self::new_for_tests( + *node_pubkey, + *authorized_voter, + 0, // Epoch + *authorized_withdrawer, + commission, + bls_pubkey, + ); + vote_state.serialize_into(account.data_as_mut_slice()); + account + } + + /// Return whether the vote account is initialized + pub fn is_initialized(&self) -> bool { + self.version > 0 + } + + /// Deserialize a vote state from input data. + /// Callers can use this with the `data` field from an `AccountInfo` + pub fn deserialize(vote_account_data: &[u8]) -> Result<&VoteState, ProgramError> { + spl_pod::bytemuck::pod_from_bytes::(vote_account_data) + } + + /// Serializes a vote state into an output buffer + /// Callers can use this with the mutable reference to `data` from + /// an `AccountInfo` + pub fn serialize_into(&self, vote_account_data: &mut [u8]) { + vote_account_data.copy_from_slice(bytemuck::bytes_of(self)) + } + + /// The size of the vote account that stores this VoteState + pub const fn size() -> usize { + std::mem::size_of::() + } + + /// Vote state version + pub fn version(&self) -> u8 { + self.version + } + + /// Validator that votes in this account + pub fn node_pubkey(&self) -> &Pubkey { + &self.node_pubkey + } + + /// Signer for withdrawals + pub fn authorized_withdrawer(&self) -> &Pubkey { + &self.authorized_withdrawer + } + + /// Percentage (0-100) that represents what part of a rewards + /// payout should be given to this VoteAccount + pub fn commission(&self) -> u8 { + self.commission + } + + /// The authorized voter for the given epoch + pub fn get_authorized_voter(&self, epoch: Epoch) -> Option { + if let Some(av) = self.next_authorized_voter { + if epoch >= av.epoch() { + return Some(av.voter); + } + } + if epoch >= self.authorized_voter.epoch() { + return Some(self.authorized_voter.voter); + } + None + } + + /// Get rent exempt reserve + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { + rent.minimum_balance(Self::size()) + } + + /// The signer for vote transactions in this epoch + pub fn authorized_voter(&self) -> &AuthorizedVoter { + &self.authorized_voter + } + + /// The signer for vote transactions in an upcoming epoch + pub fn next_authorized_voter(&self) -> Option<&AuthorizedVoter> { + self.next_authorized_voter.as_ref() + } + + /// How many credits this validator is earning in this Epoch + pub fn epoch_credits(&self) -> &EpochCredit { + &self.epoch_credits + } + + /// Most recent timestamp submitted with a vote + pub fn latest_timestamp_legacy_format(&self) -> LegacyBlockTimestamp { + // TODO: fix once we figure out how to do timestamps in BLS + LegacyBlockTimestamp::from(&BlockTimestamp::default()) + } + + /// Set the node_pubkey + pub fn set_node_pubkey(&mut self, node_pubkey: Pubkey) { + self.node_pubkey = node_pubkey + } + + /// Set the authorized withdrawer + pub fn set_authorized_withdrawer(&mut self, authorized_withdrawer: Pubkey) { + self.authorized_withdrawer = authorized_withdrawer + } + + /// Set the commission + pub fn set_commission(&mut self, commission: u8) { + self.commission = commission + } + + /// Set the authorized voter + pub fn set_authorized_voter(&mut self, authorized_voter: AuthorizedVoter) { + self.authorized_voter = authorized_voter + } + + /// Set the next authorized voter + pub fn set_next_authorized_voter(&mut self, next_authorized_voter: AuthorizedVoter) { + self.next_authorized_voter = Some(next_authorized_voter) + } + + /// Set the epoch credits + pub fn set_epoch_credits(&mut self, epoch_credits: EpochCredit) { + self.epoch_credits = epoch_credits + } + + /// Get the BLS pubkey + pub fn bls_pubkey(&self) -> &BlsPubkey { + &self.bls_pubkey + } +} diff --git a/votor-messages/src/vote.rs b/votor-messages/src/vote.rs new file mode 100644 index 0000000000..939d3ad5a5 --- /dev/null +++ b/votor-messages/src/vote.rs @@ -0,0 +1,263 @@ +//! Vote data types for use by clients +use { + serde::{Deserialize, Serialize}, + solana_hash::Hash, + solana_program::clock::Slot, +}; + +/// Enum that clients can use to parse and create the vote +/// structures expected by the program +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor), + frozen_abi(digest = "FRn4f3PTtbvw3uv2r3qF8K49a5UF4QqDuVdyeshtipTW") +)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum Vote { + /// A notarization vote + Notarize(NotarizationVote), + /// A finalization vote + Finalize(FinalizationVote), + /// A skip vote + Skip(SkipVote), + /// A notarization fallback vote + NotarizeFallback(NotarizationFallbackVote), + /// A skip fallback vote + SkipFallback(SkipFallbackVote), +} + +impl Vote { + /// Create a new notarization vote + pub fn new_notarization_vote(slot: Slot, block_id: Hash) -> Self { + Self::from(NotarizationVote::new(slot, block_id)) + } + + /// Create a new finalization vote + pub fn new_finalization_vote(slot: Slot) -> Self { + Self::from(FinalizationVote::new(slot)) + } + + /// Create a new skip vote + pub fn new_skip_vote(slot: Slot) -> Self { + Self::from(SkipVote::new(slot)) + } + + /// Create a new notarization fallback vote + pub fn new_notarization_fallback_vote(slot: Slot, block_id: Hash) -> Self { + Self::from(NotarizationFallbackVote::new(slot, block_id)) + } + + /// Create a new skip fallback vote + pub fn new_skip_fallback_vote(slot: Slot) -> Self { + Self::from(SkipFallbackVote::new(slot)) + } + + /// The slot which was voted for + pub fn slot(&self) -> Slot { + match self { + Self::Notarize(vote) => vote.slot(), + Self::Finalize(vote) => vote.slot(), + Self::Skip(vote) => vote.slot(), + Self::NotarizeFallback(vote) => vote.slot(), + Self::SkipFallback(vote) => vote.slot(), + } + } + + /// The block id associated with the block which was voted for + pub fn block_id(&self) -> Option<&Hash> { + match self { + Self::Notarize(vote) => Some(vote.block_id()), + Self::NotarizeFallback(vote) => Some(vote.block_id()), + Self::Finalize(_) | Self::Skip(_) | Self::SkipFallback(_) => None, + } + } + + /// Whether the vote is a notarization vote + pub fn is_notarization(&self) -> bool { + matches!(self, Self::Notarize(_)) + } + + /// Whether the vote is a finalization vote + pub fn is_finalize(&self) -> bool { + matches!(self, Self::Finalize(_)) + } + + /// Whether the vote is a skip vote + pub fn is_skip(&self) -> bool { + matches!(self, Self::Skip(_)) + } + + /// Whether the vote is a notarization fallback vote + pub fn is_notarize_fallback(&self) -> bool { + matches!(self, Self::NotarizeFallback(_)) + } + + /// Whether the vote is a skip fallback vote + pub fn is_skip_fallback(&self) -> bool { + matches!(self, Self::SkipFallback(_)) + } + + /// Whether the vote is a notarization or finalization + pub fn is_notarization_or_finalization(&self) -> bool { + matches!(self, Self::Notarize(_) | Self::Finalize(_)) + } +} + +impl From for Vote { + fn from(vote: NotarizationVote) -> Self { + Self::Notarize(vote) + } +} + +impl From for Vote { + fn from(vote: FinalizationVote) -> Self { + Self::Finalize(vote) + } +} + +impl From for Vote { + fn from(vote: SkipVote) -> Self { + Self::Skip(vote) + } +} + +impl From for Vote { + fn from(vote: NotarizationFallbackVote) -> Self { + Self::NotarizeFallback(vote) + } +} + +impl From for Vote { + fn from(vote: SkipFallbackVote) -> Self { + Self::SkipFallback(vote) + } +} + +/// A notarization vote +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "5AdwChAjsj5QUXLdpDnGGK2L2nA8y8EajVXi6jsmTv1m") +)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct NotarizationVote { + slot: Slot, + block_id: Hash, +} + +impl NotarizationVote { + /// Construct a notarization vote for `slot` + pub fn new(slot: Slot, block_id: Hash) -> Self { + Self { slot, block_id } + } + + /// The slot to notarize + pub fn slot(&self) -> Slot { + self.slot + } + + /// The block_id of the notarization slot + pub fn block_id(&self) -> &Hash { + &self.block_id + } +} + +/// A finalization vote +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "2XQ5N6YLJjF28w7cMFFUQ9SDgKuf9JpJNtAiXSPA8vR2") +)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct FinalizationVote { + slot: Slot, +} + +impl FinalizationVote { + /// Construct a finalization vote for `slot` + pub fn new(slot: Slot) -> Self { + Self { slot } + } + + /// The slot to finalize + pub fn slot(&self) -> Slot { + self.slot + } +} + +/// A skip vote +/// Represents a range of slots to skip +/// inclusive on both ends +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "G8Nrx3sMYdnLpHsCNark3BGA58BmW2sqNnqjkYhQHtN") +)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct SkipVote { + pub(crate) slot: Slot, +} + +impl SkipVote { + /// Construct a skip vote for `slot` + pub fn new(slot: Slot) -> Self { + Self { slot } + } + + /// The slot to skip + pub fn slot(&self) -> Slot { + self.slot + } +} + +/// A notarization fallback vote +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "7j5ZPwwyz1FaG3fpyQv5PVnQXicdSmqSk8NvqzkG1Eqz") +)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct NotarizationFallbackVote { + slot: Slot, + block_id: Hash, +} + +impl NotarizationFallbackVote { + /// Construct a notarization vote for `slot` + pub fn new(slot: Slot, block_id: Hash) -> Self { + Self { slot, block_id } + } + + /// The slot to notarize + pub fn slot(&self) -> Slot { + self.slot + } + + /// The block_id of the notarization slot + pub fn block_id(&self) -> &Hash { + &self.block_id + } +} + +/// A skip fallback vote +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "WsUNum8V62gjRU1yAnPuBMAQui4YvMwD1RwrzHeYkeF") +)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct SkipFallbackVote { + pub(crate) slot: Slot, +} + +impl SkipFallbackVote { + /// Construct a skip fallback vote for `slot` + pub fn new(slot: Slot) -> Self { + Self { slot } + } + + /// The slot to skip + pub fn slot(&self) -> Slot { + self.slot + } +} diff --git a/votor/Cargo.toml b/votor/Cargo.toml new file mode 100644 index 0000000000..94d3baeb68 --- /dev/null +++ b/votor/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "solana-votor" +description = "Blockchain, Rebuilt for Scale" +documentation = "https://docs.rs/solana-votor" +readme = "../README.md" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[features] +dev-context-only-utils = ["solana-runtime/dev-context-only-utils"] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-accounts-db/frozen-abi", + "solana-bloom/frozen-abi", + "solana-ledger/frozen-abi", + "solana-runtime/frozen-abi", + "solana-sdk/frozen-abi", + "solana-vote/frozen-abi", + "solana-vote-program/frozen-abi", +] + +[dependencies] +anyhow = { workspace = true } +bincode = { workspace = true } +bitvec = { workspace = true } +bs58 = { workspace = true } +crossbeam-channel = { workspace = true } +dashmap = { workspace = true, features = ["rayon", "raw-api"] } +etcd-client = { workspace = true, features = ["tls"] } +itertools = { workspace = true } +log = { workspace = true } +qualifier_attr = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +serde_derive = { workspace = true } +solana-accounts-db = { workspace = true } +solana-bloom = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["solana-signer-derive"] } +solana-entry = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-gossip = { workspace = true } +solana-ledger = { workspace = true } +solana-logger = { workspace = true } +solana-measure = { workspace = true } +solana-metrics = { workspace = true } +solana-pubkey = { workspace = true } +solana-rpc = { workspace = true } +solana-runtime = { workspace = true } +solana-sdk = { workspace = true } +solana-vote = { workspace = true } +solana-vote-program = { workspace = true } +solana-votor-messages = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } +test-case = { workspace = true } + +[lints] +workspace = true diff --git a/votor/src/certificate_pool.rs b/votor/src/certificate_pool.rs new file mode 100644 index 0000000000..e7d1a4dac3 --- /dev/null +++ b/votor/src/certificate_pool.rs @@ -0,0 +1,1919 @@ +use { + crate::{ + certificate_limits_and_vote_types, + certificate_pool::{ + parent_ready_tracker::ParentReadyTracker, + stats::CertificatePoolStats, + vote_certificate_builder::{CertificateError, VoteCertificateBuilder}, + vote_pool::{DuplicateBlockVotePool, SimpleVotePool, VotePool, VotePoolType}, + }, + commitment::AlpenglowCommitmentError, + conflicting_types, + event::VotorEvent, + vote_to_certificate_ids, Certificate, Stake, VoteType, + MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE, MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES, + SAFE_TO_NOTAR_MIN_NOTARIZE_AND_SKIP, SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP, + SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY, SAFE_TO_SKIP_THRESHOLD, + }, + crossbeam_channel::Sender, + solana_ledger::blockstore::Blockstore, + solana_pubkey::Pubkey, + solana_runtime::{bank::Bank, epoch_stakes::EpochStakes}, + solana_sdk::{ + clock::{Epoch, Slot}, + epoch_schedule::EpochSchedule, + hash::Hash, + }, + solana_votor_messages::{ + bls_message::{BLSMessage, Block, CertificateMessage, VoteMessage}, + vote::Vote, + }, + std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, + }, + thiserror::Error, +}; + +pub mod parent_ready_tracker; +mod stats; +mod vote_certificate_builder; +mod vote_pool; + +impl VoteType { + pub fn get_type(vote: &Vote) -> VoteType { + match vote { + Vote::Notarize(_) => VoteType::Notarize, + Vote::NotarizeFallback(_) => VoteType::NotarizeFallback, + Vote::Skip(_) => VoteType::Skip, + Vote::SkipFallback(_) => VoteType::SkipFallback, + Vote::Finalize(_) => VoteType::Finalize, + } + } +} + +pub type PoolId = (Slot, VoteType); + +#[derive(Debug, Error, PartialEq)] +pub enum AddVoteError { + #[error("Conflicting vote type: {0:?} vs existing {1:?} for slot: {2} pubkey: {3}")] + ConflictingVoteType(VoteType, VoteType, Slot, Pubkey), + + #[error("Epoch stakes missing for epoch: {0}")] + EpochStakesNotFound(Epoch), + + #[error("Unrooted slot")] + UnrootedSlot, + + #[error("Slot in the future")] + SlotInFuture, + + #[error("Certificate error: {0}")] + Certificate(#[from] CertificateError), + + #[error("{0} channel disconnected")] + ChannelDisconnected(String), + + #[error("Voting Service queue full")] + VotingServiceQueueFull, + + #[error("Invalid rank: {0}")] + InvalidRank(u16), +} + +impl From for AddVoteError { + fn from(_: AlpenglowCommitmentError) -> Self { + AddVoteError::ChannelDisconnected("CommitmentSender".to_string()) + } +} + +#[derive(Default)] +pub struct CertificatePool { + // Vote pools to do bean counting for votes. + vote_pools: BTreeMap, + /// Completed certificates + completed_certificates: BTreeMap>, + /// Tracks slots which have reached the parent ready condition: + /// - They have a potential parent block with a NotarizeFallback certificate + /// - All slots from the parent have a Skip certificate + pub parent_ready_tracker: ParentReadyTracker, + /// Highest block that has a NotarizeFallback certificate, for use in producing our leader window + highest_notarized_fallback: Option<(Slot, Hash)>, + /// Highest slot that has a Finalized variant certificate, for use in notifying RPC + highest_finalized_slot: Option, + /// Highest slot that has Finalize+Notarize or FinalizeFast, for use in standstill + highest_finalized_with_notarize: Option, + // Cached epoch_schedule + epoch_schedule: EpochSchedule, + // Cached epoch_stakes_map + epoch_stakes_map: Arc>, + // The current root, no need to save anything before this slot. + root: Slot, + // The epoch of current root. + root_epoch: Epoch, + /// The certificate sender, if set, newly created certificates will be sent here + certificate_sender: Option>, + /// Stats for the certificate pool + stats: CertificatePoolStats, +} + +impl CertificatePool { + pub fn new_from_root_bank( + my_pubkey: Pubkey, + bank: &Bank, + certificate_sender: Option>, + ) -> Self { + // To account for genesis and snapshots we allow default block id until + // block id can be serialized as part of the snapshot + let root_block = (bank.slot(), bank.block_id().unwrap_or_default()); + let parent_ready_tracker = ParentReadyTracker::new(my_pubkey, root_block); + + let mut pool = Self { + vote_pools: BTreeMap::new(), + completed_certificates: BTreeMap::new(), + highest_notarized_fallback: None, + highest_finalized_slot: None, + highest_finalized_with_notarize: None, + epoch_schedule: EpochSchedule::default(), + epoch_stakes_map: Arc::new(HashMap::new()), + root: bank.slot(), + root_epoch: Epoch::default(), + certificate_sender, + parent_ready_tracker, + stats: CertificatePoolStats::new(), + }; + + // Update the epoch_stakes_map and root + pool.update_epoch_stakes_map(bank); + pool.root = bank.slot(); + + pool + } + + pub fn root(&self) -> Slot { + self.root + } + + fn update_epoch_stakes_map(&mut self, bank: &Bank) { + let epoch = bank.epoch(); + if self.epoch_stakes_map.is_empty() || epoch > self.root_epoch { + self.epoch_stakes_map = Arc::new(bank.epoch_stakes_map().clone()); + self.root_epoch = epoch; + self.epoch_schedule = bank.epoch_schedule().clone(); + } + } + + fn new_vote_pool(vote_type: VoteType) -> VotePoolType { + match vote_type { + VoteType::NotarizeFallback => VotePoolType::DuplicateBlockVotePool( + DuplicateBlockVotePool::new(MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE), + ), + VoteType::Notarize => VotePoolType::DuplicateBlockVotePool( + DuplicateBlockVotePool::new(MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES), + ), + _ => VotePoolType::SimpleVotePool(SimpleVotePool::new()), + } + } + + fn update_vote_pool( + &mut self, + slot: Slot, + vote_type: VoteType, + block_id: Option, + transaction: &VoteMessage, + validator_vote_key: &Pubkey, + validator_stake: Stake, + ) -> bool { + let pool = self + .vote_pools + .entry((slot, vote_type)) + .or_insert_with(|| Self::new_vote_pool(vote_type)); + match pool { + VotePoolType::SimpleVotePool(pool) => { + pool.add_vote(validator_vote_key, validator_stake, transaction) + } + VotePoolType::DuplicateBlockVotePool(pool) => pool.add_vote( + validator_vote_key, + block_id.expect("Duplicate block pool expects a block id"), + transaction, + validator_stake, + ), + } + } + + /// For a new vote `slot` , `vote_type` checks if any + /// of the related certificates are newly complete. + /// For each newly constructed certificate + /// - Insert it into `self.certificates` + /// - Potentially update `self.highest_notarized_fallback`, + /// - If it is a `is_critical` certificate, send via the certificate sender + /// - Potentially update `self.highest_finalized_slot`, + /// - If we have a new highest finalized slot, return it + /// - update any newly created events + fn update_certificates( + &mut self, + vote: &Vote, + block_id: Option, + events: &mut Vec, + total_stake: Stake, + ) -> Result>, AddVoteError> { + let slot = vote.slot(); + let mut new_certificates_to_send = Vec::new(); + for cert_id in vote_to_certificate_ids(vote) { + // If the certificate is already complete, skip it + if self.completed_certificates.contains_key(&cert_id) { + continue; + } + // Otherwise check whether the certificate is complete + let (limit, vote_types) = certificate_limits_and_vote_types(cert_id); + let accumulated_stake = vote_types + .iter() + .filter_map(|vote_type| { + Some(match self.vote_pools + .get(&(slot, *vote_type))? { + VotePoolType::SimpleVotePool(pool) => pool.total_stake(), + VotePoolType::DuplicateBlockVotePool(pool) => pool.total_stake_by_block_id(block_id.as_ref().expect("Duplicate block pool for {vote_type:?} expects a block id for certificate {cert_id:?}")), + }) + }) + .sum::(); + if accumulated_stake as f64 / (total_stake as f64) < limit { + continue; + } + let mut vote_certificate_builder = VoteCertificateBuilder::new(cert_id); + vote_types.iter().for_each(|vote_type| { + if let Some(vote_pool) = self.vote_pools.get(&(slot, *vote_type)) { + match vote_pool { + VotePoolType::SimpleVotePool(pool) => pool.add_to_certificate(&mut vote_certificate_builder), + VotePoolType::DuplicateBlockVotePool(pool) => pool.add_to_certificate(block_id.as_ref().expect("Duplicate block pool for {vote_type:?} expects a block id for certificate {cert_id:?}"), &mut vote_certificate_builder), + }; + } + }); + let new_cert = Arc::new(vote_certificate_builder.build()); + self.send_and_insert_certificate(cert_id, new_cert.clone(), events)?; + self.stats + .incr_cert_type(new_cert.certificate.certificate_type(), true); + new_certificates_to_send.push(new_cert); + } + Ok(new_certificates_to_send) + } + + fn send_and_insert_certificate( + &mut self, + cert_id: Certificate, + vote_certificate: Arc, + events: &mut Vec, + ) -> Result<(), AddVoteError> { + if let Some(sender) = &self.certificate_sender { + if cert_id.is_critical() { + if let Err(e) = sender.try_send((cert_id, (*vote_certificate).clone())) { + error!("Unable to send certificate {cert_id:?}: {e:?}"); + return Err(AddVoteError::ChannelDisconnected( + "CertificateSender".to_string(), + )); + } + } + } + self.insert_certificate(cert_id, vote_certificate, events); + Ok(()) + } + + fn has_conflicting_vote( + &self, + slot: Slot, + vote_type: VoteType, + validator_vote_key: &Pubkey, + block_id: &Option, + ) -> Option { + for conflicting_type in conflicting_types(vote_type) { + if let Some(pool) = self.vote_pools.get(&(slot, *conflicting_type)) { + let is_conflicting = match pool { + // In a simple vote pool, just check if the validator previously voted at all. If so, that's a conflict + VotePoolType::SimpleVotePool(pool) => { + pool.has_prev_validator_vote(validator_vote_key) + } + // In a duplicate block vote pool, because some conflicts between things like Notarize and NotarizeFallback + // for different blocks are allowed, we need a more specific check. + // TODO: This can be made much cleaner/safer if VoteType carried the bank hash, block id so we + // could check which exact VoteType(blockid, bankhash) was the source of the conflict. + VotePoolType::DuplicateBlockVotePool(pool) => { + if let Some(block_id) = &block_id { + // Reject votes for the same block with a conflicting type, i.e. + // a NotarizeFallback vote for the same block as a Notarize vote. + pool.has_prev_validator_vote_for_block(validator_vote_key, block_id) + } else { + pool.has_prev_validator_vote(validator_vote_key) + } + } + }; + if is_conflicting { + return Some(*conflicting_type); + } + } + } + None + } + + fn insert_certificate( + &mut self, + cert_id: Certificate, + cert: Arc, + events: &mut Vec, + ) { + self.completed_certificates.insert(cert_id, cert); + match cert_id { + Certificate::NotarizeFallback(slot, block_id) => { + self.parent_ready_tracker + .add_new_notar_fallback((slot, block_id), events); + if self + .highest_notarized_fallback + .map_or(true, |(s, _)| s < slot) + { + self.highest_notarized_fallback = Some((slot, block_id)); + } + } + Certificate::Skip(slot) => self.parent_ready_tracker.add_new_skip(slot, events), + Certificate::Notarize(slot, block_id) => { + events.push(VotorEvent::BlockNotarized((slot, block_id))); + if self.is_finalized(slot) { + events.push(VotorEvent::Finalized((slot, block_id))); + if self + .highest_finalized_with_notarize + .map_or(true, |s| s < slot) + { + self.highest_finalized_with_notarize = Some(slot); + } + } + } + Certificate::Finalize(slot) => { + if let Some(block) = self.get_notarized_block(slot) { + events.push(VotorEvent::Finalized(block)); + if self + .highest_finalized_with_notarize + .map_or(true, |s| s < slot) + { + self.highest_finalized_with_notarize = Some(slot); + } + } + if self.highest_finalized_slot.map_or(true, |s| s < slot) { + self.highest_finalized_slot = Some(slot); + } + } + Certificate::FinalizeFast(slot, block_id) => { + events.push(VotorEvent::Finalized((slot, block_id))); + if self.highest_finalized_slot.map_or(true, |s| s < slot) { + self.highest_finalized_slot = Some(slot); + } + if self + .highest_finalized_with_notarize + .map_or(true, |s| s < slot) + { + self.highest_finalized_with_notarize = Some(slot); + } + } + } + } + + fn get_key_and_stakes( + &self, + slot: Slot, + rank: u16, + ) -> Result<(Pubkey, Stake, Stake), AddVoteError> { + let epoch = self.epoch_schedule.get_epoch(slot); + let epoch_stakes = self + .epoch_stakes_map + .get(&epoch) + .ok_or(AddVoteError::EpochStakesNotFound(epoch))?; + let Some((vote_key, _)) = epoch_stakes + .bls_pubkey_to_rank_map() + .get_pubkey(rank as usize) + else { + return Err(AddVoteError::InvalidRank(rank)); + }; + let stake = epoch_stakes.vote_account_stake(vote_key); + if stake == 0 { + // Since we have a valid rank, this should never happen, there is no rank for zero stake. + panic!("Validator stake is zero for pubkey: {vote_key}"); + } + Ok((*vote_key, stake, epoch_stakes.total_stake())) + } + + /// Adds the new vote the the certificate pool. If a new certificate is created + /// as a result of this, send it via the `self.certificate_sender` + /// + /// Any new votor events that are a result of adding this new vote will be added + /// to `events`. + /// + /// If this resulted in a new highest Finalize or FastFinalize certificate, + /// return the slot + pub fn add_message( + &mut self, + my_vote_pubkey: &Pubkey, + message: &BLSMessage, + events: &mut Vec, + ) -> Result<(Option, Vec>), AddVoteError> { + let current_highest_finalized_slot = self.highest_finalized_slot; + let new_certficates_to_send = match message { + BLSMessage::Vote(vote_message) => { + self.add_vote(my_vote_pubkey, vote_message, events)? + } + BLSMessage::Certificate(certificate_message) => { + self.add_certificate(certificate_message, events)? + } + }; + // If we have a new highest finalized slot, return it + let new_finalized_slot = if self.highest_finalized_slot > current_highest_finalized_slot { + self.highest_finalized_slot + } else { + None + }; + Ok((new_finalized_slot, new_certficates_to_send)) + } + + fn add_vote( + &mut self, + my_vote_pubkey: &Pubkey, + vote_message: &VoteMessage, + events: &mut Vec, + ) -> Result>, AddVoteError> { + let vote = &vote_message.vote; + let rank = vote_message.rank; + let slot = vote.slot(); + let (validator_vote_key, validator_stake, total_stake) = + self.get_key_and_stakes(slot, rank)?; + + // Since we have a valid rank, this should never happen, there is no rank for zero stake. + assert_ne!( + validator_stake, 0, + "Validator stake is zero for pubkey: {validator_vote_key}" + ); + + self.stats.incoming_votes = self.stats.incoming_votes.saturating_add(1); + if slot < self.root { + self.stats.out_of_range_votes = self.stats.out_of_range_votes.saturating_add(1); + return Err(AddVoteError::UnrootedSlot); + } + let block_id = vote.block_id().map(|block_id| { + if !matches!(vote, Vote::Notarize(_) | Vote::NotarizeFallback(_)) { + panic!("expected Notarize or NotarizeFallback vote"); + } + *block_id + }); + let vote_type = VoteType::get_type(vote); + if let Some(conflicting_type) = + self.has_conflicting_vote(slot, vote_type, &validator_vote_key, &block_id) + { + self.stats.conflicting_votes = self.stats.conflicting_votes.saturating_add(1); + return Err(AddVoteError::ConflictingVoteType( + vote_type, + conflicting_type, + slot, + validator_vote_key, + )); + } + if !self.update_vote_pool( + slot, + vote_type, + block_id, + vote_message, + &validator_vote_key, + validator_stake, + ) { + self.stats.exist_votes = self.stats.exist_votes.saturating_add(1); + return Ok(vec![]); + } + // Check if this new vote generated a safe to notar or safe to skip + // TODO: we should just handle this when adding the vote rather than + // calling out here again. Also deal with duplicate events, don't notify + // everytime. + if self.safe_to_skip(my_vote_pubkey, slot) { + events.push(VotorEvent::SafeToSkip(slot)); + self.stats.event_safe_to_skip = self.stats.event_safe_to_skip.saturating_add(1); + } + for block_id in self.safe_to_notar(my_vote_pubkey, slot) { + events.push(VotorEvent::SafeToNotar((slot, block_id))); + self.stats.event_safe_to_notarize = self.stats.event_safe_to_notarize.saturating_add(1); + } + + self.stats.incr_ingested_vote_type(vote_type); + + self.update_certificates(vote, block_id, events, total_stake) + } + + fn add_certificate( + &mut self, + certificate_message: &CertificateMessage, + events: &mut Vec, + ) -> Result>, AddVoteError> { + let certificate_id = certificate_message.certificate; + self.stats.incoming_certs = self.stats.incoming_certs.saturating_add(1); + if certificate_id.slot() < self.root { + self.stats.out_of_range_certs = self.stats.out_of_range_certs.saturating_add(1); + return Err(AddVoteError::UnrootedSlot); + } + if self.completed_certificates.contains_key(&certificate_id) { + self.stats.exist_certs = self.stats.exist_certs.saturating_add(1); + return Ok(vec![]); + } + let new_certificate = Arc::new(certificate_message.clone()); + self.send_and_insert_certificate(certificate_id, new_certificate.clone(), events)?; + + self.stats + .incr_cert_type(certificate_id.certificate_type(), false); + + Ok(vec![new_certificate]) + } + + /// The highest notarized fallback slot, for use as the parent slot in leader window + pub fn highest_notarized_fallback(&self) -> Option<(Slot, Hash)> { + self.highest_notarized_fallback + } + + /// Get the notarized block in `slot` + pub fn get_notarized_block(&self, slot: Slot) -> Option { + self.completed_certificates + .iter() + .find_map(|(cert_id, _)| match cert_id { + Certificate::Notarize(s, block_id) if slot == *s => Some((*s, *block_id)), + _ => None, + }) + } + + #[cfg(test)] + fn highest_notarized_slot(&self) -> Slot { + // Return the max of CertificateType::Notarize and CertificateType::NotarizeFallback + self.completed_certificates + .iter() + .filter_map(|(cert_id, _)| match cert_id { + Certificate::Notarize(s, _) => Some(s), + Certificate::NotarizeFallback(s, _) => Some(s), + _ => None, + }) + .max() + .copied() + .unwrap_or(0) + } + + #[cfg(test)] + fn highest_skip_slot(&self) -> Slot { + self.completed_certificates + .iter() + .filter_map(|(cert_id, _)| match cert_id { + Certificate::Skip(s) => Some(s), + _ => None, + }) + .max() + .copied() + .unwrap_or(0) + } + + pub fn highest_finalized_slot(&self) -> Slot { + self.completed_certificates + .iter() + .filter_map(|(cert_id, _)| match cert_id { + Certificate::Finalize(s) => Some(s), + Certificate::FinalizeFast(s, _) => Some(s), + _ => None, + }) + .max() + .copied() + .unwrap_or(0) + } + + pub fn highest_fast_finalized_block(&self) -> Option { + self.completed_certificates + .iter() + .filter_map(|(cert_id, _)| match cert_id { + Certificate::FinalizeFast(s, bid) => Some((*s, *bid)), + _ => None, + }) + .max() + } + + /// Checks if any block in the slot `s` is finalized + pub fn is_finalized(&self, slot: Slot) -> bool { + self.completed_certificates.keys().any(|cert_id| { + matches!(cert_id, Certificate::Finalize(s) | Certificate::FinalizeFast(s, _) if *s == slot) + }) + } + + /// Check if the specific block `(block_id)` in slot `s` is notarized + pub fn is_notarized(&self, slot: Slot, block_id: Hash) -> bool { + self.completed_certificates + .contains_key(&Certificate::Notarize(slot, block_id)) + } + + /// Checks if the any block in slot `slot` has received a `NotarizeFallback` certificate, if so return + /// the size of the certificate + #[cfg(test)] + pub fn slot_has_notarized_fallback(&self, slot: Slot) -> bool { + self.completed_certificates + .iter() + .any(|(cert_id, _)| matches!(cert_id, Certificate::NotarizeFallback(s,_) if *s == slot)) + } + + /// Checks if `slot` has a `Skip` certificate + pub fn skip_certified(&self, slot: Slot) -> bool { + self.completed_certificates + .contains_key(&Certificate::Skip(slot)) + } + + /// Checks if we have voted to skip `slot` or already notarized some block `b` in `slot` + /// Additionally check if there exists blocks `b` in `slot` such that: + /// (i) At least 40% of stake has voted to notarize `b` + /// (ii) At least 20% of stake voted to notarize `b` and at least 60% of stake voted to either notarize `b` or skip `slot` + /// and we have not already cast a notarize fallback for this `b` in `slot` + /// If all the above hold, return the block ids `Vec` for all such `b` + pub fn safe_to_notar(&self, my_vote_pubkey: &Pubkey, slot: Slot) -> Vec { + let Some(epoch_stakes) = self + .epoch_stakes_map + .get(&self.epoch_schedule.get_epoch(slot)) + else { + return vec![]; + }; + let total_stake = epoch_stakes.total_stake(); + + let skip_ratio = self + .vote_pools + .get(&(slot, VoteType::Skip)) + .map_or(0, |pool| pool.total_stake()) as f64 + / total_stake as f64; + + let voted_skip = self + .vote_pools + .get(&(slot, VoteType::Skip)) + .is_some_and(|pool| pool.has_prev_validator_vote(my_vote_pubkey)); + + let Some(notarize_pool) = self.vote_pools.get(&(slot, VoteType::Notarize)) else { + return vec![]; + }; + let notarize_pool = notarize_pool.unwrap_duplicate_block_vote_pool( + "Notarize vote pool should be a DuplicateBlockVotePool", + ); + let my_prev_voted_block_id = notarize_pool.get_prev_voted_block_id(my_vote_pubkey); + + let mut safe_to_notar = vec![]; + for (block_id, votes) in notarize_pool.votes.iter() { + if !voted_skip + && my_prev_voted_block_id + .as_ref() + .is_none_or(|prev_block_id| *prev_block_id == *block_id) + { + // We either have not voted for the slot or we voted notarize on this block. + // Not eligble for safe to notar + continue; + } + + let notarized_ratio = votes.total_stake_by_key as f64 / total_stake as f64; + let qualifies = + // Check if the block fits condition (i) 40% of stake holders voted notarize + notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY + // Check if the block fits condition (ii) 20% notarized, and 60% notarized or skip + || (notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP + && notarized_ratio + skip_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_AND_SKIP); + + if qualifies { + safe_to_notar.push(*block_id); + } + } + safe_to_notar + } + + /// Checks if we have already voted to notarize some block in `slot` and additionally that + /// votedStake(s) - topNotarStake(s) >= 40% where: + /// - votedStake(s) is the cumulative stake of all nodes who voted notarize or skip on s + /// - topNotarStake(s) the highest of cumulative notarize stake per block in s + pub fn safe_to_skip(&self, my_vote_pubkey: &Pubkey, slot: Slot) -> bool { + let epoch = self.epoch_schedule.get_epoch(slot); + let Some(epoch_stakes) = self.epoch_stakes_map.get(&epoch) else { + return false; + }; + let total_stake = epoch_stakes.total_stake(); + + let Some(notarize_pool) = self.vote_pools.get(&(slot, VoteType::Notarize)) else { + return false; + }; + let notarize_pool = notarize_pool.unwrap_duplicate_block_vote_pool( + "Notarize vote pool should be a DuplicateBlockVotePool", + ); + if !notarize_pool.has_prev_validator_vote(my_vote_pubkey) { + return false; + } + let voted_stake = notarize_pool.total_stake().saturating_add( + self.vote_pools + .get(&(slot, VoteType::Skip)) + .map_or(0, |pool| pool.total_stake()), + ); + let top_notarized_stake = notarize_pool.top_entry_stake(); + (voted_stake.saturating_sub(top_notarized_stake)) as f64 / total_stake as f64 + >= SAFE_TO_SKIP_THRESHOLD + } + + #[cfg(test)] + fn make_start_leader_decision( + &self, + my_leader_slot: Slot, + parent_slot: Slot, + first_alpenglow_slot: Slot, + ) -> bool { + // TODO: for GCE tests we WFSM on 1 so slot 1 is exempt + let needs_notarization_certificate = parent_slot >= first_alpenglow_slot && parent_slot > 1; + + if needs_notarization_certificate + && !self.slot_has_notarized_fallback(parent_slot) + && !self.is_finalized(parent_slot) + { + error!("Missing notarization certificate {parent_slot}"); + return false; + } + + let needs_skip_certificate = + // handles cases where we are entering the alpenglow epoch, where the first + // slot in the epoch will pass my_leader_slot == parent_slot + my_leader_slot != first_alpenglow_slot && + my_leader_slot != parent_slot.saturating_add(1); + + if needs_skip_certificate { + let begin_skip_slot = first_alpenglow_slot.max(parent_slot.saturating_add(1)); + for slot in begin_skip_slot..my_leader_slot { + if !self.skip_certified(slot) { + error!( + "Missing skip certificate for {slot}, required for skip certificate \ + from {begin_skip_slot} to build {my_leader_slot}" + ); + return false; + } + } + } + + true + } + + /// Cleanup any old slots from the certificate pool + pub fn handle_new_root(&mut self, bank: Arc) { + let new_root = bank.slot(); + self.root = new_root; + // `completed_certificates`` now only contains entries >= `slot` + self.completed_certificates + .retain(|cert_id, _| match cert_id { + Certificate::Finalize(s) + | Certificate::FinalizeFast(s, _) + | Certificate::Notarize(s, _) + | Certificate::NotarizeFallback(s, _) + | Certificate::Skip(s) => *s >= self.root, + }); + self.vote_pools = self.vote_pools.split_off(&(new_root, VoteType::Finalize)); + self.parent_ready_tracker.set_root(new_root); + self.update_epoch_stakes_map(&bank); + } + + /// Updates the pubkey used for logging purposes only. + /// This avoids the need to recreate the entire certificate pool since it's + /// not distinguished by the pubkey. + pub fn update_pubkey(&mut self, new_pubkey: Pubkey) { + self.parent_ready_tracker.update_pubkey(new_pubkey); + } + + pub fn maybe_report(&mut self) { + self.stats.maybe_report(); + } + + pub fn get_certs_for_standstill(&self) -> Vec> { + self.completed_certificates + .iter() + .filter_map(|(_, cert)| { + if Some(cert.certificate.slot()) >= self.highest_finalized_with_notarize { + Some(cert.clone()) + } else { + None + } + }) + .collect() + } +} + +pub fn load_from_blockstore( + my_pubkey: &Pubkey, + root_bank: &Bank, + blockstore: &Blockstore, + certificate_sender: Option>, + events: &mut Vec, +) -> CertificatePool { + let mut cert_pool = + CertificatePool::new_from_root_bank(*my_pubkey, root_bank, certificate_sender); + for (slot, slot_cert) in blockstore + .slot_certificates_iterator(root_bank.slot()) + .unwrap() + { + let certs = slot_cert + .notarize_fallback_certificates + .into_iter() + .map(|(block_id, cert)| { + let cert_id = Certificate::NotarizeFallback(slot, block_id); + (cert_id, cert) + }) + .chain(slot_cert.skip_certificate.map(|cert| { + let cert_id = Certificate::Skip(slot); + (cert_id, cert) + })); + + for (cert_id, cert) in certs { + trace!("{my_pubkey}: loading certificate {cert_id:?} from blockstore into certificate pool"); + cert_pool.insert_certificate(cert_id, cert.into(), events); + } + } + cert_pool +} + +#[cfg(test)] +mod tests { + use { + super::*, + bitvec::prelude::*, + itertools::Itertools, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, Signature as BLSSignature}, + solana_runtime::{ + bank::{Bank, NewBankOptions}, + bank_forks::BankForks, + genesis_utils::{ + create_genesis_config_with_alpenglow_vote_accounts_no_program, + ValidatorVoteKeypairs, + }, + }, + solana_sdk::{clock::Slot, pubkey::Pubkey, signer::Signer}, + solana_votor_messages::bls_message::{ + CertificateType, VoteMessage, BLS_KEYPAIR_DERIVE_SEED, + }, + std::sync::{Arc, RwLock}, + test_case::test_case, + }; + + fn dummy_transaction( + keypairs: &[ValidatorVoteKeypairs], + vote: &Vote, + rank: usize, + ) -> BLSMessage { + let bls_keypair = + BLSKeypair::derive_from_signer(&keypairs[rank].vote_keypair, BLS_KEYPAIR_DERIVE_SEED) + .unwrap(); + let signature: BLSSignature = bls_keypair + .sign(bincode::serialize(vote).unwrap().as_slice()) + .into(); + BLSMessage::new_vote(*vote, signature, rank as u16) + } + + fn create_bank(slot: Slot, parent: Arc, pubkey: &Pubkey) -> Bank { + Bank::new_from_parent_with_options(parent, pubkey, slot, NewBankOptions::default()) + } + + fn create_bank_forks(validator_keypairs: &[ValidatorVoteKeypairs]) -> Arc> { + let genesis = create_genesis_config_with_alpenglow_vote_accounts_no_program( + 1_000_000_000, + validator_keypairs, + vec![100; validator_keypairs.len()], + ); + let bank0 = Bank::new_for_tests(&genesis.genesis_config); + BankForks::new_rw_arc(bank0) + } + + fn create_keypairs_and_pool() -> (Vec, CertificatePool) { + // Create 10 node validatorvotekeypairs vec + let validator_keypairs = (0..10) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let bank_forks = create_bank_forks(&validator_keypairs); + let root_bank = bank_forks.read().unwrap().root_bank(); + ( + validator_keypairs, + CertificatePool::new_from_root_bank(Pubkey::new_unique(), &root_bank.clone(), None), + ) + } + + #[cfg(test)] + fn add_certificate( + pool: &mut CertificatePool, + validator_keypairs: &[ValidatorVoteKeypairs], + vote: Vote, + ) { + for rank in 0..6 { + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(validator_keypairs, &vote, 6), + &mut vec![] + ) + .is_ok()); + match vote { + Vote::Notarize(vote) => assert_eq!(pool.highest_notarized_slot(), vote.slot()), + Vote::NotarizeFallback(vote) => assert_eq!(pool.highest_notarized_slot(), vote.slot()), + Vote::Skip(vote) => assert_eq!(pool.highest_skip_slot(), vote.slot()), + Vote::SkipFallback(vote) => assert_eq!(pool.highest_skip_slot(), vote.slot()), + Vote::Finalize(vote) => assert_eq!(pool.highest_finalized_slot(), vote.slot()), + } + } + + fn add_skip_vote_range( + pool: &mut CertificatePool, + start: Slot, + end: Slot, + keypairs: &[ValidatorVoteKeypairs], + rank: usize, + ) { + for slot in start..=end { + let vote = Vote::new_skip_vote(slot); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + } + + #[test] + fn test_make_decision_leader_does_not_start_if_notarization_missing() { + let (_, pool) = create_keypairs_and_pool(); + + // No notarization set, pool is default + let parent_slot = 2; + let my_leader_slot = 3; + let first_alpenglow_slot = 0; + let decision = + pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot); + assert!( + !decision, + "Leader should not be allowed to start without notarization" + ); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_1() { + let (_, pool) = create_keypairs_and_pool(); + + // If parent_slot == 0, you don't need a notarization certificate + // Because leader_slot == parent_slot + 1, you don't need a skip certificate + let parent_slot = 0; + let my_leader_slot = 1; + let first_alpenglow_slot = 0; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_2() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // If parent_slot < first_alpenglow_slot, and parent_slot > 0 + // no notarization certificate is required, but a skip + // certificate will be + let parent_slot = 1; + let my_leader_slot = 3; + let first_alpenglow_slot = 2; + + assert!(!pool.make_start_leader_decision( + my_leader_slot, + parent_slot, + first_alpenglow_slot, + )); + + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_skip_vote(first_alpenglow_slot), + ); + + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_3() { + let (_, pool) = create_keypairs_and_pool(); + // If parent_slot == first_alpenglow_slot, and + // first_alpenglow_slot > 0, you need a notarization certificate + let parent_slot = 2; + let my_leader_slot = 3; + let first_alpenglow_slot = 2; + assert!(!pool.make_start_leader_decision( + my_leader_slot, + parent_slot, + first_alpenglow_slot, + )); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_4() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // If parent_slot < first_alpenglow_slot, and parent_slot == 0, + // no notarization certificate is required, but a skip certificate will + // be + let parent_slot = 0; + let my_leader_slot = 2; + let first_alpenglow_slot = 1; + + assert!(!pool.make_start_leader_decision( + my_leader_slot, + parent_slot, + first_alpenglow_slot, + )); + + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_skip_vote(first_alpenglow_slot), + ); + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_5() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // Valid skip certificate for 1-9 exists + for slot in 1..=9 { + add_certificate(&mut pool, &validator_keypairs, Vote::new_skip_vote(slot)); + } + + // Parent slot is equal to 0, so no notarization certificate required + let my_leader_slot = 10; + let parent_slot = 0; + let first_alpenglow_slot = 0; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_first_alpenglow_slot_edge_case_6() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // Valid skip certificate for 1-9 exists + for slot in 1..=9 { + add_certificate(&mut pool, &validator_keypairs, Vote::new_skip_vote(slot)); + } + // Parent slot is less than first_alpenglow_slot, so no notarization certificate required + let my_leader_slot = 10; + let parent_slot = 4; + let first_alpenglow_slot = 5; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_leader_does_not_start_if_skip_certificate_missing() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + let bank_forks = create_bank_forks(&validator_keypairs); + let my_pubkey = validator_keypairs[0].vote_keypair.pubkey(); + + // Create bank 5 + let bank = create_bank(5, bank_forks.read().unwrap().get(0).unwrap(), &my_pubkey); + bank.freeze(); + bank_forks.write().unwrap().insert(bank); + + // Notarize slot 5 + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_notarization_vote(5, Hash::default()), + ); + assert_eq!(pool.highest_notarized_slot(), 5); + + // No skip certificate for 6-10 + let my_leader_slot = 10; + let parent_slot = 5; + let first_alpenglow_slot = 0; + let decision = + pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot); + assert!( + !decision, + "Leader should not be allowed to start if a skip certificate is missing" + ); + } + + #[test] + fn test_make_decision_leader_starts_when_no_skip_required() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // Notarize slot 5 + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_notarization_vote(5, Hash::default()), + ); + assert_eq!(pool.highest_notarized_slot(), 5); + + // Leader slot is just +1 from notarized slot (no skip needed) + let my_leader_slot = 6; + let parent_slot = 5; + let first_alpenglow_slot = 0; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_leader_starts_if_notarized_and_skips_valid() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // Notarize slot 5 + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_notarization_vote(5, Hash::default()), + ); + assert_eq!(pool.highest_notarized_slot(), 5); + + // Valid skip certificate for 6-9 exists + for slot in 6..=9 { + add_certificate(&mut pool, &validator_keypairs, Vote::new_skip_vote(slot)); + } + + let my_leader_slot = 10; + let parent_slot = 5; + let first_alpenglow_slot = 0; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test] + fn test_make_decision_leader_starts_if_skip_range_superset() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // Notarize slot 5 + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_notarization_vote(5, Hash::default()), + ); + assert_eq!(pool.highest_notarized_slot(), 5); + + // Valid skip certificate for 4-9 exists + // Should start leader block even if the beginning of the range is from + // before your last notarized slot + for slot in 4..=9 { + add_certificate( + &mut pool, + &validator_keypairs, + Vote::new_skip_fallback_vote(slot), + ); + } + + let my_leader_slot = 10; + let parent_slot = 5; + let first_alpenglow_slot = 0; + assert!(pool.make_start_leader_decision(my_leader_slot, parent_slot, first_alpenglow_slot)); + } + + #[test_case(Vote::new_finalization_vote(5), vec![CertificateType::Finalize])] + #[test_case(Vote::new_notarization_vote(6, Hash::new_unique()), vec![CertificateType::Notarize, CertificateType::NotarizeFallback])] + #[test_case(Vote::new_notarization_fallback_vote(7, Hash::new_unique()), vec![CertificateType::NotarizeFallback])] + #[test_case(Vote::new_skip_vote(8), vec![CertificateType::Skip])] + #[test_case(Vote::new_skip_fallback_vote(9), vec![CertificateType::Skip])] + fn test_add_vote_and_create_new_certificate_with_types( + vote: Vote, + expected_certificate_types: Vec, + ) { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + let my_validator_ix = 5; + let highest_slot_fn = match &vote { + Vote::Finalize(_) => |pool: &CertificatePool| pool.highest_finalized_slot(), + Vote::Notarize(_) => |pool: &CertificatePool| pool.highest_notarized_slot(), + Vote::NotarizeFallback(_) => |pool: &CertificatePool| pool.highest_notarized_slot(), + Vote::Skip(_) => |pool: &CertificatePool| pool.highest_skip_slot(), + Vote::SkipFallback(_) => |pool: &CertificatePool| pool.highest_skip_slot(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, my_validator_ix), + &mut vec![] + ) + .is_ok()); + let slot = vote.slot(); + assert!(highest_slot_fn(&pool) < slot); + // Same key voting again shouldn't make a certificate + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, my_validator_ix), + &mut vec![] + ) + .is_ok()); + assert!(highest_slot_fn(&pool) < slot); + for rank in 0..4 { + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert!(highest_slot_fn(&pool) < slot); + let new_validator_ix = 6; + let (new_finalized_slot, certs_to_send) = pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, new_validator_ix), + &mut vec![], + ) + .unwrap(); + if vote.is_finalize() { + assert_eq!(new_finalized_slot, Some(slot)); + } else { + assert!(new_finalized_slot.is_none()); + } + // Assert certs_to_send contains the expected certificate types + for cert_type in expected_certificate_types { + assert!(certs_to_send.iter().any(|cert| { + cert.certificate.certificate_type() == cert_type && cert.certificate.slot() == slot + })); + } + assert_eq!(highest_slot_fn(&pool), slot); + // Now add the same certificate again, this should silently exit. + for cert in certs_to_send { + let (new_finalized_slot, certs_to_send) = pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate((*cert).clone()), + &mut vec![], + ) + .unwrap(); + assert!(new_finalized_slot.is_none()); + assert_eq!(certs_to_send, []); + } + } + + #[test_case(CertificateType::Finalize, Vote::new_finalization_vote(5))] + #[test_case( + CertificateType::FinalizeFast, + Vote::new_notarization_vote(6, Hash::new_unique()) + )] + #[test_case( + CertificateType::Notarize, + Vote::new_notarization_vote(6, Hash::new_unique()) + )] + #[test_case( + CertificateType::NotarizeFallback, + Vote::new_notarization_fallback_vote(7, Hash::new_unique()) + )] + #[test_case(CertificateType::Skip, Vote::new_skip_vote(8))] + fn test_add_certificate_with_types(certificate_type: CertificateType, vote: Vote) { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + let certificate = Certificate::new(certificate_type, vote.slot(), vote.block_id().copied()); + + let certificate_message = CertificateMessage { + certificate, + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + let bls_message = BLSMessage::Certificate(certificate_message.clone()); + // Add the certificate to the pool + let (new_finalized_slot, certs_to_send) = pool + .add_message(&Pubkey::new_unique(), &bls_message, &mut vec![]) + .unwrap(); + // Because this is the first certificate of this type, it should be sent out. + if certificate_type == CertificateType::Finalize + || certificate_type == CertificateType::FinalizeFast + { + assert_eq!(new_finalized_slot, Some(certificate.slot())); + } else { + assert!(new_finalized_slot.is_none()); + } + assert_eq!(certs_to_send.len(), 1); + assert_eq!(*certs_to_send[0], certificate_message); + + // Adding the cert again will not trigger another send + let (new_finalized_slot, certs_to_send) = pool + .add_message(&Pubkey::new_unique(), &bls_message, &mut vec![]) + .unwrap(); + assert!(new_finalized_slot.is_none()); + assert_eq!(certs_to_send, []); + + // Now add the vote from everyone else, this will not trigger a certificate send + for rank in 0..validator_keypairs.len() { + let (_, certs_to_send) = pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![], + ) + .unwrap(); + assert!(!certs_to_send + .iter() + .any(|cert| { cert.certificate.certificate_type() == certificate_type })); + } + } + + #[test] + fn test_add_vote_zero_stake() { + let (_, mut pool) = create_keypairs_and_pool(); + assert_eq!( + pool.add_message( + &Pubkey::new_unique(), + &BLSMessage::Vote(VoteMessage { + vote: Vote::new_skip_vote(5), + rank: 100, + signature: BLSSignature::default(), + }), + &mut vec![] + ), + Err(AddVoteError::InvalidRank(100)) + ); + } + + fn assert_single_certificate_range( + pool: &CertificatePool, + exp_range_start: Slot, + exp_range_end: Slot, + ) { + for i in exp_range_start..=exp_range_end { + assert!(pool.skip_certified(i)); + } + } + + #[test] + fn test_consecutive_slots() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + add_certificate(&mut pool, &validator_keypairs, Vote::new_skip_vote(15)); + assert_eq!(pool.highest_skip_slot(), 15); + + for i in 0..validator_keypairs.len() { + let slot = (i as u64).saturating_add(16); + let vote = Vote::new_skip_vote(slot); + // These should not extend the skip range + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, i), + &mut vec![] + ) + .is_ok()); + } + + assert_single_certificate_range(&pool, 15, 15); + } + + #[test] + fn test_multi_skip_cert() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // We have 10 validators, 40% voted for (5, 15) + for rank in 0..4 { + add_skip_vote_range(&mut pool, 5, 15, &validator_keypairs, rank); + } + // 30% voted for (5, 8) + for rank in 4..7 { + add_skip_vote_range(&mut pool, 5, 8, &validator_keypairs, rank); + } + // The rest voted for (11, 15) + for rank in 7..10 { + add_skip_vote_range(&mut pool, 11, 15, &validator_keypairs, rank); + } + // Test slots from 5 to 15, [5, 8] and [11, 15] should be certified, the others aren't + for slot in 5..9 { + assert!(pool.skip_certified(slot)); + } + for slot in 9..11 { + assert!(!pool.skip_certified(slot)); + } + for slot in 11..=15 { + assert!(pool.skip_certified(slot)); + } + } + + #[test] + fn test_add_multiple_votes() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + + // 10 validators, half vote for (5, 15), the other (20, 30) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 5, 15, &validator_keypairs, rank); + } + for rank in 5..10 { + add_skip_vote_range(&mut pool, 20, 30, &validator_keypairs, rank); + } + assert_eq!(pool.highest_skip_slot(), 0); + + // Now the first half vote for (5, 30) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 5, 30, &validator_keypairs, rank); + } + assert_single_certificate_range(&pool, 20, 30); + } + + #[test] + fn test_add_multiple_disjoint_votes() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + // 50% of the validators vote for (1, 10) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 1, 10, &validator_keypairs, rank); + } + // 10% vote for skip 2 + let vote = Vote::new_skip_vote(2); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 6), + &mut vec![] + ) + .is_ok()); + assert_eq!(pool.highest_skip_slot(), 2); + + assert_single_certificate_range(&pool, 2, 2); + // 10% vote for skip 4 + let vote = Vote::new_skip_vote(4); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 7), + &mut vec![] + ) + .is_ok()); + assert_eq!(pool.highest_skip_slot(), 4); + + assert_single_certificate_range(&pool, 2, 2); + assert_single_certificate_range(&pool, 4, 4); + // 10% vote for skip 3 + let vote = Vote::new_skip_vote(3); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 8), + &mut vec![] + ) + .is_ok()); + assert_eq!(pool.highest_skip_slot(), 4); + assert_single_certificate_range(&pool, 2, 4); + assert!(pool.skip_certified(3)); + // Let the last 10% vote for (3, 10) now + add_skip_vote_range(&mut pool, 3, 10, &validator_keypairs, 8); + assert_eq!(pool.highest_skip_slot(), 10); + assert_single_certificate_range(&pool, 2, 10); + assert!(pool.skip_certified(7)); + } + + #[test] + fn test_update_existing_singleton_vote() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + // 50% voted on (1, 6) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 1, 6, &validator_keypairs, rank); + } + // Range expansion on a singleton vote should be ok + let vote = Vote::new_skip_vote(1); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 6), + &mut vec![] + ) + .is_ok()); + assert_eq!(pool.highest_skip_slot(), 1); + add_skip_vote_range(&mut pool, 1, 6, &validator_keypairs, 6); + assert_eq!(pool.highest_skip_slot(), 6); + assert_single_certificate_range(&pool, 1, 6); + } + + #[test] + fn test_update_existing_vote() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + // 50% voted for (10, 25) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 10, 25, &validator_keypairs, rank); + } + + add_skip_vote_range(&mut pool, 10, 20, &validator_keypairs, 6); + assert_eq!(pool.highest_skip_slot(), 20); + assert_single_certificate_range(&pool, 10, 20); + + // AlreadyExists, silently fail + let vote = Vote::new_skip_vote(20); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 6), + &mut vec![] + ) + .is_ok()); + } + + #[test] + fn test_threshold_not_reached() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + // half voted (5, 15) and the other half voted (20, 30) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 5, 15, &validator_keypairs, rank); + } + for rank in 5..10 { + add_skip_vote_range(&mut pool, 20, 30, &validator_keypairs, rank); + } + for slot in 5..31 { + assert!(!pool.skip_certified(slot)); + } + } + + #[test] + fn test_update_and_skip_range_certify() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + // half voted (5, 15) and the other half voted (10, 30) + for rank in 0..5 { + add_skip_vote_range(&mut pool, 5, 15, &validator_keypairs, rank); + } + for rank in 5..10 { + add_skip_vote_range(&mut pool, 10, 30, &validator_keypairs, rank); + } + for slot in 5..10 { + assert!(!pool.skip_certified(slot)); + } + for slot in 16..31 { + assert!(!pool.skip_certified(slot)); + } + assert_single_certificate_range(&pool, 10, 15); + } + + #[test] + fn test_safe_to_notar() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + let (my_vote_key, _, _) = pool.get_key_and_stakes(0, 0).unwrap(); + + // Create bank 2 + let slot = 2; + let block_id = Hash::new_unique(); + + // With no votes, this should fail. + assert!(pool.safe_to_notar(&my_vote_key, slot).is_empty()); + + // Add a skip from myself. + let vote = Vote::new_skip_vote(2); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 0), + &mut vec![] + ) + .is_ok()); + // 40% notarized, should succeed + for rank in 1..5 { + let vote = Vote::new_notarization_vote(2, block_id); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert_eq!(pool.safe_to_notar(&my_vote_key, slot), vec![block_id]); + + // Create bank 3 + let slot = 3; + let block_id = Hash::new_unique(); + + // Add 20% notarize, but no vote from myself, should fail + for rank in 1..3 { + let vote = Vote::new_notarization_vote(3, block_id); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert!(pool.safe_to_notar(&my_vote_key, slot).is_empty()); + + // Add a notarize from myself for some other block, but still not enough notar or skip, should fail. + let vote = Vote::new_notarization_vote(3, Hash::new_unique()); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 0), + &mut vec![] + ) + .is_ok()); + assert!(pool.safe_to_notar(&my_vote_key, slot).is_empty()); + + // Now add 40% skip, should succeed + for rank in 3..7 { + let vote = Vote::new_skip_vote(3); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert_eq!(pool.safe_to_notar(&my_vote_key, slot), vec![block_id]); + + // Add 20% notarization for another block, we should notify on both + let duplicate_block_id = Hash::new_unique(); + for rank in 7..9 { + let vote = Vote::new_notarization_vote(3, duplicate_block_id); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + + assert_eq!( + pool.safe_to_notar(&my_vote_key, slot) + .into_iter() + .sorted() + .collect::>(), + vec![block_id, duplicate_block_id,] + .into_iter() + .sorted() + .collect::>() + ); + } + + #[test] + fn test_safe_to_skip() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + let (my_vote_key, _, _) = pool.get_key_and_stakes(0, 0).unwrap(); + let slot = 2; + // No vote from myself, should fail. + assert!(!pool.safe_to_skip(&my_vote_key, slot)); + + // Add a notarize from myself. + let block_id = Hash::new_unique(); + let vote = Vote::new_notarization_vote(2, block_id); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 0), + &mut vec![] + ) + .is_ok()); + // Should still fail because there are no other votes. + assert!(!pool.safe_to_skip(&my_vote_key, slot)); + // Add 50% skip, should succeed + for rank in 1..6 { + let vote = Vote::new_skip_vote(2); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, rank), + &mut vec![] + ) + .is_ok()); + } + assert!(pool.safe_to_skip(&my_vote_key, slot)); + // Add 10% more notarize, still safe to skip any more because total voted increased. + let vote = Vote::new_notarization_vote(2, block_id); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 6), + &mut vec![] + ) + .is_ok()); + assert!(pool.safe_to_skip(&my_vote_key, slot)); + } + + fn create_new_vote(vote_type: VoteType, slot: Slot) -> Vote { + match vote_type { + VoteType::Notarize => Vote::new_notarization_vote(slot, Hash::default()), + VoteType::NotarizeFallback => { + Vote::new_notarization_fallback_vote(slot, Hash::default()) + } + VoteType::Skip => Vote::new_skip_vote(slot), + VoteType::SkipFallback => Vote::new_skip_fallback_vote(slot), + VoteType::Finalize => Vote::new_finalization_vote(slot), + } + } + + fn test_reject_conflicting_vote( + pool: &mut CertificatePool, + validator_keypairs: &[ValidatorVoteKeypairs], + vote_type_1: VoteType, + vote_type_2: VoteType, + slot: Slot, + ) { + let vote_1 = create_new_vote(vote_type_1, slot); + let vote_2 = create_new_vote(vote_type_2, slot); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(validator_keypairs, &vote_1, 0), + &mut vec![] + ) + .is_ok()); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(validator_keypairs, &vote_2, 0), + &mut vec![] + ) + .is_err()); + } + + #[test] + fn test_reject_conflicting_votes_with_type() { + let (validator_keypairs, mut pool) = create_keypairs_and_pool(); + let mut slot = 2; + for vote_type_1 in [ + VoteType::Finalize, + VoteType::Notarize, + VoteType::NotarizeFallback, + VoteType::Skip, + VoteType::SkipFallback, + ] { + let conflicting_vote_types = conflicting_types(vote_type_1); + for vote_type_2 in conflicting_vote_types { + test_reject_conflicting_vote( + &mut pool, + &validator_keypairs, + vote_type_1, + *vote_type_2, + slot, + ); + } + slot = slot.saturating_add(4); + } + } + + #[test] + fn test_handle_new_root() { + let validator_keypairs = (0..10) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let bank_forks = create_bank_forks(&validator_keypairs); + let root_bank = bank_forks.read().unwrap().root_bank(); + let mut pool: CertificatePool = + CertificatePool::new_from_root_bank(Pubkey::new_unique(), &root_bank.clone(), None); + assert_eq!(pool.root(), 0); + + let new_bank = Arc::new(create_bank(2, root_bank, &Pubkey::new_unique())); + pool.handle_new_root(new_bank.clone()); + assert_eq!(pool.root(), 2); + let new_bank = Arc::new(create_bank(3, new_bank, &Pubkey::new_unique())); + pool.handle_new_root(new_bank); + assert_eq!(pool.root(), 3); + // Send a vote on slot 1, it should be rejected + let vote = Vote::new_skip_vote(1); + assert!(pool + .add_message( + &Pubkey::new_unique(), + &dummy_transaction(&validator_keypairs, &vote, 0), + &mut vec![] + ) + .is_err()); + + // Send a cert on slot 2, it should be rejected + let certificate = Certificate::new(CertificateType::Notarize, 2, Some(Hash::new_unique())); + + let cert = BLSMessage::Certificate(CertificateMessage { + certificate, + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }); + assert!(pool + .add_message(&Pubkey::new_unique(), &cert, &mut vec![]) + .is_err()); + } + + #[test] + fn test_get_certs_for_standstill() { + let (_, mut pool) = create_keypairs_and_pool(); + + // Should return empty vector if no certificates + assert!(pool.get_certs_for_standstill().is_empty()); + + // Add notar-fallback cert on 3 and finalize cert on 4 + let cert_3 = CertificateMessage { + certificate: Certificate::new( + CertificateType::NotarizeFallback, + 3, + Some(Hash::new_unique()), + ), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_3.clone()), + &mut vec![] + ) + .is_ok()); + let cert_4 = CertificateMessage { + certificate: Certificate::new(CertificateType::Finalize, 4, None), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_4.clone()), + &mut vec![] + ) + .is_ok()); + // Should return both certificates + let certs = pool.get_certs_for_standstill(); + assert_eq!(certs.len(), 2); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 3 + && cert.certificate.certificate_type() == CertificateType::NotarizeFallback)); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 4 + && cert.certificate.certificate_type() == CertificateType::Finalize)); + + // Add FinalizeFast cert on 5 + let cert_5 = CertificateMessage { + certificate: Certificate::new( + CertificateType::FinalizeFast, + 5, + Some(Hash::new_unique()), + ), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_5.clone()), + &mut vec![] + ) + .is_ok()); + // Should return only cert on 5 + let certs = pool.get_certs_for_standstill(); + assert_eq!(certs.len(), 1); + assert!( + certs[0].certificate.slot() == 5 + && certs[0].certificate.certificate_type() == CertificateType::FinalizeFast + ); + + // Now add Notarize cert on 6 + let cert_6 = CertificateMessage { + certificate: Certificate::new(CertificateType::Notarize, 6, Some(Hash::new_unique())), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_6.clone()), + &mut vec![] + ) + .is_ok()); + // Should return certs on 5 and 6 + let certs = pool.get_certs_for_standstill(); + assert_eq!(certs.len(), 2); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 5 + && cert.certificate.certificate_type() == CertificateType::FinalizeFast)); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 6 + && cert.certificate.certificate_type() == CertificateType::Notarize)); + + // Add another Finalize cert on 6 + let cert_6_finalize = CertificateMessage { + certificate: Certificate::new(CertificateType::Finalize, 6, None), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_6_finalize.clone()), + &mut vec![] + ) + .is_ok()); + // Should only return both certs on 6 + let certs = pool.get_certs_for_standstill(); + assert_eq!(certs.len(), 2); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 6 + && cert.certificate.certificate_type() == CertificateType::Finalize)); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 6 + && cert.certificate.certificate_type() == CertificateType::Notarize)); + + // Add another skip on 7 + let cert_7 = CertificateMessage { + certificate: Certificate::new(CertificateType::Skip, 7, None), + signature: BLSSignature::default(), + bitmap: BitVec::new(), + }; + assert!(pool + .add_message( + &Pubkey::new_unique(), + &BLSMessage::Certificate(cert_7.clone()), + &mut vec![] + ) + .is_ok()); + // Should return certs on 6 and 7 + let certs = pool.get_certs_for_standstill(); + assert_eq!(certs.len(), 3); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 6 + && cert.certificate.certificate_type() == CertificateType::Finalize)); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 6 + && cert.certificate.certificate_type() == CertificateType::Notarize)); + assert!(certs.iter().any(|cert| cert.certificate.slot() == 7 + && cert.certificate.certificate_type() == CertificateType::Skip)); + } +} diff --git a/votor/src/certificate_pool/parent_ready_tracker.rs b/votor/src/certificate_pool/parent_ready_tracker.rs new file mode 100644 index 0000000000..9ab4ee468d --- /dev/null +++ b/votor/src/certificate_pool/parent_ready_tracker.rs @@ -0,0 +1,383 @@ +//! Tracks the parent-ready condition +//! +//! The parent-ready condition pertains to a slot `s` and a block hash `hash(b)`, +//! where `s` is the first slot of a leader window and `s > slot(b)`. +//! Specifically, it is defined as the following: +//! - Block `b` is notarized or notarized-fallback, and +//! - slots `slot(b) + 1` (inclusive) to `s` (non-inclusive) are skip-certified. +//! +//! Additional restriction on notarization votes ensure that the parent-ready +//! condition holds for a block `b` only if it also holds for all ancestors of `b`. +//! Together this ensures that the block `b` is a valid parent for block +//! production, i.e., under good network conditions an honest leader proposing +//! a block with parent `b` in slot `s` will have their block finalized. + +use { + crate::{event::VotorEvent, MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE}, + solana_pubkey::Pubkey, + solana_sdk::clock::{Slot, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_votor_messages::bls_message::Block, + std::collections::HashMap, +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum BlockProductionParent { + MissedWindow, + ParentNotReady, + Parent(Block), +} + +#[derive(Clone, Debug, Default)] +pub struct ParentReadyTracker { + /// Our pubkey for logging + my_pubkey: Pubkey, + + /// Parent ready status for each slot + slot_statuses: HashMap, + + /// Root + root: Slot, + + /// Highest slot with parent ready status + // TODO: While the voting loop is sequential we track every slot (not just the first in window) + // However once we handle all slots concurrently we will update this to only count first leader + // slot in window + highest_with_parent_ready: Slot, +} + +#[derive(Clone, Default, Debug)] +struct ParentReadyStatus { + /// Whether this slot has a skip certificate + skip: bool, + /// The blocks that have been notar fallbacked in this slot + notar_fallbacks: Vec, + /// The parent blocks that achieve parent ready in this slot, + /// Theses blocks are all potential parents choosable in this slot + parents_ready: Vec, +} + +impl ParentReadyTracker { + /// Creates a new tracker with the root bank as implicitely notarized fallback + pub fn new(my_pubkey: Pubkey, root_block @ (root_slot, _): Block) -> Self { + let mut slot_statuses = HashMap::new(); + slot_statuses.insert( + root_slot, + ParentReadyStatus { + skip: false, + notar_fallbacks: vec![root_block], + parents_ready: vec![], + }, + ); + slot_statuses.insert( + root_slot.saturating_add(1), + ParentReadyStatus { + skip: false, + notar_fallbacks: vec![], + parents_ready: vec![root_block], + }, + ); + Self { + my_pubkey, + slot_statuses, + root: root_slot, + highest_with_parent_ready: root_slot.saturating_add(1), + } + } + + /// Adds a new notar-fallback certificate + pub fn add_new_notar_fallback( + &mut self, + block @ (slot, _): Block, + events: &mut Vec, + ) { + if slot <= self.root { + return; + } + + let status = self.slot_statuses.entry(slot).or_default(); + if status.notar_fallbacks.contains(&block) { + return; + } + trace!( + "{}: Adding new notar fallback for {block:?}", + self.my_pubkey + ); + status.notar_fallbacks.push(block); + assert!(status.notar_fallbacks.len() <= MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE); + + // Add this block as valid parent to skip connected future blocks + for s in slot.saturating_add(1).. { + trace!( + "{}: Adding new parent ready for {s} parent {block:?}", + self.my_pubkey + ); + let status = self.slot_statuses.entry(s).or_default(); + if !status.parents_ready.contains(&block) { + status.parents_ready.push(block); + + // Only notify for parent ready on first leader slots + if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + events.push(VotorEvent::ParentReady { + slot: s, + parent_block: block, + }); + } + + self.highest_with_parent_ready = s.max(self.highest_with_parent_ready); + } + + if !status.skip { + break; + } + } + } + + /// Adds a new skip certificate + pub fn add_new_skip(&mut self, slot: Slot, events: &mut Vec) { + if slot <= self.root { + return; + } + + trace!("{}: Adding new skip for {slot:?}", self.my_pubkey); + let status = self.slot_statuses.entry(slot).or_default(); + status.skip = true; + + // Get newly connected future slots + let mut future_slots = vec![]; + for s in slot.saturating_add(1).. { + future_slots.push(s); + if !self.slot_statuses.get(&s).is_some_and(|ss| ss.skip) { + break; + } + } + + // Find possible parents using the previous slot + let mut potential_parents = vec![]; + let Some(status) = self.slot_statuses.get(&(slot.saturating_sub(1))) else { + return; + }; + for nf in &status.notar_fallbacks { + // If there's a notarize fallback certificate we can use the previous slot + // as a parent + potential_parents.push(*nf); + } + if status.skip { + // If there's a skip certificate we can use the parents of the previous slot + // as a parent + for parent in &status.parents_ready { + potential_parents.push(*parent); + } + } + + if potential_parents.is_empty() { + return; + } + + // Add these as valid parents to the future slots + for s in future_slots { + trace!( + "{}: Adding new parent ready for {s} parents {potential_parents:?}", + self.my_pubkey, + ); + let status = self.slot_statuses.entry(s).or_default(); + for &block in &potential_parents { + if status.parents_ready.contains(&block) { + // We already have this parent ready + continue; + } + status.parents_ready.push(block); + // Only notify for parent ready on first leader slots + if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + events.push(VotorEvent::ParentReady { + slot: s, + parent_block: block, + }); + } + } + + self.highest_with_parent_ready = s.max(self.highest_with_parent_ready); + } + } + + pub fn parent_ready(&self, slot: Slot, parent: Block) -> bool { + self.slot_statuses + .get(&slot) + .is_some_and(|ss| ss.parents_ready.contains(&parent)) + } + + /// For our leader slot `slot`, which block should we use as the parent + pub fn block_production_parent(&self, slot: Slot) -> BlockProductionParent { + if self.highest_parent_ready() > slot { + // This indicates that our block has already received a certificate + // either because we were too slow, or because we are restarting + // and catching up. Either way we should not attempt to produce this slot + return BlockProductionParent::MissedWindow; + } + // TODO: for duplicate blocks we should adjust this to choose the + // parent with the least amount of duplicate blocks if possible. + // Notice that each scenario with multiple NotarFallbacks also will eventually + // have a skip for that slot, so prefer the skip if we've received it. + match self + .slot_statuses + .get(&slot) + .and_then(|ss| ss.parents_ready.iter().max().copied()) + { + Some(parent) => BlockProductionParent::Parent(parent), + // TODO: this will be plugged in for optimistic block production + None => BlockProductionParent::ParentNotReady, + } + } + + pub fn highest_parent_ready(&self) -> Slot { + self.highest_with_parent_ready + } + + pub fn set_root(&mut self, root: Slot) { + self.root = root; + self.slot_statuses.retain(|&s, _| s >= root); + } + + /// Updates the pubkey. Note that the pubkey is used for logging purposes only. + pub fn update_pubkey(&mut self, new_pubkey: Pubkey) { + self.my_pubkey = new_pubkey; + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_pubkey::Pubkey, + solana_sdk::{clock::NUM_CONSECUTIVE_LEADER_SLOTS, hash::Hash}, + }; + + #[test] + fn basic() { + let genesis = Block::default(); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), genesis); + let mut events = vec![]; + + for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS { + let block = (i, Hash::new_unique()); + tracker.add_new_notar_fallback(block, &mut events); + assert_eq!(tracker.highest_parent_ready(), i + 1); + assert!(tracker.parent_ready(i + 1, block)); + } + } + + #[test] + fn skips() { + let genesis = Block::default(); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), genesis); + let mut events = vec![]; + let block = (1, Hash::new_unique()); + + tracker.add_new_notar_fallback(block, &mut events); + tracker.add_new_skip(1, &mut events); + tracker.add_new_skip(2, &mut events); + tracker.add_new_skip(3, &mut events); + + assert!(tracker.parent_ready(4, block)); + assert!(tracker.parent_ready(4, genesis)); + assert_eq!(tracker.highest_parent_ready(), 4); + } + + #[test] + fn out_of_order() { + let genesis = Block::default(); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), genesis); + let mut events = vec![]; + let block = (1, Hash::new_unique()); + + tracker.add_new_skip(3, &mut events); + tracker.add_new_skip(2, &mut events); + + tracker.add_new_notar_fallback(block, &mut events); + assert!(tracker.parent_ready(4, block)); + assert!(!tracker.parent_ready(4, genesis)); + + tracker.add_new_skip(1, &mut events); + assert!(tracker.parent_ready(4, block)); + assert!(tracker.parent_ready(4, genesis)); + } + + #[test] + fn snapshot_wfsm() { + let root_slot = 2147; + let root_block = (root_slot, Hash::new_unique()); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), root_block); + let mut events = vec![]; + + assert!(tracker.parent_ready(root_slot + 1, root_block)); + assert_eq!(tracker.highest_parent_ready(), root_slot + 1); + + // Skipping root slot shouldn't do anything + tracker.add_new_skip(root_slot, &mut events); + assert!(tracker.parent_ready(root_slot + 1, root_block)); + assert_eq!(tracker.highest_parent_ready(), root_slot + 1); + + // Adding new certs should work as root slot is implicitely notarized fallback + tracker.add_new_skip(root_slot + 1, &mut events); + tracker.add_new_skip(root_slot + 2, &mut events); + assert!(tracker.parent_ready(root_slot + 3, root_block)); + assert_eq!(tracker.highest_parent_ready(), root_slot + 3); + + let block = (root_slot + 4, Hash::new_unique()); + tracker.add_new_notar_fallback(block, &mut events); + assert!(tracker.parent_ready(root_slot + 3, root_block)); + assert!(tracker.parent_ready(root_slot + 5, block)); + assert_eq!(tracker.highest_parent_ready(), root_slot + 5); + } + + #[test] + fn highest_parent_ready_out_of_order() { + let genesis = Block::default(); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), genesis); + let mut events = vec![]; + assert_eq!(tracker.highest_parent_ready(), 1); + + tracker.add_new_skip(2, &mut events); + assert_eq!(tracker.highest_parent_ready(), 1); + + tracker.add_new_skip(3, &mut events); + assert_eq!(tracker.highest_parent_ready(), 1); + + tracker.add_new_skip(1, &mut events); + assert!(tracker.parent_ready(4, genesis)); + assert_eq!(tracker.highest_parent_ready(), 4); + assert_eq!( + tracker.block_production_parent(4), + BlockProductionParent::Parent(genesis) + ); + } + + #[test] + fn missed_window() { + let genesis = Block::default(); + let mut tracker = ParentReadyTracker::new(Pubkey::default(), genesis); + let mut events = vec![]; + assert_eq!(tracker.highest_parent_ready(), 1); + assert_eq!( + tracker.block_production_parent(4), + BlockProductionParent::ParentNotReady + ); + + tracker.add_new_notar_fallback((4, Hash::new_unique()), &mut events); + assert_eq!(tracker.highest_parent_ready(), 5); + assert_eq!( + tracker.block_production_parent(4), + BlockProductionParent::MissedWindow + ); + + assert_eq!( + tracker.block_production_parent(8), + BlockProductionParent::ParentNotReady + ); + tracker.add_new_notar_fallback((64, Hash::new_unique()), &mut events); + assert_eq!(tracker.highest_parent_ready(), 65); + assert_eq!( + tracker.block_production_parent(8), + BlockProductionParent::MissedWindow + ); + } +} diff --git a/votor/src/certificate_pool/stats.rs b/votor/src/certificate_pool/stats.rs new file mode 100644 index 0000000000..3079bb2173 --- /dev/null +++ b/votor/src/certificate_pool/stats.rs @@ -0,0 +1,229 @@ +use { + crate::VoteType, + solana_metrics::datapoint_info, + solana_votor_messages::bls_message::CertificateType, + std::time::{Duration, Instant}, +}; + +const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(10); + +#[derive(Debug)] +pub(crate) struct CertificatePoolStats { + pub(crate) conflicting_votes: u32, + pub(crate) event_safe_to_notarize: u32, + pub(crate) event_safe_to_skip: u32, + pub(crate) exist_certs: u32, + pub(crate) exist_votes: u32, + pub(crate) incoming_certs: u32, + pub(crate) incoming_votes: u32, + pub(crate) out_of_range_certs: u32, + pub(crate) out_of_range_votes: u32, + + pub(crate) new_certs_generated: Vec, + pub(crate) new_certs_ingested: Vec, + pub(crate) ingested_votes: Vec, + + pub(crate) last_request_time: Instant, +} + +impl Default for CertificatePoolStats { + fn default() -> Self { + Self::new() + } +} + +impl CertificatePoolStats { + pub fn new() -> Self { + let num_vote_types = (VoteType::SkipFallback as usize).saturating_add(1); + let num_cert_types = (CertificateType::Skip as usize).saturating_add(1); + Self { + conflicting_votes: 0, + event_safe_to_notarize: 0, + event_safe_to_skip: 0, + exist_certs: 0, + exist_votes: 0, + incoming_certs: 0, + incoming_votes: 0, + out_of_range_certs: 0, + out_of_range_votes: 0, + + new_certs_ingested: vec![0; num_cert_types], + new_certs_generated: vec![0; num_cert_types], + ingested_votes: vec![0; num_vote_types], + + last_request_time: Instant::now(), + } + } + + pub fn incr_ingested_vote_type(&mut self, vote_type: VoteType) { + let index = vote_type as usize; + + self.ingested_votes[index] = self.ingested_votes[index].saturating_add(1); + } + + pub fn incr_cert_type(&mut self, cert_type: CertificateType, is_generated: bool) { + let index = cert_type as usize; + let array = if is_generated { + &mut self.new_certs_generated + } else { + &mut self.new_certs_ingested + }; + + array[index] = array[index].saturating_add(1); + } + + fn report(&self) { + datapoint_info!( + "certificate_pool_stats", + ("conflicting_votes", self.conflicting_votes as i64, i64), + ("event_safe_to_skip", self.event_safe_to_skip as i64, i64), + ( + "event_safe_to_notarize", + self.event_safe_to_notarize as i64, + i64 + ), + ("exist_votes", self.exist_votes as i64, i64), + ("exist_certs", self.exist_certs as i64, i64), + ("incoming_votes", self.incoming_votes as i64, i64), + ("incoming_certs", self.incoming_certs as i64, i64), + ("out_of_range_votes", self.out_of_range_votes as i64, i64), + ("out_of_range_certs", self.out_of_range_certs as i64, i64), + ); + + datapoint_info!( + "certificate_pool_ingested_votes", + ( + "finalize", + *self + .ingested_votes + .get(VoteType::Finalize as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize", + *self + .ingested_votes + .get(VoteType::Notarize as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize_fallback", + *self + .ingested_votes + .get(VoteType::NotarizeFallback as usize) + .unwrap() as i64, + i64 + ), + ( + "skip", + *self.ingested_votes.get(VoteType::Skip as usize).unwrap() as i64, + i64 + ), + ( + "skip_fallback", + *self + .ingested_votes + .get(VoteType::SkipFallback as usize) + .unwrap() as i64, + i64 + ), + ); + + datapoint_info!( + "certfificate_pool_ingested_certs", + ( + "finalize", + *self + .new_certs_ingested + .get(CertificateType::Finalize as usize) + .unwrap() as i64, + i64 + ), + ( + "finalize_fast", + *self + .new_certs_ingested + .get(CertificateType::FinalizeFast as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize", + *self + .new_certs_ingested + .get(CertificateType::Notarize as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize_fallback", + *self + .new_certs_ingested + .get(CertificateType::NotarizeFallback as usize) + .unwrap() as i64, + i64 + ), + ( + "skip", + *self + .new_certs_ingested + .get(CertificateType::Skip as usize) + .unwrap() as i64, + i64 + ), + ); + + datapoint_info!( + "certificate_pool_generated_certs", + ( + "finalize", + *self + .new_certs_generated + .get(CertificateType::Finalize as usize) + .unwrap() as i64, + i64 + ), + ( + "finalize_fast", + *self + .new_certs_generated + .get(CertificateType::FinalizeFast as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize", + *self + .new_certs_generated + .get(CertificateType::Notarize as usize) + .unwrap() as i64, + i64 + ), + ( + "notarize_fallback", + *self + .new_certs_generated + .get(CertificateType::NotarizeFallback as usize) + .unwrap() as i64, + i64 + ), + ( + "skip", + *self + .new_certs_generated + .get(CertificateType::Skip as usize) + .unwrap() as i64, + i64 + ), + ); + } + + pub fn maybe_report(&mut self) { + if self.last_request_time.elapsed() >= STATS_REPORT_INTERVAL { + self.report(); + *self = Self::new(); + } + } +} diff --git a/votor/src/certificate_pool/vote_certificate_builder.rs b/votor/src/certificate_pool/vote_certificate_builder.rs new file mode 100644 index 0000000000..d9cd99b3f0 --- /dev/null +++ b/votor/src/certificate_pool/vote_certificate_builder.rs @@ -0,0 +1,102 @@ +use { + crate::Certificate, + bitvec::prelude::*, + solana_bls_signatures::{BlsError, Pubkey as BlsPubkey, PubkeyProjective, SignatureProjective}, + solana_runtime::epoch_stakes::BLSPubkeyToRankMap, + solana_votor_messages::bls_message::{CertificateMessage, VoteMessage}, + thiserror::Error, +}; + +/// Maximum number of validators in a certificate +/// +/// There are around 1500 validators currently. For a clean power-of-two +/// implementation, we should chosoe either 2048 or 4096. Choose a more +/// conservative number 4096 for now. +const MAXIMUM_VALIDATORS: usize = 4096; + +#[derive(Debug, Error, PartialEq)] +pub enum CertificateError { + #[error("BLS error: {0}")] + BlsError(#[from] BlsError), + #[error("Invalid pubkey")] + InvalidPubkey, + #[error("Validator does not exist for given rank: {0}")] + ValidatorDoesNotExist(usize), +} + +/// A builder for creating a `CertificateMessage` by efficiently aggregating BLS signatures. +#[derive(Clone)] +pub struct VoteCertificateBuilder { + certificate: Certificate, + signature: SignatureProjective, + bitmap: BitVec, +} + +impl TryFrom for VoteCertificateBuilder { + type Error = CertificateError; + + fn try_from(message: CertificateMessage) -> Result { + let projective_signature = SignatureProjective::try_from(message.signature)?; + Ok(VoteCertificateBuilder { + certificate: message.certificate, + signature: projective_signature, + bitmap: message.bitmap, + }) + } +} + +impl VoteCertificateBuilder { + pub fn new(certificate_id: Certificate) -> Self { + Self { + certificate: certificate_id, + signature: SignatureProjective::identity(), + bitmap: BitVec::::repeat(false, MAXIMUM_VALIDATORS), + } + } + + /// Aggregates a slice of `VoteMessage`s into the builder. + pub fn aggregate(&mut self, messages: &[VoteMessage]) -> Result<(), CertificateError> { + for vote_message in messages { + if self.bitmap.len() <= vote_message.rank as usize { + return Err(CertificateError::ValidatorDoesNotExist( + vote_message.rank as usize, + )); + } + self.bitmap.set(vote_message.rank as usize, true); + } + let signature_iter = messages.iter().map(|vote_message| &vote_message.signature); + Ok(self.signature.aggregate_with(signature_iter)?) + } + + pub fn build(self) -> CertificateMessage { + CertificateMessage { + certificate: self.certificate, + signature: self.signature.into(), + bitmap: self.bitmap, + } + } +} + +/// Given a bit vector and a list of validator BLS pubkeys, generate an +/// aggregate BLS pubkey. +#[allow(dead_code)] +pub fn aggregate_pubkey( + bitmap: &BitVec, + bls_pubkey_to_rank_map: &BLSPubkeyToRankMap, +) -> Result { + let mut aggregate_pubkey = PubkeyProjective::identity(); + for (i, included) in bitmap.iter().enumerate() { + if *included { + let bls_pubkey: PubkeyProjective = bls_pubkey_to_rank_map + .get_pubkey(i) + .ok_or(CertificateError::ValidatorDoesNotExist(i))? + .1 + .try_into() + .map_err(|_| CertificateError::InvalidPubkey)?; + + aggregate_pubkey.aggregate_with([&bls_pubkey])?; + } + } + + Ok(aggregate_pubkey.into()) +} diff --git a/votor/src/certificate_pool/vote_pool.rs b/votor/src/certificate_pool/vote_pool.rs new file mode 100644 index 0000000000..cd8b86c2df --- /dev/null +++ b/votor/src/certificate_pool/vote_pool.rs @@ -0,0 +1,322 @@ +use { + crate::{certificate_pool::vote_certificate_builder::VoteCertificateBuilder, Stake}, + solana_pubkey::Pubkey, + solana_sdk::hash::Hash, + solana_votor_messages::bls_message::VoteMessage, + std::collections::{HashMap, HashSet}, +}; + +#[derive(Debug)] +pub(crate) struct VoteEntry { + pub(crate) transactions: Vec, + pub(crate) total_stake_by_key: Stake, +} + +impl VoteEntry { + pub fn new() -> Self { + Self { + transactions: Vec::new(), + total_stake_by_key: 0, + } + } +} + +pub(crate) trait VotePool { + fn total_stake(&self) -> Stake; + fn has_prev_validator_vote(&self, validator_vote_key: &Pubkey) -> bool; +} + +/// There are two types of vote pools: +/// - SimpleVotePool: Tracks all votes of a specfic vote type made by validators for some slot N, but only one vote per block. +/// - DuplicateBlockVotePool: Tracks all votes of a specfic vote type made by validators for some slot N, +/// but allows votes for different blocks by the same validator. Only relevant for VotePool's that are of type +/// Notarization or NotarizationFallback +pub(crate) enum VotePoolType { + SimpleVotePool(SimpleVotePool), + DuplicateBlockVotePool(DuplicateBlockVotePool), +} + +impl VotePoolType { + pub(crate) fn total_stake(&self) -> Stake { + match self { + VotePoolType::SimpleVotePool(pool) => pool.total_stake(), + VotePoolType::DuplicateBlockVotePool(pool) => pool.total_stake(), + } + } + + pub(crate) fn has_prev_validator_vote(&self, validator_vote_key: &Pubkey) -> bool { + match self { + VotePoolType::SimpleVotePool(pool) => pool.has_prev_validator_vote(validator_vote_key), + VotePoolType::DuplicateBlockVotePool(pool) => { + pool.has_prev_validator_vote(validator_vote_key) + } + } + } + + pub(crate) fn unwrap_duplicate_block_vote_pool( + &self, + error_message: &str, + ) -> &DuplicateBlockVotePool { + match self { + VotePoolType::SimpleVotePool(_pool) => panic!("{}", error_message), + VotePoolType::DuplicateBlockVotePool(pool) => pool, + } + } +} + +pub(crate) struct SimpleVotePool { + /// Tracks all votes of a specfic vote type made by validators for some slot N. + pub(crate) vote_entry: VoteEntry, + prev_voted_validators: HashSet, +} + +impl SimpleVotePool { + pub fn new() -> Self { + Self { + vote_entry: VoteEntry::new(), + prev_voted_validators: HashSet::new(), + } + } + + pub fn add_vote( + &mut self, + validator_vote_key: &Pubkey, + validator_stake: Stake, + transaction: &VoteMessage, + ) -> bool { + if self.prev_voted_validators.contains(validator_vote_key) { + return false; + } + self.prev_voted_validators.insert(*validator_vote_key); + self.vote_entry.transactions.push(*transaction); + self.vote_entry.total_stake_by_key = self + .vote_entry + .total_stake_by_key + .saturating_add(validator_stake); + true + } + + pub fn add_to_certificate(&self, output: &mut VoteCertificateBuilder) { + output + .aggregate(&self.vote_entry.transactions) + .expect("Incoming vote message signatures are assumed to be valid") + } +} + +impl VotePool for SimpleVotePool { + fn total_stake(&self) -> Stake { + self.vote_entry.total_stake_by_key + } + fn has_prev_validator_vote(&self, validator_vote_key: &Pubkey) -> bool { + self.prev_voted_validators.contains(validator_vote_key) + } +} + +pub(crate) struct DuplicateBlockVotePool { + max_entries_per_pubkey: usize, + pub(crate) votes: HashMap, + total_stake: Stake, + prev_voted_block_ids: HashMap>, + top_entry_stake: Stake, +} + +impl DuplicateBlockVotePool { + pub fn new(max_entries_per_pubkey: usize) -> Self { + Self { + max_entries_per_pubkey, + votes: HashMap::new(), + total_stake: 0, + prev_voted_block_ids: HashMap::new(), + top_entry_stake: 0, + } + } + + pub fn add_vote( + &mut self, + validator_vote_key: &Pubkey, + voted_block_id: Hash, + transaction: &VoteMessage, + validator_stake: Stake, + ) -> bool { + // Check whether the validator_vote_key already used the same voted_block_id or exceeded max_entries_per_pubkey + // If so, return false, otherwise add the voted_block_id to the prev_votes + let prev_voted_block_ids = self + .prev_voted_block_ids + .entry(*validator_vote_key) + .or_default(); + if prev_voted_block_ids.contains(&voted_block_id) { + return false; + } + let inserted_first_time = prev_voted_block_ids.is_empty(); + if prev_voted_block_ids.len() >= self.max_entries_per_pubkey { + return false; + } + prev_voted_block_ids.push(voted_block_id); + + let vote_entry = self + .votes + .entry(voted_block_id) + .or_insert_with(VoteEntry::new); + vote_entry.transactions.push(*transaction); + vote_entry.total_stake_by_key = vote_entry + .total_stake_by_key + .saturating_add(validator_stake); + + if inserted_first_time { + self.total_stake = self.total_stake.saturating_add(validator_stake); + } + if vote_entry.total_stake_by_key > self.top_entry_stake { + self.top_entry_stake = vote_entry.total_stake_by_key; + } + true + } + + pub fn total_stake_by_block_id(&self, block_id: &Hash) -> Stake { + self.votes + .get(block_id) + .map_or(0, |vote_entries| vote_entries.total_stake_by_key) + } + + pub fn add_to_certificate(&self, block_id: &Hash, output: &mut VoteCertificateBuilder) { + if let Some(vote_entries) = self.votes.get(block_id) { + output + .aggregate(&vote_entries.transactions) + .expect("Incoming vote message signatures are assumed to be valid") + } + } + + // Get the previous notarization vote, only used for safe to notar to figure out previous notar vote + pub(crate) fn get_prev_voted_block_id(&self, validator_vote_key: &Pubkey) -> Option { + self.prev_voted_block_ids + .get(validator_vote_key) + .and_then(|vs| vs.first().cloned()) + } + + pub fn has_prev_validator_vote_for_block( + &self, + validator_vote_key: &Pubkey, + block_id: &Hash, + ) -> bool { + self.prev_voted_block_ids + .get(validator_vote_key) + .is_some_and(|vs| vs.contains(block_id)) + } + + pub fn top_entry_stake(&self) -> Stake { + self.top_entry_stake + } +} + +impl VotePool for DuplicateBlockVotePool { + fn total_stake(&self) -> Stake { + self.total_stake + } + fn has_prev_validator_vote(&self, validator_vote_key: &Pubkey) -> bool { + self.prev_voted_block_ids.contains_key(validator_vote_key) + } +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_bls_signatures::Signature as BLSSignature, + solana_votor_messages::{bls_message::VoteMessage, vote::Vote}, + }; + + #[test] + fn test_skip_vote_pool() { + let mut vote_pool = SimpleVotePool::new(); + let vote = Vote::new_skip_vote(5); + let transaction = VoteMessage { + vote, + signature: BLSSignature::default(), + rank: 1, + }; + let my_pubkey = Pubkey::new_unique(); + + assert!(vote_pool.add_vote(&my_pubkey, 10, &transaction)); + assert_eq!(vote_pool.total_stake(), 10); + + // Adding the same key again should fail + assert!(!vote_pool.add_vote(&my_pubkey, 10, &transaction)); + assert_eq!(vote_pool.total_stake(), 10); + + // Adding a different key should succeed + let new_pubkey = Pubkey::new_unique(); + assert!(vote_pool.add_vote(&new_pubkey, 60, &transaction),); + assert_eq!(vote_pool.total_stake(), 70); + } + + #[test] + fn test_notarization_pool() { + let mut vote_pool = DuplicateBlockVotePool::new(1); + let my_pubkey = Pubkey::new_unique(); + let block_id = Hash::new_unique(); + let vote = Vote::new_notarization_vote(3, block_id); + let transaction = VoteMessage { + vote, + signature: BLSSignature::default(), + rank: 1, + }; + assert!(vote_pool.add_vote(&my_pubkey, block_id, &transaction, 10,)); + assert_eq!(vote_pool.total_stake(), 10); + assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 10); + + // Adding the same key again should fail + assert!(!vote_pool.add_vote(&my_pubkey, block_id, &transaction, 10)); + assert_eq!(vote_pool.total_stake(), 10); + + // Adding a different bankhash should fail + assert!(!vote_pool.add_vote(&my_pubkey, block_id, &transaction, 10)); + assert_eq!(vote_pool.total_stake(), 10); + + // Adding a different key should succeed + let new_pubkey = Pubkey::new_unique(); + assert!(vote_pool.add_vote(&new_pubkey, block_id, &transaction, 60),); + assert_eq!(vote_pool.total_stake(), 70); + assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 70); + } + + #[test] + fn test_notarization_fallback_pool() { + solana_logger::setup(); + let mut vote_pool = DuplicateBlockVotePool::new(3); + let vote = Vote::new_notarization_fallback_vote(7, Hash::new_unique()); + let transaction = VoteMessage { + vote, + signature: BLSSignature::default(), + rank: 1, + }; + let my_pubkey = Pubkey::new_unique(); + + let block_ids: Vec = (0..4).map(|_| Hash::new_unique()).collect(); + + // Adding the first 3 votes should succeed, but total_stake should remain at 10 + for block_id in &block_ids[0..3] { + assert!(vote_pool.add_vote(&my_pubkey, *block_id, &transaction, 10)); + assert_eq!(vote_pool.total_stake(), 10); + assert_eq!(vote_pool.total_stake_by_block_id(block_id), 10); + } + // Adding the 4th vote should fail + assert!(!vote_pool.add_vote(&my_pubkey, block_ids[3], &transaction, 10)); + assert_eq!(vote_pool.total_stake(), 10); + assert_eq!(vote_pool.total_stake_by_block_id(&block_ids[3]), 0); + + // Adding a different key should succeed + let new_pubkey = Pubkey::new_unique(); + for block_id in &block_ids[1..3] { + assert!(vote_pool.add_vote(&new_pubkey, *block_id, &transaction, 60)); + assert_eq!(vote_pool.total_stake(), 70); + assert_eq!(vote_pool.total_stake_by_block_id(block_id), 70); + } + + // The new key only added 2 votes, so adding block_ids[3] should succeed + assert!(vote_pool.add_vote(&new_pubkey, block_ids[3], &transaction, 60)); + assert_eq!(vote_pool.total_stake(), 70); + assert_eq!(vote_pool.total_stake_by_block_id(&block_ids[3]), 60); + + // Now if adding the same key again, it should fail + assert!(!vote_pool.add_vote(&new_pubkey, block_ids[0], &transaction, 60)); + } +} diff --git a/votor/src/certificate_pool_service.rs b/votor/src/certificate_pool_service.rs new file mode 100644 index 0000000000..68fd685c10 --- /dev/null +++ b/votor/src/certificate_pool_service.rs @@ -0,0 +1,405 @@ +//! Service in charge of ingesting new messages into the certificate pool +//! and notifying votor of new events that occur + +mod stats; + +use { + crate::{ + certificate_pool::{ + self, parent_ready_tracker::BlockProductionParent, AddVoteError, CertificatePool, + }, + commitment::{ + alpenglow_update_commitment_cache, AlpenglowCommitmentAggregationData, + AlpenglowCommitmentType, + }, + event::{LeaderWindowInfo, VotorEvent, VotorEventSender}, + voting_utils::BLSOp, + votor::Votor, + Certificate, STANDSTILL_TIMEOUT, + }, + crossbeam_channel::{select, Sender, TrySendError}, + solana_ledger::{ + blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, + leader_schedule_utils::last_of_consecutive_leader_slots, + }, + solana_pubkey::Pubkey, + solana_runtime::{ + root_bank_cache::RootBankCache, vote_sender_types::BLSVerifiedMessageReceiver, + }, + solana_sdk::clock::Slot, + solana_votor_messages::bls_message::{BLSMessage, CertificateMessage}, + stats::CertificatePoolServiceStats, + std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Condvar, Mutex, + }, + thread::{self, Builder, JoinHandle}, + time::{Duration, Instant}, + }, +}; + +/// Inputs for the certificate pool thread +pub(crate) struct CertificatePoolContext { + pub(crate) exit: Arc, + pub(crate) start: Arc<(Mutex, Condvar)>, + + pub(crate) my_pubkey: Pubkey, + pub(crate) my_vote_pubkey: Pubkey, + pub(crate) blockstore: Arc, + pub(crate) root_bank_cache: RootBankCache, + pub(crate) leader_schedule_cache: Arc, + + // TODO: for now we ingest our own votes into the certificate pool + // just like regular votes. However do we need to convert + // Vote -> BLSMessage -> Vote? + // consider adding a separate pathway in cert_pool.add_transaction for ingesting own votes + pub(crate) bls_receiver: BLSVerifiedMessageReceiver, + + pub(crate) bls_sender: Sender, + pub(crate) event_sender: VotorEventSender, + pub(crate) commitment_sender: Sender, + pub(crate) certificate_sender: Sender<(Certificate, CertificateMessage)>, +} + +pub(crate) struct CertificatePoolService { + t_ingest: JoinHandle<()>, +} + +impl CertificatePoolService { + pub(crate) fn new(ctx: CertificatePoolContext) -> Self { + let t_ingest = Builder::new() + .name("solCertPoolIngest".to_string()) + .spawn(move || { + if let Err(e) = Self::certificate_pool_ingest_loop(ctx) { + info!("Certificate pool service exited: {e:?}. Shutting down"); + } + }) + .unwrap(); + + Self { t_ingest } + } + + fn maybe_update_root_and_send_new_certificates( + cert_pool: &mut CertificatePool, + root_bank_cache: &mut RootBankCache, + bls_sender: &Sender, + new_finalized_slot: Option, + new_certificates_to_send: Vec>, + current_root: &mut Slot, + standstill_timer: &mut Instant, + stats: &mut CertificatePoolServiceStats, + ) -> Result<(), AddVoteError> { + // If we have a new finalized slot, update the root and send new certificates + if new_finalized_slot.is_some() { + // Reset standstill timer + *standstill_timer = Instant::now(); + CertificatePoolServiceStats::incr_u16(&mut stats.new_finalized_slot); + // Set root + let root_bank = root_bank_cache.root_bank(); + if root_bank.slot() > *current_root { + CertificatePoolServiceStats::incr_u16(&mut stats.new_root); + *current_root = root_bank.slot(); + cert_pool.handle_new_root(root_bank); + } + } + // Send new certificates to peers + Self::send_certificates(bls_sender, new_certificates_to_send, stats) + } + + fn send_certificates( + bls_sender: &Sender, + certificates_to_send: Vec>, + stats: &mut CertificatePoolServiceStats, + ) -> Result<(), AddVoteError> { + for (i, certificate) in certificates_to_send.iter().enumerate() { + // The buffer should normally be large enough, so we don't handle + // certificate re-send here. + match bls_sender.try_send(BLSOp::PushCertificate { + certificate: certificate.clone(), + }) { + Ok(_) => { + CertificatePoolServiceStats::incr_u16(&mut stats.certificates_sent); + } + Err(TrySendError::Disconnected(_)) => { + return Err(AddVoteError::ChannelDisconnected( + "VotingService".to_string(), + )); + } + Err(TrySendError::Full(_)) => { + let dropped = certificates_to_send.len().saturating_sub(i) as u16; + stats.certificates_dropped = stats.certificates_dropped.saturating_add(dropped); + return Err(AddVoteError::VotingServiceQueueFull); + } + } + } + Ok(()) + } + + fn process_bls_message( + ctx: &mut CertificatePoolContext, + message: &BLSMessage, + cert_pool: &mut CertificatePool, + events: &mut Vec, + current_root: &mut Slot, + standstill_timer: &mut Instant, + stats: &mut CertificatePoolServiceStats, + ) -> Result<(), AddVoteError> { + match message { + BLSMessage::Certificate(_) => { + CertificatePoolServiceStats::incr_u32(&mut stats.received_certificates); + } + BLSMessage::Vote(_) => { + CertificatePoolServiceStats::incr_u32(&mut stats.received_votes); + } + } + let (new_finalized_slot, new_certificates_to_send) = + Self::add_message_and_maybe_update_commitment( + &ctx.my_pubkey, + &ctx.my_vote_pubkey, + message, + cert_pool, + events, + &ctx.commitment_sender, + )?; + Self::maybe_update_root_and_send_new_certificates( + cert_pool, + &mut ctx.root_bank_cache, + &ctx.bls_sender, + new_finalized_slot, + new_certificates_to_send, + current_root, + standstill_timer, + stats, + ) + } + + fn handle_channel_disconnected( + ctx: &mut CertificatePoolContext, + channel_name: &str, + ) -> Result<(), ()> { + info!("{}: {} disconnected. Exiting", ctx.my_pubkey, channel_name); + ctx.exit.store(true, Ordering::Relaxed); + Err(()) + } + + // Main loop for the certificate pool service, it only exits when any channel is disconnected + fn certificate_pool_ingest_loop(mut ctx: CertificatePoolContext) -> Result<(), ()> { + let mut events = vec![]; + let mut cert_pool = certificate_pool::load_from_blockstore( + &ctx.my_pubkey, + &ctx.root_bank_cache.root_bank(), + ctx.blockstore.as_ref(), + Some(ctx.certificate_sender.clone()), + &mut events, + ); + + // Wait until migration has completed + info!("{}: Certificate pool loop initialized", &ctx.my_pubkey); + Votor::wait_for_migration_or_exit(&ctx.exit, &ctx.start); + info!("{}: Certificate pool loop starting", &ctx.my_pubkey); + let mut current_root = ctx.root_bank_cache.root_bank().slot(); + let mut stats = CertificatePoolServiceStats::new(); + + // Standstill tracking + let mut standstill_timer = Instant::now(); + + // Kick off parent ready + let root_bank = ctx.root_bank_cache.root_bank(); + let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); + let mut highest_parent_ready = root_bank.slot(); + events.push(VotorEvent::ParentReady { + slot: root_bank.slot().checked_add(1).unwrap(), + parent_block: root_block, + }); + + // Ingest votes into certificate pool and notify voting loop of new events + while !ctx.exit.load(Ordering::Relaxed) { + // TODO: we need set identity here as well + Self::add_produce_block_event( + &mut highest_parent_ready, + &cert_pool, + &mut ctx, + &mut events, + &mut stats, + ); + + if standstill_timer.elapsed() > STANDSTILL_TIMEOUT { + events.push(VotorEvent::Standstill(cert_pool.highest_finalized_slot())); + stats.standstill = true; + standstill_timer = Instant::now(); + match Self::send_certificates( + &ctx.bls_sender, + cert_pool.get_certs_for_standstill(), + &mut stats, + ) { + Ok(()) => (), + Err(AddVoteError::ChannelDisconnected(channel_name)) => { + return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); + } + Err(e) => { + trace!( + "{}: unable to push standstill certificates into pool {}", + &ctx.my_pubkey, + e + ); + } + } + } + + if events + .drain(..) + .try_for_each(|event| ctx.event_sender.send(event)) + .is_err() + { + return Self::handle_channel_disconnected(&mut ctx, "Votor event receiver"); + } + + let bls_messages: Vec = select! { + recv(ctx.bls_receiver) -> msg => { + let Ok(first) = msg else { + return Self::handle_channel_disconnected(&mut ctx, "BLS receiver"); + }; + std::iter::once(first).chain(ctx.bls_receiver.try_iter()).collect() + }, + default(Duration::from_secs(1)) => continue + }; + + for message in bls_messages { + match Self::process_bls_message( + &mut ctx, + &message, + &mut cert_pool, + &mut events, + &mut current_root, + &mut standstill_timer, + &mut stats, + ) { + Ok(()) => {} + Err(AddVoteError::ChannelDisconnected(channel_name)) => { + return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()) + } + Err(e) => { + // This is a non critical error, a duplicate vote for example + trace!("{}: unable to push vote into pool {}", &ctx.my_pubkey, e); + CertificatePoolServiceStats::incr_u32(&mut stats.add_message_failed); + } + } + } + stats.maybe_report(); + cert_pool.maybe_report(); + } + Ok(()) + } + + /// Adds a vote to the certificate pool and updates the commitment cache if necessary + /// + /// If a new finalization slot was recognized, returns the slot + fn add_message_and_maybe_update_commitment( + my_pubkey: &Pubkey, + my_vote_pubkey: &Pubkey, + message: &BLSMessage, + cert_pool: &mut CertificatePool, + votor_events: &mut Vec, + commitment_sender: &Sender, + ) -> Result<(Option, Vec>), AddVoteError> { + let (new_finalized_slot, new_certificates_to_send) = + cert_pool.add_message(my_vote_pubkey, message, votor_events)?; + let Some(new_finalized_slot) = new_finalized_slot else { + return Ok((None, new_certificates_to_send)); + }; + trace!("{my_pubkey}: new finalization certificate for {new_finalized_slot}"); + alpenglow_update_commitment_cache( + AlpenglowCommitmentType::Finalized, + new_finalized_slot, + commitment_sender, + )?; + Ok((Some(new_finalized_slot), new_certificates_to_send)) + } + + fn add_produce_block_event( + highest_parent_ready: &mut Slot, + cert_pool: &CertificatePool, + ctx: &mut CertificatePoolContext, + events: &mut Vec, + stats: &mut CertificatePoolServiceStats, + ) { + let Some(new_highest_parent_ready) = events + .iter() + .filter_map(|event| match event { + VotorEvent::ParentReady { slot, .. } => Some(slot), + _ => None, + }) + .max() + .copied() + else { + return; + }; + + if new_highest_parent_ready <= *highest_parent_ready { + return; + } + *highest_parent_ready = new_highest_parent_ready; + + let Some(leader_pubkey) = ctx.leader_schedule_cache.slot_leader_at( + *highest_parent_ready, + Some(&ctx.root_bank_cache.root_bank()), + ) else { + error!("Unable to compute the leader at slot {highest_parent_ready}. Something is wrong, exiting"); + ctx.exit.store(true, Ordering::Relaxed); + return; + }; + + if leader_pubkey != ctx.my_pubkey { + return; + } + + let start_slot = *highest_parent_ready; + let end_slot = last_of_consecutive_leader_slots(start_slot); + + if (start_slot..=end_slot).any(|s| ctx.blockstore.has_existing_shreds_for_slot(s)) { + warn!( + "{}: We have already produced shreds in the window {start_slot}-{end_slot}, \ + skipping production of our leader window", + ctx.my_pubkey + ); + return; + } + + match cert_pool + .parent_ready_tracker + .block_production_parent(start_slot) + { + BlockProductionParent::MissedWindow => { + warn!( + "{}: Leader slot {start_slot} has already been certified, \ + skipping production of {start_slot}-{end_slot}", + ctx.my_pubkey, + ); + CertificatePoolServiceStats::incr_u16(&mut stats.parent_ready_missed_window); + } + BlockProductionParent::ParentNotReady => { + // This can't happen, place holder depending on how we hook up optimistic + ctx.exit.store(true, Ordering::Relaxed); + panic!( + "Must have a block production parent: {:#?}", + cert_pool.parent_ready_tracker + ); + } + BlockProductionParent::Parent(parent_block) => { + events.push(VotorEvent::ProduceWindow(LeaderWindowInfo { + start_slot, + end_slot, + parent_block, + // TODO: we can just remove this + skip_timer: Instant::now(), + })); + CertificatePoolServiceStats::incr_u16(&mut stats.parent_ready_produce_window); + } + } + } + + pub(crate) fn join(self) -> thread::Result<()> { + self.t_ingest.join() + } +} diff --git a/votor/src/certificate_pool_service/stats.rs b/votor/src/certificate_pool_service/stats.rs new file mode 100644 index 0000000000..d6b8e55278 --- /dev/null +++ b/votor/src/certificate_pool_service/stats.rs @@ -0,0 +1,92 @@ +use { + solana_metrics::datapoint_info, + std::time::{Duration, Instant}, +}; + +const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(10); + +#[derive(Debug)] +pub(crate) struct CertificatePoolServiceStats { + pub(crate) add_message_failed: u32, + pub(crate) certificates_sent: u16, + pub(crate) certificates_dropped: u16, + pub(crate) new_finalized_slot: u16, + pub(crate) new_root: u16, + pub(crate) parent_ready_missed_window: u16, + pub(crate) parent_ready_produce_window: u16, + pub(crate) received_votes: u32, + pub(crate) received_certificates: u32, + pub(crate) standstill: bool, + last_request_time: Instant, +} + +impl CertificatePoolServiceStats { + pub fn new() -> Self { + Self { + add_message_failed: 0, + certificates_sent: 0, + certificates_dropped: 0, + new_finalized_slot: 0, + new_root: 0, + parent_ready_missed_window: 0, + parent_ready_produce_window: 0, + received_votes: 0, + received_certificates: 0, + standstill: false, + last_request_time: Instant::now(), + } + } + + pub fn incr_u16(value: &mut u16) { + *value = value.saturating_add(1); + } + + pub fn incr_u32(value: &mut u32) { + *value = value.saturating_add(1); + } + + fn reset(&mut self) { + self.add_message_failed = 0; + self.certificates_sent = 0; + self.certificates_dropped = 0; + self.new_finalized_slot = 0; + self.new_root = 0; + self.parent_ready_missed_window = 0; + self.parent_ready_produce_window = 0; + self.received_votes = 0; + self.received_certificates = 0; + self.standstill = false; + self.last_request_time = Instant::now(); + } + + fn report(&self) { + datapoint_info!( + "cert_pool_service", + ("add_message_failed", self.add_message_failed, i64), + ("certificates_sent", self.certificates_sent, i64), + ("certificates_dropped", self.certificates_dropped, i64), + ("new_root", self.new_root, i64), + ("new_finalized_slot", self.new_finalized_slot, i64), + ( + "parent_ready_missed_window", + self.parent_ready_missed_window, + i64 + ), + ( + "parent_ready_produce_window", + self.parent_ready_produce_window, + i64 + ), + ("received_votes", self.received_votes, i64), + ("received_certificates", self.received_certificates, i64), + ("standstill", self.standstill, i64), + ); + } + + pub fn maybe_report(&mut self) { + if self.last_request_time.elapsed() >= STATS_REPORT_INTERVAL { + self.report(); + self.reset(); + } + } +} diff --git a/votor/src/commitment.rs b/votor/src/commitment.rs new file mode 100644 index 0000000000..b09347d0bb --- /dev/null +++ b/votor/src/commitment.rs @@ -0,0 +1,42 @@ +use { + crossbeam_channel::{Sender, TrySendError}, + solana_sdk::clock::Slot, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum AlpenglowCommitmentError { + #[error("Failed to send commitment data, channel disconnected")] + ChannelDisconnected, +} + +pub enum AlpenglowCommitmentType { + /// Our node has voted notarize for the slot + Notarize, + /// We have observed a finalization certificate for the slot + Finalized, +} + +pub struct AlpenglowCommitmentAggregationData { + pub commitment_type: AlpenglowCommitmentType, + pub slot: Slot, +} + +pub fn alpenglow_update_commitment_cache( + commitment_type: AlpenglowCommitmentType, + slot: Slot, + commitment_sender: &Sender, +) -> Result<(), AlpenglowCommitmentError> { + match commitment_sender.try_send(AlpenglowCommitmentAggregationData { + commitment_type, + slot, + }) { + Err(TrySendError::Disconnected(_)) => { + info!("commitment_sender has disconnected"); + return Err(AlpenglowCommitmentError::ChannelDisconnected); + } + Err(TrySendError::Full(_)) => error!("commitment_sender is backed up, something is wrong"), + Ok(_) => (), + } + Ok(()) +} diff --git a/votor/src/event.rs b/votor/src/event.rs new file mode 100644 index 0000000000..36f324b225 --- /dev/null +++ b/votor/src/event.rs @@ -0,0 +1,84 @@ +use { + crossbeam_channel::{Receiver, Sender}, + solana_runtime::bank::Bank, + solana_sdk::clock::Slot, + solana_votor_messages::bls_message::Block, + std::{sync::Arc, time::Instant}, +}; + +#[derive(Debug, Clone)] +pub struct CompletedBlock { + pub slot: Slot, + // TODO: once we have the async execution changes this can be (block_id, parent_block_id) instead + pub bank: Arc, +} + +/// Context for the block creation loop to start a leader window +#[derive(Copy, Clone, Debug)] +pub struct LeaderWindowInfo { + pub start_slot: Slot, + pub end_slot: Slot, + pub parent_block: Block, + pub skip_timer: Instant, +} + +pub type VotorEventSender = Sender; +pub type VotorEventReceiver = Receiver; + +/// Events that trigger actions in Votor +/// TODO: remove bank hash once we update votes +#[derive(Debug, Clone)] +pub enum VotorEvent { + /// A block has completed replay and is ready for voting + Block(CompletedBlock), + + /// The block has received a notarization certificate + BlockNotarized(Block), + + /// The pool has marked the given block as a ready parent for `slot` + ParentReady { slot: Slot, parent_block: Block }, + + /// The skip timer has fired for the given slot + Timeout(Slot), + + /// The given block has reached the safe to notar status + SafeToNotar(Block), + + /// The given slot has reached the safe to skip status + SafeToSkip(Slot), + + /// We are the leader for this window and have reached the parent ready status + /// Produce the window + ProduceWindow(LeaderWindowInfo), + + /// The block has received a slow or fast finalization certificate and is eligble for rooting + Finalized(Block), + + /// We have not observed a finalization and reached the standstill timeout + /// The slot is the highest finalized slot + Standstill(Slot), + + /// The identity keypair has changed due to an operator calling set-identity + SetIdentity, +} + +impl VotorEvent { + /// Ignore old events + pub(crate) fn should_ignore(&self, root: Slot) -> bool { + match self { + VotorEvent::Block(completed_block) => completed_block.slot <= root, + VotorEvent::BlockNotarized((s, _)) => *s <= root, + VotorEvent::ParentReady { + slot, + parent_block: _, + } => *slot <= root, + VotorEvent::Timeout(s) => *s <= root, + VotorEvent::SafeToNotar((s, _)) => *s <= root, + VotorEvent::SafeToSkip(s) => *s <= root, + VotorEvent::ProduceWindow(_) => false, + VotorEvent::Finalized((s, _)) => *s <= root, + VotorEvent::Standstill(_) => false, + VotorEvent::SetIdentity => false, + } + } +} diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs new file mode 100644 index 0000000000..97812217a6 --- /dev/null +++ b/votor/src/event_handler.rs @@ -0,0 +1,572 @@ +//! Handles incoming VotorEvents to take action or +//! notify block creation loop + +use { + crate::{ + commitment::{alpenglow_update_commitment_cache, AlpenglowCommitmentType}, + event::{CompletedBlock, VotorEvent, VotorEventReceiver}, + root_utils::{self, RootContext}, + skip_timer::SkipTimerManager, + vote_history::{VoteHistory, VoteHistoryError}, + voting_utils::{self, BLSOp, VoteError, VotingContext}, + votor::{SharedContext, Votor}, + }, + crossbeam_channel::{select, RecvError, SendError}, + solana_ledger::leader_schedule_utils::{ + first_of_consecutive_leader_slots, last_of_consecutive_leader_slots, leader_slot_index, + }, + solana_pubkey::Pubkey, + solana_runtime::{bank::Bank, bank_forks::SetRootError}, + solana_sdk::{clock::Slot, hash::Hash, signature::Signer}, + solana_votor_messages::{bls_message::Block, vote::Vote}, + std::{ + collections::{BTreeMap, BTreeSet}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Condvar, Mutex, RwLock, + }, + thread::{self, Builder, JoinHandle}, + time::Duration, + }, + thiserror::Error, +}; + +/// Banks that have completed replay, but are yet to be voted on +/// in the form of (block, parent block) +pub(crate) type PendingBlocks = BTreeMap>; + +/// Inputs for the event handler thread +pub(crate) struct EventHandlerContext { + pub(crate) exit: Arc, + pub(crate) start: Arc<(Mutex, Condvar)>, + + pub(crate) event_receiver: VotorEventReceiver, + pub(crate) skip_timer: Arc>, + + // Contexts + pub(crate) shared_context: SharedContext, + pub(crate) voting_context: VotingContext, + pub(crate) root_context: RootContext, +} + +#[derive(Debug, Error)] +enum EventLoopError { + #[error("Receiver is disconnected")] + ReceiverDisconnected(#[from] RecvError), + + #[error("Sender is disconnected")] + SenderDisconnected(#[from] SendError<()>), + + #[error("Error generating and inserting vote")] + VotingError(#[from] VoteError), + + #[error("Unable to set root")] + SetRootError(#[from] SetRootError), + + #[error("Set identity error")] + SetIdentityError(#[from] VoteHistoryError), +} + +pub(crate) struct EventHandler { + t_event_handler: JoinHandle<()>, +} + +impl EventHandler { + pub(crate) fn new(ctx: EventHandlerContext) -> Self { + let exit = ctx.exit.clone(); + let t_event_handler = Builder::new() + .name("solVotorEventLoop".to_string()) + .spawn(move || { + if let Err(e) = Self::event_loop(ctx) { + info!("Event loop exited: {e:?}. Shutting down"); + exit.store(true, Ordering::Relaxed); + } + }) + .unwrap(); + + Self { t_event_handler } + } + + fn event_loop(context: EventHandlerContext) -> Result<(), EventLoopError> { + let EventHandlerContext { + exit, + start, + event_receiver, + skip_timer, + shared_context: ctx, + voting_context: mut vctx, + root_context: rctx, + } = context; + let mut my_pubkey = vctx.identity_keypair.pubkey(); + let mut pending_blocks = PendingBlocks::default(); + let mut finalized_blocks = BTreeSet::default(); + + // Wait until migration has completed + info!("{my_pubkey}: Event loop initialized"); + Votor::wait_for_migration_or_exit(&exit, &start); + info!("{my_pubkey}: Event loop starting"); + + if exit.load(Ordering::Relaxed) { + return Ok(()); + } + + // Check for set identity + if let Err(e) = Self::handle_set_identity(&mut my_pubkey, &ctx, &mut vctx) { + error!( + "Unable to load new vote history when attempting to change identity from {} \ + to {} on voting loop startup, Exiting: {}", + vctx.vote_history.node_pubkey, + ctx.cluster_info.id(), + e + ); + return Err(EventLoopError::SetIdentityError(e)); + } + + while !exit.load(Ordering::Relaxed) { + let event = select! { + recv(event_receiver) -> msg => { + msg? + }, + default(Duration::from_secs(1)) => continue + }; + + if event.should_ignore(vctx.root_bank_cache.root_bank().slot()) { + continue; + } + + let votes = Self::handle_event( + &mut my_pubkey, + event, + &skip_timer, + &ctx, + &mut vctx, + &rctx, + &mut pending_blocks, + &mut finalized_blocks, + )?; + + // TODO: properly bubble up error handling here and in call graph + for vote in votes { + vctx.bls_sender.send(vote?).map_err(|_| SendError(()))?; + } + } + + Ok(()) + } + + fn handle_event( + my_pubkey: &mut Pubkey, + event: VotorEvent, + skip_timer: &RwLock, + ctx: &SharedContext, + vctx: &mut VotingContext, + rctx: &RootContext, + pending_blocks: &mut PendingBlocks, + finalized_blocks: &mut BTreeSet, + ) -> Result>, EventLoopError> { + let mut votes = vec![]; + match event { + // Block has completed replay + VotorEvent::Block(CompletedBlock { slot, bank }) => { + debug_assert!(bank.is_frozen()); + let (block, parent_block) = Self::get_block_parent_block(&bank); + info!("{my_pubkey}: Block {block:?} parent {parent_block:?}"); + if Self::try_notar( + my_pubkey, + block, + parent_block, + pending_blocks, + vctx, + &mut votes, + ) { + Self::check_pending_blocks(my_pubkey, pending_blocks, vctx, &mut votes); + } else if !vctx.vote_history.voted(slot) { + pending_blocks + .entry(slot) + .or_default() + .push((block, parent_block)); + } + Self::check_rootable_blocks( + my_pubkey, + ctx, + vctx, + rctx, + pending_blocks, + finalized_blocks, + )?; + } + + // Block has received a notarization certificate + VotorEvent::BlockNotarized(block) => { + info!("{my_pubkey}: Block Notarized {block:?}"); + vctx.vote_history.add_block_notarized(block); + Self::try_final(my_pubkey, block, vctx, &mut votes); + } + + // Received a parent ready notification for `slot` + VotorEvent::ParentReady { slot, parent_block } => { + info!("{my_pubkey}: Parent ready {slot} {parent_block:?}"); + let should_set_timeouts = vctx.vote_history.add_parent_ready(slot, parent_block); + Self::check_pending_blocks(my_pubkey, pending_blocks, vctx, &mut votes); + if should_set_timeouts { + skip_timer.write().unwrap().set_timeouts(slot); + } + } + + // Skip timer for the slot has fired + VotorEvent::Timeout(slot) => { + info!("{my_pubkey}: Timeout {slot}"); + if vctx.vote_history.voted(slot) { + return Ok(votes); + } + Self::try_skip_window(my_pubkey, slot, vctx, &mut votes); + } + + // We have observed the safe to notar condition, and can send a notar fallback vote + // TODO: update cert pool to check parent block id for intra window slots + VotorEvent::SafeToNotar(block @ (slot, block_id)) => { + info!("{my_pubkey}: SafeToNotar {block:?}"); + Self::try_skip_window(my_pubkey, slot, vctx, &mut votes); + if vctx.vote_history.its_over(slot) + || vctx.vote_history.voted_notar_fallback(slot, block_id) + { + return Ok(votes); + } + info!("{my_pubkey}: Voting notarize-fallback for {slot} {block_id}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + Vote::new_notarization_fallback_vote(slot, block_id), + false, + vctx, + )); + } + + // We have observed the safe to skip condition, and can send a skip fallback vote + VotorEvent::SafeToSkip(slot) => { + info!("{my_pubkey}: SafeToSkip {slot}"); + Self::try_skip_window(my_pubkey, slot, vctx, &mut votes); + if vctx.vote_history.its_over(slot) || vctx.vote_history.voted_skip_fallback(slot) { + return Ok(votes); + } + info!("{my_pubkey}: Voting skip-fallback for {slot}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + Vote::new_skip_fallback_vote(slot), + false, + vctx, + )); + } + + // It is time to produce our leader window + VotorEvent::ProduceWindow(window_info) => { + info!("{my_pubkey}: ProduceWindow {window_info:?}"); + let mut l_window_info = ctx.leader_window_notifier.window_info.lock().unwrap(); + if let Some(old_window_info) = l_window_info.as_ref() { + error!( + "{my_pubkey}: Attempting to start leader window for {}-{}, \ + however there is already a pending window to produce {}-{}. \ + Our production is lagging, discarding in favor of the newer window", + window_info.start_slot, + window_info.end_slot, + old_window_info.start_slot, + old_window_info.end_slot, + ); + } + *l_window_info = Some(window_info); + ctx.leader_window_notifier.window_notification.notify_one(); + } + + // We have finalized this block consider it for rooting + VotorEvent::Finalized(block) => { + info!("{my_pubkey}: Finalized {block:?}"); + finalized_blocks.insert(block); + Self::check_rootable_blocks( + my_pubkey, + ctx, + vctx, + rctx, + pending_blocks, + finalized_blocks, + )?; + } + + // We have not observed a finalization certificate in a while, refresh our votes + VotorEvent::Standstill(highest_finalized_slot) => { + info!("{my_pubkey}: Standstill {highest_finalized_slot}"); + // certs refresh happens in CertificatePoolService + Self::refresh_votes(my_pubkey, highest_finalized_slot, vctx, &mut votes); + } + + // Operator called set identity make sure that our keypair is updated for voting + // TODO: plug this in from cli + VotorEvent::SetIdentity => { + info!("{my_pubkey}: SetIdentity"); + if let Err(e) = Self::handle_set_identity(my_pubkey, ctx, vctx) { + error!( + "Unable to load new vote history when attempting to change identity from {} \ + to {} in voting loop, Exiting: {}", + vctx.vote_history.node_pubkey, + ctx.cluster_info.id(), + e + ); + return Err(EventLoopError::SetIdentityError(e)); + } + } + } + Ok(votes) + } + + fn handle_set_identity( + my_pubkey: &mut Pubkey, + ctx: &SharedContext, + vctx: &mut VotingContext, + ) -> Result<(), VoteHistoryError> { + let new_identity = ctx.cluster_info.keypair(); + let new_pubkey = new_identity.pubkey(); + // This covers both: + // - startup set-identity so that vote_history is outdated but my_pubkey == new_pubkey + // - set-identity during normal operation, vote_history == my_pubkey != new_pubkey + if *my_pubkey != new_pubkey || vctx.vote_history.node_pubkey != new_pubkey { + let my_old_pubkey = vctx.vote_history.node_pubkey; + *my_pubkey = new_pubkey; + vctx.vote_history = VoteHistory::restore(ctx.vote_history_storage.as_ref(), my_pubkey)?; + vctx.identity_keypair = new_identity.clone(); + warn!("set-identity: from {my_old_pubkey} to {my_pubkey}"); + } + Ok(()) + } + + fn get_block_parent_block(bank: &Bank) -> (Block, Block) { + let slot = bank.slot(); + let block = ( + slot, + bank.block_id().expect("Block id must be set upstream"), + ); + let parent_slot = bank.parent_slot(); + let parent_block_id = bank.parent_block_id().unwrap_or_else(|| { + // To account for child of genesis and snapshots we insert a + // default block id here. Charlie is working on a SIMD to add block + // id to snapshots, which can allow us to remove this and update + // the default case in parent ready tracker. + trace!("Using default block id for {slot} parent {parent_slot}"); + Hash::default() + }); + let parent_block = (parent_slot, parent_block_id); + (block, parent_block) + } + + /// Tries to vote notarize on `block`: + /// - We have not voted notarize or skip for `slot(block)` + /// - Either it's the first leader block of the window and we are parent ready + /// - or it's a consecutive slot and we have voted notarize on the parent + /// + /// + /// If successful returns true + fn try_notar( + my_pubkey: &Pubkey, + (slot, block_id): Block, + parent_block @ (parent_slot, parent_block_id): Block, + pending_blocks: &mut PendingBlocks, + voting_context: &mut VotingContext, + votes: &mut Vec>, + ) -> bool { + if voting_context.vote_history.voted(slot) { + return false; + } + + if leader_slot_index(slot) == 0 || slot == 1 { + if !voting_context + .vote_history + .is_parent_ready(slot, &parent_block) + { + // Need to ingest more certificates first + return false; + } + } else { + if parent_slot.saturating_add(1) != slot { + // Non consecutive + return false; + } + if voting_context.vote_history.voted_notar(parent_slot) != Some(parent_block_id) { + // Voted skip, or notarize on a different version of the parent + return false; + } + } + + info!("{my_pubkey}: Voting notarize for {slot} {block_id}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + Vote::new_notarization_vote(slot, block_id), + false, + voting_context, + )); + let _ = alpenglow_update_commitment_cache( + AlpenglowCommitmentType::Notarize, + slot, + &voting_context.commitment_sender, + ); + pending_blocks.remove(&slot); + + true + } + + /// Checks the pending blocks that have completed replay to see if they + /// are eligble to be voted on now + fn check_pending_blocks( + my_pubkey: &Pubkey, + pending_blocks: &mut PendingBlocks, + voting_context: &mut VotingContext, + votes: &mut Vec>, + ) { + let blocks_to_check: Vec<(Block, Block)> = pending_blocks + .values() + .flat_map(|blocks| blocks.iter()) + .copied() + .collect(); + + for (block, parent_block) in blocks_to_check { + Self::try_notar( + my_pubkey, + block, + parent_block, + pending_blocks, + voting_context, + votes, + ); + } + } + + /// Tries to send a finalize vote for the block if + /// - the block has a notarization certificate + /// - we have not already voted finalize + /// - we voted notarize for the block + /// - we have not voted skip, notarize fallback or skip fallback in the slot (bad window) + /// + /// If successful returns true + fn try_final( + my_pubkey: &Pubkey, + block @ (slot, block_id): Block, + voting_context: &mut VotingContext, + votes: &mut Vec>, + ) -> bool { + if !voting_context.vote_history.is_block_notarized(&block) + || voting_context.vote_history.its_over(slot) + || voting_context.vote_history.bad_window(slot) + { + return false; + } + + if voting_context + .vote_history + .voted_notar(slot) + .is_none_or(|bid| bid != block_id) + { + return false; + } + + info!("{my_pubkey}: Voting finalize for {slot}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + Vote::new_finalization_vote(slot), + false, + voting_context, + )); + true + } + + fn try_skip_window( + my_pubkey: &Pubkey, + slot: Slot, + voting_context: &mut VotingContext, + votes: &mut Vec>, + ) { + // In case we set root in the middle of a leader window, + // it's not necessary to vote skip prior to it and we won't + // be able to check vote history if we've already voted on it + let start = first_of_consecutive_leader_slots(slot) + .max(voting_context.root_bank_cache.root_bank().slot()); + for s in start..=last_of_consecutive_leader_slots(slot) { + if voting_context.vote_history.voted(s) { + continue; + } + info!("{my_pubkey}: Voting skip for {s}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + Vote::new_skip_vote(s), + false, + voting_context, + )); + } + } + + /// Refresh all votes cast for slots >= highest_finalized_slot + fn refresh_votes( + my_pubkey: &Pubkey, + highest_finalized_slot: Slot, + voting_context: &mut VotingContext, + votes: &mut Vec>, + ) { + for vote in voting_context + .vote_history + .votes_cast_since(highest_finalized_slot) + { + info!("{my_pubkey}: Refreshing vote {vote:?}"); + votes.push(voting_utils::insert_vote_and_create_bls_message( + my_pubkey, + vote, + true, + voting_context, + )); + } + } + + /// Checks if we can set root on a new block + /// The block must be: + /// - Present in bank forks + /// - Newer than the current root + /// - We must have already voted on bank.slot() + /// - Bank is frozen and finished shredding + /// - Block has a finalization certificate + /// + /// If so set root on the highest block that fits these conditions + fn check_rootable_blocks( + my_pubkey: &Pubkey, + ctx: &SharedContext, + vctx: &mut VotingContext, + rctx: &RootContext, + pending_blocks: &mut PendingBlocks, + finalized_blocks: &mut BTreeSet, + ) -> Result<(), SetRootError> { + let bank_forks_r = ctx.bank_forks.read().unwrap(); + let old_root = bank_forks_r.root(); + let Some(new_root) = finalized_blocks + .iter() + .filter_map(|&(slot, block_id)| { + let bank = bank_forks_r.get(slot)?; + (slot > old_root + && vctx.vote_history.voted(slot) + && bank.is_frozen() + && bank.block_id().is_some_and(|bid| bid == block_id)) + .then_some(slot) + }) + .max() + else { + // No rootable banks + return Ok(()); + }; + drop(bank_forks_r); + root_utils::set_root( + my_pubkey, + new_root, + ctx, + vctx, + rctx, + pending_blocks, + finalized_blocks, + ) + } + + pub(crate) fn join(self) -> thread::Result<()> { + self.t_event_handler.join() + } +} diff --git a/votor/src/lib.rs b/votor/src/lib.rs new file mode 100644 index 0000000000..75982fb310 --- /dev/null +++ b/votor/src/lib.rs @@ -0,0 +1,140 @@ +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +use { + solana_votor_messages::{bls_message::Certificate, vote::Vote}, + std::time::Duration, +}; + +pub mod certificate_pool; +mod certificate_pool_service; +pub mod commitment; +pub mod event; +mod event_handler; +pub mod root_utils; +mod skip_timer; +pub mod vote_history; +pub mod vote_history_storage; +pub mod voting_utils; +pub mod votor; + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate serde_derive; + +#[cfg_attr(feature = "frozen-abi", macro_use)] +#[cfg(feature = "frozen-abi")] +extern crate solana_frozen_abi_macro; + +// Core consensus types and constants +pub type Stake = u64; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VoteType { + Finalize, + Notarize, + NotarizeFallback, + Skip, + SkipFallback, +} + +impl VoteType { + #[allow(dead_code)] + pub fn is_notarize_type(&self) -> bool { + matches!(self, Self::Notarize | Self::NotarizeFallback) + } +} + +pub const fn conflicting_types(vote_type: VoteType) -> &'static [VoteType] { + match vote_type { + VoteType::Finalize => &[VoteType::NotarizeFallback, VoteType::Skip], + VoteType::Notarize => &[VoteType::Skip, VoteType::NotarizeFallback], + VoteType::NotarizeFallback => &[VoteType::Finalize, VoteType::Notarize], + VoteType::Skip => &[ + VoteType::Finalize, + VoteType::Notarize, + VoteType::SkipFallback, + ], + VoteType::SkipFallback => &[VoteType::Skip], + } +} + +/// Lookup from `CertificateId` to the `VoteType`s that contribute, +/// as well as the stake fraction required for certificate completion. +/// +/// Must be in sync with `vote_to_certificate_ids` +pub const fn certificate_limits_and_vote_types( + cert_type: Certificate, +) -> (f64, &'static [VoteType]) { + match cert_type { + Certificate::Notarize(_, _) => (0.6, &[VoteType::Notarize]), + Certificate::NotarizeFallback(_, _) => { + (0.6, &[VoteType::Notarize, VoteType::NotarizeFallback]) + } + Certificate::FinalizeFast(_, _) => (0.8, &[VoteType::Notarize]), + Certificate::Finalize(_) => (0.6, &[VoteType::Finalize]), + Certificate::Skip(_) => (0.6, &[VoteType::Skip, VoteType::SkipFallback]), + } +} + +/// Lookup from `Vote` to the `CertificateId`s the vote accounts for +/// +/// Must be in sync with `certificate_limits_and_vote_types` and `VoteType::get_type` +pub fn vote_to_certificate_ids(vote: &Vote) -> Vec { + match vote { + Vote::Notarize(vote) => vec![ + Certificate::Notarize(vote.slot(), *vote.block_id()), + Certificate::NotarizeFallback(vote.slot(), *vote.block_id()), + Certificate::FinalizeFast(vote.slot(), *vote.block_id()), + ], + Vote::NotarizeFallback(vote) => { + vec![Certificate::NotarizeFallback(vote.slot(), *vote.block_id())] + } + Vote::Finalize(vote) => vec![Certificate::Finalize(vote.slot())], + Vote::Skip(vote) => vec![Certificate::Skip(vote.slot())], + Vote::SkipFallback(vote) => vec![Certificate::Skip(vote.slot())], + } +} + +pub const MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES: usize = 1; +pub const MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE: usize = 3; + +pub const SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY: f64 = 0.4; +pub const SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP: f64 = 0.2; +pub const SAFE_TO_NOTAR_MIN_NOTARIZE_AND_SKIP: f64 = 0.6; + +pub const SAFE_TO_SKIP_THRESHOLD: f64 = 0.4; + +pub const STANDSTILL_TIMEOUT: Duration = Duration::from_secs(10); + +/// Alpenglow block constants +/// The amount of time a leader has to build their block +pub const BLOCKTIME: Duration = Duration::from_millis(400); + +/// The maximum message delay +pub const DELTA: Duration = Duration::from_millis(200); + +/// The maximum delay a node can observe between entering the loop iteration +/// for a window and receiving any shred of the first block of the leader. +/// As a conservative global constant we set this to 3 * DELTA +pub const DELTA_TIMEOUT: Duration = DELTA.saturating_mul(3); + +/// The timeout in ms for the leader block index within the leader window +#[inline] +pub fn skip_timeout(leader_block_index: usize) -> Duration { + DELTA_TIMEOUT + .saturating_add( + BLOCKTIME + .saturating_mul(leader_block_index as u32) + .saturating_add(BLOCKTIME), + ) + .saturating_add(DELTA) +} + +/// Block timeout, when we should publish the final shred for the leader block index +/// within the leader window +#[inline] +pub fn block_timeout(leader_block_index: usize) -> Duration { + // TODO: based on testing, perhaps adjust this + BLOCKTIME.saturating_mul((leader_block_index as u32).saturating_add(1)) +} diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs new file mode 100644 index 0000000000..c2c7936dd0 --- /dev/null +++ b/votor/src/root_utils.rs @@ -0,0 +1,222 @@ +use { + crate::{event_handler::PendingBlocks, voting_utils::VotingContext, votor::SharedContext}, + crossbeam_channel::Sender, + solana_ledger::{blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache}, + solana_rpc::{ + optimistically_confirmed_bank_tracker::{BankNotification, BankNotificationSenderConfig}, + rpc_subscriptions::RpcSubscriptions, + }, + solana_runtime::{ + accounts_background_service::AbsRequestSender, + bank_forks::{BankForks, SetRootError}, + installed_scheduler_pool::BankWithScheduler, + }, + solana_sdk::{ + clock::Slot, hash::Hash, pubkey::Pubkey, signature::Signature, timing::timestamp, + }, + solana_votor_messages::bls_message::Block, + std::{ + collections::BTreeSet, + sync::{Arc, RwLock}, + }, +}; + +/// Structures that are not used in the event loop, but need to be updated +/// or notified when setting root +pub(crate) struct RootContext { + pub(crate) leader_schedule_cache: Arc, + pub(crate) accounts_background_request_sender: AbsRequestSender, + pub(crate) bank_notification_sender: Option, + pub(crate) drop_bank_sender: Sender>, +} + +/// Sets the root for the votor event handling loop. Handles rooting all things +/// except the certificate pool +pub(crate) fn set_root( + my_pubkey: &Pubkey, + new_root: Slot, + ctx: &SharedContext, + vctx: &mut VotingContext, + rctx: &RootContext, + pending_blocks: &mut PendingBlocks, + finalized_blocks: &mut BTreeSet, +) -> Result<(), SetRootError> { + info!("{my_pubkey}: setting root {new_root}"); + vctx.vote_history.set_root(new_root); + *pending_blocks = pending_blocks.split_off(&new_root); + *finalized_blocks = finalized_blocks.split_off(&(new_root, Hash::default())); + + check_and_handle_new_root( + new_root, + new_root, + &rctx.accounts_background_request_sender, + Some(new_root), + &rctx.bank_notification_sender, + &rctx.drop_bank_sender, + &ctx.blockstore, + &rctx.leader_schedule_cache, + &ctx.bank_forks, + &ctx.rpc_subscriptions, + my_pubkey, + &mut false, + &mut vec![], + |_| {}, + )?; + + // Distinguish between duplicate versions of same slot + let hash = ctx.bank_forks.read().unwrap().bank_hash(new_root).unwrap(); + if let Err(e) = + ctx.blockstore + .insert_optimistic_slot(new_root, &hash, timestamp().try_into().unwrap()) + { + error!( + "failed to record optimistic slot in blockstore: slot={}: {:?}", + new_root, &e + ); + } + + // It is critical to send the OC notification in order to keep compatibility with + // the RPC API. Additionally the PrioritizationFeeCache relies on this notification + // in order to perform cleanup. In the future we will look to deprecate OC and remove + // these code paths. + if let Some(config) = &rctx.bank_notification_sender { + // TODO: propagate error + let _ = config + .sender + .send(BankNotification::OptimisticallyConfirmed(new_root)); + } + Ok(()) +} + +/// Sets the new root, additionally performs the callback after setting the bank forks root +/// During this transition period where both replay stage and voting loop can root depending on the feature flag we +/// have a callback that cleans up progress map and other tower bft structures. Then the callgraph is +/// +/// ReplayStage::check_and_handle_new_root -> root_utils::check_and_handle_new_root(callback) +/// | +/// v +/// ReplayStage::handle_new_root -> root_utils::set_bank_forks_root(callback) -> callback() +#[allow(clippy::too_many_arguments)] +pub fn check_and_handle_new_root( + parent_slot: Slot, + new_root: Slot, + accounts_background_request_sender: &AbsRequestSender, + highest_super_majority_root: Option, + bank_notification_sender: &Option, + drop_bank_sender: &Sender>, + blockstore: &Blockstore, + leader_schedule_cache: &Arc, + bank_forks: &RwLock, + rpc_subscriptions: &Arc, + my_pubkey: &Pubkey, + has_new_vote_been_rooted: &mut bool, + voted_signatures: &mut Vec, + callback: CB, +) -> Result<(), SetRootError> +where + CB: FnOnce(&BankForks), +{ + // get the root bank before squash + let root_bank = bank_forks + .read() + .unwrap() + .get(new_root) + .expect("Root bank doesn't exist"); + let mut rooted_banks = root_bank.parents(); + let oldest_parent = rooted_banks.last().map(|last| last.parent_slot()); + rooted_banks.push(root_bank.clone()); + let rooted_slots: Vec<_> = rooted_banks.iter().map(|bank| bank.slot()).collect(); + // The following differs from rooted_slots by including the parent slot of the oldest parent bank. + let rooted_slots_with_parents = bank_notification_sender + .as_ref() + .is_some_and(|sender| sender.should_send_parents) + .then(|| { + let mut new_chain = rooted_slots.clone(); + new_chain.push(oldest_parent.unwrap_or(parent_slot)); + new_chain + }); + + // Call leader schedule_cache.set_root() before blockstore.set_root() because + // bank_forks.root is consumed by repair_service to update gossip, so we don't want to + // get shreds for repair on gossip before we update leader schedule, otherwise they may + // get dropped. + leader_schedule_cache.set_root(rooted_banks.last().unwrap()); + blockstore + .set_roots(rooted_slots.iter()) + .expect("Ledger set roots failed"); + set_bank_forks_root( + new_root, + bank_forks, + accounts_background_request_sender, + highest_super_majority_root, + has_new_vote_been_rooted, + voted_signatures, + drop_bank_sender, + callback, + )?; + blockstore.slots_stats.mark_rooted(new_root); + rpc_subscriptions.notify_roots(rooted_slots); + if let Some(sender) = bank_notification_sender { + sender + .sender + .send(BankNotification::NewRootBank(root_bank)) + .unwrap_or_else(|err| warn!("bank_notification_sender failed: {:?}", err)); + + if let Some(new_chain) = rooted_slots_with_parents { + sender + .sender + .send(BankNotification::NewRootedChain(new_chain)) + .unwrap_or_else(|err| warn!("bank_notification_sender failed: {:?}", err)); + } + } + info!("{} new root {}", my_pubkey, new_root); + Ok(()) +} + +/// Sets the bank forks root: +/// - Prune the program cache +/// - Prune bank forks and drop the removed banks +/// - Calls the callback for use in replay stage and tests +pub fn set_bank_forks_root( + new_root: Slot, + bank_forks: &RwLock, + accounts_background_request_sender: &AbsRequestSender, + highest_super_majority_root: Option, + has_new_vote_been_rooted: &mut bool, + voted_signatures: &mut Vec, + drop_bank_sender: &Sender>, + callback: CB, +) -> Result<(), SetRootError> +where + CB: FnOnce(&BankForks), +{ + bank_forks.read().unwrap().prune_program_cache(new_root); + let removed_banks = bank_forks.write().unwrap().set_root( + new_root, + accounts_background_request_sender, + highest_super_majority_root, + )?; + + drop_bank_sender + .send(removed_banks) + .unwrap_or_else(|err| warn!("bank drop failed: {:?}", err)); + + // Dropping the bank_forks write lock and reacquiring as a read lock is + // safe because updates to bank_forks are only made by a single thread. + // TODO(ashwin): Once PR #245 lands move this back to ReplayStage + let r_bank_forks = bank_forks.read().unwrap(); + let new_root_bank = &r_bank_forks[new_root]; + if !*has_new_vote_been_rooted { + for signature in voted_signatures.iter() { + if new_root_bank.get_signature_status(signature).is_some() { + *has_new_vote_been_rooted = true; + break; + } + } + if *has_new_vote_been_rooted { + std::mem::take(voted_signatures); + } + } + callback(&r_bank_forks); + Ok(()) +} diff --git a/votor/src/skip_timer.rs b/votor/src/skip_timer.rs new file mode 100644 index 0000000000..b051fb048b --- /dev/null +++ b/votor/src/skip_timer.rs @@ -0,0 +1,171 @@ +//! Controls the queueing and firing of skip timer events for use +//! in the event loop. +//! TODO: Make this mockable in event_handler for tests +use { + crate::{ + event::{VotorEvent, VotorEventSender}, + skip_timeout, BLOCKTIME, + }, + solana_ledger::leader_schedule_utils::{ + last_of_consecutive_leader_slots, remaining_slots_in_window, + }, + solana_sdk::clock::Slot, + std::{ + collections::{BinaryHeap, VecDeque}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, RwLock, + }, + thread::{Builder, JoinHandle}, + time::{Duration, Instant}, + }, +}; + +#[derive(Debug, Copy, Clone)] +struct SkipTimer { + id: u64, + next_fire: Instant, + interval: Duration, + remaining: u64, + start_slot: Slot, +} + +impl SkipTimer { + fn slot_to_fire(&self) -> Slot { + let end_slot = last_of_consecutive_leader_slots(self.start_slot); + end_slot + .checked_sub(self.remaining) + .unwrap() + .checked_add(1) + .unwrap() + } +} + +impl PartialEq for SkipTimer { + fn eq(&self, other: &Self) -> bool { + self.next_fire.eq(&other.next_fire) + } +} +impl Eq for SkipTimer {} +impl PartialOrd for SkipTimer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(other.next_fire.cmp(&self.next_fire)) + } +} +impl Ord for SkipTimer { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + other.next_fire.cmp(&self.next_fire) + } +} + +pub(crate) struct SkipTimerManager { + heap: BinaryHeap, + order: VecDeque, + max_timers: usize, + next_id: u64, +} + +impl SkipTimerManager { + pub(crate) fn new(max_timers: usize) -> Self { + SkipTimerManager { + heap: BinaryHeap::new(), + order: VecDeque::new(), + max_timers, + next_id: 0, + } + } + + pub(crate) fn set_timeouts(&mut self, start_slot: Slot) { + // Evict oldest if needed + if self.order.len() == self.max_timers { + if let Some(evict_id) = self.order.pop_front() { + self.heap = self.heap.drain().filter(|t| t.id != evict_id).collect(); + } + } + + let id = self.next_id; + self.next_id = self.next_id.wrapping_add(1); + + // To account for restarting from a snapshot in the middle of a leader window + // or from genesis, we compute the exact length of this leader window: + let remaining = remaining_slots_in_window(start_slot); + // TODO: should we change the first fire as well? + let next_fire = Instant::now().checked_add(skip_timeout(0)).unwrap(); + + let timer = SkipTimer { + id, + next_fire, + interval: BLOCKTIME, + remaining, + start_slot, + }; + self.order.push_back(id); + self.heap.push(timer); + } +} + +pub(crate) struct SkipTimerService { + t_skip_timer: JoinHandle<()>, +} + +impl SkipTimerService { + pub(crate) fn new( + exit: Arc, + max_timers: usize, + event_sender: VotorEventSender, + ) -> (Self, Arc>) { + let manager = Arc::new(RwLock::new(SkipTimerManager::new(max_timers))); + let manager_c = manager.clone(); + let t_skip_timer = Builder::new() + .name("solSkipTimer".to_string()) + .spawn(move || Self::timer_thread(exit, manager_c, event_sender)) + .unwrap(); + (Self { t_skip_timer }, manager) + } + + pub(crate) fn timer_thread( + exit: Arc, + manager: Arc>, + event_sender: VotorEventSender, + ) { + while !exit.load(Ordering::Relaxed) { + let now = Instant::now(); + + // Fire all timers that are due + let mut manager_w = manager.write().unwrap(); + while let Some(mut timer) = manager_w.heap.peek().copied() { + if timer.next_fire <= now { + manager_w.heap.pop(); + + // Send timeout event + // TODO: handle error + let slot = timer.slot_to_fire(); + event_sender.send(VotorEvent::Timeout(slot)).unwrap(); + + timer.remaining = timer.remaining.checked_sub(1).unwrap(); + if timer.remaining > 0 { + timer.next_fire = timer.next_fire.checked_add(timer.interval).unwrap(); + manager_w.heap.push(timer); + } else { + // Remove from order list + manager_w.order.retain(|&x| x != timer.id); + } + } else { + break; + } + } + + // Sleep until next timer or 100ms (small enough that a new timer added cannot fire) + let sleep_duration = manager_w + .heap + .peek() + .map(|t| t.next_fire.saturating_duration_since(Instant::now())) + .unwrap_or(Duration::from_millis(100)); + drop(manager_w); + std::thread::sleep(sleep_duration); + } + } + pub(crate) fn join(self) -> std::thread::Result<()> { + self.t_skip_timer.join() + } +} diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs new file mode 100644 index 0000000000..3b47c9cf52 --- /dev/null +++ b/votor/src/vote_history.rs @@ -0,0 +1,304 @@ +use { + super::vote_history_storage::{ + Result, SavedVoteHistory, SavedVoteHistoryVersions, VoteHistoryStorage, + }, + solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey, signature::Keypair}, + solana_votor_messages::{bls_message::Block, vote::Vote}, + std::collections::{hash_map::Entry, HashMap, HashSet}, + thiserror::Error, +}; + +pub const VOTE_THRESHOLD_SIZE: f64 = 2f64 / 3f64; + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(PartialEq, Eq, Debug, Default, Clone, Copy, Serialize, Deserialize)] +pub(crate) enum BlockhashStatus { + /// No vote since restart + #[default] + Uninitialized, + /// Non voting validator + NonVoting, + /// Hot spare validator + HotSpare, + /// Successfully generated vote tx with blockhash + Blockhash(Slot, Hash), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum VoteHistoryVersions { + Current(VoteHistory), +} +impl VoteHistoryVersions { + pub fn new_current(vote_history: VoteHistory) -> Self { + Self::Current(vote_history) + } + + pub fn convert_to_current(self) -> VoteHistory { + match self { + VoteHistoryVersions::Current(vote_history) => vote_history, + } + } +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "9iyX7m9Mox17wuF3db86JNuXT5vjDzLuMxomEbHZcHLi") +)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)] +pub struct VoteHistory { + /// The validator identity that cast votes + pub node_pubkey: Pubkey, + + /// The slots which this node has cast either a notarization or skip vote + voted: HashSet, + + /// The blocks for which this node has cast a notarization vote + /// In the format of slot, block_id, bank_hash + voted_notar: HashMap, + + /// The blocks for which this node has cast a notarization fallback + /// vote in this slot + voted_notar_fallback: HashMap>, + + /// The slots for which this node has cast a skip fallback vote + voted_skip_fallback: HashSet, + + /// The slots in which this node has cast at least one of: + /// - `SkipVote` + /// - `SkipFallback` + /// - `NotarizeFallback` + skipped: HashSet, + + /// The slots for which this node has cast a finalization vote. This node + /// will not cast any additional votes for these slots + its_over: HashSet, + + /// All votes cast for a `slot`, for use in refresh + votes_cast: HashMap>, + + /// Blocks which have a notarization certificate via the certificate pool + notarized_blocks: HashSet, + + /// Slots which have a parent ready condition via the certificate pool + parent_ready_slots: HashMap>, + + /// The latest root set by the voting loop. The above structures will not + /// contain votes for slots before `root` + root: Slot, +} + +impl VoteHistory { + pub fn new(node_pubkey: Pubkey, root: Slot) -> Self { + Self { + node_pubkey, + root, + ..Self::default() + } + } + + /// Have we cast a notarization or skip vote for `slot` + pub fn voted(&self, slot: Slot) -> bool { + assert!(slot >= self.root); + self.voted.contains(&slot) + } + + /// The block for which we voted notarize in slot `slot` + pub fn voted_notar(&self, slot: Slot) -> Option { + assert!(slot >= self.root); + self.voted_notar.get(&slot).copied() + } + + /// Whether we voted notarize fallback in `slot` for block `(block_id, bank_hash)` + pub fn voted_notar_fallback(&self, slot: Slot, block_id: Hash) -> bool { + assert!(slot >= self.root); + self.voted_notar_fallback + .get(&slot) + .is_some_and(|v| v.contains(&block_id)) + } + + /// Whether we voted skip fallback for `slot` + pub fn voted_skip_fallback(&self, slot: Slot) -> bool { + assert!(slot >= self.root); + self.voted_skip_fallback.contains(&slot) + } + + /// Have we cast any skip vote variation for `slot` + pub fn skipped(&self, slot: Slot) -> bool { + assert!(slot >= self.root); + self.skipped.contains(&slot) + } + + /// Have we casted a finalization vote for `slot` + pub fn its_over(&self, slot: Slot) -> bool { + assert!(slot >= self.root); + self.its_over.contains(&slot) + } + + /// All votes cast since `slot` including `slot`, for use in + /// refresh + pub fn votes_cast_since(&self, slot: Slot) -> Vec { + self.votes_cast + .iter() + .filter(|(&s, _)| s >= slot) + .flat_map(|(_, votes)| votes.iter()) + .cloned() + .collect() + } + + /// Have we casted a bad window vote for `slot`: + /// - Skip + /// - Notarize fallback + /// - Skip fallback + pub fn bad_window(&self, slot: Slot) -> bool { + assert!(slot >= self.root); + self.skipped.contains(&slot) + || self.voted_notar_fallback.contains_key(&slot) + || self.voted_skip_fallback.contains(&slot) + } + + pub fn is_block_notarized(&self, block: &Block) -> bool { + self.notarized_blocks.contains(block) + } + + pub fn is_parent_ready(&self, slot: Slot, parent: &Block) -> bool { + self.parent_ready_slots + .get(&slot) + .is_some_and(|ps| ps.contains(parent)) + } + + /// The latest root slot set by the voting loop + pub fn root(&self) -> Slot { + self.root + } + + /// Add a new vote to the voting history + pub fn add_vote(&mut self, vote: Vote) { + assert!(vote.slot() >= self.root); + // TODO: these assert!s are for my debugging, can consider removing + // in final version + match vote { + Vote::Notarize(vote) => { + assert!(self.voted.insert(vote.slot())); + assert!(self + .voted_notar + .insert(vote.slot(), *vote.block_id()) + .is_none()); + } + Vote::Finalize(vote) => { + assert!(!self.skipped(vote.slot())); + self.its_over.insert(vote.slot()); + } + Vote::Skip(vote) => { + self.voted.insert(vote.slot()); + self.skipped.insert(vote.slot()); + } + Vote::NotarizeFallback(vote) => { + assert!(self.voted(vote.slot())); + assert!(!self.its_over(vote.slot())); + self.skipped.insert(vote.slot()); + self.voted_notar_fallback + .entry(vote.slot()) + .or_default() + .insert(*vote.block_id()); + } + Vote::SkipFallback(vote) => { + assert!(self.voted(vote.slot())); + assert!(!self.its_over(vote.slot())); + self.skipped.insert(vote.slot()); + self.voted_skip_fallback.insert(vote.slot()); + } + } + self.votes_cast.entry(vote.slot()).or_default().push(vote); + } + + /// Add a new notarized block + pub fn add_block_notarized(&mut self, block @ (slot, _): Block) { + if slot < self.root { + return; + } + self.notarized_blocks.insert(block); + } + + /// Add a new parent ready slot + /// + /// Returns true if the insertion was successful and this was the + /// first parent ready for this slot, indicating we should set timeouts. + pub fn add_parent_ready(&mut self, slot: Slot, parent: Block) -> bool { + if slot < self.root { + return false; + } + match self.parent_ready_slots.entry(slot) { + Entry::Occupied(mut entry) => { + entry.get_mut().insert(parent); + false + } + Entry::Vacant(entry) => { + entry.insert(HashSet::from([parent])); + true + } + } + } + + /// Sets the new root slot and cleans up outdated slots < `root` + pub fn set_root(&mut self, root: Slot) { + self.root = root; + self.voted.retain(|s| *s >= root); + self.voted_notar.retain(|s, _| *s >= root); + self.voted_notar_fallback.retain(|s, _| *s >= root); + self.voted_skip_fallback.retain(|s| *s >= root); + self.skipped.retain(|s| *s >= root); + self.its_over.retain(|s| *s >= root); + self.votes_cast.retain(|s, _| *s >= root); + self.notarized_blocks.retain(|(s, _)| *s >= root); + self.parent_ready_slots.retain(|s, _| *s >= root); + } + + #[allow(dead_code)] + /// Save the vote history to `vote_history_storage` signed by `node_keypair` + pub fn save( + &self, + vote_history_storage: &dyn VoteHistoryStorage, + node_keypair: &Keypair, + ) -> Result<()> { + let saved_vote_history = SavedVoteHistory::new(self, node_keypair)?; + vote_history_storage.store(&SavedVoteHistoryVersions::from(saved_vote_history))?; + Ok(()) + } + + /// Restore the saved vote history from `vote_history_storage` for `node_pubkey` + pub fn restore( + vote_history_storage: &dyn VoteHistoryStorage, + node_pubkey: &Pubkey, + ) -> Result { + vote_history_storage.load(node_pubkey) + } +} + +#[derive(Error, Debug)] +pub enum VoteHistoryError { + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Serialization Error: {0}")] + SerializeError(#[from] bincode::Error), + + #[error("The signature on the saved vote history is invalid")] + InvalidSignature, + + #[error("The vote history does not match this validator: {0}")] + WrongVoteHistory(String), + + #[error("The vote history is useless because of new hard fork: {0}")] + HardFork(Slot), +} + +impl VoteHistoryError { + pub fn is_file_missing(&self) -> bool { + if let VoteHistoryError::IoError(io_err) = &self { + io_err.kind() == std::io::ErrorKind::NotFound + } else { + false + } + } +} diff --git a/votor/src/vote_history_storage.rs b/votor/src/vote_history_storage.rs new file mode 100644 index 0000000000..1fca2a29f8 --- /dev/null +++ b/votor/src/vote_history_storage.rs @@ -0,0 +1,171 @@ +use { + super::vote_history::*, + solana_sdk::{ + pubkey::Pubkey, + signature::{Signature, Signer}, + }, + std::{ + fs::{self, File}, + io::{self, BufReader}, + path::PathBuf, + }, +}; + +pub type Result = std::result::Result; + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub enum SavedVoteHistoryVersions { + Current(SavedVoteHistory), +} + +impl SavedVoteHistoryVersions { + fn try_into_vote_history(&self, node_pubkey: &Pubkey) -> Result { + // This method assumes that `self` was just deserialized + assert_eq!(self.pubkey(), Pubkey::default()); + + let vote_history = match self { + SavedVoteHistoryVersions::Current(t) => { + if !t.signature.verify(node_pubkey.as_ref(), &t.data) { + return Err(VoteHistoryError::InvalidSignature); + } + bincode::deserialize(&t.data).map(VoteHistoryVersions::Current) + } + }; + vote_history + .map_err(|e| e.into()) + .and_then(|vote_history: VoteHistoryVersions| { + let vote_history = vote_history.convert_to_current(); + if vote_history.node_pubkey != *node_pubkey { + return Err(VoteHistoryError::WrongVoteHistory(format!( + "node_pubkey is {:?} but found vote history for {:?}", + node_pubkey, vote_history.node_pubkey + ))); + } + Ok(vote_history) + }) + } + + fn serialize_into(&self, file: &mut File) -> Result<()> { + bincode::serialize_into(file, self).map_err(|e| e.into()) + } + + fn pubkey(&self) -> Pubkey { + match self { + SavedVoteHistoryVersions::Current(t) => t.node_pubkey, + } + } +} + +impl From for SavedVoteHistoryVersions { + fn from(vote_history: SavedVoteHistory) -> SavedVoteHistoryVersions { + SavedVoteHistoryVersions::Current(vote_history) + } +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "2kq63kt6dJvJaUG7c1jGazLKeGXZc5yN3GDocMz8c5jB") +)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct SavedVoteHistory { + signature: Signature, + #[serde(with = "serde_bytes")] + data: Vec, + #[serde(skip)] + node_pubkey: Pubkey, +} + +impl SavedVoteHistory { + pub fn new(vote_history: &VoteHistory, keypair: &T) -> Result { + let node_pubkey = keypair.pubkey(); + if vote_history.node_pubkey != node_pubkey { + return Err(VoteHistoryError::WrongVoteHistory(format!( + "node_pubkey is {:?} but found vote history for {:?}", + node_pubkey, vote_history.node_pubkey + ))); + } + + let data = bincode::serialize(&vote_history)?; + let signature = keypair.sign_message(&data); + Ok(Self { + signature, + data, + node_pubkey, + }) + } +} + +pub trait VoteHistoryStorage: Sync + Send { + fn load(&self, node_pubkey: &Pubkey) -> Result; + fn store(&self, saved_vote_history: &SavedVoteHistoryVersions) -> Result<()>; +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct NullVoteHistoryStorage {} + +impl VoteHistoryStorage for NullVoteHistoryStorage { + fn load(&self, _node_pubkey: &Pubkey) -> Result { + Err(VoteHistoryError::IoError(io::Error::new( + io::ErrorKind::Other, + "NullVoteHistoryStorage::load() not available", + ))) + } + + fn store(&self, _saved_vote_history: &SavedVoteHistoryVersions) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct FileVoteHistoryStorage { + pub vote_history_path: PathBuf, +} + +impl FileVoteHistoryStorage { + pub fn new(vote_history_path: PathBuf) -> Self { + Self { vote_history_path } + } + + pub fn filename(&self, node_pubkey: &Pubkey) -> PathBuf { + self.vote_history_path + .join(format!("vote_history-{node_pubkey}")) + .with_extension("bin") + } +} + +impl VoteHistoryStorage for FileVoteHistoryStorage { + fn load(&self, node_pubkey: &Pubkey) -> Result { + let filename = self.filename(node_pubkey); + trace!("load {}", filename.display()); + + // Ensure to create parent dir here, because restore() precedes save() always + fs::create_dir_all(filename.parent().unwrap())?; + + // New format + let file = File::open(&filename)?; + let mut stream = BufReader::new(file); + + bincode::deserialize_from(&mut stream) + .map_err(|e| e.into()) + .and_then(|t: SavedVoteHistoryVersions| t.try_into_vote_history(node_pubkey)) + } + + fn store(&self, saved_vote_history: &SavedVoteHistoryVersions) -> Result<()> { + let pubkey = saved_vote_history.pubkey(); + let filename = self.filename(&pubkey); + trace!("store: {}", filename.display()); + let new_filename = filename.with_extension("bin.new"); + + { + // overwrite anything if exists + let mut file = File::create(&new_filename)?; + saved_vote_history.serialize_into(&mut file)?; + // file.sync_all() hurts performance; pipeline sync-ing and submitting votes to the cluster! + } + fs::rename(&new_filename, &filename)?; + // self.path.parent().sync_all() hurts performance same as the above sync + Ok(()) + } +} diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs new file mode 100644 index 0000000000..3afa9354b4 --- /dev/null +++ b/votor/src/voting_utils.rs @@ -0,0 +1,278 @@ +use { + crate::{ + commitment::AlpenglowCommitmentAggregationData, + vote_history::VoteHistory, + vote_history_storage::{SavedVoteHistory, SavedVoteHistoryVersions}, + }, + crossbeam_channel::{SendError, Sender}, + solana_bls_signatures::{keypair::Keypair as BLSKeypair, BlsError, Pubkey as BLSPubkey}, + solana_runtime::{ + bank::Bank, root_bank_cache::RootBankCache, vote_sender_types::BLSVerifiedMessageSender, + }, + solana_sdk::{ + clock::Slot, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, + transaction::Transaction, + }, + solana_votor_messages::{ + bls_message::{BLSMessage, CertificateMessage, VoteMessage, BLS_KEYPAIR_DERIVE_SEED}, + vote::Vote, + }, + std::{collections::HashMap, sync::Arc}, + thiserror::Error, +}; + +// TODO(ashwin): This will be removed in PR #245 +const MAX_VOTE_SIGNATURES: usize = 200; + +#[derive(Debug)] +pub enum GenerateVoteTxResult { + // non voting validator, not eligible for refresh + // until authorized keypair is overriden + NonVoting, + // hot spare validator, not eligble for refresh + // until set identity is invoked + HotSpare, + // failed generation, eligible for refresh + Failed, + // no rank found. + NoRankFound, + // Generated a vote transaction + Tx(Transaction), + // Generated a BLS message + BLSMessage(BLSMessage), +} + +impl GenerateVoteTxResult { + pub fn is_non_voting(&self) -> bool { + matches!(self, Self::NonVoting) + } + + pub fn is_hot_spare(&self) -> bool { + matches!(self, Self::HotSpare) + } +} + +pub enum BLSOp { + PushVote { + bls_message: Arc, + slot: Slot, + saved_vote_history: SavedVoteHistoryVersions, + }, + PushCertificate { + certificate: Arc, + }, +} + +#[derive(Debug, Error)] +pub enum VoteError { + #[error("Unable to generate bls vote message")] + GenerationError(Box), + + #[error("Unable to send to certificate pool")] + CertificatePoolError(#[from] SendError<()>), +} + +/// Context required to construct vote transactions +pub struct VotingContext { + pub vote_history: VoteHistory, + pub vote_account_pubkey: Pubkey, + pub identity_keypair: Arc, + pub authorized_voter_keypairs: Arc>>>, + // The BLS keypair should always change with authorized_voter_keypairs. + pub derived_bls_keypairs: HashMap>, + pub has_new_vote_been_rooted: bool, + pub own_vote_sender: BLSVerifiedMessageSender, + pub bls_sender: Sender, + pub commitment_sender: Sender, + pub wait_to_vote_slot: Option, + pub voted_signatures: Vec, + pub root_bank_cache: RootBankCache, +} + +pub fn get_bls_keypair( + context: &mut VotingContext, + authorized_voter_keypair: &Arc, +) -> Result, BlsError> { + let pubkey = authorized_voter_keypair.pubkey(); + if let Some(existing) = context.derived_bls_keypairs.get(&pubkey) { + return Ok(existing.clone()); + } + + let bls_keypair = Arc::new(BLSKeypair::derive_from_signer( + authorized_voter_keypair, + BLS_KEYPAIR_DERIVE_SEED, + )?); + + context + .derived_bls_keypairs + .insert(pubkey, bls_keypair.clone()); + + Ok(bls_keypair) +} + +pub fn generate_vote_tx( + vote: &Vote, + bank: &Bank, + context: &mut VotingContext, +) -> GenerateVoteTxResult { + let vote_account_pubkey = context.vote_account_pubkey; + let authorized_voter_keypair; + let bls_pubkey_in_vote_account; + { + let authorized_voter_keypairs = context.authorized_voter_keypairs.read().unwrap(); + if !bank.is_startup_verification_complete() { + info!("startup verification incomplete, so unable to vote"); + return GenerateVoteTxResult::Failed; + } + if authorized_voter_keypairs.is_empty() { + return GenerateVoteTxResult::NonVoting; + } + if let Some(slot) = context.wait_to_vote_slot { + if vote.slot() < slot { + return GenerateVoteTxResult::Failed; + } + } + let Some(vote_account) = bank.get_vote_account(&context.vote_account_pubkey) else { + warn!("Vote account {vote_account_pubkey} does not exist. Unable to vote"); + return GenerateVoteTxResult::Failed; + }; + let Some(vote_state) = vote_account.alpenglow_vote_state() else { + warn!( + "Vote account {vote_account_pubkey} does not have an Alpenglow vote state. Unable to vote", + ); + return GenerateVoteTxResult::Failed; + }; + if *vote_state.node_pubkey() != context.identity_keypair.pubkey() { + info!( + "Vote account node_pubkey mismatch: {} (expected: {}). Unable to vote", + vote_state.node_pubkey(), + context.identity_keypair.pubkey() + ); + return GenerateVoteTxResult::HotSpare; + } + bls_pubkey_in_vote_account = match vote_account.bls_pubkey() { + None => { + panic!( + "No BLS pubkey in vote account {}", + context.identity_keypair.pubkey() + ); + } + Some(key) => *key, + }; + + let Some(authorized_voter_pubkey) = vote_state.get_authorized_voter(bank.epoch()) else { + warn!("Vote account {vote_account_pubkey} has no authorized voter for epoch {}. Unable to vote", + bank.epoch() + ); + return GenerateVoteTxResult::Failed; + }; + + let Some(keypair) = authorized_voter_keypairs + .iter() + .find(|keypair| keypair.pubkey() == authorized_voter_pubkey) + else { + warn!( + "The authorized keypair {authorized_voter_pubkey} for vote account \ + {vote_account_pubkey} is not available. Unable to vote" + ); + return GenerateVoteTxResult::NonVoting; + }; + + authorized_voter_keypair = keypair.clone(); + } + + let bls_keypair = get_bls_keypair(context, &authorized_voter_keypair) + .unwrap_or_else(|e| panic!("Failed to derive my own BLS keypair: {e:?}")); + let my_bls_pubkey: BLSPubkey = bls_keypair.public.into(); + if my_bls_pubkey != bls_pubkey_in_vote_account { + panic!( + "Vote account bls_pubkey mismatch: {:?} (expected: {:?}). Unable to vote", + bls_pubkey_in_vote_account, my_bls_pubkey + ); + } + let vote_serialized = bincode::serialize(&vote).unwrap(); + let signature = authorized_voter_keypair.sign_message(&vote_serialized); + if !context.has_new_vote_been_rooted { + context.voted_signatures.push(signature); + if context.voted_signatures.len() > MAX_VOTE_SIGNATURES { + context.voted_signatures.remove(0); + } + } else { + context.voted_signatures.clear(); + } + + let Some(epoch_stakes) = bank.epoch_stakes(bank.epoch()) else { + panic!( + "The bank {} doesn't have its own epoch_stakes for {}", + bank.slot(), + bank.epoch() + ); + }; + let Some(my_rank) = epoch_stakes + .bls_pubkey_to_rank_map() + .get_rank(&my_bls_pubkey) + else { + return GenerateVoteTxResult::NoRankFound; + }; + GenerateVoteTxResult::BLSMessage(BLSMessage::Vote(VoteMessage { + vote: *vote, + signature: bls_keypair.sign(&vote_serialized).into(), + rank: *my_rank, + })) +} + +/// Send an alpenglow vote as a BLSMessage +/// `bank` will be used for: +/// - startup verification +/// - vote account checks +/// - authorized voter checks +/// +/// We also update the vote history and send the vote to +/// the certificate pool thread for ingestion. +/// +/// Returns false if we are currently a non-voting node +pub(crate) fn insert_vote_and_create_bls_message( + my_pubkey: &Pubkey, + vote: Vote, + is_refresh: bool, + context: &mut VotingContext, +) -> Result { + // Update and save the vote history + if !is_refresh { + context.vote_history.add_vote(vote); + } + + let bank = context.root_bank_cache.root_bank(); + let bls_message = match generate_vote_tx(&vote, &bank, context) { + GenerateVoteTxResult::BLSMessage(bls_message) => bls_message, + e => { + return Err(VoteError::GenerationError(Box::new(e))); + } + }; + context + .own_vote_sender + .send(bls_message.clone()) + .map_err(|_| SendError(()))?; + + // TODO: for refresh votes use a different BLSOp so we don't have to rewrite the same vote history to file + let saved_vote_history = + SavedVoteHistory::new(&context.vote_history, &context.identity_keypair).unwrap_or_else( + |err| { + error!( + "{my_pubkey}: Unable to create saved vote history: {:?}", + err + ); + // TODO: maybe unify this with exit flag instead + std::process::exit(1); + }, + ); + + // Return vote for sending + Ok(BLSOp::PushVote { + bls_message: Arc::new(bls_message), + slot: vote.slot(), + saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), + }) +} diff --git a/votor/src/votor.rs b/votor/src/votor.rs new file mode 100644 index 0000000000..27d7893b36 --- /dev/null +++ b/votor/src/votor.rs @@ -0,0 +1,276 @@ +//! The entrypoint into votor the module responsible for voting, rooting, and notifying +//! the core to create a new block. +//! +//! Votor +//! ┌────────────────────────────────────────────────────────────────────────────┐ +//! │ │ +//! │ Push Certificate │ +//! │ ┌───────────────────────────────────────────────────────────────────│────────┐ +//! │ │ Parent Ready │ │ +//! │ │ Standstill │ │ +//! │ │ Finalized │ │ +//! │ │ Block Notarized │ │ +//! │ │ ┌─────────Safe To Notar/Skip───┐ Push │ │ +//! │ │ │ Produce Window │ Vote │ │ +//! │ │ │ │ ┌────────────────────────│──────┐ │ +//! │ │ │ │ │ │ ┌────▼─▼───────┐ +//! │ │ │ │ │ │ │Voting Service│ +//! │ │ │ │ │ │ └──────────────┘ +//! │ │ │ │ │ │ +//! │ ┌────┼─────────┼───────────────┐ │ │ │ +//! │ │ │ │ │ Block │ ┌────────────────────┐ +//! │ │ Certificate Pool Service │ │ │ ┌─────────────────────│─┼ Replay / Broadcast │ +//! │ │ │ │ │ │ │ └────────────────────┘ +//! │ │ ┌──────────────────────────┐ │ │ │ │ │ +//! │ │ │ │ │ │ │ │ │ +//! │ │ │ Certificate Pool │ │ │ │ │ │ +//! │ │ │ ┌────────────────────┐ │ │ ┌────▼─┼──▼───────┐ Start │ +//! │ │ │ │Parent ready tracker│ │ │ Vote │ │ Leader window ┌──────────────────────┐ +//! │ │ │ └────────────────────┘ │ ◄─────────┼ Event Handler ┼─────────────│─► Block creation loop │ +//! │ │ └──────────────────────────┘ │ │ │ │ └──────────────────────┘ +//! │ │ │ └─▲───────────┬───┘ │ +//! │ └──────────────────────────────┘ │ │ │ +//! │ Timeout │ │ │ +//! │ │ │ Set Timeouts │ +//! │ │ │ │ +//! │ ┌───────────────────┴┐ ┌────▼───────────────┐ │ +//! │ │ │ │ │ │ +//! │ │ Skip Timer Service ┼─────┼ Skip timer Manager │ │ +//! │ │ │ │ │ │ +//! │ └────────────────────┘ └────────────────────┘ │ +//! └────────────────────────────────────────────────────────────────────────────┘ +//! +use { + crate::{ + certificate_pool_service::{CertificatePoolContext, CertificatePoolService}, + commitment::AlpenglowCommitmentAggregationData, + event::{LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, + event_handler::{EventHandler, EventHandlerContext}, + root_utils::RootContext, + skip_timer::SkipTimerService, + vote_history::VoteHistory, + vote_history_storage::VoteHistoryStorage, + voting_utils::{BLSOp, VotingContext}, + }, + crossbeam_channel::Sender, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::{blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache}, + solana_pubkey::Pubkey, + solana_rpc::{ + optimistically_confirmed_bank_tracker::BankNotificationSenderConfig, + rpc_subscriptions::RpcSubscriptions, + }, + solana_runtime::{ + accounts_background_service::AbsRequestSender, + bank_forks::BankForks, + installed_scheduler_pool::BankWithScheduler, + root_bank_cache::RootBankCache, + vote_sender_types::{BLSVerifiedMessageReceiver, BLSVerifiedMessageSender}, + }, + solana_sdk::{clock::Slot, signature::Keypair, signer::Signer}, + solana_votor_messages::bls_message::{Certificate, CertificateMessage}, + std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Condvar, Mutex, RwLock, + }, + thread, + time::Duration, + }, +}; + +/// Communication with the block creation loop to notify leader window +#[derive(Default)] +pub struct LeaderWindowNotifier { + pub window_info: Mutex>, + pub window_notification: Condvar, +} + +/// Inputs to Votor +pub struct VotorConfig { + pub exit: Arc, + // Validator config + pub vote_account: Pubkey, + pub wait_to_vote_slot: Option, + pub wait_for_vote_to_start_leader: bool, + pub vote_history: VoteHistory, + pub vote_history_storage: Arc, + + // Shared state + pub authorized_voter_keypairs: Arc>>>, + pub blockstore: Arc, + pub bank_forks: Arc>, + pub cluster_info: Arc, + pub leader_schedule_cache: Arc, + pub rpc_subscriptions: Arc, + + // Senders / Notifiers + pub accounts_background_request_sender: AbsRequestSender, + pub bls_sender: Sender, + pub commitment_sender: Sender, + pub drop_bank_sender: Sender>, + pub bank_notification_sender: Option, + pub leader_window_notifier: Arc, + pub certificate_sender: Sender<(Certificate, CertificateMessage)>, + pub event_sender: VotorEventSender, + pub own_vote_sender: BLSVerifiedMessageSender, + + // Receivers + pub event_receiver: VotorEventReceiver, + pub bls_receiver: BLSVerifiedMessageReceiver, +} + +/// Context shared with block creation, replay, gossip, banking stage etc +pub(crate) struct SharedContext { + pub(crate) blockstore: Arc, + pub(crate) bank_forks: Arc>, + pub(crate) cluster_info: Arc, + pub(crate) rpc_subscriptions: Arc, + pub(crate) leader_window_notifier: Arc, + pub(crate) vote_history_storage: Arc, +} + +pub struct Votor { + // TODO: Just a placeholder for how migration could look like, + // will fix once we finish the strategy + #[allow(dead_code)] + start: Arc<(Mutex, Condvar)>, + + event_handler: EventHandler, + certificate_pool_service: CertificatePoolService, + skip_timer_service: SkipTimerService, +} + +impl Votor { + pub fn new(config: VotorConfig) -> Self { + let VotorConfig { + exit, + vote_account, + wait_to_vote_slot, + wait_for_vote_to_start_leader, + vote_history, + vote_history_storage, + authorized_voter_keypairs, + blockstore, + bank_forks, + cluster_info, + leader_schedule_cache, + rpc_subscriptions, + accounts_background_request_sender, + bls_sender, + commitment_sender, + drop_bank_sender, + bank_notification_sender, + leader_window_notifier, + certificate_sender, + event_sender, + event_receiver, + own_vote_sender, + bls_receiver, + } = config; + + let start = Arc::new((Mutex::new(false), Condvar::new())); + + let identity_keypair = cluster_info.keypair().clone(); + let my_pubkey = identity_keypair.pubkey(); + let has_new_vote_been_rooted = !wait_for_vote_to_start_leader; + + let shared_context = SharedContext { + blockstore: blockstore.clone(), + bank_forks: bank_forks.clone(), + cluster_info: cluster_info.clone(), + rpc_subscriptions, + leader_window_notifier, + vote_history_storage, + }; + + let voting_context = VotingContext { + vote_history, + vote_account_pubkey: vote_account, + identity_keypair: identity_keypair.clone(), + authorized_voter_keypairs, + derived_bls_keypairs: HashMap::new(), + has_new_vote_been_rooted, + own_vote_sender, + bls_sender: bls_sender.clone(), + commitment_sender: commitment_sender.clone(), + wait_to_vote_slot, + voted_signatures: vec![], + root_bank_cache: RootBankCache::new(bank_forks.clone()), + }; + + let root_context = RootContext { + leader_schedule_cache: leader_schedule_cache.clone(), + accounts_background_request_sender, + bank_notification_sender, + drop_bank_sender, + }; + + let (skip_timer_service, skip_timer) = + SkipTimerService::new(exit.clone(), 100, event_sender.clone()); + + let event_handler_context = EventHandlerContext { + exit: exit.clone(), + start: start.clone(), + event_receiver, + skip_timer, + shared_context, + voting_context, + root_context, + }; + + let cert_pool_context = CertificatePoolContext { + exit: exit.clone(), + start: start.clone(), + my_pubkey, + my_vote_pubkey: vote_account, + blockstore, + root_bank_cache: RootBankCache::new(bank_forks.clone()), + leader_schedule_cache, + bls_receiver, + bls_sender, + event_sender, + commitment_sender, + certificate_sender, + }; + + let event_handler = EventHandler::new(event_handler_context); + let certificate_pool_service = CertificatePoolService::new(cert_pool_context); + + Self { + start, + event_handler, + certificate_pool_service, + skip_timer_service, + } + } + + pub fn start_migration(&self) { + // TODO: evaluate once we have actual migration logic + let (lock, cvar) = &*self.start; + let mut started = lock.lock().unwrap(); + *started = true; + cvar.notify_all(); + } + + pub(crate) fn wait_for_migration_or_exit( + exit: &AtomicBool, + (lock, cvar): &(Mutex, Condvar), + ) { + let mut started = lock.lock().unwrap(); + while !*started { + if exit.load(Ordering::Relaxed) { + return; + } + // Add timeout to check for exit flag + (started, _) = cvar.wait_timeout(started, Duration::from_secs(5)).unwrap(); + } + } + + pub fn join(self) -> thread::Result<()> { + self.certificate_pool_service.join()?; + self.skip_timer_service.join()?; + self.event_handler.join() + } +} diff --git a/zk-keygen/Cargo.toml b/zk-keygen/Cargo.toml index fac3785608..ecc300b350 100644 --- a/zk-keygen/Cargo.toml +++ b/zk-keygen/Cargo.toml @@ -19,6 +19,7 @@ edition = { workspace = true } bs58 = { workspace = true } clap = { version = "3.1.5", features = ["cargo", "derive"] } dirs-next = { workspace = true } +solana-bls-signatures = { workspace = true } solana-clap-v3-utils = { workspace = true } solana-remote-wallet = { workspace = true, features = ["default"] } solana-seed-derivable = { workspace = true } diff --git a/zk-keygen/src/main.rs b/zk-keygen/src/main.rs index e63522038b..3d5163bd35 100644 --- a/zk-keygen/src/main.rs +++ b/zk-keygen/src/main.rs @@ -1,6 +1,7 @@ use { bip39::{Mnemonic, MnemonicType, Seed}, clap::{crate_description, crate_name, Arg, ArgMatches, Command, PossibleValue}, + solana_bls_signatures::{keypair::Keypair as BlsKeypair, Pubkey as BlsPubkey}, solana_clap_v3_utils::{ input_parsers::{signer::SignerSourceParserBuilder, STDOUT_OUTFILE_TOKEN}, keygen::{ @@ -36,6 +37,23 @@ fn output_encodable_key( Ok(()) } +// BLS keypair do not (yet) implement the `EncodableKey` trait, so handle it +// separately for now +fn write_bls_keypair( + keypair: &BlsKeypair, + outfile: &str, + source: &str, +) -> Result<(), Box> { + if outfile == STDOUT_OUTFILE_TOKEN { + let mut stdout = std::io::stdout(); + keypair.write_json(&mut stdout)?; + } else { + keypair.write_json_file(outfile)?; + println!("Wrote {source} to {outfile}"); + } + Ok(()) +} + fn app(crate_version: &str) -> Command { Command::new(crate_name!()) .about(crate_description!()) @@ -53,7 +71,7 @@ fn app(crate_version: &str) -> Command { .value_parser(clap::value_parser!(KeyType)) .value_name("TYPE") .required(true) - .help("The type of encryption key") + .help("The type of encryption key [possible values: elgamal, aes128, bls]") ) .arg( Arg::new("outfile") @@ -85,7 +103,8 @@ fn app(crate_version: &str) -> Command { .index(1) .takes_value(true) .value_parser([ - PossibleValue::new("elgamal") + PossibleValue::new("elgamal"), + PossibleValue::new("bls"), ]) .value_name("TYPE") .required(true) @@ -226,12 +245,35 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { ); } } + KeyType::Bls => { + if !silent { + eprintln!("Generating a new Bls keypair"); + } + + let bls_keypair = BlsKeypair::derive(seed.as_bytes())?; + if let Some(outfile) = outfile { + write_bls_keypair(&bls_keypair, outfile, "new BLS keypair") + .map_err(|err| format!("Unable to write {outfile}: {err}"))?; + } + + if !silent { + let phrase: &str = mnemonic.phrase(); + let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); + let bls_pubkey: BlsPubkey = bls_keypair.public.into(); + println!( + "{}\npubkey: {}\n{}\nSave this seed phrase{} to recover your new ElGamal keypair:\n{}\n{}", + ÷r, bls_pubkey, ÷r, passphrase_message, phrase, ÷r + ); + } + } } } ("pubkey", matches) => { let key_type = matches.try_get_one::("type")?.unwrap(); let key_type = if key_type == "elgamal" { KeyType::ElGamal + } else if key_type == "bls" { + KeyType::Bls } else { return Err("unsupported key type".into()); }; @@ -253,6 +295,13 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { let elgamal_pubkey = elgamal_keypair.pubkey(); println!("{elgamal_pubkey}"); } + KeyType::Bls => { + // TODO: BLS only supports JSON files for now + let bls_keypair_path = matches.get_one::("keypair").unwrap(); + let bls_keypair = BlsKeypair::read_json_file(bls_keypair_path)?; + let bls_pubkey: BlsPubkey = bls_keypair.public.into(); + println!("{bls_pubkey}"); + } _ => unreachable!(), } } @@ -295,6 +344,9 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { }; output_encodable_key(&key, outfile, "recovered AES128 key")?; } + KeyType::Bls => { + println!("Recovery of BLS keypairs is not yet supported") + } } } _ => unreachable!(), @@ -307,6 +359,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { enum KeyType { ElGamal, Aes128, + Bls, } impl KeyType { @@ -314,6 +367,7 @@ impl KeyType { match self { KeyType::ElGamal => "elgamal.json", KeyType::Aes128 => "aes128.json", + KeyType::Bls => "bls.json", } } } @@ -329,6 +383,7 @@ impl FromStr for KeyType { match s.as_str() { "elgamal" => Ok(Self::ElGamal), "aes128" => Ok(Self::Aes128), + "bls" => Ok(Self::Bls), _ => Err(KeyTypeError(s)), } } @@ -448,7 +503,50 @@ mod tests { } #[test] - fn test_pubkey() { + fn test_new_bls() { + let outfile_dir = tempdir().unwrap(); + // use `Pubkey::new_unique()` to generate names for temporary key files + let outfile_path = tmp_outfile_path(&outfile_dir, &Pubkey::new_unique().to_string()); + + // general success case + process_test_command(&[ + "solana-zk-keygen", + "new", + "bls", + "--outfile", + &outfile_path, + "--no-bip39-passphrase", + ]) + .unwrap(); + + // refuse to overwrite file + let result = process_test_command(&[ + "solana-zk-keygen", + "new", + "bls", + "--outfile", + &outfile_path, + "--no-bip39-passphrase", + ]) + .unwrap_err() + .to_string(); + + let expected = format!("Refusing to overwrite {outfile_path} without --force flag"); + assert_eq!(result, expected); + + // no outfile + process_test_command(&[ + "solana-keygen", + "new", + "bls", + "--no-bip39-passphrase", + "--no-outfile", + ]) + .unwrap(); + } + + #[test] + fn test_pubkey_elgamal() { let keypair_out_dir = tempdir().unwrap(); // use `Pubkey::new_unique()` to generate names for temporary key files let keypair_path = tmp_outfile_path(&keypair_out_dir, &Pubkey::new_unique().to_string()); @@ -458,4 +556,16 @@ mod tests { process_test_command(&["solana-keygen", "pubkey", "elgamal", &keypair_path]).unwrap(); } + + #[test] + fn test_pubkey_bls() { + let keypair_out_dir = tempdir().unwrap(); + // use `Pubkey::new_unique()` to generate names for temporary key files + let keypair_path = tmp_outfile_path(&keypair_out_dir, &Pubkey::new_unique().to_string()); + + let keypair = BlsKeypair::new(); + keypair.write_json_file(&keypair_path).unwrap(); + + process_test_command(&["solana-keygen", "pubkey", "bls", &keypair_path]).unwrap(); + } }