diff --git a/Cargo.lock b/Cargo.lock index cffcd0831..e3d9db5b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4488,6 +4488,7 @@ dependencies = [ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", "libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/core/src/validator.rs b/core/src/validator.rs index 7a9c062db..1d434b44d 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -78,6 +78,7 @@ pub struct ValidatorConfig { pub trusted_validators: Option>, // None = trust all pub halt_on_trusted_validators_accounts_hash_mismatch: bool, pub accounts_hash_fault_injection_slots: u64, // 0 = no fault injection + pub frozen_accounts: Vec, } impl Default for ValidatorConfig { @@ -102,6 +103,7 @@ impl Default for ValidatorConfig { trusted_validators: None, halt_on_trusted_validators_accounts_hash_mismatch: false, accounts_hash_fault_injection_slots: 0, + frozen_accounts: vec![], } } } @@ -594,6 +596,7 @@ fn new_banks_from_blockstore( poh_verify, dev_halt_at_slot: config.dev_halt_at_slot, new_hard_forks: config.new_hard_forks.clone(), + frozen_accounts: config.frozen_accounts.clone(), ..blockstore_processor::ProcessOptions::default() }; diff --git a/core/tests/bank_forks.rs b/core/tests/bank_forks.rs index 9e68b1bab..dc42ab8d9 100644 --- a/core/tests/bank_forks.rs +++ b/core/tests/bank_forks.rs @@ -45,6 +45,7 @@ mod tests { let bank0 = Bank::new_with_paths( &genesis_config_info.genesis_config, vec![accounts_dir.path().to_path_buf()], + &[], ); bank0.freeze(); let mut bank_forks = BankForks::new(0, bank0); @@ -80,6 +81,7 @@ mod tests { let deserialized_bank = snapshot_utils::bank_from_archive( &account_paths, + &[], &old_bank_forks .snapshot_config .as_ref() diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index a32227840..109a32ee4 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -66,6 +66,7 @@ pub fn load( let deserialized_bank = snapshot_utils::bank_from_archive( &account_paths, + &process_options.frozen_accounts, &snapshot_config.snapshot_path, &archive_filename, ) diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 9d8571bf1..2d03d4a52 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -23,6 +23,7 @@ use solana_sdk::{ clock::{Slot, MAX_RECENT_BLOCKHASHES}, genesis_config::GenesisConfig, hash::Hash, + pubkey::Pubkey, signature::Keypair, timing::duration_as_ms, transaction::{Result, Transaction, TransactionError}, @@ -271,6 +272,7 @@ pub struct ProcessOptions { pub entry_callback: Option, pub override_num_threads: Option, pub new_hard_forks: Option>, + pub frozen_accounts: Vec, } pub fn process_blockstore( @@ -289,7 +291,11 @@ pub fn process_blockstore( } // Setup bank for slot 0 - let bank0 = Arc::new(Bank::new_with_paths(&genesis_config, account_paths)); + let bank0 = Arc::new(Bank::new_with_paths( + &genesis_config, + account_paths, + &opts.frozen_accounts, + )); info!("processing ledger for slot 0..."); let recyclers = VerifyRecyclers::default(); process_bank_0(&bank0, blockstore, &opts, &recyclers)?; @@ -2611,7 +2617,7 @@ pub mod tests { genesis_config: &GenesisConfig, account_paths: Vec, ) -> EpochSchedule { - let bank = Bank::new_with_paths(&genesis_config, account_paths); + let bank = Bank::new_with_paths(&genesis_config, account_paths, &[]); bank.epoch_schedule().clone() } diff --git a/ledger/src/snapshot_utils.rs b/ledger/src/snapshot_utils.rs index a66007a7f..40c0000bb 100644 --- a/ledger/src/snapshot_utils.rs +++ b/ledger/src/snapshot_utils.rs @@ -12,7 +12,7 @@ use solana_runtime::{ MAX_SNAPSHOT_DATA_FILE_SIZE, }, }; -use solana_sdk::{clock::Slot, hash::Hash}; +use solana_sdk::{clock::Slot, hash::Hash, pubkey::Pubkey}; use std::{ cmp::Ordering, env, @@ -432,6 +432,7 @@ pub fn remove_snapshot>(slot: Slot, snapshot_path: P) -> Result<( pub fn bank_from_archive>( account_paths: &[PathBuf], + frozen_account_pubkeys: &[Pubkey], snapshot_path: &PathBuf, snapshot_tar: P, ) -> Result { @@ -450,6 +451,7 @@ pub fn bank_from_archive>( let bank = rebuild_bank_from_snapshots( snapshot_version.trim(), account_paths, + frozen_account_pubkeys, &unpacked_snapshots_dir, unpacked_accounts_dir, )?; @@ -575,6 +577,7 @@ pub fn untar_snapshot_in, Q: AsRef>( fn rebuild_bank_from_snapshots

( snapshot_version: &str, account_paths: &[PathBuf], + frozen_account_pubkeys: &[Pubkey], unpacked_snapshots_dir: &PathBuf, append_vecs_path: P, ) -> Result @@ -606,12 +609,16 @@ where } }; info!("Rebuilding accounts..."); - bank.set_bank_rc( - bank::BankRc::new(account_paths.to_vec(), 0, bank.slot()), - bank::StatusCacheRc::default(), - ); - bank.rc - .accounts_from_stream(stream.by_ref(), &append_vecs_path)?; + let rc = bank::BankRc::from_stream( + account_paths, + bank.slot(), + &bank.ancestors, + frozen_account_pubkeys, + stream.by_ref(), + &append_vecs_path, + )?; + + bank.set_bank_rc(rc, bank::StatusCacheRc::default()); Ok(bank) }, )?; diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 835a883cc..a4447a697 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -300,7 +300,7 @@ impl LocalCluster { self.exit(); for (_, node) in self.validators.iter_mut() { if let Some(v) = node.validator.take() { - v.join().unwrap(); + v.join().expect("Validator join failed"); } } diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index d49e3c190..e4f41f6af 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -18,13 +18,14 @@ use solana_local_cluster::{ local_cluster::{ClusterConfig, LocalCluster}, }; use solana_sdk::{ - client::SyncClient, + client::{AsyncClient, SyncClient}, clock::{self, Slot}, commitment_config::CommitmentConfig, epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, genesis_config::OperatingMode, hash::Hash, poh_config::PohConfig, + pubkey::Pubkey, signature::{Keypair, Signer}, }; use std::sync::atomic::{AtomicBool, Ordering}; @@ -548,7 +549,7 @@ fn test_listener_startup() { #[test] #[serial] -fn test_softlaunch_operating_mode() { +fn test_stable_operating_mode() { solana_logger::setup(); let config = ClusterConfig { @@ -567,7 +568,7 @@ fn test_softlaunch_operating_mode() { solana_core::cluster_info::VALIDATOR_PORT_RANGE, ); - // Programs that are available at soft launch epoch 0 + // Programs that are available at epoch 0 for program_id in [ &solana_config_program::id(), &solana_sdk::system_program::id(), @@ -587,7 +588,7 @@ fn test_softlaunch_operating_mode() { ); } - // Programs that are not available at soft launch epoch 0 + // Programs that are not available at epoch 0 for program_id in [ &solana_sdk::bpf_loader::id(), &solana_storage_program::id(), @@ -607,6 +608,110 @@ fn test_softlaunch_operating_mode() { } } +fn generate_frozen_account_panic(mut cluster: LocalCluster, frozen_account: Arc) { + let client = cluster + .get_validator_client(&frozen_account.pubkey()) + .unwrap(); + + // Check the validator is alive by poking it over RPC + trace!( + "validator slot: {}", + client + .get_slot_with_commitment(CommitmentConfig::recent()) + .expect("get slot") + ); + + // Reset the frozen account panic signal + solana_runtime::accounts_db::FROZEN_ACCOUNT_PANIC.store(false, Ordering::Relaxed); + + // Wait for the frozen account panic signal + let mut i = 0; + while !solana_runtime::accounts_db::FROZEN_ACCOUNT_PANIC.load(Ordering::Relaxed) { + // Transfer from frozen account + let (blockhash, _fee_calculator) = client + .get_recent_blockhash_with_commitment(CommitmentConfig::recent()) + .unwrap(); + client + .async_transfer(1, &frozen_account, &Pubkey::new_rand(), blockhash) + .unwrap(); + + sleep(Duration::from_secs(1)); + i += 1; + if i > 10 { + panic!("FROZEN_ACCOUNT_PANIC still false"); + } + } + + // The validator is now broken and won't shutdown properly. Avoid LocalCluster panic in Drop + // with some manual cleanup: + cluster.exit(); + cluster.validators = HashMap::default(); +} + +#[test] +#[serial] +fn test_frozen_account_from_genesis() { + solana_logger::setup(); + let validator_identity = + Arc::new(solana_sdk::signature::keypair_from_seed(&[0u8; 32]).unwrap()); + + let config = ClusterConfig { + validator_keys: Some(vec![validator_identity.clone()]), + node_stakes: vec![100; 1], + cluster_lamports: 1_000, + validator_configs: vec![ + ValidatorConfig { + // Freeze the validator identity account + frozen_accounts: vec![validator_identity.pubkey()], + ..ValidatorConfig::default() + }; + 1 + ], + ..ClusterConfig::default() + }; + generate_frozen_account_panic(LocalCluster::new(&config), validator_identity); +} + +#[test] +#[serial] +fn test_frozen_account_from_snapshot() { + solana_logger::setup(); + let validator_identity = + Arc::new(solana_sdk::signature::keypair_from_seed(&[0u8; 32]).unwrap()); + + let mut snapshot_test_config = setup_snapshot_validator_config(5, 1); + // Freeze the validator identity account + snapshot_test_config.validator_config.frozen_accounts = vec![validator_identity.pubkey()]; + + let config = ClusterConfig { + validator_keys: Some(vec![validator_identity.clone()]), + node_stakes: vec![100; 1], + cluster_lamports: 1_000, + validator_configs: vec![snapshot_test_config.validator_config.clone()], + ..ClusterConfig::default() + }; + let mut cluster = LocalCluster::new(&config); + + let snapshot_package_output_path = &snapshot_test_config + .validator_config + .snapshot_config + .as_ref() + .unwrap() + .snapshot_package_output_path; + + trace!("Waiting for snapshot at {:?}", snapshot_package_output_path); + let (archive_filename, _archive_snapshot_hash) = + wait_for_next_snapshot(&cluster, &snapshot_package_output_path); + + trace!("Found snapshot: {:?}", archive_filename); + + // Restart the validator from a snapshot + let validator_info = cluster.exit_node(&validator_identity.pubkey()); + cluster.restart_node(&validator_identity.pubkey(), validator_info); + + generate_frozen_account_panic(cluster, validator_identity); +} + #[test] #[serial] fn test_consistency_halt() { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 18c1dd8a7..e28a3e800 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -15,6 +15,7 @@ byteorder = "1.3.4" fnv = "1.0.6" fs_extra = "1.1.0" itertools = "0.9.0" +lazy_static = "1.4.0" libc = "0.2.68" libloading = "0.5.2" log = "0.4.8" diff --git a/runtime/benches/accounts.rs b/runtime/benches/accounts.rs index 56018132b..19b53beee 100644 --- a/runtime/benches/accounts.rs +++ b/runtime/benches/accounts.rs @@ -32,7 +32,7 @@ fn bench_has_duplicates(bencher: &mut Bencher) { #[bench] fn test_accounts_create(bencher: &mut Bencher) { let (genesis_config, _) = create_genesis_config(10_000); - let bank0 = Bank::new_with_paths(&genesis_config, vec![PathBuf::from("bench_a0")]); + let bank0 = Bank::new_with_paths(&genesis_config, vec![PathBuf::from("bench_a0")], &[]); bencher.iter(|| { let mut pubkeys: Vec = vec![]; deposit_many(&bank0, &mut pubkeys, 1000); @@ -46,6 +46,7 @@ fn test_accounts_squash(bencher: &mut Bencher) { banks.push(Arc::new(Bank::new_with_paths( &genesis_config, vec![PathBuf::from("bench_a1")], + &[], ))); let mut pubkeys: Vec = vec![]; deposit_many(&banks[0], &mut pubkeys, 250000); diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index d884cdc05..a794fdd12 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -60,15 +60,9 @@ pub type TransactionLoadResult = (TransactionAccounts, TransactionLoaders, Trans impl Accounts { pub fn new(paths: Vec) -> Self { - let accounts_db = Arc::new(AccountsDB::new(paths)); - - Accounts { - slot: 0, - accounts_db, - account_locks: Mutex::new(HashSet::new()), - readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), - } + Self::new_with_frozen_accounts(paths, &HashMap::default(), &[]) } + pub fn new_from_parent(parent: &Accounts, slot: Slot, parent_slot: Slot) -> Self { let accounts_db = parent.accounts_db.clone(); accounts_db.set_hash(slot, parent_slot); @@ -80,13 +74,48 @@ impl Accounts { } } - pub fn accounts_from_stream>( - &self, + pub fn new_with_frozen_accounts( + paths: Vec, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + ) -> Self { + let mut accounts = Accounts { + slot: 0, + accounts_db: Arc::new(AccountsDB::new(paths)), + account_locks: Mutex::new(HashSet::new()), + readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + }; + accounts.freeze_accounts(ancestors, frozen_account_pubkeys); + accounts + } + + pub fn freeze_accounts( + &mut self, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + ) { + Arc::get_mut(&mut self.accounts_db) + .unwrap() + .freeze_accounts(ancestors, frozen_account_pubkeys); + } + + pub fn from_stream>( + account_paths: &[PathBuf], + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], stream: &mut BufReader, - append_vecs_path: P, - ) -> std::result::Result<(), IOError> { - self.accounts_db - .accounts_from_stream(stream, append_vecs_path) + stream_append_vecs_path: P, + ) -> std::result::Result { + let mut accounts_db = AccountsDB::new(account_paths.to_vec()); + accounts_db.accounts_from_stream(stream, stream_append_vecs_path)?; + accounts_db.freeze_accounts(ancestors, frozen_account_pubkeys); + + Ok(Accounts { + slot: 0, + accounts_db: Arc::new(accounts_db), + account_locks: Mutex::new(HashSet::new()), + readonly_locks: Arc::new(RwLock::new(Some(HashMap::new()))), + }) } /// Return true if the slice has any duplicate elements @@ -1401,10 +1430,14 @@ mod tests { let buf = writer.into_inner(); let mut reader = BufReader::new(&buf[..]); let (_accounts_dir, daccounts_paths) = get_temp_accounts_paths(2).unwrap(); - let daccounts = Accounts::new(daccounts_paths.clone()); - assert!(daccounts - .accounts_from_stream(&mut reader, copied_accounts.path()) - .is_ok()); + let daccounts = Accounts::from_stream( + &daccounts_paths, + &HashMap::default(), + &[], + &mut reader, + copied_accounts.path(), + ) + .unwrap(); check_accounts(&daccounts, &pubkeys, 100); assert_eq!(accounts.bank_hash_at(0), daccounts.bank_hash_at(0)); } diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index 2b72496cc..60e15a139 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -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 = { 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, + /// 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>( &self, mut stream: &mut BufReader, - 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, + 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 diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 3fbab35a4..021dbba63 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4,9 +4,7 @@ //! already been signed and verified. use crate::{ accounts::{Accounts, TransactionAccounts, TransactionLoadResult, TransactionLoaders}, - accounts_db::{ - AccountsDBSerialize, AppendVecId, ErrorCounters, SnapshotStorage, SnapshotStorages, - }, + accounts_db::{AccountsDBSerialize, ErrorCounters, SnapshotStorage, SnapshotStorages}, blockhash_queue::BlockhashQueue, message_processor::{MessageProcessor, ProcessInstruction}, nonce_utils, @@ -85,30 +83,30 @@ pub struct BankRc { } impl BankRc { - pub fn new(account_paths: Vec, id: AppendVecId, slot: Slot) -> Self { - let accounts = Accounts::new(account_paths); - accounts - .accounts_db - .next_id - .store(id as usize, Ordering::Relaxed); - BankRc { + pub fn from_stream>( + account_paths: &[PathBuf], + slot: Slot, + ancestors: &HashMap, + frozen_account_pubkeys: &[Pubkey], + mut stream: &mut BufReader, + stream_append_vecs_path: P, + ) -> std::result::Result { + let _len: usize = + deserialize_from(&mut stream).map_err(|e| BankRc::get_io_error(&e.to_string()))?; + + let accounts = Accounts::from_stream( + account_paths, + ancestors, + frozen_account_pubkeys, + stream, + stream_append_vecs_path, + )?; + + Ok(BankRc { accounts: Arc::new(accounts), parent: RwLock::new(None), slot, - } - } - - pub fn accounts_from_stream>( - &self, - mut stream: &mut BufReader, - append_vecs_path: P, - ) -> std::result::Result<(), IOError> { - let _len: usize = - deserialize_from(&mut stream).map_err(|e| BankRc::get_io_error(&e.to_string()))?; - self.accounts - .accounts_from_stream(stream, append_vecs_path)?; - - Ok(()) + }) } pub fn get_snapshot_storages(&self, slot: Slot) -> SnapshotStorages { @@ -358,14 +356,25 @@ impl Default for BlockhashQueue { impl Bank { pub fn new(genesis_config: &GenesisConfig) -> Self { - Self::new_with_paths(&genesis_config, Vec::new()) + Self::new_with_paths(&genesis_config, Vec::new(), &[]) } - pub fn new_with_paths(genesis_config: &GenesisConfig, paths: Vec) -> Self { + pub fn new_with_paths( + genesis_config: &GenesisConfig, + paths: Vec, + frozen_account_pubkeys: &[Pubkey], + ) -> Self { let mut bank = Self::default(); bank.ancestors.insert(bank.slot(), 0); + bank.rc.accounts = Arc::new(Accounts::new(paths)); bank.process_genesis_config(genesis_config); + + // Freeze accounts after process_genesis_config creates the initial append vecs + Arc::get_mut(&mut bank.rc.accounts) + .unwrap() + .freeze_accounts(&bank.ancestors, frozen_account_pubkeys); + // genesis needs stakes for all epochs up to the epoch implied by // slot = 0 and genesis configuration { @@ -4769,14 +4778,21 @@ mod tests { let (_accounts_dir, dbank_paths) = get_temp_accounts_paths(4).unwrap(); let ref_sc = StatusCacheRc::default(); ref_sc.status_cache.write().unwrap().add_root(2); - dbank.set_bank_rc(BankRc::new(dbank_paths.clone(), 0, dbank.slot()), ref_sc); // Create a directory to simulate AppendVecs unpackaged from a snapshot tar let copied_accounts = TempDir::new().unwrap(); copy_append_vecs(&bank2.rc.accounts.accounts_db, copied_accounts.path()).unwrap(); - dbank - .rc - .accounts_from_stream(&mut reader, copied_accounts.path()) - .unwrap(); + dbank.set_bank_rc( + BankRc::from_stream( + &dbank_paths, + dbank.slot(), + &dbank.ancestors, + &[], + &mut reader, + copied_accounts.path(), + ) + .unwrap(), + ref_sc, + ); assert_eq!(dbank.get_balance(&key1.pubkey()), 0); assert_eq!(dbank.get_balance(&key2.pubkey()), 10); assert_eq!(dbank.get_balance(&key3.pubkey()), 0); diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 546d1fb9d..f092ef575 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -110,7 +110,7 @@ impl PreAccount { return Err(InstructionError::ExecutableModified); } - // No one modifies r ent_epoch (yet). + // No one modifies rent_epoch (yet). if self.rent_epoch != post.rent_epoch { return Err(InstructionError::RentEpochModified); } diff --git a/validator/src/main.rs b/validator/src/main.rs index 80f0f5e57..1cc05e0e2 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1,5 +1,6 @@ use clap::{ - crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App, Arg, ArgMatches, + crate_description, crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit, App, Arg, + ArgMatches, }; use log::*; use rand::{thread_rng, Rng}; @@ -662,6 +663,17 @@ pub fn main() { .takes_value(false) .help("Abort the validator if a bank hash mismatch is detected within trusted validator set"), ) + .arg( + clap::Arg::with_name("frozen_accounts") + .long("frozen-account") + .validator(is_pubkey) + .value_name("PUBKEY") + .multiple(true) + .takes_value(true) + .help("Freeze the specified account. This will cause the validator to \ + intentionally crash should any transaction modify the frozen account in any way \ + other than increasing the account balance"), + ) .get_matches(); let identity_keypair = Arc::new(keypair_of(&matches, "identity").unwrap_or_else(Keypair::new)); @@ -733,6 +745,7 @@ pub fn main() { voting_disabled: matches.is_present("no_voting"), wait_for_supermajority: value_t!(matches, "wait_for_supermajority", Slot).ok(), trusted_validators, + frozen_accounts: values_t!(matches, "frozen_accounts", Pubkey).unwrap_or_default(), ..ValidatorConfig::default() };