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-02-05 13:48:55 -06:00
|
|
|
use rayon::ThreadPool;
|
2021-05-26 09:15:46 -06:00
|
|
|
use solana_gossip::cluster_info::{ClusterInfo, MAX_SNAPSHOT_HASHES};
|
2021-02-05 13:48:55 -06:00
|
|
|
use solana_runtime::{
|
|
|
|
accounts_db,
|
2021-08-08 07:57:06 -05:00
|
|
|
snapshot_archive_info::SnapshotArchiveInfoGetter,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config::SnapshotConfig,
|
2021-08-17 13:01:59 -05:00
|
|
|
snapshot_package::{
|
|
|
|
AccountsPackage, AccountsPackageReceiver, PendingSnapshotPackage, SnapshotPackage,
|
|
|
|
},
|
2021-08-13 16:08:09 -05:00
|
|
|
snapshot_utils,
|
2021-02-04 09:00:33 -06:00
|
|
|
};
|
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,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config: Option<SnapshotConfig>,
|
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()
|
2021-04-19 12:16:58 -05:00
|
|
|
.name("solana-hash-accounts".to_string())
|
2020-03-16 08:37:31 -07:00
|
|
|
.spawn(move || {
|
|
|
|
let mut hashes = vec![];
|
2021-02-05 13:48:55 -06:00
|
|
|
let mut thread_pool_storage = None;
|
2020-03-16 08:37:31 -07:00
|
|
|
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-05 13:48:55 -06:00
|
|
|
if accounts_package.hash_for_testing.is_some()
|
|
|
|
&& thread_pool_storage.is_none()
|
|
|
|
{
|
|
|
|
thread_pool_storage =
|
|
|
|
Some(accounts_db::make_min_priority_thread_pool());
|
|
|
|
}
|
|
|
|
|
2021-08-13 16:08:09 -05:00
|
|
|
Self::process_accounts_package(
|
2020-04-16 15:12:20 -07:00
|
|
|
accounts_package,
|
2020-03-16 08:37:31 -07:00
|
|
|
&cluster_info,
|
2021-08-10 14:02:34 -05:00
|
|
|
trusted_validators.as_ref(),
|
2020-03-16 08:37:31 -07:00
|
|
|
halt_on_trusted_validators_accounts_hash_mismatch,
|
2021-08-10 14:02:34 -05:00
|
|
|
pending_snapshot_package.as_ref(),
|
2020-03-16 08:37:31 -07:00
|
|
|
&mut hashes,
|
|
|
|
&exit,
|
|
|
|
fault_injection_rate_slots,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config.as_ref(),
|
2021-02-05 13:48:55 -06:00
|
|
|
thread_pool_storage.as_ref(),
|
2020-03-16 08:37:31 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
Err(RecvTimeoutError::Disconnected) => break,
|
|
|
|
Err(RecvTimeoutError::Timeout) => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
Self {
|
|
|
|
t_accounts_hash_verifier,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-05 13:48:55 -06:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2021-08-13 16:08:09 -05:00
|
|
|
fn process_accounts_package(
|
|
|
|
accounts_package: AccountsPackage,
|
2021-02-04 09:00:33 -06:00
|
|
|
cluster_info: &ClusterInfo,
|
2021-08-10 14:02:34 -05:00
|
|
|
trusted_validators: Option<&HashSet<Pubkey>>,
|
2021-02-04 09:00:33 -06:00
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch: bool,
|
2021-08-10 14:02:34 -05:00
|
|
|
pending_snapshot_package: Option<&PendingSnapshotPackage>,
|
2021-02-04 09:00:33 -06:00
|
|
|
hashes: &mut Vec<(Slot, Hash)>,
|
|
|
|
exit: &Arc<AtomicBool>,
|
|
|
|
fault_injection_rate_slots: u64,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config: Option<&SnapshotConfig>,
|
2021-02-05 13:48:55 -06:00
|
|
|
thread_pool: Option<&ThreadPool>,
|
2021-02-04 09:00:33 -06:00
|
|
|
) {
|
2021-08-13 16:08:09 -05:00
|
|
|
let snapshot_package =
|
|
|
|
snapshot_utils::process_accounts_package(accounts_package, thread_pool, None);
|
|
|
|
Self::process_snapshot_package(
|
|
|
|
snapshot_package,
|
2021-02-04 09:00:33 -06:00
|
|
|
cluster_info,
|
|
|
|
trusted_validators,
|
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch,
|
|
|
|
pending_snapshot_package,
|
|
|
|
hashes,
|
|
|
|
exit,
|
|
|
|
fault_injection_rate_slots,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config,
|
2021-02-04 09:00:33 -06:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-13 16:08:09 -05:00
|
|
|
fn process_snapshot_package(
|
|
|
|
snapshot_package: SnapshotPackage,
|
2020-04-21 12:54:45 -07:00
|
|
|
cluster_info: &ClusterInfo,
|
2021-08-10 14:02:34 -05:00
|
|
|
trusted_validators: Option<&HashSet<Pubkey>>,
|
2020-03-16 08:37:31 -07:00
|
|
|
halt_on_trusted_validator_accounts_hash_mismatch: bool,
|
2021-08-10 14:02:34 -05: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,
|
2021-08-10 14:02:34 -05:00
|
|
|
snapshot_config: Option<&SnapshotConfig>,
|
2020-03-16 08:37:31 -07:00
|
|
|
) {
|
2021-08-13 16:08:09 -05:00
|
|
|
let hash = *snapshot_package.hash();
|
2020-03-16 08:37:31 -07:00
|
|
|
if fault_injection_rate_slots != 0
|
2021-08-13 16:08:09 -05:00
|
|
|
&& snapshot_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-08-13 16:08:09 -05:00
|
|
|
warn!("inserting fault at slot: {}", snapshot_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-08-13 16:08:09 -05:00
|
|
|
hashes.push((snapshot_package.slot(), hash));
|
2020-03-16 08:37:31 -07:00
|
|
|
} else {
|
2021-08-13 16:08:09 -05:00
|
|
|
hashes.push((snapshot_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);
|
|
|
|
}
|
2021-06-18 15:34:46 +02:00
|
|
|
if Self::should_halt(cluster_info, trusted_validators, &mut slot_to_hash) {
|
2020-03-16 08:37:31 -07:00
|
|
|
exit.store(true, Ordering::Relaxed);
|
|
|
|
}
|
|
|
|
}
|
2020-04-16 15:12:20 -07:00
|
|
|
|
2021-08-10 14:02:34 -05:00
|
|
|
if let Some(snapshot_config) = snapshot_config {
|
2021-08-13 16:08:09 -05:00
|
|
|
if snapshot_package.block_height % snapshot_config.full_snapshot_archive_interval_slots
|
2021-08-10 14:02:34 -05:00
|
|
|
== 0
|
|
|
|
{
|
|
|
|
if let Some(pending_snapshot_package) = pending_snapshot_package {
|
2021-08-13 16:08:09 -05:00
|
|
|
*pending_snapshot_package.lock().unwrap() = Some(snapshot_package);
|
2021-08-10 14:02:34 -05:00
|
|
|
}
|
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,
|
2021-08-10 14:02:34 -05:00
|
|
|
trusted_validators: Option<&HashSet<Pubkey>>,
|
2020-03-16 08:37:31 -07:00
|
|
|
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;
|
2021-08-10 14:02:34 -05:00
|
|
|
if let Some(trusted_validators) = trusted_validators {
|
2020-03-16 08:37:31 -07:00
|
|
|
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::*;
|
2021-05-26 09:15:46 -06:00
|
|
|
use solana_gossip::{cluster_info::make_accounts_hashes_message, contact_info::ContactInfo};
|
2021-08-17 13:01:59 -05:00
|
|
|
use solana_runtime::{
|
|
|
|
snapshot_package::SnapshotType,
|
|
|
|
snapshot_utils::{ArchiveFormat, SnapshotVersion},
|
|
|
|
};
|
2020-03-16 08:37:31 -07:00
|
|
|
use solana_sdk::{
|
|
|
|
hash::hash,
|
|
|
|
signature::{Keypair, Signer},
|
|
|
|
};
|
2021-07-23 15:25:03 +00:00
|
|
|
use solana_streamer::socket::SocketAddrSpace;
|
|
|
|
|
|
|
|
fn new_test_cluster_info(contact_info: ContactInfo) -> ClusterInfo {
|
|
|
|
ClusterInfo::new(
|
|
|
|
contact_info,
|
|
|
|
Arc::new(Keypair::new()),
|
|
|
|
SocketAddrSpace::Unspecified,
|
|
|
|
)
|
|
|
|
}
|
2020-03-16 08:37:31 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_should_halt() {
|
|
|
|
let keypair = Keypair::new();
|
|
|
|
|
|
|
|
let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0);
|
2021-07-23 15:25:03 +00:00
|
|
|
let cluster_info = new_test_cluster_info(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,
|
2021-08-10 14:02:34 -05:00
|
|
|
Some(&trusted_validators),
|
2020-03-16 08:37:31 -07:00
|
|
|
&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,
|
2021-08-10 14:02:34 -05: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);
|
2021-07-23 15:25:03 +00:00
|
|
|
let cluster_info = new_test_cluster_info(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![];
|
2021-08-10 14:02:34 -05:00
|
|
|
let full_snapshot_archive_interval_slots = 100;
|
|
|
|
let snapshot_config = SnapshotConfig {
|
|
|
|
full_snapshot_archive_interval_slots,
|
|
|
|
incremental_snapshot_archive_interval_slots: Slot::MAX,
|
|
|
|
snapshot_package_output_path: PathBuf::default(),
|
|
|
|
snapshot_path: PathBuf::default(),
|
|
|
|
archive_format: ArchiveFormat::Tar,
|
|
|
|
snapshot_version: SnapshotVersion::default(),
|
|
|
|
maximum_snapshots_to_retain: usize::MAX,
|
|
|
|
};
|
2020-03-31 21:39:48 -07:00
|
|
|
for i in 0..MAX_SNAPSHOT_HASHES + 1 {
|
2021-08-10 14:02:34 -05:00
|
|
|
let slot = full_snapshot_archive_interval_slots + i as u64;
|
|
|
|
let block_height = full_snapshot_archive_interval_slots + i as u64;
|
2021-08-06 20:16:06 -05:00
|
|
|
let slot_deltas = vec![];
|
2020-03-31 21:39:48 -07:00
|
|
|
let snapshot_links = TempDir::new().unwrap();
|
2021-08-06 20:16:06 -05:00
|
|
|
let storages = vec![];
|
|
|
|
let snapshot_archive_path = PathBuf::from(".");
|
|
|
|
let hash = hash(&[i as u8]);
|
|
|
|
let archive_format = ArchiveFormat::TarBzip2;
|
|
|
|
let snapshot_version = SnapshotVersion::default();
|
2021-08-13 16:08:09 -05:00
|
|
|
let snapshot_package = SnapshotPackage::new(
|
2021-08-06 20:16:06 -05:00
|
|
|
slot,
|
|
|
|
block_height,
|
|
|
|
slot_deltas,
|
2020-03-31 21:39:48 -07:00
|
|
|
snapshot_links,
|
2021-08-06 20:16:06 -05:00
|
|
|
storages,
|
|
|
|
snapshot_archive_path,
|
|
|
|
hash,
|
|
|
|
archive_format,
|
|
|
|
snapshot_version,
|
2021-08-17 13:01:59 -05:00
|
|
|
SnapshotType::FullSnapshot,
|
2021-08-06 20:16:06 -05:00
|
|
|
);
|
2020-03-31 21:39:48 -07:00
|
|
|
|
2021-08-13 16:08:09 -05:00
|
|
|
AccountsHashVerifier::process_snapshot_package(
|
|
|
|
snapshot_package,
|
2020-03-31 21:39:48 -07:00
|
|
|
&cluster_info,
|
2021-08-10 14:02:34 -05:00
|
|
|
Some(&trusted_validators),
|
2020-03-31 21:39:48 -07:00
|
|
|
false,
|
2021-08-10 14:02:34 -05:00
|
|
|
None,
|
2020-03-31 21:39:48 -07:00
|
|
|
&mut hashes,
|
|
|
|
&exit,
|
|
|
|
0,
|
2021-08-10 14:02:34 -05:00
|
|
|
Some(&snapshot_config),
|
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);
|
2021-08-10 14:02:34 -05:00
|
|
|
assert_eq!(
|
|
|
|
cluster_hashes[0],
|
|
|
|
(full_snapshot_archive_interval_slots + 1, hash(&[1]))
|
|
|
|
);
|
2020-03-31 21:39:48 -07:00
|
|
|
assert_eq!(
|
|
|
|
cluster_hashes[MAX_SNAPSHOT_HASHES - 1],
|
|
|
|
(
|
2021-08-10 14:02:34 -05:00
|
|
|
full_snapshot_archive_interval_slots + MAX_SNAPSHOT_HASHES as u64,
|
2020-03-31 21:39:48 -07:00
|
|
|
hash(&[MAX_SNAPSHOT_HASHES as u8])
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2020-03-16 08:37:31 -07:00
|
|
|
}
|