Skip to main content

jito_bundle/
analysis.rs

1use crate::constants::SOLANA_MAX_TX_SIZE;
2use solana_instruction::Instruction;
3use solana_pubkey::Pubkey;
4use solana_sdk::address_lookup_table::AddressLookupTableAccount;
5use solana_sdk::transaction::VersionedTransaction;
6use std::collections::HashSet;
7
8#[derive(Debug)]
9pub struct TransactionSizeInfo {
10    pub size: usize,
11    pub max_size: usize,
12    pub is_oversized: bool,
13}
14
15#[derive(Debug)]
16pub struct AccountsNotInLuts {
17    pub accounts: Vec<Pubkey>,
18    pub total_accounts: usize,
19    pub accounts_in_luts: usize,
20}
21
22pub struct TransactionAnalysis;
23
24impl TransactionAnalysis {
25    pub fn analyze_transaction_size(tx: &VersionedTransaction) -> TransactionSizeInfo {
26        let size = bincode::serialize(tx).map_or(0, |s| s.len());
27        TransactionSizeInfo {
28            size,
29            max_size: SOLANA_MAX_TX_SIZE,
30            is_oversized: size > SOLANA_MAX_TX_SIZE,
31        }
32    }
33
34    pub fn get_accounts_not_in_luts(
35        instructions: &[Instruction],
36        lookup_tables: &[AddressLookupTableAccount],
37    ) -> AccountsNotInLuts {
38        let all_accounts: HashSet<Pubkey> = instructions
39            .iter()
40            .flat_map(|ix| {
41                std::iter::once(ix.program_id).chain(ix.accounts.iter().map(|m| m.pubkey))
42            })
43            .collect();
44
45        let lut_accounts: HashSet<Pubkey> = lookup_tables
46            .iter()
47            .flat_map(|lut| lut.addresses.iter().copied())
48            .collect();
49
50        let accounts: Vec<Pubkey> = all_accounts.difference(&lut_accounts).copied().collect();
51        let total_accounts = all_accounts.len();
52        let accounts_in_luts = total_accounts.saturating_sub(accounts.len());
53
54        AccountsNotInLuts {
55            accounts,
56            total_accounts,
57            accounts_in_luts,
58        }
59    }
60
61    pub fn log_transaction_size_warning(tx: &VersionedTransaction, tx_index: usize) {
62        let size_info = Self::analyze_transaction_size(tx);
63        if size_info.is_oversized {
64            tracing::error!(
65                "transaction {tx_index} is OVERSIZED: {size}/{max} bytes (exceeds by {excess} bytes)",
66                size = size_info.size,
67                max = size_info.max_size,
68                excess = size_info.size - size_info.max_size
69            );
70        } else {
71            tracing::info!(
72                "transaction {tx_index} size: {size}/{max} bytes ({remaining} bytes remaining)",
73                size = size_info.size,
74                max = size_info.max_size,
75                remaining = size_info.max_size - size_info.size
76            );
77        }
78    }
79
80    pub fn log_accounts_not_in_luts(
81        instructions: &[Instruction],
82        lookup_tables: &[AddressLookupTableAccount],
83        context: &str,
84    ) {
85        let analysis = Self::get_accounts_not_in_luts(instructions, lookup_tables);
86
87        if analysis.accounts.is_empty() {
88            tracing::info!(
89                "[{context}] all {total} accounts are in LUTs",
90                total = analysis.total_accounts
91            );
92            return;
93        }
94
95        tracing::warn!(
96            "[{context}] {missing_count}/{total} accounts NOT in LUTs ({in_luts} in LUTs)",
97            missing_count = analysis.accounts.len(),
98            total = analysis.total_accounts,
99            in_luts = analysis.accounts_in_luts
100        );
101
102        for (i, account) in analysis.accounts.iter().enumerate() {
103            tracing::warn!("[{context}] missing LUT account [{i}]: {account}");
104        }
105    }
106
107    pub fn log_bundle_failure_analysis(
108        transactions: &[VersionedTransaction],
109        all_instructions: &[Option<Vec<Instruction>>],
110        lookup_tables: &[AddressLookupTableAccount],
111        error: &str,
112    ) {
113        tracing::error!("=== BUNDLE FAILURE ANALYSIS ===");
114        tracing::error!("error: {error}");
115
116        for (i, tx) in transactions.iter().enumerate() {
117            tracing::error!("--- transaction {i} ---");
118            Self::log_transaction_size_warning(tx, i);
119
120            if let Some(Some(ixs)) = all_instructions.get(i) {
121                Self::log_accounts_not_in_luts(ixs, lookup_tables, &format!("TX{i}"));
122            }
123        }
124
125        tracing::error!("=== END ANALYSIS ===");
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use solana_instruction::AccountMeta;
133    use solana_pubkey::Pubkey;
134
135    #[test]
136    fn log_bundle_failure_analysis_handles_option_slots() {
137        let ix = Instruction {
138            program_id: Pubkey::new_unique(),
139            accounts: vec![AccountMeta::new(Pubkey::new_unique(), true)],
140            data: vec![1, 2, 3],
141        };
142        let slots: [Option<Vec<Instruction>>; 5] = [Some(vec![ix]), None, None, None, None];
143        TransactionAnalysis::log_bundle_failure_analysis(&[], &slots, &[], "test error");
144    }
145}