solana_program_runtime/
loaded_programs.rs

1use {
2    crate::{invoke_context::InvokeContext, timings::ExecuteDetailsTimings},
3    solana_measure::measure::Measure,
4    solana_rbpf::{
5        elf::Executable,
6        error::EbpfError,
7        verifier::RequisiteVerifier,
8        vm::{BuiltInProgram, VerifiedExecutable},
9    },
10    solana_sdk::{
11        bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, clock::Slot, pubkey::Pubkey,
12        saturating_add_assign,
13    },
14    std::{
15        collections::HashMap,
16        fmt::{Debug, Formatter},
17        sync::{atomic::AtomicU64, Arc},
18    },
19};
20
21/// Relationship between two fork IDs
22#[derive(Copy, Clone, PartialEq)]
23pub enum BlockRelation {
24    /// The slot is on the same fork and is an ancestor of the other slot
25    Ancestor,
26    /// The two slots are equal and are on the same fork
27    Equal,
28    /// The slot is on the same fork and is a descendant of the other slot
29    Descendant,
30    /// The slots are on two different forks and may have had a common ancestor at some point
31    Unrelated,
32    /// Either one or both of the slots are either older than the latest root, or are in future
33    Unknown,
34}
35
36/// Maps relationship between two slots.
37pub trait ForkGraph {
38    /// Returns the BlockRelation of A to B
39    fn relationship(&self, a: Slot, b: Slot) -> BlockRelation;
40}
41
42/// Provides information about current working slot, and its ancestors
43pub trait WorkingSlot {
44    /// Returns the current slot value
45    fn current_slot(&self) -> Slot;
46
47    /// Returns true if the `other` slot is an ancestor of self, false otherwise
48    fn is_ancestor(&self, other: Slot) -> bool;
49}
50
51#[derive(Default)]
52pub enum LoadedProgramType {
53    /// Tombstone for undeployed, closed or unloadable programs
54    #[default]
55    Invalid,
56    LegacyV0(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
57    LegacyV1(VerifiedExecutable<RequisiteVerifier, InvokeContext<'static>>),
58    // Typed(TypedProgram<InvokeContext<'static>>),
59    BuiltIn(BuiltInProgram<InvokeContext<'static>>),
60}
61
62impl Debug for LoadedProgramType {
63    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64        match self {
65            LoadedProgramType::Invalid => write!(f, "LoadedProgramType::Invalid"),
66            LoadedProgramType::LegacyV0(_) => write!(f, "LoadedProgramType::LegacyV0"),
67            LoadedProgramType::LegacyV1(_) => write!(f, "LoadedProgramType::LegacyV1"),
68            LoadedProgramType::BuiltIn(_) => write!(f, "LoadedProgramType::BuiltIn"),
69        }
70    }
71}
72
73#[derive(Debug, Default)]
74pub struct LoadedProgram {
75    /// The program of this entry
76    pub program: LoadedProgramType,
77    /// Size of account that stores the program and program data
78    pub account_size: usize,
79    /// Slot in which the program was (re)deployed
80    pub deployment_slot: Slot,
81    /// Slot in which this entry will become active (can be in the future)
82    pub effective_slot: Slot,
83    /// How often this entry was used
84    pub usage_counter: AtomicU64,
85}
86
87#[derive(Debug, Default)]
88pub struct LoadProgramMetrics {
89    pub program_id: String,
90    pub register_syscalls_us: u64,
91    pub load_elf_us: u64,
92    pub verify_code_us: u64,
93    pub jit_compile_us: u64,
94}
95
96impl LoadProgramMetrics {
97    pub fn submit_datapoint(&self, timings: &mut ExecuteDetailsTimings) {
98        saturating_add_assign!(
99            timings.create_executor_register_syscalls_us,
100            self.register_syscalls_us
101        );
102        saturating_add_assign!(timings.create_executor_load_elf_us, self.load_elf_us);
103        saturating_add_assign!(timings.create_executor_verify_code_us, self.verify_code_us);
104        saturating_add_assign!(timings.create_executor_jit_compile_us, self.jit_compile_us);
105        datapoint_trace!(
106            "create_executor_trace",
107            ("program_id", self.program_id, String),
108            ("register_syscalls_us", self.register_syscalls_us, i64),
109            ("load_elf_us", self.load_elf_us, i64),
110            ("verify_code_us", self.verify_code_us, i64),
111            ("jit_compile_us", self.jit_compile_us, i64),
112        );
113    }
114}
115
116impl LoadedProgram {
117    /// Creates a new user program
118    pub fn new(
119        loader_key: &Pubkey,
120        loader: Arc<BuiltInProgram<InvokeContext<'static>>>,
121        deployment_slot: Slot,
122        elf_bytes: &[u8],
123        account_size: usize,
124        use_jit: bool,
125        metrics: &mut LoadProgramMetrics,
126    ) -> Result<Self, EbpfError> {
127        let mut load_elf_time = Measure::start("load_elf_time");
128        let executable = Executable::load(elf_bytes, loader.clone())?;
129        load_elf_time.stop();
130        metrics.load_elf_us = load_elf_time.as_us();
131
132        let mut verify_code_time = Measure::start("verify_code_time");
133
134        // Allowing mut here, since it may be needed for jit compile, which is under a config flag
135        #[allow(unused_mut)]
136        let mut program = if bpf_loader_deprecated::check_id(loader_key) {
137            LoadedProgramType::LegacyV0(VerifiedExecutable::from_executable(executable)?)
138        } else if bpf_loader::check_id(loader_key) || bpf_loader_upgradeable::check_id(loader_key) {
139            LoadedProgramType::LegacyV1(VerifiedExecutable::from_executable(executable)?)
140        } else {
141            panic!();
142        };
143        verify_code_time.stop();
144        metrics.verify_code_us = verify_code_time.as_us();
145
146        if use_jit {
147            #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))]
148            {
149                let mut jit_compile_time = Measure::start("jit_compile_time");
150                match &mut program {
151                    LoadedProgramType::LegacyV0(executable) => executable.jit_compile(),
152                    LoadedProgramType::LegacyV1(executable) => executable.jit_compile(),
153                    _ => Err(EbpfError::JitNotCompiled),
154                }?;
155                jit_compile_time.stop();
156                metrics.jit_compile_us = jit_compile_time.as_us();
157            }
158        }
159
160        Ok(Self {
161            deployment_slot,
162            account_size,
163            effective_slot: deployment_slot.saturating_add(1),
164            usage_counter: AtomicU64::new(0),
165            program,
166        })
167    }
168
169    /// Creates a new built-in program
170    pub fn new_built_in(
171        deployment_slot: Slot,
172        program: BuiltInProgram<InvokeContext<'static>>,
173    ) -> Self {
174        Self {
175            deployment_slot,
176            account_size: 0,
177            effective_slot: deployment_slot.saturating_add(1),
178            usage_counter: AtomicU64::new(0),
179            program: LoadedProgramType::BuiltIn(program),
180        }
181    }
182
183    pub fn new_tombstone() -> Self {
184        Self {
185            program: LoadedProgramType::Invalid,
186            account_size: 0,
187            deployment_slot: 0,
188            effective_slot: 0,
189            usage_counter: AtomicU64::default(),
190        }
191    }
192
193    pub fn is_tombstone(&self) -> bool {
194        matches!(self.program, LoadedProgramType::Invalid)
195    }
196}
197
198#[derive(Debug, Default)]
199pub struct LoadedPrograms {
200    /// A two level index:
201    ///
202    /// Pubkey is the address of a program, multiple versions can coexists simultaneously under the same address (in different slots).
203    entries: HashMap<Pubkey, Vec<Arc<LoadedProgram>>>,
204}
205
206#[cfg(RUSTC_WITH_SPECIALIZATION)]
207impl solana_frozen_abi::abi_example::AbiExample for LoadedPrograms {
208    fn example() -> Self {
209        // Delegate AbiExample impl to Default before going deep and stuck with
210        // not easily impl-able Arc<dyn Executor> due to rust's coherence issue
211        // This is safe because LoadedPrograms isn't serializable by definition.
212        Self::default()
213    }
214}
215
216pub enum LoadedProgramEntry {
217    WasOccupied(Arc<LoadedProgram>),
218    WasVacant(Arc<LoadedProgram>),
219}
220
221impl LoadedPrograms {
222    /// Inserts a single entry
223    pub fn insert_entry(&mut self, key: Pubkey, entry: LoadedProgram) -> LoadedProgramEntry {
224        let second_level = self.entries.entry(key).or_insert_with(Vec::new);
225        let index = second_level
226            .iter()
227            .position(|at| at.effective_slot >= entry.effective_slot);
228        if let Some(index) = index {
229            let existing = second_level
230                .get(index)
231                .expect("Missing entry, even though position was found");
232            if existing.deployment_slot == entry.deployment_slot
233                && existing.effective_slot == entry.effective_slot
234            {
235                return LoadedProgramEntry::WasOccupied(existing.clone());
236            }
237        }
238        let new_entry = Arc::new(entry);
239        second_level.insert(index.unwrap_or(second_level.len()), new_entry.clone());
240        LoadedProgramEntry::WasVacant(new_entry)
241    }
242
243    /// Before rerooting the blockstore this removes all programs of orphan forks
244    pub fn prune<F: ForkGraph>(&mut self, fork_graph: &F, new_root: Slot) {
245        self.entries.retain(|_key, second_level| {
246            let mut first_ancestor = true;
247            *second_level = second_level
248                .iter()
249                .rev()
250                .filter(|entry| {
251                    let relation = fork_graph.relationship(entry.deployment_slot, new_root);
252                    if entry.deployment_slot >= new_root {
253                        matches!(relation, BlockRelation::Equal | BlockRelation::Descendant)
254                    } else if first_ancestor {
255                        first_ancestor = false;
256                        matches!(relation, BlockRelation::Ancestor)
257                    } else {
258                        false
259                    }
260                })
261                .cloned()
262                .collect();
263            second_level.reverse();
264            !second_level.is_empty()
265        });
266    }
267
268    /// Extracts a subset of the programs relevant to a transaction batch
269    /// and returns which program accounts the accounts DB needs to load.
270    pub fn extract<S: WorkingSlot>(
271        &self,
272        working_slot: &S,
273        keys: impl Iterator<Item = Pubkey>,
274    ) -> (HashMap<Pubkey, Arc<LoadedProgram>>, Vec<Pubkey>) {
275        let mut missing = Vec::new();
276        let found = keys
277            .filter_map(|key| {
278                if let Some(second_level) = self.entries.get(&key) {
279                    for entry in second_level.iter().rev() {
280                        if working_slot.current_slot() >= entry.effective_slot
281                            && working_slot.is_ancestor(entry.deployment_slot)
282                        {
283                            return Some((key, entry.clone()));
284                        }
285                    }
286                }
287                missing.push(key);
288                None
289            })
290            .collect();
291        (found, missing)
292    }
293
294    /// Evicts programs which were used infrequently
295    pub fn sort_and_evict(&mut self) {
296        // TODO: Sort programs by their usage_counter
297        // TODO: Truncate the end of the list
298    }
299
300    /// Removes the entries at the given keys, if they exist
301    pub fn remove_entries(&mut self, _key: impl Iterator<Item = Pubkey>) {
302        // TODO: Remove at primary index level
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use {
309        crate::loaded_programs::{
310            BlockRelation, ForkGraph, LoadedProgram, LoadedProgramEntry, LoadedProgramType,
311            LoadedPrograms, WorkingSlot,
312        },
313        solana_sdk::{clock::Slot, pubkey::Pubkey},
314        std::{
315            collections::HashMap,
316            ops::ControlFlow,
317            sync::{atomic::AtomicU64, Arc},
318        },
319    };
320
321    #[test]
322    fn test_tombstone() {
323        let tombstone = LoadedProgram::new_tombstone();
324        assert!(matches!(tombstone.program, LoadedProgramType::Invalid));
325        assert!(tombstone.is_tombstone());
326    }
327
328    struct TestForkGraph {
329        relation: BlockRelation,
330    }
331    impl ForkGraph for TestForkGraph {
332        fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation {
333            self.relation
334        }
335    }
336
337    #[test]
338    fn test_prune_empty() {
339        let mut cache = LoadedPrograms::default();
340        let fork_graph = TestForkGraph {
341            relation: BlockRelation::Unrelated,
342        };
343
344        cache.prune(&fork_graph, 0);
345        assert!(cache.entries.is_empty());
346
347        cache.prune(&fork_graph, 10);
348        assert!(cache.entries.is_empty());
349
350        let fork_graph = TestForkGraph {
351            relation: BlockRelation::Ancestor,
352        };
353
354        cache.prune(&fork_graph, 0);
355        assert!(cache.entries.is_empty());
356
357        cache.prune(&fork_graph, 10);
358        assert!(cache.entries.is_empty());
359
360        let fork_graph = TestForkGraph {
361            relation: BlockRelation::Descendant,
362        };
363
364        cache.prune(&fork_graph, 0);
365        assert!(cache.entries.is_empty());
366
367        cache.prune(&fork_graph, 10);
368        assert!(cache.entries.is_empty());
369
370        let fork_graph = TestForkGraph {
371            relation: BlockRelation::Unknown,
372        };
373
374        cache.prune(&fork_graph, 0);
375        assert!(cache.entries.is_empty());
376
377        cache.prune(&fork_graph, 10);
378        assert!(cache.entries.is_empty());
379    }
380
381    #[derive(Default)]
382    struct TestForkGraphSpecific {
383        forks: Vec<Vec<Slot>>,
384    }
385
386    impl TestForkGraphSpecific {
387        fn insert_fork(&mut self, fork: &[Slot]) {
388            let mut fork = fork.to_vec();
389            fork.sort();
390            self.forks.push(fork)
391        }
392    }
393
394    impl ForkGraph for TestForkGraphSpecific {
395        fn relationship(&self, a: Slot, b: Slot) -> BlockRelation {
396            match self.forks.iter().try_for_each(|fork| {
397                let relation = fork
398                    .iter()
399                    .position(|x| *x == a)
400                    .and_then(|a_pos| {
401                        fork.iter().position(|x| *x == b).and_then(|b_pos| {
402                            (a_pos == b_pos)
403                                .then_some(BlockRelation::Equal)
404                                .or_else(|| (a_pos < b_pos).then_some(BlockRelation::Ancestor))
405                                .or(Some(BlockRelation::Descendant))
406                        })
407                    })
408                    .unwrap_or(BlockRelation::Unrelated);
409
410                if relation != BlockRelation::Unrelated {
411                    return ControlFlow::Break(relation);
412                }
413
414                ControlFlow::Continue(())
415            }) {
416                ControlFlow::Break(relation) => relation,
417                _ => BlockRelation::Unrelated,
418            }
419        }
420    }
421
422    struct TestWorkingSlot {
423        slot: Slot,
424        fork: Vec<Slot>,
425        slot_pos: usize,
426    }
427
428    impl TestWorkingSlot {
429        fn new(slot: Slot, fork: &[Slot]) -> Self {
430            let mut fork = fork.to_vec();
431            fork.sort();
432            let slot_pos = fork
433                .iter()
434                .position(|current| *current == slot)
435                .expect("The fork didn't have the slot in it");
436            TestWorkingSlot {
437                slot,
438                fork,
439                slot_pos,
440            }
441        }
442
443        fn update_slot(&mut self, slot: Slot) {
444            self.slot = slot;
445            self.slot_pos = self
446                .fork
447                .iter()
448                .position(|current| *current == slot)
449                .expect("The fork didn't have the slot in it");
450        }
451    }
452
453    impl WorkingSlot for TestWorkingSlot {
454        fn current_slot(&self) -> Slot {
455            self.slot
456        }
457
458        fn is_ancestor(&self, other: Slot) -> bool {
459            self.fork
460                .iter()
461                .position(|current| *current == other)
462                .map(|other_pos| other_pos < self.slot_pos)
463                .unwrap_or(false)
464        }
465    }
466
467    fn new_test_loaded_program(deployment_slot: Slot, effective_slot: Slot) -> LoadedProgram {
468        LoadedProgram {
469            program: LoadedProgramType::Invalid,
470            account_size: 0,
471            deployment_slot,
472            effective_slot,
473            usage_counter: AtomicU64::default(),
474        }
475    }
476
477    fn match_slot(
478        table: &HashMap<Pubkey, Arc<LoadedProgram>>,
479        program: &Pubkey,
480        deployment_slot: Slot,
481    ) -> bool {
482        table
483            .get(program)
484            .map(|entry| entry.deployment_slot == deployment_slot)
485            .unwrap_or(false)
486    }
487
488    #[test]
489    fn test_fork_extract_and_prune() {
490        let mut cache = LoadedPrograms::default();
491
492        // Fork graph created for the test
493        //                   0
494        //                 /   \
495        //                10    5
496        //                |     |
497        //                20    11
498        //                |     | \
499        //                22   15  25
500        //                      |   |
501        //                     16  27
502        //                      |
503        //                     19
504        //                      |
505        //                     23
506
507        let mut fork_graph = TestForkGraphSpecific::default();
508        fork_graph.insert_fork(&[0, 10, 20, 22]);
509        fork_graph.insert_fork(&[0, 5, 11, 15, 16]);
510        fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
511
512        let program1 = Pubkey::new_unique();
513        assert!(matches!(
514            cache.insert_entry(program1, new_test_loaded_program(0, 1)),
515            LoadedProgramEntry::WasVacant(_)
516        ));
517        assert!(matches!(
518            cache.insert_entry(program1, new_test_loaded_program(10, 11)),
519            LoadedProgramEntry::WasVacant(_)
520        ));
521        assert!(matches!(
522            cache.insert_entry(program1, new_test_loaded_program(20, 21)),
523            LoadedProgramEntry::WasVacant(_)
524        ));
525
526        // Test: inserting duplicate entry return pre existing entry from the cache
527        assert!(matches!(
528            cache.insert_entry(program1, new_test_loaded_program(20, 21)),
529            LoadedProgramEntry::WasOccupied(_)
530        ));
531
532        let program2 = Pubkey::new_unique();
533        assert!(matches!(
534            cache.insert_entry(program2, new_test_loaded_program(5, 6)),
535            LoadedProgramEntry::WasVacant(_)
536        ));
537        assert!(matches!(
538            cache.insert_entry(program2, new_test_loaded_program(11, 12)),
539            LoadedProgramEntry::WasVacant(_)
540        ));
541
542        let program3 = Pubkey::new_unique();
543        assert!(matches!(
544            cache.insert_entry(program3, new_test_loaded_program(25, 26)),
545            LoadedProgramEntry::WasVacant(_)
546        ));
547
548        let program4 = Pubkey::new_unique();
549        assert!(matches!(
550            cache.insert_entry(program4, new_test_loaded_program(0, 1)),
551            LoadedProgramEntry::WasVacant(_)
552        ));
553        assert!(matches!(
554            cache.insert_entry(program4, new_test_loaded_program(5, 6)),
555            LoadedProgramEntry::WasVacant(_)
556        ));
557        // The following is a special case, where effective slot is 4 slots in the future
558        assert!(matches!(
559            cache.insert_entry(program4, new_test_loaded_program(15, 19)),
560            LoadedProgramEntry::WasVacant(_)
561        ));
562
563        // Current fork graph
564        //                   0
565        //                 /   \
566        //                10    5
567        //                |     |
568        //                20    11
569        //                |     | \
570        //                22   15  25
571        //                      |   |
572        //                     16  27
573        //                      |
574        //                     19
575        //                      |
576        //                     23
577
578        // Testing fork 0 - 10 - 12 - 22 with current slot at 22
579        let working_slot = TestWorkingSlot::new(22, &[0, 10, 20, 22]);
580        let (found, missing) = cache.extract(
581            &working_slot,
582            vec![program1, program2, program3, program4].into_iter(),
583        );
584
585        assert!(match_slot(&found, &program1, 20));
586        assert!(match_slot(&found, &program4, 0));
587
588        assert!(missing.contains(&program2));
589        assert!(missing.contains(&program3));
590
591        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 16
592        let mut working_slot = TestWorkingSlot::new(16, &[0, 5, 11, 15, 16, 19, 23]);
593        let (found, missing) = cache.extract(
594            &working_slot,
595            vec![program1, program2, program3, program4].into_iter(),
596        );
597
598        assert!(match_slot(&found, &program1, 0));
599        assert!(match_slot(&found, &program2, 11));
600
601        // The effective slot of program4 deployed in slot 15 is 19. So it should not be usable in slot 16.
602        assert!(match_slot(&found, &program4, 5));
603
604        assert!(missing.contains(&program3));
605
606        // Testing the same fork above, but current slot is now 19 (equal to effective slot of program4).
607        working_slot.update_slot(19);
608        let (found, missing) = cache.extract(
609            &working_slot,
610            vec![program1, program2, program3, program4].into_iter(),
611        );
612
613        assert!(match_slot(&found, &program1, 0));
614        assert!(match_slot(&found, &program2, 11));
615
616        // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 19.
617        assert!(match_slot(&found, &program4, 15));
618
619        assert!(missing.contains(&program3));
620
621        // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4).
622        working_slot.update_slot(23);
623        let (found, missing) = cache.extract(
624            &working_slot,
625            vec![program1, program2, program3, program4].into_iter(),
626        );
627
628        assert!(match_slot(&found, &program1, 0));
629        assert!(match_slot(&found, &program2, 11));
630
631        // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 23.
632        assert!(match_slot(&found, &program4, 15));
633
634        assert!(missing.contains(&program3));
635
636        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11
637        let working_slot = TestWorkingSlot::new(11, &[0, 5, 11, 15, 16]);
638        let (found, missing) = cache.extract(
639            &working_slot,
640            vec![program1, program2, program3, program4].into_iter(),
641        );
642
643        assert!(match_slot(&found, &program1, 0));
644        assert!(match_slot(&found, &program2, 5));
645        assert!(match_slot(&found, &program4, 5));
646
647        assert!(missing.contains(&program3));
648
649        cache.prune(&fork_graph, 5);
650
651        // Fork graph after pruning
652        //                   0
653        //                   |
654        //                   5
655        //                   |
656        //                   11
657        //                   | \
658        //                  15  25
659        //                   |   |
660        //                  16  27
661        //                   |
662        //                  19
663        //                   |
664        //                  23
665
666        // Testing fork 0 - 10 - 12 - 22 (which was pruned) with current slot at 22
667        let working_slot = TestWorkingSlot::new(22, &[0, 10, 20, 22]);
668        let (found, missing) = cache.extract(
669            &working_slot,
670            vec![program1, program2, program3, program4].into_iter(),
671        );
672
673        // Since the fork was pruned, we should not find the entry deployed at slot 20.
674        assert!(match_slot(&found, &program1, 0));
675        assert!(match_slot(&found, &program4, 0));
676
677        assert!(missing.contains(&program2));
678        assert!(missing.contains(&program3));
679
680        // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27
681        let working_slot = TestWorkingSlot::new(27, &[0, 5, 11, 25, 27]);
682        let (found, _missing) = cache.extract(
683            &working_slot,
684            vec![program1, program2, program3, program4].into_iter(),
685        );
686
687        assert!(match_slot(&found, &program1, 0));
688        assert!(match_slot(&found, &program2, 11));
689        assert!(match_slot(&found, &program3, 25));
690        assert!(match_slot(&found, &program4, 5));
691
692        cache.prune(&fork_graph, 15);
693
694        // Fork graph after pruning
695        //                  0
696        //                  |
697        //                  5
698        //                  |
699        //                  11
700        //                  |
701        //                  15
702        //                  |
703        //                  16
704        //                  |
705        //                  19
706        //                  |
707        //                  23
708
709        // Testing fork 0 - 5 - 11 - 25 - 27 (with root at 15, slot 25, 27 are pruned) with current slot at 27
710        let working_slot = TestWorkingSlot::new(27, &[0, 5, 11, 25, 27]);
711        let (found, missing) = cache.extract(
712            &working_slot,
713            vec![program1, program2, program3, program4].into_iter(),
714        );
715
716        assert!(match_slot(&found, &program1, 0));
717        assert!(match_slot(&found, &program2, 11));
718        assert!(match_slot(&found, &program4, 5));
719
720        // program3 was deployed on slot 25, which has been pruned
721        assert!(missing.contains(&program3));
722    }
723}