From 19ae5568579135d69d3dfaf3a62209b8da271143 Mon Sep 17 00:00:00 2001 From: sakridge Date: Fri, 20 Sep 2019 13:21:12 -0700 Subject: [PATCH] hash account state on store (#5573) --- Cargo.lock | 1 + banking_bench/src/main.rs | 14 +-- core/src/snapshot_utils.rs | 6 ++ programs/bpf/Cargo.lock | 1 + runtime/benches/append_vec.rs | 13 ++- runtime/src/accounts.rs | 40 +++------ runtime/src/accounts_db.rs | 141 +++++++++++++++++++++++++++-- runtime/src/append_vec.rs | 27 ++++-- runtime/src/bank.rs | 102 ++++++++++++++++++++- sdk/Cargo.toml | 2 + sdk/src/account.rs | 24 ++++- sdk/src/bank_hash.rs | 163 ++++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 2 + sdk/src/native_loader.rs | 2 + 14 files changed, 484 insertions(+), 54 deletions(-) create mode 100644 sdk/src/bank_hash.rs diff --git a/Cargo.lock b/Cargo.lock index 10ca08f7c..3712cd0cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3601,6 +3601,7 @@ dependencies = [ "num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/banking_bench/src/main.rs b/banking_bench/src/main.rs index 18d49fea4..d64a91719 100644 --- a/banking_bench/src/main.rs +++ b/banking_bench/src/main.rs @@ -236,26 +236,30 @@ fn main() { assert!(txs_processed < bank.transaction_count()); txs_processed = bank.transaction_count(); tx_total += duration_as_us(&now.elapsed()); - let mut new_bank_time = Measure::start("new_bank"); - new_bank_time.stop(); - let mut insert_time = Measure::start("insert_time"); - insert_time.stop(); + let mut poh_time = Measure::start("poh_time"); poh_recorder.lock().unwrap().reset( bank.last_blockhash(), bank.slot(), Some((bank.slot(), bank.slot() + 1)), ); + poh_time.stop(); + + let mut new_bank_time = Measure::start("new_bank"); let new_bank = Bank::new_from_parent(&bank, &collector, bank.slot() + 1); + new_bank_time.stop(); + + let mut insert_time = Measure::start("insert_time"); bank_forks.insert(new_bank); bank = bank_forks.working_bank(); + insert_time.stop(); + poh_recorder.lock().unwrap().set_bank(&bank); assert!(poh_recorder.lock().unwrap().bank().is_some()); if bank.slot() > 32 { bank_forks.set_root(root, &None); root += 1; } - poh_time.stop(); debug!( "new_bank_time: {}us insert_time: {}us poh_time: {}us", new_bank_time.as_us(), diff --git a/core/src/snapshot_utils.rs b/core/src/snapshot_utils.rs index 0366572d7..655b8ebe1 100644 --- a/core/src/snapshot_utils.rs +++ b/core/src/snapshot_utils.rs @@ -220,6 +220,12 @@ pub fn bank_from_archive>( let snapshot_paths = get_snapshot_paths(&unpacked_snapshots_dir); let bank = rebuild_bank_from_snapshots(account_paths, &snapshot_paths, unpacked_accounts_dir)?; + if !bank.verify_hash_internal_state() { + warn!("Invalid snapshot hash value!"); + } else { + info!("Snapshot hash value matches."); + } + // Move the unpacked snapshots into `snapshot_config.snapshot_path` let dir_files = fs::read_dir(&unpacked_snapshots_dir).unwrap_or_else(|err| { panic!( diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index f682ff107..16236c4d6 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -1462,6 +1462,7 @@ dependencies = [ "num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/runtime/benches/append_vec.rs b/runtime/benches/append_vec.rs index af2da6c3e..b339011a9 100644 --- a/runtime/benches/append_vec.rs +++ b/runtime/benches/append_vec.rs @@ -4,6 +4,7 @@ extern crate test; use rand::{thread_rng, Rng}; use solana_runtime::append_vec::test_utils::{create_test_account, get_append_vec_path}; use solana_runtime::append_vec::AppendVec; +use solana_sdk::hash::Hash; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::thread::spawn; @@ -16,7 +17,10 @@ fn append_vec_append(bencher: &mut Bencher) { let vec = AppendVec::new(&path.path, true, 64 * 1024); bencher.iter(|| { let (meta, account) = create_test_account(0); - if vec.append_account(meta, &account).is_none() { + if vec + .append_account(meta, &account, Hash::default()) + .is_none() + { vec.reset(); } }); @@ -27,7 +31,8 @@ fn add_test_accounts(vec: &AppendVec, size: usize) -> Vec<(usize, usize)> { .into_iter() .filter_map(|sample| { let (meta, account) = create_test_account(sample); - vec.append_account(meta, &account).map(|pos| (sample, pos)) + vec.append_account(meta, &account, Hash::default()) + .map(|pos| (sample, pos)) }) .collect() } @@ -71,7 +76,7 @@ fn append_vec_concurrent_append_read(bencher: &mut Bencher) { spawn(move || loop { let sample = indexes1.lock().unwrap().len(); let (meta, account) = create_test_account(sample); - if let Some(pos) = vec1.append_account(meta, &account) { + if let Some(pos) = vec1.append_account(meta, &account, Hash::default()) { indexes1.lock().unwrap().push((sample, pos)) } else { break; @@ -116,7 +121,7 @@ fn append_vec_concurrent_read_append(bencher: &mut Bencher) { bencher.iter(|| { let sample: usize = thread_rng().gen_range(0, 256); let (meta, account) = create_test_account(sample); - if let Some(pos) = vec.append_account(meta, &account) { + if let Some(pos) = vec.append_account(meta, &account, Hash::default()) { indexes.lock().unwrap().push((sample, pos)) } }); diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index e9e6446b1..3b02023d5 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -4,17 +4,15 @@ use crate::append_vec::StoredAccount; use crate::blockhash_queue::BlockhashQueue; use crate::message_processor::has_duplicates; use crate::rent_collector::RentCollector; -use bincode::serialize; use log::*; use rayon::slice::ParallelSliceMut; use solana_metrics::inc_new_counter_error; use solana_sdk::account::Account; -use solana_sdk::hash::{Hash, Hasher}; +use solana_sdk::bank_hash::BankHash; use solana_sdk::message::Message; use solana_sdk::native_loader; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_program; -use solana_sdk::sysvar; use solana_sdk::transaction::Result; use solana_sdk::transaction::{Transaction, TransactionError}; use std::collections::{HashMap, HashSet}; @@ -69,8 +67,9 @@ impl Accounts { credit_only_account_locks: Arc::new(RwLock::new(Some(HashMap::new()))), } } - pub fn new_from_parent(parent: &Accounts) -> Self { + pub fn new_from_parent(parent: &Accounts, slot: Fork, parent_slot: Fork) -> Self { let accounts_db = parent.accounts_db.clone(); + accounts_db.set_hash(slot, parent_slot); Accounts { accounts_db, account_locks: Mutex::new(HashSet::new()), @@ -317,6 +316,10 @@ impl Accounts { }) } + pub fn verify_hash_internal_state(&self, fork: Fork, ancestors: &HashMap) -> bool { + self.accounts_db.verify_hash_internal_state(fork, ancestors) + } + pub fn load_by_program( &self, ancestors: &HashMap, @@ -450,30 +453,13 @@ impl Accounts { } } - fn hash_account(stored_account: &StoredAccount) -> Hash { - let mut hasher = Hasher::default(); - hasher.hash(&serialize(&stored_account.balance).unwrap()); - hasher.hash(stored_account.data); - hasher.result() - } - - pub fn hash_internal_state(&self, fork_id: Fork) -> Option { - let account_hashes = self.scan_fork(fork_id, |stored_account| { - if !sysvar::check_id(&stored_account.balance.owner) { - Some(Self::hash_account(stored_account)) - } else { - None - } - }); - - if account_hashes.is_empty() { - None + pub fn hash_internal_state(&self, fork_id: Fork) -> Option { + let fork_hashes = self.accounts_db.fork_hashes.read().unwrap(); + let fork_hash = fork_hashes.get(&fork_id)?; + if fork_hash.0 { + Some(fork_hash.1) } else { - let mut hasher = Hasher::default(); - for hash in account_hashes { - hasher.hash(hash.as_ref()); - } - Some(hasher.result()) + None } } diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index f25ecee1f..f17f6ddf9 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -21,6 +21,7 @@ use crate::accounts_index::{AccountsIndex, Fork}; use crate::append_vec::{AppendVec, StorageMeta, StoredAccount}; use bincode::{deserialize_from, serialize_into}; +use byteorder::{ByteOrder, LittleEndian}; use fs_extra::dir::CopyOptions; use log::*; use rand::{thread_rng, Rng}; @@ -32,7 +33,10 @@ use serde::{Deserialize, Serialize}; use solana_measure::measure::Measure; use solana_rayon_threadlimit::get_thread_count; use solana_sdk::account::Account; +use solana_sdk::bank_hash::BankHash; +use solana_sdk::hash::{Hash, Hasher}; use solana_sdk::pubkey::Pubkey; +use solana_sdk::sysvar; use std::collections::{HashMap, HashSet}; use std::fmt; use std::io::{BufReader, Cursor, Error as IOError, ErrorKind, Read, Result as IOResult}; @@ -333,6 +337,8 @@ impl<'a> Serialize for AccountsDBSerialize<'a> { let account_storage_serialize = AccountStorageSerialize::new(&*storage, self.slot); serialize_into(&mut wr, &account_storage_serialize).map_err(Error::custom)?; serialize_into(&mut wr, &version).map_err(Error::custom)?; + let fork_hashes = self.accounts_db.fork_hashes.read().unwrap(); + serialize_into(&mut wr, &*fork_hashes).map_err(Error::custom)?; let len = wr.position() as usize; serializer.serialize_bytes(&wr.into_inner()[..len]) } @@ -365,7 +371,12 @@ pub struct AccountsDB { /// Thread pool used for par_iter pub thread_pool: ThreadPool, + /// Number of append vecs to create to maximize parallelism when scanning + /// the accounts min_num_stores: usize, + + /// fork to BankHash and a status flag to indicate if the hash has been initialized or not + pub fork_hashes: RwLock>, } impl Default for AccountsDB { @@ -385,6 +396,7 @@ impl Default for AccountsDB { .build() .unwrap(), min_num_stores: num_threads, + fork_hashes: RwLock::new(HashMap::default()), } } } @@ -489,6 +501,10 @@ impl AccountsDB { let version: u64 = deserialize_from(&mut stream) .map_err(|_| AccountsDB::get_io_error("write version deserialize error"))?; + let fork_hashes: HashMap = deserialize_from(&mut stream) + .map_err(|_| AccountsDB::get_io_error("fork hashes deserialize error"))?; + *self.fork_hashes.write().unwrap() = fork_hashes; + // Process deserialized data, set necessary fields in self *self.paths.write().unwrap() = local_account_paths; let max_id: usize = *storage @@ -500,12 +516,6 @@ impl AccountsDB { { let mut stores = self.storage.write().unwrap(); - /*if let Some((_, store0)) = storage.0.remove_entry(&0) { - let fork_storage0 = stores.0.entry(0).or_insert_with(HashMap::new); - for (id, store) in store0.iter() { - fork_storage0.insert(*id, store.clone()); - } - }*/ stores.0.extend(storage.0); } @@ -599,6 +609,14 @@ impl AccountsDB { }) } + pub fn set_hash(&self, slot: Fork, parent_slot: Fork) { + let mut fork_hashes = self.fork_hashes.write().unwrap(); + let hash = *fork_hashes + .get(&parent_slot) + .expect("accounts_db::set_hash::no parent slot"); + fork_hashes.insert(slot, (false, hash.1)); + } + pub fn load( storage: &AccountStorage, ancestors: &HashMap, @@ -702,7 +720,42 @@ impl AccountsDB { } } - fn store_accounts(&self, fork_id: Fork, accounts: &[(&Pubkey, &Account)]) -> Vec { + pub fn hash_stored_account(fork: Fork, account: &StoredAccount) -> Hash { + Self::hash_account_data( + fork, + account.balance.lamports, + account.data, + &account.meta.pubkey, + ) + } + + pub fn hash_account(fork: Fork, account: &Account, pubkey: &Pubkey) -> Hash { + Self::hash_account_data(fork, account.lamports, &account.data, pubkey) + } + + pub fn hash_account_data(fork: Fork, lamports: u64, data: &[u8], pubkey: &Pubkey) -> Hash { + let mut hasher = Hasher::default(); + let mut buf = [0u8; 8]; + + LittleEndian::write_u64(&mut buf[..], lamports); + hasher.hash(&buf); + + LittleEndian::write_u64(&mut buf[..], fork); + hasher.hash(&buf); + + hasher.hash(&data); + + hasher.hash(&pubkey.as_ref()); + + hasher.result() + } + + fn store_accounts( + &self, + fork_id: Fork, + accounts: &[(&Pubkey, &Account)], + hashes: &[Hash], + ) -> Vec { let with_meta: Vec<(StorageMeta, &Account)> = accounts .iter() .map(|(pubkey, account)| { @@ -724,7 +777,9 @@ impl AccountsDB { let mut infos: Vec = vec![]; while infos.len() < with_meta.len() { let storage = self.find_storage_candidate(fork_id); - let rvs = storage.accounts.append_accounts(&with_meta[infos.len()..]); + let rvs = storage + .accounts + .append_accounts(&with_meta[infos.len()..], &hashes); if rvs.is_empty() { storage.set_status(AccountStorageStatus::Full); @@ -749,6 +804,40 @@ impl AccountsDB { infos } + pub fn verify_hash_internal_state(&self, fork: Fork, ancestors: &HashMap) -> bool { + let mut hash_state = BankHash::default(); + let hashes: Vec<_> = self.scan_accounts( + ancestors, + |collector: &mut Vec, option: Option<(&Pubkey, Account, Fork)>| { + if let Some((pubkey, account, fork)) = option { + if !sysvar::check_id(&account.owner) { + let hash = BankHash::from_hash(&Self::hash_account(fork, &account, pubkey)); + debug!("xoring..{} key: {}", hash, pubkey); + collector.push(hash); + } + } + }, + ); + for hash in hashes { + hash_state.xor(hash); + } + let fork_hashes = self.fork_hashes.read().unwrap(); + if let Some((_, state)) = fork_hashes.get(&fork) { + hash_state == *state + } else { + false + } + } + + pub fn xor_in_hash_state(&self, fork_id: Fork, hash: BankHash) { + let mut fork_hashes = self.fork_hashes.write().unwrap(); + let fork_hash_state = fork_hashes + .entry(fork_id) + .or_insert((false, BankHash::default())); + fork_hash_state.1.xor(hash); + fork_hash_state.0 = true; + } + fn update_index( &self, fork_id: Fork, @@ -820,10 +909,44 @@ impl AccountsDB { } } + fn hash_accounts(&self, fork_id: Fork, accounts: &[(&Pubkey, &Account)]) -> Vec { + let mut hashes = vec![]; + let mut hash_state = BankHash::default(); + let mut had_account = false; + for (pubkey, account) in accounts { + if !sysvar::check_id(&account.owner) { + let hash = BankHash::from_hash(&account.hash); + let new_hash = Self::hash_account(fork_id, account, pubkey); + let new_bank_hash = BankHash::from_hash(&new_hash); + debug!( + "hash_accounts: key: {} xor {} current: {}", + pubkey, hash, hash_state + ); + if !had_account { + hash_state = hash; + had_account = true; + } else { + hash_state.xor(hash); + } + hash_state.xor(new_bank_hash); + hashes.push(new_hash); + } else { + hashes.push(Hash::default()); + } + } + + if had_account { + self.xor_in_hash_state(fork_id, hash_state); + } + hashes + } + /// Store the account update. pub fn store(&self, fork_id: Fork, accounts: &[(&Pubkey, &Account)]) { + let hashes = self.hash_accounts(fork_id, accounts); + let mut store_accounts = Measure::start("store::store_accounts"); - let infos = self.store_accounts(fork_id, accounts); + let infos = self.store_accounts(fork_id, accounts, &hashes); store_accounts.stop(); let mut update_index = Measure::start("store::update_index"); diff --git a/runtime/src/append_vec.rs b/runtime/src/append_vec.rs index 2cf51d1b8..40d7cd75b 100644 --- a/runtime/src/append_vec.rs +++ b/runtime/src/append_vec.rs @@ -1,7 +1,7 @@ use bincode::{deserialize_from, serialize_into, serialized_size}; use memmap::MmapMut; use serde::{Deserialize, Serialize}; -use solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey}; +use solana_sdk::{account::Account, clock::Epoch, hash::Hash, pubkey::Pubkey}; use std::fmt; use std::fs::{create_dir_all, remove_file, OpenOptions}; use std::io; @@ -50,6 +50,7 @@ pub struct StoredAccount<'a> { pub balance: &'a AccountBalance, pub data: &'a [u8], pub offset: usize, + pub hash: &'a Hash, } impl<'a> StoredAccount<'a> { @@ -60,6 +61,7 @@ impl<'a> StoredAccount<'a> { executable: self.balance.executable, rent_epoch: self.balance.rent_epoch, data: self.data.to_vec(), + hash: *self.hash, } } } @@ -243,6 +245,7 @@ impl AppendVec { pub fn get_account<'a>(&'a self, offset: usize) -> Option<(StoredAccount<'a>, usize)> { let (meta, next): (&'a StorageMeta, _) = self.get_type(offset)?; let (balance, next): (&'a AccountBalance, _) = self.get_type(next)?; + let (hash, next): (&'a Hash, _) = self.get_type(next)?; let (data, next) = self.get_slice(next, meta.data_len as usize)?; Some(( StoredAccount { @@ -250,6 +253,7 @@ impl AppendVec { balance, data, offset, + hash, }, next, )) @@ -274,10 +278,14 @@ impl AppendVec { } #[allow(clippy::mutex_atomic)] - pub fn append_accounts(&self, accounts: &[(StorageMeta, &Account)]) -> Vec { + pub fn append_accounts( + &self, + accounts: &[(StorageMeta, &Account)], + hashes: &[Hash], + ) -> Vec { let mut offset = self.append_offset.lock().unwrap(); let mut rv = vec![]; - for (storage_meta, account) in accounts { + for ((storage_meta, account), hash) in accounts.iter().zip(hashes) { let meta_ptr = storage_meta as *const StorageMeta; let balance = AccountBalance { lamports: account.lamports, @@ -288,9 +296,11 @@ impl AppendVec { let balance_ptr = &balance as *const AccountBalance; let data_len = storage_meta.data_len as usize; let data_ptr = account.data.as_ptr(); + let hash_ptr = hash.as_ref().as_ptr(); let ptrs = [ (meta_ptr as *const u8, mem::size_of::()), (balance_ptr as *const u8, mem::size_of::()), + (hash_ptr as *const u8, mem::size_of::()), (data_ptr, data_len), ]; if let Some(res) = self.append_ptrs_locked(&mut offset, &ptrs) { @@ -302,14 +312,19 @@ impl AppendVec { rv } - pub fn append_account(&self, storage_meta: StorageMeta, account: &Account) -> Option { - self.append_accounts(&[(storage_meta, account)]) + pub fn append_account( + &self, + storage_meta: StorageMeta, + account: &Account, + hash: Hash, + ) -> Option { + self.append_accounts(&[(storage_meta, account)], &[hash]) .first() .cloned() } pub fn append_account_test(&self, data: &(StorageMeta, Account)) -> Option { - self.append_account(data.0.clone(), &data.1) + self.append_account(data.0.clone(), &data.1, Hash::default()) } } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index d2fa08cf0..74e7235f7 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -285,7 +285,11 @@ impl Bank { assert_ne!(slot, parent.slot()); let rc = BankRc { - accounts: Arc::new(Accounts::new_from_parent(&parent.rc.accounts)), + accounts: Arc::new(Accounts::new_from_parent( + &parent.rc.accounts, + slot, + parent.slot(), + )), parent: RwLock::new(Some(parent.clone())), slot, }; @@ -1338,6 +1342,14 @@ impl Bank { } } + /// Recalculate the hash_internal_state from the account stores. Would be used to verify a + /// snaphsot. + pub fn verify_hash_internal_state(&self) -> bool { + self.rc + .accounts + .verify_hash_internal_state(self.slot(), &self.ancestors) + } + /// Return the number of ticks per slot pub fn ticks_per_slot(&self) -> u64 { self.ticks_per_slot @@ -1807,6 +1819,7 @@ mod tests { #[test] fn test_transfer_to_newb() { + solana_logger::setup(); let (genesis_block, mint_keypair) = create_genesis_block(10_000); let bank = Bank::new(&genesis_block); let pubkey = Pubkey::new_rand(); @@ -2316,6 +2329,55 @@ mod tests { // Checkpointing should not change its state let bank2 = new_from_parent(&Arc::new(bank1)); assert_eq!(bank0.hash_internal_state(), bank2.hash_internal_state()); + + let pubkey2 = Pubkey::new_rand(); + info!("transfer 2 {}", pubkey2); + bank2.transfer(10, &mint_keypair, &pubkey2).unwrap(); + assert!(bank2.verify_hash_internal_state()); + } + + #[test] + fn test_bank_hash_internal_state_verify() { + solana_logger::setup(); + let (genesis_block, mint_keypair) = create_genesis_block(2_000); + let bank0 = Bank::new(&genesis_block); + + let pubkey = Pubkey::new_rand(); + info!("transfer 0 {} mint: {}", pubkey, mint_keypair.pubkey()); + bank0.transfer(1_000, &mint_keypair, &pubkey).unwrap(); + + let bank0_state = bank0.hash_internal_state(); + // Checkpointing should not change its state + let bank2 = new_from_parent(&Arc::new(bank0)); + assert_eq!(bank0_state, bank2.hash_internal_state()); + + let pubkey2 = Pubkey::new_rand(); + info!("transfer 2 {}", pubkey2); + bank2.transfer(10, &mint_keypair, &pubkey2).unwrap(); + assert!(bank2.verify_hash_internal_state()); + } + + // Test that two bank forks with the same accounts should not hash to the same value. + #[test] + fn test_bank_hash_internal_state_same_account_different_fork() { + solana_logger::setup(); + let (genesis_block, mint_keypair) = create_genesis_block(2_000); + let bank0 = Arc::new(Bank::new(&genesis_block)); + let initial_state = bank0.hash_internal_state(); + let bank1 = Bank::new_from_parent(&bank0.clone(), &Pubkey::default(), 1); + assert_eq!(bank1.hash_internal_state(), initial_state); + + info!("transfer bank1"); + let pubkey = Pubkey::new_rand(); + bank1.transfer(1_000, &mint_keypair, &pubkey).unwrap(); + assert_ne!(bank1.hash_internal_state(), initial_state); + + info!("transfer bank2"); + // bank2 should not hash the same as bank1 + let bank2 = Bank::new_from_parent(&bank0, &Pubkey::default(), 2); + bank2.transfer(1_000, &mint_keypair, &pubkey).unwrap(); + assert_ne!(bank2.hash_internal_state(), initial_state); + assert_ne!(bank1.hash_internal_state(), bank2.hash_internal_state()); } #[test] @@ -2325,6 +2387,44 @@ mod tests { assert_ne!(bank0.hash_internal_state(), bank1.hash_internal_state()); } + // See that the order of two transfers does not affect the result + // of hash_internal_state + #[test] + fn test_hash_internal_state_order() { + let (genesis_block, mint_keypair) = create_genesis_block(100); + let bank0 = Bank::new(&genesis_block); + let bank1 = Bank::new(&genesis_block); + assert_eq!(bank0.hash_internal_state(), bank1.hash_internal_state()); + let key0 = Pubkey::new_rand(); + let key1 = Pubkey::new_rand(); + bank0.transfer(10, &mint_keypair, &key0).unwrap(); + bank0.transfer(20, &mint_keypair, &key1).unwrap(); + + bank1.transfer(20, &mint_keypair, &key1).unwrap(); + bank1.transfer(10, &mint_keypair, &key0).unwrap(); + + assert_eq!(bank0.hash_internal_state(), bank1.hash_internal_state()); + } + + #[test] + fn test_hash_internal_state_error() { + solana_logger::setup(); + let (genesis_block, mint_keypair) = create_genesis_block(100); + let bank = Bank::new(&genesis_block); + let key0 = Pubkey::new_rand(); + bank.transfer(10, &mint_keypair, &key0).unwrap(); + let orig = bank.hash_internal_state(); + + // Transfer will error but still take a fee + assert!(bank.transfer(1000, &mint_keypair, &key0).is_err()); + assert_ne!(orig, bank.hash_internal_state()); + + let orig = bank.hash_internal_state(); + let empty_keypair = Keypair::new(); + assert!(bank.transfer(1000, &empty_keypair, &key0).is_err()); + assert_eq!(orig, bank.hash_internal_state()); + } + #[test] fn test_bank_hash_internal_state_squash() { let collector_id = Pubkey::default(); diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e64b17287..b6409f328 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -18,6 +18,7 @@ default = [ "chrono", "memmap", "rand", + "rand_chacha", "rayon", "serde_json", "solana-ed25519-dalek", @@ -40,6 +41,7 @@ memmap = { version = "0.6.2", optional = true } num-derive = { version = "0.2" } num-traits = { version = "0.2" } rand = { version = "0.6.5", optional = true } +rand_chacha = { version = "0.1.1", optional = true } rayon = { version = "1.2.0", optional = true } serde = "1.0.101" serde_derive = "1.0.101" diff --git a/sdk/src/account.rs b/sdk/src/account.rs index dbd0bf0dc..b7413af78 100644 --- a/sdk/src/account.rs +++ b/sdk/src/account.rs @@ -1,9 +1,10 @@ +use crate::hash::Hash; use crate::{clock::Epoch, pubkey::Pubkey}; use std::{cmp, fmt}; /// An Account with data that is stored on chain #[repr(C)] -#[derive(Serialize, Deserialize, Clone, Default, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Default)] pub struct Account { /// lamports in the account pub lamports: u64, @@ -15,8 +16,26 @@ pub struct Account { pub executable: bool, /// the epoch at which this account will next owe rent pub rent_epoch: Epoch, + /// Hash of this account's state, skip serializing as to not expose to external api + /// Used for keeping the accounts state hash updated. + #[serde(skip_serializing, skip_deserializing)] + pub hash: Hash, } +/// skip comparison of account.hash, since it is only meaningful when the account is loaded in a +/// given fork and some tests do not have that. +impl PartialEq for Account { + fn eq(&self, other: &Self) -> bool { + self.lamports == other.lamports + && self.data == other.data + && self.owner == other.owner + && self.executable == other.executable + && self.rent_epoch == other.rent_epoch + } +} + +impl Eq for Account {} + impl fmt::Debug for Account { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let data_len = cmp::min(64, self.data.len()); @@ -27,13 +46,14 @@ impl fmt::Debug for Account { }; write!( f, - "Account {{ lamports: {} data.len: {} owner: {} executable: {} rent_epoch: {}{} }}", + "Account {{ lamports: {} data.len: {} owner: {} executable: {} rent_epoch: {}{} hash: {} }}", self.lamports, self.data.len(), self.owner, self.executable, self.rent_epoch, data_str, + self.hash, ) } } diff --git a/sdk/src/bank_hash.rs b/sdk/src/bank_hash.rs new file mode 100644 index 000000000..6a769113e --- /dev/null +++ b/sdk/src/bank_hash.rs @@ -0,0 +1,163 @@ +use crate::hash::Hash; +use bincode::deserialize_from; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaChaRng; +use serde::{Deserialize, Serialize, Serializer}; +use std::fmt; +use std::io::Cursor; + +// Type for representing a bank accounts state. +// Taken by xor of a sha256 of accounts state for lower 32-bytes, and +// then generating the rest of the bytes with a chacha rng init'ed with that state. +// 440 bytes solves the birthday problem when xor'ing of preventing an attacker of +// finding a value or set of values that could be xor'ed to match the bitpattern +// of an existing state value. +const BANK_HASH_BYTES: usize = 448; +#[derive(Clone, Copy)] +pub struct BankHash([u8; BANK_HASH_BYTES]); + +impl fmt::Debug for BankHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BankHash {}", hex::encode(&self.0[..32])) + } +} + +impl fmt::Display for BankHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl PartialEq for BankHash { + fn eq(&self, other: &Self) -> bool { + self.0[..] == other.0[..] + } +} +impl Eq for BankHash {} + +impl Default for BankHash { + fn default() -> Self { + BankHash([0u8; BANK_HASH_BYTES]) + } +} + +impl BankHash { + pub fn from_hash(hash: &Hash) -> Self { + let mut new = BankHash::default(); + + // default hash should result in all 0s thus nop for xor + if *hash == Hash::default() { + return new; + } + + new.0[..32].copy_from_slice(hash.as_ref()); + let mut seed = [0u8; 32]; + seed.copy_from_slice(hash.as_ref()); + let mut generator = ChaChaRng::from_seed(seed); + generator.fill(&mut new.0[32..]); + new + } + + pub fn is_default(&self) -> bool { + self.0[0..32] == Hash::default().as_ref()[0..32] + } + + pub fn xor(self: &mut BankHash, hash: BankHash) { + for (i, b) in hash.as_ref().iter().enumerate() { + self.0.as_mut()[i] ^= b; + } + } +} + +impl Serialize for BankHash { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0[..]) + } +} + +struct BankHashVisitor; + +impl<'a> serde::de::Visitor<'a> for BankHashVisitor { + type Value = BankHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Expecting BankHash") + } + + #[allow(clippy::mutex_atomic)] + fn visit_bytes(self, data: &[u8]) -> std::result::Result + where + E: serde::de::Error, + { + use serde::de::Error; + let mut new = BankHash::default(); + let mut rd = Cursor::new(&data[..]); + for i in 0..BANK_HASH_BYTES { + new.0[i] = deserialize_from(&mut rd).map_err(Error::custom)?; + } + + Ok(new) + } +} + +impl<'de> Deserialize<'de> for BankHash { + fn deserialize(deserializer: D) -> std::result::Result + where + D: ::serde::Deserializer<'de>, + { + deserializer.deserialize_bytes(BankHashVisitor) + } +} + +impl AsRef<[u8]> for BankHash { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl AsMut<[u8]> for BankHash { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0[..] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::hash; + use bincode::{deserialize, serialize}; + use log::*; + + #[test] + fn test_bankhash() { + let hash = hash(&[1, 2, 3, 4]); + let bank_hash = BankHash::from_hash(&hash); + assert!(!bank_hash.is_default()); + + let default = BankHash::default(); + assert!(default.is_default()); + assert!(bank_hash != default); + assert!(bank_hash == bank_hash); + + for i in 0..BANK_HASH_BYTES / 32 { + let start = i * 32; + let end = start + 32; + assert!(bank_hash.0[start..end] != [0u8; 32]); + } + } + + #[test] + fn test_serialize() { + solana_logger::setup(); + let hash = hash(&[1, 2, 3, 4]); + let bank_hash = BankHash::from_hash(&hash); + info!("{}", bank_hash); + let bytes = serialize(&bank_hash).unwrap(); + let new: BankHash = deserialize(&bytes).unwrap(); + info!("{}", new); + assert_eq!(new, bank_hash); + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 70877e44b..2a2d5dd7b 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -28,6 +28,8 @@ pub mod log; // Modules not usable by on-chain programs #[cfg(not(feature = "program"))] +pub mod bank_hash; +#[cfg(not(feature = "program"))] pub mod client; #[cfg(not(feature = "program"))] pub mod genesis_block; diff --git a/sdk/src/native_loader.rs b/sdk/src/native_loader.rs index ce0cdded2..55ad33a8c 100644 --- a/sdk/src/native_loader.rs +++ b/sdk/src/native_loader.rs @@ -1,4 +1,5 @@ use crate::account::Account; +use crate::hash::Hash; const ID: [u8; 32] = [ 5, 135, 132, 191, 20, 139, 164, 40, 47, 176, 18, 87, 72, 136, 169, 241, 83, 160, 125, 173, 247, @@ -15,5 +16,6 @@ pub fn create_loadable_account(name: &str) -> Account { data: name.as_bytes().to_vec(), executable: true, rent_epoch: 0, + hash: Hash::default(), } }