solana_runtime/bank/
bank_hash_details.rs

1//! Container to capture information relevant to computing a bank hash
2
3use {
4    super::Bank,
5    base64::{prelude::BASE64_STANDARD, Engine},
6    log::*,
7    serde::{
8        de::{self, Deserialize, Deserializer},
9        ser::{Serialize, SerializeSeq, Serializer},
10    },
11    solana_accounts_db::{accounts_db::PubkeyHashAccount, accounts_hash::AccountHash},
12    solana_sdk::{
13        account::{Account, AccountSharedData, ReadableAccount},
14        clock::{Epoch, Slot},
15        feature_set,
16        fee::FeeDetails,
17        hash::Hash,
18        inner_instruction::InnerInstructionsList,
19        pubkey::Pubkey,
20        transaction::Result as TransactionResult,
21        transaction_context::TransactionReturnData,
22    },
23    solana_svm::transaction_commit_result::CommittedTransaction,
24    solana_transaction_status_client_types::UiInstruction,
25    std::str::FromStr,
26};
27
28#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
29pub struct BankHashDetails {
30    /// The client version
31    pub version: String,
32    /// The encoding format for account data buffers
33    pub account_data_encoding: String,
34    /// Bank hash details for a collection of banks
35    pub bank_hash_details: Vec<SlotDetails>,
36}
37
38impl BankHashDetails {
39    pub fn new(bank_hash_details: Vec<SlotDetails>) -> Self {
40        Self {
41            version: solana_version::version!().to_string(),
42            account_data_encoding: "base64".to_string(),
43            bank_hash_details,
44        }
45    }
46
47    /// Determines a filename given the currently held bank details
48    pub fn filename(&self) -> Result<String, String> {
49        if self.bank_hash_details.is_empty() {
50            return Err("BankHashDetails does not contains details for any banks".to_string());
51        }
52        // From here on, .unwrap() on .first() and .second() is safe as
53        // self.bank_hash_details is known to be non-empty
54        let (first_slot, first_hash) = {
55            let details = self.bank_hash_details.first().unwrap();
56            (details.slot, &details.bank_hash)
57        };
58
59        let filename = if self.bank_hash_details.len() == 1 {
60            format!("{first_slot}-{first_hash}.json")
61        } else {
62            let (last_slot, last_hash) = {
63                let details = self.bank_hash_details.last().unwrap();
64                (details.slot, &details.bank_hash)
65            };
66            format!("{first_slot}-{first_hash}_{last_slot}-{last_hash}.json")
67        };
68        Ok(filename)
69    }
70}
71
72#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
73pub struct TransactionDetails {
74    pub signature: String,
75    pub index: usize,
76    pub accounts: Vec<String>,
77    pub instructions: Vec<UiInstruction>,
78    pub is_simple_vote_tx: bool,
79    pub commit_details: Option<TransactionCommitDetails>,
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83pub struct TransactionCommitDetails {
84    pub status: TransactionResult<()>,
85    pub log_messages: Option<Vec<String>>,
86    pub inner_instructions: Option<InnerInstructionsList>,
87    pub return_data: Option<TransactionReturnData>,
88    pub executed_units: u64,
89    pub fee_details: FeeDetails,
90}
91
92impl From<CommittedTransaction> for TransactionCommitDetails {
93    fn from(committed_tx: CommittedTransaction) -> Self {
94        Self {
95            status: committed_tx.status,
96            log_messages: committed_tx.log_messages,
97            inner_instructions: committed_tx.inner_instructions,
98            return_data: committed_tx.return_data,
99            executed_units: committed_tx.executed_units,
100            fee_details: committed_tx.fee_details,
101        }
102    }
103}
104
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
106pub struct SlotDetails {
107    pub slot: Slot,
108    pub bank_hash: String,
109    #[serde(skip_serializing_if = "Option::is_none", default, flatten)]
110    pub bank_hash_components: Option<BankHashComponents>,
111    #[serde(skip_serializing_if = "Vec::is_empty", default)]
112    pub transactions: Vec<TransactionDetails>,
113}
114
115/// The components that go into a bank hash calculation for a single bank
116#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
117pub struct BankHashComponents {
118    pub parent_bank_hash: String,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub accounts_delta_hash: Option<String>,
121    pub signature_count: u64,
122    pub last_blockhash: String,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub epoch_accounts_hash: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub accounts_lt_hash_checksum: Option<String>,
127    pub accounts: AccountsDetails,
128}
129
130impl SlotDetails {
131    pub fn new_from_bank(bank: &Bank, include_bank_hash_components: bool) -> Result<Self, String> {
132        let slot = bank.slot();
133        if !bank.is_frozen() {
134            return Err(format!(
135                "Bank {slot} must be frozen in order to get bank hash details"
136            ));
137        }
138
139        let bank_hash_components = if include_bank_hash_components {
140            let accounts_delta_hash = (!bank
141                .feature_set
142                .is_active(&feature_set::remove_accounts_delta_hash::id()))
143            .then(|| {
144                // This bank is frozen; as a result, we know that the state has been
145                // hashed which means the delta hash is Some(). So, .unwrap() is safe
146                bank.rc
147                    .accounts
148                    .accounts_db
149                    .get_accounts_delta_hash(slot)
150                    .unwrap()
151                    .0
152                    .to_string()
153            });
154            let accounts = bank.get_accounts_for_bank_hash_details();
155
156            Some(BankHashComponents {
157                parent_bank_hash: bank.parent_hash().to_string(),
158                accounts_delta_hash,
159                signature_count: bank.signature_count(),
160                last_blockhash: bank.last_blockhash().to_string(),
161                // The bank is already frozen so this should not have to wait
162                epoch_accounts_hash: bank
163                    .wait_get_epoch_accounts_hash()
164                    .map(|hash| hash.as_ref().to_string()),
165                accounts_lt_hash_checksum: bank
166                    .feature_set
167                    .is_active(&feature_set::accounts_lt_hash::id())
168                    .then(|| {
169                        bank.accounts_lt_hash
170                            .lock()
171                            .unwrap()
172                            .0
173                            .checksum()
174                            .to_string()
175                    }),
176                accounts: AccountsDetails { accounts },
177            })
178        } else {
179            None
180        };
181
182        Ok(Self {
183            slot,
184            bank_hash: bank.hash().to_string(),
185            bank_hash_components,
186            transactions: Vec::new(),
187        })
188    }
189}
190
191/// Wrapper around a Vec<_> to facilitate custom Serialize/Deserialize trait
192/// implementations.
193#[derive(Clone, Debug, Eq, PartialEq, Default)]
194pub struct AccountsDetails {
195    pub accounts: Vec<PubkeyHashAccount>,
196}
197
198/// Used as an intermediate for serializing and deserializing account fields
199/// into a human readable format.
200#[derive(Deserialize, Serialize)]
201struct SerdeAccount {
202    pubkey: String,
203    hash: String,
204    owner: String,
205    lamports: u64,
206    rent_epoch: Epoch,
207    executable: bool,
208    data: String,
209}
210
211impl From<&PubkeyHashAccount> for SerdeAccount {
212    fn from(pubkey_hash_account: &PubkeyHashAccount) -> Self {
213        let PubkeyHashAccount {
214            pubkey,
215            hash,
216            account,
217        } = pubkey_hash_account;
218        Self {
219            pubkey: pubkey.to_string(),
220            hash: hash.0.to_string(),
221            owner: account.owner().to_string(),
222            lamports: account.lamports(),
223            rent_epoch: account.rent_epoch(),
224            executable: account.executable(),
225            data: BASE64_STANDARD.encode(account.data()),
226        }
227    }
228}
229
230impl TryFrom<SerdeAccount> for PubkeyHashAccount {
231    type Error = String;
232
233    fn try_from(temp_account: SerdeAccount) -> Result<Self, Self::Error> {
234        let pubkey = Pubkey::from_str(&temp_account.pubkey).map_err(|err| err.to_string())?;
235        let hash = AccountHash(Hash::from_str(&temp_account.hash).map_err(|err| err.to_string())?);
236
237        let account = AccountSharedData::from(Account {
238            lamports: temp_account.lamports,
239            data: BASE64_STANDARD
240                .decode(temp_account.data)
241                .map_err(|err| err.to_string())?,
242            owner: Pubkey::from_str(&temp_account.owner).map_err(|err| err.to_string())?,
243            executable: temp_account.executable,
244            rent_epoch: temp_account.rent_epoch,
245        });
246
247        Ok(Self {
248            pubkey,
249            hash,
250            account,
251        })
252    }
253}
254
255impl Serialize for AccountsDetails {
256    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
257    where
258        S: Serializer,
259    {
260        let mut seq = serializer.serialize_seq(Some(self.accounts.len()))?;
261        for account in self.accounts.iter() {
262            let temp_account = SerdeAccount::from(account);
263            seq.serialize_element(&temp_account)?;
264        }
265        seq.end()
266    }
267}
268
269impl<'de> Deserialize<'de> for AccountsDetails {
270    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
271    where
272        D: Deserializer<'de>,
273    {
274        let temp_accounts: Vec<SerdeAccount> = Deserialize::deserialize(deserializer)?;
275        let pubkey_hash_accounts: Result<Vec<_>, _> = temp_accounts
276            .into_iter()
277            .map(PubkeyHashAccount::try_from)
278            .collect();
279        let pubkey_hash_accounts = pubkey_hash_accounts.map_err(de::Error::custom)?;
280        Ok(AccountsDetails {
281            accounts: pubkey_hash_accounts,
282        })
283    }
284}
285
286/// Output the components that comprise the overall bank hash for the supplied `Bank`
287pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), String> {
288    let slot_details = SlotDetails::new_from_bank(bank, /*include_bank_hash_mixins:*/ true)?;
289    let details = BankHashDetails::new(vec![slot_details]);
290
291    let parent_dir = bank
292        .rc
293        .accounts
294        .accounts_db
295        .get_base_working_path()
296        .join("bank_hash_details");
297    let path = parent_dir.join(details.filename()?);
298    // A file with the same name implies the same hash for this slot. Skip
299    // rewriting a duplicate file in this scenario
300    if !path.exists() {
301        info!("writing bank hash details file: {}", path.display());
302
303        // std::fs::write may fail (depending on platform) if the full directory
304        // path does not exist. So, call std::fs_create_dir_all first.
305        // https://doc.rust-lang.org/std/fs/fn.write.html
306        _ = std::fs::create_dir_all(parent_dir);
307        let file = std::fs::File::create(&path)
308            .map_err(|err| format!("Unable to create file at {}: {err}", path.display()))?;
309
310        // writing the json file ends up with a syscall for each number, comma, indentation etc.
311        // use BufWriter to speed things up
312        let writer = std::io::BufWriter::new(file);
313
314        serde_json::to_writer_pretty(writer, &details)
315            .map_err(|err| format!("Unable to write file at {}: {err}", path.display()))?;
316    }
317    Ok(())
318}
319
320#[cfg(test)]
321pub mod tests {
322    use super::*;
323
324    fn build_details(num_slots: usize) -> BankHashDetails {
325        let slot_details: Vec<_> = (0..num_slots)
326            .map(|slot| {
327                let slot = slot as u64;
328
329                let account = AccountSharedData::from(Account {
330                    lamports: 123_456_789,
331                    data: vec![0, 9, 1, 8, 2, 7, 3, 6, 4, 5],
332                    owner: Pubkey::new_unique(),
333                    executable: true,
334                    rent_epoch: 123,
335                });
336                let account_pubkey = Pubkey::new_unique();
337                let account_hash = AccountHash(solana_sdk::hash::hash("account".as_bytes()));
338                let accounts = AccountsDetails {
339                    accounts: vec![PubkeyHashAccount {
340                        pubkey: account_pubkey,
341                        hash: account_hash,
342                        account,
343                    }],
344                };
345
346                SlotDetails {
347                    slot,
348                    bank_hash: format!("bank{slot}"),
349                    bank_hash_components: Some(BankHashComponents {
350                        parent_bank_hash: "parent_bank_hash".into(),
351                        accounts_delta_hash: if slot % 4 == 0 {
352                            None
353                        } else {
354                            Some("accounts_delta_hash".into())
355                        },
356                        signature_count: slot + 10,
357                        last_blockhash: "last_blockhash".into(),
358                        epoch_accounts_hash: if slot % 2 == 0 {
359                            Some("epoch_accounts_hash".into())
360                        } else {
361                            None
362                        },
363                        accounts_lt_hash_checksum: if slot % 3 == 0 {
364                            None
365                        } else {
366                            Some("accounts_lt_hash_checksum".into())
367                        },
368                        accounts,
369                    }),
370                    transactions: vec![],
371                }
372            })
373            .collect();
374
375        BankHashDetails::new(slot_details)
376    }
377
378    #[test]
379    fn test_serde_bank_hash_details() {
380        let num_slots = 10;
381        let bank_hash_details = build_details(num_slots);
382
383        let serialized_bytes = serde_json::to_vec(&bank_hash_details).unwrap();
384        let deserialized_bank_hash_details: BankHashDetails =
385            serde_json::from_slice(&serialized_bytes).unwrap();
386
387        assert_eq!(bank_hash_details, deserialized_bank_hash_details);
388    }
389}