1use {
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 pub version: String,
34 pub account_data_encoding: String,
36 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 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 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#[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 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 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#[derive(Clone, Debug, Eq, PartialEq, Default)]
176pub struct AccountsDetails {
177 pub accounts: Vec<PubkeyHashAccount>,
178}
179
180#[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
268pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), String> {
270 let slot_details = SlotDetails::new_from_bank(bank, 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 if !path.exists() {
283 info!("writing bank hash details file: {}", path.display());
284
285 _ = 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 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}