Skip to main content

solana_program_runtime/
loaded_programs.rs

1use {
2    crate::{
3        invoke_context::InvokeContext,
4        loading_task::LoadingTaskWaiter,
5        program_cache_entry::{
6            ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType, retention_score,
7        },
8        program_metrics::{EMA_SCALE, ProgramCacheStats},
9    },
10    log::error,
11    solana_clock::{Epoch, Slot},
12    solana_pubkey::Pubkey,
13    solana_sbpf::program::BuiltinProgram,
14    solana_svm_type_overrides::{
15        rand::{Rng, rng},
16        sync::{Arc, Mutex, RwLock, atomic::Ordering},
17        thread,
18    },
19    std::{
20        collections::{HashMap, hash_map::Entry},
21        sync::Weak,
22    },
23};
24
25#[repr(transparent)]
26#[derive(Clone, Debug)]
27pub struct ProgramRuntimeEnvironment(Arc<BuiltinProgram<InvokeContext<'static, 'static>>>);
28impl std::hash::Hash for ProgramRuntimeEnvironment {
29    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
30        Arc::<BuiltinProgram<InvokeContext<'static, 'static>>>::as_ptr(&self.0).hash(state);
31    }
32}
33impl PartialEq for ProgramRuntimeEnvironment {
34    fn eq(&self, other: &Self) -> bool {
35        Arc::ptr_eq(&self.0, &other.0)
36    }
37}
38impl Eq for ProgramRuntimeEnvironment {}
39impl std::ops::Deref for ProgramRuntimeEnvironment {
40    type Target = Arc<BuiltinProgram<InvokeContext<'static, 'static>>>;
41
42    fn deref(&self) -> &Self::Target {
43        &self.0
44    }
45}
46impl ProgramRuntimeEnvironment {
47    pub fn from(inner: BuiltinProgram<InvokeContext<'static, 'static>>) -> Self {
48        Self(Arc::new(inner))
49    }
50
51    pub const fn from_ref<'a>(
52        inner: &'a Arc<BuiltinProgram<InvokeContext<'static, 'static>>>,
53    ) -> &'a Self {
54        // Safety: This wrapper type is transparent and shares the same representation as the underlying type
55        unsafe { std::mem::transmute(inner) }
56    }
57}
58
59/// Paired execution and deployment environments.
60///
61/// Registered functions within each program runtime environment (syscalls)
62/// depend on per-epoch feature gate statuses. In most cases, the list of
63/// registered functions in the two environments will be the same. However,
64/// it's possible that the effective epoch of deployment could be in the
65/// *next epoch*.
66pub struct ProgramRuntimeEnvironments {
67    /// Environment compiled for the current epoch in which programs are
68    /// executing.
69    execution: ProgramRuntimeEnvironment,
70    /// Environment compiled for the epoch of the next slot at which a program
71    /// deployed in the current slot will execute.
72    deployment: ProgramRuntimeEnvironment,
73}
74
75impl ProgramRuntimeEnvironments {
76    /// Create a new ProgramRuntimeEnvironments from an `execution` and
77    /// `deployment` environment.
78    pub fn new(
79        execution: ProgramRuntimeEnvironment,
80        deployment: ProgramRuntimeEnvironment,
81    ) -> Self {
82        Self {
83            execution,
84            deployment,
85        }
86    }
87
88    /// Get the program runtime environment for execution.
89    pub fn get_env_for_execution(&self) -> &ProgramRuntimeEnvironment {
90        &self.execution
91    }
92
93    /// Get the program runtime environment for deployment.
94    pub fn get_env_for_deployment(&self) -> &ProgramRuntimeEnvironment {
95        &self.deployment
96    }
97
98    #[cfg(feature = "dev-context-only-utils")]
99    pub fn mock() -> Self {
100        Self {
101            execution: get_mock_program_runtime_environment(),
102            deployment: get_mock_program_runtime_environment(),
103        }
104    }
105}
106
107#[cfg(feature = "dev-context-only-utils")]
108pub fn get_mock_program_runtime_environment() -> ProgramRuntimeEnvironment {
109    static MOCK_ENVIRONMENT: std::sync::OnceLock<ProgramRuntimeEnvironment> =
110        std::sync::OnceLock::<ProgramRuntimeEnvironment>::new();
111    MOCK_ENVIRONMENT
112        .get_or_init(|| ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()))
113        .clone()
114}
115
116pub const MAX_LOADED_ENTRY_COUNT: usize = 1024;
117pub const MAX_TOMBSTONE_AGE_IN_SLOTS: u64 = 2250; // 15 Minutes at 400ms slot time
118
119/// A percentage, expected to be in the range `0..=100`.
120pub type Percent = u8;
121
122/// The given percentage of [`MAX_LOADED_ENTRY_COUNT`], as an entry count.
123/// Equivalent to the former `percentage` crate's
124/// `Percentage::from(percent).apply_to(MAX_LOADED_ENTRY_COUNT)`,
125/// i.e. floor(MAX_LOADED_ENTRY_COUNT * percent / 100).
126fn percent_of_max_entries(percent: Percent) -> usize {
127    debug_assert!(percent <= 100, "percent must be <= 100");
128    MAX_LOADED_ENTRY_COUNT.saturating_mul(percent as usize) / 100
129}
130
131/// Relationship between two fork IDs
132#[derive(Copy, Clone, Debug, PartialEq)]
133pub enum BlockRelation {
134    /// The slot is on the same fork and is an ancestor of the other slot
135    Ancestor,
136    /// The two slots are equal and are on the same fork
137    Equal,
138    /// The slot is on the same fork and is a descendant of the other slot
139    Descendant,
140    /// The slots are on two different forks and may have had a common ancestor at some point
141    Unrelated,
142    /// Either one or both of the slots are either older than the latest root, or are in future
143    Unknown,
144}
145
146/// Maps relationship between two slots.
147pub trait ForkGraph {
148    /// Returns the BlockRelation of A to B
149    fn relationship(&self, a: Slot, b: Slot) -> BlockRelation;
150}
151
152/// Globally manages the transition between environments at the epoch boundary
153#[derive(Debug, Default)]
154pub struct EpochBoundaryPreparation {
155    /// The epoch of the upcoming_environment
156    pub upcoming_epoch: Epoch,
157    /// Anticipated replacement for `environments` at the next epoch
158    ///
159    /// This is `None` during most of an epoch, and only `Some` around the boundaries (at the end and beginning of an epoch).
160    /// More precisely, it starts with the cache preparation phase a few hundred slots before the epoch boundary,
161    /// and it ends with the first rerooting after the epoch boundary.
162    pub upcoming_environment: Option<ProgramRuntimeEnvironment>,
163    /// List of loaded programs which should be recompiled before the next epoch (but don't have to).
164    pub programs_to_recompile: Vec<(Pubkey, Arc<ProgramCacheEntry>)>,
165}
166
167impl EpochBoundaryPreparation {
168    pub fn new(epoch: Epoch) -> Self {
169        Self {
170            upcoming_epoch: epoch,
171            upcoming_environment: None,
172            programs_to_recompile: Vec::default(),
173        }
174    }
175
176    /// Returns the upcoming environments depending on the given epoch
177    pub fn get_upcoming_environment_for_epoch(
178        &self,
179        epoch: Epoch,
180    ) -> Option<ProgramRuntimeEnvironment> {
181        if epoch == self.upcoming_epoch {
182            return self.upcoming_environment.clone();
183        }
184        None
185    }
186
187    /// Before rerooting the blockstore this concludes the epoch boundary preparation
188    pub fn reroot(&mut self, epoch: Epoch) -> Option<ProgramRuntimeEnvironment> {
189        if epoch == self.upcoming_epoch
190            && let Some(upcoming_environment) = self.upcoming_environment.take()
191        {
192            self.programs_to_recompile.clear();
193            return Some(upcoming_environment);
194        }
195
196        None
197    }
198}
199
200/// Input of ProgramCache::extract()
201#[derive(Clone, PartialEq, Debug)]
202pub struct ProgramToLoad<'a> {
203    /// The program address
204    pub program_id: &'a Pubkey,
205    /// The program loader
206    pub loader: ProgramCacheEntryOwner,
207    /// The slot the program was (re)deployed in, as reported by the program
208    /// account on the caller's own fork.
209    ///
210    /// For Loader V1/V2 and builtin programs, this is 0.
211    pub deployment_slot: Slot,
212    /// When the program account was last written to (might be after the deployment slot)
213    pub last_modification_slot: Slot,
214}
215
216#[derive(Debug)]
217pub(crate) enum IndexImplementation {
218    /// Fork-graph aware index implementation
219    V1 {
220        /// A two level index:
221        ///
222        /// - the first level is for the address at which programs are deployed
223        /// - the second level for the slot (and thus also fork), sorted by slot
224        ///   number from smallest to largest.
225        entries: HashMap<Pubkey, Vec<Arc<ProgramCacheEntry>>>,
226        /// The entries that are getting loaded and have not yet finished loading.
227        ///
228        /// The key is the program address, the value is a tuple of the slot in which the program is
229        /// being loaded and the thread ID doing the load.
230        ///
231        /// It is possible that multiple TX batches from different slots need different versions of a
232        /// program. The deployment slot of a program is only known after load tho,
233        /// so all loads for a given program key are serialized.
234        loading_entries: Mutex<HashMap<Pubkey, (Slot, thread::ThreadId)>>,
235    },
236}
237
238/// This structure is the global cache of loaded, verified and compiled programs.
239///
240/// It ...
241/// - is validator global and fork graph aware, so it can optimize the commonalities across banks.
242/// - handles the visibility rules of un/re/deployments.
243/// - stores the usage statistics and verification status of each program.
244/// - is elastic and uses a probabilistic eviction strategy based on the usage statistics.
245/// - also keeps the compiled executables around, but only for the most used programs.
246/// - supports various kinds of tombstones to avoid loading programs which can not be loaded.
247/// - cleans up entries on orphan branches when the block store is rerooted.
248/// - supports the cache preparation phase before feature activations which can change cached programs.
249/// - manages the environments of the programs and upcoming environments for the next epoch.
250/// - allows for cooperative loading of TX batches which hit the same missing programs simultaneously.
251/// - enforces that all programs used in a batch are eagerly loaded ahead of execution.
252/// - is not persisted to disk or a snapshot, so it needs to cold start and warm up first.
253pub struct ProgramCache<FG: ForkGraph> {
254    /// Index of the cached entries and cooperative loading tasks
255    pub(crate) index: IndexImplementation,
256    /// The slot of the last rerooting
257    pub latest_root_slot: Slot,
258    /// Statistics counters
259    pub stats: ProgramCacheStats,
260    /// Reference to the block store
261    pub fork_graph: Option<Weak<RwLock<FG>>>,
262    /// Coordinates TX batches waiting for others to complete their task during cooperative loading
263    pub loading_task_waiter: Arc<LoadingTaskWaiter>,
264}
265
266impl<FG: ForkGraph> std::fmt::Debug for ProgramCache<FG> {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        f.debug_struct("ProgramCache")
269            .field("root slot", &self.latest_root_slot)
270            .field("stats", &self.stats)
271            .field("index", &self.index)
272            .finish()
273    }
274}
275
276/// Local view into [ProgramCache] which was extracted for a specific TX batch.
277///
278/// This isolation enables the global [ProgramCache] to continue to evolve (e.g. evictions),
279/// while the TX batch is guaranteed it will continue to find all the programs it requires.
280/// For program management instructions this also buffers them before they are merged back into the global [ProgramCache].
281#[derive(Clone, Debug, Default)]
282pub struct ProgramCacheForTxBatch {
283    /// Pubkey is the address of a program.
284    /// ProgramCacheEntry is the corresponding program entry valid for the slot in which a transaction is being executed.
285    entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
286    /// Program entries modified during the transaction batch.
287    modified_entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
288    slot: Slot,
289    pub hit_max_limit: bool,
290    pub loaded_missing: bool,
291    pub merged_modified: bool,
292}
293
294impl ProgramCacheForTxBatch {
295    pub fn new(slot: Slot) -> Self {
296        Self {
297            entries: HashMap::new(),
298            modified_entries: HashMap::new(),
299            slot,
300            hit_max_limit: false,
301            loaded_missing: false,
302            merged_modified: false,
303        }
304    }
305
306    /// Refill the cache with a single entry. It's typically called during transaction loading, and
307    /// transaction processing (for program management instructions).
308    /// It replaces the existing entry (if any) with the provided entry. The return value contains
309    /// `true` if an entry existed.
310    /// The function also returns the newly inserted value.
311    pub fn replenish(
312        &mut self,
313        key: Pubkey,
314        entry: Arc<ProgramCacheEntry>,
315    ) -> (bool, Arc<ProgramCacheEntry>) {
316        (self.entries.insert(key, entry.clone()).is_some(), entry)
317    }
318
319    /// Store an entry in `modified_entries` for a program modified during the
320    /// transaction batch.
321    pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc<ProgramCacheEntry>) {
322        self.modified_entries.insert(key, entry);
323    }
324
325    /// Drain the program cache's modified entries, returning the owned
326    /// collection.
327    pub fn drain_modified_entries(&mut self) -> HashMap<Pubkey, Arc<ProgramCacheEntry>> {
328        std::mem::take(&mut self.modified_entries)
329    }
330
331    pub fn find(&self, key: &Pubkey) -> Option<Arc<ProgramCacheEntry>> {
332        // First lookup the cache of the programs modified by the current
333        // transaction. If not found, lookup the cache of the cache of the
334        // programs that are loaded for the transaction batch.
335        self.modified_entries
336            .get(key)
337            .or_else(|| self.entries.get(key))
338            .map(|entry| {
339                if entry.is_implicit_delay_visibility_tombstone(self.slot) {
340                    // Found a program entry on the current fork, but it's not effective
341                    // yet. It indicates that the program has delayed visibility. Return
342                    // the tombstone to reflect that.
343                    Arc::new(ProgramCacheEntry::new_delay_visibility_tombstone(
344                        entry.deployment_slot,
345                        entry.account_owner,
346                        Arc::clone(&entry.stats),
347                    ))
348                } else {
349                    entry.clone()
350                }
351            })
352    }
353
354    pub fn slot(&self) -> Slot {
355        self.slot
356    }
357
358    pub fn set_slot_for_tests(&mut self, slot: Slot) {
359        self.slot = slot;
360    }
361
362    pub fn merge(&mut self, modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>) {
363        modified_entries.iter().for_each(|(key, entry)| {
364            self.merged_modified = true;
365            self.replenish(*key, entry.clone());
366        })
367    }
368
369    /// Remove an entry from the `entries` list.
370    /// Note: DOES NOT remove modified entries!
371    pub fn remove_entry(&mut self, key: &Pubkey) {
372        self.entries.remove(key);
373    }
374
375    pub fn is_empty(&self) -> bool {
376        self.entries.is_empty()
377    }
378}
379
380impl<FG: ForkGraph> ProgramCache<FG> {
381    pub fn new(root_slot: Slot) -> Self {
382        Self {
383            index: IndexImplementation::V1 {
384                entries: HashMap::new(),
385                loading_entries: Mutex::new(HashMap::new()),
386            },
387            latest_root_slot: root_slot,
388            stats: ProgramCacheStats::default(),
389            fork_graph: None,
390            loading_task_waiter: Arc::new(LoadingTaskWaiter::default()),
391        }
392    }
393
394    pub fn set_fork_graph(&mut self, fork_graph: Weak<RwLock<FG>>) {
395        self.fork_graph = Some(fork_graph);
396    }
397
398    /// Insert a single entry. It's typically called during transaction loading,
399    /// when the cache doesn't contain the entry corresponding to program `key`.
400    pub fn assign_program(
401        &mut self,
402        program_runtime_environment: &ProgramRuntimeEnvironment,
403        key: Pubkey,
404        _last_modification_slot: Slot,
405        entry: Arc<ProgramCacheEntry>,
406    ) -> bool {
407        debug_assert!(
408            !matches!(&entry.program, ProgramCacheEntryType::DelayVisibility),
409            "Unexpected assignment of a DelayVisibility tombstone"
410        );
411        // This function always returns `true` during normal operation.
412        // Only during the cache preparation phase this can return `false`
413        // for entries with `upcoming_environment`.
414        fn is_current_env(
415            program_runtime_environment: &ProgramRuntimeEnvironment,
416            env_opt: Option<&ProgramRuntimeEnvironment>,
417        ) -> bool {
418            env_opt
419                .map(|env| env == program_runtime_environment)
420                .unwrap_or(true)
421        }
422        match &mut self.index {
423            IndexImplementation::V1 { entries, .. } => {
424                let slot_versions = &mut entries.entry(key).or_default();
425                let insertion_point = slot_versions.binary_search_by(|at| {
426                    at.deployment_slot
427                        .cmp(&entry.deployment_slot)
428                        .then(at.account_owner.cmp(&entry.account_owner))
429                        .then(
430                            // This `.then()` has no effect during normal operation.
431                            // Only during the cache preparation phase this does allow entries
432                            // which only differ in their environment to be interleaved in `slot_versions`.
433                            is_current_env(
434                                program_runtime_environment,
435                                at.program.get_environment(),
436                            )
437                            .cmp(&is_current_env(
438                                program_runtime_environment,
439                                entry.program.get_environment(),
440                            )),
441                        )
442                });
443                match insertion_point {
444                    Ok(index) => {
445                        let existing = slot_versions.get_mut(index).unwrap();
446                        match (&existing.program, &entry.program) {
447                            (
448                                ProgramCacheEntryType::Builtin(_),
449                                ProgramCacheEntryType::Builtin(_),
450                            )
451                            | (ProgramCacheEntryType::Closed, ProgramCacheEntryType::Unloaded(_))
452                            | (
453                                ProgramCacheEntryType::Unloaded(_),
454                                ProgramCacheEntryType::Loaded(_),
455                            )
456                            | (
457                                ProgramCacheEntryType::Unloaded(_),
458                                ProgramCacheEntryType::FailedVerification(_),
459                            ) => {}
460                            _ => {
461                                // Something is wrong, I can feel it ...
462                                error!(
463                                    "ProgramCache::assign_program() failed key={key:?} \
464                                     existing={slot_versions:?} entry={entry:?}"
465                                );
466                                debug_assert!(false, "Unexpected replacement of an entry");
467                                self.stats.replacements.fetch_add(1, Ordering::Relaxed);
468                                return true;
469                            }
470                        }
471                        entry.stats.merge_from(&existing.stats);
472                        *existing = Arc::clone(&entry);
473                        self.stats.reloads.fetch_add(1, Ordering::Relaxed);
474                    }
475                    Err(index) => {
476                        self.stats.insertions.fetch_add(1, Ordering::Relaxed);
477                        slot_versions.insert(index, Arc::clone(&entry));
478                    }
479                }
480                // Remove existing entries in the same deployment slot unless they are for a different
481                // environment.
482                // This overwrites the current status of a program in program management instructions.
483                slot_versions.retain(|existing| {
484                    existing.deployment_slot != entry.deployment_slot
485                        || existing
486                            .program
487                            .get_environment()
488                            .zip(entry.program.get_environment())
489                            .map(|(a, b)| a != b)
490                            .unwrap_or(false)
491                        || Arc::ptr_eq(existing, &entry)
492                });
493            }
494        }
495        false
496    }
497
498    pub fn prune_by_deployment_slot(&mut self, slot: Slot) {
499        match &mut self.index {
500            IndexImplementation::V1 { entries, .. } => {
501                for second_level in entries.values_mut() {
502                    second_level.retain(|entry| entry.deployment_slot != slot);
503                }
504                self.remove_programs_with_no_entries();
505            }
506        }
507    }
508
509    /// Before rerooting the blockstore this removes all superfluous entries
510    pub fn prune(
511        &mut self,
512        new_root_slot: Slot,
513        new_environment: Option<ProgramRuntimeEnvironment>,
514        fork_graph: &FG,
515    ) {
516        match &mut self.index {
517            IndexImplementation::V1 { entries, .. } => {
518                let tombstone_slot_cutoff =
519                    new_root_slot.saturating_sub(MAX_TOMBSTONE_AGE_IN_SLOTS);
520                entries.retain(|_id, second_level| {
521                    // Clean up tombstones and unloaded entries
522                    if let [candidate] = &second_level[..]
523                        && (matches!(candidate.program, ProgramCacheEntryType::Unloaded(_))
524                            || candidate.is_tombstone())
525                        && candidate.deployment_slot <= self.latest_root_slot
526                        && candidate.latest_access_slot.load(Ordering::Relaxed)
527                            < tombstone_slot_cutoff
528                    {
529                        self.stats.prunes_stale.fetch_add(1, Ordering::Relaxed);
530                        return false;
531                    }
532                    // Remove entries un/re/deployed on orphan forks
533                    let mut first_ancestor_found = false;
534                    let mut first_ancestor_env = None;
535                    *second_level = second_level
536                        .iter()
537                        .rev()
538                        .filter(|entry| {
539                            let relation =
540                                fork_graph.relationship(entry.deployment_slot, new_root_slot);
541                            if entry.deployment_slot >= new_root_slot {
542                                matches!(relation, BlockRelation::Equal | BlockRelation::Descendant)
543                            } else if matches!(relation, BlockRelation::Ancestor)
544                                || entry.deployment_slot <= self.latest_root_slot
545                            {
546                                if !first_ancestor_found {
547                                    first_ancestor_found = true;
548                                    first_ancestor_env = entry.program.get_environment();
549                                    return true;
550                                }
551                                // Do not prune the entry if the runtime environment of the entry is
552                                // different than the entry that was previously found (stored in
553                                // first_ancestor_env). Different environment indicates that this entry
554                                // might belong to an older epoch that had a different environment (e.g.
555                                // different feature set). Once the root moves to the new/current epoch,
556                                // the entry will get pruned. But, until then the entry might still be
557                                // getting used by an older slot.
558                                if let Some(entry_env) = entry.program.get_environment()
559                                    && let Some(env) = first_ancestor_env
560                                    && entry_env != env
561                                {
562                                    return true;
563                                }
564                                self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed);
565                                false
566                            } else {
567                                self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed);
568                                false
569                            }
570                        })
571                        .filter(|entry| {
572                            // Remove outdated environment of previous feature set
573                            if let Some(new_environment) = new_environment.as_ref()
574                                && !Self::matches_environment(entry, new_environment)
575                            {
576                                self.stats
577                                    .prunes_environment
578                                    .fetch_add(1, Ordering::Relaxed);
579                                return false;
580                            }
581                            true
582                        })
583                        .cloned()
584                        .collect();
585                    second_level.reverse();
586                    true
587                });
588            }
589        }
590        self.remove_programs_with_no_entries();
591        debug_assert!(self.latest_root_slot <= new_root_slot);
592        self.latest_root_slot = new_root_slot;
593    }
594
595    fn matches_environment(
596        entry: &Arc<ProgramCacheEntry>,
597        program_runtime_environment: &ProgramRuntimeEnvironment,
598    ) -> bool {
599        let Some(environment) = entry.program.get_environment() else {
600            return true;
601        };
602        environment == program_runtime_environment
603    }
604
605    /// Extracts a subset of the programs relevant to a transaction batch
606    /// and returns which program accounts the accounts DB needs to load.
607    pub fn extract(
608        &self,
609        search_for: &mut Vec<ProgramToLoad>,
610        loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch,
611        program_runtime_environment_for_execution: &ProgramRuntimeEnvironment,
612        increment_usage_counter: bool,
613        count_hits_and_misses: bool,
614    ) -> Option<Pubkey> {
615        debug_assert!(self.fork_graph.is_some());
616        let fork_graph = self.fork_graph.as_ref().unwrap().upgrade().unwrap();
617        let locked_fork_graph = fork_graph.read().unwrap();
618        let entries_in_batch = loaded_programs_for_tx_batch.entries.len();
619        let mut cooperative_loading_task = None;
620        match &self.index {
621            IndexImplementation::V1 {
622                entries,
623                loading_entries,
624            } => {
625                search_for.retain(|program_to_load| {
626                    if let Some(second_level) = entries.get(program_to_load.program_id) {
627                        for entry in second_level.iter().rev() {
628                            // The entry must have been deployed in the slot reported by
629                            // the caller's own program account, and by the same loader.
630                            if program_to_load.deployment_slot != entry.deployment_slot
631                                || program_to_load.loader != entry.account_owner
632                            {
633                                continue;
634                            }
635
636                            // At this point we're sitting on an entry with a matching
637                            // deployment slot and owner.
638                            //
639                            // Fork-graph analysis below this is now redundant, and it
640                            // can be removed in follow-up.
641                            let entry_in_same_branch = entry.deployment_slot
642                                <= self.latest_root_slot
643                                || matches!(
644                                    locked_fork_graph.relationship(
645                                        entry.deployment_slot,
646                                        loaded_programs_for_tx_batch.slot
647                                    ),
648                                    BlockRelation::Equal | BlockRelation::Ancestor
649                                );
650                            if entry_in_same_branch {
651                                let entry_is_effective =
652                                    loaded_programs_for_tx_batch.slot >= entry.effective_slot();
653                                let entry_to_return = if entry_is_effective {
654                                    if !Self::matches_environment(
655                                        entry,
656                                        program_runtime_environment_for_execution,
657                                    ) {
658                                        // We found an entry that would work, had its environment
659                                        // matched the one we're planning to use for this slot. A
660                                        // sibling compiled against that environment may follow.
661                                        continue;
662                                    }
663                                    if let ProgramCacheEntryType::Unloaded(_environment) =
664                                        &entry.program
665                                    {
666                                        break;
667                                    }
668                                    entry.clone()
669                                } else if entry.is_implicit_delay_visibility_tombstone(
670                                    loaded_programs_for_tx_batch.slot,
671                                ) {
672                                    // Found a program entry on the current fork, but it's not effective
673                                    // yet. It indicates that the program has delayed visibility. Return
674                                    // the tombstone to reflect that.
675                                    Arc::new(ProgramCacheEntry::new_delay_visibility_tombstone(
676                                        entry.deployment_slot,
677                                        entry.account_owner,
678                                        Arc::clone(&entry.stats),
679                                    ))
680                                } else {
681                                    continue;
682                                };
683                                entry.update_access_slot(loaded_programs_for_tx_batch.slot);
684                                if increment_usage_counter {
685                                    entry_to_return.stats.uses.fetch_add(1, Ordering::Relaxed);
686                                }
687                                loaded_programs_for_tx_batch
688                                    .entries
689                                    .insert(*program_to_load.program_id, entry_to_return);
690                                return false;
691                            }
692                        }
693                    }
694                    if cooperative_loading_task.is_none() {
695                        let mut loading_entries = loading_entries.lock().unwrap();
696                        let entry = loading_entries.entry(*program_to_load.program_id);
697                        if let Entry::Vacant(entry) = entry {
698                            entry.insert((
699                                loaded_programs_for_tx_batch.slot,
700                                thread::current().id(),
701                            ));
702                            cooperative_loading_task = Some(*program_to_load.program_id);
703                        }
704                    }
705                    true
706                });
707            }
708        }
709        drop(locked_fork_graph);
710        if count_hits_and_misses {
711            let misses = search_for.len() as u64;
712            let hits = loaded_programs_for_tx_batch
713                .entries
714                .len()
715                .saturating_sub(entries_in_batch) as u64;
716            self.stats.misses.fetch_add(misses, Ordering::Relaxed);
717            self.stats.hits.fetch_add(hits, Ordering::Relaxed);
718        }
719        cooperative_loading_task
720    }
721
722    /// Called by Bank::replenish_program_cache() for each program that is done loading.
723    pub fn finish_cooperative_loading_task(
724        &mut self,
725        program_runtime_environment: &ProgramRuntimeEnvironment,
726        current_slot: Slot,
727        key: Pubkey,
728        last_modification_slot: Slot,
729        loaded_program: Arc<ProgramCacheEntry>,
730    ) -> bool {
731        match &mut self.index {
732            IndexImplementation::V1 {
733                loading_entries, ..
734            } => {
735                let loading_thread = loading_entries.get_mut().unwrap().remove(&key);
736                debug_assert_eq!(loading_thread, Some((current_slot, thread::current().id())));
737                // Check that it will be visible to our own fork once inserted
738                if loaded_program.deployment_slot > self.latest_root_slot
739                    && !matches!(
740                        self.fork_graph
741                            .as_ref()
742                            .unwrap()
743                            .upgrade()
744                            .unwrap()
745                            .read()
746                            .unwrap()
747                            .relationship(loaded_program.deployment_slot, current_slot),
748                        BlockRelation::Equal | BlockRelation::Ancestor
749                    )
750                {
751                    self.stats.lost_insertions.fetch_add(1, Ordering::Relaxed);
752                }
753                let was_occupied = self.assign_program(
754                    program_runtime_environment,
755                    key,
756                    last_modification_slot,
757                    loaded_program,
758                );
759                self.loading_task_waiter.notify();
760                was_occupied
761            }
762        }
763    }
764
765    pub fn merge(
766        &mut self,
767        program_runtime_environment: &ProgramRuntimeEnvironment,
768        current_slot: Slot,
769        modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>,
770    ) {
771        modified_entries.iter().for_each(|(key, entry)| {
772            self.assign_program(
773                program_runtime_environment,
774                *key,
775                current_slot,
776                entry.clone(),
777            );
778        })
779    }
780
781    /// Returns the list of entries which are verified and compiled.
782    pub fn get_flattened_entries(&self) -> Vec<(Pubkey, Slot, Arc<ProgramCacheEntry>)> {
783        match &self.index {
784            IndexImplementation::V1 { entries, .. } => entries
785                .iter()
786                .flat_map(|(id, second_level)| {
787                    second_level
788                        .iter()
789                        .filter_map(move |program| match program.program {
790                            ProgramCacheEntryType::Loaded(_) => Some((*id, 0, program.clone())),
791                            _ => None,
792                        })
793                })
794                .collect(),
795        }
796    }
797
798    /// Returns the list of all entries in the cache.
799    #[cfg(feature = "dev-context-only-utils")]
800    pub fn get_flattened_entries_for_tests(&self) -> Vec<(Pubkey, Arc<ProgramCacheEntry>)> {
801        match &self.index {
802            IndexImplementation::V1 { entries, .. } => entries
803                .iter()
804                .flat_map(|(id, second_level)| {
805                    second_level.iter().map(|program| (*id, program.clone()))
806                })
807                .collect(),
808        }
809    }
810
811    /// Returns the slot versions for the given program id.
812    pub fn get_slot_versions_for_tests(&self, key: &Pubkey) -> &[Arc<ProgramCacheEntry>] {
813        match &self.index {
814            IndexImplementation::V1 { entries, .. } => entries
815                .get(key)
816                .map(|second_level| second_level.as_ref())
817                .unwrap_or(&[]),
818        }
819    }
820
821    /// Unloads programs which were used infrequently
822    pub fn sort_and_unload(&mut self, shrink_to_percent: Percent) {
823        let mut sorted_candidates = self.get_flattened_entries();
824        sorted_candidates.sort_by_cached_key(|(_id, _last_modification_slot, program)| {
825            program.stats.uses.load(Ordering::Relaxed)
826        });
827        let num_to_unload = sorted_candidates
828            .len()
829            .saturating_sub(percent_of_max_entries(shrink_to_percent));
830        for (program, last_modification_slot, entry) in sorted_candidates.iter().take(num_to_unload)
831        {
832            self.unload_program_entry(*program, *last_modification_slot, entry);
833        }
834    }
835
836    /// Evicts programs using random selection, choosing the worst scoring program out of the
837    /// entries sampled.
838    ///
839    /// The eviction is performed enough number of times to reduce the cache usage to the given
840    /// percentage.
841    pub fn evict_using_random_selection(&mut self, shrink_to_percent: Percent, now: Slot) {
842        let mut candidates = self.get_flattened_entries();
843        let mut rng = rng();
844        self.stats
845            .water_level
846            .store(candidates.len() as u64, Ordering::Relaxed);
847        let num_to_unload = candidates
848            .len()
849            .saturating_sub(percent_of_max_entries(shrink_to_percent));
850        let mut sample_entry = |candidates: &Vec<(Pubkey, u64, Arc<ProgramCacheEntry>)>| {
851            // gen_range is deprecated in favor of random_range in rand>=0.9, but we also get
852            // rnd() from shuttle, which doesn't yet support rand 0.9 APIs
853            #[cfg(feature = "shuttle-test")]
854            let index = rng.gen_range(0..candidates.len());
855            #[cfg(not(feature = "shuttle-test"))]
856            let index = rng.random_range(0..candidates.len());
857            let usage_counter = candidates
858                .get(index)
859                .expect("Failed to get cached entry")
860                .2
861                .retention_score();
862            (index, usage_counter)
863        };
864
865        // Random sampling with just 2 choices can frequently lead to a situation where both
866        // entries chosen have relatively high retention scores, having us to pick one out of two
867        // poor options. We can tell what a relatively high retention score is, so we can make a
868        // few additional samples until we hit some other entry that isn't as highly scoring.
869        //
870        // Note that the "high enough" compilation time and use count numbers used here are
871        // relatively arbitrary.
872        const MAX_ADDITIONAL_SAMPLES: usize = 3;
873        let avoid_evicting_above_score = retention_score(now, 500 * EMA_SCALE, 500);
874        for _ in 0..num_to_unload {
875            let (mut index, mut score) = sample_entry(&candidates);
876            for _ in 0..MAX_ADDITIONAL_SAMPLES {
877                let (sample_index, sample_score) = sample_entry(&candidates);
878                if score > sample_score {
879                    index = sample_index;
880                    score = sample_score;
881                }
882                if score < avoid_evicting_above_score {
883                    break;
884                }
885            }
886            let (id, last_modification_slot, entry) = candidates.swap_remove(index);
887            self.unload_program_entry(id, last_modification_slot, &entry);
888        }
889    }
890
891    /// Removes all the entries at the given keys, if they exist
892    pub fn remove_programs(&mut self, keys: impl Iterator<Item = Pubkey>) {
893        match &mut self.index {
894            IndexImplementation::V1 { entries, .. } => {
895                for k in keys {
896                    entries.remove(&k);
897                }
898            }
899        }
900    }
901
902    /// This function removes the given entry for the given program from the cache.
903    /// The function expects that the program and entry exists in the cache. Otherwise it'll panic.
904    fn unload_program_entry(
905        &mut self,
906        id: Pubkey,
907        _last_modification_slot: Slot,
908        remove_entry: &Arc<ProgramCacheEntry>,
909    ) {
910        match &mut self.index {
911            IndexImplementation::V1 { entries, .. } => {
912                let second_level = entries.get_mut(&id).expect("Cache lookup failed");
913                let candidate = second_level
914                    .iter_mut()
915                    .find(|entry| Arc::ptr_eq(entry, remove_entry))
916                    .expect("Program entry not found");
917
918                // Only loaded entries shall be unloaded by eviction.
919                if let ProgramCacheEntryType::Loaded(_) = candidate.program
920                    && let Some(unloaded) = candidate.to_unloaded()
921                {
922                    if candidate.stats.uses.load(Ordering::Relaxed) == 1 {
923                        self.stats.one_hit_wonders.fetch_add(1, Ordering::Relaxed);
924                    }
925                    self.stats
926                        .evictions
927                        .entry(id)
928                        .and_modify(|c| *c = c.saturating_add(1))
929                        .or_insert(1);
930                    *candidate = Arc::new(unloaded);
931                }
932            }
933        }
934    }
935
936    fn remove_programs_with_no_entries(&mut self) {
937        match &mut self.index {
938            IndexImplementation::V1 { entries, .. } => {
939                let num_programs_before_removal = entries.len();
940                entries.retain(|_key, second_level| !second_level.is_empty());
941                if entries.len() < num_programs_before_removal {
942                    self.stats.empty_entries.fetch_add(
943                        num_programs_before_removal.saturating_sub(entries.len()) as u64,
944                        Ordering::Relaxed,
945                    );
946                }
947            }
948        }
949    }
950}
951
952#[cfg(feature = "frozen-abi")]
953impl solana_frozen_abi::abi_example::AbiExample for ProgramCacheEntry {
954    fn example() -> Self {
955        // ProgramCacheEntry isn't serializable by definition.
956        Self::default()
957    }
958}
959
960#[cfg(feature = "frozen-abi")]
961impl<FG: ForkGraph> solana_frozen_abi::abi_example::AbiExample for ProgramCache<FG> {
962    fn example() -> Self {
963        // ProgramCache isn't serializable by definition.
964        Self::new(Slot::default())
965    }
966}
967
968#[cfg(test)]
969pub(crate) mod tests {
970    use {
971        crate::{
972            loaded_programs::{
973                BlockRelation, ForkGraph, IndexImplementation, MAX_TOMBSTONE_AGE_IN_SLOTS, Percent,
974                ProgramCache, ProgramCacheForTxBatch, ProgramRuntimeEnvironment, ProgramToLoad,
975                get_mock_program_runtime_environment,
976            },
977            program_cache_entry::{
978                ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType,
979            },
980            program_metrics::ProgramStatistics,
981        },
982        assert_matches::assert_matches,
983        solana_clock::Slot,
984        solana_pubkey::Pubkey,
985        solana_sbpf::{elf::Executable, program::BuiltinProgram},
986        solana_svm_type_overrides::{
987            sync::{
988                Arc, RwLock,
989                atomic::{AtomicU64, Ordering},
990            },
991            thread,
992        },
993        std::{fs::File, io::Read, ops::ControlFlow},
994        test_case::{test_case, test_matrix},
995    };
996
997    fn new_test_entry(deployment_slot: Slot) -> Arc<ProgramCacheEntry> {
998        new_test_entry_with_usage(deployment_slot, ProgramStatistics::default())
999    }
1000
1001    fn new_unloaded_entry(env: ProgramRuntimeEnvironment) -> ProgramCacheEntryType {
1002        ProgramCacheEntryType::Unloaded(env)
1003    }
1004
1005    fn new_loaded_entry(env: ProgramRuntimeEnvironment) -> ProgramCacheEntryType {
1006        let mut elf = Vec::new();
1007        File::open("../programs/bpf_loader/test_elfs/out/noop_aligned.so")
1008            .unwrap()
1009            .read_to_end(&mut elf)
1010            .unwrap();
1011        let executable = Executable::load(&elf, Arc::clone(&*env)).unwrap();
1012        ProgramCacheEntryType::Loaded(executable)
1013    }
1014
1015    fn new_test_entry_with_owner(
1016        deployment_slot: Slot,
1017        account_owner: ProgramCacheEntryOwner,
1018        program: ProgramCacheEntryType,
1019    ) -> Arc<ProgramCacheEntry> {
1020        Arc::new(ProgramCacheEntry {
1021            program,
1022            account_owner,
1023            deployment_slot,
1024            stats: Arc::default(),
1025            latest_access_slot: AtomicU64::default(),
1026        })
1027    }
1028
1029    fn new_test_cache_with_fork_graph(
1030        relation: BlockRelation,
1031    ) -> (ProgramCache<TestForkGraph>, Arc<RwLock<TestForkGraph>>) {
1032        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1033        let fork_graph = Arc::new(RwLock::new(TestForkGraph { relation }));
1034        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1035        (cache, fork_graph)
1036    }
1037
1038    pub(crate) fn new_test_entry_with_usage(
1039        deployment_slot: Slot,
1040        stats: ProgramStatistics,
1041    ) -> Arc<ProgramCacheEntry> {
1042        Arc::new(ProgramCacheEntry {
1043            program: new_loaded_entry(get_mock_program_runtime_environment()),
1044            account_owner: ProgramCacheEntryOwner::LoaderV2,
1045            deployment_slot,
1046            stats: Arc::new(stats),
1047            latest_access_slot: AtomicU64::new(deployment_slot),
1048        })
1049    }
1050
1051    fn new_test_builtin_entry(deployment_slot: Slot) -> Arc<ProgramCacheEntry> {
1052        Arc::new(ProgramCacheEntry {
1053            program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1054            account_owner: ProgramCacheEntryOwner::NativeLoader,
1055            deployment_slot,
1056            stats: Arc::default(),
1057            latest_access_slot: AtomicU64::default(),
1058        })
1059    }
1060
1061    fn set_failed_verification_tombstone<FG: ForkGraph>(
1062        cache: &mut ProgramCache<FG>,
1063        key: Pubkey,
1064        current_slot: Slot,
1065        env: ProgramRuntimeEnvironment,
1066    ) -> Arc<ProgramCacheEntry> {
1067        let program = Arc::new(ProgramCacheEntry::new_failed_verification_tombstone(
1068            current_slot,
1069            ProgramCacheEntryOwner::LoaderV2,
1070            ProgramRuntimeEnvironment::clone(&env),
1071        ));
1072        cache.assign_program(&env, key, current_slot, program.clone());
1073        program
1074    }
1075
1076    fn insert_unloaded_entry<FG: ForkGraph>(
1077        cache: &mut ProgramCache<FG>,
1078        key: Pubkey,
1079        current_slot: Slot,
1080    ) -> Arc<ProgramCacheEntry> {
1081        let env = get_mock_program_runtime_environment();
1082        let loaded = new_test_entry_with_usage(current_slot, ProgramStatistics::default());
1083        let unloaded = Arc::new(loaded.to_unloaded().expect("Failed to unload the program"));
1084        cache.assign_program(&env, key, current_slot, unloaded.clone());
1085        unloaded
1086    }
1087
1088    fn num_matching_entries<P, FG>(cache: &ProgramCache<FG>, predicate: P) -> usize
1089    where
1090        P: Fn(&ProgramCacheEntryType) -> bool,
1091        FG: ForkGraph,
1092    {
1093        cache
1094            .get_flattened_entries_for_tests()
1095            .iter()
1096            .filter(|(_key, program)| predicate(&program.program))
1097            .count()
1098    }
1099
1100    #[expect(clippy::arithmetic_side_effects)]
1101    fn program_deploy_test_helper(
1102        cache: &mut ProgramCache<TestForkGraph>,
1103        program: Pubkey,
1104        deployment_slots: Vec<Slot>,
1105        usage_counters: Vec<u64>,
1106        programs: &mut Vec<(Pubkey, Slot, u64)>,
1107    ) {
1108        let env = get_mock_program_runtime_environment();
1109        // Add multiple entries for program
1110        deployment_slots
1111            .iter()
1112            .enumerate()
1113            .for_each(|(i, deployment_slot)| {
1114                let usage_counter = *usage_counters.get(i).unwrap_or(&0);
1115                let stats = ProgramStatistics {
1116                    uses: usage_counter.into(),
1117                    ..Default::default()
1118                };
1119                cache.assign_program(
1120                    &env,
1121                    program,
1122                    *deployment_slot,
1123                    new_test_entry_with_usage(*deployment_slot, stats),
1124                );
1125                programs.push((program, *deployment_slot, usage_counter));
1126            });
1127
1128        let next_slot = deployment_slots.iter().max().map_or(0, |slot| slot + 1);
1129
1130        // Add tombstones entries for program
1131        let env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
1132        for slot in next_slot..next_slot + 10 {
1133            set_failed_verification_tombstone(
1134                cache,
1135                program,
1136                slot,
1137                ProgramRuntimeEnvironment::clone(&env),
1138            );
1139        }
1140
1141        // Add unloaded entries for program
1142        for slot in next_slot + 10..next_slot + 20 {
1143            insert_unloaded_entry(cache, program, slot);
1144        }
1145    }
1146
1147    #[test]
1148    fn test_random_eviction() {
1149        let mut programs = vec![];
1150        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1151
1152        // This test adds different kind of entries to the cache.
1153        // Tombstones and unloaded entries are expected to not be evicted.
1154        // It also adds multiple entries for three programs as it tries to create a typical cache instance.
1155
1156        // Program 1
1157        program_deploy_test_helper(
1158            &mut cache,
1159            Pubkey::new_unique(),
1160            vec![0, 10, 20, 30, 40],
1161            vec![4, 5, 25, 35, 12],
1162            &mut programs,
1163        );
1164
1165        // Program 2
1166        program_deploy_test_helper(
1167            &mut cache,
1168            Pubkey::new_unique(),
1169            vec![5, 11, 21, 24],
1170            vec![0, 2, 30, 45],
1171            &mut programs,
1172        );
1173
1174        // Program 3
1175        program_deploy_test_helper(
1176            &mut cache,
1177            Pubkey::new_unique(),
1178            vec![0, 5, 15, 25],
1179            vec![100, 3, 20, 40],
1180            &mut programs,
1181        );
1182
1183        // 1 for each deployment slot
1184        let num_loaded_expected = 13;
1185        // 10 for each program
1186        let num_unloaded_expected = 30;
1187        // 10 for each program
1188        let num_tombstones_expected = 30;
1189
1190        // Count the number of loaded, unloaded and tombstone entries.
1191        programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
1192        let num_loaded = num_matching_entries(&cache, |program_type| {
1193            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1194        });
1195        let num_unloaded = num_matching_entries(&cache, |program_type| {
1196            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1197        });
1198        let num_tombstones = num_matching_entries(&cache, |program_type| {
1199            matches!(
1200                program_type,
1201                ProgramCacheEntryType::DelayVisibility
1202                    | ProgramCacheEntryType::FailedVerification(_)
1203                    | ProgramCacheEntryType::Closed
1204            )
1205        });
1206
1207        // Test that the cache is constructed with the expected number of entries.
1208        assert_eq!(num_loaded, num_loaded_expected);
1209        assert_eq!(num_unloaded, num_unloaded_expected);
1210        assert_eq!(num_tombstones, num_tombstones_expected);
1211
1212        // Evict entries from the cache
1213        let eviction_pct: Percent = 1;
1214
1215        let num_loaded_expected = crate::loaded_programs::percent_of_max_entries(eviction_pct);
1216        let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
1217        cache.evict_using_random_selection(eviction_pct, 21);
1218
1219        // Count the number of loaded, unloaded and tombstone entries.
1220        let num_loaded = num_matching_entries(&cache, |program_type| {
1221            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1222        });
1223        let num_unloaded = num_matching_entries(&cache, |program_type| {
1224            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1225        });
1226        let num_tombstones = num_matching_entries(&cache, |program_type| {
1227            matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
1228        });
1229
1230        // However many entries are left after the shrink
1231        assert_eq!(num_loaded, num_loaded_expected);
1232        // The original unloaded entries + the evicted loaded entries
1233        assert_eq!(num_unloaded, num_unloaded_expected);
1234        // The original tombstones are not evicted
1235        assert_eq!(num_tombstones, num_tombstones_expected);
1236    }
1237
1238    #[test]
1239    fn test_eviction() {
1240        let mut programs = vec![];
1241        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1242
1243        // Program 1
1244        program_deploy_test_helper(
1245            &mut cache,
1246            Pubkey::new_unique(),
1247            vec![0, 10, 20, 30, 40],
1248            vec![4, 5, 25, 35, 12],
1249            &mut programs,
1250        );
1251
1252        // Program 2
1253        program_deploy_test_helper(
1254            &mut cache,
1255            Pubkey::new_unique(),
1256            vec![5, 11, 21, 24],
1257            vec![0, 2, 30, 45],
1258            &mut programs,
1259        );
1260
1261        // Program 3
1262        program_deploy_test_helper(
1263            &mut cache,
1264            Pubkey::new_unique(),
1265            vec![0, 5, 15, 25],
1266            vec![100, 3, 20, 40],
1267            &mut programs,
1268        );
1269
1270        // 1 for each deployment slot
1271        let num_loaded_expected = 13;
1272        // 10 for each program
1273        let num_unloaded_expected = 30;
1274        // 10 for each program
1275        let num_tombstones_expected = 30;
1276
1277        // Count the number of loaded, unloaded and tombstone entries.
1278        programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
1279        let num_loaded = num_matching_entries(&cache, |program_type| {
1280            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1281        });
1282        let num_unloaded = num_matching_entries(&cache, |program_type| {
1283            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1284        });
1285        let num_tombstones = num_matching_entries(&cache, |program_type| {
1286            matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
1287        });
1288
1289        // Test that the cache is constructed with the expected number of entries.
1290        assert_eq!(num_loaded, num_loaded_expected);
1291        assert_eq!(num_unloaded, num_unloaded_expected);
1292        assert_eq!(num_tombstones, num_tombstones_expected);
1293
1294        // Evict entries from the cache
1295        let eviction_pct: Percent = 1;
1296
1297        let num_loaded_expected = crate::loaded_programs::percent_of_max_entries(eviction_pct);
1298        let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
1299
1300        cache.sort_and_unload(eviction_pct);
1301
1302        // Check that every program is still in the cache.
1303        let entries = cache.get_flattened_entries_for_tests();
1304        programs.iter().for_each(|entry| {
1305            assert!(entries.iter().any(|(key, _entry)| key == &entry.0));
1306        });
1307
1308        let unloaded = entries
1309            .iter()
1310            .filter_map(|(key, program)| {
1311                matches!(program.program, ProgramCacheEntryType::Unloaded(_))
1312                    .then_some((*key, program.stats.uses.load(Ordering::Relaxed)))
1313            })
1314            .collect::<Vec<(Pubkey, u64)>>();
1315
1316        for index in 0..3 {
1317            let expected = programs.get(index).expect("Missing program");
1318            assert!(unloaded.contains(&(expected.0, expected.2)));
1319        }
1320
1321        // Count the number of loaded, unloaded and tombstone entries.
1322        let num_loaded = num_matching_entries(&cache, |program_type| {
1323            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1324        });
1325        let num_unloaded = num_matching_entries(&cache, |program_type| {
1326            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1327        });
1328        let num_tombstones = num_matching_entries(&cache, |program_type| {
1329            matches!(
1330                program_type,
1331                ProgramCacheEntryType::DelayVisibility
1332                    | ProgramCacheEntryType::FailedVerification(_)
1333                    | ProgramCacheEntryType::Closed
1334            )
1335        });
1336
1337        // However many entries are left after the shrink
1338        assert_eq!(num_loaded, num_loaded_expected);
1339        // The original unloaded entries + the evicted loaded entries
1340        assert_eq!(num_unloaded, num_unloaded_expected);
1341        // The original tombstones are not evicted
1342        assert_eq!(num_tombstones, num_tombstones_expected);
1343    }
1344
1345    #[test]
1346    fn test_usage_count_of_unloaded_program() {
1347        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1348        let env = get_mock_program_runtime_environment();
1349
1350        let program = Pubkey::new_unique();
1351        let evict_to_pct: Percent = 2;
1352        let cache_capacity_after_shrink =
1353            crate::loaded_programs::percent_of_max_entries(evict_to_pct);
1354        // Add enough programs to the cache to trigger 1 eviction after shrinking.
1355        let num_total_programs = (cache_capacity_after_shrink + 1) as u64;
1356        (0..num_total_programs).for_each(|i| {
1357            let stats = ProgramStatistics {
1358                uses: (i + 10).into(),
1359                ..Default::default()
1360            };
1361            let entry = new_test_entry_with_usage(i, stats);
1362            cache.assign_program(&env, program, i, entry);
1363        });
1364
1365        cache.sort_and_unload(evict_to_pct);
1366
1367        let num_unloaded = num_matching_entries(&cache, |program_type| {
1368            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1369        });
1370        assert_eq!(num_unloaded, 1);
1371
1372        cache
1373            .get_flattened_entries_for_tests()
1374            .iter()
1375            .for_each(|(_key, program)| {
1376                if matches!(program.program, ProgramCacheEntryType::Unloaded(_)) {
1377                    // Test that the usage counter is retained for the unloaded program
1378                    assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10);
1379                    assert_eq!(program.deployment_slot, 0);
1380                    assert_eq!(program.effective_slot(), 1);
1381                }
1382            });
1383
1384        // Replenish the program that was just unloaded. Use 0 as the usage counter. This should be
1385        // updated with the usage counter from the unloaded program.
1386        cache.assign_program(
1387            &env,
1388            program,
1389            0,
1390            new_test_entry_with_usage(0, ProgramStatistics::default()),
1391        );
1392
1393        cache
1394            .get_flattened_entries_for_tests()
1395            .iter()
1396            .for_each(|(_key, program)| {
1397                if matches!(program.program, ProgramCacheEntryType::Unloaded(_))
1398                    && program.deployment_slot == 0
1399                    && program.effective_slot() == 1
1400                {
1401                    // Test that the usage counter was correctly updated.
1402                    assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10);
1403                }
1404            });
1405    }
1406
1407    #[test]
1408    #[should_panic(expected = "Unexpected assignment of a DelayVisibility tombstone")]
1409    fn test_assign_program_delay_visibility_tombstone_panics() {
1410        // A tombstone minted by `extract` only ever lives in the batch cache.
1411        // Assigning one into the global cache is a caller error.
1412        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1413        let env = get_mock_program_runtime_environment();
1414        cache.assign_program(
1415            &env,
1416            Pubkey::new_unique(),
1417            100,
1418            Arc::new(ProgramCacheEntry::new_delay_visibility_tombstone(
1419                100,
1420                ProgramCacheEntryOwner::LoaderV3,
1421                Arc::default(),
1422            )),
1423        );
1424    }
1425
1426    #[test]
1427    fn test_fuzz_assign_program_order() {
1428        use rand::prelude::SliceRandom;
1429        const EXPECTED_ENTRIES: [(u64, bool); 5] =
1430            [(1, true), (3, false), (5, true), (9, true), (10, false)];
1431        let mut rng = rand::rng();
1432        let program_id = Pubkey::new_unique();
1433        let env = get_mock_program_runtime_environment();
1434        for _ in 0..1000 {
1435            let mut entries = EXPECTED_ENTRIES.to_vec();
1436            entries.shuffle(&mut rng);
1437            let mut cache = ProgramCache::<TestForkGraph>::new(0);
1438            for (deployment_slot, delay_visibility) in entries {
1439                let entry = Arc::new(if delay_visibility {
1440                    ProgramCacheEntry {
1441                        program: new_loaded_entry(ProgramRuntimeEnvironment::from(
1442                            BuiltinProgram::new_mock(),
1443                        )), // Assign them different environments
1444                        account_owner: ProgramCacheEntryOwner::LoaderV2,
1445                        deployment_slot,
1446                        stats: Arc::default(),
1447                        latest_access_slot: AtomicU64::new(deployment_slot),
1448                    }
1449                } else {
1450                    ProgramCacheEntry::new_failed_verification_tombstone(
1451                        deployment_slot,
1452                        ProgramCacheEntryOwner::LoaderV2,
1453                        ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()), // Assign them different environments
1454                    )
1455                });
1456                assert!(!cache.assign_program(&env, program_id, deployment_slot, entry));
1457            }
1458            for ((deployment_slot, delay_visibility), entry) in EXPECTED_ENTRIES
1459                .iter()
1460                .zip(cache.get_slot_versions_for_tests(&program_id).iter())
1461            {
1462                assert_eq!(entry.deployment_slot, *deployment_slot);
1463                assert_eq!(
1464                    entry.effective_slot(),
1465                    deployment_slot.saturating_add(*delay_visibility as u64)
1466                );
1467            }
1468        }
1469    }
1470
1471    #[test_matrix(
1472        (
1473            ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
1474            new_loaded_entry(get_mock_program_runtime_environment()),
1475        ),
1476        (
1477            ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
1478            ProgramCacheEntryType::Closed,
1479            ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1480            new_loaded_entry(get_mock_program_runtime_environment()),
1481            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1482        )
1483    )]
1484    #[test_matrix(
1485        ProgramCacheEntryType::Closed,
1486        (
1487            ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
1488            ProgramCacheEntryType::Closed,
1489            new_loaded_entry(get_mock_program_runtime_environment()),
1490            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1491        )
1492    )]
1493    #[test_matrix(
1494        ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1495        (
1496            ProgramCacheEntryType::Closed,
1497            ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1498            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1499        )
1500    )]
1501    #[test_matrix(
1502        (ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),),
1503        (
1504            ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
1505            ProgramCacheEntryType::Closed,
1506            ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1507            new_loaded_entry(get_mock_program_runtime_environment()),
1508        )
1509    )]
1510    #[should_panic(expected = "Unexpected replacement of an entry")]
1511    fn test_assign_program_failure(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
1512        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1513        let env = get_mock_program_runtime_environment();
1514        let program_id = Pubkey::new_unique();
1515        assert!(!cache.assign_program(
1516            &env,
1517            program_id,
1518            10,
1519            Arc::new(ProgramCacheEntry {
1520                program: old,
1521                account_owner: ProgramCacheEntryOwner::LoaderV2,
1522                deployment_slot: 10,
1523                stats: Arc::default(),
1524                latest_access_slot: AtomicU64::default(),
1525            }),
1526        ));
1527        cache.assign_program(
1528            &env,
1529            program_id,
1530            10,
1531            Arc::new(ProgramCacheEntry {
1532                program: new,
1533                account_owner: ProgramCacheEntryOwner::LoaderV2,
1534                deployment_slot: 10,
1535                stats: Arc::default(),
1536                latest_access_slot: AtomicU64::default(),
1537            }),
1538        );
1539    }
1540
1541    #[test_matrix(
1542        ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1543        (
1544            new_loaded_entry(get_mock_program_runtime_environment()),
1545            ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
1546        )
1547    )]
1548    #[test_case(
1549        ProgramCacheEntryType::Closed,
1550        ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment())
1551    )]
1552    #[test_case(
1553        ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1554        ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock())
1555    )]
1556    fn test_assign_program_success(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
1557        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1558        let env = get_mock_program_runtime_environment();
1559        let program_id = Pubkey::new_unique();
1560        assert!(!cache.assign_program(
1561            &env,
1562            program_id,
1563            10,
1564            Arc::new(ProgramCacheEntry {
1565                program: old,
1566                account_owner: ProgramCacheEntryOwner::LoaderV2,
1567                deployment_slot: 10,
1568                stats: Arc::default(),
1569                latest_access_slot: AtomicU64::default(),
1570            }),
1571        ));
1572        assert!(!cache.assign_program(
1573            &env,
1574            program_id,
1575            10,
1576            Arc::new(ProgramCacheEntry {
1577                program: new,
1578                account_owner: ProgramCacheEntryOwner::LoaderV2,
1579                deployment_slot: 10,
1580                stats: Arc::default(),
1581                latest_access_slot: AtomicU64::default(),
1582            }),
1583        ));
1584    }
1585
1586    #[test]
1587    fn test_assign_program_removes_entries_in_same_slot() {
1588        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1589        let env = get_mock_program_runtime_environment();
1590        let program_id = Pubkey::new_unique();
1591        let closed_other_slot = Arc::new(ProgramCacheEntry {
1592            program: ProgramCacheEntryType::Closed,
1593            account_owner: ProgramCacheEntryOwner::LoaderV2,
1594            deployment_slot: 9,
1595            stats: Arc::default(),
1596            latest_access_slot: AtomicU64::default(),
1597        });
1598        let closed_current_slot = Arc::new(ProgramCacheEntry {
1599            program: ProgramCacheEntryType::Closed,
1600            account_owner: ProgramCacheEntryOwner::LoaderV2,
1601            deployment_slot: 10,
1602            stats: Arc::default(),
1603            latest_access_slot: AtomicU64::default(),
1604        });
1605        let loaded_entry_current_env = Arc::new(ProgramCacheEntry {
1606            program: ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
1607            account_owner: ProgramCacheEntryOwner::LoaderV2,
1608            deployment_slot: 10,
1609            stats: Arc::default(),
1610            latest_access_slot: AtomicU64::default(),
1611        });
1612        let loaded_entry_upcoming_env = Arc::new(ProgramCacheEntry {
1613            program: ProgramCacheEntryType::Unloaded(ProgramRuntimeEnvironment::from(
1614                BuiltinProgram::new_mock(),
1615            )),
1616            account_owner: ProgramCacheEntryOwner::LoaderV2,
1617            deployment_slot: 10,
1618            stats: Arc::default(),
1619            latest_access_slot: AtomicU64::default(),
1620        });
1621        assert!(!cache.assign_program(&env, program_id, 9, closed_other_slot.clone()));
1622        assert!(!cache.assign_program(&env, program_id, 10, closed_current_slot));
1623        assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_upcoming_env.clone()));
1624        assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_current_env.clone()));
1625        // Only the conflicting entry in the same slot which does not have a different environment is removed
1626        assert_eq!(
1627            cache.get_slot_versions_for_tests(&program_id),
1628            &[
1629                closed_other_slot,
1630                loaded_entry_current_env,
1631                loaded_entry_upcoming_env
1632            ]
1633        );
1634    }
1635
1636    #[test]
1637    fn test_assign_program_reload_merges_statistics() {
1638        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1639        let env = get_mock_program_runtime_environment();
1640        let program_id = Pubkey::new_unique();
1641
1642        let stats = Arc::new(ProgramStatistics {
1643            uses: 1.into(),
1644            compilations: 2.into(),
1645            total_compilation_time_us: 3.into(),
1646            compilation_time_ema: 100.into(),
1647            jit_invocations: 4.into(),
1648            total_jit_execution_time_us: 5.into(),
1649            jit_execution_time_ema: 200.into(),
1650            interpreted_invocations: 6.into(),
1651            total_interpretation_time_us: 7.into(),
1652            interpretation_time_ema: 300.into(),
1653        });
1654        let unloaded = Arc::new(ProgramCacheEntry {
1655            program: ProgramCacheEntryType::Unloaded(env.clone()),
1656            account_owner: ProgramCacheEntryOwner::LoaderV3,
1657            deployment_slot: 100,
1658            stats: Arc::clone(&stats),
1659            latest_access_slot: AtomicU64::default(),
1660        });
1661        cache.assign_program(&env, program_id, 100, unloaded);
1662
1663        // `Unloaded` -> `Loaded` matches the existing entry, so it is a reload.
1664        let loaded = Arc::new(ProgramCacheEntry {
1665            program: new_loaded_entry(env.clone()),
1666            account_owner: ProgramCacheEntryOwner::LoaderV3,
1667            deployment_slot: 100,
1668            stats: Arc::default(), // <-- Empty stats
1669            latest_access_slot: AtomicU64::default(),
1670        });
1671        cache.assign_program(&env, program_id, 100, Arc::clone(&loaded));
1672
1673        assert_eq!(cache.stats.insertions.load(Ordering::Relaxed), 1);
1674        assert_eq!(cache.stats.reloads.load(Ordering::Relaxed), 1);
1675
1676        let merged = &loaded.stats;
1677        let ord = Ordering::Relaxed;
1678        assert_eq!(merged.uses.load(ord), stats.uses.load(ord));
1679        assert_eq!(merged.compilations.load(ord), stats.compilations.load(ord));
1680        assert_eq!(
1681            merged.total_compilation_time_us.load(ord),
1682            stats.total_compilation_time_us.load(ord)
1683        );
1684        assert_eq!(
1685            merged.jit_invocations.load(ord),
1686            stats.jit_invocations.load(ord)
1687        );
1688        assert_eq!(
1689            merged.total_jit_execution_time_us.load(ord),
1690            stats.total_jit_execution_time_us.load(ord)
1691        );
1692        assert_eq!(
1693            merged.interpreted_invocations.load(ord),
1694            stats.interpreted_invocations.load(ord)
1695        );
1696        assert_eq!(
1697            merged.total_interpretation_time_us.load(ord),
1698            stats.total_interpretation_time_us.load(ord)
1699        );
1700
1701        // The moving averages are weighted against the empty ones of the new
1702        // entry, which halves them.
1703        const EMA_DIVISOR: u64 = 2;
1704        assert_eq!(
1705            merged.compilation_time_ema.load(ord),
1706            stats
1707                .compilation_time_ema
1708                .load(ord)
1709                .wrapping_div(EMA_DIVISOR)
1710        );
1711        assert_eq!(
1712            merged.jit_execution_time_ema.load(ord),
1713            stats
1714                .jit_execution_time_ema
1715                .load(ord)
1716                .wrapping_div(EMA_DIVISOR)
1717        );
1718        assert_eq!(
1719            merged.interpretation_time_ema.load(ord),
1720            stats
1721                .interpretation_time_ema
1722                .load(ord)
1723                .wrapping_div(EMA_DIVISOR)
1724        );
1725    }
1726
1727    #[test]
1728    fn test_tombstone() {
1729        let env = get_mock_program_runtime_environment();
1730        let tombstone = ProgramCacheEntry::new_failed_verification_tombstone(
1731            0,
1732            ProgramCacheEntryOwner::LoaderV2,
1733            env.clone(),
1734        );
1735        assert_matches!(
1736            tombstone.program,
1737            ProgramCacheEntryType::FailedVerification(_)
1738        );
1739        assert!(tombstone.is_tombstone());
1740        assert_eq!(tombstone.deployment_slot, 0);
1741        assert_eq!(tombstone.effective_slot(), 0);
1742
1743        let tombstone =
1744            ProgramCacheEntry::new_closed_tombstone(100, ProgramCacheEntryOwner::LoaderV2);
1745        assert_matches!(tombstone.program, ProgramCacheEntryType::Closed);
1746        assert!(tombstone.is_tombstone());
1747        assert_eq!(tombstone.deployment_slot, 100);
1748        assert_eq!(tombstone.effective_slot(), 100);
1749
1750        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1751        let program1 = Pubkey::new_unique();
1752        let tombstone = set_failed_verification_tombstone(&mut cache, program1, 10, env.clone());
1753        let slot_versions = cache.get_slot_versions_for_tests(&program1);
1754        assert_eq!(slot_versions.len(), 1);
1755        assert!(slot_versions.first().unwrap().is_tombstone());
1756        assert_eq!(tombstone.deployment_slot, 10);
1757        assert_eq!(tombstone.effective_slot(), 10);
1758
1759        // Add a program at slot 50, and a tombstone for the program at slot 60
1760        let program2 = Pubkey::new_unique();
1761        cache.assign_program(&env, program2, 50, new_test_builtin_entry(50));
1762        let slot_versions = cache.get_slot_versions_for_tests(&program2);
1763        assert_eq!(slot_versions.len(), 1);
1764        assert!(!slot_versions.first().unwrap().is_tombstone());
1765
1766        let tombstone = set_failed_verification_tombstone(&mut cache, program2, 60, env);
1767        let slot_versions = cache.get_slot_versions_for_tests(&program2);
1768        assert_eq!(slot_versions.len(), 2);
1769        assert!(!slot_versions.first().unwrap().is_tombstone());
1770        assert!(slot_versions.get(1).unwrap().is_tombstone());
1771        assert!(tombstone.is_tombstone());
1772        assert_eq!(tombstone.deployment_slot, 60);
1773        assert_eq!(tombstone.effective_slot(), 60);
1774    }
1775
1776    struct TestForkGraph {
1777        relation: BlockRelation,
1778    }
1779    impl ForkGraph for TestForkGraph {
1780        fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation {
1781            self.relation
1782        }
1783    }
1784
1785    #[test]
1786    fn test_prune_empty() {
1787        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1788        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1789            relation: BlockRelation::Unrelated,
1790        }));
1791
1792        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1793
1794        cache.prune(0, None, &fork_graph.read().unwrap());
1795        assert!(cache.get_flattened_entries_for_tests().is_empty());
1796
1797        cache.prune(10, None, &fork_graph.read().unwrap());
1798        assert!(cache.get_flattened_entries_for_tests().is_empty());
1799
1800        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1801        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1802            relation: BlockRelation::Ancestor,
1803        }));
1804
1805        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1806
1807        cache.prune(0, None, &fork_graph.read().unwrap());
1808        assert!(cache.get_flattened_entries_for_tests().is_empty());
1809
1810        cache.prune(10, None, &fork_graph.read().unwrap());
1811        assert!(cache.get_flattened_entries_for_tests().is_empty());
1812
1813        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1814        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1815            relation: BlockRelation::Descendant,
1816        }));
1817
1818        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1819
1820        cache.prune(0, None, &fork_graph.read().unwrap());
1821        assert!(cache.get_flattened_entries_for_tests().is_empty());
1822
1823        cache.prune(10, None, &fork_graph.read().unwrap());
1824        assert!(cache.get_flattened_entries_for_tests().is_empty());
1825
1826        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1827        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1828            relation: BlockRelation::Unknown,
1829        }));
1830        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1831
1832        cache.prune(0, None, &fork_graph.read().unwrap());
1833        assert!(cache.get_flattened_entries_for_tests().is_empty());
1834
1835        cache.prune(10, None, &fork_graph.read().unwrap());
1836        assert!(cache.get_flattened_entries_for_tests().is_empty());
1837    }
1838
1839    #[test]
1840    fn test_prune_tombstones() {
1841        let env = get_mock_program_runtime_environment();
1842        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1843            relation: BlockRelation::Ancestor,
1844        }));
1845
1846        let program1 = Pubkey::new_unique();
1847        let entries = [
1848            Arc::new(ProgramCacheEntry::new_unloaded(
1849                20,
1850                ProgramCacheEntryOwner::LoaderV3,
1851                ProgramRuntimeEnvironment::clone(&env),
1852            )),
1853            Arc::new(ProgramCacheEntry::new_closed_tombstone(
1854                20,
1855                ProgramCacheEntryOwner::LoaderV3,
1856            )),
1857            Arc::new(ProgramCacheEntry::new_failed_verification_tombstone(
1858                20,
1859                ProgramCacheEntryOwner::LoaderV3,
1860                ProgramRuntimeEnvironment::clone(&env),
1861            )),
1862        ];
1863        for entry in &entries {
1864            let mut cache = ProgramCache::<TestForkGraph>::new(0);
1865            cache.set_fork_graph(Arc::downgrade(&fork_graph));
1866            // Test that multiple entries prevent pruning
1867            cache.assign_program(&env, program1, 10, new_test_entry(10));
1868            cache.assign_program(&env, program1, entry.deployment_slot, Arc::clone(entry));
1869            cache.prune(
1870                MAX_TOMBSTONE_AGE_IN_SLOTS,
1871                None,
1872                &fork_graph.read().unwrap(),
1873            );
1874            let slot_versions = cache.get_slot_versions_for_tests(&program1);
1875            assert_eq!(slot_versions, std::slice::from_ref(entry));
1876            // Test that latest_access_slot prevents pruning
1877            cache.prune(
1878                MAX_TOMBSTONE_AGE_IN_SLOTS
1879                    .saturating_add(entry.latest_access_slot.load(Ordering::Relaxed)),
1880                None,
1881                &fork_graph.read().unwrap(),
1882            );
1883            let slot_versions = cache.get_slot_versions_for_tests(&program1);
1884            assert_eq!(slot_versions, std::slice::from_ref(entry));
1885            // Test that exeeding latest_access_slot + MAX_TOMBSTONE_AGE_IN_SLOTS prunes
1886            cache.prune(
1887                MAX_TOMBSTONE_AGE_IN_SLOTS
1888                    .saturating_add(entry.latest_access_slot.load(Ordering::Relaxed))
1889                    .saturating_add(1),
1890                None,
1891                &fork_graph.read().unwrap(),
1892            );
1893            assert!(cache.get_flattened_entries_for_tests().is_empty());
1894        }
1895    }
1896
1897    #[test]
1898    fn test_prune_with_two_environments_before_epoch_boundary() {
1899        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1900        let env = get_mock_program_runtime_environment();
1901        let new_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
1902        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1903            relation: BlockRelation::Ancestor,
1904        }));
1905        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1906
1907        let program1 = Pubkey::new_unique();
1908        cache.assign_program(&env, program1, 10, new_test_entry(10));
1909        let updated_program = Arc::new(ProgramCacheEntry {
1910            program: new_loaded_entry(new_env.clone()),
1911            deployment_slot: 20,
1912            ..Default::default()
1913        });
1914        cache.assign_program(&env, program1, 20, updated_program.clone());
1915
1916        // Test that there are 2 entries for the program
1917        assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
1918
1919        cache.prune(21, None, &fork_graph.read().unwrap());
1920
1921        // Test that prune didn't remove the entry, since environments are different.
1922        assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
1923    }
1924
1925    #[test]
1926    fn test_prune_with_two_environments_after_epoch_boundary() {
1927        let mut cache = ProgramCache::<TestForkGraph>::new(0);
1928        let env = get_mock_program_runtime_environment();
1929        let new_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
1930        let fork_graph = Arc::new(RwLock::new(TestForkGraph {
1931            relation: BlockRelation::Ancestor,
1932        }));
1933        cache.set_fork_graph(Arc::downgrade(&fork_graph));
1934        let program1 = Pubkey::new_unique();
1935
1936        let old_program_old_env = Arc::new(ProgramCacheEntry {
1937            program: new_loaded_entry(env.clone()),
1938            deployment_slot: 10,
1939            ..Default::default()
1940        });
1941        let old_program_new_env = Arc::new(ProgramCacheEntry {
1942            program: new_loaded_entry(new_env.clone()),
1943            deployment_slot: 10,
1944            ..Default::default()
1945        });
1946        let new_program_old_env = Arc::new(ProgramCacheEntry {
1947            program: new_loaded_entry(env.clone()),
1948            deployment_slot: 20,
1949            ..Default::default()
1950        });
1951        cache.assign_program(&env, program1, 10, old_program_old_env.clone());
1952        cache.assign_program(&env, program1, 10, old_program_new_env.clone());
1953        cache.assign_program(&env, program1, 20, new_program_old_env.clone());
1954        let slot_versions = cache.get_slot_versions_for_tests(&program1);
1955        assert_eq!(
1956            &slot_versions,
1957            &[
1958                old_program_new_env.clone(),
1959                old_program_old_env.clone(),
1960                new_program_old_env.clone(),
1961            ]
1962        );
1963
1964        cache.prune(21, Some(new_env.clone()), &fork_graph.read().unwrap());
1965        let slot_versions = cache.get_slot_versions_for_tests(&program1);
1966        assert_eq!(&slot_versions, &[old_program_new_env]);
1967        assert!(matches!(
1968            &slot_versions.first().unwrap().program,
1969            ProgramCacheEntryType::Loaded(_)
1970        ));
1971    }
1972
1973    #[derive(Default)]
1974    struct TestForkGraphSpecific {
1975        forks: Vec<Vec<Slot>>,
1976    }
1977
1978    impl TestForkGraphSpecific {
1979        fn insert_fork(&mut self, fork: &[Slot]) {
1980            let mut fork = fork.to_vec();
1981            fork.sort();
1982            self.forks.push(fork)
1983        }
1984    }
1985
1986    impl ForkGraph for TestForkGraphSpecific {
1987        fn relationship(&self, a: Slot, b: Slot) -> BlockRelation {
1988            match self.forks.iter().try_for_each(|fork| {
1989                let relation = fork
1990                    .iter()
1991                    .position(|x| *x == a)
1992                    .and_then(|a_pos| {
1993                        fork.iter().position(|x| *x == b).and_then(|b_pos| {
1994                            (a_pos == b_pos)
1995                                .then_some(BlockRelation::Equal)
1996                                .or_else(|| (a_pos < b_pos).then_some(BlockRelation::Ancestor))
1997                                .or(Some(BlockRelation::Descendant))
1998                        })
1999                    })
2000                    .unwrap_or(BlockRelation::Unrelated);
2001
2002                if relation != BlockRelation::Unrelated {
2003                    return ControlFlow::Break(relation);
2004                }
2005
2006                ControlFlow::Continue(())
2007            }) {
2008                ControlFlow::Break(relation) => relation,
2009                _ => BlockRelation::Unrelated,
2010            }
2011        }
2012    }
2013
2014    fn get_entries_to_load<'a>(
2015        cache: &ProgramCache<TestForkGraphSpecific>,
2016        loading_slot: Slot,
2017        keys: &'a [Pubkey],
2018    ) -> Vec<ProgramToLoad<'a>> {
2019        let fork_graph = cache.fork_graph.as_ref().unwrap().upgrade().unwrap();
2020        let locked_fork_graph = fork_graph.read().unwrap();
2021        let entries = cache.get_flattened_entries_for_tests();
2022        keys.iter()
2023            .filter_map(|key| {
2024                entries
2025                    .iter()
2026                    .rev()
2027                    .find(|(program_id, entry)| {
2028                        program_id == key
2029                            && matches!(
2030                                locked_fork_graph.relationship(entry.deployment_slot, loading_slot),
2031                                BlockRelation::Equal | BlockRelation::Ancestor,
2032                            )
2033                    })
2034                    .map(|(_program_id, entry)| ProgramToLoad {
2035                        program_id: key,
2036                        loader: entry.account_owner,
2037                        deployment_slot: entry.deployment_slot,
2038                        last_modification_slot: entry.deployment_slot,
2039                    })
2040            })
2041            .collect()
2042    }
2043
2044    fn match_slot(
2045        extracted: &ProgramCacheForTxBatch,
2046        program: &Pubkey,
2047        deployment_slot: Slot,
2048        working_slot: Slot,
2049    ) -> bool {
2050        assert_eq!(extracted.slot, working_slot);
2051        extracted
2052            .entries
2053            .get(program)
2054            .map(|entry| entry.deployment_slot == deployment_slot)
2055            .unwrap_or(false)
2056    }
2057
2058    fn match_missing(
2059        missing: &[ProgramToLoad],
2060        program_id: &Pubkey,
2061        expected_result: bool,
2062    ) -> bool {
2063        missing.iter().any(|entry| entry.program_id == program_id) == expected_result
2064    }
2065
2066    #[test]
2067    fn test_fork_extract_and_prune() {
2068        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
2069        let env = get_mock_program_runtime_environment();
2070
2071        // Fork graph created for the test
2072        //                   0
2073        //                 /   \
2074        //                10    5
2075        //                |     |
2076        //                20    11
2077        //                |     | \
2078        //                22   15  25
2079        //                      |   |
2080        //                     16  27
2081        //                      |
2082        //                     19
2083        //                      |
2084        //                     23
2085
2086        let mut fork_graph = TestForkGraphSpecific::default();
2087        fork_graph.insert_fork(&[0, 10, 20, 22]);
2088        fork_graph.insert_fork(&[0, 5, 11, 15, 16, 18, 19, 21, 23]);
2089        fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
2090
2091        let fork_graph = Arc::new(RwLock::new(fork_graph));
2092        cache.set_fork_graph(Arc::downgrade(&fork_graph));
2093
2094        let program1 = Pubkey::new_unique();
2095        cache.assign_program(&env, program1, 0, new_test_entry(0));
2096        cache.assign_program(&env, program1, 10, new_test_entry(10));
2097        cache.assign_program(&env, program1, 20, new_test_entry(20));
2098
2099        let program2 = Pubkey::new_unique();
2100        cache.assign_program(&env, program2, 5, new_test_entry(5));
2101        cache.assign_program(&env, program2, 11, new_test_entry(11));
2102
2103        let program3 = Pubkey::new_unique();
2104        cache.assign_program(&env, program3, 25, new_test_entry(25));
2105
2106        let program4 = Pubkey::new_unique();
2107        cache.assign_program(&env, program4, 0, new_test_entry(0));
2108        cache.assign_program(&env, program4, 5, new_test_entry(5));
2109        // The following is a special case, where effective slot is 3 slots in the future
2110        cache.assign_program(&env, program4, 15, new_test_entry(15));
2111
2112        // Current fork graph
2113        //                   0
2114        //                 /   \
2115        //                10    5
2116        //                |     |
2117        //                20    11
2118        //                |     | \
2119        //                22   15  25
2120        //                      |   |
2121        //                     16  27
2122        //                      |
2123        //                     19
2124        //                      |
2125        //                     23
2126
2127        // Testing fork 0 - 10 - 20 - 22 with current slot at 22
2128        let keys = &[program1, program2, program3, program4];
2129        let mut missing = get_entries_to_load(&cache, 22, keys);
2130        assert!(match_missing(&missing, &program2, false));
2131        assert!(match_missing(&missing, &program3, false));
2132        let mut extracted = ProgramCacheForTxBatch::new(22);
2133        cache.extract(&mut missing, &mut extracted, &env, true, true);
2134        assert!(match_slot(&extracted, &program1, 20, 22));
2135        assert!(match_slot(&extracted, &program4, 0, 22));
2136
2137        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 15
2138        let mut missing = get_entries_to_load(&cache, 15, keys);
2139        assert!(match_missing(&missing, &program3, false));
2140        let mut extracted = ProgramCacheForTxBatch::new(15);
2141        cache.extract(&mut missing, &mut extracted, &env, true, true);
2142        assert!(match_slot(&extracted, &program1, 0, 15));
2143        assert!(match_slot(&extracted, &program2, 11, 15));
2144        // The effective slot of program4 deployed in slot 15 is 19. So it should not be usable in slot 16.
2145        // A delay visibility tombstone should be returned here.
2146        let tombstone = extracted
2147            .find(&program4)
2148            .expect("Failed to find the tombstone");
2149        assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
2150        assert_eq!(tombstone.deployment_slot, 15);
2151
2152        // Testing the same fork above, but current slot is now 18 (equal to effective slot of program4).
2153        let mut missing = get_entries_to_load(&cache, 18, keys);
2154        assert!(match_missing(&missing, &program3, false));
2155        let mut extracted = ProgramCacheForTxBatch::new(18);
2156        cache.extract(&mut missing, &mut extracted, &env, true, true);
2157        assert!(match_slot(&extracted, &program1, 0, 18));
2158        assert!(match_slot(&extracted, &program2, 11, 18));
2159        // The effective slot of program4 deployed in slot 15 is 18. So it should be usable in slot 18.
2160        assert!(match_slot(&extracted, &program4, 15, 18));
2161
2162        // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4).
2163        let mut missing = get_entries_to_load(&cache, 23, keys);
2164        assert!(match_missing(&missing, &program3, false));
2165        let mut extracted = ProgramCacheForTxBatch::new(23);
2166        cache.extract(&mut missing, &mut extracted, &env, true, true);
2167        assert!(match_slot(&extracted, &program1, 0, 23));
2168        assert!(match_slot(&extracted, &program2, 11, 23));
2169        // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 23.
2170        assert!(match_slot(&extracted, &program4, 15, 23));
2171
2172        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11
2173        let mut missing = get_entries_to_load(&cache, 11, keys);
2174        assert!(match_missing(&missing, &program3, false));
2175        let mut extracted = ProgramCacheForTxBatch::new(11);
2176        cache.extract(&mut missing, &mut extracted, &env, true, true);
2177        assert!(match_slot(&extracted, &program1, 0, 11));
2178        // program2 was updated at slot 11, but is not effective till slot 12. The result should contain a tombstone.
2179        let tombstone = extracted
2180            .find(&program2)
2181            .expect("Failed to find the tombstone");
2182        assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
2183        assert_eq!(tombstone.deployment_slot, 11);
2184        assert!(match_slot(&extracted, &program4, 5, 11));
2185
2186        cache.prune(5, None, &fork_graph.read().unwrap());
2187
2188        // Fork graph after pruning
2189        //                   0
2190        //                   |
2191        //                   5
2192        //                   |
2193        //                   11
2194        //                   | \
2195        //                  15  25
2196        //                   |   |
2197        //                  16  27
2198        //                   |
2199        //                  19
2200        //                   |
2201        //                  23
2202
2203        // Testing fork 11 - 15 - 16- 19 - 22 with root at 5 and current slot at 22
2204        let mut missing = get_entries_to_load(&cache, 21, keys);
2205        assert!(match_missing(&missing, &program3, false));
2206        let mut extracted = ProgramCacheForTxBatch::new(21);
2207        cache.extract(&mut missing, &mut extracted, &env, true, true);
2208        // Since the fork was pruned, we should not find the entry deployed at slot 20.
2209        assert!(match_slot(&extracted, &program1, 0, 21));
2210        assert!(match_slot(&extracted, &program2, 11, 21));
2211        assert!(match_slot(&extracted, &program4, 15, 21));
2212
2213        // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27
2214        let mut missing = get_entries_to_load(&cache, 27, keys);
2215        let mut extracted = ProgramCacheForTxBatch::new(27);
2216        cache.extract(&mut missing, &mut extracted, &env, true, true);
2217        assert!(match_slot(&extracted, &program1, 0, 27));
2218        assert!(match_slot(&extracted, &program2, 11, 27));
2219        assert!(match_slot(&extracted, &program3, 25, 27));
2220        assert!(match_slot(&extracted, &program4, 5, 27));
2221
2222        cache.prune(15, None, &fork_graph.read().unwrap());
2223
2224        // Fork graph after pruning
2225        //                  0
2226        //                  |
2227        //                  5
2228        //                  |
2229        //                  11
2230        //                  |
2231        //                  15
2232        //                  |
2233        //                  16
2234        //                  |
2235        //                  19
2236        //                  |
2237        //                  23
2238
2239        // Testing fork 16, 19, 23, with root at 15, current slot at 23
2240        let mut missing = get_entries_to_load(&cache, 23, keys);
2241        assert!(match_missing(&missing, &program3, false));
2242        let mut extracted = ProgramCacheForTxBatch::new(23);
2243        cache.extract(&mut missing, &mut extracted, &env, true, true);
2244        assert!(match_slot(&extracted, &program1, 0, 23));
2245        assert!(match_slot(&extracted, &program2, 11, 23));
2246        assert!(match_slot(&extracted, &program4, 15, 23));
2247    }
2248
2249    #[test]
2250    fn test_extract_using_deployment_slot() {
2251        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
2252        let env = get_mock_program_runtime_environment();
2253
2254        // Fork graph created for the test
2255        //                   0
2256        //                 /   \
2257        //                10    5
2258        //                |     |
2259        //                20    11
2260        //                |     | \
2261        //                22   15  25
2262        //                      |   |
2263        //                     16  27
2264        //                      |
2265        //                     19
2266        //                      |
2267        //                     23
2268
2269        let mut fork_graph = TestForkGraphSpecific::default();
2270        fork_graph.insert_fork(&[0, 10, 20, 22]);
2271        fork_graph.insert_fork(&[0, 5, 11, 12, 15, 16, 18, 19, 21, 23]);
2272        fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
2273
2274        let fork_graph = Arc::new(RwLock::new(fork_graph));
2275        cache.set_fork_graph(Arc::downgrade(&fork_graph));
2276
2277        let program1 = Pubkey::new_unique();
2278        cache.assign_program(&env, program1, 0, new_test_entry(0));
2279        cache.assign_program(&env, program1, 20, new_test_entry(20));
2280
2281        let program2 = Pubkey::new_unique();
2282        cache.assign_program(&env, program2, 5, new_test_entry(5));
2283        cache.assign_program(&env, program2, 11, new_test_entry(11));
2284
2285        let program3 = Pubkey::new_unique();
2286        cache.assign_program(&env, program3, 25, new_test_entry(25));
2287
2288        // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19
2289        let keys = &[program1, program2, program3];
2290        let mut missing = get_entries_to_load(&cache, 12, keys);
2291        assert!(match_missing(&missing, &program3, false));
2292        let mut extracted = ProgramCacheForTxBatch::new(12);
2293        cache.extract(&mut missing, &mut extracted, &env, true, true);
2294        assert!(match_slot(&extracted, &program1, 0, 12));
2295        assert!(match_slot(&extracted, &program2, 11, 12));
2296
2297        // Now try extractions that previously worked under the "deployed on
2298        // or after" criteria, but won't work with exact matching.
2299        let mut missing = get_entries_to_load(&cache, 12, keys);
2300        // Program 2's newest entry is at slot 11. Asking for 5 doesn't extract
2301        // the latest (11) anymore. You get 5.
2302        missing.get_mut(1).unwrap().deployment_slot = 5;
2303        assert!(match_missing(&missing, &program3, false));
2304        let mut extracted = ProgramCacheForTxBatch::new(12);
2305        cache.extract(&mut missing, &mut extracted, &env, true, true);
2306        assert!(match_slot(&extracted, &program1, 0, 12));
2307        assert!(match_slot(&extracted, &program2, 5, 12));
2308    }
2309
2310    #[test]
2311    fn test_extract_rejects_entry_deployed_after_the_requested_slot() {
2312        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
2313        let env = get_mock_program_runtime_environment();
2314
2315        let mut fork_graph = TestForkGraphSpecific::default();
2316        fork_graph.insert_fork(&[0, 5, 11, 12]);
2317        let fork_graph = Arc::new(RwLock::new(fork_graph));
2318        cache.set_fork_graph(Arc::downgrade(&fork_graph));
2319
2320        // The only cached entry was deployed in slot 11.
2321        let program = Pubkey::new_unique();
2322        cache.assign_program(&env, program, 11, new_test_entry(11));
2323
2324        // A caller whose account state holds slot 5 must not be handed it.
2325        let mut missing = vec![ProgramToLoad {
2326            program_id: &program,
2327            loader: ProgramCacheEntryOwner::LoaderV2,
2328            deployment_slot: 5,
2329            last_modification_slot: 0,
2330        }];
2331        let mut extracted = ProgramCacheForTxBatch::new(12);
2332        cache.extract(&mut missing, &mut extracted, &env, true, true);
2333        assert!(match_missing(&missing, &program, true));
2334        assert!(extracted.find(&program).is_none());
2335
2336        // The same caller requesting slot 11 gets it.
2337        let mut missing = vec![ProgramToLoad {
2338            program_id: &program,
2339            loader: ProgramCacheEntryOwner::LoaderV2,
2340            deployment_slot: 11,
2341            last_modification_slot: 0,
2342        }];
2343        let mut extracted = ProgramCacheForTxBatch::new(12);
2344        cache.extract(&mut missing, &mut extracted, &env, true, true);
2345        assert!(match_missing(&missing, &program, false));
2346        assert!(match_slot(&extracted, &program, 11, 12));
2347    }
2348
2349    #[test]
2350    fn test_extract_unloaded() {
2351        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
2352        let env = get_mock_program_runtime_environment();
2353
2354        // Fork graph created for the test
2355        //                   0
2356        //                 /   \
2357        //                10    5
2358        //                |     |
2359        //                20    11
2360        //                |     | \
2361        //                22   15  25
2362        //                      |   |
2363        //                     16  27
2364        //                      |
2365        //                     19
2366        //                      |
2367        //                     23
2368
2369        let mut fork_graph = TestForkGraphSpecific::default();
2370        fork_graph.insert_fork(&[0, 10, 20, 22]);
2371        fork_graph.insert_fork(&[0, 5, 11, 15, 16, 19, 21, 23]);
2372        fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
2373
2374        let fork_graph = Arc::new(RwLock::new(fork_graph));
2375        cache.set_fork_graph(Arc::downgrade(&fork_graph));
2376
2377        let program1 = Pubkey::new_unique();
2378        cache.assign_program(&env, program1, 0, new_test_entry(0));
2379        cache.assign_program(&env, program1, 20, new_test_entry(20));
2380
2381        let program2 = Pubkey::new_unique();
2382        cache.assign_program(&env, program2, 5, new_test_entry(5));
2383        cache.assign_program(&env, program2, 11, new_test_entry(11));
2384
2385        let program3 = Pubkey::new_unique();
2386        // Insert an unloaded program with correct/cache's environment at slot 25
2387        let _ = insert_unloaded_entry(&mut cache, program3, 25);
2388
2389        // Insert another unloaded program with a different environment at slot 20
2390        // Since this entry's environment won't match cache's environment, looking up this
2391        // entry should return missing instead of unloaded entry.
2392        cache.assign_program(
2393            &env,
2394            program3,
2395            20,
2396            Arc::new(
2397                new_test_entry(20)
2398                    .to_unloaded()
2399                    .expect("Failed to create unloaded program"),
2400            ),
2401        );
2402
2403        // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19
2404        let keys = &[program1, program2, program3];
2405        let mut missing = get_entries_to_load(&cache, 19, keys);
2406        assert!(match_missing(&missing, &program3, false));
2407        let mut extracted = ProgramCacheForTxBatch::new(19);
2408        cache.extract(&mut missing, &mut extracted, &env, true, true);
2409        assert!(match_slot(&extracted, &program1, 0, 19));
2410        assert!(match_slot(&extracted, &program2, 11, 19));
2411
2412        // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27
2413        let mut missing = get_entries_to_load(&cache, 27, keys);
2414        let mut extracted = ProgramCacheForTxBatch::new(27);
2415        cache.extract(&mut missing, &mut extracted, &env, true, true);
2416        assert!(match_slot(&extracted, &program1, 0, 27));
2417        assert!(match_slot(&extracted, &program2, 11, 27));
2418        assert!(match_missing(&missing, &program3, true));
2419
2420        // Testing fork 0 - 10 - 20 - 22 with current slot at 22
2421        let mut missing = get_entries_to_load(&cache, 22, keys);
2422        assert!(match_missing(&missing, &program2, false));
2423        let mut extracted = ProgramCacheForTxBatch::new(22);
2424        cache.extract(&mut missing, &mut extracted, &env, true, true);
2425        assert!(match_slot(&extracted, &program1, 20, 22));
2426        assert!(match_missing(&missing, &program3, true));
2427    }
2428
2429    #[test]
2430    fn test_extract_different_environment() {
2431        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
2432        let env = get_mock_program_runtime_environment();
2433        let other_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
2434
2435        // Fork graph created for the test
2436        //                0
2437        //                |
2438        //                10
2439        //                |
2440        //                20
2441        //                |
2442        //                22
2443
2444        let mut fork_graph = TestForkGraphSpecific::default();
2445        fork_graph.insert_fork(&[0, 10, 20, 22]);
2446
2447        let fork_graph = Arc::new(RwLock::new(fork_graph));
2448        cache.set_fork_graph(Arc::downgrade(&fork_graph));
2449
2450        let program1 = Pubkey::new_unique();
2451        cache.assign_program(
2452            &env,
2453            program1,
2454            10,
2455            Arc::new(ProgramCacheEntry::new_closed_tombstone(
2456                10,
2457                ProgramCacheEntryOwner::LoaderV3,
2458            )),
2459        );
2460        cache.assign_program(&env, program1, 20, new_test_entry(20));
2461
2462        // Testing fork 0 - 10 - 20 - 22 with current slot at 22
2463        let keys = &[program1];
2464        let mut missing = get_entries_to_load(&cache, 22, keys);
2465        let mut extracted = ProgramCacheForTxBatch::new(22);
2466        cache.extract(&mut missing, &mut extracted, &env, true, true);
2467        assert!(match_slot(&extracted, &program1, 20, 22));
2468
2469        // Looking for a different environment
2470        let mut missing = get_entries_to_load(&cache, 22, keys);
2471        let mut extracted = ProgramCacheForTxBatch::new(22);
2472        cache.extract(&mut missing, &mut extracted, &other_env, true, true);
2473        assert!(match_missing(&missing, &program1, true));
2474    }
2475
2476    #[test_matrix((false, true))]
2477    fn test_extract_no_second_level(empty_second_level: bool) {
2478        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2479        let env = get_mock_program_runtime_environment();
2480        let program_id = Pubkey::new_unique();
2481        if empty_second_level {
2482            // Make the entry already exist, but with an empty second level.
2483            match &mut cache.index {
2484                IndexImplementation::V1 { entries, .. } => {
2485                    entries.insert(program_id, Vec::new());
2486                }
2487            }
2488        }
2489
2490        // There is nothing to iterate either way, so the program is left to be
2491        // loaded.
2492        let mut search_for = vec![ProgramToLoad {
2493            program_id: &program_id,
2494            loader: ProgramCacheEntryOwner::LoaderV3,
2495            deployment_slot: 0,
2496            last_modification_slot: 0,
2497        }];
2498        let mut extracted = ProgramCacheForTxBatch::new(100);
2499        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
2500        assert_eq!(search_for.len(), 1);
2501        assert!(extracted.entries.is_empty());
2502        assert_eq!(task, Some(program_id));
2503    }
2504
2505    #[test]
2506    fn test_extract_account_owner_mismatch() {
2507        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2508        let env = get_mock_program_runtime_environment();
2509        let program_id = Pubkey::new_unique();
2510        let owned_by_v2 = new_test_entry_with_owner(
2511            100,
2512            ProgramCacheEntryOwner::LoaderV2,
2513            new_loaded_entry(env.clone()),
2514        );
2515        cache.assign_program(&env, program_id, 100, Arc::clone(&owned_by_v2));
2516
2517        // The only entry has an owner the search does not ask for.
2518        // Nothing is extracted. The caller must reload.
2519        let mut search_for = vec![ProgramToLoad {
2520            program_id: &program_id,
2521            loader: ProgramCacheEntryOwner::LoaderV3,
2522            deployment_slot: 100,
2523            last_modification_slot: 0,
2524        }];
2525        let mut extracted = ProgramCacheForTxBatch::new(200);
2526        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2527        assert_eq!(search_for.len(), 1);
2528        assert!(extracted.entries.is_empty());
2529
2530        // A loader migration, where only the newest entry has the new owner. A
2531        // search for the old one skips it and takes the entry below it.
2532        let owned_by_v3 = new_test_entry_with_owner(
2533            150,
2534            ProgramCacheEntryOwner::LoaderV3,
2535            new_loaded_entry(env.clone()),
2536        );
2537        cache.assign_program(&env, program_id, 150, Arc::clone(&owned_by_v3));
2538
2539        // Here the cache has the original v2 at 100 followed by the v3 at 150.
2540        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
2541        assert_eq!(slot_versions.len(), 2);
2542        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &owned_by_v2));
2543        assert!(Arc::ptr_eq(slot_versions.get(1).unwrap(), &owned_by_v3));
2544
2545        // Try searching for the v2 version, from some fork that did not see
2546        // the migration. Assert the v2 entry is returned.
2547        let mut search_for = vec![ProgramToLoad {
2548            program_id: &program_id,
2549            loader: ProgramCacheEntryOwner::LoaderV2,
2550            deployment_slot: 100,
2551            last_modification_slot: 0,
2552        }];
2553        let mut extracted = ProgramCacheForTxBatch::new(200);
2554        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2555        assert!(search_for.is_empty());
2556        assert!(Arc::ptr_eq(
2557            extracted.entries.get(&program_id).unwrap(),
2558            &owned_by_v2
2559        ));
2560
2561        // And a fork which did see the migration finds the v3 entry, so both
2562        // owners are reachable from the same second level.
2563        let mut search_for = vec![ProgramToLoad {
2564            program_id: &program_id,
2565            loader: ProgramCacheEntryOwner::LoaderV3,
2566            deployment_slot: 150,
2567            last_modification_slot: 0,
2568        }];
2569        let mut extracted = ProgramCacheForTxBatch::new(200);
2570        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2571        assert!(search_for.is_empty());
2572        assert!(Arc::ptr_eq(
2573            extracted.entries.get(&program_id).unwrap(),
2574            &owned_by_v3
2575        ));
2576    }
2577
2578    #[test]
2579    fn test_extract_environment_mismatch() {
2580        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2581        let env = get_mock_program_runtime_environment();
2582        let other_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
2583        let program_id = Pubkey::new_unique();
2584        let on_other_env = new_test_entry_with_owner(
2585            100,
2586            ProgramCacheEntryOwner::LoaderV3,
2587            new_loaded_entry(other_env.clone()),
2588        );
2589        cache.assign_program(&other_env, program_id, 100, Arc::clone(&on_other_env));
2590
2591        // The only entry is in the same branch and effective, but it was built
2592        // for another environment.
2593        // Nothing is extracted. The caller must reload.
2594        // This is "reload when in doubt" in its smallest form.
2595        let mut search_for = vec![ProgramToLoad {
2596            program_id: &program_id,
2597            loader: ProgramCacheEntryOwner::LoaderV3,
2598            deployment_slot: 100,
2599            last_modification_slot: 0,
2600        }];
2601        let mut extracted = ProgramCacheForTxBatch::new(200);
2602        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2603        assert_eq!(search_for.len(), 1);
2604        assert!(extracted.entries.is_empty());
2605
2606        // The same deployment, compiled for the environment which is asked
2607        // for. Both are kept, since they differ in env.
2608        let on_execution_env = new_test_entry_with_owner(
2609            100,
2610            ProgramCacheEntryOwner::LoaderV3,
2611            new_loaded_entry(env.clone()),
2612        );
2613        cache.assign_program(&env, program_id, 100, Arc::clone(&on_execution_env));
2614
2615        // Here the cache has the one on the other environment first, since
2616        // entries for the current one sort last.
2617        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
2618        assert_eq!(slot_versions.len(), 2);
2619        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &on_other_env));
2620        assert!(Arc::ptr_eq(
2621            slot_versions.get(1).unwrap(),
2622            &on_execution_env
2623        ));
2624
2625        // Try searching for the entry with the current env. Assert it is
2626        // returned.
2627        let mut search_for = vec![ProgramToLoad {
2628            program_id: &program_id,
2629            loader: ProgramCacheEntryOwner::LoaderV3,
2630            deployment_slot: 100,
2631            last_modification_slot: 0,
2632        }];
2633        let mut extracted = ProgramCacheForTxBatch::new(200);
2634        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2635        assert!(search_for.is_empty());
2636        assert!(Arc::ptr_eq(
2637            extracted.entries.get(&program_id).unwrap(),
2638            &on_execution_env
2639        ));
2640
2641        // And searching under the other environment returns the entry built
2642        // for it, so both are reachable from the same second level.
2643        let mut search_for = vec![ProgramToLoad {
2644            program_id: &program_id,
2645            loader: ProgramCacheEntryOwner::LoaderV3,
2646            deployment_slot: 100,
2647            last_modification_slot: 0,
2648        }];
2649        let mut extracted = ProgramCacheForTxBatch::new(200);
2650        cache.extract(&mut search_for, &mut extracted, &other_env, true, true);
2651        assert!(search_for.is_empty());
2652        assert!(Arc::ptr_eq(
2653            extracted.entries.get(&program_id).unwrap(),
2654            &on_other_env
2655        ));
2656    }
2657
2658    #[test]
2659    fn test_extract_unloaded_entry() {
2660        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2661        let env = get_mock_program_runtime_environment();
2662        let program_id = Pubkey::new_unique();
2663        let unloaded = new_test_entry_with_owner(
2664            100,
2665            ProgramCacheEntryOwner::LoaderV3,
2666            new_unloaded_entry(env.clone()),
2667        );
2668        cache.assign_program(&env, program_id, 100, Arc::clone(&unloaded));
2669
2670        // The only entry clears every check documented in the previous test,
2671        // but its executable has been evicted.
2672        // Nothing is extracted. The caller must reload.
2673        let mut search_for = vec![ProgramToLoad {
2674            program_id: &program_id,
2675            loader: ProgramCacheEntryOwner::LoaderV3,
2676            deployment_slot: 100,
2677            last_modification_slot: 0,
2678        }];
2679        let mut extracted = ProgramCacheForTxBatch::new(200);
2680        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2681        assert_eq!(search_for.len(), 1);
2682        assert!(extracted.entries.is_empty());
2683
2684        // Reloading it is an allowed replacement, so it takes the same place.
2685        let loaded = new_test_entry_with_owner(
2686            100,
2687            ProgramCacheEntryOwner::LoaderV3,
2688            new_loaded_entry(env.clone()),
2689        );
2690        cache.assign_program(&env, program_id, 100, Arc::clone(&loaded));
2691        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
2692        assert_eq!(slot_versions.len(), 1);
2693        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &loaded));
2694
2695        // Extracting now gives the loaded entry. The unloaded is gone.
2696        let mut search_for = vec![ProgramToLoad {
2697            program_id: &program_id,
2698            loader: ProgramCacheEntryOwner::LoaderV3,
2699            deployment_slot: 100,
2700            last_modification_slot: 0,
2701        }];
2702        let mut extracted = ProgramCacheForTxBatch::new(200);
2703        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2704        assert!(search_for.is_empty());
2705        assert!(Arc::ptr_eq(
2706            extracted.entries.get(&program_id).unwrap(),
2707            &loaded
2708        ));
2709    }
2710
2711    #[test_case(false)]
2712    #[test_case(true)]
2713    fn test_extract_environment_filter_same_slot(other_env_first: bool) {
2714        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2715        let env = get_mock_program_runtime_environment();
2716        let other_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
2717        let program_id = Pubkey::new_unique();
2718
2719        // Two entries at one deployment slot, one per environment.
2720        let on_other_env = new_test_entry_with_owner(
2721            100,
2722            ProgramCacheEntryOwner::LoaderV3,
2723            new_loaded_entry(other_env.clone()),
2724        );
2725        let on_execution_env = new_test_entry_with_owner(
2726            100,
2727            ProgramCacheEntryOwner::LoaderV3,
2728            new_loaded_entry(env.clone()),
2729        );
2730        if other_env_first {
2731            cache.assign_program(&other_env, program_id, 100, Arc::clone(&on_other_env));
2732            cache.assign_program(&env, program_id, 100, Arc::clone(&on_execution_env));
2733        } else {
2734            cache.assign_program(&env, program_id, 100, Arc::clone(&on_execution_env));
2735            cache.assign_program(&other_env, program_id, 100, Arc::clone(&on_other_env));
2736        }
2737
2738        // Each is assigned under its own environment, so whichever came
2739        // first sits first.
2740        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
2741        assert_eq!(slot_versions.len(), 2);
2742        let (first, second) = if other_env_first {
2743            (&on_other_env, &on_execution_env)
2744        } else {
2745            (&on_execution_env, &on_other_env)
2746        };
2747        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), first));
2748        assert!(Arc::ptr_eq(slot_versions.get(1).unwrap(), second));
2749
2750        // No matter the ordering, the one on the environment which is asked
2751        // for is the one returned.
2752        let mut search_for = vec![ProgramToLoad {
2753            program_id: &program_id,
2754            loader: ProgramCacheEntryOwner::LoaderV3,
2755            deployment_slot: 100,
2756            last_modification_slot: 0,
2757        }];
2758        let mut extracted = ProgramCacheForTxBatch::new(200);
2759        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2760        assert!(Arc::ptr_eq(
2761            extracted.entries.get(&program_id).unwrap(),
2762            &on_execution_env
2763        ));
2764
2765        // And asking for the other environment reaches the other entry.
2766        let mut search_for = vec![ProgramToLoad {
2767            program_id: &program_id,
2768            loader: ProgramCacheEntryOwner::LoaderV3,
2769            deployment_slot: 100,
2770            last_modification_slot: 0,
2771        }];
2772        let mut extracted = ProgramCacheForTxBatch::new(200);
2773        cache.extract(&mut search_for, &mut extracted, &other_env, true, true);
2774        assert!(Arc::ptr_eq(
2775            extracted.entries.get(&program_id).unwrap(),
2776            &on_other_env
2777        ));
2778    }
2779
2780    #[test_matrix((false, true))]
2781    fn test_extract_usage_counter(increment_usage_counter: bool) {
2782        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2783        let env = get_mock_program_runtime_environment();
2784        let program_id = Pubkey::new_unique();
2785        let entry = new_test_entry_with_owner(
2786            100,
2787            ProgramCacheEntryOwner::LoaderV3,
2788            new_loaded_entry(env.clone()),
2789        );
2790        cache.assign_program(&env, program_id, 100, Arc::clone(&entry));
2791
2792        let mut search_for = vec![ProgramToLoad {
2793            program_id: &program_id,
2794            loader: ProgramCacheEntryOwner::LoaderV3,
2795            deployment_slot: 100,
2796            last_modification_slot: 0,
2797        }];
2798        let mut extracted = ProgramCacheForTxBatch::new(200);
2799        cache.extract(
2800            &mut search_for,
2801            &mut extracted,
2802            &env,
2803            increment_usage_counter,
2804            true,
2805        );
2806
2807        // The access slot moves either way, the usage counter only when asked.
2808        assert_eq!(entry.latest_access_slot.load(Ordering::Relaxed), 200);
2809        assert_eq!(
2810            entry.stats.uses.load(Ordering::Relaxed),
2811            u64::from(increment_usage_counter)
2812        );
2813    }
2814
2815    #[test]
2816    fn test_extract_usage_counter_delayed_visibility() {
2817        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2818        let env = get_mock_program_runtime_environment();
2819        let program_id = Pubkey::new_unique();
2820        let entry = new_test_entry_with_owner(
2821            100,
2822            ProgramCacheEntryOwner::LoaderV3,
2823            new_loaded_entry(env.clone()),
2824        );
2825        cache.assign_program(&env, program_id, 100, Arc::clone(&entry));
2826
2827        // Extract at the deployment slot itself, which is inside the delay
2828        // visibility window, so a `DelayVisibility` tombstone stands in for
2829        // the entry.
2830        let mut search_for = vec![ProgramToLoad {
2831            program_id: &program_id,
2832            loader: ProgramCacheEntryOwner::LoaderV3,
2833            deployment_slot: 100,
2834            last_modification_slot: 0,
2835        }];
2836        let mut extracted = ProgramCacheForTxBatch::new(100);
2837        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2838
2839        let tombstone = extracted.entries.get(&program_id).unwrap();
2840        assert!(matches!(
2841            tombstone.program,
2842            ProgramCacheEntryType::DelayVisibility
2843        ));
2844        assert!(!Arc::ptr_eq(tombstone, &entry));
2845
2846        // The access slot lands on the entry the tombstone stands in for. The
2847        // tombstone's own is never touched, and is dropped with the batch.
2848        assert_eq!(entry.latest_access_slot.load(Ordering::Relaxed), 100);
2849        assert_eq!(tombstone.latest_access_slot.load(Ordering::Relaxed), 0);
2850
2851        // The usage counter reaches the entry either way, through the
2852        // statistics the two share.
2853        assert!(Arc::ptr_eq(&tombstone.stats, &entry.stats));
2854        assert_eq!(entry.stats.uses.load(Ordering::Relaxed), 1);
2855    }
2856
2857    #[test_matrix((false, true))]
2858    fn test_extract_hits_and_misses(count_hits_and_misses: bool) {
2859        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2860        let env = get_mock_program_runtime_environment();
2861        let found = Pubkey::new_unique();
2862        let missing = Pubkey::new_unique();
2863        cache.assign_program(
2864            &env,
2865            found,
2866            100,
2867            new_test_entry_with_owner(
2868                100,
2869                ProgramCacheEntryOwner::LoaderV3,
2870                new_loaded_entry(env.clone()),
2871            ),
2872        );
2873
2874        let mut search_for = vec![
2875            ProgramToLoad {
2876                program_id: &found,
2877                loader: ProgramCacheEntryOwner::LoaderV3,
2878                deployment_slot: 100,
2879                last_modification_slot: 0,
2880            },
2881            ProgramToLoad {
2882                program_id: &missing,
2883                loader: ProgramCacheEntryOwner::LoaderV3,
2884                deployment_slot: 0,
2885                last_modification_slot: 0,
2886            },
2887        ];
2888        let mut extracted = ProgramCacheForTxBatch::new(200);
2889        cache.extract(
2890            &mut search_for,
2891            &mut extracted,
2892            &env,
2893            true,
2894            count_hits_and_misses,
2895        );
2896
2897        let expected = u64::from(count_hits_and_misses);
2898        assert_eq!(cache.stats.hits.load(Ordering::Relaxed), expected);
2899        assert_eq!(cache.stats.misses.load(Ordering::Relaxed), expected);
2900    }
2901
2902    #[test]
2903    fn test_extract_hits_count_only_this_call() {
2904        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2905        let env = get_mock_program_runtime_environment();
2906        let program_id = Pubkey::new_unique();
2907        cache.assign_program(
2908            &env,
2909            program_id,
2910            100,
2911            new_test_entry_with_owner(
2912                100,
2913                ProgramCacheEntryOwner::LoaderV3,
2914                new_loaded_entry(env.clone()),
2915            ),
2916        );
2917
2918        // Anything already in the batch, such as the builtins it is seeded
2919        // with.
2920        let mut extracted = ProgramCacheForTxBatch::new(200);
2921        extracted.replenish(Pubkey::new_unique(), new_test_builtin_entry(0));
2922
2923        // One entry is found, and only that one is counted. The entry seeded
2924        // above is still in the batch, but it was not found by this call.
2925        let mut search_for = vec![ProgramToLoad {
2926            program_id: &program_id,
2927            loader: ProgramCacheEntryOwner::LoaderV3,
2928            deployment_slot: 100,
2929            last_modification_slot: 0,
2930        }];
2931        cache.extract(&mut search_for, &mut extracted, &env, true, true);
2932        assert_eq!(extracted.entries.len(), 2);
2933        assert_eq!(cache.stats.hits.load(Ordering::Relaxed), 1);
2934    }
2935
2936    #[test]
2937    fn test_extract_cooperative_loading_task() {
2938        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
2939        let env = get_mock_program_runtime_environment();
2940        let program_ids = [Pubkey::new_unique(), Pubkey::new_unique()];
2941
2942        // Both are missing, but only the first one becomes a task.
2943        let mut search_for = program_ids
2944            .iter()
2945            .map(|program_id| ProgramToLoad {
2946                program_id,
2947                loader: ProgramCacheEntryOwner::LoaderV3,
2948                deployment_slot: 0,
2949                last_modification_slot: 0,
2950            })
2951            .collect();
2952        let mut extracted = ProgramCacheForTxBatch::new(100);
2953        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
2954        assert_eq!(search_for.len(), 2);
2955        assert_eq!(task, program_ids.first().copied());
2956        match &cache.index {
2957            IndexImplementation::V1 {
2958                loading_entries, ..
2959            } => {
2960                let loading_entries = loading_entries.lock().unwrap();
2961                assert_eq!(loading_entries.len(), 1);
2962                assert_eq!(
2963                    loading_entries.get(program_ids.first().unwrap()),
2964                    Some(&(100, thread::current().id()))
2965                );
2966            }
2967        }
2968
2969        // Asking again for the one which is already loading returns nothing.
2970        let mut search_for = vec![ProgramToLoad {
2971            program_id: program_ids.first().unwrap(),
2972            loader: ProgramCacheEntryOwner::LoaderV3,
2973            deployment_slot: 0,
2974            last_modification_slot: 0,
2975        }];
2976        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
2977        assert_eq!(search_for.len(), 1);
2978        assert_eq!(task, None);
2979
2980        // Submitting the finished task notifies whoever is waiting on one.
2981        let cookie = cache.loading_task_waiter.cookie();
2982        let loaded = new_test_entry_with_owner(
2983            50,
2984            ProgramCacheEntryOwner::LoaderV3,
2985            new_loaded_entry(env.clone()),
2986        );
2987        cache.finish_cooperative_loading_task(
2988            &env,
2989            100,
2990            *program_ids.first().unwrap(),
2991            50,
2992            Arc::clone(&loaded),
2993        );
2994        assert_ne!(cache.loading_task_waiter.wait(cookie), cookie);
2995
2996        // It is no longer loading, and extracting it now finds it.
2997        match &cache.index {
2998            IndexImplementation::V1 {
2999                loading_entries, ..
3000            } => assert!(loading_entries.lock().unwrap().is_empty()),
3001        }
3002        let mut search_for = vec![ProgramToLoad {
3003            program_id: program_ids.first().unwrap(),
3004            loader: ProgramCacheEntryOwner::LoaderV3,
3005            deployment_slot: 50,
3006            last_modification_slot: 0,
3007        }];
3008        let mut extracted = ProgramCacheForTxBatch::new(100);
3009        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
3010        assert!(search_for.is_empty());
3011        assert_eq!(task, None);
3012        assert!(Arc::ptr_eq(
3013            extracted.entries.get(program_ids.first().unwrap()).unwrap(),
3014            &loaded
3015        ));
3016    }
3017
3018    #[test]
3019    fn test_extract_cooperative_loading_task_ordering() {
3020        let (cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
3021        let env = get_mock_program_runtime_environment();
3022        let program_ids = [Pubkey::new_unique(), Pubkey::new_unique()];
3023        let mut extracted = ProgramCacheForTxBatch::new(100);
3024
3025        // The first one reached becomes the task.
3026        let mut search_for = program_ids
3027            .iter()
3028            .map(|program_id| ProgramToLoad {
3029                program_id,
3030                loader: ProgramCacheEntryOwner::LoaderV3,
3031                deployment_slot: 0,
3032                last_modification_slot: 0,
3033            })
3034            .collect();
3035        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
3036        assert_eq!(task, program_ids.first().copied());
3037
3038        // Asking again in the reverse order reaches the one which is not
3039        // loading yet first, so that one becomes a task of its own.
3040        let mut search_for = program_ids
3041            .iter()
3042            .rev()
3043            .map(|program_id| ProgramToLoad {
3044                program_id,
3045                loader: ProgramCacheEntryOwner::LoaderV3,
3046                deployment_slot: 0,
3047                last_modification_slot: 0,
3048            })
3049            .collect();
3050        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
3051        assert_eq!(task, program_ids.get(1).copied());
3052
3053        // Both are loading now, by this thread and for this slot.
3054        match &cache.index {
3055            IndexImplementation::V1 {
3056                loading_entries, ..
3057            } => {
3058                let loading_entries = loading_entries.lock().unwrap();
3059                assert_eq!(loading_entries.len(), 2);
3060                for program_id in &program_ids {
3061                    assert_eq!(
3062                        loading_entries.get(program_id),
3063                        Some(&(100, thread::current().id()))
3064                    );
3065                }
3066            }
3067        }
3068
3069        // Neither of them can become a task again.
3070        let mut search_for = program_ids
3071            .iter()
3072            .map(|program_id| ProgramToLoad {
3073                program_id,
3074                loader: ProgramCacheEntryOwner::LoaderV3,
3075                deployment_slot: 0,
3076                last_modification_slot: 0,
3077            })
3078            .collect();
3079        let task = cache.extract(&mut search_for, &mut extracted, &env, true, true);
3080        assert_eq!(search_for.len(), 2);
3081        assert_eq!(task, None);
3082    }
3083
3084    #[test]
3085    fn test_extract_entry_not_in_same_branch() {
3086        // Fork graph created for the test
3087        //                0
3088        //              /   \
3089        //            50     100
3090        //             |
3091        //            200
3092        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
3093        let mut fork_graph = TestForkGraphSpecific::default();
3094        fork_graph.insert_fork(&[0, 50, 200]);
3095        fork_graph.insert_fork(&[0, 100]);
3096        let fork_graph = Arc::new(RwLock::new(fork_graph));
3097        cache.set_fork_graph(Arc::downgrade(&fork_graph));
3098
3099        let env = get_mock_program_runtime_environment();
3100        let program_id = Pubkey::new_unique();
3101        let on_other_fork = new_test_entry_with_owner(
3102            100,
3103            ProgramCacheEntryOwner::LoaderV3,
3104            new_loaded_entry(env.clone()),
3105        );
3106        cache.assign_program(&env, program_id, 100, Arc::clone(&on_other_fork));
3107
3108        // The only entry was deployed on a fork the batch is not on, which is
3109        // still evaluated *in addition to* the exact deployment slot matching.
3110        //
3111        // Once fork-tracking is removed from `extract`, `deployment_slot` is
3112        // assumed to be the slot the caller's account state reports, so naming
3113        // 100 is what places the entry here.
3114        //
3115        // Until then, it cannot be resolved since fork tracking determines it
3116        // to be on another fork.
3117        let mut search_for = vec![ProgramToLoad {
3118            program_id: &program_id,
3119            loader: ProgramCacheEntryOwner::LoaderV3,
3120            deployment_slot: 100,
3121            last_modification_slot: 0,
3122        }];
3123        let mut extracted = ProgramCacheForTxBatch::new(200);
3124        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3125        assert_eq!(search_for.len(), 1);
3126        assert!(extracted.entries.is_empty());
3127
3128        // An older deployment, on the fork the batch is on.
3129        let on_same_fork = new_test_entry_with_owner(
3130            50,
3131            ProgramCacheEntryOwner::LoaderV3,
3132            new_loaded_entry(env.clone()),
3133        );
3134        cache.assign_program(&env, program_id, 50, Arc::clone(&on_same_fork));
3135
3136        // Here the cache has the one at 50 followed by the one at 100.
3137        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
3138        assert_eq!(slot_versions.len(), 2);
3139        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &on_same_fork));
3140        assert!(Arc::ptr_eq(slot_versions.get(1).unwrap(), &on_other_fork));
3141
3142        // Asking for 50 still reaches the entry at 50, whichever fork the
3143        // one above it is on.
3144        let mut search_for = vec![ProgramToLoad {
3145            program_id: &program_id,
3146            loader: ProgramCacheEntryOwner::LoaderV3,
3147            deployment_slot: 50,
3148            last_modification_slot: 0,
3149        }];
3150        let mut extracted = ProgramCacheForTxBatch::new(200);
3151        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3152        assert!(search_for.is_empty());
3153        assert!(Arc::ptr_eq(
3154            extracted.entries.get(&program_id).unwrap(),
3155            &on_same_fork
3156        ));
3157    }
3158
3159    #[test]
3160    fn test_extract_deployment_slot_mismatch() {
3161        // We keep the cache's `latest_root_slot` at 0 and deploy a loaded
3162        // program entry for slot 100 to avoid running into the infamous
3163        // `entry.deployment_slot <= self.latest_root_slot` check.
3164        //
3165        // As such, the `entry_in_same_branch` conditional depends exclusively
3166        // on the fork graph relationship, which we set to `Ancestor` here.
3167        //
3168        // Unlike the mismatched owner test above, a mismatched deployment slot
3169        // is only a genuine miss when the targeted `deployment_slot`
3170        // is too new.
3171        //
3172        // So, we produce a scenario where `entry_in_same_branch`,
3173        // `entry_is_effective` and `matches_environment` all evaluate to
3174        // `true`, finally trapping and breaking out on
3175        // `entry.deployment_slot < program_to_load.deployment_slot`.
3176        let (mut cache, _fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
3177        assert_eq!(cache.latest_root_slot, 0);
3178        let env = get_mock_program_runtime_environment();
3179        let program_id = Pubkey::new_unique();
3180        let deployed_at_100 = new_test_entry_with_owner(
3181            100,
3182            ProgramCacheEntryOwner::LoaderV3,
3183            new_loaded_entry(env.clone()),
3184        );
3185        cache.assign_program(&env, program_id, 100, Arc::clone(&deployed_at_100));
3186
3187        // The only entry is in the same branch, effective and on the right
3188        // environment, but it is older than the search demands.
3189        // Nothing is extracted. The caller must reload.
3190        let mut search_for = vec![ProgramToLoad {
3191            program_id: &program_id,
3192            loader: ProgramCacheEntryOwner::LoaderV3,
3193            deployment_slot: 150,
3194            last_modification_slot: 0,
3195        }];
3196        let mut extracted = ProgramCacheForTxBatch::new(200);
3197        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3198        assert_eq!(search_for.len(), 1);
3199        assert!(extracted.entries.is_empty());
3200
3201        // A redeployment at the slot the search asks for.
3202        let deployed_at_150 = new_test_entry_with_owner(
3203            150,
3204            ProgramCacheEntryOwner::LoaderV3,
3205            new_loaded_entry(env.clone()),
3206        );
3207        cache.assign_program(&env, program_id, 150, Arc::clone(&deployed_at_150));
3208
3209        // Here the cache has the original entry at 100 followed by the one at
3210        // 150.
3211        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
3212        assert_eq!(slot_versions.len(), 2);
3213        assert!(Arc::ptr_eq(
3214            slot_versions.first().unwrap(),
3215            &deployed_at_100
3216        ));
3217        assert!(Arc::ptr_eq(slot_versions.get(1).unwrap(), &deployed_at_150));
3218
3219        // Which is reached first, and is not older than the search demands.
3220        let mut search_for = vec![ProgramToLoad {
3221            program_id: &program_id,
3222            loader: ProgramCacheEntryOwner::LoaderV3,
3223            deployment_slot: 150,
3224            last_modification_slot: 0,
3225        }];
3226        let mut extracted = ProgramCacheForTxBatch::new(200);
3227        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3228        assert!(search_for.is_empty());
3229        assert!(Arc::ptr_eq(
3230            extracted.entries.get(&program_id).unwrap(),
3231            &deployed_at_150
3232        ));
3233    }
3234
3235    #[test]
3236    fn test_extract_older_entry_on_the_callers_fork() {
3237        // Fork graph created for the test
3238        //                0
3239        //              /   \
3240        //            50     150
3241        //             |
3242        //            200
3243        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
3244        let mut fork_graph = TestForkGraphSpecific::default();
3245        fork_graph.insert_fork(&[0, 50, 200]);
3246        fork_graph.insert_fork(&[0, 150]);
3247        let fork_graph = Arc::new(RwLock::new(fork_graph));
3248        cache.set_fork_graph(Arc::downgrade(&fork_graph));
3249
3250        let env = get_mock_program_runtime_environment();
3251        let program_id = Pubkey::new_unique();
3252        let on_other_fork = new_test_entry_with_owner(
3253            150,
3254            ProgramCacheEntryOwner::LoaderV3,
3255            new_loaded_entry(env.clone()),
3256        );
3257        let on_same_fork = new_test_entry_with_owner(
3258            50,
3259            ProgramCacheEntryOwner::LoaderV3,
3260            new_loaded_entry(env.clone()),
3261        );
3262        cache.assign_program(&env, program_id, 150, Arc::clone(&on_other_fork));
3263        cache.assign_program(&env, program_id, 50, Arc::clone(&on_same_fork));
3264
3265        // The account on this fork names 50, so the entry at 150 on the other
3266        // fork is not what is asked for and the one at 50 is served. There is
3267        // no fallback involved: the caller named the slot it wanted.
3268        let mut search_for = vec![ProgramToLoad {
3269            program_id: &program_id,
3270            loader: ProgramCacheEntryOwner::LoaderV3,
3271            deployment_slot: 50,
3272            last_modification_slot: 0,
3273        }];
3274        let mut extracted = ProgramCacheForTxBatch::new(200);
3275        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3276        assert!(Arc::ptr_eq(
3277            extracted.entries.get(&program_id).unwrap(),
3278            &on_same_fork
3279        ));
3280
3281        // Similar to the case in `test_extract_entry_not_in_same_branch`,
3282        // because fork tracking is still evaluated *in addition to* the exact
3283        // deployment slot matching, this entry can't be extracted by a batch
3284        // in slot 200.
3285        //
3286        // Once fork-tracking is removed from `extract`, `deployment_slot` is
3287        // assumed to be the slot the caller's account state reports, so naming
3288        // 150 is what places the entry here.
3289        //
3290        // Until then, it cannot be resolved since fork tracking determines it
3291        // to be on another fork.
3292        let mut search_for = vec![ProgramToLoad {
3293            program_id: &program_id,
3294            loader: ProgramCacheEntryOwner::LoaderV3,
3295            deployment_slot: 150,
3296            last_modification_slot: 0,
3297        }];
3298        let mut extracted = ProgramCacheForTxBatch::new(200);
3299        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3300        assert_eq!(search_for.len(), 1);
3301        assert!(extracted.entries.is_empty());
3302    }
3303
3304    #[test]
3305    fn test_extract_below_deployment_slot() {
3306        let (mut cache, fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
3307        let env = get_mock_program_runtime_environment();
3308        let program_id = Pubkey::new_unique();
3309        let entry = new_test_entry_with_owner(
3310            100,
3311            ProgramCacheEntryOwner::LoaderV3,
3312            new_loaded_entry(env.clone()),
3313        );
3314        cache.assign_program(&env, program_id, 100, Arc::clone(&entry));
3315
3316        // Rooting past the entry puts it in the branch without the fork graph
3317        // being consulted.
3318        cache.prune(200, None, &fork_graph.read().unwrap());
3319        assert_eq!(cache.latest_root_slot, 200);
3320
3321        // It survives that, since the fork graph said it was an `Ancestor`.
3322        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
3323        assert_eq!(slot_versions.len(), 1);
3324        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &entry));
3325
3326        // Overwrite the fork graph to use `BlockRelation::Unknown`, to show
3327        // only the `entry.deployment_slot <= self.latest_root_slot` check is
3328        // evaluated here.
3329        fork_graph.write().unwrap().relation = BlockRelation::Unknown;
3330
3331        // The batch is below the deployment slot, so the entry is neither
3332        // effective nor a delay visibility tombstone.
3333        let mut search_for = vec![ProgramToLoad {
3334            program_id: &program_id,
3335            loader: ProgramCacheEntryOwner::LoaderV3,
3336            deployment_slot: 0,
3337            last_modification_slot: 0,
3338        }];
3339        let mut extracted = ProgramCacheForTxBatch::new(50);
3340        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3341        assert_eq!(search_for.len(), 1);
3342        assert!(extracted.entries.is_empty());
3343    }
3344
3345    #[test]
3346    fn test_extract_entry_older_than_root() {
3347        // The same setup as above, extracted from a slot above the entry
3348        // rather than below it.
3349        let (mut cache, fork_graph) = new_test_cache_with_fork_graph(BlockRelation::Ancestor);
3350        let env = get_mock_program_runtime_environment();
3351        let program_id = Pubkey::new_unique();
3352        let entry = new_test_entry_with_owner(
3353            100,
3354            ProgramCacheEntryOwner::LoaderV3,
3355            new_loaded_entry(env.clone()),
3356        );
3357        cache.assign_program(&env, program_id, 100, Arc::clone(&entry));
3358
3359        // Rooting past the entry puts it in the branch without the fork graph
3360        // being consulted.
3361        cache.prune(200, None, &fork_graph.read().unwrap());
3362        assert_eq!(cache.latest_root_slot, 200);
3363
3364        // It survives that, since the fork graph said it was an `Ancestor`.
3365        let slot_versions = cache.get_slot_versions_for_tests(&program_id);
3366        assert_eq!(slot_versions.len(), 1);
3367        assert!(Arc::ptr_eq(slot_versions.first().unwrap(), &entry));
3368
3369        // Overwrite the fork graph to use `BlockRelation::Unknown`, to show
3370        // only the `entry.deployment_slot <= self.latest_root_slot` check is
3371        // evaluated here.
3372        fork_graph.write().unwrap().relation = BlockRelation::Unknown;
3373
3374        // That check alone is still enough to serve the entry, and there is
3375        // still no telling which fork it belongs to. What keeps it correct is
3376        // that only a caller whose own account names slot 100 can ask for it,
3377        // and such a caller has that deployment on its fork by definition.
3378        let mut search_for = vec![ProgramToLoad {
3379            program_id: &program_id,
3380            loader: ProgramCacheEntryOwner::LoaderV3,
3381            deployment_slot: 100,
3382            last_modification_slot: 0,
3383        }];
3384        let mut extracted = ProgramCacheForTxBatch::new(300);
3385        cache.extract(&mut search_for, &mut extracted, &env, true, true);
3386        assert!(search_for.is_empty());
3387        assert!(Arc::ptr_eq(
3388            extracted.entries.get(&program_id).unwrap(),
3389            &entry
3390        ));
3391    }
3392
3393    #[test]
3394    fn test_unloaded() {
3395        let mut cache = ProgramCache::<TestForkGraph>::new(0);
3396        let env = get_mock_program_runtime_environment();
3397        for program_cache_entry_type in [
3398            ProgramCacheEntryType::Closed,
3399            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
3400        ] {
3401            let entry = Arc::new(ProgramCacheEntry {
3402                program: program_cache_entry_type,
3403                account_owner: ProgramCacheEntryOwner::LoaderV2,
3404                deployment_slot: 0,
3405                stats: Arc::default(),
3406                latest_access_slot: AtomicU64::default(),
3407            });
3408            assert!(entry.to_unloaded().is_none());
3409
3410            // Check that unload_program_entry() does nothing for this entry
3411            let program_id = Pubkey::new_unique();
3412            cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone());
3413            cache.unload_program_entry(program_id, entry.deployment_slot, &entry);
3414            assert_eq!(cache.get_slot_versions_for_tests(&program_id).len(), 1);
3415            assert!(cache.stats.evictions.is_empty());
3416        }
3417
3418        let stats = ProgramStatistics {
3419            uses: 3.into(),
3420            ..Default::default()
3421        };
3422        let entry = new_test_entry_with_usage(1, stats);
3423        let unloaded_entry = entry.to_unloaded().unwrap();
3424        assert_eq!(unloaded_entry.deployment_slot, 1);
3425        assert_eq!(unloaded_entry.effective_slot(), 2);
3426        assert_eq!(unloaded_entry.latest_access_slot.load(Ordering::Relaxed), 1);
3427        assert_eq!(unloaded_entry.stats.uses.load(Ordering::Relaxed), 3);
3428
3429        // Check that unload_program_entry() does its work
3430        let program_id = Pubkey::new_unique();
3431        cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone());
3432        cache.unload_program_entry(program_id, entry.deployment_slot, &entry);
3433        assert!(cache.stats.evictions.contains_key(&program_id));
3434    }
3435
3436    #[test]
3437    fn test_fork_prune_find_first_ancestor() {
3438        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
3439        let env = get_mock_program_runtime_environment();
3440
3441        // Fork graph created for the test
3442        //                   0
3443        //                 /   \
3444        //                10    5
3445        //                |
3446        //                20
3447
3448        // Deploy program on slot 0, and slot 5.
3449        // Prune the fork that has slot 5. The cache should still have the program
3450        // deployed at slot 0.
3451        let mut fork_graph = TestForkGraphSpecific::default();
3452        fork_graph.insert_fork(&[0, 10, 20]);
3453        fork_graph.insert_fork(&[0, 5]);
3454        let fork_graph = Arc::new(RwLock::new(fork_graph));
3455        cache.set_fork_graph(Arc::downgrade(&fork_graph));
3456
3457        let program1 = Pubkey::new_unique();
3458        cache.assign_program(&env, program1, 0, new_test_entry(0));
3459        cache.assign_program(&env, program1, 5, new_test_entry(5));
3460
3461        cache.prune(10, None, &fork_graph.read().unwrap());
3462
3463        let keys = &[program1];
3464        let mut missing = get_entries_to_load(&cache, 20, keys);
3465        let mut extracted = ProgramCacheForTxBatch::new(20);
3466        cache.extract(&mut missing, &mut extracted, &env, true, true);
3467
3468        // The cache should have the program deployed at slot 0
3469        assert_eq!(
3470            extracted
3471                .find(&program1)
3472                .expect("Did not find the program")
3473                .deployment_slot,
3474            0
3475        );
3476    }
3477
3478    #[test]
3479    fn test_prune_by_deployment_slot() {
3480        let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
3481        let env = get_mock_program_runtime_environment();
3482
3483        // Fork graph created for the test
3484        //                   0
3485        //                 /   \
3486        //                10    5
3487        //                |
3488        //                20
3489
3490        // Deploy program on slot 0, and slot 5.
3491        // Prune the fork that has slot 5. The cache should still have the program
3492        // deployed at slot 0.
3493        let mut fork_graph = TestForkGraphSpecific::default();
3494        fork_graph.insert_fork(&[0, 10, 20]);
3495        fork_graph.insert_fork(&[0, 5, 6]);
3496        let fork_graph = Arc::new(RwLock::new(fork_graph));
3497        cache.set_fork_graph(Arc::downgrade(&fork_graph));
3498
3499        let program1 = Pubkey::new_unique();
3500        cache.assign_program(&env, program1, 0, new_test_entry(0));
3501        cache.assign_program(&env, program1, 5, new_test_entry(5));
3502
3503        let program2 = Pubkey::new_unique();
3504        cache.assign_program(&env, program2, 10, new_test_entry(10));
3505
3506        let keys = &[program1, program2];
3507        let mut missing = get_entries_to_load(&cache, 20, keys);
3508        let mut extracted = ProgramCacheForTxBatch::new(20);
3509        cache.extract(&mut missing, &mut extracted, &env, true, true);
3510        assert!(match_slot(&extracted, &program1, 0, 20));
3511        assert!(match_slot(&extracted, &program2, 10, 20));
3512
3513        let mut missing = get_entries_to_load(&cache, 6, keys);
3514        assert!(match_missing(&missing, &program2, false));
3515        let mut extracted = ProgramCacheForTxBatch::new(6);
3516        cache.extract(&mut missing, &mut extracted, &env, true, true);
3517        assert!(match_slot(&extracted, &program1, 5, 6));
3518
3519        // Pruning slot 5 will remove program1 entry deployed at slot 5.
3520        // On fork chaining from slot 5, the entry deployed at slot 0 will become visible.
3521        cache.prune_by_deployment_slot(5);
3522
3523        let mut missing = get_entries_to_load(&cache, 20, keys);
3524        let mut extracted = ProgramCacheForTxBatch::new(20);
3525        cache.extract(&mut missing, &mut extracted, &env, true, true);
3526        assert!(match_slot(&extracted, &program1, 0, 20));
3527        assert!(match_slot(&extracted, &program2, 10, 20));
3528
3529        let mut missing = get_entries_to_load(&cache, 6, keys);
3530        assert!(match_missing(&missing, &program2, false));
3531        let mut extracted = ProgramCacheForTxBatch::new(6);
3532        cache.extract(&mut missing, &mut extracted, &env, true, true);
3533        assert!(match_slot(&extracted, &program1, 0, 6));
3534
3535        // Pruning slot 10 will remove program2 entry deployed at slot 10.
3536        // As there is no other entry for program2, extract() will return it as missing.
3537        cache.prune_by_deployment_slot(10);
3538
3539        let mut missing = get_entries_to_load(&cache, 20, keys);
3540        assert!(match_missing(&missing, &program2, false));
3541        let mut extracted = ProgramCacheForTxBatch::new(20);
3542        cache.extract(&mut missing, &mut extracted, &env, true, true);
3543        assert!(match_slot(&extracted, &program1, 0, 20));
3544    }
3545}