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