Skip to main content

rialo_s_program_runtime/
loaded_programs.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3// This file is either (a) original to Subzero Labs, Inc. or (b) derived from the Anza codebase and modified by Subzero Labs, Inc.
4
5use std::{
6    collections::{hash_map::Entry, HashMap},
7    fmt::{Debug, Formatter},
8};
9
10use log::{debug, error, log_enabled, trace};
11use percentage::PercentageInteger;
12use polkavm::{ArcBytes, ProgramBlob, ProgramParts};
13use rialo_s_clock::Slot;
14use rialo_s_pubkey::Pubkey;
15use rialo_s_sdk_ids::native_loader;
16#[cfg(feature = "metrics")]
17use rialo_s_timings::ExecuteDetailsTimings;
18use rialo_s_type_overrides::{
19    rand::{thread_rng, Rng},
20    sync::{
21        atomic::{AtomicU64, Ordering},
22        Arc, Condvar, Mutex,
23    },
24    thread,
25};
26
27use crate::invoke_context::BuiltinFunctionWithContext;
28
29pub const MAX_LOADED_ENTRY_COUNT: usize = 512;
30pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1;
31
32/// Relationship between two fork IDs
33#[derive(Copy, Clone, Debug, PartialEq)]
34pub enum BlockRelation {
35    /// The slot is on the same fork and is an ancestor of the other slot
36    Ancestor,
37    /// The two slots are equal and are on the same fork
38    Equal,
39    /// The slot is on the same fork and is a descendant of the other slot
40    Descendant,
41    /// The slots are on two different forks and may have had a common ancestor at some point
42    Unrelated,
43    /// Either one or both of the slots are either older than the latest root, or are in future
44    Unknown,
45}
46
47/// Maps relationship between two slots.
48pub trait ForkGraph: Copy + Clone + Send + Sync {
49    /// Returns the BlockRelation of A to B
50    fn relationship(&self, a: Slot, b: Slot) -> BlockRelation;
51}
52
53/// The owner of a programs accounts, thus the loader of a program
54#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
55pub enum ProgramCacheEntryOwner {
56    #[default]
57    NativeLoader,
58    RiscVLoader,
59}
60
61impl TryFrom<&Pubkey> for ProgramCacheEntryOwner {
62    type Error = ();
63    fn try_from(loader_key: &Pubkey) -> Result<Self, ()> {
64        if native_loader::check_id(loader_key) {
65            Ok(ProgramCacheEntryOwner::NativeLoader)
66        } else if rialo_risc_v_loader_interface::check_id(loader_key) {
67            Ok(ProgramCacheEntryOwner::RiscVLoader)
68        } else {
69            Err(())
70        }
71    }
72}
73
74impl From<ProgramCacheEntryOwner> for Pubkey {
75    fn from(program_cache_entry_owner: ProgramCacheEntryOwner) -> Self {
76        match program_cache_entry_owner {
77            ProgramCacheEntryOwner::NativeLoader => native_loader::id(),
78            ProgramCacheEntryOwner::RiscVLoader => rialo_risc_v_loader_interface::id(),
79        }
80    }
81}
82
83/*
84    The possible ProgramCacheEntryType transitions:
85
86    DelayVisibility is special in that it is never stored in the cache.
87    It is only returned by ProgramCacheForTxBatch::find() when a Loaded entry
88    is encountered which is not effective yet.
89
90    Builtin re/deployment:
91    - Empty => Builtin in TransactionBatchProcessor::add_builtin
92    - Builtin => Builtin in TransactionBatchProcessor::add_builtin
93
94    Un/re/deployment (with delay and cooldown):
95    - Empty / Closed => Loaded on program deployment
96    - Loaded / FailedVerification => Loaded on program upgrade
97    - Loaded / FailedVerification => Closed on program close
98
99    Eviction and unloading (in the same slot):
100    - Unloaded => Loaded in ProgramCache::assign_program
101    - Loaded => Unloaded in ProgramCache::unload_program_entry
102
103    At epoch boundary (when feature set and environment changes):
104    - Loaded => FailedVerification in Bank::_new_from_parent
105    - FailedVerification => Loaded in Bank::_new_from_parent
106
107    Through pruning (when on orphan fork or overshadowed on the rooted fork):
108    - Closed / Unloaded / Loaded / Builtin => Empty in ProgramCache::prune
109*/
110
111/// Executable kind.
112pub enum ExecutableKind {
113    /// RISC-V.
114    RiscV(Vec<u8>),
115}
116
117/// Actual payload of [ProgramCacheEntry].
118#[derive(Default)]
119pub enum ProgramCacheEntryType {
120    /// Tombstone for programs which currently do not pass the verifier but could if the feature set changed.
121    FailedVerification,
122    /// Tombstone for programs that were either explicitly closed or never deployed.
123    ///
124    /// It's also used for accounts belonging to program loaders, that don't actually contain program code (e.g. buffer accounts for LoaderV3 programs).
125    #[default]
126    Closed,
127    /// Tombstone for programs which have recently been modified but the new version is not visible yet.
128    DelayVisibility,
129    /// Successfully verified but not currently compiled.
130    ///
131    /// It continues to track usage statistics even when the compiled executable of the program is evicted from memory.
132    Unloaded,
133    /// Verified and compiled program
134    Loaded(ExecutableKind),
135    /// A built-in program which is not stored on-chain but backed into and distributed with the validator
136    Builtin(BuiltinFunctionWithContext),
137}
138
139impl Debug for ProgramCacheEntryType {
140    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
141        match self {
142            ProgramCacheEntryType::FailedVerification => {
143                write!(f, "ProgramCacheEntryType::FailedVerification")
144            }
145            ProgramCacheEntryType::Closed => write!(f, "ProgramCacheEntryType::Closed"),
146            ProgramCacheEntryType::DelayVisibility => {
147                write!(f, "ProgramCacheEntryType::DelayVisibility")
148            }
149            ProgramCacheEntryType::Unloaded => write!(f, "ProgramCacheEntryType::Unloaded"),
150            ProgramCacheEntryType::Loaded(_) => write!(f, "ProgramCacheEntryType::Loaded"),
151            ProgramCacheEntryType::Builtin(_) => write!(f, "ProgramCacheEntryType::Builtin"),
152        }
153    }
154}
155
156/// Holds a program version at a specific address and on a specific slot / fork.
157///
158/// It contains the actual program in [ProgramCacheEntryType] and a bunch of meta-data.
159#[derive(Debug, Default)]
160pub struct ProgramCacheEntry {
161    /// The program of this entry
162    pub program: ProgramCacheEntryType,
163    /// The loader of this entry
164    pub account_owner: ProgramCacheEntryOwner,
165    /// Size of account that stores the program and program data
166    pub account_size: usize,
167    /// Slot in which the program was (re)deployed
168    pub deployment_slot: Slot,
169    /// Slot in which this entry will become active (can be in the future)
170    pub effective_slot: Slot,
171    /// How often this entry was used by a transaction
172    pub tx_usage_counter: AtomicU64,
173    /// How often this entry was used by an instruction
174    pub ix_usage_counter: AtomicU64,
175    /// Latest slot in which the entry was used
176    pub latest_access_slot: AtomicU64,
177}
178
179/// Global cache statistics for [ProgramCache].
180#[derive(Debug, Default)]
181pub struct ProgramCacheStats {
182    /// a program was already in the cache
183    pub hits: AtomicU64,
184    /// a program was not found and loaded instead
185    pub misses: AtomicU64,
186    /// a compiled executable was unloaded, counted per program id.
187    ///
188    /// Private, and reachable from outside this module only through
189    /// `total_evictions`, so this map's iteration order cannot escape into
190    /// a caller's output. The one order-sensitive read inside the module is
191    /// `sorted_eviction_report`, which sorts explicitly by count and feeds a
192    /// trace-level log line. This surface is not shared across validators
193    /// (see this crate's NORTHSTAR "Bundled control-plane surface").
194    #[allow(
195        clippy::disallowed_types,
196        reason = "private; reachable from outside this module only through total_evictions, and the one in-module iteration sorts explicitly by count for a trace-level log line"
197    )]
198    evictions: HashMap<Pubkey, u64>,
199    /// an unloaded program was loaded again (opposite of eviction)
200    pub reloads: AtomicU64,
201    /// a program was loaded or un/re/deployed
202    pub insertions: AtomicU64,
203    /// a program was loaded but can not be extracted on its own fork anymore
204    pub lost_insertions: AtomicU64,
205    /// a program which was already in the cache was reloaded by mistake
206    pub replacements: AtomicU64,
207    /// a program was only used once before being unloaded
208    pub one_hit_wonders: AtomicU64,
209    /// a program had no entries because all slot versions got pruned
210    pub empty_entries: AtomicU64,
211    /// water level of loaded entries currently cached
212    pub water_level: AtomicU64,
213}
214
215impl ProgramCacheStats {
216    pub fn reset(&mut self) {
217        *self = ProgramCacheStats::default();
218    }
219    /// Total evictions across every program id.
220    ///
221    /// This is the only read of `evictions` available outside this module.
222    /// Returning the sum rather than the map keeps the map's hash-seeded,
223    /// per-process iteration order from reaching any consumer.
224    pub fn total_evictions(&self) -> u64 {
225        self.evictions.values().sum()
226    }
227    pub fn log(&self) {
228        let hits = self.hits.load(Ordering::Relaxed);
229        let misses = self.misses.load(Ordering::Relaxed);
230        let evictions = self.total_evictions();
231        let reloads = self.reloads.load(Ordering::Relaxed);
232        let insertions = self.insertions.load(Ordering::Relaxed);
233        let lost_insertions = self.lost_insertions.load(Ordering::Relaxed);
234        let replacements = self.replacements.load(Ordering::Relaxed);
235        let one_hit_wonders = self.one_hit_wonders.load(Ordering::Relaxed);
236        let empty_entries = self.empty_entries.load(Ordering::Relaxed);
237        let water_level = self.water_level.load(Ordering::Relaxed);
238        debug!(
239            "Loaded Programs Cache Stats -- Hits: {}, Misses: {}, Evictions: {}, Reloads: {}, Insertions: {}, Lost-Insertions: {}, Replacements: {}, One-Hit-Wonders: {}, Empty: {}, Water-Level: {}",
240            hits, misses, evictions, reloads, insertions, lost_insertions, replacements, one_hit_wonders, empty_entries, water_level
241        );
242        if log_enabled!(log::Level::Trace) && !self.evictions.is_empty() {
243            let evictions = self
244                .sorted_eviction_report()
245                .into_iter()
246                .rev()
247                .map(|(program_id, evictions)| {
248                    format!("  {:<44}  {}", program_id.to_string(), evictions)
249                })
250                .collect::<Vec<_>>()
251                .join("\n");
252            trace!(
253                "Eviction Details:\n  {:<44}  {}\n{}",
254                "Program",
255                "Count",
256                evictions
257            );
258        }
259    }
260
261    /// Eviction counts sorted ascending by count. Ties are broken by
262    /// `evictions`' own (unordered) iteration order, which only affects a
263    /// trace-level log line and is not shared across validators.
264    fn sorted_eviction_report(&self) -> Vec<(Pubkey, u64)> {
265        let mut evictions: Vec<(Pubkey, u64)> =
266            self.evictions.iter().map(|(k, v)| (*k, *v)).collect();
267        evictions.sort_by_key(|e| e.1);
268        evictions
269    }
270}
271
272#[cfg(feature = "metrics")]
273/// Time measurements for loading a single [ProgramCacheEntry].
274#[derive(Debug, Default)]
275pub struct LoadProgramMetrics {
276    /// Program address, but as text
277    pub program_id: String,
278    /// Microseconds it took to `create_program_runtime_environment`
279    pub register_syscalls_us: u64,
280}
281
282#[cfg(feature = "metrics")]
283impl LoadProgramMetrics {
284    pub fn submit_datapoint(&self, timings: &mut ExecuteDetailsTimings) {
285        timings.create_executor_register_syscalls_us += self.register_syscalls_us;
286        datapoint_trace!(
287            "create_executor_trace",
288            ("program_id", self.program_id, String),
289            ("register_syscalls_us", self.register_syscalls_us, i64),
290        );
291    }
292}
293
294impl PartialEq for ProgramCacheEntry {
295    fn eq(&self, other: &Self) -> bool {
296        self.effective_slot == other.effective_slot
297            && self.deployment_slot == other.deployment_slot
298            && self.is_tombstone() == other.is_tombstone()
299    }
300}
301
302impl ProgramCacheEntry {
303    /// Creates a RISC-V new user program
304    pub fn new_risc_v(
305        loader_key: &Pubkey,
306        deployment_slot: Slot,
307        effective_slot: Slot,
308        elf_bytes: &[u8],
309        account_size: usize,
310    ) -> Result<Self, Box<dyn std::error::Error>> {
311        Self::new_internal_risc_v(
312            loader_key,
313            deployment_slot,
314            effective_slot,
315            elf_bytes,
316            account_size,
317            false, /* reloading */
318        )
319    }
320
321    /// Reloads a RISC-V user program, *without* running the verifier.
322    ///
323    /// # Safety
324    ///
325    /// This method is unsafe since it assumes that the program has already been verified. Should
326    /// only be called when the program was previously verified and loaded in the cache, but was
327    /// unloaded due to inactivity. It should also be checked that the `program_runtime_environment`
328    /// hasn't changed since it was unloaded.
329    pub unsafe fn reload_risc_v(
330        loader_key: &Pubkey,
331        deployment_slot: Slot,
332        effective_slot: Slot,
333        elf_bytes: &[u8],
334        account_size: usize,
335    ) -> Result<Self, Box<dyn std::error::Error>> {
336        Self::new_internal_risc_v(
337            loader_key,
338            deployment_slot,
339            effective_slot,
340            elf_bytes,
341            account_size,
342            true, /* reloading */
343        )
344    }
345
346    fn new_internal_risc_v(
347        loader_key: &Pubkey,
348        deployment_slot: Slot,
349        effective_slot: Slot,
350        elf_bytes: &[u8],
351        account_size: usize,
352        reloading: bool,
353    ) -> Result<Self, Box<dyn std::error::Error>> {
354        // verify that `elf_bytes` is a valid polkavm program
355        //
356        // the program is considered valid if it:
357        //  * parses into a valid polkavm program blob
358        //  * is a 64-bit program
359        //  * has stack size <= 0x40000
360        //  * exports an entrypoint
361        if !reloading {
362            const MAX_STACK_SIZE: u32 = 262144;
363
364            let program = Into::<ArcBytes>::into(elf_bytes);
365
366            let program_parts = ProgramParts::from_bytes(program.clone())
367                .map_err(|error| format!("failed to parse program: {error:?}"))?;
368
369            if !program_parts.is_64_bit {
370                return Err(String::from("program is not a 64-bit program").into());
371            }
372
373            if program_parts.stack_size > MAX_STACK_SIZE {
374                return Err(String::from("program stack is too large").into());
375            }
376
377            let blob = ProgramBlob::parse(program)
378                .map_err(|error| format!("failed to parse program into program blob: {error:?}"))?;
379
380            if !blob
381                .exports()
382                .any(|entry| entry.symbol().as_bytes() == b"entrypoint")
383            {
384                return Err(String::from("program doesn't have an entrypoint").into());
385            }
386        }
387
388        Ok(Self {
389            deployment_slot,
390            account_owner: ProgramCacheEntryOwner::try_from(loader_key).unwrap(),
391            account_size,
392            effective_slot,
393            tx_usage_counter: AtomicU64::new(0),
394            program: ProgramCacheEntryType::Loaded(ExecutableKind::RiscV(elf_bytes.to_vec())),
395            ix_usage_counter: AtomicU64::new(0),
396            latest_access_slot: AtomicU64::new(0),
397        })
398    }
399
400    pub fn to_unloaded(&self) -> Option<Self> {
401        match &self.program {
402            ProgramCacheEntryType::Loaded(_) => {}
403            ProgramCacheEntryType::FailedVerification
404            | ProgramCacheEntryType::Closed
405            | ProgramCacheEntryType::DelayVisibility
406            | ProgramCacheEntryType::Unloaded
407            | ProgramCacheEntryType::Builtin(_) => {
408                return None;
409            }
410        }
411        Some(Self {
412            program: ProgramCacheEntryType::Unloaded,
413            account_owner: self.account_owner,
414            account_size: self.account_size,
415            deployment_slot: self.deployment_slot,
416            effective_slot: self.effective_slot,
417            tx_usage_counter: AtomicU64::new(self.tx_usage_counter.load(Ordering::Relaxed)),
418            ix_usage_counter: AtomicU64::new(self.ix_usage_counter.load(Ordering::Relaxed)),
419            latest_access_slot: AtomicU64::new(self.latest_access_slot.load(Ordering::Relaxed)),
420        })
421    }
422
423    /// Creates a new built-in program
424    pub fn new_builtin(
425        deployment_slot: Slot,
426        account_size: usize,
427        builtin_function: BuiltinFunctionWithContext,
428    ) -> Self {
429        Self {
430            deployment_slot,
431            account_owner: ProgramCacheEntryOwner::NativeLoader,
432            account_size,
433            effective_slot: deployment_slot,
434            tx_usage_counter: AtomicU64::new(0),
435            program: ProgramCacheEntryType::Builtin(builtin_function),
436            ix_usage_counter: AtomicU64::new(0),
437            latest_access_slot: AtomicU64::new(0),
438        }
439    }
440
441    pub fn new_tombstone(
442        slot: Slot,
443        account_owner: ProgramCacheEntryOwner,
444        reason: ProgramCacheEntryType,
445    ) -> Self {
446        let tombstone = Self {
447            program: reason,
448            account_owner,
449            account_size: 0,
450            deployment_slot: slot,
451            effective_slot: slot,
452            tx_usage_counter: AtomicU64::default(),
453            ix_usage_counter: AtomicU64::default(),
454            latest_access_slot: AtomicU64::new(0),
455        };
456        debug_assert!(tombstone.is_tombstone());
457        tombstone
458    }
459
460    pub fn is_tombstone(&self) -> bool {
461        matches!(
462            self.program,
463            ProgramCacheEntryType::FailedVerification
464                | ProgramCacheEntryType::Closed
465                | ProgramCacheEntryType::DelayVisibility
466        )
467    }
468
469    fn is_implicit_delay_visibility_tombstone(&self, slot: Slot) -> bool {
470        !matches!(self.program, ProgramCacheEntryType::Builtin(_))
471            && self.effective_slot.saturating_sub(self.deployment_slot)
472                == DELAY_VISIBILITY_SLOT_OFFSET
473            && slot >= self.deployment_slot
474            && slot < self.effective_slot
475    }
476
477    pub fn update_access_slot(&self, slot: Slot) {
478        let _ = self.latest_access_slot.fetch_max(slot, Ordering::Relaxed);
479    }
480
481    pub fn decayed_usage_counter(&self, now: Slot) -> u64 {
482        let last_access = self.latest_access_slot.load(Ordering::Relaxed);
483        // Shifting the u64 value for more than 63 will cause an overflow.
484        let decaying_for = std::cmp::min(63, now.saturating_sub(last_access));
485        self.tx_usage_counter.load(Ordering::Relaxed) >> decaying_for
486    }
487
488    pub fn account_owner(&self) -> Pubkey {
489        self.account_owner.into()
490    }
491}
492
493#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
494pub struct LoadingTaskCookie(u64);
495
496impl LoadingTaskCookie {
497    fn new() -> Self {
498        Self(0)
499    }
500
501    fn update(&mut self) {
502        let LoadingTaskCookie(cookie) = self;
503        *cookie = cookie.wrapping_add(1);
504    }
505}
506
507/// Suspends the thread in case no cooprative loading task was assigned
508#[derive(Debug, Default)]
509pub struct LoadingTaskWaiter {
510    cookie: Mutex<LoadingTaskCookie>,
511    cond: Condvar,
512}
513
514impl LoadingTaskWaiter {
515    pub fn new() -> Self {
516        Self {
517            cookie: Mutex::new(LoadingTaskCookie::new()),
518            cond: Condvar::new(),
519        }
520    }
521
522    pub fn cookie(&self) -> LoadingTaskCookie {
523        *self.cookie.lock().unwrap()
524    }
525
526    pub fn notify(&self) {
527        let mut cookie = self.cookie.lock().unwrap();
528        cookie.update();
529        self.cond.notify_all();
530    }
531
532    pub fn wait(&self, cookie: LoadingTaskCookie) -> LoadingTaskCookie {
533        let cookie_guard = self.cookie.lock().unwrap();
534        *self
535            .cond
536            .wait_while(cookie_guard, |current_cookie| *current_cookie == cookie)
537            .unwrap()
538    }
539}
540
541#[derive(Debug)]
542enum IndexImplementation {
543    /// Fork-graph aware index implementation
544    V1 {
545        /// A two level index:
546        ///
547        /// - the first level is for the address at which programs are deployed
548        /// - the second level for the slot (and thus also fork), sorted by slot number.
549        ///
550        /// Looked up by program id by `extract` during a batch's cooperative
551        /// loading rounds; the per-instruction dispatch lookups go through
552        /// `ProgramCacheForTxBatch::find` / `ProgramCacheForTx::find`, not this
553        /// map.
554        ///
555        /// Iterated by `get_flattened_entries`, which feeds the local,
556        /// per-validator cache-eviction heuristics; by
557        /// `get_flattened_entries_for_tests`, which is also live under
558        /// `dev-context-only-utils`; and by the derived `Debug`, reached through
559        /// `ProgramCache`'s own `Debug` impl. `evict_using_2s_random_selection`
560        /// is the production eviction path and picks its victims at random;
561        /// `sort_and_unload` is a stable sort by usage counter with no
562        /// production caller. Every one of those destinations is local
563        /// per-validator cache state or a diagnostic — none is committed output.
564        #[allow(
565            clippy::disallowed_types,
566            reason = "keyed lookup during batch program loading; iterated only for local cache eviction and diagnostics, never for committed output"
567        )]
568        entries: HashMap<Pubkey, Vec<Arc<ProgramCacheEntry>>>,
569        /// The entries that are getting loaded and have not yet finished loading.
570        ///
571        /// The key is the program address, the value is a tuple of the slot in which the program is
572        /// being loaded and the thread ID doing the load.
573        ///
574        /// It is possible that multiple TX batches from different slots need different versions of a
575        /// program. The deployment slot of a program is only known after load tho,
576        /// so all loads for a given program key are serialized.
577        #[allow(
578            clippy::disallowed_types,
579            reason = "cooperative-loading coordination keyed by program id; the only iteration is by this enum's derived Debug, which does not depend on order"
580        )]
581        loading_entries: Mutex<HashMap<Pubkey, (Slot, thread::ThreadId)>>,
582    },
583}
584
585/// This structure is the global cache of loaded, verified and compiled programs.
586///
587/// It ...
588/// - is validator global and fork graph aware, so it can optimize the commonalities across banks.
589/// - handles the visibility rules of un/re/deployments.
590/// - stores the usage statistics and verification status of each program.
591/// - is elastic and uses a probabilistic eviction stragety based on the usage statistics.
592/// - also keeps the compiled executables around, but only for the most used programs.
593/// - supports various kinds of tombstones to avoid loading programs which can not be loaded.
594/// - cleans up entries on orphan branches when the block store is rerooted.
595/// - supports the cache preparation phase before feature activations which can change cached programs.
596/// - manages the environments of the programs and upcoming environments for the next epoch.
597/// - allows for cooperative loading of TX batches which hit the same missing programs simultaneously.
598/// - enforces that all programs used in a batch are eagerly loaded ahead of execution.
599/// - is not persisted to disk or a snapshot, so it needs to cold start and warm up first.
600pub struct ProgramCache {
601    /// Index of the cached entries and cooperative loading tasks
602    index: IndexImplementation,
603    /// Statistics counters
604    pub stats: ProgramCacheStats,
605    /// Coordinates TX batches waiting for others to complete their task during cooperative loading
606    pub loading_task_waiter: Arc<LoadingTaskWaiter>,
607}
608
609impl Debug for ProgramCache {
610    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
611        f.debug_struct("ProgramCache")
612            .field("stats", &self.stats)
613            .field("index", &self.index)
614            .finish()
615    }
616}
617
618/// Program cache for a transaction.
619///
620/// This is a per-transaction view of the program cache of the entire transaction batch.
621///
622/// Sole purpose is performance improvement as the cache holds a reference to the batch's cache.
623///
624/// `entries`/`modified_entries` stay `HashMap<Pubkey, _>`: both are read
625/// only by key (`find`) or drained whole (`drain_modified_entries`),
626/// never iterated in this crate. The drained map's type is shared with
627/// `svm-execution::TransactionExecutionResult::programs_modified_by_tx`,
628/// which is later folded into `ProgramCache` — see `ProgramCache::merge`
629/// for why that fold does not depend on iteration order.
630pub struct ProgramCacheForTx<'a> {
631    /// Pubkey is the address of a program.
632    /// ProgramCacheEntry is the corresponding program entry valid for the slot in which a transaction is being executed.
633    #[allow(
634        clippy::disallowed_types,
635        reason = "looked up by key only, see struct doc"
636    )]
637    entries: &'a HashMap<Pubkey, Arc<ProgramCacheEntry>>,
638    /// Program entries modified during the transaction batch.
639    #[allow(
640        clippy::disallowed_types,
641        reason = "inserted/drained by key only, see struct doc"
642    )]
643    modified_entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
644    slot: Slot,
645    pub hit_max_limit: bool,
646    pub loaded_missing: bool,
647    pub merged_modified: bool,
648}
649
650impl<'a> ProgramCacheForTx<'a> {
651    #[allow(
652        clippy::disallowed_types,
653        reason = "constructs modified_entries, see struct doc"
654    )]
655    pub fn from_cache(cache: &'a ProgramCacheForTxBatch) -> Self {
656        Self {
657            entries: &cache.entries,
658            modified_entries: HashMap::new(),
659            slot: cache.slot,
660            hit_max_limit: cache.hit_max_limit,
661            loaded_missing: cache.loaded_missing,
662            merged_modified: cache.merged_modified,
663        }
664    }
665
666    /// Store an entry in `modified_entries` for a program modified during the
667    /// transaction batch.
668    pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc<ProgramCacheEntry>) {
669        self.modified_entries.insert(key, entry);
670    }
671
672    /// Drain the program cache's modified entries, returning the owned
673    /// collection.
674    #[allow(
675        clippy::disallowed_types,
676        reason = "return type shared with svm-execution, see struct doc"
677    )]
678    pub fn drain_modified_entries(&mut self) -> HashMap<Pubkey, Arc<ProgramCacheEntry>> {
679        std::mem::take(&mut self.modified_entries)
680    }
681
682    pub fn find(&self, key: &Pubkey) -> Option<Arc<ProgramCacheEntry>> {
683        // First lookup the cache of the programs modified by the current
684        // transaction. If not found, lookup the cache of the cache of the
685        // programs that are loaded for the transaction batch.
686        self.modified_entries
687            .get(key)
688            .or(self.entries.get(key))
689            .map(|entry| {
690                if entry.is_implicit_delay_visibility_tombstone(self.slot) {
691                    // Found a program entry on the current fork, but it's not effective
692                    // yet. It indicates that the program has delayed visibility. Return
693                    // the tombstone to reflect that.
694                    Arc::new(ProgramCacheEntry::new_tombstone(
695                        entry.deployment_slot,
696                        entry.account_owner,
697                        ProgramCacheEntryType::DelayVisibility,
698                    ))
699                } else {
700                    entry.clone()
701                }
702            })
703    }
704
705    pub fn slot(&self) -> Slot {
706        self.slot
707    }
708
709    pub fn set_slot_for_tests(&mut self, slot: Slot) {
710        self.slot = slot;
711    }
712
713    pub fn is_empty(&self) -> bool {
714        self.entries.is_empty()
715    }
716}
717
718/// Local view into [ProgramCache] which was extracted for a specific TX batch.
719///
720/// This isolation enables the global [ProgramCache] to continue to evolve (e.g. evictions),
721/// while the TX batch is guaranteed it will continue to find all the programs it requires.
722/// For program management instructions this also buffers them before they are merged back into the global [ProgramCache].
723#[derive(Clone, Debug, Default)]
724pub struct ProgramCacheForTxBatch {
725    /// Pubkey is the address of a program.
726    /// ProgramCacheEntry is the corresponding program entry valid for the slot in which a transaction is being executed.
727    #[allow(
728        clippy::disallowed_types,
729        reason = "looked up by key only; the only iteration is by this struct's derived Clone and Debug, neither of which depends on order"
730    )]
731    entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
732    /// Program entries modified during the transaction batch.
733    #[allow(
734        clippy::disallowed_types,
735        reason = "inserted/drained by key only; the only iteration is by this struct's derived Clone and Debug, neither of which depends on order"
736    )]
737    modified_entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
738    slot: Slot,
739    pub hit_max_limit: bool,
740    pub loaded_missing: bool,
741    pub merged_modified: bool,
742}
743
744impl ProgramCacheForTxBatch {
745    #[allow(
746        clippy::disallowed_types,
747        reason = "constructs the fields declared above"
748    )]
749    pub fn new(slot: Slot) -> Self {
750        Self {
751            entries: HashMap::new(),
752            modified_entries: HashMap::new(),
753            slot,
754            hit_max_limit: false,
755            loaded_missing: false,
756            merged_modified: false,
757        }
758    }
759
760    /// Refill the cache with a single entry. It's typically called during transaction loading, and
761    /// transaction processing (for program management instructions).
762    /// It replaces the existing entry (if any) with the provided entry. The return value contains
763    /// `true` if an entry existed.
764    /// The function also returns the newly inserted value.
765    pub fn replenish(
766        &mut self,
767        key: Pubkey,
768        entry: Arc<ProgramCacheEntry>,
769    ) -> (bool, Arc<ProgramCacheEntry>) {
770        (self.entries.insert(key, entry.clone()).is_some(), entry)
771    }
772
773    /// Store an entry in `modified_entries` for a program modified during the
774    /// transaction batch.
775    pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc<ProgramCacheEntry>) {
776        self.modified_entries.insert(key, entry);
777    }
778
779    /// Drain the program cache's modified entries, returning the owned
780    /// collection.
781    #[allow(
782        clippy::disallowed_types,
783        reason = "return type shared with svm-execution's TransactionExecutionResult"
784    )]
785    pub fn drain_modified_entries(&mut self) -> HashMap<Pubkey, Arc<ProgramCacheEntry>> {
786        std::mem::take(&mut self.modified_entries)
787    }
788
789    pub fn find(&self, key: &Pubkey) -> Option<Arc<ProgramCacheEntry>> {
790        // First lookup the cache of the programs modified by the current
791        // transaction. If not found, lookup the cache of the cache of the
792        // programs that are loaded for the transaction batch.
793        self.modified_entries
794            .get(key)
795            .or(self.entries.get(key))
796            .map(|entry| {
797                if entry.is_implicit_delay_visibility_tombstone(self.slot) {
798                    // Found a program entry on the current fork, but it's not effective
799                    // yet. It indicates that the program has delayed visibility. Return
800                    // the tombstone to reflect that.
801                    Arc::new(ProgramCacheEntry::new_tombstone(
802                        entry.deployment_slot,
803                        entry.account_owner,
804                        ProgramCacheEntryType::DelayVisibility,
805                    ))
806                } else {
807                    entry.clone()
808                }
809            })
810    }
811
812    pub fn slot(&self) -> Slot {
813        self.slot
814    }
815
816    #[cfg(any(test, feature = "dev-context-only-utils"))]
817    pub fn set_slot_for_tests(&mut self, slot: Slot) {
818        self.slot = slot;
819    }
820
821    /// Batch-local counterpart of `ProgramCache::merge`, which states the
822    /// fold invariant this shares.
823    #[allow(
824        clippy::disallowed_types,
825        reason = "parameter type shared with svm-execution's TransactionExecutionResult"
826    )]
827    pub fn merge(&mut self, modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>) {
828        modified_entries.iter().for_each(|(key, entry)| {
829            self.merged_modified = true;
830            self.replenish(*key, entry.clone());
831        })
832    }
833
834    pub fn is_empty(&self) -> bool {
835        self.entries.is_empty()
836    }
837}
838
839pub enum ProgramCacheMatchCriteria {
840    DeployedOnOrAfterSlot(Slot),
841    Tombstone,
842    NoCriteria,
843}
844
845impl Default for ProgramCache {
846    fn default() -> Self {
847        Self::new()
848    }
849}
850
851impl ProgramCache {
852    #[allow(
853        clippy::disallowed_types,
854        reason = "constructs the index fields declared above, both retained as HashMap"
855    )]
856    pub fn new() -> Self {
857        Self {
858            index: IndexImplementation::V1 {
859                entries: HashMap::new(),
860                loading_entries: Mutex::new(HashMap::new()),
861            },
862            stats: ProgramCacheStats::default(),
863            loading_task_waiter: Arc::new(LoadingTaskWaiter::default()),
864        }
865    }
866
867    /// Insert a single entry. It's typically called during transaction loading,
868    /// when the cache doesn't contain the entry corresponding to program `key`.
869    pub fn assign_program(&mut self, key: Pubkey, entry: Arc<ProgramCacheEntry>) -> bool {
870        debug_assert!(!matches!(
871            &entry.program,
872            ProgramCacheEntryType::DelayVisibility
873        ));
874        match &mut self.index {
875            IndexImplementation::V1 { entries, .. } => {
876                let slot_versions = &mut entries.entry(key).or_default();
877                match slot_versions.binary_search_by(|at| {
878                    at.effective_slot
879                        .cmp(&entry.effective_slot)
880                        .then(at.deployment_slot.cmp(&entry.deployment_slot))
881                }) {
882                    Ok(index) => {
883                        let existing = slot_versions.get_mut(index).unwrap();
884                        match (&existing.program, &entry.program) {
885                            (
886                                ProgramCacheEntryType::Builtin(_),
887                                ProgramCacheEntryType::Builtin(_),
888                            )
889                            | (ProgramCacheEntryType::Unloaded, ProgramCacheEntryType::Loaded(_)) =>
890                                {}
891                            _ => {
892                                // Something is wrong, I can feel it ...
893                                error!("ProgramCache::assign_program() failed key={:?} existing={:?} entry={:?}", key, slot_versions, entry);
894                                debug_assert!(false, "Unexpected replacement of an entry");
895                                self.stats.replacements.fetch_add(1, Ordering::Relaxed);
896                                return true;
897                            }
898                        }
899                        // Copy over the usage counter to the new entry
900                        entry.tx_usage_counter.fetch_add(
901                            existing.tx_usage_counter.load(Ordering::Relaxed),
902                            Ordering::Relaxed,
903                        );
904                        entry.ix_usage_counter.fetch_add(
905                            existing.ix_usage_counter.load(Ordering::Relaxed),
906                            Ordering::Relaxed,
907                        );
908                        *existing = Arc::clone(&entry);
909                        self.stats.reloads.fetch_add(1, Ordering::Relaxed);
910                    }
911                    Err(index) => {
912                        self.stats.insertions.fetch_add(1, Ordering::Relaxed);
913                        slot_versions.insert(index, Arc::clone(&entry));
914                    }
915                }
916            }
917        }
918        false
919    }
920
921    fn matches_criteria(
922        program: &Arc<ProgramCacheEntry>,
923        criteria: &ProgramCacheMatchCriteria,
924    ) -> bool {
925        match criteria {
926            ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) => {
927                program.deployment_slot >= *slot
928            }
929            ProgramCacheMatchCriteria::Tombstone => program.is_tombstone(),
930            ProgramCacheMatchCriteria::NoCriteria => true,
931        }
932    }
933
934    /// Extracts a subset of the programs relevant to a transaction batch
935    /// and returns which program accounts the accounts DB needs to load.
936    pub fn extract(
937        &self,
938        search_for: &mut Vec<(Pubkey, (ProgramCacheMatchCriteria, u64))>,
939        loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch,
940        is_first_round: bool,
941    ) -> Option<(Pubkey, u64)> {
942        let mut cooperative_loading_task = None;
943        match &self.index {
944            IndexImplementation::V1 {
945                entries,
946                loading_entries,
947            } => {
948                search_for.retain(|(key, (match_criteria, usage_count))| {
949                    if let Some(second_level) = entries.get(key) {
950                        for entry in second_level.iter().rev() {
951                            if entry.deployment_slot <= loaded_programs_for_tx_batch.slot {
952                                let entry_to_return = if loaded_programs_for_tx_batch.slot
953                                    >= entry.effective_slot
954                                {
955                                    if !Self::matches_criteria(entry, match_criteria) {
956                                        break;
957                                    }
958                                    if matches!(&entry.program, ProgramCacheEntryType::Unloaded) {
959                                        break;
960                                    }
961                                    entry.clone()
962                                } else if entry.is_implicit_delay_visibility_tombstone(
963                                    loaded_programs_for_tx_batch.slot,
964                                ) {
965                                    // Found a program entry on the current fork, but it's not effective
966                                    // yet. It indicates that the program has delayed visibility. Return
967                                    // the tombstone to reflect that.
968                                    Arc::new(ProgramCacheEntry::new_tombstone(
969                                        entry.deployment_slot,
970                                        entry.account_owner,
971                                        ProgramCacheEntryType::DelayVisibility,
972                                    ))
973                                } else {
974                                    continue;
975                                };
976                                entry_to_return
977                                    .update_access_slot(loaded_programs_for_tx_batch.slot);
978                                entry_to_return
979                                    .tx_usage_counter
980                                    .fetch_add(*usage_count, Ordering::Relaxed);
981                                loaded_programs_for_tx_batch
982                                    .entries
983                                    .insert(*key, entry_to_return);
984                                return false;
985                            }
986                        }
987                    }
988                    if cooperative_loading_task.is_none() {
989                        let mut loading_entries = loading_entries.lock().unwrap();
990                        let entry = loading_entries.entry(*key);
991                        if let Entry::Vacant(entry) = entry {
992                            entry.insert((
993                                loaded_programs_for_tx_batch.slot,
994                                thread::current().id(),
995                            ));
996                            cooperative_loading_task = Some((*key, *usage_count));
997                        }
998                    }
999                    true
1000                });
1001            }
1002        }
1003        if is_first_round {
1004            self.stats
1005                .misses
1006                .fetch_add(search_for.len() as u64, Ordering::Relaxed);
1007            self.stats.hits.fetch_add(
1008                loaded_programs_for_tx_batch.entries.len() as u64,
1009                Ordering::Relaxed,
1010            );
1011        }
1012        cooperative_loading_task
1013    }
1014
1015    /// Called by Bank::replenish_program_cache() for each program that is done loading.
1016    pub fn finish_cooperative_loading_task(
1017        &mut self,
1018        slot: Slot,
1019        key: Pubkey,
1020        loaded_program: Arc<ProgramCacheEntry>,
1021    ) -> bool {
1022        match &mut self.index {
1023            IndexImplementation::V1 {
1024                loading_entries, ..
1025            } => {
1026                let loading_thread = loading_entries.get_mut().unwrap().remove(&key);
1027                debug_assert_eq!(loading_thread, Some((slot, thread::current().id())));
1028                // Check that it will be visible to our own fork once inserted
1029                if loaded_program.deployment_slot > slot {
1030                    self.stats.lost_insertions.fetch_add(1, Ordering::Relaxed);
1031                }
1032                let was_occupied = self.assign_program(key, loaded_program);
1033                self.loading_task_waiter.notify();
1034                was_occupied
1035            }
1036        }
1037    }
1038
1039    /// Folds a transaction's modified program entries back into the global
1040    /// index.
1041    ///
1042    /// Each key is assigned independently and every key in the map is
1043    /// distinct, so the merged result does not depend on the iteration
1044    /// order of `modified_entries`. That holds only while `assign_program`
1045    /// stays free of capacity and eviction side effects: if it began
1046    /// dropping entries once the index was full, which key survived would
1047    /// depend on the order they arrived in. Eviction is driven separately,
1048    /// after the fold, by `evict_using_2s_random_selection`.
1049    ///
1050    /// This is the production fold point: `svm-execution`'s
1051    /// `TransactionExecutionResult::programs_modified_by_tx` (same map type)
1052    /// is folded in here after every successfully executed transaction.
1053    #[allow(
1054        clippy::disallowed_types,
1055        reason = "parameter type shared with svm-execution's TransactionExecutionResult"
1056    )]
1057    pub fn merge(&mut self, modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>) {
1058        modified_entries.iter().for_each(|(key, entry)| {
1059            self.assign_program(*key, entry.clone());
1060        })
1061    }
1062
1063    /// Returns the list of entries which are verified and compiled.
1064    fn get_flattened_entries(&self) -> Vec<(Pubkey, Arc<ProgramCacheEntry>)> {
1065        match &self.index {
1066            IndexImplementation::V1 { entries, .. } => entries
1067                .iter()
1068                .flat_map(|(id, second_level)| {
1069                    second_level
1070                        .iter()
1071                        .filter_map(move |program| match program.program {
1072                            ProgramCacheEntryType::Loaded(_) => Some((*id, program.clone())),
1073                            _ => None,
1074                        })
1075                })
1076                .collect(),
1077        }
1078    }
1079
1080    /// Returns the list of all entries in the cache.
1081    #[cfg(any(test, feature = "dev-context-only-utils"))]
1082    pub fn get_flattened_entries_for_tests(&self) -> Vec<(Pubkey, Arc<ProgramCacheEntry>)> {
1083        match &self.index {
1084            IndexImplementation::V1 { entries, .. } => entries
1085                .iter()
1086                .flat_map(|(id, second_level)| {
1087                    second_level.iter().map(|program| (*id, program.clone()))
1088                })
1089                .collect(),
1090        }
1091    }
1092
1093    /// Returns the slot versions for the given program id.
1094    #[cfg(any(test, feature = "dev-context-only-utils"))]
1095    pub fn get_slot_versions_for_tests(&self, key: &Pubkey) -> &[Arc<ProgramCacheEntry>] {
1096        match &self.index {
1097            IndexImplementation::V1 { entries, .. } => entries
1098                .get(key)
1099                .map(|second_level| second_level.as_ref())
1100                .unwrap_or(&[]),
1101        }
1102    }
1103
1104    /// Unloads programs which were used infrequently
1105    pub fn sort_and_unload(&mut self, shrink_to: PercentageInteger) {
1106        let mut sorted_candidates = self.get_flattened_entries();
1107        sorted_candidates
1108            .sort_by_cached_key(|(_id, program)| program.tx_usage_counter.load(Ordering::Relaxed));
1109        let num_to_unload = sorted_candidates
1110            .len()
1111            .saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
1112        self.unload_program_entries(sorted_candidates.iter().take(num_to_unload));
1113    }
1114
1115    /// Evicts programs using 2's random selection, choosing the least used program out of the two entries.
1116    /// The eviction is performed enough number of times to reduce the cache usage to the given percentage.
1117    pub fn evict_using_2s_random_selection(&mut self, shrink_to: PercentageInteger, now: Slot) {
1118        let mut candidates = self.get_flattened_entries();
1119        self.stats
1120            .water_level
1121            .store(candidates.len() as u64, Ordering::Relaxed);
1122        let num_to_unload = candidates
1123            .len()
1124            .saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
1125        fn random_index_and_usage_counter(
1126            candidates: &[(Pubkey, Arc<ProgramCacheEntry>)],
1127            now: Slot,
1128        ) -> (usize, u64) {
1129            let mut rng = thread_rng();
1130            let index = rng.gen_range(0..candidates.len());
1131            let usage_counter = candidates
1132                .get(index)
1133                .expect("Failed to get cached entry")
1134                .1
1135                .decayed_usage_counter(now);
1136            (index, usage_counter)
1137        }
1138
1139        for _ in 0..num_to_unload {
1140            let (index1, usage_counter1) = random_index_and_usage_counter(&candidates, now);
1141            let (index2, usage_counter2) = random_index_and_usage_counter(&candidates, now);
1142
1143            let (program, entry) = if usage_counter1 < usage_counter2 {
1144                candidates.swap_remove(index1)
1145            } else {
1146                candidates.swap_remove(index2)
1147            };
1148            self.unload_program_entry(&program, &entry);
1149        }
1150    }
1151
1152    /// Removes all the entries at the given keys, if they exist
1153    pub fn remove_programs(&mut self, keys: impl Iterator<Item = Pubkey>) {
1154        match &mut self.index {
1155            IndexImplementation::V1 { entries, .. } => {
1156                for k in keys {
1157                    entries.remove(&k);
1158                }
1159            }
1160        }
1161    }
1162
1163    /// This function removes the given entry for the given program from the cache.
1164    /// The function expects that the program and entry exists in the cache. Otherwise it'll panic.
1165    fn unload_program_entry(&mut self, program: &Pubkey, remove_entry: &Arc<ProgramCacheEntry>) {
1166        match &mut self.index {
1167            IndexImplementation::V1 { entries, .. } => {
1168                let second_level = entries.get_mut(program).expect("Cache lookup failed");
1169                let candidate = second_level
1170                    .iter_mut()
1171                    .find(|entry| entry == &remove_entry)
1172                    .expect("Program entry not found");
1173
1174                // Certain entry types cannot be unloaded, such as tombstones, or already unloaded entries.
1175                // For such entries, `to_unloaded()` will return None.
1176                // These entry types do not occupy much memory.
1177                if let Some(unloaded) = candidate.to_unloaded() {
1178                    if candidate.tx_usage_counter.load(Ordering::Relaxed) == 1 {
1179                        self.stats.one_hit_wonders.fetch_add(1, Ordering::Relaxed);
1180                    }
1181                    self.stats
1182                        .evictions
1183                        .entry(*program)
1184                        .and_modify(|c| *c = c.saturating_add(1))
1185                        .or_insert(1);
1186                    *candidate = Arc::new(unloaded);
1187                }
1188            }
1189        }
1190    }
1191
1192    fn unload_program_entries<'a>(
1193        &mut self,
1194        remove: impl Iterator<Item = &'a (Pubkey, Arc<ProgramCacheEntry>)>,
1195    ) {
1196        for (program, entry) in remove {
1197            self.unload_program_entry(program, entry);
1198        }
1199    }
1200}
1201
1202#[cfg(feature = "frozen-abi")]
1203impl rialo_frozen_abi::abi_example::AbiExample for ProgramCacheEntry {
1204    fn example() -> Self {
1205        // ProgramCacheEntry isn't serializable by definition.
1206        Self::default()
1207    }
1208}
1209
1210#[cfg(feature = "frozen-abi")]
1211impl rialo_frozen_abi::abi_example::AbiExample for ProgramCache {
1212    fn example() -> Self {
1213        // ProgramCache isn't serializable by definition.
1214        Self::new()
1215    }
1216}
1217
1218#[cfg(false)] // TODO(SUB-1547): Investigate how to re-enable tests after importing Agave crates
1219#[cfg(test)]
1220mod tests {
1221    use std::{
1222        fs::File,
1223        io::Read,
1224        sync::{
1225            atomic::{AtomicU64, Ordering},
1226            Arc,
1227        },
1228    };
1229
1230    use assert_matches::assert_matches;
1231    use percentage::Percentage;
1232    use rialo_s_clock::Slot;
1233    use rialo_s_pubkey::Pubkey;
1234    use solana_sbpf::{elf::Executable, program::BuiltinProgram};
1235    use test_case::{test_case, test_matrix};
1236
1237    use crate::loaded_programs::{
1238        ProgramCache, ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType,
1239        ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironment,
1240        ProgramRuntimeEnvironments, DELAY_VISIBILITY_SLOT_OFFSET,
1241    };
1242
1243    static MOCK_ENVIRONMENT: std::sync::OnceLock<ProgramRuntimeEnvironment> =
1244        std::sync::OnceLock::<ProgramRuntimeEnvironment>::new();
1245
1246    fn get_mock_env() -> ProgramRuntimeEnvironment {
1247        MOCK_ENVIRONMENT
1248            .get_or_init(|| Arc::new(BuiltinProgram::new_mock()))
1249            .clone()
1250    }
1251
1252    fn new_mock_cache() -> ProgramCache {
1253        let mut cache = ProgramCache::new(0, 0);
1254        cache.environments.program_runtime_v1 = get_mock_env();
1255        cache
1256    }
1257
1258    fn new_test_entry(deployment_slot: Slot, effective_slot: Slot) -> Arc<ProgramCacheEntry> {
1259        new_test_entry_with_usage(deployment_slot, effective_slot, AtomicU64::default())
1260    }
1261
1262    fn new_loaded_entry(env: ProgramRuntimeEnvironment) -> ProgramCacheEntryType {
1263        let mut elf = Vec::new();
1264        File::open("../runtime/vm/ebpf/bpf_loader/test_elfs/out/noop_aligned.so")
1265            .unwrap()
1266            .read_to_end(&mut elf)
1267            .unwrap();
1268        let executable = Executable::load(&elf, env).unwrap();
1269        ProgramCacheEntryType::Loaded(super::ExecutableKind::Ebpf(executable))
1270    }
1271
1272    fn new_test_entry_with_usage(
1273        deployment_slot: Slot,
1274        effective_slot: Slot,
1275        usage_counter: AtomicU64,
1276    ) -> Arc<ProgramCacheEntry> {
1277        Arc::new(ProgramCacheEntry {
1278            program: new_loaded_entry(get_mock_env()),
1279            account_owner: ProgramCacheEntryOwner::LoaderV2,
1280            account_size: 0,
1281            deployment_slot,
1282            effective_slot,
1283            tx_usage_counter: usage_counter,
1284            ix_usage_counter: AtomicU64::default(),
1285            latest_access_slot: AtomicU64::new(deployment_slot),
1286        })
1287    }
1288
1289    fn new_test_builtin_entry(
1290        deployment_slot: Slot,
1291        effective_slot: Slot,
1292    ) -> Arc<ProgramCacheEntry> {
1293        Arc::new(ProgramCacheEntry {
1294            program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1295            account_owner: ProgramCacheEntryOwner::NativeLoader,
1296            account_size: 0,
1297            deployment_slot,
1298            effective_slot,
1299            tx_usage_counter: AtomicU64::default(),
1300            ix_usage_counter: AtomicU64::default(),
1301            latest_access_slot: AtomicU64::default(),
1302        })
1303    }
1304
1305    fn set_tombstone(
1306        cache: &mut ProgramCache,
1307        key: Pubkey,
1308        slot: Slot,
1309        reason: ProgramCacheEntryType,
1310    ) -> Arc<ProgramCacheEntry> {
1311        let program = Arc::new(ProgramCacheEntry::new_tombstone(
1312            slot,
1313            ProgramCacheEntryOwner::LoaderV2,
1314            reason,
1315        ));
1316        cache.assign_program(key, program.clone());
1317        program
1318    }
1319
1320    fn insert_unloaded_entry(
1321        cache: &mut ProgramCache,
1322        key: Pubkey,
1323        slot: Slot,
1324    ) -> Arc<ProgramCacheEntry> {
1325        let loaded = new_test_entry_with_usage(slot, slot.saturating_add(1), AtomicU64::default());
1326        let unloaded = Arc::new(loaded.to_unloaded().expect("Failed to unload the program"));
1327        cache.assign_program(key, unloaded.clone());
1328        unloaded
1329    }
1330
1331    fn num_matching_entries<P>(cache: &ProgramCache, predicate: P) -> usize
1332    where
1333        P: Fn(&ProgramCacheEntryType) -> bool,
1334    {
1335        cache
1336            .get_flattened_entries_for_tests()
1337            .iter()
1338            .filter(|(_key, program)| predicate(&program.program))
1339            .count()
1340    }
1341
1342    #[test]
1343    fn test_usage_counter_decay() {
1344        let _cache = new_mock_cache();
1345        let program = new_test_entry_with_usage(10, 11, AtomicU64::new(32));
1346        program.update_access_slot(15);
1347        assert_eq!(program.decayed_usage_counter(15), 32);
1348        assert_eq!(program.decayed_usage_counter(16), 16);
1349        assert_eq!(program.decayed_usage_counter(17), 8);
1350        assert_eq!(program.decayed_usage_counter(18), 4);
1351        assert_eq!(program.decayed_usage_counter(19), 2);
1352        assert_eq!(program.decayed_usage_counter(20), 1);
1353        assert_eq!(program.decayed_usage_counter(21), 0);
1354        assert_eq!(program.decayed_usage_counter(15), 32);
1355        assert_eq!(program.decayed_usage_counter(14), 32);
1356
1357        program.update_access_slot(18);
1358        assert_eq!(program.decayed_usage_counter(15), 32);
1359        assert_eq!(program.decayed_usage_counter(16), 32);
1360        assert_eq!(program.decayed_usage_counter(17), 32);
1361        assert_eq!(program.decayed_usage_counter(18), 32);
1362        assert_eq!(program.decayed_usage_counter(19), 16);
1363        assert_eq!(program.decayed_usage_counter(20), 8);
1364        assert_eq!(program.decayed_usage_counter(21), 4);
1365
1366        // Decay for 63 or more slots
1367        assert_eq!(program.decayed_usage_counter(18 + 63), 0);
1368        assert_eq!(program.decayed_usage_counter(100), 0);
1369    }
1370
1371    fn program_deploy_test_helper(
1372        cache: &mut ProgramCache,
1373        program: Pubkey,
1374        deployment_slots: Vec<Slot>,
1375        usage_counters: Vec<u64>,
1376        programs: &mut Vec<(Pubkey, Slot, u64)>,
1377    ) {
1378        // Add multiple entries for program
1379        deployment_slots
1380            .iter()
1381            .enumerate()
1382            .for_each(|(i, deployment_slot)| {
1383                let usage_counter = *usage_counters.get(i).unwrap_or(&0);
1384                cache.assign_program(
1385                    program,
1386                    new_test_entry_with_usage(
1387                        *deployment_slot,
1388                        (*deployment_slot).saturating_add(2),
1389                        AtomicU64::new(usage_counter),
1390                    ),
1391                );
1392                programs.push((program, *deployment_slot, usage_counter));
1393            });
1394
1395        // Add tombstones entries for program
1396        let env = Arc::new(BuiltinProgram::new_mock());
1397        for slot in 21..31 {
1398            set_tombstone(
1399                cache,
1400                program,
1401                slot,
1402                ProgramCacheEntryType::FailedVerification(env.clone()),
1403            );
1404        }
1405
1406        // Add unloaded entries for program
1407        for slot in 31..41 {
1408            insert_unloaded_entry(cache, program, slot);
1409        }
1410    }
1411
1412    #[test]
1413    fn test_random_eviction() {
1414        let mut programs = vec![];
1415
1416        let mut cache = new_mock_cache();
1417
1418        // This test adds different kind of entries to the cache.
1419        // Tombstones and unloaded entries are expected to not be evicted.
1420        // It also adds multiple entries for three programs as it tries to create a typical cache instance.
1421
1422        // Program 1
1423        program_deploy_test_helper(
1424            &mut cache,
1425            Pubkey::new_unique(),
1426            vec![0, 10, 20],
1427            vec![4, 5, 25],
1428            &mut programs,
1429        );
1430
1431        // Program 2
1432        program_deploy_test_helper(
1433            &mut cache,
1434            Pubkey::new_unique(),
1435            vec![5, 11],
1436            vec![0, 2],
1437            &mut programs,
1438        );
1439
1440        // Program 3
1441        program_deploy_test_helper(
1442            &mut cache,
1443            Pubkey::new_unique(),
1444            vec![0, 5, 15],
1445            vec![100, 3, 20],
1446            &mut programs,
1447        );
1448
1449        // 1 for each deployment slot
1450        let num_loaded_expected = 8;
1451        // 10 for each program
1452        let num_unloaded_expected = 30;
1453        // 10 for each program
1454        let num_tombstones_expected = 30;
1455
1456        // Count the number of loaded, unloaded and tombstone entries.
1457        programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
1458        let num_loaded = num_matching_entries(&cache, |program_type| {
1459            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1460        });
1461        let num_unloaded = num_matching_entries(&cache, |program_type| {
1462            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1463        });
1464        let num_tombstones = num_matching_entries(&cache, |program_type| {
1465            matches!(
1466                program_type,
1467                ProgramCacheEntryType::DelayVisibility
1468                    | ProgramCacheEntryType::FailedVerification(_)
1469                    | ProgramCacheEntryType::Closed
1470            )
1471        });
1472
1473        // Test that the cache is constructed with the expected number of entries.
1474        assert_eq!(num_loaded, num_loaded_expected);
1475        assert_eq!(num_unloaded, num_unloaded_expected);
1476        assert_eq!(num_tombstones, num_tombstones_expected);
1477
1478        // Evict entries from the cache
1479        let eviction_pct = 1;
1480
1481        let num_loaded_expected =
1482            Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
1483        let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
1484        cache.evict_using_2s_random_selection(Percentage::from(eviction_pct), 21);
1485
1486        // Count the number of loaded, unloaded and tombstone entries.
1487        let num_loaded = num_matching_entries(&cache, |program_type| {
1488            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1489        });
1490        let num_unloaded = num_matching_entries(&cache, |program_type| {
1491            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1492        });
1493        let num_tombstones = num_matching_entries(&cache, |program_type| {
1494            matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
1495        });
1496
1497        // However many entries are left after the shrink
1498        assert_eq!(num_loaded, num_loaded_expected);
1499        // The original unloaded entries + the evicted loaded entries
1500        assert_eq!(num_unloaded, num_unloaded_expected);
1501        // The original tombstones are not evicted
1502        assert_eq!(num_tombstones, num_tombstones_expected);
1503    }
1504
1505    #[test]
1506    fn test_eviction() {
1507        let mut programs = vec![];
1508        let mut cache = new_mock_cache();
1509
1510        // Program 1
1511        program_deploy_test_helper(
1512            &mut cache,
1513            Pubkey::new_unique(),
1514            vec![0, 10, 20],
1515            vec![4, 5, 25],
1516            &mut programs,
1517        );
1518
1519        // Program 2
1520        program_deploy_test_helper(
1521            &mut cache,
1522            Pubkey::new_unique(),
1523            vec![5, 11],
1524            vec![0, 2],
1525            &mut programs,
1526        );
1527
1528        // Program 3
1529        program_deploy_test_helper(
1530            &mut cache,
1531            Pubkey::new_unique(),
1532            vec![0, 5, 15],
1533            vec![100, 3, 20],
1534            &mut programs,
1535        );
1536
1537        // 1 for each deployment slot
1538        let num_loaded_expected = 8;
1539        // 10 for each program
1540        let num_unloaded_expected = 30;
1541        // 10 for each program
1542        let num_tombstones_expected = 30;
1543
1544        // Count the number of loaded, unloaded and tombstone entries.
1545        programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
1546        let num_loaded = num_matching_entries(&cache, |program_type| {
1547            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1548        });
1549        let num_unloaded = num_matching_entries(&cache, |program_type| {
1550            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1551        });
1552        let num_tombstones = num_matching_entries(&cache, |program_type| {
1553            matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
1554        });
1555
1556        // Test that the cache is constructed with the expected number of entries.
1557        assert_eq!(num_loaded, num_loaded_expected);
1558        assert_eq!(num_unloaded, num_unloaded_expected);
1559        assert_eq!(num_tombstones, num_tombstones_expected);
1560
1561        // Evict entries from the cache
1562        let eviction_pct = 1;
1563
1564        let num_loaded_expected =
1565            Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
1566        let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
1567
1568        cache.sort_and_unload(Percentage::from(eviction_pct));
1569
1570        // Check that every program is still in the cache.
1571        let entries = cache.get_flattened_entries_for_tests();
1572        programs.iter().for_each(|entry| {
1573            assert!(entries.iter().any(|(key, _entry)| key == &entry.0));
1574        });
1575
1576        let unloaded = entries
1577            .iter()
1578            .filter_map(|(key, program)| {
1579                matches!(program.program, ProgramCacheEntryType::Unloaded(_))
1580                    .then_some((*key, program.tx_usage_counter.load(Ordering::Relaxed)))
1581            })
1582            .collect::<Vec<(Pubkey, u64)>>();
1583
1584        for index in 0..3 {
1585            let expected = programs.get(index).expect("Missing program");
1586            assert!(unloaded.contains(&(expected.0, expected.2)));
1587        }
1588
1589        // Count the number of loaded, unloaded and tombstone entries.
1590        let num_loaded = num_matching_entries(&cache, |program_type| {
1591            matches!(program_type, ProgramCacheEntryType::Loaded(_))
1592        });
1593        let num_unloaded = num_matching_entries(&cache, |program_type| {
1594            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1595        });
1596        let num_tombstones = num_matching_entries(&cache, |program_type| {
1597            matches!(
1598                program_type,
1599                ProgramCacheEntryType::DelayVisibility
1600                    | ProgramCacheEntryType::FailedVerification(_)
1601                    | ProgramCacheEntryType::Closed
1602            )
1603        });
1604
1605        // However many entries are left after the shrink
1606        assert_eq!(num_loaded, num_loaded_expected);
1607        // The original unloaded entries + the evicted loaded entries
1608        assert_eq!(num_unloaded, num_unloaded_expected);
1609        // The original tombstones are not evicted
1610        assert_eq!(num_tombstones, num_tombstones_expected);
1611    }
1612
1613    #[test]
1614    fn test_usage_count_of_unloaded_program() {
1615        let mut cache = new_mock_cache();
1616
1617        let program = Pubkey::new_unique();
1618        let evict_to_pct = 2;
1619        let cache_capacity_after_shrink =
1620            Percentage::from(evict_to_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
1621        // Add enough programs to the cache to trigger 1 eviction after shrinking.
1622        let num_total_programs = (cache_capacity_after_shrink + 1) as u64;
1623        (0..num_total_programs).for_each(|i| {
1624            cache.assign_program(
1625                program,
1626                new_test_entry_with_usage(i, i + 2, AtomicU64::new(i + 10)),
1627            );
1628        });
1629
1630        cache.sort_and_unload(Percentage::from(evict_to_pct));
1631
1632        let num_unloaded = num_matching_entries(&cache, |program_type| {
1633            matches!(program_type, ProgramCacheEntryType::Unloaded(_))
1634        });
1635        assert_eq!(num_unloaded, 1);
1636
1637        cache
1638            .get_flattened_entries_for_tests()
1639            .iter()
1640            .for_each(|(_key, program)| {
1641                if matches!(program.program, ProgramCacheEntryType::Unloaded(_)) {
1642                    // Test that the usage counter is retained for the unloaded program
1643                    assert_eq!(program.tx_usage_counter.load(Ordering::Relaxed), 10);
1644                    assert_eq!(program.deployment_slot, 0);
1645                    assert_eq!(program.effective_slot, 2);
1646                }
1647            });
1648
1649        // Replenish the program that was just unloaded. Use 0 as the usage counter. This should be
1650        // updated with the usage counter from the unloaded program.
1651        cache.assign_program(program, new_test_entry_with_usage(0, 2, AtomicU64::new(0)));
1652
1653        cache
1654            .get_flattened_entries_for_tests()
1655            .iter()
1656            .for_each(|(_key, program)| {
1657                if matches!(program.program, ProgramCacheEntryType::Unloaded(_))
1658                    && program.deployment_slot == 0
1659                    && program.effective_slot == 2
1660                {
1661                    // Test that the usage counter was correctly updated.
1662                    assert_eq!(program.tx_usage_counter.load(Ordering::Relaxed), 10);
1663                }
1664            });
1665    }
1666
1667    #[test]
1668    fn test_fuzz_assign_program_order() {
1669        use rand::prelude::SliceRandom;
1670        const EXPECTED_ENTRIES: [(u64, u64); 7] =
1671            [(1, 2), (5, 5), (5, 6), (5, 10), (9, 10), (10, 10), (3, 12)];
1672        let mut rng = rand::thread_rng();
1673        let program_id = Pubkey::new_unique();
1674        for _ in 0..1000 {
1675            let mut entries = EXPECTED_ENTRIES.to_vec();
1676            entries.shuffle(&mut rng);
1677            let mut cache = new_mock_cache();
1678            for (deployment_slot, effective_slot) in entries {
1679                assert!(!cache
1680                    .assign_program(program_id, new_test_entry(deployment_slot, effective_slot)));
1681            }
1682            for ((deployment_slot, effective_slot), entry) in EXPECTED_ENTRIES
1683                .iter()
1684                .zip(cache.get_slot_versions_for_tests(&program_id).iter())
1685            {
1686                assert_eq!(entry.deployment_slot, *deployment_slot);
1687                assert_eq!(entry.effective_slot, *effective_slot);
1688            }
1689        }
1690    }
1691
1692    #[test_matrix(
1693        (
1694            ProgramCacheEntryType::Closed,
1695            ProgramCacheEntryType::FailedVerification(get_mock_env()),
1696            new_loaded_entry(get_mock_env()),
1697        ),
1698        (
1699            ProgramCacheEntryType::FailedVerification(get_mock_env()),
1700            ProgramCacheEntryType::Closed,
1701            ProgramCacheEntryType::Unloaded(get_mock_env()),
1702            new_loaded_entry(get_mock_env()),
1703            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1704        )
1705    )]
1706    #[test_matrix(
1707        (
1708            ProgramCacheEntryType::Unloaded(get_mock_env()),
1709        ),
1710        (
1711            ProgramCacheEntryType::FailedVerification(get_mock_env()),
1712            ProgramCacheEntryType::Closed,
1713            ProgramCacheEntryType::Unloaded(get_mock_env()),
1714            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1715        )
1716    )]
1717    #[test_matrix(
1718        (ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),),
1719        (
1720            ProgramCacheEntryType::FailedVerification(get_mock_env()),
1721            ProgramCacheEntryType::Closed,
1722            ProgramCacheEntryType::Unloaded(get_mock_env()),
1723            new_loaded_entry(get_mock_env()),
1724        )
1725    )]
1726    #[should_panic(expected = "Unexpected replacement of an entry")]
1727    fn test_assign_program_failure(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
1728        let mut cache = new_mock_cache();
1729        let program_id = Pubkey::new_unique();
1730        assert!(!cache.assign_program(
1731            program_id,
1732            Arc::new(ProgramCacheEntry {
1733                program: old,
1734                account_owner: ProgramCacheEntryOwner::LoaderV2,
1735                account_size: 0,
1736                deployment_slot: 10,
1737                effective_slot: 11,
1738                tx_usage_counter: AtomicU64::default(),
1739                ix_usage_counter: AtomicU64::default(),
1740                latest_access_slot: AtomicU64::default(),
1741            }),
1742        ));
1743        cache.assign_program(
1744            program_id,
1745            Arc::new(ProgramCacheEntry {
1746                program: new,
1747                account_owner: ProgramCacheEntryOwner::LoaderV2,
1748                account_size: 0,
1749                deployment_slot: 10,
1750                effective_slot: 11,
1751                tx_usage_counter: AtomicU64::default(),
1752                ix_usage_counter: AtomicU64::default(),
1753                latest_access_slot: AtomicU64::default(),
1754            }),
1755        );
1756    }
1757
1758    #[test_case(
1759        ProgramCacheEntryType::Unloaded(Arc::new(BuiltinProgram::new_mock())),
1760        new_loaded_entry(get_mock_env())
1761    )]
1762    #[test_case(
1763        ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
1764        ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock())
1765    )]
1766    fn test_assign_program_success(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
1767        let mut cache = new_mock_cache();
1768        let program_id = Pubkey::new_unique();
1769        assert!(!cache.assign_program(
1770            program_id,
1771            Arc::new(ProgramCacheEntry {
1772                program: old,
1773                account_owner: ProgramCacheEntryOwner::LoaderV2,
1774                account_size: 0,
1775                deployment_slot: 10,
1776                effective_slot: 11,
1777                tx_usage_counter: AtomicU64::default(),
1778                ix_usage_counter: AtomicU64::default(),
1779                latest_access_slot: AtomicU64::default(),
1780            }),
1781        ));
1782        assert!(!cache.assign_program(
1783            program_id,
1784            Arc::new(ProgramCacheEntry {
1785                program: new,
1786                account_owner: ProgramCacheEntryOwner::LoaderV2,
1787                account_size: 0,
1788                deployment_slot: 10,
1789                effective_slot: 11,
1790                tx_usage_counter: AtomicU64::default(),
1791                ix_usage_counter: AtomicU64::default(),
1792                latest_access_slot: AtomicU64::default(),
1793            }),
1794        ));
1795    }
1796
1797    #[test]
1798    fn test_tombstone() {
1799        let env = Arc::new(BuiltinProgram::new_mock());
1800        let tombstone = ProgramCacheEntry::new_tombstone(
1801            0,
1802            ProgramCacheEntryOwner::LoaderV2,
1803            ProgramCacheEntryType::FailedVerification(env.clone()),
1804        );
1805        assert_matches!(
1806            tombstone.program,
1807            ProgramCacheEntryType::FailedVerification(_)
1808        );
1809        assert!(tombstone.is_tombstone());
1810        assert_eq!(tombstone.deployment_slot, 0);
1811        assert_eq!(tombstone.effective_slot, 0);
1812
1813        let tombstone = ProgramCacheEntry::new_tombstone(
1814            100,
1815            ProgramCacheEntryOwner::LoaderV2,
1816            ProgramCacheEntryType::Closed,
1817        );
1818        assert_matches!(tombstone.program, ProgramCacheEntryType::Closed);
1819        assert!(tombstone.is_tombstone());
1820        assert_eq!(tombstone.deployment_slot, 100);
1821        assert_eq!(tombstone.effective_slot, 100);
1822
1823        let mut cache = new_mock_cache();
1824        let program1 = Pubkey::new_unique();
1825        let tombstone = set_tombstone(
1826            &mut cache,
1827            program1,
1828            10,
1829            ProgramCacheEntryType::FailedVerification(env.clone()),
1830        );
1831        let slot_versions = cache.get_slot_versions_for_tests(&program1);
1832        assert_eq!(slot_versions.len(), 1);
1833        assert!(slot_versions.first().unwrap().is_tombstone());
1834        assert_eq!(tombstone.deployment_slot, 10);
1835        assert_eq!(tombstone.effective_slot, 10);
1836
1837        // Add a program at slot 50, and a tombstone for the program at slot 60
1838        let program2 = Pubkey::new_unique();
1839        cache.assign_program(program2, new_test_builtin_entry(50, 51));
1840        let slot_versions = cache.get_slot_versions_for_tests(&program2);
1841        assert_eq!(slot_versions.len(), 1);
1842        assert!(!slot_versions.first().unwrap().is_tombstone());
1843
1844        let tombstone = set_tombstone(
1845            &mut cache,
1846            program2,
1847            60,
1848            ProgramCacheEntryType::FailedVerification(env),
1849        );
1850        let slot_versions = cache.get_slot_versions_for_tests(&program2);
1851        assert_eq!(slot_versions.len(), 2);
1852        assert!(!slot_versions.first().unwrap().is_tombstone());
1853        assert!(slot_versions.get(1).unwrap().is_tombstone());
1854        assert!(tombstone.is_tombstone());
1855        assert_eq!(tombstone.deployment_slot, 60);
1856        assert_eq!(tombstone.effective_slot, 60);
1857    }
1858
1859    #[test]
1860    fn test_prune_empty() {
1861        let mut cache = new_mock_cache();
1862
1863        cache.prune(0, 0);
1864        assert!(cache.get_flattened_entries_for_tests().is_empty());
1865
1866        cache.prune(10, 0);
1867        assert!(cache.get_flattened_entries_for_tests().is_empty());
1868
1869        let mut cache = new_mock_cache();
1870
1871        cache.prune(0, 0);
1872        assert!(cache.get_flattened_entries_for_tests().is_empty());
1873
1874        cache.prune(10, 0);
1875        assert!(cache.get_flattened_entries_for_tests().is_empty());
1876
1877        let mut cache = new_mock_cache();
1878
1879        cache.prune(0, 0);
1880        assert!(cache.get_flattened_entries_for_tests().is_empty());
1881
1882        cache.prune(10, 0);
1883        assert!(cache.get_flattened_entries_for_tests().is_empty());
1884
1885        let mut cache = new_mock_cache();
1886
1887        cache.prune(0, 0);
1888        assert!(cache.get_flattened_entries_for_tests().is_empty());
1889
1890        cache.prune(10, 0);
1891        assert!(cache.get_flattened_entries_for_tests().is_empty());
1892    }
1893
1894    #[test]
1895    fn test_prune_different_env() {
1896        let mut cache = new_mock_cache();
1897
1898        let program1 = Pubkey::new_unique();
1899        cache.assign_program(program1, new_test_entry(10, 10));
1900
1901        let new_env = Arc::new(BuiltinProgram::new_mock());
1902        cache.upcoming_environments = Some(ProgramRuntimeEnvironments {
1903            program_runtime_v1: new_env.clone(),
1904            program_runtime_v2: new_env.clone(),
1905        });
1906        let updated_program = Arc::new(ProgramCacheEntry {
1907            program: new_loaded_entry(new_env.clone()),
1908            account_owner: ProgramCacheEntryOwner::LoaderV2,
1909            account_size: 0,
1910            deployment_slot: 20,
1911            effective_slot: 20,
1912            tx_usage_counter: AtomicU64::default(),
1913            ix_usage_counter: AtomicU64::default(),
1914            latest_access_slot: AtomicU64::default(),
1915        });
1916        cache.assign_program(program1, updated_program.clone());
1917
1918        // Test that there are 2 entries for the program
1919        assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
1920
1921        cache.prune(21, cache.latest_root_epoch);
1922
1923        // Test that prune didn't remove the entry, since environments are different.
1924        assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
1925
1926        cache.prune(22, cache.latest_root_epoch.saturating_add(1));
1927
1928        // Test that prune removed 1 entry, since epoch changed
1929        assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 1);
1930
1931        let entry = cache
1932            .get_slot_versions_for_tests(&program1)
1933            .first()
1934            .expect("Failed to get the program")
1935            .clone();
1936        // Test that the correct entry remains in the cache
1937        assert_eq!(entry, updated_program);
1938    }
1939
1940    fn get_entries_to_load(
1941        cache: &ProgramCache,
1942        loading_slot: Slot,
1943        keys: &[Pubkey],
1944    ) -> Vec<(Pubkey, (ProgramCacheMatchCriteria, u64))> {
1945        let entries = cache.get_flattened_entries_for_tests();
1946        keys.iter()
1947            .filter_map(|key| {
1948                entries
1949                    .iter()
1950                    .rev()
1951                    .find(|(program_id, entry)| {
1952                        program_id == key && entry.deployment_slot <= loading_slot
1953                    })
1954                    .map(|(program_id, _entry)| {
1955                        (*program_id, (ProgramCacheMatchCriteria::NoCriteria, 1))
1956                    })
1957            })
1958            .collect()
1959    }
1960
1961    fn match_slot(
1962        extracted: &ProgramCacheForTxBatch,
1963        program: &Pubkey,
1964        deployment_slot: Slot,
1965        working_slot: Slot,
1966    ) -> bool {
1967        assert_eq!(extracted.slot, working_slot);
1968        extracted
1969            .entries
1970            .get(program)
1971            .map(|entry| entry.deployment_slot == deployment_slot)
1972            .unwrap_or(false)
1973    }
1974
1975    fn match_missing(
1976        missing: &[(Pubkey, (ProgramCacheMatchCriteria, u64))],
1977        program: &Pubkey,
1978        expected_result: bool,
1979    ) -> bool {
1980        missing.iter().any(|(key, _)| key == program) == expected_result
1981    }
1982
1983    #[test]
1984    fn test_fork_extract_and_prune() {
1985        let mut cache = new_mock_cache();
1986
1987        // Fork graph created for the test
1988        //                   0
1989        //                 /   \
1990        //                10    5
1991        //                |     |
1992        //                20    11
1993        //                |     | \
1994        //                22   15  25
1995        //                      |   |
1996        //                     16  27
1997        //                      |
1998        //                     19
1999        //                      |
2000        //                     23
2001        let program1 = Pubkey::new_unique();
2002        cache.assign_program(program1, new_test_entry(0, 1));
2003        cache.assign_program(program1, new_test_entry(10, 11));
2004        cache.assign_program(program1, new_test_entry(20, 21));
2005
2006        let program2 = Pubkey::new_unique();
2007        cache.assign_program(program2, new_test_entry(5, 6));
2008        cache.assign_program(
2009            program2,
2010            new_test_entry(11, 11 + DELAY_VISIBILITY_SLOT_OFFSET),
2011        );
2012
2013        let program3 = Pubkey::new_unique();
2014        cache.assign_program(program3, new_test_entry(25, 26));
2015
2016        let program4 = Pubkey::new_unique();
2017        cache.assign_program(program4, new_test_entry(0, 1));
2018        cache.assign_program(program4, new_test_entry(5, 6));
2019        // The following is a special case, where effective slot is 3 slots in the future
2020        cache.assign_program(
2021            program4,
2022            new_test_entry(15, 15 + DELAY_VISIBILITY_SLOT_OFFSET),
2023        );
2024
2025        // Current fork graph
2026        //                   0
2027        //                 /   \
2028        //                10    5
2029        //                |     |
2030        //                20    11
2031        //                |     | \
2032        //                22   15  25
2033        //                      |   |
2034        //                     16  27
2035        //                      |
2036        //                     19
2037        //                      |
2038        //                     23
2039
2040        // Testing fork 0 - 10 - 12 - 22 with current slot at 22
2041        let mut missing =
2042            get_entries_to_load(&cache, 22, &[program1, program2, program3, program4]);
2043        assert!(match_missing(&missing, &program2, false));
2044        assert!(match_missing(&missing, &program3, false));
2045        let mut extracted = ProgramCacheForTxBatch::new(22, cache.environments.clone(), None, 0);
2046        cache.extract(&mut missing, &mut extracted, true);
2047        assert!(match_slot(&extracted, &program1, 20, 22));
2048        assert!(match_slot(&extracted, &program4, 0, 22));
2049
2050        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 15
2051        let mut missing =
2052            get_entries_to_load(&cache, 15, &[program1, program2, program3, program4]);
2053        assert!(match_missing(&missing, &program3, false));
2054        let mut extracted = ProgramCacheForTxBatch::new(15, cache.environments.clone(), None, 0);
2055        cache.extract(&mut missing, &mut extracted, true);
2056        assert!(match_slot(&extracted, &program1, 0, 15));
2057        assert!(match_slot(&extracted, &program2, 11, 15));
2058        // The effective slot of program4 deployed in slot 15 is 19. So it should not be usable in slot 16.
2059        // A delay visibility tombstone should be returned here.
2060        let tombstone = extracted
2061            .find(&program4)
2062            .expect("Failed to find the tombstone");
2063        assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
2064        assert_eq!(tombstone.deployment_slot, 15);
2065
2066        // Testing the same fork above, but current slot is now 18 (equal to effective slot of program4).
2067        let mut missing =
2068            get_entries_to_load(&cache, 18, &[program1, program2, program3, program4]);
2069        assert!(match_missing(&missing, &program3, false));
2070        let mut extracted = ProgramCacheForTxBatch::new(18, cache.environments.clone(), None, 0);
2071        cache.extract(&mut missing, &mut extracted, true);
2072        assert!(match_slot(&extracted, &program1, 0, 18));
2073        assert!(match_slot(&extracted, &program2, 11, 18));
2074        // The effective slot of program4 deployed in slot 15 is 18. So it should be usable in slot 18.
2075        assert!(match_slot(&extracted, &program4, 15, 18));
2076
2077        // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4).
2078        let mut missing =
2079            get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]);
2080        assert!(match_missing(&missing, &program3, false));
2081        let mut extracted = ProgramCacheForTxBatch::new(23, cache.environments.clone(), None, 0);
2082        cache.extract(&mut missing, &mut extracted, true);
2083        assert!(match_slot(&extracted, &program1, 0, 23));
2084        assert!(match_slot(&extracted, &program2, 11, 23));
2085        // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 23.
2086        assert!(match_slot(&extracted, &program4, 15, 23));
2087
2088        // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11
2089        let mut missing =
2090            get_entries_to_load(&cache, 11, &[program1, program2, program3, program4]);
2091        assert!(match_missing(&missing, &program3, false));
2092        let mut extracted = ProgramCacheForTxBatch::new(11, cache.environments.clone(), None, 0);
2093        cache.extract(&mut missing, &mut extracted, true);
2094        assert!(match_slot(&extracted, &program1, 0, 11));
2095        // program2 was updated at slot 11, but is not effective till slot 12. The result should contain a tombstone.
2096        let tombstone = extracted
2097            .find(&program2)
2098            .expect("Failed to find the tombstone");
2099        assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
2100        assert_eq!(tombstone.deployment_slot, 11);
2101        assert!(match_slot(&extracted, &program4, 5, 11));
2102
2103        cache.prune(5, 0);
2104
2105        // Fork graph after pruning
2106        //                   0
2107        //                   |
2108        //                   5
2109        //                   |
2110        //                   11
2111        //                   | \
2112        //                  15  25
2113        //                   |   |
2114        //                  16  27
2115        //                   |
2116        //                  19
2117        //                   |
2118        //                  23
2119
2120        // Testing fork 11 - 15 - 16- 19 - 22 with root at 5 and current slot at 22
2121        let mut missing =
2122            get_entries_to_load(&cache, 21, &[program1, program2, program3, program4]);
2123        assert!(match_missing(&missing, &program3, false));
2124        let mut extracted = ProgramCacheForTxBatch::new(21, cache.environments.clone(), None, 0);
2125        cache.extract(&mut missing, &mut extracted, true);
2126        // Since the fork was pruned, we should not find the entry deployed at slot 20.
2127        assert!(match_slot(&extracted, &program1, 0, 21));
2128        assert!(match_slot(&extracted, &program2, 11, 21));
2129        assert!(match_slot(&extracted, &program4, 15, 21));
2130
2131        // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27
2132        let mut missing =
2133            get_entries_to_load(&cache, 27, &[program1, program2, program3, program4]);
2134        let mut extracted = ProgramCacheForTxBatch::new(27, cache.environments.clone(), None, 0);
2135        cache.extract(&mut missing, &mut extracted, true);
2136        assert!(match_slot(&extracted, &program1, 0, 27));
2137        assert!(match_slot(&extracted, &program2, 11, 27));
2138        assert!(match_slot(&extracted, &program3, 25, 27));
2139        assert!(match_slot(&extracted, &program4, 5, 27));
2140
2141        cache.prune(15, 0);
2142
2143        // Fork graph after pruning
2144        //                  0
2145        //                  |
2146        //                  5
2147        //                  |
2148        //                  11
2149        //                  |
2150        //                  15
2151        //                  |
2152        //                  16
2153        //                  |
2154        //                  19
2155        //                  |
2156        //                  23
2157
2158        // Testing fork 16, 19, 23, with root at 15, current slot at 23
2159        let mut missing =
2160            get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]);
2161        assert!(match_missing(&missing, &program3, false));
2162        let mut extracted = ProgramCacheForTxBatch::new(23, cache.environments.clone(), None, 0);
2163        cache.extract(&mut missing, &mut extracted, true);
2164        assert!(match_slot(&extracted, &program1, 0, 23));
2165        assert!(match_slot(&extracted, &program2, 11, 23));
2166        assert!(match_slot(&extracted, &program4, 15, 23));
2167    }
2168
2169    #[test]
2170    fn test_extract_using_deployment_slot() {
2171        let mut cache = new_mock_cache();
2172
2173        // Fork graph created for the test
2174        //                   0
2175        //                 /   \
2176        //                10    5
2177        //                |     |
2178        //                20    11
2179        //                |     | \
2180        //                22   15  25
2181        //                      |   |
2182        //                     16  27
2183        //                      |
2184        //                     19
2185        //                      |
2186        //                     23
2187        let program1 = Pubkey::new_unique();
2188        cache.assign_program(program1, new_test_entry(0, 1));
2189        cache.assign_program(program1, new_test_entry(20, 21));
2190
2191        let program2 = Pubkey::new_unique();
2192        cache.assign_program(program2, new_test_entry(5, 6));
2193        cache.assign_program(program2, new_test_entry(11, 12));
2194
2195        let program3 = Pubkey::new_unique();
2196        cache.assign_program(program3, new_test_entry(25, 26));
2197
2198        // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19
2199        let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]);
2200        assert!(match_missing(&missing, &program3, false));
2201        let mut extracted = ProgramCacheForTxBatch::new(12, cache.environments.clone(), None, 0);
2202        cache.extract(&mut missing, &mut extracted, true);
2203        assert!(match_slot(&extracted, &program1, 0, 12));
2204        assert!(match_slot(&extracted, &program2, 11, 12));
2205
2206        // Test the same fork, but request the program modified at a later slot than what's in the cache.
2207        let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]);
2208        missing.get_mut(0).unwrap().1 .0 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5);
2209        missing.get_mut(1).unwrap().1 .0 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5);
2210        assert!(match_missing(&missing, &program3, false));
2211        let mut extracted = ProgramCacheForTxBatch::new(12, cache.environments.clone(), None, 0);
2212        cache.extract(&mut missing, &mut extracted, true);
2213        assert!(match_missing(&missing, &program1, true));
2214        assert!(match_slot(&extracted, &program2, 11, 12));
2215    }
2216
2217    #[test]
2218    fn test_extract_unloaded() {
2219        let mut cache = new_mock_cache();
2220
2221        // Fork graph created for the test
2222        //                   0
2223        //                 /   \
2224        //                10    5
2225        //                |     |
2226        //                20    11
2227        //                |     | \
2228        //                22   15  25
2229        //                      |   |
2230        //                     16  27
2231        //                      |
2232        //                     19
2233        //                      |
2234        //                     23
2235
2236        let program1 = Pubkey::new_unique();
2237        cache.assign_program(program1, new_test_entry(0, 1));
2238        cache.assign_program(program1, new_test_entry(20, 21));
2239
2240        let program2 = Pubkey::new_unique();
2241        cache.assign_program(program2, new_test_entry(5, 6));
2242        cache.assign_program(program2, new_test_entry(11, 12));
2243
2244        let program3 = Pubkey::new_unique();
2245        // Insert an unloaded program with correct/cache's environment at slot 25
2246        let _ = insert_unloaded_entry(&mut cache, program3, 25);
2247
2248        // Insert another unloaded program with a different environment at slot 20
2249        // Since this entry's environment won't match cache's environment, looking up this
2250        // entry should return missing instead of unloaded entry.
2251        cache.assign_program(
2252            program3,
2253            Arc::new(
2254                new_test_entry(20, 21)
2255                    .to_unloaded()
2256                    .expect("Failed to create unloaded program"),
2257            ),
2258        );
2259
2260        // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19
2261        let mut missing = get_entries_to_load(&cache, 19, &[program1, program2, program3]);
2262        assert!(match_missing(&missing, &program3, false));
2263        let mut extracted = ProgramCacheForTxBatch::new(19, cache.environments.clone(), None, 0);
2264        cache.extract(&mut missing, &mut extracted, true);
2265        assert!(match_slot(&extracted, &program1, 0, 19));
2266        assert!(match_slot(&extracted, &program2, 11, 19));
2267
2268        // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27
2269        let mut missing = get_entries_to_load(&cache, 27, &[program1, program2, program3]);
2270        let mut extracted = ProgramCacheForTxBatch::new(27, cache.environments.clone(), None, 0);
2271        cache.extract(&mut missing, &mut extracted, true);
2272        assert!(match_slot(&extracted, &program1, 0, 27));
2273        assert!(match_slot(&extracted, &program2, 11, 27));
2274        assert!(match_missing(&missing, &program3, true));
2275
2276        // Testing fork 0 - 10 - 20 - 22 with current slot at 22
2277        let mut missing = get_entries_to_load(&cache, 22, &[program1, program2, program3]);
2278        assert!(match_missing(&missing, &program2, false));
2279        let mut extracted = ProgramCacheForTxBatch::new(22, cache.environments.clone(), None, 0);
2280        cache.extract(&mut missing, &mut extracted, true);
2281        assert!(match_slot(&extracted, &program1, 20, 22));
2282        assert!(match_missing(&missing, &program3, true));
2283    }
2284
2285    #[test]
2286    fn test_extract_nonexistent() {
2287        let cache = new_mock_cache();
2288
2289        let program1 = Pubkey::new_unique();
2290        let mut missing = vec![(program1, (ProgramCacheMatchCriteria::NoCriteria, 1))];
2291        let mut extracted = ProgramCacheForTxBatch::new(0, cache.environments.clone(), None, 0);
2292        cache.extract(&mut missing, &mut extracted, true);
2293        assert!(match_missing(&missing, &program1, true));
2294    }
2295
2296    #[test]
2297    fn test_unloaded() {
2298        let mut cache = new_mock_cache();
2299        for program_cache_entry_type in [
2300            ProgramCacheEntryType::FailedVerification(
2301                cache.environments.program_runtime_v1.clone(),
2302            ),
2303            ProgramCacheEntryType::Closed,
2304            ProgramCacheEntryType::Unloaded(cache.environments.program_runtime_v1.clone()),
2305            ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
2306        ] {
2307            let entry = Arc::new(ProgramCacheEntry {
2308                program: program_cache_entry_type,
2309                account_owner: ProgramCacheEntryOwner::LoaderV2,
2310                account_size: 0,
2311                deployment_slot: 0,
2312                effective_slot: 0,
2313                tx_usage_counter: AtomicU64::default(),
2314                ix_usage_counter: AtomicU64::default(),
2315                latest_access_slot: AtomicU64::default(),
2316            });
2317            assert!(entry.to_unloaded().is_none());
2318
2319            // Check that unload_program_entry() does nothing for this entry
2320            let program_id = Pubkey::new_unique();
2321            cache.assign_program(program_id, entry.clone());
2322            cache.unload_program_entry(&program_id, &entry);
2323            assert_eq!(cache.get_slot_versions_for_tests(&program_id).len(), 1);
2324            assert!(cache.stats.evictions.is_empty());
2325        }
2326
2327        let entry = new_test_entry_with_usage(1, 2, AtomicU64::new(3));
2328        let unloaded_entry = entry.to_unloaded().unwrap();
2329        assert_eq!(unloaded_entry.deployment_slot, 1);
2330        assert_eq!(unloaded_entry.effective_slot, 2);
2331        assert_eq!(unloaded_entry.latest_access_slot.load(Ordering::Relaxed), 1);
2332        assert_eq!(unloaded_entry.tx_usage_counter.load(Ordering::Relaxed), 3);
2333
2334        // Check that unload_program_entry() does its work
2335        let program_id = Pubkey::new_unique();
2336        cache.assign_program(program_id, entry.clone());
2337        cache.unload_program_entry(&program_id, &entry);
2338        assert!(cache.stats.evictions.contains_key(&program_id));
2339    }
2340
2341    #[test]
2342    fn test_fork_prune_find_first_ancestor() {
2343        let mut cache = new_mock_cache();
2344
2345        // Fork graph created for the test
2346        //                   0
2347        //                 /   \
2348        //                10    5
2349        //                |
2350        //                20
2351
2352        // Deploy program on slot 0, and slot 5.
2353        // Prune the fork that has slot 5. The cache should still have the program
2354        // deployed at slot 0.
2355        let program1 = Pubkey::new_unique();
2356        cache.assign_program(program1, new_test_entry(0, 1));
2357        cache.assign_program(program1, new_test_entry(5, 6));
2358
2359        cache.prune(10, 0);
2360
2361        let mut missing = get_entries_to_load(&cache, 20, &[program1]);
2362        let mut extracted = ProgramCacheForTxBatch::new(20, cache.environments.clone(), None, 0);
2363        cache.extract(&mut missing, &mut extracted, true);
2364
2365        // The cache should have the program deployed at slot 0
2366        assert_eq!(
2367            extracted
2368                .find(&program1)
2369                .expect("Did not find the program")
2370                .deployment_slot,
2371            0
2372        );
2373    }
2374
2375    #[test]
2376    fn test_prune_by_deployment_slot() {
2377        let mut cache = new_mock_cache();
2378
2379        // Fork graph created for the test
2380        //                   0
2381        //                 /   \
2382        //                10    5
2383        //                |
2384        //                20
2385
2386        // Deploy program on slot 0, and slot 5.
2387        // Prune the fork that has slot 5. The cache should still have the program
2388        // deployed at slot 0.
2389        let program1 = Pubkey::new_unique();
2390        cache.assign_program(program1, new_test_entry(0, 1));
2391        cache.assign_program(program1, new_test_entry(5, 6));
2392
2393        let program2 = Pubkey::new_unique();
2394        cache.assign_program(program2, new_test_entry(10, 11));
2395
2396        let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
2397        let mut extracted = ProgramCacheForTxBatch::new(20, cache.environments.clone(), None, 0);
2398        cache.extract(&mut missing, &mut extracted, true);
2399        assert!(match_slot(&extracted, &program1, 0, 20));
2400        assert!(match_slot(&extracted, &program2, 10, 20));
2401
2402        let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]);
2403        assert!(match_missing(&missing, &program2, false));
2404        let mut extracted = ProgramCacheForTxBatch::new(6, cache.environments.clone(), None, 0);
2405        cache.extract(&mut missing, &mut extracted, true);
2406        assert!(match_slot(&extracted, &program1, 5, 6));
2407
2408        // Pruning slot 5 will remove program1 entry deployed at slot 5.
2409        // On fork chaining from slot 5, the entry deployed at slot 0 will become visible.
2410        cache.prune_by_deployment_slot(5);
2411
2412        let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
2413        let mut extracted = ProgramCacheForTxBatch::new(20, cache.environments.clone(), None, 0);
2414        cache.extract(&mut missing, &mut extracted, true);
2415        assert!(match_slot(&extracted, &program1, 0, 20));
2416        assert!(match_slot(&extracted, &program2, 10, 20));
2417
2418        let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]);
2419        assert!(match_missing(&missing, &program2, false));
2420        let mut extracted = ProgramCacheForTxBatch::new(6, cache.environments.clone(), None, 0);
2421        cache.extract(&mut missing, &mut extracted, true);
2422        assert!(match_slot(&extracted, &program1, 0, 6));
2423
2424        // Pruning slot 10 will remove program2 entry deployed at slot 10.
2425        // As there is no other entry for program2, extract() will return it as missing.
2426        cache.prune_by_deployment_slot(10);
2427
2428        let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
2429        assert!(match_missing(&missing, &program2, false));
2430        let mut extracted = ProgramCacheForTxBatch::new(20, cache.environments.clone(), None, 0);
2431        cache.extract(&mut missing, &mut extracted, true);
2432        assert!(match_slot(&extracted, &program1, 0, 20));
2433    }
2434
2435    #[test]
2436    fn test_usable_entries_for_slot() {
2437        new_mock_cache();
2438        let tombstone = Arc::new(ProgramCacheEntry::new_tombstone(
2439            0,
2440            ProgramCacheEntryOwner::LoaderV2,
2441            ProgramCacheEntryType::Closed,
2442        ));
2443
2444        assert!(ProgramCache::matches_criteria(
2445            &tombstone,
2446            &ProgramCacheMatchCriteria::NoCriteria
2447        ));
2448
2449        assert!(ProgramCache::matches_criteria(
2450            &tombstone,
2451            &ProgramCacheMatchCriteria::Tombstone
2452        ));
2453
2454        assert!(ProgramCache::matches_criteria(
2455            &tombstone,
2456            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
2457        ));
2458
2459        assert!(!ProgramCache::matches_criteria(
2460            &tombstone,
2461            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
2462        ));
2463
2464        let program = new_test_entry(0, 1);
2465
2466        assert!(ProgramCache::matches_criteria(
2467            &program,
2468            &ProgramCacheMatchCriteria::NoCriteria
2469        ));
2470
2471        assert!(!ProgramCache::matches_criteria(
2472            &program,
2473            &ProgramCacheMatchCriteria::Tombstone
2474        ));
2475
2476        assert!(ProgramCache::matches_criteria(
2477            &program,
2478            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
2479        ));
2480
2481        assert!(!ProgramCache::matches_criteria(
2482            &program,
2483            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
2484        ));
2485
2486        let program = Arc::new(new_test_entry_with_usage(0, 1, AtomicU64::default()));
2487
2488        assert!(ProgramCache::matches_criteria(
2489            &program,
2490            &ProgramCacheMatchCriteria::NoCriteria
2491        ));
2492
2493        assert!(!ProgramCache::matches_criteria(
2494            &program,
2495            &ProgramCacheMatchCriteria::Tombstone
2496        ));
2497
2498        assert!(ProgramCache::matches_criteria(
2499            &program,
2500            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
2501        ));
2502
2503        assert!(!ProgramCache::matches_criteria(
2504            &program,
2505            &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
2506        ));
2507    }
2508}