-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Check for incorrect account hash value on snapshot ingest (v2) #7559
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -345,8 +345,8 @@ impl Accounts { | |
| }) | ||
| } | ||
|
|
||
| pub fn verify_hash_internal_state(&self, slot: Slot, ancestors: &HashMap<Slot, usize>) -> bool { | ||
| self.accounts_db.verify_hash_internal_state(slot, ancestors) | ||
| pub fn verify_bank_hash(&self, slot: Slot, ancestors: &HashMap<Slot, usize>) -> bool { | ||
| self.accounts_db.verify_bank_hash(slot, ancestors).is_ok() | ||
| } | ||
|
|
||
| pub fn load_by_program( | ||
|
|
@@ -476,7 +476,7 @@ impl Accounts { | |
| } | ||
| } | ||
|
|
||
| pub fn hash_internal_state(&self, slot_id: Slot) -> BankHash { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed from |
||
| pub fn bank_hash_at(&self, slot_id: Slot) -> BankHash { | ||
| let slot_hashes = self.accounts_db.slot_hashes.read().unwrap(); | ||
| *slot_hashes | ||
| .get(&slot_id) | ||
|
|
@@ -1164,17 +1164,17 @@ mod tests { | |
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn test_accounts_empty_hash_internal_state() { | ||
| fn test_accounts_empty_bank_hash() { | ||
| let accounts = Accounts::new(Vec::new()); | ||
| accounts.hash_internal_state(0); | ||
| accounts.bank_hash_at(0); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn test_accounts_empty_account_hash_internal_state() { | ||
| fn test_accounts_empty_account_bank_hash() { | ||
| let accounts = Accounts::new(Vec::new()); | ||
| accounts.store_slow(0, &Pubkey::default(), &Account::new(1, 0, &sysvar::id())); | ||
| accounts.hash_internal_state(0); | ||
| accounts.bank_hash_at(0); | ||
| } | ||
|
|
||
| fn check_accounts(accounts: &Accounts, pubkeys: &Vec<Pubkey>, num: usize) { | ||
|
|
@@ -1221,10 +1221,7 @@ mod tests { | |
| .accounts_from_stream(&mut reader, &daccounts_paths, copied_accounts.path()) | ||
| .is_ok()); | ||
| check_accounts(&daccounts, &pubkeys, 100); | ||
| assert_eq!( | ||
| accounts.hash_internal_state(0), | ||
| daccounts.hash_internal_state(0) | ||
| ); | ||
| assert_eq!(accounts.bank_hash_at(0), daccounts.bank_hash_at(0)); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,6 +175,13 @@ pub enum AccountStorageStatus { | |
| Candidate = 2, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub enum BankHashVerificatonError { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion, the naming here is very clear after all the hustle of naming changes of this PR. :) |
||
| MismatchedAccountHash, | ||
| MismatchedBankHash, | ||
| MissingBankHash, | ||
| } | ||
|
|
||
| /// Persistent storage structure holding the accounts | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| pub struct AccountStorageEntry { | ||
|
|
@@ -354,13 +361,11 @@ pub struct AccountsDB { | |
| /// Keeps tracks of index into AppendVec on a per slot basis | ||
| pub accounts_index: RwLock<AccountsIndex<AccountInfo>>, | ||
|
|
||
| /// Account storage | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this as the same reason as this. |
||
| pub storage: RwLock<AccountStorage>, | ||
|
|
||
| /// distribute the accounts across storage lists | ||
| pub next_id: AtomicUsize, | ||
|
|
||
| /// write version | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit aggressive here, but remove this comment as it adds no additional information to the commented code itself. use fewer comments and remove otherwise as it's too common for engineers to forget to maintain them up-to-date (including me!) as this is the case. |
||
| write_version: AtomicUsize, | ||
|
|
||
| /// Set of storage paths to pick from | ||
|
|
@@ -379,7 +384,6 @@ pub struct AccountsDB { | |
| /// the accounts | ||
| min_num_stores: usize, | ||
|
|
||
| /// slot to BankHash and a status flag to indicate if the hash has been initialized or not | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit aggressive, but delete this comment as it's outdated and now adds no additional information by the comment.
|
||
| pub slot_hashes: RwLock<HashMap<Slot, BankHash>>, | ||
| } | ||
|
|
||
|
|
@@ -969,28 +973,49 @@ impl AccountsDB { | |
| datapoint_info!("accounts_db-stores", ("total_count", total_count, i64)); | ||
| } | ||
|
|
||
| pub fn verify_hash_internal_state(&self, slot: Slot, ancestors: &HashMap<Slot, usize>) -> bool { | ||
| let mut hash_state = BankHash::default(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to its use site and renamed. |
||
| let hashes: Vec<_> = self.scan_accounts( | ||
| pub fn verify_bank_hash( | ||
| &self, | ||
| slot: Slot, | ||
| ancestors: &HashMap<Slot, usize>, | ||
| ) -> Result<(), BankHashVerificatonError> { | ||
| use BankHashVerificatonError::*; | ||
|
|
||
| let (hashes, mismatch_found) = self.scan_accounts( | ||
| ancestors, | ||
| |collector: &mut Vec<BankHash>, option: Option<(&Pubkey, Account, Slot)>| { | ||
| |(collector, mismatch_found): &mut (Vec<BankHash>, bool), | ||
| option: Option<(&Pubkey, Account, Slot)>| { | ||
| if let Some((pubkey, account, slot)) = option { | ||
| if !sysvar::check_id(&account.owner) { | ||
| let hash = BankHash::from_hash(&Self::hash_account(slot, &account, pubkey)); | ||
| let hash = Self::hash_account(slot, &account, pubkey); | ||
| if hash != account.hash { | ||
| *mismatch_found = true; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally assigned |
||
| } | ||
| if *mismatch_found { | ||
| return; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added by me, bail out early as the PR description explains. |
||
| } | ||
| let hash = BankHash::from_hash(&hash); | ||
| debug!("xoring..{} key: {}", hash, pubkey); | ||
| collector.push(hash); | ||
| } | ||
| } | ||
| }, | ||
| ); | ||
| if mismatch_found { | ||
| return Err(MismatchedAccountHash); | ||
| } | ||
| let mut calculated_hash = BankHash::default(); | ||
| for hash in hashes { | ||
| hash_state.xor(hash); | ||
| calculated_hash.xor(hash); | ||
| } | ||
| let slot_hashes = self.slot_hashes.read().unwrap(); | ||
| if let Some(state) = slot_hashes.get(&slot) { | ||
| hash_state == *state | ||
| if let Some(found_hash) = slot_hashes.get(&slot) { | ||
| if calculated_hash == *found_hash { | ||
| Ok(()) | ||
| } else { | ||
| Err(MismatchedBankHash) | ||
| } | ||
| } else { | ||
| false | ||
| Err(BankHashVerificatonError::MissingBankHash) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1120,9 +1145,12 @@ impl AccountsDB { | |
| /// Store the account update. | ||
| pub fn store(&self, slot_id: Slot, accounts: &[(&Pubkey, &Account)]) { | ||
| let hashes = self.hash_accounts(slot_id, accounts); | ||
| self.store_with_hashes(slot_id, accounts, &hashes); | ||
| } | ||
|
|
||
| fn store_with_hashes(&self, slot_id: Slot, accounts: &[(&Pubkey, &Account)], hashes: &[Hash]) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted into new |
||
| let mut store_accounts = Measure::start("store::store_accounts"); | ||
| let infos = self.store_accounts(slot_id, accounts, &hashes); | ||
| let infos = self.store_accounts(slot_id, accounts, hashes); | ||
| store_accounts.stop(); | ||
|
|
||
| let mut update_index = Measure::start("store::update_index"); | ||
|
|
@@ -1234,9 +1262,11 @@ pub mod tests { | |
| // TODO: all the bank tests are bank specific, issue: 2194 | ||
| use super::*; | ||
| use crate::append_vec::AccountMeta; | ||
| use assert_matches::assert_matches; | ||
| use bincode::serialize_into; | ||
| use rand::{thread_rng, Rng}; | ||
| use solana_sdk::account::Account; | ||
| use solana_sdk::hash::HASH_BYTES; | ||
| use std::fs; | ||
| use std::str::FromStr; | ||
| use tempfile::TempDir; | ||
|
|
@@ -2195,4 +2225,78 @@ pub mod tests { | |
| "Account-based hashing must be consistent with StoredAccount-based one." | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_verify_bank_hash() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the coverage report from the CI will reveal, these tests does the 100% coverage. :p |
||
| use BankHashVerificatonError::*; | ||
| solana_logger::setup(); | ||
| let db = AccountsDB::new(Vec::new()); | ||
|
|
||
| let key = Pubkey::default(); | ||
| let some_data_len = 0; | ||
| let some_slot: Slot = 0; | ||
| let account = Account::new(1, some_data_len, &key); | ||
| let ancestors = vec![(some_slot, 0)].into_iter().collect(); | ||
|
|
||
| db.store(some_slot, &[(&key, &account)]); | ||
| db.add_root(some_slot); | ||
| assert_matches!(db.verify_bank_hash(some_slot, &ancestors), Ok(_)); | ||
|
|
||
| db.slot_hashes.write().unwrap().remove(&some_slot).unwrap(); | ||
| assert_matches!( | ||
| db.verify_bank_hash(some_slot, &ancestors), | ||
| Err(MissingBankHash) | ||
| ); | ||
|
|
||
| let some_bank_hash = BankHash::from_hash(&Hash::new(&[0xca; HASH_BYTES])); | ||
| db.slot_hashes | ||
| .write() | ||
| .unwrap() | ||
| .insert(some_slot, some_bank_hash); | ||
| assert_matches!( | ||
| db.verify_bank_hash(some_slot, &ancestors), | ||
| Err(MismatchedBankHash) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_verify_bank_hash_no_account() { | ||
| solana_logger::setup(); | ||
| let db = AccountsDB::new(Vec::new()); | ||
|
|
||
| let some_slot: Slot = 0; | ||
| let ancestors = vec![(some_slot, 0)].into_iter().collect(); | ||
|
|
||
| db.slot_hashes | ||
| .write() | ||
| .unwrap() | ||
| .insert(some_slot, BankHash::default()); | ||
| db.add_root(some_slot); | ||
| assert_matches!(db.verify_bank_hash(some_slot, &ancestors), Ok(_)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_verify_bank_hash_bad_account_hash() { | ||
| use BankHashVerificatonError::*; | ||
| solana_logger::setup(); | ||
| let db = AccountsDB::new(Vec::new()); | ||
|
|
||
| let key = Pubkey::default(); | ||
| let some_data_len = 0; | ||
| let some_slot: Slot = 0; | ||
| let account = Account::new(1, some_data_len, &key); | ||
| let ancestors = vec![(some_slot, 0)].into_iter().collect(); | ||
|
|
||
| let accounts = &[(&key, &account)]; | ||
| // update AccountsDB's bank hash but discard real account hashes | ||
| db.hash_accounts(some_slot, accounts); | ||
| // provide bogus account hashes | ||
| let some_hash = Hash::new(&[0xca; HASH_BYTES]); | ||
| db.store_with_hashes(some_slot, accounts, &vec![some_hash]); | ||
| db.add_root(some_slot); | ||
| assert_matches!( | ||
| db.verify_bank_hash(some_slot, &ancestors), | ||
| Err(MismatchedAccountHash) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1595,7 +1595,7 @@ impl Bank { | |
| /// of the delta of the ledger since the last vote and up to now | ||
| fn hash_internal_state(&self) -> Hash { | ||
| // If there are no accounts, return the hash of the previous state and the latest blockhash | ||
| let accounts_delta_hash = self.rc.accounts.hash_internal_state(self.slot()); | ||
| let accounts_delta_hash = self.rc.accounts.bank_hash_at(self.slot()); | ||
| let mut signature_count_buf = [0u8; 8]; | ||
| LittleEndian::write_u64(&mut signature_count_buf[..], self.signature_count() as u64); | ||
| hashv(&[ | ||
|
|
@@ -1611,7 +1611,7 @@ impl Bank { | |
| pub fn verify_hash_internal_state(&self) -> bool { | ||
| self.rc | ||
| .accounts | ||
| .verify_hash_internal_state(self.slot(), &self.ancestors) | ||
| .verify_bank_hash(self.slot(), &self.ancestors) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this place, we semantically translate verify-ing Bank's |
||
| } | ||
|
|
||
| /// A snapshot bank should be purged of 0 lamport accounts which are not part of the hash | ||
|
|
@@ -1620,7 +1620,7 @@ impl Bank { | |
| self.purge_zero_lamport_accounts(); | ||
| self.rc | ||
| .accounts | ||
| .verify_hash_internal_state(self.slot(), &self.ancestors) | ||
| .verify_bank_hash(self.slot(), &self.ancestors) | ||
| } | ||
|
|
||
| /// Return the number of hashes per tick | ||
|
|
@@ -1806,8 +1806,8 @@ impl Bank { | |
| let dsc = dbank.src.status_cache.read().unwrap(); | ||
| assert_eq!(*sc, *dsc); | ||
| assert_eq!( | ||
| self.rc.accounts.hash_internal_state(self.slot), | ||
| dbank.rc.accounts.hash_internal_state(dbank.slot) | ||
| self.rc.accounts.bank_hash_at(self.slot), | ||
| dbank.rc.accounts.bank_hash_at(dbank.slot) | ||
| ); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,9 +7,10 @@ use std::fmt; | |
| use std::mem; | ||
| use std::str::FromStr; | ||
|
|
||
| pub const HASH_BYTES: usize = 32; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A tiny nicety. This aligns with BANK_HASH_BYTES in |
||
| #[derive(Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] | ||
| #[repr(transparent)] | ||
| pub struct Hash([u8; 32]); | ||
| pub struct Hash([u8; HASH_BYTES]); | ||
|
|
||
| #[derive(Clone, Default)] | ||
| pub struct Hasher { | ||
|
|
@@ -28,7 +29,7 @@ impl Hasher { | |
| pub fn result(self) -> Hash { | ||
| // At the time of this writing, the sha2 library is stuck on an old version | ||
| // of generic_array (0.9.0). Decouple ourselves with a clone to our version. | ||
| Hash(<[u8; 32]>::try_from(self.hasher.result().as_slice()).unwrap()) | ||
| Hash(<[u8; HASH_BYTES]>::try_from(self.hasher.result().as_slice()).unwrap()) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -73,7 +74,7 @@ impl FromStr for Hash { | |
|
|
||
| impl Hash { | ||
| pub fn new(hash_slice: &[u8]) -> Self { | ||
| Hash(<[u8; 32]>::try_from(hash_slice).unwrap()) | ||
| Hash(<[u8; HASH_BYTES]>::try_from(hash_slice).unwrap()) | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very layer boundary of "Bank<=>AccountsDB", we discard detailed error and encapsulate the result into a bool.