Skip to main content

solana_core/
completed_data_sets_service.rs

1//! [`CompletedDataSetsService`] is a hub that runs different operations when a completed data set
2//! is received by the validator.
3//!
4//! A completed data set is a contiguous range of data shreds whose combined payload deserializes
5//! to a single [`Vec<Entry>`].
6//!
7//! Currently, `WindowService` sends [`CompletedDataSetInfo`]s via a `completed_sets_receiver`
8//! provided to the [`CompletedDataSetsService`].
9
10use {
11    crossbeam_channel::{Receiver, RecvTimeoutError, Sender},
12    solana_entry::entry::Entry,
13    solana_ledger::{
14        blockstore::{Blockstore, CompletedDataSetInfo},
15        deshred_transaction_notifier_interface::{
16            DeshredTransactionNotifier, DeshredTransactionNotifierArc,
17        },
18    },
19    solana_measure::measure::Measure,
20    solana_message::{VersionedMessage, v0::LoadedAddresses},
21    solana_metrics::*,
22    solana_rpc::{max_slots::MaxSlots, rpc_subscriptions::RpcSubscriptions},
23    solana_runtime::bank_forks::BankForks,
24    solana_signature::Signature,
25    solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup,
26    solana_transaction::{
27        simple_vote_transaction_checker::is_simple_vote_transaction_impl,
28        versioned::VersionedTransaction,
29    },
30    std::{
31        sync::{
32            Arc, RwLock,
33            atomic::{AtomicBool, Ordering},
34        },
35        thread::{self, Builder, JoinHandle},
36        time::Duration,
37    },
38};
39
40pub type CompletedDataSetsReceiver = Receiver<Vec<CompletedDataSetInfo>>;
41pub type CompletedDataSetsSender = Sender<Vec<CompletedDataSetInfo>>;
42
43/// Check if a versioned transaction is a simple vote transaction.
44/// This avoids cloning by extracting the required data directly.
45fn is_simple_vote_transaction(tx: &VersionedTransaction) -> bool {
46    let is_legacy = matches!(&tx.message, VersionedMessage::Legacy(_));
47    let instruction_programs = tx.message.instructions().iter().filter_map(|ix| {
48        tx.message
49            .static_account_keys()
50            .get(ix.program_id_index as usize)
51    });
52    is_simple_vote_transaction_impl(&tx.signatures, is_legacy, instruction_programs)
53}
54
55/// Result of attempting to load addresses from address lookup tables.
56enum LutLoadResult {
57    /// Transaction has no address table lookups (legacy or empty lookups).
58    NoLookups,
59    /// Lookups were present and resolved successfully.
60    Resolved(LoadedAddresses),
61    /// Lookups were present but resolution failed.
62    Failed,
63}
64
65/// Load addresses from address lookup tables for a versioned transaction.
66/// Takes a Bank reference to avoid repeated lock acquisition.
67fn load_transaction_addresses(
68    tx: &VersionedTransaction,
69    bank: &solana_runtime::bank::Bank,
70) -> LutLoadResult {
71    let Some(address_table_lookups) = tx.message.address_table_lookups() else {
72        return LutLoadResult::NoLookups;
73    };
74    if address_table_lookups.is_empty() {
75        return LutLoadResult::NoLookups;
76    }
77
78    match bank.load_addresses_from_ref(
79        address_table_lookups
80            .iter()
81            .map(SVMMessageAddressTableLookup::from),
82    ) {
83        Ok((addresses, _deactivation_slot)) => LutLoadResult::Resolved(addresses),
84        Err(_) => LutLoadResult::Failed,
85    }
86}
87
88#[derive(Debug, Default, PartialEq, Eq)]
89struct DeshredBatchStats {
90    total_lut_load_us: u64,
91    total_notify_us: u64,
92    total_transactions: u64,
93    total_entries: u64,
94    total_data_sets: u64,
95    lut_transactions: u64,
96    lut_failures: u64,
97}
98
99pub struct CompletedDataSetsService {
100    thread_hdl: JoinHandle<()>,
101}
102
103impl CompletedDataSetsService {
104    pub fn new(
105        completed_sets_receiver: CompletedDataSetsReceiver,
106        blockstore: Arc<Blockstore>,
107        rpc_subscriptions: Arc<RpcSubscriptions>,
108        deshred_transaction_notifier: Option<DeshredTransactionNotifierArc>,
109        exit: Arc<AtomicBool>,
110        max_slots: Arc<MaxSlots>,
111        bank_forks: Arc<RwLock<BankForks>>,
112    ) -> Self {
113        let thread_hdl = Builder::new()
114            .name("solComplDataSet".to_string())
115            .spawn(move || {
116                info!("CompletedDataSetsService has started");
117                loop {
118                    if exit.load(Ordering::Relaxed) {
119                        break;
120                    }
121                    if let Err(RecvTimeoutError::Disconnected) = Self::recv_completed_data_sets(
122                        &completed_sets_receiver,
123                        &blockstore,
124                        &rpc_subscriptions,
125                        &deshred_transaction_notifier,
126                        &max_slots,
127                        &bank_forks,
128                    ) {
129                        break;
130                    }
131                }
132                info!("CompletedDataSetsService has stopped");
133            })
134            .unwrap();
135        Self { thread_hdl }
136    }
137
138    fn recv_completed_data_sets(
139        completed_sets_receiver: &CompletedDataSetsReceiver,
140        blockstore: &Blockstore,
141        rpc_subscriptions: &RpcSubscriptions,
142        deshred_transaction_notifier: &Option<DeshredTransactionNotifierArc>,
143        max_slots: &Arc<MaxSlots>,
144        bank_forks: &RwLock<BankForks>,
145    ) -> Result<(), RecvTimeoutError> {
146        const RECV_TIMEOUT: Duration = Duration::from_secs(1);
147        let first_completed_data_sets = completed_sets_receiver.recv_timeout(RECV_TIMEOUT)?;
148        let root_bank = deshred_transaction_notifier
149            .as_ref()
150            .filter(|notifier| notifier.alt_resolution_enabled())
151            .map(|_| {
152                // Best-effort ALT resolution uses the rooted bank to avoid surfacing fork-local state.
153                bank_forks.read().unwrap().root_bank()
154            });
155        let mut batch_measure = Measure::start("deshred_geyser_batch");
156        let mut stats = DeshredBatchStats::default();
157
158        let slots = std::iter::once(first_completed_data_sets)
159            .chain(completed_sets_receiver.try_iter())
160            .flatten()
161            .map(|completed_data_set_info| {
162                let CompletedDataSetInfo { slot, indices } = completed_data_set_info;
163                let completed_data_set_starting_shred_index = indices.start;
164                let completed_data_set_ending_shred_index_exclusive = indices.end;
165                match blockstore.get_entries_in_data_block(slot, indices, /*slot_meta:*/ None) {
166                    Ok(entries) => {
167                        Self::notify_deshred_transactions_for_completed_data_set(
168                            slot,
169                            completed_data_set_starting_shred_index,
170                            completed_data_set_ending_shred_index_exclusive,
171                            &entries,
172                            deshred_transaction_notifier.as_deref(),
173                            root_bank.as_deref(),
174                            &mut stats,
175                        );
176
177                        let transactions = Self::get_transaction_signatures(entries);
178                        if !transactions.is_empty() {
179                            rpc_subscriptions.notify_signatures_received((slot, transactions));
180                        }
181                    }
182                    Err(e) => warn!("completed-data-set-service deserialize error: {e:?}"),
183                }
184                slot
185            });
186
187        if let Some(slot) = slots.max() {
188            max_slots.shred_insert.fetch_max(slot, Ordering::Relaxed);
189        }
190
191        batch_measure.stop();
192
193        if deshred_transaction_notifier.is_some() {
194            let avg_notify_us = stats
195                .total_notify_us
196                .checked_div(stats.total_transactions)
197                .unwrap_or(0);
198            datapoint_info!(
199                "deshred_geyser_timing",
200                ("batch_total_us", batch_measure.as_us() as i64, i64),
201                ("notify_total_us", stats.total_notify_us as i64, i64),
202                ("lut_load_total_us", stats.total_lut_load_us as i64, i64),
203                ("transactions_count", stats.total_transactions as i64, i64),
204                ("lut_transactions_count", stats.lut_transactions as i64, i64),
205                ("lut_failures_count", stats.lut_failures as i64, i64),
206                ("entries_count", stats.total_entries as i64, i64),
207                ("data_sets_count", stats.total_data_sets as i64, i64),
208                ("avg_notify_us", avg_notify_us as i64, i64),
209            );
210        }
211
212        Ok(())
213    }
214
215    fn notify_deshred_transactions_for_completed_data_set(
216        slot: u64,
217        completed_data_set_starting_shred_index: u32,
218        completed_data_set_ending_shred_index_exclusive: u32,
219        entries: &[Entry],
220        deshred_transaction_notifier: Option<&(dyn DeshredTransactionNotifier + Send + Sync)>,
221        root_bank: Option<&solana_runtime::bank::Bank>,
222        stats: &mut DeshredBatchStats,
223    ) {
224        let Some(notifier) = deshred_transaction_notifier else {
225            return;
226        };
227
228        stats.total_data_sets += 1;
229        stats.total_entries += entries.len() as u64;
230
231        for entry in entries {
232            for tx in &entry.transactions {
233                let Some(signature) = tx.signatures.first() else {
234                    continue;
235                };
236
237                stats.total_transactions += 1;
238                let is_vote = is_simple_vote_transaction(tx);
239
240                let mut lut_measure = Measure::start("load_lut");
241                let lut_result = root_bank
242                    .map(|bank| load_transaction_addresses(tx, bank))
243                    .unwrap_or(LutLoadResult::NoLookups);
244                lut_measure.stop();
245
246                let loaded_addresses = match lut_result {
247                    LutLoadResult::Resolved(addresses) => {
248                        stats.lut_transactions += 1;
249                        stats.total_lut_load_us += lut_measure.as_us();
250                        Some(addresses)
251                    }
252                    LutLoadResult::Failed => {
253                        stats.lut_failures += 1;
254                        stats.total_lut_load_us += lut_measure.as_us();
255                        None
256                    }
257                    LutLoadResult::NoLookups => None,
258                };
259
260                let mut notify_measure = Measure::start("notify_deshred");
261                notifier.notify_deshred_transaction(
262                    slot,
263                    completed_data_set_starting_shred_index,
264                    completed_data_set_ending_shred_index_exclusive,
265                    signature,
266                    is_vote,
267                    tx,
268                    loaded_addresses.as_ref(),
269                );
270                notify_measure.stop();
271                stats.total_notify_us += notify_measure.as_us();
272            }
273        }
274    }
275
276    fn get_transaction_signatures(entries: Vec<Entry>) -> Vec<Signature> {
277        entries
278            .into_iter()
279            .flat_map(|e| {
280                e.transactions
281                    .into_iter()
282                    .filter_map(|mut t| t.signatures.drain(..).next())
283            })
284            .collect::<Vec<Signature>>()
285    }
286
287    pub fn join(self) -> thread::Result<()> {
288        self.thread_hdl.join()
289    }
290}
291
292#[cfg(test)]
293pub mod test {
294    use {
295        super::*,
296        crossbeam_channel::bounded,
297        solana_entry::entry::next_versioned_entry,
298        solana_genesis_config::GenesisConfig,
299        solana_hash::Hash,
300        solana_instruction::Instruction,
301        solana_keypair::Keypair,
302        solana_ledger::{
303            blockstore, blockstore::Blockstore, get_tmp_ledger_path_auto_delete,
304            shred::max_ticks_per_n_shreds,
305        },
306        solana_message::{
307            Message, VersionedMessage,
308            v0::{self, LoadedAddresses},
309        },
310        solana_pubkey::Pubkey,
311        solana_rpc::{
312            max_slots::MaxSlots,
313            optimistically_confirmed_bank_tracker::OptimisticallyConfirmedBank,
314            rpc_subscriptions::RpcSubscriptions,
315        },
316        solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache},
317        solana_signature::Signature,
318        solana_signer::Signer,
319        solana_transaction::{Transaction, versioned::VersionedTransaction},
320        std::sync::{
321            Arc, Mutex, RwLock,
322            atomic::{AtomicBool, AtomicU64, Ordering},
323        },
324    };
325
326    #[derive(Clone, Debug, PartialEq, Eq)]
327    struct DeshredNotification {
328        slot: u64,
329        completed_data_set_starting_shred_index: u32,
330        completed_data_set_ending_shred_index_exclusive: u32,
331        signature: Signature,
332        is_vote: bool,
333        transaction: VersionedTransaction,
334        loaded_addresses: Option<LoadedAddresses>,
335    }
336
337    #[derive(Default)]
338    struct TestDeshredTransactionNotifier {
339        notifications: Mutex<Vec<DeshredNotification>>,
340    }
341
342    impl DeshredTransactionNotifier for TestDeshredTransactionNotifier {
343        fn notify_deshred_transaction(
344            &self,
345            slot: u64,
346            completed_data_set_starting_shred_index: u32,
347            completed_data_set_ending_shred_index_exclusive: u32,
348            signature: &Signature,
349            is_vote: bool,
350            transaction: &VersionedTransaction,
351            loaded_addresses: Option<&LoadedAddresses>,
352        ) {
353            self.notifications
354                .lock()
355                .unwrap()
356                .push(DeshredNotification {
357                    slot,
358                    completed_data_set_starting_shred_index,
359                    completed_data_set_ending_shred_index_exclusive,
360                    signature: *signature,
361                    is_vote,
362                    transaction: transaction.clone(),
363                    loaded_addresses: loaded_addresses.cloned(),
364                });
365        }
366
367        fn alt_resolution_enabled(&self) -> bool {
368            false
369        }
370    }
371
372    fn legacy_transaction(instruction: Instruction) -> VersionedTransaction {
373        let keypair = Keypair::new();
374        VersionedTransaction::try_new(
375            VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey()))),
376            &[&keypair],
377        )
378        .unwrap()
379    }
380
381    fn versioned_v0_transaction(instruction: Instruction) -> VersionedTransaction {
382        let keypair = Keypair::new();
383        let message =
384            v0::Message::try_compile(&keypair.pubkey(), &[instruction], &[], Hash::default())
385                .unwrap();
386        VersionedTransaction::try_new(VersionedMessage::V0(message), &[&keypair]).unwrap()
387    }
388
389    #[test]
390    fn test_zero_signatures() {
391        let tx = Transaction::new_with_payer(&[], None);
392        let entries = vec![Entry::new(&Hash::default(), 1, vec![tx])];
393        let signatures = CompletedDataSetsService::get_transaction_signatures(entries);
394        assert!(signatures.is_empty());
395    }
396
397    #[test]
398    fn test_multi_signatures() {
399        let kp = Keypair::new();
400        let tx =
401            Transaction::new_signed_with_payer(&[], Some(&kp.pubkey()), &[&kp], Hash::default());
402        let entries = vec![Entry::new(&Hash::default(), 1, vec![tx.clone()])];
403        let signatures = CompletedDataSetsService::get_transaction_signatures(entries);
404        assert_eq!(signatures.len(), 1);
405
406        let entries = vec![
407            Entry::new(&Hash::default(), 1, vec![tx.clone(), tx.clone()]),
408            Entry::new(&Hash::default(), 1, vec![tx]),
409        ];
410        let signatures = CompletedDataSetsService::get_transaction_signatures(entries);
411        assert_eq!(signatures.len(), 3);
412    }
413
414    #[test]
415    fn test_is_simple_vote_transaction_paths() {
416        let vote_instruction =
417            Instruction::new_with_bytes(solana_sdk_ids::vote::ID, &[], Vec::new());
418        let non_vote_instruction =
419            Instruction::new_with_bytes(Pubkey::new_unique(), &[], Vec::new());
420
421        assert!(is_simple_vote_transaction(&legacy_transaction(
422            vote_instruction
423        )));
424        assert!(!is_simple_vote_transaction(&legacy_transaction(
425            non_vote_instruction
426        )));
427        assert!(!is_simple_vote_transaction(&versioned_v0_transaction(
428            Instruction::new_with_bytes(solana_sdk_ids::vote::ID, &[], Vec::new()),
429        )));
430    }
431
432    #[test]
433    fn test_load_transaction_addresses_returns_no_lookups_without_lookups() {
434        let bank = Bank::new_for_tests(&GenesisConfig::default());
435
436        assert!(matches!(
437            load_transaction_addresses(
438                &legacy_transaction(Instruction::new_with_bytes(
439                    Pubkey::new_unique(),
440                    &[],
441                    Vec::new(),
442                )),
443                &bank,
444            ),
445            LutLoadResult::NoLookups
446        ));
447        assert!(matches!(
448            load_transaction_addresses(
449                &versioned_v0_transaction(Instruction::new_with_bytes(
450                    Pubkey::new_unique(),
451                    &[],
452                    Vec::new(),
453                )),
454                &bank,
455            ),
456            LutLoadResult::NoLookups
457        ));
458    }
459
460    #[test]
461    fn test_notify_deshred_transactions_for_completed_data_set() {
462        let notifier = TestDeshredTransactionNotifier::default();
463        let legacy_vote_tx = legacy_transaction(Instruction::new_with_bytes(
464            solana_sdk_ids::vote::ID,
465            &[],
466            Vec::new(),
467        ));
468        let legacy_non_vote_tx = legacy_transaction(Instruction::new_with_bytes(
469            Pubkey::new_unique(),
470            &[],
471            Vec::new(),
472        ));
473        let unsigned_tx = VersionedTransaction {
474            signatures: vec![],
475            message: VersionedMessage::Legacy(Message::new(&[], None)),
476        };
477        let entries = vec![
478            next_versioned_entry(&Hash::default(), 1, vec![legacy_vote_tx.clone()]),
479            next_versioned_entry(
480                &Hash::new_unique(),
481                1,
482                vec![legacy_non_vote_tx.clone(), unsigned_tx],
483            ),
484        ];
485        let mut stats = DeshredBatchStats::default();
486
487        CompletedDataSetsService::notify_deshred_transactions_for_completed_data_set(
488            42,
489            7,
490            9,
491            &entries,
492            Some(&notifier),
493            None,
494            &mut stats,
495        );
496
497        let notifications = notifier.notifications.lock().unwrap().clone();
498        assert_eq!(notifications.len(), 2);
499        assert_eq!(notifications[0].slot, 42);
500        assert_eq!(notifications[0].completed_data_set_starting_shred_index, 7);
501        assert_eq!(
502            notifications[0].completed_data_set_ending_shred_index_exclusive,
503            9
504        );
505        assert_eq!(notifications[0].signature, legacy_vote_tx.signatures[0]);
506        assert!(notifications[0].is_vote);
507        assert_eq!(notifications[1].completed_data_set_starting_shred_index, 7);
508        assert_eq!(
509            notifications[1].completed_data_set_ending_shred_index_exclusive,
510            9
511        );
512        assert_eq!(notifications[1].signature, legacy_non_vote_tx.signatures[0]);
513        assert!(!notifications[1].is_vote);
514        assert!(
515            notifications
516                .iter()
517                .all(|notification| notification.loaded_addresses.is_none())
518        );
519        assert_eq!(stats.total_transactions, 2);
520        assert_eq!(stats.total_entries, 2);
521        assert_eq!(stats.total_data_sets, 1);
522        assert_eq!(stats.total_lut_load_us, 0);
523        assert_eq!(stats.lut_transactions, 0);
524        assert_eq!(stats.lut_failures, 0);
525    }
526
527    #[test]
528    fn test_recv_completed_data_sets_notifies_and_updates_max_slot() {
529        let ledger_path = get_tmp_ledger_path_auto_delete!();
530        let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap());
531        let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&GenesisConfig::default()));
532        let rpc_subscriptions = RpcSubscriptions::new_for_tests_with_blockstore(
533            Arc::new(AtomicBool::new(false)),
534            Arc::new(AtomicU64::default()),
535            blockstore.clone(),
536            bank_forks.clone(),
537            Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())),
538            OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
539        );
540        let max_slots = Arc::new(MaxSlots::default());
541        let test_notifier = Arc::new(TestDeshredTransactionNotifier::default());
542        let notifier = Some(test_notifier.clone() as DeshredTransactionNotifierArc);
543        let (sender, receiver) = bounded(1);
544        let entries = vec![next_versioned_entry(
545            &Hash::default(),
546            1,
547            vec![legacy_transaction(Instruction::new_with_bytes(
548                Pubkey::new_unique(),
549                &[],
550                Vec::new(),
551            ))],
552        )];
553        let shreds = blockstore::entries_to_test_shreds(&entries, 11, 10, true, 0);
554        let completed_data_sets = blockstore.insert_shreds(shreds, None, true).unwrap();
555        assert_eq!(completed_data_sets.len(), 1);
556        let completed_data_set = completed_data_sets[0].clone();
557        sender.send(completed_data_sets).unwrap();
558
559        CompletedDataSetsService::recv_completed_data_sets(
560            &receiver,
561            &blockstore,
562            &rpc_subscriptions,
563            &notifier,
564            &max_slots,
565            &bank_forks,
566        )
567        .unwrap();
568
569        let notifications = test_notifier.notifications.lock().unwrap().clone();
570        assert_eq!(notifications.len(), 1);
571        assert_eq!(notifications[0].slot, 11);
572        assert_eq!(
573            notifications[0].completed_data_set_starting_shred_index,
574            completed_data_set.indices.start
575        );
576        assert_eq!(
577            notifications[0].completed_data_set_ending_shred_index_exclusive,
578            completed_data_set.indices.end
579        );
580        assert_eq!(max_slots.shred_insert.load(Ordering::Relaxed), 11);
581    }
582
583    #[test]
584    fn test_recv_completed_data_sets_notifies_completed_data_set_range_for_multi_shred_batch() {
585        let ledger_path = get_tmp_ledger_path_auto_delete!();
586        let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap());
587        let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&GenesisConfig::default()));
588        let rpc_subscriptions = RpcSubscriptions::new_for_tests_with_blockstore(
589            Arc::new(AtomicBool::new(false)),
590            Arc::new(AtomicU64::default()),
591            blockstore.clone(),
592            bank_forks.clone(),
593            Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())),
594            OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks),
595        );
596        let max_slots = Arc::new(MaxSlots::default());
597        let test_notifier = Arc::new(TestDeshredTransactionNotifier::default());
598        let notifier = Some(test_notifier.clone() as DeshredTransactionNotifierArc);
599        let (sender, receiver) = bounded(1);
600
601        let num_entries = max_ticks_per_n_shreds(1, None) as usize + 1;
602        let mut previous_hash = Hash::default();
603        let entries: Vec<_> = (0..num_entries)
604            .map(|_| {
605                let entry = next_versioned_entry(
606                    &previous_hash,
607                    1,
608                    vec![legacy_transaction(Instruction::new_with_bytes(
609                        Pubkey::new_unique(),
610                        &[],
611                        Vec::new(),
612                    ))],
613                );
614                previous_hash = entry.hash;
615                entry
616            })
617            .collect();
618        let shreds = blockstore::entries_to_test_shreds(&entries, 12, 11, true, 0);
619        assert!(shreds.len() > 1);
620        let completed_data_sets = blockstore.insert_shreds(shreds, None, true).unwrap();
621        assert_eq!(completed_data_sets.len(), 1);
622        let completed_data_set = completed_data_sets[0].clone();
623        sender.send(completed_data_sets).unwrap();
624
625        CompletedDataSetsService::recv_completed_data_sets(
626            &receiver,
627            &blockstore,
628            &rpc_subscriptions,
629            &notifier,
630            &max_slots,
631            &bank_forks,
632        )
633        .unwrap();
634
635        let notifications = test_notifier.notifications.lock().unwrap().clone();
636        assert_eq!(notifications.len(), num_entries);
637        assert!(notifications.iter().all(|notification| {
638            notification.slot == 12
639                && notification.completed_data_set_starting_shred_index
640                    == completed_data_set.indices.start
641                && notification.completed_data_set_ending_shred_index_exclusive
642                    == completed_data_set.indices.end
643        }));
644    }
645
646    #[test]
647    fn test_lut_failure_stats_accumulated() {
648        let notifier = TestDeshredTransactionNotifier::default();
649        let bank = Bank::new_for_tests(&GenesisConfig::default());
650        let keypair = Keypair::new();
651        // V0 transaction with a lookup referencing a non-existent table
652        let message = v0::Message {
653            header: solana_message::MessageHeader {
654                num_required_signatures: 1,
655                num_readonly_signed_accounts: 0,
656                num_readonly_unsigned_accounts: 0,
657            },
658            account_keys: vec![keypair.pubkey()],
659            recent_blockhash: Hash::default(),
660            instructions: vec![],
661            address_table_lookups: vec![solana_message::v0::MessageAddressTableLookup {
662                account_key: Pubkey::new_unique(),
663                writable_indexes: vec![0],
664                readonly_indexes: vec![],
665            }],
666        };
667        let tx_with_lut =
668            VersionedTransaction::try_new(VersionedMessage::V0(message), &[&keypair]).unwrap();
669        let entries = vec![next_versioned_entry(&Hash::default(), 1, vec![tx_with_lut])];
670        let mut stats = DeshredBatchStats::default();
671
672        CompletedDataSetsService::notify_deshred_transactions_for_completed_data_set(
673            10,
674            0,
675            1,
676            &entries,
677            Some(&notifier),
678            Some(&bank),
679            &mut stats,
680        );
681
682        assert_eq!(stats.total_transactions, 1);
683        assert_eq!(stats.lut_failures, 1);
684        assert_eq!(stats.lut_transactions, 0);
685        // Failed lookups should still accumulate timing
686        // (the actual value depends on execution speed, just verify it was set)
687        assert!(stats.total_lut_load_us > 0 || stats.lut_failures == 1);
688
689        let notifications = notifier.notifications.lock().unwrap().clone();
690        assert_eq!(notifications.len(), 1);
691        assert!(notifications[0].loaded_addresses.is_none());
692    }
693
694    #[test]
695    fn test_alt_resolution_skipped_when_root_bank_absent() {
696        // When root_bank is None (ALT resolution not opted in), no LUT stats are recorded
697        let notifier = TestDeshredTransactionNotifier::default();
698        let keypair = Keypair::new();
699        let message = v0::Message {
700            header: solana_message::MessageHeader {
701                num_required_signatures: 1,
702                num_readonly_signed_accounts: 0,
703                num_readonly_unsigned_accounts: 0,
704            },
705            account_keys: vec![keypair.pubkey()],
706            recent_blockhash: Hash::default(),
707            instructions: vec![],
708            address_table_lookups: vec![solana_message::v0::MessageAddressTableLookup {
709                account_key: Pubkey::new_unique(),
710                writable_indexes: vec![0],
711                readonly_indexes: vec![],
712            }],
713        };
714        let tx_with_lut =
715            VersionedTransaction::try_new(VersionedMessage::V0(message), &[&keypair]).unwrap();
716        let entries = vec![next_versioned_entry(&Hash::default(), 1, vec![tx_with_lut])];
717        let mut stats = DeshredBatchStats::default();
718
719        // Pass None for root_bank, simulates ALT resolution not being opted in
720        CompletedDataSetsService::notify_deshred_transactions_for_completed_data_set(
721            10,
722            0,
723            1,
724            &entries,
725            Some(&notifier),
726            None,
727            &mut stats,
728        );
729
730        assert_eq!(stats.total_transactions, 1);
731        assert_eq!(stats.lut_failures, 0);
732        assert_eq!(stats.lut_transactions, 0);
733        assert_eq!(stats.total_lut_load_us, 0);
734
735        let notifications = notifier.notifications.lock().unwrap().clone();
736        assert_eq!(notifications.len(), 1);
737        assert!(notifications[0].loaded_addresses.is_none());
738    }
739}