@@ -26,6 +26,7 @@ use crate::{
|
||||
use bincode::{deserialize_from, serialize_into};
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use fs_extra::dir::CopyOptions;
|
||||
use lazy_static::lazy_static;
|
||||
use log::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use rayon::{prelude::*, ThreadPool};
|
||||
@@ -56,6 +57,12 @@ pub const DEFAULT_FILE_SIZE: u64 = 4 * 1024 * 1024;
|
||||
pub const DEFAULT_NUM_THREADS: u32 = 8;
|
||||
pub const DEFAULT_NUM_DIRS: u32 = 4;
|
||||
|
||||
lazy_static! {
|
||||
// FROZEN_ACCOUNT_PANIC is used to signal local_cluster that an AccountsDB panic has occurred,
|
||||
// as |cargo test| cannot observe panics in other threads
|
||||
pub static ref FROZEN_ACCOUNT_PANIC: Arc<AtomicBool> = { Arc::new(AtomicBool::new(false)) };
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ErrorCounters {
|
||||
pub total: usize,
|
||||
@@ -426,6 +433,12 @@ pub struct BankHashInfo {
|
||||
pub stats: BankHashStats,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FrozenAccountInfo {
|
||||
pub hash: Hash, // Hash generated by hash_frozen_account_data()
|
||||
pub lamports: u64, // Account balance cannot be lower than this amount
|
||||
}
|
||||
|
||||
// This structure handles the load/store of the accounts
|
||||
#[derive(Debug)]
|
||||
pub struct AccountsDB {
|
||||
@@ -448,6 +461,9 @@ pub struct AccountsDB {
|
||||
/// Starting file size of appendvecs
|
||||
file_size: u64,
|
||||
|
||||
/// Accounts that will cause a panic! if data modified or lamports decrease
|
||||
frozen_accounts: HashMap<Pubkey, FrozenAccountInfo>,
|
||||
|
||||
/// Thread pool used for par_iter
|
||||
pub thread_pool: ThreadPool,
|
||||
|
||||
@@ -478,6 +494,7 @@ impl Default for AccountsDB {
|
||||
.unwrap(),
|
||||
min_num_stores: num_threads,
|
||||
bank_hashes: RwLock::new(bank_hashes),
|
||||
frozen_accounts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,7 +508,7 @@ impl AccountsDB {
|
||||
..Self::default()
|
||||
}
|
||||
} else {
|
||||
// Create a temprorary set of accounts directories, used primarily
|
||||
// Create a temporary set of accounts directories, used primarily
|
||||
// for testing
|
||||
let (temp_dirs, paths) = get_temp_accounts_paths(DEFAULT_NUM_DIRS).unwrap();
|
||||
Self {
|
||||
@@ -526,7 +543,7 @@ impl AccountsDB {
|
||||
pub fn accounts_from_stream<R: Read, P: AsRef<Path>>(
|
||||
&self,
|
||||
mut stream: &mut BufReader<R>,
|
||||
append_vecs_path: P,
|
||||
stream_append_vecs_path: P,
|
||||
) -> Result<(), IOError> {
|
||||
let _len: usize =
|
||||
deserialize_from(&mut stream).map_err(|e| AccountsDB::get_io_error(&e.to_string()))?;
|
||||
@@ -549,8 +566,9 @@ impl AccountsDB {
|
||||
// at by `local_dir`
|
||||
let append_vec_relative_path =
|
||||
AppendVec::new_relative_path(slot_id, storage_entry.id);
|
||||
let append_vec_abs_path =
|
||||
append_vecs_path.as_ref().join(&append_vec_relative_path);
|
||||
let append_vec_abs_path = stream_append_vecs_path
|
||||
.as_ref()
|
||||
.join(&append_vec_relative_path);
|
||||
let target = local_dir.join(append_vec_abs_path.file_name().unwrap());
|
||||
if std::fs::rename(append_vec_abs_path.clone(), target).is_err() {
|
||||
let mut copy_options = CopyOptions::new();
|
||||
@@ -976,6 +994,21 @@ impl AccountsDB {
|
||||
)
|
||||
}
|
||||
|
||||
fn hash_frozen_account_data(account: &Account) -> Hash {
|
||||
let mut hasher = Hasher::default();
|
||||
|
||||
hasher.hash(&account.data);
|
||||
hasher.hash(&account.owner.as_ref());
|
||||
|
||||
if account.executable {
|
||||
hasher.hash(&[1u8; 1]);
|
||||
} else {
|
||||
hasher.hash(&[0u8; 1]);
|
||||
}
|
||||
|
||||
hasher.result()
|
||||
}
|
||||
|
||||
pub fn hash_account_data(
|
||||
slot: Slot,
|
||||
lamports: u64,
|
||||
@@ -1387,8 +1420,62 @@ impl AccountsDB {
|
||||
hashes
|
||||
}
|
||||
|
||||
pub fn freeze_accounts(
|
||||
&mut self,
|
||||
ancestors: &HashMap<Slot, usize>,
|
||||
account_pubkeys: &[Pubkey],
|
||||
) {
|
||||
for account_pubkey in account_pubkeys {
|
||||
if let Some((account, _slot)) = self.load_slow(ancestors, &account_pubkey) {
|
||||
let frozen_account_info = FrozenAccountInfo {
|
||||
hash: Self::hash_frozen_account_data(&account),
|
||||
lamports: account.lamports,
|
||||
};
|
||||
warn!(
|
||||
"Account {} is now frozen at lamports={}, hash={}",
|
||||
account_pubkey, frozen_account_info.lamports, frozen_account_info.hash
|
||||
);
|
||||
self.frozen_accounts
|
||||
.insert(*account_pubkey, frozen_account_info);
|
||||
} else {
|
||||
panic!(
|
||||
"Unable to freeze an account that does not exist: {}",
|
||||
account_pubkey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cause a panic if frozen accounts would be affected by data in `accounts`
|
||||
fn assert_frozen_accounts(&self, accounts: &[(&Pubkey, &Account)]) {
|
||||
if self.frozen_accounts.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (account_pubkey, account) in accounts.iter() {
|
||||
if let Some(frozen_account_info) = self.frozen_accounts.get(*account_pubkey) {
|
||||
if account.lamports < frozen_account_info.lamports {
|
||||
FROZEN_ACCOUNT_PANIC.store(true, Ordering::Relaxed);
|
||||
panic!(
|
||||
"Frozen account {} modified. Lamports decreased from {} to {}",
|
||||
account_pubkey, frozen_account_info.lamports, account.lamports,
|
||||
)
|
||||
}
|
||||
|
||||
let hash = Self::hash_frozen_account_data(&account);
|
||||
if hash != frozen_account_info.hash {
|
||||
FROZEN_ACCOUNT_PANIC.store(true, Ordering::Relaxed);
|
||||
panic!(
|
||||
"Frozen account {} modified. Hash changed from {} to {}",
|
||||
account_pubkey, frozen_account_info.hash, hash,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the account update.
|
||||
pub fn store(&self, slot_id: Slot, accounts: &[(&Pubkey, &Account)]) {
|
||||
self.assert_frozen_accounts(accounts);
|
||||
let hashes = self.hash_accounts(slot_id, accounts);
|
||||
self.store_with_hashes(slot_id, accounts, &hashes);
|
||||
}
|
||||
@@ -2712,6 +2799,139 @@ pub mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_frozen_account_data() {
|
||||
let account = Account::new(1, 42, &Pubkey::default());
|
||||
|
||||
let hash = AccountsDB::hash_frozen_account_data(&account);
|
||||
assert_ne!(hash, Hash::default()); // Better not be the default Hash
|
||||
|
||||
// Lamports changes to not affect the hash
|
||||
let mut account_modified = account.clone();
|
||||
account_modified.lamports -= 1;
|
||||
assert_eq!(
|
||||
hash,
|
||||
AccountsDB::hash_frozen_account_data(&account_modified)
|
||||
);
|
||||
|
||||
// Rent epoch may changes to not affect the hash
|
||||
let mut account_modified = account.clone();
|
||||
account_modified.rent_epoch += 1;
|
||||
assert_eq!(
|
||||
hash,
|
||||
AccountsDB::hash_frozen_account_data(&account_modified)
|
||||
);
|
||||
|
||||
// Account data may not be modified
|
||||
let mut account_modified = account.clone();
|
||||
account_modified.data[0] = 42;
|
||||
assert_ne!(
|
||||
hash,
|
||||
AccountsDB::hash_frozen_account_data(&account_modified)
|
||||
);
|
||||
|
||||
// Owner may not be modified
|
||||
let mut account_modified = account.clone();
|
||||
account_modified.owner =
|
||||
Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
|
||||
assert_ne!(
|
||||
hash,
|
||||
AccountsDB::hash_frozen_account_data(&account_modified)
|
||||
);
|
||||
|
||||
// Executable may not be modified
|
||||
let mut account_modified = account.clone();
|
||||
account_modified.executable = true;
|
||||
assert_ne!(
|
||||
hash,
|
||||
AccountsDB::hash_frozen_account_data(&account_modified)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frozen_account_lamport_increase() {
|
||||
let frozen_pubkey =
|
||||
Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
|
||||
let mut db = AccountsDB::new(Vec::new());
|
||||
|
||||
let mut account = Account::new(1, 42, &frozen_pubkey);
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
let ancestors = vec![(0, 0)].into_iter().collect();
|
||||
db.freeze_accounts(&ancestors, &[frozen_pubkey]);
|
||||
|
||||
// Store with no account changes is ok
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
// Store with an increase in lamports is ok
|
||||
account.lamports = 2;
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
// Store with an decrease that does not go below the frozen amount of lamports is tolerated
|
||||
account.lamports = 1;
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
// A store of any value over the frozen value of '1' across different slots is also ok
|
||||
account.lamports = 3;
|
||||
db.store(1, &[(&frozen_pubkey, &account)]);
|
||||
account.lamports = 2;
|
||||
db.store(2, &[(&frozen_pubkey, &account)]);
|
||||
account.lamports = 1;
|
||||
db.store(3, &[(&frozen_pubkey, &account)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Frozen account My11111111111111111111111111111111111111111 modified. Lamports decreased from 1 to 0"
|
||||
)]
|
||||
fn test_frozen_account_lamport_decrease() {
|
||||
let frozen_pubkey =
|
||||
Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
|
||||
let mut db = AccountsDB::new(Vec::new());
|
||||
|
||||
let mut account = Account::new(1, 42, &frozen_pubkey);
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
let ancestors = vec![(0, 0)].into_iter().collect();
|
||||
db.freeze_accounts(&ancestors, &[frozen_pubkey]);
|
||||
|
||||
// Store with a decrease below the frozen amount of lamports is not ok
|
||||
account.lamports -= 1;
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Unable to freeze an account that does not exist: My11111111111111111111111111111111111111111"
|
||||
)]
|
||||
fn test_frozen_account_nonexistent() {
|
||||
let frozen_pubkey =
|
||||
Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
|
||||
let mut db = AccountsDB::new(Vec::new());
|
||||
|
||||
let ancestors = vec![(0, 0)].into_iter().collect();
|
||||
db.freeze_accounts(&ancestors, &[frozen_pubkey]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Frozen account My11111111111111111111111111111111111111111 modified. Hash changed from 8wHcxDkjiwdrkPAsDnmNrF1UDGJFAtZzPQBSVweY3yRA to JdscGYB1uczVssmYuJusDD1Bfe6wpNeeho8XjcH8inN"
|
||||
)]
|
||||
fn test_frozen_account_data_modified() {
|
||||
let frozen_pubkey =
|
||||
Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
|
||||
let mut db = AccountsDB::new(Vec::new());
|
||||
|
||||
let mut account = Account::new(1, 42, &frozen_pubkey);
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
|
||||
let ancestors = vec![(0, 0)].into_iter().collect();
|
||||
db.freeze_accounts(&ancestors, &[frozen_pubkey]);
|
||||
|
||||
account.data[0] = 42;
|
||||
db.store(0, &[(&frozen_pubkey, &account)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_stored_account() {
|
||||
// This test uses some UNSAFE trick to detect most of account's field
|
||||
|
Reference in New Issue
Block a user