diff --git a/ci/localnet-sanity.sh b/ci/localnet-sanity.sh index c8fcfc0ca..d03dd9a81 100755 --- a/ci/localnet-sanity.sh +++ b/ci/localnet-sanity.sh @@ -152,6 +152,7 @@ startNodes() { addLogs=true fi initCompleteFiles=() + maybeExpectedGenesisBlockhash= for i in $(seq 0 $((${#nodes[@]} - 1))); do declare cmd=${nodes[$i]} @@ -160,7 +161,7 @@ startNodes() { rm -f "$initCompleteFile" initCompleteFiles+=("$initCompleteFile") fi - startNode "$i" "$cmd" + startNode "$i" "$cmd $maybeExpectedGenesisBlockhash" if $addLogs; then logs+=("$(getNodeLogFile "$i" "$cmd")") fi @@ -170,6 +171,14 @@ startNodes() { if [[ "$i" -eq 1 ]]; then SECONDS= waitForNodeToInit "$initCompleteFile" + + ( + source multinode-demo/common.sh + set -x + $solana_cli --keypair config/bootstrap-leader/identity-keypair.json \ + --url http://127.0.0.1:8899 get-genesis-blockhash + ) | tee genesis-blockhash.log + maybeExpectedGenesisBlockhash="--expected-genesis-blockhash $(tail -n1 genesis-blockhash.log)" fi done diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index bf5e7ca71..9727da1c6 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -96,6 +96,7 @@ pub enum WalletCommand { ClaimStorageReward(Pubkey, Pubkey), ShowStorageAccount(Pubkey), Deploy(String), + GetGenesisBlockhash, GetSlot, GetEpochInfo, GetTransactionCount, @@ -343,6 +344,7 @@ pub fn parse_command( .unwrap() .to_string(), )), + ("get-genesis-blockhash", Some(_matches)) => Ok(WalletCommand::GetGenesisBlockhash), ("get-slot", Some(_matches)) => Ok(WalletCommand::GetSlot), ("get-epoch-info", Some(_matches)) => Ok(WalletCommand::GetEpochInfo), ("get-transaction-count", Some(_matches)) => Ok(WalletCommand::GetTransactionCount), @@ -1086,6 +1088,11 @@ fn process_cancel(rpc_client: &RpcClient, config: &WalletConfig, pubkey: &Pubkey log_instruction_custom_error::(result) } +fn process_get_genesis_blockhash(rpc_client: &RpcClient) -> ProcessResult { + let genesis_blockhash = rpc_client.get_genesis_blockhash()?; + Ok(genesis_blockhash.to_string()) +} + fn process_get_slot(rpc_client: &RpcClient) -> ProcessResult { let slot = rpc_client.get_slot()?; Ok(slot.to_string()) @@ -1501,6 +1508,7 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { process_deploy(&rpc_client, config, program_location) } + WalletCommand::GetGenesisBlockhash => process_get_genesis_blockhash(&rpc_client), WalletCommand::GetSlot => process_get_slot(&rpc_client), WalletCommand::GetEpochInfo => process_get_epoch_info(&rpc_client), WalletCommand::GetTransactionCount => process_get_transaction_count(&rpc_client), @@ -2196,6 +2204,10 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .help("/path/to/program.o"), ), // TODO: Add "loader" argument; current default is bpf_loader ) + .subcommand( + SubCommand::with_name("get-genesis-blockhash") + .about("Get the genesis blockhash"), + ) .subcommand( SubCommand::with_name("get-slot") .about("Get current slot"), diff --git a/core/src/validator.rs b/core/src/validator.rs index 723b9fbbe..bd6498fcb 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -497,12 +497,13 @@ pub fn new_banks_from_blocktree( ) { let genesis_block = GenesisBlock::load(blocktree_path).expect("Failed to load genesis block"); let genesis_blockhash = genesis_block.hash(); + info!("genesis blockhash: {}", genesis_blockhash); if let Some(expected_genesis_blockhash) = expected_genesis_blockhash { if genesis_blockhash != expected_genesis_blockhash { error!( - "Genesis blockhash mismatch: expected {} but local genesis blockhash is {}", - expected_genesis_blockhash, genesis_blockhash, + "genesis blockhash mismatch: expected {}", + expected_genesis_blockhash ); error!( "Delete the ledger directory to continue: {:?}", diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index aff08efba..e2fefbc01 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -67,6 +67,9 @@ while [[ -n $1 ]]; do elif [[ $1 = --blockstream ]]; then args+=("$1" "$2") shift 2 + elif [[ $1 = --expected-genesis-blockhash ]]; then + args+=("$1" "$2") + shift 2 elif [[ $1 = --identity ]]; then identity_keypair_path=$2 args+=("$1" "$2") diff --git a/validator/src/lib.rs b/validator/src/lib.rs index a6fdb54d5..5d9442489 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -20,6 +20,7 @@ use std::io::{self, Read}; use std::net::{SocketAddr, TcpListener}; use std::path::{Path, PathBuf}; use std::process::exit; +use std::str::FromStr; use std::sync::Arc; use std::time::Instant; @@ -31,6 +32,12 @@ fn port_range_validator(port_range: String) -> Result<(), String> { } } +fn hash_validator(hash: String) -> Result<(), String> { + Hash::from_str(&hash) + .map(|_| ()) + .map_err(|e| format!("{:?}", e)) +} + static TRUCK: Emoji = Emoji("🚚 ", ""); static SPARKLE: Emoji = Emoji("✨ ", ""); @@ -171,7 +178,7 @@ fn initialize_ledger_path( .map(|addrs| addrs.0) .find(|rpc_addr| rpc_addr.ip() == entrypoint.gossip.ip()) .unwrap_or_else(|| { - eprintln!( + error!( "Entrypoint ({:?}) is not running the RPC service", entrypoint.gossip.ip() ); @@ -199,7 +206,7 @@ fn initialize_ledger_path( snapshot_package.parent().unwrap(), false, ) - .unwrap_or_else(|err| eprintln!("Warning: Unable to fetch snapshot: {:?}", err)); + .unwrap_or_else(|err| warn!("Unable to fetch snapshot: {:?}", err)); } match client.get_slot() { @@ -396,6 +403,14 @@ pub fn main() { .takes_value(false) .help("Use CUDA"), ) + .arg( + Arg::with_name("expected_genesis_blockhash") + .long("expected-genesis-blockhash") + .value_name("HASH") + .takes_value(true) + .validator(hash_validator) + .help("Require the genesis block have this blockhash"), + ) .get_matches(); if matches.is_present("cuda") { @@ -405,7 +420,7 @@ pub fn main() { let mut validator_config = ValidatorConfig::default(); let keypair = if let Some(identity) = matches.value_of("identity") { read_keypair(identity).unwrap_or_else(|err| { - eprintln!("{}: Unable to open keypair file: {}", err, identity); + error!("{}: Unable to open keypair file: {}", err, identity); exit(1); }) } else { @@ -414,7 +429,7 @@ pub fn main() { let voting_keypair = if let Some(identity) = matches.value_of("voting_keypair") { read_keypair(identity).unwrap_or_else(|err| { - eprintln!("{}: Unable to open keypair file: {}", err, identity); + error!("{}: Unable to open keypair file: {}", err, identity); exit(1); }) } else { @@ -422,7 +437,7 @@ pub fn main() { }; let storage_keypair = if let Some(storage_keypair) = matches.value_of("storage_keypair") { read_keypair(storage_keypair).unwrap_or_else(|err| { - eprintln!("{}: Unable to open keypair file: {}", err, storage_keypair); + error!("{}: Unable to open keypair file: {}", err, storage_keypair); exit(1); }) } else { @@ -471,7 +486,7 @@ pub fn main() { let snapshot_interval_slots = value_t_or_exit!(matches, "snapshot_interval_slots", usize); let snapshot_path = ledger_path.clone().join("snapshot"); fs::create_dir_all(&snapshot_path).unwrap_or_else(|err| { - eprintln!( + error!( "Failed to create snapshots directory {:?}: {}", snapshot_path, err ); @@ -495,7 +510,7 @@ pub fn main() { let entrypoint_addr = solana_netutil::parse_host_port(entrypoint) .expect("failed to parse entrypoint address"); let ip_addr = solana_netutil::get_public_ip_addr(&entrypoint_addr).unwrap_or_else(|err| { - eprintln!( + error!( "Failed to contact cluster entrypoint {} ({}): {}", entrypoint, entrypoint_addr, err ); @@ -543,7 +558,7 @@ pub fn main() { if let Some(port) = matches.value_of("rpc_port") { let port_number = port.to_string().parse().expect("integer"); if port_number == 0 { - eprintln!("Invalid RPC port requested: {:?}", port); + error!("Invalid RPC port requested: {:?}", port); exit(1); } node.info.rpc = SocketAddr::new(node.info.gossip.ip(), port_number); @@ -551,6 +566,10 @@ pub fn main() { tcp_ports = vec![port_number, port_number + 1]; }; + validator_config.expected_genesis_blockhash = matches + .value_of("expected_genesis_blockhash") + .map(|s| Hash::from_str(&s).unwrap()); + if let Some(ref cluster_entrypoint) = cluster_entrypoint { let udp_sockets = [ &node.sockets.gossip, @@ -583,21 +602,31 @@ pub fn main() { &udp_sockets, ); - let expected_genesis_blockhash = initialize_ledger_path( + let genesis_blockhash = initialize_ledger_path( cluster_entrypoint, &ledger_path, matches.is_present("no_snapshot_fetch"), ) .unwrap_or_else(|err| { - eprintln!("Failed to download ledger: {}", err); + error!("Failed to download ledger: {}", err); exit(1); }); - validator_config.expected_genesis_blockhash = Some(expected_genesis_blockhash); + + if let Some(expected_genesis_blockhash) = validator_config.expected_genesis_blockhash { + if expected_genesis_blockhash != genesis_blockhash { + error!( + "Genesis blockhash mismatch: expected {} but local genesis blockhash is {}", + expected_genesis_blockhash, genesis_blockhash, + ); + exit(1); + } + } + validator_config.expected_genesis_blockhash = Some(genesis_blockhash); } else { // Without a cluster entrypoint, ledger_path must already be present if !ledger_path.is_dir() { - eprintln!( - "Error: ledger directory does not exist or is not accessible: {:?}", + error!( + "ledger directory does not exist or is not accessible: {:?}", ledger_path ); exit(1); @@ -618,7 +647,7 @@ pub fn main() { if let Some(filename) = init_complete_file { File::create(filename).unwrap_or_else(|_| { - eprintln!("Unable to create: {}", filename); + error!("Unable to create: {}", filename); exit(1); }); }