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::{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 pub version: String,
32 pub account_data_encoding: String,
34 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 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 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#[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 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 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#[derive(Clone, Debug, Eq, PartialEq, Default)]
194pub struct AccountsDetails {
195 pub accounts: Vec<PubkeyHashAccount>,
196}
197
198#[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
286pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), String> {
288 let slot_details = SlotDetails::new_from_bank(bank, 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 if !path.exists() {
301 info!("writing bank hash details file: {}", path.display());
302
303 _ = 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 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}