From 1f7c714acf547360d2ba605362f8c719c2fb8ece Mon Sep 17 00:00:00 2001 From: steviez Date: Wed, 27 Dec 2023 12:37:11 -0500 Subject: [PATCH] Allow bank hash file to contain details for multiple slots (#34516) The initial implementation only allowed for outputting details for one bank/slot. There are several usecases that could take advantage of the file being able to contain details for multiple banks. So, this PR adjusts the structure to contain details about multiple banks. Note that using a basic diff will not work for comparing the old format to this new format. Instead, a basic transform with jq will be necessary to do so. --- runtime/src/bank/bank_hash_details.rs | 145 +++++++++++++++++--------- 1 file changed, 97 insertions(+), 48 deletions(-) diff --git a/runtime/src/bank/bank_hash_details.rs b/runtime/src/bank/bank_hash_details.rs index 6b40e7aef6e4ba..9072f6a12f1496 100644 --- a/runtime/src/bank/bank_hash_details.rs +++ b/runtime/src/bank/bank_hash_details.rs @@ -23,9 +23,51 @@ use { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub(crate) struct BankHashDetails { - /// client version + /// The client version pub version: String, + /// The encoding format for account data buffers pub account_data_encoding: String, + /// Bank hash details for a collection of banks + pub bank_hash_details: Vec, +} + +impl BankHashDetails { + pub fn new(bank_hash_details: Vec) -> Self { + Self { + version: solana_version::version!().to_string(), + account_data_encoding: "base64".to_string(), + bank_hash_details, + } + } + + /// Determines a filename given the currently held bank details + pub fn filename(&self) -> Result { + if self.bank_hash_details.is_empty() { + return Err("BankHashDetails does not contains details for any banks".to_string()); + } + // From here on, .unwrap() on .first() and .second() is safe as + // self.bank_hash_details is known to be non-empty + let (first_slot, first_hash) = { + let details = self.bank_hash_details.first().unwrap(); + (details.slot, &details.bank_hash) + }; + + let filename = if self.bank_hash_details.len() == 1 { + format!("{first_slot}-{first_hash}.json") + } else { + let (last_slot, last_hash) = { + let details = self.bank_hash_details.last().unwrap(); + (details.slot, &details.bank_hash) + }; + format!("{first_slot}-{first_hash}_{last_slot}-{last_hash}.json") + }; + Ok(filename) + } +} + +/// The components that go into a bank hash calculation for a single bank/slot. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct BankHashSlotDetails { pub slot: Slot, pub bank_hash: String, pub parent_bank_hash: String, @@ -35,7 +77,7 @@ pub(crate) struct BankHashDetails { pub accounts: BankHashAccounts, } -impl BankHashDetails { +impl BankHashSlotDetails { pub fn new( slot: Slot, bank_hash: Hash, @@ -46,8 +88,6 @@ impl BankHashDetails { accounts: BankHashAccounts, ) -> Self { Self { - version: solana_version::version!().to_string(), - account_data_encoding: "base64".to_string(), slot, bank_hash: bank_hash.to_string(), parent_bank_hash: parent_bank_hash.to_string(), @@ -59,7 +99,7 @@ impl BankHashDetails { } } -impl TryFrom<&Bank> for BankHashDetails { +impl TryFrom<&Bank> for BankHashSlotDetails { type Error = String; fn try_from(bank: &Bank) -> Result { @@ -99,15 +139,16 @@ impl TryFrom<&Bank> for BankHashDetails { } } -// Wrap the Vec<...> so we can implement custom Serialize/Deserialize traits on the wrapper type +/// Wrapper around a Vec<_> to facilitate custom Serialize/Deserialize trait +/// implementations. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct BankHashAccounts { pub accounts: Vec, } -#[derive(Deserialize, Serialize)] /// Used as an intermediate for serializing and deserializing account fields /// into a human readable format. +#[derive(Deserialize, Serialize)] struct SerdeAccount { pubkey: String, hash: String, @@ -193,24 +234,22 @@ impl<'de> Deserialize<'de> for BankHashAccounts { } } -/// Output the components that comprise bank hash +/// Output the components that comprise the overall bank hash for the supplied `Bank` pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), String> { - let details = BankHashDetails::try_from(bank)?; + let slot_details = BankHashSlotDetails::try_from(bank)?; + let details = BankHashDetails::new(vec![slot_details]); - let slot = details.slot; - let hash = &details.bank_hash; - let file_name = format!("{slot}-{hash}.json"); let parent_dir = bank .rc .accounts .accounts_db .get_base_working_path() .join("bank_hash_details"); - let path = parent_dir.join(file_name); + let path = parent_dir.join(details.filename()?); // A file with the same name implies the same hash for this slot. Skip // rewriting a duplicate file in this scenario if !path.exists() { - info!("writing details of bank {} to {}", slot, path.display()); + info!("writing bank hash details file: {}", path.display()); // std::fs::write may fail (depending on platform) if the full directory // path does not exist. So, call std::fs_create_dir_all first. @@ -228,44 +267,54 @@ pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), Stri pub mod tests { use super::*; - #[test] - fn test_serde_bank_hash_details() { - use solana_sdk::hash::hash; + fn build_details(num_slots: usize) -> BankHashDetails { + use solana_sdk::hash::{hash, hashv}; - let slot = 123_456_789; - let signature_count = 314; + let slot_details: Vec<_> = (0..num_slots) + .map(|slot| { + let signature_count = 314; - let account = AccountSharedData::from(Account { - lamports: 123_456_789, - data: vec![0, 9, 1, 8, 2, 7, 3, 6, 4, 5], - owner: Pubkey::new_unique(), - executable: true, - rent_epoch: 123, - }); - let account_pubkey = Pubkey::new_unique(); - let account_hash = AccountHash(hash("account".as_bytes())); - let accounts = BankHashAccounts { - accounts: vec![PubkeyHashAccount { - pubkey: account_pubkey, - hash: account_hash, - account, - }], - }; + let account = AccountSharedData::from(Account { + lamports: 123_456_789, + data: vec![0, 9, 1, 8, 2, 7, 3, 6, 4, 5], + owner: Pubkey::new_unique(), + executable: true, + rent_epoch: 123, + }); + let account_pubkey = Pubkey::new_unique(); + let account_hash = AccountHash(hash("account".as_bytes())); + let accounts = BankHashAccounts { + accounts: vec![PubkeyHashAccount { + pubkey: account_pubkey, + hash: account_hash, + account, + }], + }; - let bank_hash = hash("bank".as_bytes()); - let parent_bank_hash = hash("parent_bank".as_bytes()); - let accounts_delta_hash = hash("accounts_delta".as_bytes()); - let last_blockhash = hash("last_blockhash".as_bytes()); + let bank_hash = hashv(&["bank".as_bytes(), &slot.to_le_bytes()]); + let parent_bank_hash = hash("parent_bank".as_bytes()); + let accounts_delta_hash = hash("accounts_delta".as_bytes()); + let last_blockhash = hash("last_blockhash".as_bytes()); - let bank_hash_details = BankHashDetails::new( - slot, - bank_hash, - parent_bank_hash, - accounts_delta_hash, - signature_count, - last_blockhash, - accounts, - ); + BankHashSlotDetails::new( + slot as Slot, + bank_hash, + parent_bank_hash, + accounts_delta_hash, + signature_count, + last_blockhash, + accounts, + ) + }) + .collect(); + + BankHashDetails::new(slot_details) + } + + #[test] + fn test_serde_bank_hash_details() { + let num_slots = 10; + let bank_hash_details = build_details(num_slots); let serialized_bytes = serde_json::to_vec(&bank_hash_details).unwrap(); let deserialized_bank_hash_details: BankHashDetails =