From e2d6f01ad3c1fd35a391a09747f7378c5b655017 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Wed, 21 Aug 2019 18:16:40 -0700 Subject: [PATCH] solana-validator now verifies its genesis blockhash against the cluster entrypoint (#5589) --- Cargo.lock | 1 + book/src/jsonrpc-api.md | 20 ++++++++++++++++++++ client/src/rpc_client.rs | 29 ++++++++++++++++++++++++++++- client/src/rpc_request.rs | 2 ++ core/src/rpc.rs | 12 ++++++++++++ core/src/rpc_service.rs | 4 ++++ core/src/validator.rs | 19 +++++++++++++++++++ core/tests/tvu.rs | 3 ++- multinode-demo/validator.sh | 16 ++++++---------- validator/Cargo.toml | 1 + validator/src/main.rs | 13 ++++++++++--- 11 files changed, 105 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a391e499b..c572b6a4d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3846,6 +3846,7 @@ dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-client 0.18.0-pre2", "solana-core 0.18.0-pre2", "solana-drone 0.18.0-pre2", "solana-logger 0.18.0-pre2", diff --git a/book/src/jsonrpc-api.md b/book/src/jsonrpc-api.md index d89db87f31..6dc3f8289c 100644 --- a/book/src/jsonrpc-api.md +++ b/book/src/jsonrpc-api.md @@ -26,6 +26,7 @@ Methods * [getBalance](#getbalance) * [getClusterNodes](#getclusternodes) * [getEpochInfo](#getepochinfo) +* [getGenesisBlockhash](#getgenesisblockhash) * [getLeaderSchedule](#getleaderschedule) * [getProgramAccounts](#getprogramaccounts) * [getRecentBlockhash](#getrecentblockhash) @@ -197,6 +198,25 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":{"epoch":3,"slotIndex":126,"slotsInEpoch":256},"id":1} ``` +--- +### getGenesisBlockhash +Returns the genesis block hash + +##### Parameters: +None + +##### Results: +* `string` - a Hash as base-58 encoded string + +##### Example: +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getGenesisBlockhash"}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":"GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC","id":1} +``` + --- ### getLeaderSchedule diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 065b51f429..6d3c287c9f 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -369,7 +369,7 @@ impl RpcClient { let blockhash = blockhash.parse().map_err(|err| { io::Error::new( io::ErrorKind::Other, - format!("GetRecentBlockhash parse failure: {:?}", err), + format!("GetRecentBlockhash hash parse failure: {:?}", err), ) })?; Ok((blockhash, fee_calculator)) @@ -397,6 +397,33 @@ impl RpcClient { )) } + pub fn get_genesis_blockhash(&self) -> io::Result { + let response = self + .client + .send(&RpcRequest::GetGenesisBlockhash, None, 0) + .map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("GetGenesisBlockhash request failure: {:?}", err), + ) + })?; + + let blockhash = serde_json::from_value::(response).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("GetGenesisBlockhash parse failure: {:?}", err), + ) + })?; + + let blockhash = blockhash.parse().map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("GetGenesisBlockhash hash parse failure: {:?}", err), + ) + })?; + Ok(blockhash) + } + pub fn poll_balance_with_timeout( &self, pubkey: &Pubkey, diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index c1748803c9..c992355b30 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -9,6 +9,7 @@ pub enum RpcRequest { GetAccountInfo, GetBalance, GetClusterNodes, + GetGenesisBlockhash, GetNumBlocksSinceSignatureConfirmation, GetProgramAccounts, GetRecentBlockhash, @@ -38,6 +39,7 @@ impl RpcRequest { RpcRequest::GetAccountInfo => "getAccountInfo", RpcRequest::GetBalance => "getBalance", RpcRequest::GetClusterNodes => "getClusterNodes", + RpcRequest::GetGenesisBlockhash => "getGenesisBlockhash", RpcRequest::GetNumBlocksSinceSignatureConfirmation => { "getNumBlocksSinceSignatureConfirmation" } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 43bc68b81e..62733d5e29 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -14,6 +14,7 @@ use solana_drone::drone::request_airdrop_transaction; use solana_runtime::bank::Bank; use solana_sdk::account::Account; use solana_sdk::fee_calculator::FeeCalculator; +use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signature; use solana_sdk::transaction::{self, Transaction}; @@ -213,6 +214,7 @@ fn verify_signature(input: &str) -> Result { pub struct Meta { pub request_processor: Arc>, pub cluster_info: Arc>, + pub genesis_blockhash: Hash, } impl Metadata for Meta {} @@ -298,6 +300,9 @@ pub trait RpcSol { #[rpc(meta, name = "getEpochInfo")] fn get_epoch_info(&self, _: Self::Metadata) -> Result; + #[rpc(meta, name = "getGenesisBlockhash")] + fn get_genesis_blockhash(&self, _: Self::Metadata) -> Result; + #[rpc(meta, name = "getLeaderSchedule")] fn get_leader_schedule(&self, _: Self::Metadata) -> Result>>; @@ -448,6 +453,11 @@ impl RpcSol for RpcSolImpl { }) } + fn get_genesis_blockhash(&self, meta: Self::Metadata) -> Result { + debug!("get_genesis_blockhash rpc request received"); + Ok(meta.genesis_blockhash.to_string()) + } + fn get_leader_schedule(&self, meta: Self::Metadata) -> Result>> { let bank = meta.request_processor.read().unwrap().bank(); Ok( @@ -718,6 +728,7 @@ pub mod tests { let meta = Meta { request_processor, cluster_info, + genesis_blockhash: Hash::default(), }; (io, meta, bank, blockhash, alice, leader_pubkey) } @@ -1060,6 +1071,7 @@ pub mod tests { cluster_info: Arc::new(RwLock::new(ClusterInfo::new_with_invalid_keypair( ContactInfo::default(), ))), + genesis_blockhash: Hash::default(), }; let req = diff --git a/core/src/rpc_service.rs b/core/src/rpc_service.rs index 16596bbbab..d2f40b7263 100644 --- a/core/src/rpc_service.rs +++ b/core/src/rpc_service.rs @@ -12,6 +12,7 @@ use jsonrpc_http_server::{ hyper, AccessControlAllowOrigin, DomainsValidation, RequestMiddleware, RequestMiddlewareAction, ServerBuilder, }; +use solana_sdk::hash::Hash; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; @@ -91,6 +92,7 @@ impl JsonRpcService { config: JsonRpcConfig, bank_forks: Arc>, ledger_path: &Path, + genesis_blockhash: Hash, validator_exit: &Arc>>, ) -> Self { info!("rpc bound to {:?}", rpc_addr); @@ -118,6 +120,7 @@ impl JsonRpcService { ServerBuilder::with_meta_extractor(io, move |_req: &hyper::Request| Meta { request_processor: request_processor_.clone(), cluster_info: cluster_info.clone(), + genesis_blockhash }).threads(4) .cors(DomainsValidation::AllowOnly(vec![ AccessControlAllowOrigin::Any, @@ -201,6 +204,7 @@ mod tests { JsonRpcConfig::default(), bank_forks, &PathBuf::from("farf"), + Hash::default(), &validator_exit, ); let thread = rpc_service.thread_hdl.thread(); diff --git a/core/src/validator.rs b/core/src/validator.rs index cb2f601de4..6e93d789c1 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -22,6 +22,7 @@ use crate::tpu::Tpu; use crate::tvu::{Sockets, Tvu}; use solana_metrics::datapoint_info; use solana_sdk::genesis_block::GenesisBlock; +use solana_sdk::hash::Hash; use solana_sdk::poh_config::PohConfig; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, KeypairUtil}; @@ -38,6 +39,7 @@ use std::thread::Result; pub struct ValidatorConfig { pub dev_sigverify_disabled: bool, pub dev_halt_at_slot: Option, + pub expected_genesis_blockhash: Option, pub voting_disabled: bool, pub blockstream_unix_socket: Option, pub storage_slots_per_turn: u64, @@ -54,6 +56,7 @@ impl Default for ValidatorConfig { Self { dev_sigverify_disabled: false, dev_halt_at_slot: None, + expected_genesis_blockhash: None, voting_disabled: false, blockstream_unix_socket: None, storage_slots_per_turn: DEFAULT_SLOTS_PER_TURN, @@ -136,6 +139,7 @@ impl Validator { info!("creating bank..."); let ( + genesis_blockhash, bank_forks, bank_forks_info, blocktree, @@ -144,6 +148,7 @@ impl Validator { leader_schedule_cache, poh_config, ) = new_banks_from_blocktree( + config.expected_genesis_blockhash, ledger_path, config.account_paths.clone(), config.snapshot_config.clone(), @@ -184,6 +189,7 @@ impl Validator { config.rpc_config.clone(), bank_forks.clone(), ledger_path, + genesis_blockhash, &validator_exit, )) }; @@ -471,12 +477,14 @@ fn adjust_ulimit_nofile() { } pub fn new_banks_from_blocktree( + expected_genesis_blockhash: Option, blocktree_path: &Path, account_paths: Option, snapshot_config: Option, verify_ledger: bool, dev_halt_at_slot: Option, ) -> ( + Hash, BankForks, Vec, Blocktree, @@ -486,6 +494,16 @@ pub fn new_banks_from_blocktree( PohConfig, ) { let genesis_block = GenesisBlock::load(blocktree_path).expect("Failed to load genesis block"); + let genesis_blockhash = genesis_block.hash(); + + if let Some(expected_genesis_blockhash) = expected_genesis_blockhash { + if genesis_blockhash != expected_genesis_blockhash { + panic!( + "Genesis blockhash mismatch: expected {} but local genesis blockhash is {}", + expected_genesis_blockhash, genesis_blockhash, + ); + } + } adjust_ulimit_nofile(); @@ -502,6 +520,7 @@ pub fn new_banks_from_blocktree( ); ( + genesis_blockhash, bank_forks, bank_forks_info, blocktree, diff --git a/core/tests/tvu.rs b/core/tests/tvu.rs index af13a05e1b..d56f05f8d6 100644 --- a/core/tests/tvu.rs +++ b/core/tests/tvu.rs @@ -91,6 +91,7 @@ fn test_replay() { let tvu_addr = target1.info.tvu; let ( + _genesis_blockhash, bank_forks, _bank_forks_info, blocktree, @@ -98,7 +99,7 @@ fn test_replay() { completed_slots_receiver, leader_schedule_cache, _, - ) = validator::new_banks_from_blocktree(&blocktree_path, None, None, true, None); + ) = validator::new_banks_from_blocktree(None, &blocktree_path, None, None, true, None); let working_bank = bank_forks.working_bank(); assert_eq!( working_bank.get_balance(&mint_keypair.pubkey()), diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index b1d751ae8d..8dee544c51 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -272,14 +272,6 @@ setup_validator_accounts() { while true; do rpc_url=$($solana_gossip get-rpc-url --entrypoint "$gossip_entrypoint") - if new_genesis_block; then - # If the genesis block has changed remove the now stale ledger and start all - # over again - ( - set -x - rm -rf "$ledger_dir" - ) - fi [[ -r "$identity_keypair_path" ]] || $solana_keygen new -o "$identity_keypair_path" [[ -r "$voting_keypair_path" ]] || $solana_keygen new -o "$voting_keypair_path" @@ -312,7 +304,7 @@ EOF exit $? fi - secs_to_next_genesis_poll=5 + secs_to_next_genesis_poll=60 while true; do if [[ -z $pid ]] || ! kill -0 "$pid"; then [[ -z $pid ]] || wait "$pid" @@ -326,9 +318,13 @@ EOF echo "Polling for new genesis block..." if new_genesis_block; then echo "############## New genesis detected, restarting ##############" + ( + set -x + rm -rf "$ledger_dir" + ) break fi - secs_to_next_genesis_poll=5 + secs_to_next_genesis_poll=60 fi done diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 635b3ebbf9..80fc92bec3 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -14,6 +14,7 @@ clap = "2.33.0" log = "0.4.8" reqwest = "0.9.19" serde_json = "1.0.40" +solana-client = { path = "../client", version = "0.18.0-pre2" } solana-core = { path = "../core", version = "0.18.0-pre2" } solana-drone = { path = "../drone", version = "0.18.0-pre2" } solana-logger = { path = "../logger", version = "0.18.0-pre2" } diff --git a/validator/src/main.rs b/validator/src/main.rs index 8530077615..20bf97b4a6 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1,6 +1,7 @@ use bzip2::bufread::BzDecoder; use clap::{crate_description, crate_name, crate_version, value_t, App, Arg}; use log::*; +use solana_client::rpc_client::RpcClient; use solana_core::bank_forks::SnapshotConfig; use solana_core::cluster_info::{Node, FULLNODE_PORT_RANGE}; use solana_core::contact_info::ContactInfo; @@ -11,6 +12,7 @@ use solana_core::service::Service; use solana_core::socketaddr; use solana_core::validator::{Validator, ValidatorConfig}; use solana_netutil::parse_port_range; +use solana_sdk::hash::Hash; use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil}; use solana_sdk::timing::Slot; use std::fs::{self, File}; @@ -87,7 +89,7 @@ fn initialize_ledger_path( gossip_addr: &SocketAddr, ledger_path: &Path, no_snapshot_fetch: bool, -) -> Result<(), String> { +) -> Result { let (nodes, _replicators) = discover( &entrypoint.gossip, Some(1), @@ -110,6 +112,10 @@ fn initialize_ledger_path( exit(1); }); + let genesis_blockhash = RpcClient::new_socket(rpc_addr) + .get_genesis_blockhash() + .map_err(|err| err.to_string())?; + fs::create_dir_all(ledger_path).map_err(|err| err.to_string())?; download_archive(&rpc_addr, "genesis.tar.bz2", ledger_path, true)?; @@ -119,7 +125,7 @@ fn initialize_ledger_path( .unwrap_or_else(|err| eprintln!("Warning: Unable to fetch snapshot: {:?}", err)); } - Ok(()) + Ok(genesis_blockhash) } fn main() { @@ -393,7 +399,7 @@ fn main() { .map(PathBuf::from); if let Some(ref entrypoint_addr) = cluster_entrypoint { - initialize_ledger_path( + let expected_genesis_blockhash = initialize_ledger_path( entrypoint_addr, &gossip_addr, &ledger_path, @@ -403,6 +409,7 @@ fn main() { eprintln!("Failed to download ledger: {}", err); exit(1); }); + validator_config.expected_genesis_blockhash = Some(expected_genesis_blockhash); } else { // Without a cluster entrypoint, ledger_path must already be present if !ledger_path.is_dir() {