From 5d82f643cf3e59b116248e1d6a4ab10104a2bde3 Mon Sep 17 00:00:00 2001 From: Ryo Onodera Date: Sat, 12 Sep 2020 01:48:06 +0900 Subject: [PATCH] Check bank capitalization (#11927) * Check bank capitalization * Simplify and unify capitalization calculation * Improve and add tests * Avoid overflow and inhibit automatic restart * Fix test * Tweak checked sum for cap. and add tests * Fix broken build after merge conflicts.. * Rename to ClusterType * Rename confusing method * Clarify comment * Verify cap. in rent and inflation tests Co-authored-by: Stephen Akridge (cherry picked from commit de4a613610017ff2a07654c27b57f3e58e6c20a2) # Conflicts: # Cargo.lock # accounts-bench/Cargo.toml --- Cargo.lock | 5 + accounts-bench/Cargo.toml | 8 + accounts-bench/src/main.rs | 14 +- core/src/non_circulating_supply.rs | 2 +- core/src/rpc.rs | 2 +- ledger-tool/src/main.rs | 17 +- ledger/src/blockstore_processor.rs | 10 ++ runtime/benches/accounts.rs | 8 +- runtime/src/accounts.rs | 31 +++- runtime/src/accounts_db.rs | 252 +++++++++++++++++++++++++---- runtime/src/bank.rs | 114 ++++++++----- sdk/src/genesis_config.rs | 8 +- 12 files changed, 372 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1991beb72a6eee..2cb728194aadb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3261,7 +3261,12 @@ dependencies = [ "solana-logger 1.3.10", "solana-measure", "solana-runtime", +<<<<<<< HEAD "solana-sdk 1.3.10", +======= + "solana-sdk 1.4.0", + "solana-version", +>>>>>>> de4a61361... Check bank capitalization (#11927) ] [[package]] diff --git a/accounts-bench/Cargo.toml b/accounts-bench/Cargo.toml index 8af53d78e3ce9c..2f57afe2d33765 100644 --- a/accounts-bench/Cargo.toml +++ b/accounts-bench/Cargo.toml @@ -10,10 +10,18 @@ homepage = "https://solana.com/" [dependencies] log = "0.4.6" rayon = "1.4.0" +<<<<<<< HEAD solana-logger = { path = "../logger", version = "1.3.10" } solana-runtime = { path = "../runtime", version = "1.3.10" } solana-measure = { path = "../measure", version = "1.3.10" } solana-sdk = { path = "../sdk", version = "1.3.10" } +======= +solana-logger = { path = "../logger", version = "1.4.0" } +solana-runtime = { path = "../runtime", version = "1.4.0" } +solana-measure = { path = "../measure", version = "1.4.0" } +solana-sdk = { path = "../sdk", version = "1.4.0" } +solana-version = { path = "../version", version = "1.4.0" } +>>>>>>> de4a61361... Check bank capitalization (#11927) rand = "0.7.0" clap = "2.33.1" crossbeam-channel = "0.4" diff --git a/accounts-bench/src/main.rs b/accounts-bench/src/main.rs index b88489c57f0210..30ded428254e78 100644 --- a/accounts-bench/src/main.rs +++ b/accounts-bench/src/main.rs @@ -1,4 +1,4 @@ -use clap::{value_t, App, Arg}; +use clap::{crate_description, crate_name, value_t, App, Arg}; use rayon::prelude::*; use solana_measure::measure::Measure; use solana_runtime::{ @@ -6,15 +6,16 @@ use solana_runtime::{ accounts_index::Ancestors, }; use solana_sdk::{genesis_config::ClusterType, pubkey::Pubkey}; +use std::env; use std::fs; use std::path::PathBuf; fn main() { solana_logger::setup(); - let matches = App::new("crate") - .about("about") - .version("version") + let matches = App::new(crate_name!()) + .about(crate_description!()) + .version(solana_version::version!()) .arg( Arg::with_name("num_slots") .long("num_slots") @@ -50,7 +51,8 @@ fn main() { let clean = matches.is_present("clean"); println!("clean: {:?}", clean); - let path = PathBuf::from("farf/accounts-bench"); + let path = PathBuf::from(env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_owned())) + .join("accounts-bench"); if fs::remove_dir_all(path.clone()).is_err() { println!("Warning: Couldn't remove {:?}", path); } @@ -96,7 +98,7 @@ fn main() { } else { let mut pubkeys: Vec = vec![]; let mut time = Measure::start("hash"); - let hash = accounts.accounts_db.update_accounts_hash(0, &ancestors); + let hash = accounts.accounts_db.update_accounts_hash(0, &ancestors).0; time.stop(); println!("hash: {} {}", hash, time); create_test_accounts(&accounts, &mut pubkeys, 1, 0); diff --git a/core/src/non_circulating_supply.rs b/core/src/non_circulating_supply.rs index 68477dd7cf01e9..cff6dea5ab653a 100644 --- a/core/src/non_circulating_supply.rs +++ b/core/src/non_circulating_supply.rs @@ -18,7 +18,7 @@ pub fn calculate_non_circulating_supply(bank: &Arc) -> NonCirculatingSuppl let withdraw_authority_list = withdraw_authority(); let clock = bank.clock(); - let stake_accounts = bank.get_program_accounts(Some(&solana_stake_program::id())); + let stake_accounts = bank.get_program_accounts(&solana_stake_program::id()); for (pubkey, account) in stake_accounts.iter() { let stake_account = StakeState::from(&account).unwrap_or_default(); match stake_account { diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 3873b7cfa03b9c..0cc4a3254b5925 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1308,7 +1308,7 @@ fn get_filtered_program_accounts( program_id: &Pubkey, filters: Vec, ) -> impl Iterator { - bank.get_program_accounts(Some(&program_id)) + bank.get_program_accounts(&program_id) .into_iter() .filter(move |(_, account)| { filters.iter().all(|filter_type| match filter_type { diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 3ad267ec04125a..edcd681792a841 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -44,7 +44,7 @@ use solana_vote_program::{ }; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, - convert::{TryFrom, TryInto}, + convert::TryInto, ffi::OsStr, fs::{self, File}, io::{self, stdout, BufRead, BufReader, Write}, @@ -708,16 +708,7 @@ fn open_genesis_config_by(ledger_path: &Path, matches: &ArgMatches<'_>) -> Genes } fn assert_capitalization(bank: &Bank) { - let calculated_capitalization = bank.calculate_capitalization(); - assert_eq!( - bank.capitalization(), - calculated_capitalization, - "Capitalization mismatch!?: +/-{}", - Sol(u64::try_from( - (i128::from(calculated_capitalization) - i128::from(bank.capitalization())).abs() - ) - .unwrap()), - ); + assert!(bank.calculate_and_verify_capitalization()); } #[allow(clippy::cognitive_complexity)] @@ -1695,7 +1686,7 @@ fn main() { if remove_stake_accounts { for (address, mut account) in bank - .get_program_accounts(Some(&solana_stake_program::id())) + .get_program_accounts(&solana_stake_program::id()) .into_iter() { account.lamports = 0; @@ -1723,7 +1714,7 @@ fn main() { // Delete existing vote accounts for (address, mut account) in bank - .get_program_accounts(Some(&solana_vote_program::id())) + .get_program_accounts(&solana_vote_program::id()) .into_iter() { account.lamports = 0; diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 959d36de9e490b..7c80585ba57f76 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -293,6 +293,9 @@ pub enum BlockstoreProcessorError { #[error("invalid hard fork")] InvalidHardFork(Slot), + + #[error("root bank with mismatched capitalization at {0}")] + RootBankWithMismatchedCapitalization(Slot), } /// Callback for accessing bank state while processing the blockstore @@ -481,6 +484,13 @@ fn do_process_blockstore_from_root( ); assert!(bank_forks.active_banks().is_empty()); + // We might be promptly restarted after bad capitalization was detected while creating newer snapshot. + // In that case, we're most likely restored from the last good snapshot and replayed up to this root. + // So again check here for the bad capitalization to avoid to continue until the next snapshot creation. + if !bank_forks.root_bank().calculate_and_verify_capitalization() { + return Err(BlockstoreProcessorError::RootBankWithMismatchedCapitalization(root)); + } + Ok((bank_forks, leader_schedule_cache)) } diff --git a/runtime/benches/accounts.rs b/runtime/benches/accounts.rs index 47cd7c8361cf99..5ba533a5bfbf61 100644 --- a/runtime/benches/accounts.rs +++ b/runtime/benches/accounts.rs @@ -74,10 +74,12 @@ fn test_accounts_hash_bank_hash(bencher: &mut Bencher) { &ClusterType::Development, ); let mut pubkeys: Vec = vec![]; - create_test_accounts(&accounts, &mut pubkeys, 60000, 0); + let num_accounts = 60_000; + let slot = 0; + create_test_accounts(&accounts, &mut pubkeys, num_accounts, slot); let ancestors = vec![(0, 0)].into_iter().collect(); - accounts.accounts_db.update_accounts_hash(0, &ancestors); - bencher.iter(|| assert!(accounts.verify_bank_hash(0, &ancestors))); + let (_, total_lamports) = accounts.accounts_db.update_accounts_hash(0, &ancestors); + bencher.iter(|| assert!(accounts.verify_bank_hash_and_lamports(0, &ancestors, total_lamports))); } #[bench] diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index d696d6e831080a..7f4226517fe37d 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -428,9 +428,32 @@ impl Accounts { accounts_balances } + pub fn calculate_capitalization(&self, ancestors: &Ancestors) -> u64 { + let balances = self + .load_all(ancestors) + .into_iter() + .map(|(_pubkey, account, _slot)| { + AccountsDB::account_balance_for_capitalization( + account.lamports, + &account.owner, + account.executable, + ) + }); + + AccountsDB::checked_sum_for_capitalization(balances) + } + #[must_use] - pub fn verify_bank_hash(&self, slot: Slot, ancestors: &Ancestors) -> bool { - if let Err(err) = self.accounts_db.verify_bank_hash(slot, ancestors) { + pub fn verify_bank_hash_and_lamports( + &self, + slot: Slot, + ancestors: &Ancestors, + total_lamports: u64, + ) -> bool { + if let Err(err) = + self.accounts_db + .verify_bank_hash_and_lamports(slot, ancestors, total_lamports) + { warn!("verify_bank_hash failed: {:?}", err); false } else { @@ -460,13 +483,13 @@ impl Accounts { pub fn load_by_program( &self, ancestors: &Ancestors, - program_id: Option<&Pubkey>, + program_id: &Pubkey, ) -> Vec<(Pubkey, Account)> { self.accounts_db.scan_accounts( ancestors, |collector: &mut Vec<(Pubkey, Account)>, some_account_tuple| { Self::load_while_filtering(collector, some_account_tuple, |account| { - program_id.is_none() || Some(&account.owner) == program_id + account.owner == *program_id }) }, ) diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index aa0ca163f3fa0d..89fbe7e5b6d6f8 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -40,6 +40,7 @@ use solana_sdk::{ use std::convert::TryFrom; use std::{ collections::{HashMap, HashSet}, + convert::TryInto, io::{Error as IOError, Result as IOResult}, iter::FromIterator, ops::RangeBounds, @@ -154,6 +155,7 @@ pub enum BankHashVerificationError { MismatchedAccountHash, MismatchedBankHash, MissingBankHash, + MismatchedTotalLamports(u64, u64), } /// Persistent storage structure holding the accounts @@ -1614,8 +1616,11 @@ impl AccountsDB { ); } - pub fn compute_merkle_root(hashes: Vec<(Pubkey, Hash)>, fanout: usize) -> Hash { - let hashes: Vec<_> = hashes.into_iter().map(|(_pubkey, hash)| hash).collect(); + pub fn compute_merkle_root(hashes: Vec<(Pubkey, Hash, u64)>, fanout: usize) -> Hash { + let hashes: Vec<_> = hashes + .into_iter() + .map(|(_pubkey, hash, _lamports)| hash) + .collect(); let mut hashes: Vec<_> = hashes.chunks(fanout).map(|x| x.to_vec()).collect(); while hashes.len() > 1 { let mut time = Measure::start("time"); @@ -1640,27 +1645,77 @@ impl AccountsDB { hasher.result() } - fn accumulate_account_hashes(mut hashes: Vec<(Pubkey, Hash)>) -> Hash { - let mut sort = Measure::start("sort"); + fn accumulate_account_hashes(hashes: Vec<(Pubkey, Hash, u64)>) -> Hash { + let (hash, ..) = Self::do_accumulate_account_hashes_and_capitalization(hashes, false); + hash + } + + fn accumulate_account_hashes_and_capitalization( + hashes: Vec<(Pubkey, Hash, u64)>, + ) -> (Hash, u64) { + let (hash, cap) = Self::do_accumulate_account_hashes_and_capitalization(hashes, true); + (hash, cap.unwrap()) + } + + fn do_accumulate_account_hashes_and_capitalization( + mut hashes: Vec<(Pubkey, Hash, u64)>, + calculate_cap: bool, + ) -> (Hash, Option) { + let mut sort_time = Measure::start("sort"); hashes.par_sort_by(|a, b| a.0.cmp(&b.0)); - sort.stop(); - let mut hash_time = Measure::start("hash"); + sort_time.stop(); - let fanout = 16; + let mut sum_time = Measure::start("cap"); + let cap = if calculate_cap { + Some(Self::checked_sum_for_capitalization( + hashes.iter().map(|(_, _, lamports)| *lamports), + )) + } else { + None + }; + sum_time.stop(); + let mut hash_time = Measure::start("hash"); + let fanout = 16; let res = Self::compute_merkle_root(hashes, fanout); - hash_time.stop(); - debug!("{} {}", sort, hash_time); - res + debug!("{} {} {}", sort_time, hash_time, sum_time); + + (res, cap) + } + + pub fn checked_sum_for_capitalization>(balances: T) -> u64 { + balances + .map(|b| b as u128) + .sum::() + .try_into() + .expect("overflow is detected while summing capitalization") + } + + pub fn account_balance_for_capitalization( + lamports: u64, + owner: &Pubkey, + executable: bool, + ) -> u64 { + let is_specially_retained = (solana_sdk::native_loader::check_id(owner) && executable) + || solana_sdk::sysvar::check_id(owner); + + if is_specially_retained { + // specially retained accounts always have an initial 1 lamport + // balance, but could be modified by transfers which increase + // the balance but don't affect the capitalization. + lamports - 1 + } else { + lamports + } } fn calculate_accounts_hash( &self, ancestors: &Ancestors, check_hash: bool, - ) -> Result { + ) -> Result<(Hash, u64), BankHashVerificationError> { use BankHashVerificationError::*; let mut scan = Measure::start("scan"); let accounts_index = self.accounts_index.read().unwrap(); @@ -1679,6 +1734,11 @@ impl AccountsDB { .and_then(|storage_map| storage_map.get(&account_info.store_id)) .and_then(|store| { let account = store.accounts.get_account(account_info.offset)?.0; + let balance = Self::account_balance_for_capitalization( + account_info.lamports, + &account.account_meta.owner, + account.account_meta.executable, + ); if check_hash { let hash = Self::hash_stored_account( @@ -1694,7 +1754,7 @@ impl AccountsDB { } } - Some((**pubkey, *account.hash)) + Some((**pubkey, *account.hash, balance)) }) } else { None @@ -1716,7 +1776,8 @@ impl AccountsDB { let hash_total = hashes.len(); let mut accumulate = Measure::start("accumulate"); - let accumulated_hash = Self::accumulate_account_hashes(hashes); + let (accumulated_hash, total_lamports) = + Self::accumulate_account_hashes_and_capitalization(hashes); accumulate.stop(); datapoint_info!( "update_accounts_hash", @@ -1724,7 +1785,7 @@ impl AccountsDB { ("hash_accumulate", accumulate.as_us(), i64), ("hash_total", hash_total, i64), ); - Ok(accumulated_hash) + Ok((accumulated_hash, total_lamports)) } pub fn get_accounts_hash(&self, slot: Slot) -> Hash { @@ -1733,22 +1794,32 @@ impl AccountsDB { bank_hash_info.snapshot_hash } - pub fn update_accounts_hash(&self, slot: Slot, ancestors: &Ancestors) -> Hash { - let hash = self.calculate_accounts_hash(ancestors, false).unwrap(); + pub fn update_accounts_hash(&self, slot: Slot, ancestors: &Ancestors) -> (Hash, u64) { + let (hash, total_lamports) = self.calculate_accounts_hash(ancestors, false).unwrap(); let mut bank_hashes = self.bank_hashes.write().unwrap(); let mut bank_hash_info = bank_hashes.get_mut(&slot).unwrap(); bank_hash_info.snapshot_hash = hash; - hash + (hash, total_lamports) } - pub fn verify_bank_hash( + pub fn verify_bank_hash_and_lamports( &self, slot: Slot, ancestors: &Ancestors, + total_lamports: u64, ) -> Result<(), BankHashVerificationError> { use BankHashVerificationError::*; - let calculated_hash = self.calculate_accounts_hash(ancestors, true)?; + let (calculated_hash, calculated_lamports) = + self.calculate_accounts_hash(ancestors, true)?; + + if calculated_lamports != total_lamports { + warn!( + "Mismatched total lamports: {} calculated: {}", + total_lamports, calculated_lamports + ); + return Err(MismatchedTotalLamports(calculated_lamports, total_lamports)); + } let bank_hashes = self.bank_hashes.read().unwrap(); if let Some(found_hash_info) = bank_hashes.get(&slot) { @@ -1789,7 +1860,7 @@ impl AccountsDB { let mut accumulate = Measure::start("accumulate"); let hashes: Vec<_> = account_maps .into_iter() - .map(|(pubkey, (_, hash))| (pubkey, hash)) + .map(|(pubkey, (_, hash))| (pubkey, hash, 0)) .collect(); let ret = Self::accumulate_account_hashes(hashes); accumulate.stop(); @@ -3305,7 +3376,9 @@ pub mod tests { assert_load_account(&accounts, current_slot, purged_pubkey2, 0); assert_load_account(&accounts, current_slot, dummy_pubkey, dummy_lamport); - accounts.verify_bank_hash(4, &HashMap::default()).unwrap(); + accounts + .verify_bank_hash_and_lamports(4, &HashMap::default(), 1222) + .unwrap(); } #[test] @@ -3694,7 +3767,7 @@ pub mod tests { solana_logger::setup(); let db = AccountsDB::new(Vec::new(), &ClusterType::Development); - let key = Pubkey::default(); + let key = Pubkey::new_rand(); let some_data_len = 0; let some_slot: Slot = 0; let account = Account::new(1, some_data_len, &key); @@ -3703,11 +3776,14 @@ pub mod tests { db.store(some_slot, &[(&key, &account)]); db.add_root(some_slot); db.update_accounts_hash(some_slot, &ancestors); - assert_matches!(db.verify_bank_hash(some_slot, &ancestors), Ok(_)); + assert_matches!( + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), + Ok(_) + ); db.bank_hashes.write().unwrap().remove(&some_slot).unwrap(); assert_matches!( - db.verify_bank_hash(some_slot, &ancestors), + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), Err(MissingBankHash) ); @@ -3722,11 +3798,51 @@ pub mod tests { .unwrap() .insert(some_slot, bank_hash_info); assert_matches!( - db.verify_bank_hash(some_slot, &ancestors), + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), Err(MismatchedBankHash) ); } + #[test] + fn test_verify_bank_capitalization() { + use BankHashVerificationError::*; + solana_logger::setup(); + let db = AccountsDB::new(Vec::new(), &ClusterType::Development); + + let key = Pubkey::new_rand(); + 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); + db.update_accounts_hash(some_slot, &ancestors); + assert_matches!( + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), + Ok(_) + ); + + let native_account_pubkey = Pubkey::new_rand(); + db.store( + some_slot, + &[( + &native_account_pubkey, + &solana_sdk::native_loader::create_loadable_account("foo"), + )], + ); + db.update_accounts_hash(some_slot, &ancestors); + assert_matches!( + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), + Ok(_) + ); + + assert_matches!( + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 10), + Err(MismatchedTotalLamports(expected, actual)) if expected == 1 && actual == 10 + ); + } + #[test] fn test_verify_bank_hash_no_account() { solana_logger::setup(); @@ -3741,7 +3857,10 @@ pub mod tests { .insert(some_slot, BankHashInfo::default()); db.add_root(some_slot); db.update_accounts_hash(some_slot, &ancestors); - assert_matches!(db.verify_bank_hash(some_slot, &ancestors), Ok(_)); + assert_matches!( + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 0), + Ok(_) + ); } #[test] @@ -3764,7 +3883,7 @@ pub mod tests { db.store_with_hashes(some_slot, accounts, &[some_hash]); db.add_root(some_slot); assert_matches!( - db.verify_bank_hash(some_slot, &ancestors), + db.verify_bank_hash_and_lamports(some_slot, &ancestors, 1), Err(MismatchedAccountHash) ); } @@ -4184,12 +4303,12 @@ pub mod tests { let no_ancestors = HashMap::default(); accounts.update_accounts_hash(current_slot, &no_ancestors); accounts - .verify_bank_hash(current_slot, &no_ancestors) + .verify_bank_hash_and_lamports(current_slot, &no_ancestors, 22300) .unwrap(); let accounts = reconstruct_accounts_db_via_serialization(&accounts, current_slot); accounts - .verify_bank_hash(current_slot, &no_ancestors) + .verify_bank_hash_and_lamports(current_slot, &no_ancestors, 22300) .unwrap(); // repeating should be no-op @@ -4376,4 +4495,79 @@ pub mod tests { shrink_thread.join().unwrap(); } } + + #[test] + fn test_account_balance_for_capitalization_normal() { + // system accounts + assert_eq!( + AccountsDB::account_balance_for_capitalization(10, &Pubkey::default(), false), + 10 + ); + // any random program data accounts + assert_eq!( + AccountsDB::account_balance_for_capitalization(10, &Pubkey::new_rand(), false), + 10 + ); + } + + #[test] + fn test_account_balance_for_capitalization_sysvar() { + use solana_sdk::sysvar::Sysvar; + + let normal_sysvar = solana_sdk::slot_history::SlotHistory::default().create_account(1); + assert_eq!( + AccountsDB::account_balance_for_capitalization( + normal_sysvar.lamports, + &normal_sysvar.owner, + normal_sysvar.executable + ), + 0 + ); + + // currently transactions can send any lamports to sysvars although this is not sensible. + assert_eq!( + AccountsDB::account_balance_for_capitalization(10, &solana_sdk::sysvar::id(), false), + 9 + ); + } + + #[test] + fn test_account_balance_for_capitalization_native_program() { + let normal_native_program = solana_sdk::native_loader::create_loadable_account("foo"); + assert_eq!( + AccountsDB::account_balance_for_capitalization( + normal_native_program.lamports, + &normal_native_program.owner, + normal_native_program.executable + ), + 0 + ); + + // test maliciously assigned bogus native loader account + assert_eq!( + AccountsDB::account_balance_for_capitalization( + 1, + &solana_sdk::native_loader::id(), + false + ), + 1 + ) + } + + #[test] + fn test_checked_sum_for_capitalization_normal() { + assert_eq!( + AccountsDB::checked_sum_for_capitalization(vec![1, 2].into_iter()), + 3 + ); + } + + #[test] + #[should_panic(expected = "overflow is detected while summing capitalization")] + fn test_checked_sum_for_capitalization_overflow() { + assert_eq!( + AccountsDB::checked_sum_for_capitalization(vec![1, u64::max_value()].into_iter()), + 3 + ); + } } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index b5441fd6d418c7..c1e5ffa5eea29f 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2624,6 +2624,28 @@ impl Bank { } } + #[cfg(test)] + fn add_account_and_update_capitalization(&self, pubkey: &Pubkey, new_account: &Account) { + if let Some(old_account) = self.get_account(&pubkey) { + if new_account.lamports > old_account.lamports { + self.capitalization.fetch_add( + new_account.lamports - old_account.lamports, + Ordering::Relaxed, + ); + } else { + self.capitalization.fetch_sub( + old_account.lamports - new_account.lamports, + Ordering::Relaxed, + ); + } + } else { + self.capitalization + .fetch_add(new_account.lamports, Ordering::Relaxed); + } + + self.store_account(pubkey, new_account); + } + pub fn withdraw(&self, pubkey: &Pubkey, lamports: u64) -> Result<()> { match self.get_account(pubkey) { Some(mut account) => { @@ -2738,7 +2760,7 @@ impl Bank { .map(|(acc, _slot)| acc) } - pub fn get_program_accounts(&self, program_id: Option<&Pubkey>) -> Vec<(Pubkey, Account)> { + pub fn get_program_accounts(&self, program_id: &Pubkey) -> Vec<(Pubkey, Account)> { self.rc .accounts .load_by_program(&self.ancestors, program_id) @@ -2872,9 +2894,11 @@ impl Bank { /// snapshot. #[must_use] fn verify_bank_hash(&self) -> bool { - self.rc - .accounts - .verify_bank_hash(self.slot(), &self.ancestors) + self.rc.accounts.verify_bank_hash_and_lamports( + self.slot(), + &self.ancestors, + self.capitalization(), + ) } pub fn get_snapshot_storages(&self) -> SnapshotStorages { @@ -2904,22 +2928,21 @@ impl Bank { } pub fn calculate_capitalization(&self) -> u64 { - self.get_program_accounts(None) - .into_iter() - .map(|(_pubkey, account)| { - let is_specially_retained = solana_sdk::native_loader::check_id(&account.owner) - || solana_sdk::sysvar::check_id(&account.owner); - - if is_specially_retained { - // specially retained accounts are ensured to exist by - // always having a balance of 1 lamports, which is - // outside the capitalization calculation. - account.lamports - 1 - } else { - account.lamports - } - }) - .sum() + self.rc.accounts.calculate_capitalization(&self.ancestors) + } + + pub fn calculate_and_verify_capitalization(&self) -> bool { + let calculated = self.calculate_capitalization(); + let expected = self.capitalization(); + if calculated == expected { + true + } else { + warn!( + "Capitalization mismatch: calculated: {} != expected: {}", + calculated, expected + ); + false + } } /// Forcibly overwrites current capitalization by actually recalculating accounts' balances. @@ -2936,10 +2959,13 @@ impl Bank { } pub fn update_accounts_hash(&self) -> Hash { - self.rc + let (hash, total_lamports) = self + .rc .accounts .accounts_db - .update_accounts_hash(self.slot(), &self.ancestors) + .update_accounts_hash(self.slot(), &self.ancestors); + assert_eq!(total_lamports, self.capitalization()); + hash } /// A snapshot bank should be purged of 0 lamport accounts which are not part of the hash @@ -3898,6 +3924,8 @@ mod tests { #[test] fn test_rent_distribution() { + solana_logger::setup(); + let bootstrap_validator_pubkey = Pubkey::new_rand(); let bootstrap_validator_stake_lamports = 30; let mut genesis_config = create_genesis_config_with_leader( @@ -4033,11 +4061,11 @@ mod tests { let payer = Keypair::new(); let payer_account = Account::new(400, 0, &system_program::id()); - bank.store_account(&payer.pubkey(), &payer_account); + bank.add_account_and_update_capitalization(&payer.pubkey(), &payer_account); let payee = Keypair::new(); let payee_account = Account::new(70, 1, &system_program::id()); - bank.store_account(&payee.pubkey(), &payee_account); + bank.add_account_and_update_capitalization(&payee.pubkey(), &payee_account); let bootstrap_validator_initial_balance = bank.get_balance(&bootstrap_validator_pubkey); @@ -4118,6 +4146,8 @@ mod tests { previous_capitalization - current_capitalization, burned_portion ); + bank.freeze(); + assert!(bank.calculate_and_verify_capitalization()); } #[test] @@ -5066,6 +5096,8 @@ mod tests { #[test] fn test_bank_update_rewards() { + solana_logger::setup(); + // create a bank that ticks really slowly... let bank = Arc::new(Bank::new(&GenesisConfig { accounts: (0..42) @@ -5102,7 +5134,7 @@ mod tests { crate::stakes::tests::create_staked_node_accounts(1_0000); // set up accounts - bank.store_account(&stake_id, &stake_account); + bank.add_account_and_update_capitalization(&stake_id, &stake_account); // generate some rewards let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); @@ -5112,7 +5144,7 @@ mod tests { } let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); VoteState::to(&versioned, &mut vote_account).unwrap(); - bank.store_account(&vote_id, &vote_account); + bank.add_account_and_update_capitalization(&vote_id, &vote_account); match versioned { VoteStateVersions::Current(v) => { vote_state = Some(*v); @@ -5120,7 +5152,7 @@ mod tests { _ => panic!("Has to be of type Current"), }; } - bank.store_account(&vote_id, &vote_account); + bank.add_account_and_update_capitalization(&vote_id, &vote_account); let validator_points: u128 = bank .stake_delegation_accounts() @@ -5175,6 +5207,8 @@ mod tests { (rewards.validator_point_value * validator_points as f64) as i64 )]) ); + bank1.freeze(); + assert!(bank1.calculate_and_verify_capitalization()); } fn do_test_bank_update_rewards_determinism() -> u64 { @@ -5216,8 +5250,8 @@ mod tests { let (stake_id2, stake_account2) = crate::stakes::tests::create_stake_account(456, &vote_id); // set up accounts - bank.store_account(&stake_id1, &stake_account1); - bank.store_account(&stake_id2, &stake_account2); + bank.add_account_and_update_capitalization(&stake_id1, &stake_account1); + bank.add_account_and_update_capitalization(&stake_id2, &stake_account2); // generate some rewards let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); @@ -5227,7 +5261,7 @@ mod tests { } let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); VoteState::to(&versioned, &mut vote_account).unwrap(); - bank.store_account(&vote_id, &vote_account); + bank.add_account_and_update_capitalization(&vote_id, &vote_account); match versioned { VoteStateVersions::Current(v) => { vote_state = Some(*v); @@ -5235,7 +5269,7 @@ mod tests { _ => panic!("Has to be of type Current"), }; } - bank.store_account(&vote_id, &vote_account); + bank.add_account_and_update_capitalization(&vote_id, &vote_account); // put a child bank in epoch 1, which calls update_rewards()... let bank1 = Bank::new_from_parent( @@ -5246,11 +5280,15 @@ mod tests { // verify that there's inflation assert_ne!(bank1.capitalization(), bank.capitalization()); + bank1.freeze(); + assert!(bank1.calculate_and_verify_capitalization()); bank1.capitalization() } #[test] fn test_bank_update_rewards_determinism() { + solana_logger::setup(); + // The same reward should be distributed given same credits let expected_capitalization = do_test_bank_update_rewards_determinism(); // Repeat somewhat large number of iterations to expose possible different behavior @@ -6861,17 +6899,17 @@ mod tests { let parent = Arc::new(Bank::new(&genesis_config)); parent.lazy_rent_collection.store(true, Ordering::Relaxed); - let genesis_accounts: Vec<_> = parent.get_program_accounts(None); + let genesis_accounts: Vec<_> = parent.get_all_accounts_with_modified_slots(); assert!( genesis_accounts .iter() - .any(|(pubkey, _)| *pubkey == mint_keypair.pubkey()), + .any(|(pubkey, _, _)| *pubkey == mint_keypair.pubkey()), "mint pubkey not found" ); assert!( genesis_accounts .iter() - .any(|(pubkey, _)| solana_sdk::sysvar::is_sysvar_id(pubkey)), + .any(|(pubkey, _, _)| solana_sdk::sysvar::is_sysvar_id(pubkey)), "no sysvars found" ); @@ -6889,11 +6927,11 @@ mod tests { let bank1 = Arc::new(new_from_parent(&bank0)); bank1.squash(); assert_eq!( - bank0.get_program_accounts(Some(&program_id)), + bank0.get_program_accounts(&program_id), vec![(pubkey0, account0.clone())] ); assert_eq!( - bank1.get_program_accounts(Some(&program_id)), + bank1.get_program_accounts(&program_id), vec![(pubkey0, account0)] ); assert_eq!( @@ -6912,8 +6950,8 @@ mod tests { let bank3 = Arc::new(new_from_parent(&bank2)); bank3.squash(); - assert_eq!(bank1.get_program_accounts(Some(&program_id)).len(), 2); - assert_eq!(bank3.get_program_accounts(Some(&program_id)).len(), 2); + assert_eq!(bank1.get_program_accounts(&program_id).len(), 2); + assert_eq!(bank3.get_program_accounts(&program_id).len(), 2); } #[test] diff --git a/sdk/src/genesis_config.rs b/sdk/src/genesis_config.rs index 9478cac8c36ea4..3953028ce11cfd 100644 --- a/sdk/src/genesis_config.rs +++ b/sdk/src/genesis_config.rs @@ -202,10 +202,6 @@ impl GenesisConfig { self.native_instruction_processors.push((name, program_id)); } - pub fn add_rewards_pool(&mut self, pubkey: Pubkey, account: Account) { - self.rewards_pools.insert(pubkey, account); - } - pub fn hashes_per_tick(&self) -> Option { self.poh_config.hashes_per_tick } @@ -244,6 +240,8 @@ impl fmt::Display for GenesisConfig { {:?}\n\ {:?}\n\ Capitalization: {} SOL in {} accounts\n\ + Native instruction processors: {:#?}\n\ + Rewards pool: {:#?}\n\ ", Utc.timestamp(self.creation_time, 0).to_rfc3339(), self.cluster_type, @@ -272,6 +270,8 @@ impl fmt::Display for GenesisConfig { .sum::() ), self.accounts.len(), + self.native_instruction_processors, + self.rewards_pools, ) } }