2020-03-16 08:37:31 -07:00
|
|
|
// Service to verify accounts hashes with other trusted validator nodes.
|
|
|
|
//
|
|
|
|
// Each interval, publish the snapshat hash which is the full accounts state
|
|
|
|
// hash on gossip. Monitor gossip for messages from validators in the --trusted-validators
|
|
|
|
// set and halt the node if a mismatch is detected.
|
|
|
|
|
2021-01-11 10:21:15 -08:00
|
|
|
use crate::{
|
|
|
|
cluster_info::{ClusterInfo, MAX_SNAPSHOT_HASHES},
|
|
|
|
snapshot_packager_service::PendingSnapshotPackage,
|
2020-03-16 08:37:31 -07:00
|
|
|
};
|
2021-02-04 09:00:33 -06:00
|
|
|
use solana_runtime::snapshot_package::{
|
|
|
|
AccountsPackage, AccountsPackagePre, AccountsPackageReceiver,
|
|
|
|
};
|
2020-03-16 08:37:31 -07:00
|
|
|
use solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey};
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use std::{
|
|
|
|
sync::{
|
|
|
|
atomic::{AtomicBool, Ordering},
|
|
|
|
mpsc::RecvTimeoutError,
|
2020-04-21 12:54:45 -07:00
|
|
|
Arc,
|
2020-03-16 08:37:31 -07:00
|
|
|
},
|
|
|
|
thread::{self, Builder, JoinHandle},
|
|
|
|
time::Duration,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub struct AccountsHashVerifier {
|
|
|
|
t_accounts_hash_verifier: JoinHandle<()>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AccountsHashVerifier {
|
|
|
|
pub fn new(
|
2020-04-16 15:12:20 -07:00
|
|
|
accounts_package_receiver: AccountsPackageReceiver,
|
2021-01-11 10:21:15 -08:00
|
|
|
pending_snapshot_package: Option<PendingSnapshotPackage>,
|
2020-03-16 08:37:31 -07:00
|
|
|
exit: &Arc<AtomicBool>,
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info: &Arc<ClusterInfo>,
|
2020-03-16 08:37:31 -07:00
|
|
|
trusted_validators: Option<HashSet<Pubkey>>,
|
|
|
|
halt_on_trusted_validators_accounts_hash_mismatch: bool,
|
|
|
|
fault_injection_rate_slots: u64,
|
2020-04-16 15:12:20 -07:00
|
|
|
snapshot_interval_slots: u64,
|
2020-03-16 08:37:31 -07:00
|
|
|
) -> Self {
|
|
|
|
let exit = exit.clone();
|
|
|
|
let cluster_info = cluster_info.clone();
|
|
|
|
let t_accounts_hash_verifier = Builder::new()
|
|
|
|
.name("solana-accounts-hash".to_string())
|
|
|
|
.spawn(move || {
|
|
|
|
let mut hashes = vec![];
|
|
|
|
loop {
|
|
|
|
if exit.load(Ordering::Relaxed) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-04-16 15:12:20 -07:00
|
|
|
match accounts_package_receiver.recv_timeout(Duration::from_secs(1)) {
|
|
|
|
Ok(accounts_package) => {
|
2021-02-04 09:00:33 -06:00
|
|
|
Self::process_accounts_package_pre(
|
2020-04-16 15:12:20 -07:00
|
|
|
accounts_package,
|
2020-03-16 08:37:31 -07:00
|
|
|
&cluster_info,
|
|
|
|
&trusted_validators,
|
|
|
|
halt_on_trusted_validators_accounts_hash_mismatch,
|
2021-01-11 10:21:15 -08:00
|
|
|
&pending_snapshot_package,
|
2020-03-16 08:37:31 -07:00
|
|
|
&mut hashes,
|
|
|
|
&exit,
|
|
|
|
fault_injection_rate_slots,
|
2020-04-16 15:12:20 -07:00
|
|
|
snapshot_interval_slots,
|
2020-03-16 08:37:31 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
Err(RecvTimeoutError::Disconnected) => break,
|
|
|
|
Err(RecvTimeoutError::Timeout) => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
Self {
|
|
|
|
t_accounts_hash_verifier,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-04 09:00:33 -06:00
|
|
|
fn process_accounts_package_pre(
|
|
|
|
accounts_package: AccountsPackagePre,
|
|
|
|
cluster_info: &ClusterInfo,
|
|
|
|
trusted_validators: &Option<HashSet<Pubkey>>,
|
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch: bool,
|
|
|
|
pending_snapshot_package: &Option<PendingSnapshotPackage>,
|
|
|
|
hashes: &mut Vec<(Slot, Hash)>,
|
|
|
|
exit: &Arc<AtomicBool>,
|
|
|
|
fault_injection_rate_slots: u64,
|
|
|
|
snapshot_interval_slots: u64,
|
|
|
|
) {
|
|
|
|
let accounts_package =
|
|
|
|
solana_runtime::snapshot_utils::process_accounts_package_pre(accounts_package);
|
|
|
|
Self::process_accounts_package(
|
|
|
|
accounts_package,
|
|
|
|
cluster_info,
|
|
|
|
trusted_validators,
|
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch,
|
|
|
|
pending_snapshot_package,
|
|
|
|
hashes,
|
|
|
|
exit,
|
|
|
|
fault_injection_rate_slots,
|
|
|
|
snapshot_interval_slots,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-16 15:12:20 -07:00
|
|
|
fn process_accounts_package(
|
|
|
|
accounts_package: AccountsPackage,
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info: &ClusterInfo,
|
2020-03-16 08:37:31 -07:00
|
|
|
trusted_validators: &Option<HashSet<Pubkey>>,
|
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch: bool,
|
2021-01-11 10:21:15 -08:00
|
|
|
pending_snapshot_package: &Option<PendingSnapshotPackage>,
|
2020-03-16 08:37:31 -07:00
|
|
|
hashes: &mut Vec<(Slot, Hash)>,
|
|
|
|
exit: &Arc<AtomicBool>,
|
|
|
|
fault_injection_rate_slots: u64,
|
2020-04-16 15:12:20 -07:00
|
|
|
snapshot_interval_slots: u64,
|
2020-03-16 08:37:31 -07:00
|
|
|
) {
|
2021-02-04 09:00:33 -06:00
|
|
|
let hash = accounts_package.hash;
|
2020-03-16 08:37:31 -07:00
|
|
|
if fault_injection_rate_slots != 0
|
2021-01-04 13:22:58 -08:00
|
|
|
&& accounts_package.slot % fault_injection_rate_slots == 0
|
2020-03-16 08:37:31 -07:00
|
|
|
{
|
|
|
|
// For testing, publish an invalid hash to gossip.
|
|
|
|
use rand::{thread_rng, Rng};
|
|
|
|
use solana_sdk::hash::extend_and_hash;
|
2021-01-04 13:22:58 -08:00
|
|
|
warn!("inserting fault at slot: {}", accounts_package.slot);
|
2020-03-16 08:37:31 -07:00
|
|
|
let rand = thread_rng().gen_range(0, 10);
|
2021-02-04 09:00:33 -06:00
|
|
|
let hash = extend_and_hash(&hash, &[rand]);
|
2021-01-04 13:22:58 -08:00
|
|
|
hashes.push((accounts_package.slot, hash));
|
2020-03-16 08:37:31 -07:00
|
|
|
} else {
|
2021-02-04 09:00:33 -06:00
|
|
|
hashes.push((accounts_package.slot, hash));
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
|
2020-03-31 21:39:48 -07:00
|
|
|
while hashes.len() > MAX_SNAPSHOT_HASHES {
|
|
|
|
hashes.remove(0);
|
|
|
|
}
|
|
|
|
|
2020-03-16 08:37:31 -07:00
|
|
|
if halt_on_trusted_validator_accounts_hash_mismatch {
|
|
|
|
let mut slot_to_hash = HashMap::new();
|
|
|
|
for (slot, hash) in hashes.iter() {
|
|
|
|
slot_to_hash.insert(*slot, *hash);
|
|
|
|
}
|
|
|
|
if Self::should_halt(&cluster_info, trusted_validators, &mut slot_to_hash) {
|
|
|
|
exit.store(true, Ordering::Relaxed);
|
|
|
|
}
|
|
|
|
}
|
2020-04-16 15:12:20 -07:00
|
|
|
|
|
|
|
if accounts_package.block_height % snapshot_interval_slots == 0 {
|
2021-01-11 10:21:15 -08:00
|
|
|
if let Some(pending_snapshot_package) = pending_snapshot_package.as_ref() {
|
|
|
|
*pending_snapshot_package.lock().unwrap() = Some(accounts_package);
|
2020-04-16 15:12:20 -07:00
|
|
|
}
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info.push_accounts_hashes(hashes.clone());
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn should_halt(
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info: &ClusterInfo,
|
2020-03-16 08:37:31 -07:00
|
|
|
trusted_validators: &Option<HashSet<Pubkey>>,
|
|
|
|
slot_to_hash: &mut HashMap<Slot, Hash>,
|
|
|
|
) -> bool {
|
2020-03-18 08:39:09 -07:00
|
|
|
let mut verified_count = 0;
|
2020-03-31 21:39:48 -07:00
|
|
|
let mut highest_slot = 0;
|
2020-03-16 08:37:31 -07:00
|
|
|
if let Some(trusted_validators) = trusted_validators.as_ref() {
|
|
|
|
for trusted_validator in trusted_validators {
|
2020-04-21 12:54:45 -07:00
|
|
|
let is_conflicting = cluster_info.get_accounts_hash_for_node(trusted_validator, |accounts_hashes|
|
2020-03-16 08:37:31 -07:00
|
|
|
{
|
2020-04-21 12:54:45 -07:00
|
|
|
accounts_hashes.iter().any(|(slot, hash)| {
|
2020-03-16 08:37:31 -07:00
|
|
|
if let Some(reference_hash) = slot_to_hash.get(slot) {
|
|
|
|
if *hash != *reference_hash {
|
|
|
|
error!("Trusted validator {} produced conflicting hashes for slot: {} ({} != {})",
|
|
|
|
trusted_validator,
|
|
|
|
slot,
|
|
|
|
hash,
|
|
|
|
reference_hash,
|
|
|
|
);
|
2020-04-21 12:54:45 -07:00
|
|
|
true
|
2020-03-18 08:39:09 -07:00
|
|
|
} else {
|
|
|
|
verified_count += 1;
|
2020-04-21 12:54:45 -07:00
|
|
|
false
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
} else {
|
2020-03-31 21:39:48 -07:00
|
|
|
highest_slot = std::cmp::max(*slot, highest_slot);
|
2020-03-16 08:37:31 -07:00
|
|
|
slot_to_hash.insert(*slot, *hash);
|
2020-04-21 12:54:45 -07:00
|
|
|
false
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
2020-04-21 12:54:45 -07:00
|
|
|
})
|
|
|
|
}).unwrap_or(false);
|
|
|
|
|
|
|
|
if is_conflicting {
|
|
|
|
return true;
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-18 08:39:09 -07:00
|
|
|
inc_new_counter_info!("accounts_hash_verifier-hashes_verified", verified_count);
|
2020-03-31 21:39:48 -07:00
|
|
|
datapoint_info!(
|
|
|
|
"accounts_hash_verifier",
|
|
|
|
("highest_slot_verified", highest_slot, i64),
|
|
|
|
);
|
2020-03-16 08:37:31 -07:00
|
|
|
false
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn join(self) -> thread::Result<()> {
|
|
|
|
self.t_accounts_hash_verifier.join()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::cluster_info::make_accounts_hashes_message;
|
|
|
|
use crate::contact_info::ContactInfo;
|
2021-01-07 22:45:42 -08:00
|
|
|
use solana_runtime::bank_forks::ArchiveFormat;
|
2020-06-19 06:38:37 +01:00
|
|
|
use solana_runtime::snapshot_utils::SnapshotVersion;
|
2020-03-16 08:37:31 -07:00
|
|
|
use solana_sdk::{
|
|
|
|
hash::hash,
|
|
|
|
signature::{Keypair, Signer},
|
|
|
|
};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_should_halt() {
|
|
|
|
let keypair = Keypair::new();
|
|
|
|
|
|
|
|
let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0);
|
|
|
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(contact_info);
|
2020-04-21 12:54:45 -07:00
|
|
|
let cluster_info = Arc::new(cluster_info);
|
2020-03-16 08:37:31 -07:00
|
|
|
|
|
|
|
let mut trusted_validators = HashSet::new();
|
|
|
|
let mut slot_to_hash = HashMap::new();
|
|
|
|
assert!(!AccountsHashVerifier::should_halt(
|
|
|
|
&cluster_info,
|
|
|
|
&Some(trusted_validators.clone()),
|
|
|
|
&mut slot_to_hash,
|
|
|
|
));
|
|
|
|
|
|
|
|
let validator1 = Keypair::new();
|
|
|
|
let hash1 = hash(&[1]);
|
|
|
|
let hash2 = hash(&[2]);
|
|
|
|
{
|
|
|
|
let message = make_accounts_hashes_message(&validator1, vec![(0, hash1)]).unwrap();
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info.push_message(message);
|
2020-10-13 18:10:25 -07:00
|
|
|
cluster_info.flush_push_queue();
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|
|
|
|
slot_to_hash.insert(0, hash2);
|
|
|
|
trusted_validators.insert(validator1.pubkey());
|
|
|
|
assert!(AccountsHashVerifier::should_halt(
|
|
|
|
&cluster_info,
|
2020-05-15 17:35:43 +01:00
|
|
|
&Some(trusted_validators),
|
2020-03-16 08:37:31 -07:00
|
|
|
&mut slot_to_hash,
|
|
|
|
));
|
|
|
|
}
|
2020-03-31 21:39:48 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_max_hashes() {
|
|
|
|
solana_logger::setup();
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
let keypair = Keypair::new();
|
|
|
|
|
|
|
|
let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0);
|
|
|
|
let cluster_info = ClusterInfo::new_with_invalid_keypair(contact_info);
|
2020-04-21 12:54:45 -07:00
|
|
|
let cluster_info = Arc::new(cluster_info);
|
2020-03-31 21:39:48 -07:00
|
|
|
|
|
|
|
let trusted_validators = HashSet::new();
|
|
|
|
let exit = Arc::new(AtomicBool::new(false));
|
|
|
|
let mut hashes = vec![];
|
|
|
|
for i in 0..MAX_SNAPSHOT_HASHES + 1 {
|
|
|
|
let snapshot_links = TempDir::new().unwrap();
|
2020-04-16 15:12:20 -07:00
|
|
|
let accounts_package = AccountsPackage {
|
2020-03-31 21:39:48 -07:00
|
|
|
hash: hash(&[i as u8]),
|
2020-04-16 15:12:20 -07:00
|
|
|
block_height: 100 + i as u64,
|
2021-01-04 13:22:58 -08:00
|
|
|
slot: 100 + i as u64,
|
2020-03-31 21:39:48 -07:00
|
|
|
slot_deltas: vec![],
|
|
|
|
snapshot_links,
|
|
|
|
tar_output_file: PathBuf::from("."),
|
|
|
|
storages: vec![],
|
2021-01-07 22:45:42 -08:00
|
|
|
archive_format: ArchiveFormat::TarBzip2,
|
2020-06-19 06:38:37 +01:00
|
|
|
snapshot_version: SnapshotVersion::default(),
|
2020-03-31 21:39:48 -07:00
|
|
|
};
|
|
|
|
|
2020-04-16 15:12:20 -07:00
|
|
|
AccountsHashVerifier::process_accounts_package(
|
|
|
|
accounts_package,
|
2020-03-31 21:39:48 -07:00
|
|
|
&cluster_info,
|
|
|
|
&Some(trusted_validators.clone()),
|
|
|
|
false,
|
|
|
|
&None,
|
|
|
|
&mut hashes,
|
|
|
|
&exit,
|
|
|
|
0,
|
2020-04-16 15:12:20 -07:00
|
|
|
100,
|
2020-03-31 21:39:48 -07:00
|
|
|
);
|
2020-12-17 15:12:18 -08:00
|
|
|
// sleep for 1ms to create a newer timestmap for gossip entry
|
|
|
|
// otherwise the timestamp won't be newer.
|
|
|
|
std::thread::sleep(Duration::from_millis(1));
|
2020-03-31 21:39:48 -07:00
|
|
|
}
|
2020-10-13 18:10:25 -07:00
|
|
|
cluster_info.flush_push_queue();
|
2020-04-21 12:54:45 -07:00
|
|
|
let cluster_hashes = cluster_info
|
|
|
|
.get_accounts_hash_for_node(&keypair.pubkey(), |c| c.clone())
|
2020-03-31 21:39:48 -07:00
|
|
|
.unwrap();
|
|
|
|
info!("{:?}", cluster_hashes);
|
|
|
|
assert_eq!(hashes.len(), MAX_SNAPSHOT_HASHES);
|
|
|
|
assert_eq!(cluster_hashes.len(), MAX_SNAPSHOT_HASHES);
|
|
|
|
assert_eq!(cluster_hashes[0], (101, hash(&[1])));
|
|
|
|
assert_eq!(
|
|
|
|
cluster_hashes[MAX_SNAPSHOT_HASHES - 1],
|
|
|
|
(
|
|
|
|
100 + MAX_SNAPSHOT_HASHES as u64,
|
|
|
|
hash(&[MAX_SNAPSHOT_HASHES as u8])
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|