From b7386f9d84e2028e013bf8a0e55624e672e28001 Mon Sep 17 00:00:00 2001 From: sakridge Date: Fri, 21 Feb 2020 18:42:24 -0800 Subject: [PATCH] Add --trusted-validator support for snapshot hash validation (#8390) --- core/src/snapshot_packager_service.rs | 54 +++++++++++++------------ core/src/validator.rs | 30 +++++++++++++- core/tests/bank_forks.rs | 1 + ledger-tool/src/main.rs | 1 + ledger/src/bank_forks.rs | 6 ++- local-cluster/tests/local_cluster.rs | 15 ++++++- multinode-demo/validator.sh | 3 ++ validator/src/main.rs | 57 +++++++++++++++++++++++++-- 8 files changed, 135 insertions(+), 32 deletions(-) diff --git a/core/src/snapshot_packager_service.rs b/core/src/snapshot_packager_service.rs index 6a164b19a..2b619cefa 100644 --- a/core/src/snapshot_packager_service.rs +++ b/core/src/snapshot_packager_service.rs @@ -28,33 +28,37 @@ impl SnapshotPackagerService { let cluster_info = cluster_info.clone(); let t_snapshot_packager = Builder::new() .name("solana-snapshot-packager".to_string()) - .spawn(move || loop { + .spawn(move || { let mut hashes = vec![]; - if exit.load(Ordering::Relaxed) { - break; - } - - match snapshot_package_receiver.recv_timeout(Duration::from_secs(1)) { - Ok(mut snapshot_package) => { - hashes.push((snapshot_package.root, snapshot_package.hash)); - // Only package the latest - while let Ok(new_snapshot_package) = snapshot_package_receiver.try_recv() { - snapshot_package = new_snapshot_package; - hashes.push((snapshot_package.root, snapshot_package.hash)); - } - if let Err(err) = archive_snapshot_package(&snapshot_package) { - warn!("Failed to create snapshot archive: {}", err); - } - while hashes.len() > MAX_SNAPSHOT_HASHES { - hashes.remove(0); - } - cluster_info - .write() - .unwrap() - .push_snapshot_hashes(hashes.clone()); + loop { + if exit.load(Ordering::Relaxed) { + break; + } + + match snapshot_package_receiver.recv_timeout(Duration::from_secs(1)) { + Ok(mut snapshot_package) => { + hashes.push((snapshot_package.root, snapshot_package.hash)); + // Only package the latest + while let Ok(new_snapshot_package) = + snapshot_package_receiver.try_recv() + { + snapshot_package = new_snapshot_package; + hashes.push((snapshot_package.root, snapshot_package.hash)); + } + if let Err(err) = archive_snapshot_package(&snapshot_package) { + warn!("Failed to create snapshot archive: {}", err); + } + while hashes.len() > MAX_SNAPSHOT_HASHES { + hashes.remove(0); + } + cluster_info + .write() + .unwrap() + .push_snapshot_hashes(hashes.clone()); + } + Err(RecvTimeoutError::Disconnected) => break, + Err(RecvTimeoutError::Timeout) => (), } - Err(RecvTimeoutError::Disconnected) => break, - Err(RecvTimeoutError::Timeout) => (), } }) .unwrap(); diff --git a/core/src/validator.rs b/core/src/validator.rs index 3459cf2bd..62b80d5c5 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -344,8 +344,36 @@ impl Validator { .set_entrypoint(entrypoint_info.clone()); } - // If the node was loaded from a snapshot, advertise it in gossip if let Some(snapshot_hash) = snapshot_hash { + if let Some(ref trusted_validators) = + config.snapshot_config.as_ref().unwrap().trusted_validators + { + let mut trusted = false; + for _ in 0..10 { + trusted = cluster_info + .read() + .unwrap() + .get_snapshot_hash(snapshot_hash.0) + .iter() + .any(|(pubkey, hash)| { + trusted_validators.contains(pubkey) && snapshot_hash.1 == *hash + }); + if trusted { + break; + } + sleep(Duration::from_secs(1)); + } + + if !trusted { + error!( + "The snapshot hash for slot {} is not published by your trusted validators: {:?}", + snapshot_hash.0, trusted_validators + ); + process::exit(1); + } + } + + // If the node was loaded from a snapshot, advertise it in gossip cluster_info .write() .unwrap() diff --git a/core/tests/bank_forks.rs b/core/tests/bank_forks.rs index 8458b26f4..ac4750bf7 100644 --- a/core/tests/bank_forks.rs +++ b/core/tests/bank_forks.rs @@ -55,6 +55,7 @@ mod tests { snapshot_interval_slots, snapshot_package_output_path: PathBuf::from(snapshot_output_path.path()), snapshot_path: PathBuf::from(snapshot_dir.path()), + trusted_validators: None, }; bank_forks.set_snapshot_config(Some(snapshot_config.clone())); SnapshotTestConfig { diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 4aebff057..475aedd7e 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -537,6 +537,7 @@ fn load_bank_forks( snapshot_interval_slots: 0, // Value doesn't matter snapshot_package_output_path: ledger_path.clone(), snapshot_path: ledger_path.clone().join("snapshot"), + trusted_validators: None, }) }; let account_paths = if let Some(account_paths) = arg_matches.value_of("account_paths") { diff --git a/ledger/src/bank_forks.rs b/ledger/src/bank_forks.rs index b60dd3eae..62c33d57d 100644 --- a/ledger/src/bank_forks.rs +++ b/ledger/src/bank_forks.rs @@ -6,7 +6,7 @@ use log::*; use solana_measure::measure::Measure; use solana_metrics::inc_new_counter_info; use solana_runtime::{bank::Bank, status_cache::MAX_CACHE_ENTRIES}; -use solana_sdk::{clock::Slot, timing}; +use solana_sdk::{clock::Slot, pubkey::Pubkey, timing}; use std::{ collections::{HashMap, HashSet}, ops::Index, @@ -26,6 +26,10 @@ pub struct SnapshotConfig { // Where to place the snapshots for recent slots pub snapshot_path: PathBuf, + + // Validators that must vouch for a given snapshot hash before it's accepted + // None = accept any snapshot hash + pub trusted_validators: Option>, } #[derive(Error, Debug)] diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index a2c551b36..9a04b60da 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -676,13 +676,14 @@ fn test_snapshot_restart_tower() { #[test] #[serial] fn test_snapshots_blockstore_floor() { + solana_logger::setup(); // First set up the cluster with 1 snapshotting leader let snapshot_interval_slots = 10; let num_account_paths = 4; let leader_snapshot_test_config = setup_snapshot_validator_config(snapshot_interval_slots, num_account_paths); - let validator_snapshot_test_config = + let mut validator_snapshot_test_config = setup_snapshot_validator_config(snapshot_interval_slots, num_account_paths); let snapshot_package_output_path = &leader_snapshot_test_config @@ -721,6 +722,17 @@ fn test_snapshots_blockstore_floor() { // Start up a new node from a snapshot let validator_stake = 5; + + let (cluster_nodes, _) = discover_cluster(&cluster.entry_point_info.gossip, 1).unwrap(); + let mut trusted_validators = HashSet::new(); + trusted_validators.insert(cluster_nodes[0].id); + if let Some(ref mut config) = validator_snapshot_test_config + .validator_config + .snapshot_config + { + config.trusted_validators = Some(trusted_validators); + } + cluster.add_validator( &validator_snapshot_test_config.validator_config, validator_stake, @@ -1000,6 +1012,7 @@ fn setup_snapshot_validator_config( snapshot_interval_slots, snapshot_package_output_path: PathBuf::from(snapshot_output_path.path()), snapshot_path: PathBuf::from(snapshot_dir.path()), + trusted_validators: None, }; // Create the account paths diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index a18b9511c..6f7c6b528 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -139,6 +139,9 @@ while [[ -n $1 ]]; do elif [[ $1 = --log ]]; then args+=("$1" "$2") shift 2 + elif [[ $1 = --trusted-validator ]]; then + args+=("$1" "$2") + shift 2 elif [[ $1 = -h ]]; then usage "$@" else diff --git a/validator/src/main.rs b/validator/src/main.rs index c1ebaf2ac..685b47b78 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -8,7 +8,7 @@ use log::*; use rand::{thread_rng, Rng}; use solana_clap_utils::{ input_parsers::pubkey_of, - input_validators::{is_keypair, is_pubkey_or_keypair}, + input_validators::{is_keypair, is_pubkey, is_pubkey_or_keypair}, keypair::{ self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG, @@ -32,6 +32,7 @@ use solana_sdk::{ signature::{Keypair, Signer}, }; use std::{ + collections::HashSet, fs::{self, File}, io::{self, Read}, net::{SocketAddr, TcpListener}, @@ -201,6 +202,7 @@ fn get_rpc_addr( identity_keypair: &Arc, entrypoint_gossip: &SocketAddr, expected_shred_version: Option, + trusted_validators: Option<&HashSet>, snapshot_not_required: bool, ) -> (RpcClient, SocketAddr) { let mut cluster_info = ClusterInfo::new( @@ -247,6 +249,22 @@ fn get_rpc_addr( } } + let trusted_slots = if let Some(trusted_validators) = trusted_validators { + let trusted_slots = HashSet::new(); + for trusted_validator in trusted_validators { + if let Some(slot_hash) = cluster_info + .read() + .unwrap() + .get_snapshot_hash_for_node(trusted_validator) + { + trusted_slots.union(&slot_hash.iter().collect()); + } + } + Some(trusted_slots) + } else { + None + }; + if rpc_peers.is_empty() { info!("No RPC services found "); } else { @@ -262,9 +280,16 @@ fn get_rpc_addr( .unwrap() .get_snapshot_hash_for_node(&rpc_peer.id) { - let highest_snapshot_slot_for_node = snapshot_hash - .iter() - .fold(0, |a, (slot, _hash)| a.max(*slot)); + let highest_snapshot_slot_for_node = + snapshot_hash.iter().fold(0, |highest_slot, snapshot_hash| { + if let Some(ref trusted_slots) = trusted_slots { + if !trusted_slots.contains(snapshot_hash) { + // Ignore all untrusted slots + return highest_slot; + } + } + highest_slot.max(snapshot_hash.0) + }); if highest_snapshot_slot_for_node > highest_snapshot_slot { // Found a higher snapshot, remove all rpc peers with a lower snapshot @@ -682,6 +707,16 @@ pub fn main() { .takes_value(true) .help("Add a hard fork at this slot"), ) + .arg( + Arg::with_name("trusted_validators") + .long("trusted-validator") + .validator(is_pubkey) + .value_name("PUBKEY") + .multiple(true) + .takes_value(true) + .help("A snapshot hash must be published in gossip by this validator to be accepted. \ + May be specified multiple times. If unspecified any snapshot hash will be accepted"), + ) .get_matches(); let identity_keypair = Arc::new( @@ -723,6 +758,13 @@ pub fn main() { exit(1); }); + let trusted_validators = if matches.is_present("trusted_validators") { + let trusted_validators = values_t_or_exit!(matches, "trusted_validators", Pubkey); + Some(trusted_validators.into_iter().collect()) + } else { + None + }; + let mut validator_config = ValidatorConfig { blockstream_unix_socket: matches .value_of("blockstream_unix_socket") @@ -794,6 +836,7 @@ pub fn main() { }, snapshot_path, snapshot_package_output_path: ledger_path.clone(), + trusted_validators, }); if matches.is_present("limit_ledger_size") { @@ -956,6 +999,12 @@ pub fn main() { &identity_keypair, &cluster_entrypoint.gossip, validator_config.expected_shred_version, + validator_config + .snapshot_config + .as_ref() + .unwrap() + .trusted_validators + .as_ref(), no_snapshot_fetch, );