diff --git a/runtime/benches/bank.rs b/runtime/benches/bank.rs index db13f66e22..42215c31d6 100644 --- a/runtime/benches/bank.rs +++ b/runtime/benches/bank.rs @@ -9,6 +9,7 @@ use solana_runtime::loader_utils::create_invoke_instruction; use solana_sdk::account::KeyedAccount; use solana_sdk::client::AsyncClient; use solana_sdk::client::SyncClient; +use solana_sdk::clock::MAX_RECENT_BLOCKHASHES; use solana_sdk::genesis_block::create_genesis_block; use solana_sdk::instruction::InstructionError; use solana_sdk::pubkey::Pubkey; @@ -173,3 +174,26 @@ fn bench_bank_async_process_builtin_transactions(bencher: &mut Bencher) { fn bench_bank_async_process_native_loader_transactions(bencher: &mut Bencher) { do_bench_transactions(bencher, &async_bencher, &create_native_loader_transactions); } + +#[bench] +#[ignore] +fn bench_bank_update_recent_blockhashes(bencher: &mut Bencher) { + let (genesis_block, _mint_keypair) = create_genesis_block(100); + let mut bank = Arc::new(Bank::new(&genesis_block)); + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + let genesis_blockhash = bank.last_blockhash(); + // Prime blockhash_queue + for i in 0..(MAX_RECENT_BLOCKHASHES + 1) { + bank = Arc::new(Bank::new_from_parent( + &bank, + &Pubkey::default(), + (i + 1) as u64, + )); + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + } + // Verify blockhash_queue is full (genesis blockhash has been kicked out) + assert!(!bank.check_hash_age(&genesis_blockhash, MAX_RECENT_BLOCKHASHES)); + bencher.iter(|| { + bank.update_recent_blockhashes(); + }); +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index fafdd1be29..b8282e0f7f 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -291,6 +291,7 @@ impl Bank { bank.update_clock(); bank.update_rent(); bank.update_epoch_schedule(); + bank.update_recent_blockhashes(); bank } @@ -385,6 +386,7 @@ impl Bank { new.update_stake_history(Some(parent.epoch())); new.update_clock(); new.update_fees(); + new.update_recent_blockhashes(); new } @@ -539,6 +541,15 @@ impl Bank { ); } + pub fn update_recent_blockhashes(&self) { + let blockhash_queue = self.blockhash_queue.read().unwrap(); + let recent_blockhash_iter = blockhash_queue.get_recent_blockhashes(); + self.store_account( + &sysvar::recent_blockhashes::id(), + &sysvar::recent_blockhashes::create_account_with_data(1, recent_blockhash_iter), + ); + } + // If the point values are not `normal`, bring them back into range and // set them to the last value or 0. fn check_point_values( @@ -1599,6 +1610,18 @@ impl Drop for Bank { } } +pub fn goto_end_of_slot(bank: &mut Bank) { + let mut tick_hash = bank.last_blockhash(); + loop { + tick_hash = hashv(&[&tick_hash.as_ref(), &[42]]); + bank.register_tick(&tick_hash); + if tick_hash == bank.last_blockhash() { + bank.freeze(); + return; + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -2023,18 +2046,6 @@ mod tests { assert_eq!(bank.get_balance(&key.pubkey()), 1); } - fn goto_end_of_slot(bank: &mut Bank) { - let mut tick_hash = bank.last_blockhash(); - loop { - tick_hash = hashv(&[&tick_hash.as_ref(), &[42]]); - bank.register_tick(&tick_hash); - if tick_hash == bank.last_blockhash() { - bank.freeze(); - return; - } - } - } - #[test] fn test_bank_tx_fee() { let arbitrary_transfer_amount = 42; @@ -3322,4 +3333,22 @@ mod tests { // Non-native loader accounts can not be used for instruction processing bank.add_instruction_processor(mint_keypair.pubkey(), mock_ix_processor); } + + #[test] + fn test_recent_blockhashes_sysvar() { + let (genesis_block, _mint_keypair) = create_genesis_block(500); + let mut bank = Arc::new(Bank::new(&genesis_block)); + for i in 1..5 { + let bhq_account = bank.get_account(&sysvar::recent_blockhashes::id()).unwrap(); + let recent_blockhashes = + sysvar::recent_blockhashes::RecentBlockhashes::from_account(&bhq_account).unwrap(); + // Check length + assert_eq!(recent_blockhashes.len(), i); + let most_recent_hash = recent_blockhashes.iter().nth(0).unwrap(); + // Check order + assert!(bank.check_hash_age(most_recent_hash, 0)); + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + } } diff --git a/runtime/src/blockhash_queue.rs b/runtime/src/blockhash_queue.rs index 8c7bb34c7e..09a9a1a3f3 100644 --- a/runtime/src/blockhash_queue.rs +++ b/runtime/src/blockhash_queue.rs @@ -114,11 +114,16 @@ impl BlockhashQueue { } None } + + pub fn get_recent_blockhashes(&self) -> impl Iterator { + (&self.ages).iter().map(|(k, v)| (v.hash_height, k)) + } } #[cfg(test)] mod tests { use super::*; use bincode::serialize; + use solana_sdk::clock::MAX_RECENT_BLOCKHASHES; use solana_sdk::hash::hash; #[test] @@ -150,4 +155,21 @@ mod tests { assert_eq!(last_hash, hash_queue.last_hash()); assert!(hash_queue.check_hash_age(&last_hash, 0)); } + + #[test] + fn test_get_recent_blockhashes() { + let mut blockhash_queue = BlockhashQueue::new(MAX_RECENT_BLOCKHASHES); + let recent_blockhashes = blockhash_queue.get_recent_blockhashes(); + // Sanity-check an empty BlockhashQueue + assert_eq!(recent_blockhashes.count(), 0); + for i in 0..MAX_RECENT_BLOCKHASHES { + let hash = hash(&serialize(&i).unwrap()); + blockhash_queue.register_hash(&hash, &FeeCalculator::default()); + } + let recent_blockhashes = blockhash_queue.get_recent_blockhashes(); + // Verify that the returned hashes are most recent + for (_slot, hash) in recent_blockhashes { + assert!(blockhash_queue.check_hash_age(hash, MAX_RECENT_BLOCKHASHES)); + } + } } diff --git a/sdk/src/sysvar/mod.rs b/sdk/src/sysvar/mod.rs index 9686ac8be6..231786d3d4 100644 --- a/sdk/src/sysvar/mod.rs +++ b/sdk/src/sysvar/mod.rs @@ -5,6 +5,7 @@ use crate::pubkey::Pubkey; pub mod clock; pub mod epoch_schedule; pub mod fees; +pub mod recent_blockhashes; pub mod rent; pub mod rewards; pub mod slot_hashes; @@ -14,6 +15,7 @@ pub fn is_sysvar_id(id: &Pubkey) -> bool { clock::check_id(id) || epoch_schedule::check_id(id) || fees::check_id(id) + || recent_blockhashes::check_id(id) || rent::check_id(id) || rewards::check_id(id) || slot_hashes::check_id(id) diff --git a/sdk/src/sysvar/recent_blockhashes.rs b/sdk/src/sysvar/recent_blockhashes.rs new file mode 100644 index 0000000000..5df2e6383a --- /dev/null +++ b/sdk/src/sysvar/recent_blockhashes.rs @@ -0,0 +1,116 @@ +use crate::{account::Account, account_info::AccountInfo, hash::Hash, sysvar}; +use bincode::serialized_size; +use std::collections::BinaryHeap; +use std::iter::FromIterator; +use std::ops::Deref; + +const MAX_ENTRIES: usize = 32; +const ID: [u8; 32] = [ + 0x06, 0xa7, 0xd5, 0x17, 0x19, 0x2c, 0x56, 0x8e, 0xe0, 0x8a, 0x84, 0x5f, 0x73, 0xd2, 0x97, 0x88, + 0xcf, 0x03, 0x5c, 0x31, 0x45, 0xb2, 0x1a, 0xb3, 0x44, 0xd8, 0x06, 0x2e, 0xa9, 0x40, 0x00, 0x00, +]; + +crate::solana_sysvar_id!(ID, "SysvarRecentB1ockHashes11111111111111111111"); + +#[repr(C)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct RecentBlockhashes(Vec); + +impl Default for RecentBlockhashes { + fn default() -> Self { + Self(Vec::with_capacity(MAX_ENTRIES)) + } +} + +impl<'a> FromIterator<&'a Hash> for RecentBlockhashes { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let mut new = Self::default(); + for i in iter { + new.0.push(*i) + } + new + } +} + +impl RecentBlockhashes { + pub fn from_account(account: &Account) -> Option { + account.deserialize_data().ok() + } + pub fn to_account(&self, account: &mut Account) -> Option<()> { + account.serialize_data(self).unwrap(); + Some(()) + } + pub fn from_account_info(account: &AccountInfo) -> Option { + account.deserialize_data().ok() + } + pub fn to_account_info(&self, account: &mut AccountInfo) -> Option<()> { + account.serialize_data(self).ok() + } + pub fn size_of() -> usize { + serialized_size(&RecentBlockhashes(vec![Hash::default(); MAX_ENTRIES])).unwrap() as usize + } +} + +impl Deref for RecentBlockhashes { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub fn create_account(lamports: u64) -> Account { + Account::new(lamports, RecentBlockhashes::size_of(), &sysvar::id()) +} + +pub fn update_account<'a, I>(account: &mut Account, recent_blockhash_iter: I) -> Option<()> +where + I: IntoIterator, +{ + let sorted = BinaryHeap::from_iter(recent_blockhash_iter); + let recent_blockhash_iter = sorted.into_iter().take(MAX_ENTRIES).map(|(_, hash)| hash); + let recent_blockhashes = RecentBlockhashes::from_iter(recent_blockhash_iter); + recent_blockhashes.to_account(account) +} + +pub fn create_account_with_data<'a, I>(lamports: u64, recent_blockhash_iter: I) -> Account +where + I: IntoIterator, +{ + let mut account = create_account(lamports); + update_account(&mut account, recent_blockhash_iter).unwrap(); + account +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::Hash; + + #[test] + fn test_create_account_empty() { + let account = create_account_with_data(42, vec![].into_iter()); + let recent_blockhashes = RecentBlockhashes::from_account(&account).unwrap(); + assert_eq!(recent_blockhashes, RecentBlockhashes::default()); + } + + #[test] + fn test_create_account_full() { + let def_hash = Hash::default(); + let account = + create_account_with_data(42, vec![(0u64, &def_hash); MAX_ENTRIES].into_iter()); + let recent_blockhashes = RecentBlockhashes::from_account(&account).unwrap(); + assert_eq!(recent_blockhashes.len(), MAX_ENTRIES); + } + + #[test] + fn test_create_account_truncate() { + let def_hash = Hash::default(); + let account = + create_account_with_data(42, vec![(0u64, &def_hash); MAX_ENTRIES + 1].into_iter()); + let recent_blockhashes = RecentBlockhashes::from_account(&account).unwrap(); + assert_eq!(recent_blockhashes.len(), MAX_ENTRIES); + } +}