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/// Serialized-size details for a compiled transaction.
9#[derive(Debug)]
10pub struct TransactionSizeInfo {
11    /// Serialized transaction size in bytes.
12    pub size: usize,
13    /// Maximum allowed Solana transaction size in bytes.
14    pub max_size: usize,
15    /// Whether serialized size exceeds `max_size`.
16    pub is_oversized: bool,
17}
18
19/// Summary of accounts missing from provided lookup tables.
20#[derive(Debug)]
21pub struct AccountsNotInLuts {
22    /// Accounts referenced by instructions but absent from LUTs.
23    pub accounts: Vec<Pubkey>,
24    /// Total unique accounts referenced by instructions.
25    pub total_accounts: usize,
26    /// Number of referenced accounts found in LUTs.
27    pub accounts_in_luts: usize,
28}
29
30/// Helpers for transaction size and LUT diagnostics.
31pub struct TransactionAnalysis;
32
33impl TransactionAnalysis {
34    // --- Size Analysis ---
35    /// Computes serialized transaction size and max-size compliance.
36    pub fn analyze_transaction_size(tx: &VersionedTransaction) -> TransactionSizeInfo {
37        let size = bincode::serialize(tx).map_or(0, |s| s.len());
38        TransactionSizeInfo {
39            size,
40            max_size: SOLANA_MAX_TX_SIZE,
41            is_oversized: size > SOLANA_MAX_TX_SIZE,
42        }
43    }
44
45    // --- LUT Analysis ---
46    /// Returns accounts referenced by instructions but missing from LUTs.
47    pub fn get_accounts_not_in_luts(
48        instructions: &[Instruction],
49        lookup_tables: &[AddressLookupTableAccount],
50    ) -> AccountsNotInLuts {
51        let all_accounts: HashSet<Pubkey> = instructions
52            .iter()
53            .flat_map(|ix| {
54                std::iter::once(ix.program_id).chain(ix.accounts.iter().map(|m| m.pubkey))
55            })
56            .collect();
57
58        let lut_accounts: HashSet<Pubkey> = lookup_tables
59            .iter()
60            .flat_map(|lut| lut.addresses.iter().copied())
61            .collect();
62
63        let accounts: Vec<Pubkey> = all_accounts.difference(&lut_accounts).copied().collect();
64        let total_accounts = all_accounts.len();
65        let accounts_in_luts = total_accounts.saturating_sub(accounts.len());
66
67        AccountsNotInLuts {
68            accounts,
69            total_accounts,
70            accounts_in_luts,
71        }
72    }
73
74    // --- Logging Helpers ---
75    /// Logs serialized transaction size diagnostics.
76    pub fn log_transaction_size_warning(tx: &VersionedTransaction, tx_index: usize) {
77        let size_info = Self::analyze_transaction_size(tx);
78        if size_info.is_oversized {
79            tracing::error!(
80                "transaction {tx_index} is OVERSIZED: {size}/{max} bytes (exceeds by {excess} bytes)",
81                size = size_info.size,
82                max = size_info.max_size,
83                excess = size_info.size - size_info.max_size
84            );
85        } else {
86            tracing::info!(
87                "transaction {tx_index} size: {size}/{max} bytes ({remaining} bytes remaining)",
88                size = size_info.size,
89                max = size_info.max_size,
90                remaining = size_info.max_size - size_info.size
91            );
92        }
93    }
94
95    /// Logs which accounts are present or missing in provided LUTs.
96    pub fn log_accounts_not_in_luts(
97        instructions: &[Instruction],
98        lookup_tables: &[AddressLookupTableAccount],
99        context: &str,
100    ) {
101        let analysis = Self::get_accounts_not_in_luts(instructions, lookup_tables);
102
103        if analysis.accounts.is_empty() {
104            tracing::info!(
105                "[{context}] all {total} accounts are in LUTs",
106                total = analysis.total_accounts
107            );
108            return;
109        }
110
111        tracing::warn!(
112            "[{context}] {missing_count}/{total} accounts NOT in LUTs ({in_luts} in LUTs)",
113            missing_count = analysis.accounts.len(),
114            total = analysis.total_accounts,
115            in_luts = analysis.accounts_in_luts
116        );
117
118        for (i, account) in analysis.accounts.iter().enumerate() {
119            tracing::warn!("[{context}] missing LUT account [{i}]: {account}");
120        }
121    }
122
123    /// Logs full bundle diagnostics for post-failure debugging.
124    pub fn log_bundle_failure_analysis(
125        transactions: &[VersionedTransaction],
126        all_instructions: &[Option<Vec<Instruction>>],
127        lookup_tables: &[AddressLookupTableAccount],
128        error: &str,
129    ) {
130        tracing::error!("=== BUNDLE FAILURE ANALYSIS ===");
131        tracing::error!("error: {error}");
132
133        for (i, tx) in transactions.iter().enumerate() {
134            tracing::error!("--- transaction {i} ---");
135            Self::log_transaction_size_warning(tx, i);
136
137            if let Some(Some(ixs)) = all_instructions.get(i) {
138                Self::log_accounts_not_in_luts(ixs, lookup_tables, &format!("TX{i}"));
139            }
140        }
141
142        tracing::error!("=== END ANALYSIS ===");
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use solana_instruction::AccountMeta;
150    use solana_pubkey::Pubkey;
151
152    /// Verifies failure analysis handles sparse optional instruction slots.
153    #[test]
154    fn log_bundle_failure_analysis_handles_option_slots() {
155        let ix = Instruction {
156            program_id: Pubkey::new_unique(),
157            accounts: vec![AccountMeta::new(Pubkey::new_unique(), true)],
158            data: vec![1, 2, 3],
159        };
160        let slots: [Option<Vec<Instruction>>; 5] = [Some(vec![ix]), None, None, None, None];
161        TransactionAnalysis::log_bundle_failure_analysis(&[], &slots, &[], "test error");
162    }
163}