Skip to main content

clone_solana_program_test/
lib.rs

1//! The solana-program-test provides a BanksClient-based test framework SBF programs
2#![allow(clippy::arithmetic_side_effects)]
3
4// Export tokio for test clients
5pub use tokio;
6use {
7    async_trait::async_trait,
8    base64::{prelude::BASE64_STANDARD, Engine},
9    chrono_humanize::{Accuracy, HumanTime, Tense},
10    clone_agave_feature_set::FEATURE_NAMES,
11    clone_solana_accounts_db::epoch_accounts_hash::EpochAccountsHash,
12    clone_solana_banks_client::start_client,
13    clone_solana_banks_server::banks_server::start_local_server,
14    clone_solana_bpf_loader_program::serialization::serialize_parameters,
15    clone_solana_compute_budget::compute_budget::ComputeBudget,
16    clone_solana_instruction::{error::InstructionError, Instruction},
17    clone_solana_log_collector::ic_msg,
18    clone_solana_program_runtime::{
19        invoke_context::BuiltinFunctionWithContext, loaded_programs::ProgramCacheEntry, stable_log,
20    },
21    clone_solana_runtime::{
22        accounts_background_service::{AbsRequestSender, SnapshotRequestKind},
23        bank::Bank,
24        bank_forks::BankForks,
25        commitment::BlockCommitmentCache,
26        genesis_utils::{create_genesis_config_with_leader_ex, GenesisConfigInfo},
27        runtime_config::RuntimeConfig,
28    },
29    clone_solana_sdk::{
30        account::{create_account_shared_data_for_test, Account, AccountSharedData},
31        account_info::AccountInfo,
32        clock::{Epoch, Slot},
33        entrypoint::{deserialize, ProgramResult, SUCCESS},
34        fee_calculator::{FeeRateGovernor, DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE},
35        genesis_config::{ClusterType, GenesisConfig},
36        hash::Hash,
37        native_token::sol_to_lamports,
38        poh_config::PohConfig,
39        program_error::{ProgramError, UNSUPPORTED_SYSVAR},
40        pubkey::Pubkey,
41        rent::Rent,
42        signature::{Keypair, Signer},
43        stable_layout::stable_instruction::StableInstruction,
44        sysvar::{Sysvar, SysvarId},
45    },
46    clone_solana_timings::ExecuteTimings,
47    clone_solana_vote_program::vote_state::{self, VoteState, VoteStateVersions},
48    log::*,
49    std::{
50        cell::RefCell,
51        collections::{HashMap, HashSet},
52        convert::TryFrom,
53        fs::File,
54        io::{self, Read},
55        mem::transmute,
56        panic::AssertUnwindSafe,
57        path::{Path, PathBuf},
58        sync::{
59            atomic::{AtomicBool, Ordering},
60            Arc, RwLock,
61        },
62        time::{Duration, Instant},
63    },
64    thiserror::Error,
65    tokio::task::JoinHandle,
66};
67// Export types so test clients can limit their solana crate dependencies
68pub use {
69    clone_solana_banks_client::{BanksClient, BanksClientError},
70    clone_solana_banks_interface::BanksTransactionResultWithMetadata,
71    clone_solana_program_runtime::invoke_context::InvokeContext,
72    clone_solana_transaction_context::IndexOfAccount,
73    solana_sbpf::{
74        error::EbpfError,
75        vm::{get_runtime_environment_key, EbpfVm},
76    },
77};
78
79pub mod programs;
80
81/// Errors from the program test environment
82#[derive(Error, Debug, PartialEq, Eq)]
83pub enum ProgramTestError {
84    /// The chosen warp slot is not in the future, so warp is not performed
85    #[error("Warp slot not in the future")]
86    InvalidWarpSlot,
87}
88
89thread_local! {
90    static INVOKE_CONTEXT: RefCell<Option<usize>> = const { RefCell::new(None) };
91}
92fn set_invoke_context(new: &mut InvokeContext) {
93    INVOKE_CONTEXT.with(|invoke_context| unsafe {
94        invoke_context.replace(Some(transmute::<&mut InvokeContext, usize>(new)))
95    });
96}
97fn get_invoke_context<'a, 'b>() -> &'a mut InvokeContext<'b> {
98    let ptr = INVOKE_CONTEXT.with(|invoke_context| match *invoke_context.borrow() {
99        Some(val) => val,
100        None => panic!("Invoke context not set!"),
101    });
102    unsafe { transmute::<usize, &mut InvokeContext>(ptr) }
103}
104
105pub fn invoke_builtin_function(
106    builtin_function: clone_solana_sdk::entrypoint::ProcessInstruction,
107    invoke_context: &mut InvokeContext,
108) -> Result<u64, Box<dyn std::error::Error>> {
109    set_invoke_context(invoke_context);
110
111    let transaction_context = &invoke_context.transaction_context;
112    let instruction_context = transaction_context.get_current_instruction_context()?;
113    let instruction_account_indices = 0..instruction_context.get_number_of_instruction_accounts();
114
115    // mock builtin program must consume units
116    invoke_context.consume_checked(1)?;
117
118    let log_collector = invoke_context.get_log_collector();
119    let program_id = instruction_context.get_last_program_key(transaction_context)?;
120    stable_log::program_invoke(
121        &log_collector,
122        program_id,
123        invoke_context.get_stack_height(),
124    );
125
126    // Copy indices_in_instruction into a HashSet to ensure there are no duplicates
127    let deduplicated_indices: HashSet<IndexOfAccount> = instruction_account_indices.collect();
128
129    // Serialize entrypoint parameters with SBF ABI
130    let mask_out_rent_epoch_in_vm_serialization = invoke_context
131        .get_feature_set()
132        .is_active(&clone_agave_feature_set::mask_out_rent_epoch_in_vm_serialization::id());
133    let (mut parameter_bytes, _regions, _account_lengths) = serialize_parameters(
134        transaction_context,
135        instruction_context,
136        true, // copy_account_data // There is no VM so direct mapping can not be implemented here
137        mask_out_rent_epoch_in_vm_serialization,
138    )?;
139
140    // Deserialize data back into instruction params
141    let (program_id, account_infos, input) =
142        unsafe { deserialize(&mut parameter_bytes.as_slice_mut()[0] as *mut u8) };
143
144    // Execute the program
145    match std::panic::catch_unwind(AssertUnwindSafe(|| {
146        builtin_function(program_id, &account_infos, input)
147    })) {
148        Ok(program_result) => {
149            program_result.map_err(|program_error| {
150                let err = InstructionError::from(u64::from(program_error));
151                stable_log::program_failure(&log_collector, program_id, &err);
152                let err: Box<dyn std::error::Error> = Box::new(err);
153                err
154            })?;
155        }
156        Err(_panic_error) => {
157            let err = InstructionError::ProgramFailedToComplete;
158            stable_log::program_failure(&log_collector, program_id, &err);
159            let err: Box<dyn std::error::Error> = Box::new(err);
160            Err(err)?;
161        }
162    };
163
164    stable_log::program_success(&log_collector, program_id);
165
166    // Lookup table for AccountInfo
167    let account_info_map: HashMap<_, _> = account_infos.into_iter().map(|a| (a.key, a)).collect();
168
169    // Re-fetch the instruction context. The previous reference may have been
170    // invalidated due to the `set_invoke_context` in a CPI.
171    let transaction_context = &invoke_context.transaction_context;
172    let instruction_context = transaction_context.get_current_instruction_context()?;
173
174    // Commit AccountInfo changes back into KeyedAccounts
175    for i in deduplicated_indices.into_iter() {
176        let mut borrowed_account =
177            instruction_context.try_borrow_instruction_account(transaction_context, i)?;
178        if borrowed_account.is_writable() {
179            if let Some(account_info) = account_info_map.get(borrowed_account.get_key()) {
180                if borrowed_account.get_lamports() != account_info.lamports() {
181                    borrowed_account.set_lamports(account_info.lamports())?;
182                }
183
184                if borrowed_account
185                    .can_data_be_resized(account_info.data_len())
186                    .is_ok()
187                    && borrowed_account.can_data_be_changed().is_ok()
188                {
189                    borrowed_account.set_data_from_slice(&account_info.data.borrow())?;
190                }
191                if borrowed_account.get_owner() != account_info.owner {
192                    borrowed_account.set_owner(account_info.owner.as_ref())?;
193                }
194            }
195        }
196    }
197
198    Ok(0)
199}
200
201/// Converts a `solana-program`-style entrypoint into the runtime's entrypoint style, for
202/// use with `ProgramTest::add_program`
203#[macro_export]
204macro_rules! processor {
205    ($builtin_function:expr) => {
206        Some(|vm, _arg0, _arg1, _arg2, _arg3, _arg4| {
207            let vm = unsafe {
208                &mut *((vm as *mut u64).offset(-($crate::get_runtime_environment_key() as isize))
209                    as *mut $crate::EbpfVm<$crate::InvokeContext>)
210            };
211            vm.program_result =
212                $crate::invoke_builtin_function($builtin_function, vm.context_object_pointer)
213                    .map_err(|err| $crate::EbpfError::SyscallError(err))
214                    .into();
215        })
216    };
217}
218
219fn get_sysvar<T: Default + Sysvar + Sized + serde::de::DeserializeOwned + Clone>(
220    sysvar: Result<Arc<T>, InstructionError>,
221    var_addr: *mut u8,
222) -> u64 {
223    let invoke_context = get_invoke_context();
224    if invoke_context
225        .consume_checked(invoke_context.get_compute_budget().sysvar_base_cost + T::size_of() as u64)
226        .is_err()
227    {
228        panic!("Exceeded compute budget");
229    }
230
231    match sysvar {
232        Ok(sysvar_data) => unsafe {
233            *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
234            SUCCESS
235        },
236        Err(_) => UNSUPPORTED_SYSVAR,
237    }
238}
239
240struct SyscallStubs {}
241impl clone_solana_sdk::program_stubs::SyscallStubs for SyscallStubs {
242    fn sol_log(&self, message: &str) {
243        let invoke_context = get_invoke_context();
244        ic_msg!(invoke_context, "Program log: {}", message);
245    }
246
247    fn sol_invoke_signed(
248        &self,
249        instruction: &Instruction,
250        account_infos: &[AccountInfo],
251        signers_seeds: &[&[&[u8]]],
252    ) -> ProgramResult {
253        let instruction = StableInstruction::from(instruction.clone());
254        let invoke_context = get_invoke_context();
255        let log_collector = invoke_context.get_log_collector();
256        let transaction_context = &invoke_context.transaction_context;
257        let instruction_context = transaction_context
258            .get_current_instruction_context()
259            .unwrap();
260        let caller = instruction_context
261            .get_last_program_key(transaction_context)
262            .unwrap();
263
264        stable_log::program_invoke(
265            &log_collector,
266            &instruction.program_id,
267            invoke_context.get_stack_height(),
268        );
269
270        let signers = signers_seeds
271            .iter()
272            .map(|seeds| Pubkey::create_program_address(seeds, caller).unwrap())
273            .collect::<Vec<_>>();
274
275        let (instruction_accounts, program_indices) = invoke_context
276            .prepare_instruction(&instruction, &signers)
277            .unwrap();
278
279        // Copy caller's account_info modifications into invoke_context accounts
280        let transaction_context = &invoke_context.transaction_context;
281        let instruction_context = transaction_context
282            .get_current_instruction_context()
283            .unwrap();
284        let mut account_indices = Vec::with_capacity(instruction_accounts.len());
285        for instruction_account in instruction_accounts.iter() {
286            let account_key = transaction_context
287                .get_key_of_account_at_index(instruction_account.index_in_transaction)
288                .unwrap();
289            let account_info_index = account_infos
290                .iter()
291                .position(|account_info| account_info.unsigned_key() == account_key)
292                .ok_or(InstructionError::MissingAccount)
293                .unwrap();
294            let account_info = &account_infos[account_info_index];
295            let mut borrowed_account = instruction_context
296                .try_borrow_instruction_account(
297                    transaction_context,
298                    instruction_account.index_in_caller,
299                )
300                .unwrap();
301            if borrowed_account.get_lamports() != account_info.lamports() {
302                borrowed_account
303                    .set_lamports(account_info.lamports())
304                    .unwrap();
305            }
306            let account_info_data = account_info.try_borrow_data().unwrap();
307            // The redundant check helps to avoid the expensive data comparison if we can
308            match borrowed_account
309                .can_data_be_resized(account_info_data.len())
310                .and_then(|_| borrowed_account.can_data_be_changed())
311            {
312                Ok(()) => borrowed_account
313                    .set_data_from_slice(&account_info_data)
314                    .unwrap(),
315                Err(err) if borrowed_account.get_data() != *account_info_data => {
316                    panic!("{err:?}");
317                }
318                _ => {}
319            }
320            // Change the owner at the end so that we are allowed to change the lamports and data before
321            if borrowed_account.get_owner() != account_info.owner {
322                borrowed_account
323                    .set_owner(account_info.owner.as_ref())
324                    .unwrap();
325            }
326            if instruction_account.is_writable {
327                account_indices.push((instruction_account.index_in_caller, account_info_index));
328            }
329        }
330
331        let mut compute_units_consumed = 0;
332        invoke_context
333            .process_instruction(
334                &instruction.data,
335                &instruction_accounts,
336                &program_indices,
337                &mut compute_units_consumed,
338                &mut ExecuteTimings::default(),
339            )
340            .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
341
342        // Copy invoke_context accounts modifications into caller's account_info
343        let transaction_context = &invoke_context.transaction_context;
344        let instruction_context = transaction_context
345            .get_current_instruction_context()
346            .unwrap();
347        for (index_in_caller, account_info_index) in account_indices.into_iter() {
348            let borrowed_account = instruction_context
349                .try_borrow_instruction_account(transaction_context, index_in_caller)
350                .unwrap();
351            let account_info = &account_infos[account_info_index];
352            **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
353            if account_info.owner != borrowed_account.get_owner() {
354                // TODO Figure out a better way to allow the System Program to set the account owner
355                #[allow(clippy::transmute_ptr_to_ptr)]
356                #[allow(mutable_transmutes)]
357                let account_info_mut =
358                    unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
359                *account_info_mut = *borrowed_account.get_owner();
360            }
361
362            let new_data = borrowed_account.get_data();
363            let new_len = new_data.len();
364
365            // Resize account_info data
366            if account_info.data_len() != new_len {
367                account_info.realloc(new_len, false)?;
368            }
369
370            // Clone the data
371            let mut data = account_info.try_borrow_mut_data()?;
372            data.clone_from_slice(new_data);
373        }
374
375        stable_log::program_success(&log_collector, &instruction.program_id);
376        Ok(())
377    }
378
379    fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
380        get_sysvar(
381            get_invoke_context().get_sysvar_cache().get_clock(),
382            var_addr,
383        )
384    }
385
386    fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
387        get_sysvar(
388            get_invoke_context().get_sysvar_cache().get_epoch_schedule(),
389            var_addr,
390        )
391    }
392
393    fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
394        get_sysvar(
395            get_invoke_context().get_sysvar_cache().get_epoch_rewards(),
396            var_addr,
397        )
398    }
399
400    #[allow(deprecated)]
401    fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
402        get_sysvar(get_invoke_context().get_sysvar_cache().get_fees(), var_addr)
403    }
404
405    fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
406        get_sysvar(get_invoke_context().get_sysvar_cache().get_rent(), var_addr)
407    }
408
409    fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
410        get_sysvar(
411            get_invoke_context()
412                .get_sysvar_cache()
413                .get_last_restart_slot(),
414            var_addr,
415        )
416    }
417
418    fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
419        let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
420        Some((*program_id, data.to_vec()))
421    }
422
423    fn sol_set_return_data(&self, data: &[u8]) {
424        let invoke_context = get_invoke_context();
425        let transaction_context = &mut invoke_context.transaction_context;
426        let instruction_context = transaction_context
427            .get_current_instruction_context()
428            .unwrap();
429        let caller = *instruction_context
430            .get_last_program_key(transaction_context)
431            .unwrap();
432        transaction_context
433            .set_return_data(caller, data.to_vec())
434            .unwrap();
435    }
436
437    fn sol_get_stack_height(&self) -> u64 {
438        let invoke_context = get_invoke_context();
439        invoke_context.get_stack_height().try_into().unwrap()
440    }
441}
442
443pub fn find_file(filename: &str) -> Option<PathBuf> {
444    for dir in default_shared_object_dirs() {
445        let candidate = dir.join(filename);
446        if candidate.exists() {
447            return Some(candidate);
448        }
449    }
450    None
451}
452
453fn default_shared_object_dirs() -> Vec<PathBuf> {
454    let mut search_path = vec![];
455    if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
456        search_path.push(PathBuf::from(bpf_out_dir));
457    } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
458        search_path.push(PathBuf::from(bpf_out_dir));
459    }
460    search_path.push(PathBuf::from("tests/fixtures"));
461    if let Ok(dir) = std::env::current_dir() {
462        search_path.push(dir);
463    }
464    trace!("SBF .so search path: {:?}", search_path);
465    search_path
466}
467
468pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
469    let path = path.as_ref();
470    let mut file = File::open(path)
471        .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
472
473    let mut file_data = Vec::new();
474    file.read_to_end(&mut file_data)
475        .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
476    file_data
477}
478
479pub struct ProgramTest {
480    accounts: Vec<(Pubkey, AccountSharedData)>,
481    genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
482    builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
483    compute_max_units: Option<u64>,
484    prefer_bpf: bool,
485    deactivate_feature_set: HashSet<Pubkey>,
486    transaction_account_lock_limit: Option<usize>,
487}
488
489impl Default for ProgramTest {
490    /// Initialize a new ProgramTest
491    ///
492    /// If the `BPF_OUT_DIR` environment variable is defined, BPF programs will be preferred over
493    /// over a native instruction processor.  The `ProgramTest::prefer_bpf()` method may be
494    /// used to override this preference at runtime.  `cargo test-bpf` will set `BPF_OUT_DIR`
495    /// automatically.
496    ///
497    /// SBF program shared objects and account data files are searched for in
498    /// * the value of the `BPF_OUT_DIR` environment variable
499    /// * the `tests/fixtures` sub-directory
500    /// * the current working directory
501    ///
502    fn default() -> Self {
503        clone_solana_logger::setup_with_default(
504            "solana_sbpf::vm=debug,\
505             clone_solana_runtime::message_processor=debug,\
506             clone_solana_runtime::system_instruction_processor=trace,\
507             clone_solana_program_test=info",
508        );
509        let prefer_bpf =
510            std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
511
512        Self {
513            accounts: vec![],
514            genesis_accounts: vec![],
515            builtin_programs: vec![],
516            compute_max_units: None,
517            prefer_bpf,
518            deactivate_feature_set: HashSet::default(),
519            transaction_account_lock_limit: None,
520        }
521    }
522}
523
524impl ProgramTest {
525    /// Create a `ProgramTest`.
526    ///
527    /// This is a wrapper around [`default`] and [`add_program`]. See their documentation for more
528    /// details.
529    ///
530    /// [`default`]: #method.default
531    /// [`add_program`]: #method.add_program
532    pub fn new(
533        program_name: &'static str,
534        program_id: Pubkey,
535        builtin_function: Option<BuiltinFunctionWithContext>,
536    ) -> Self {
537        let mut me = Self::default();
538        me.add_program(program_name, program_id, builtin_function);
539        me
540    }
541
542    /// Override default SBF program selection
543    pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
544        self.prefer_bpf = prefer_bpf;
545    }
546
547    /// Override the default maximum compute units
548    pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
549        debug_assert!(
550            compute_max_units <= i64::MAX as u64,
551            "Compute unit limit must fit in `i64::MAX`"
552        );
553        self.compute_max_units = Some(compute_max_units);
554    }
555
556    /// Override the default transaction account lock limit
557    pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
558        self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
559    }
560
561    /// Add an account to the test environment's genesis config.
562    pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
563        self.genesis_accounts
564            .push((address, AccountSharedData::from(account)));
565    }
566
567    /// Add an account to the test environment
568    pub fn add_account(&mut self, address: Pubkey, account: Account) {
569        self.accounts
570            .push((address, AccountSharedData::from(account)));
571    }
572
573    /// Add an account to the test environment with the account data in the provided `filename`
574    pub fn add_account_with_file_data(
575        &mut self,
576        address: Pubkey,
577        lamports: u64,
578        owner: Pubkey,
579        filename: &str,
580    ) {
581        self.add_account(
582            address,
583            Account {
584                lamports,
585                data: read_file(find_file(filename).unwrap_or_else(|| {
586                    panic!("Unable to locate {filename}");
587                })),
588                owner,
589                executable: false,
590                rent_epoch: 0,
591            },
592        );
593    }
594
595    /// Add an account to the test environment with the account data in the provided as a base 64
596    /// string
597    pub fn add_account_with_base64_data(
598        &mut self,
599        address: Pubkey,
600        lamports: u64,
601        owner: Pubkey,
602        data_base64: &str,
603    ) {
604        self.add_account(
605            address,
606            Account {
607                lamports,
608                data: BASE64_STANDARD
609                    .decode(data_base64)
610                    .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
611                owner,
612                executable: false,
613                rent_epoch: 0,
614            },
615        );
616    }
617
618    pub fn add_sysvar_account<S: Sysvar>(&mut self, address: Pubkey, sysvar: &S) {
619        let account = create_account_shared_data_for_test(sysvar);
620        self.add_account(address, account.into());
621    }
622
623    /// Add a BPF Upgradeable program to the test environment's genesis config.
624    ///
625    /// When testing BPF programs using the program ID of a runtime builtin
626    /// program - such as Core BPF programs - the program accounts must be
627    /// added to the genesis config in order to make them available to the new
628    /// Bank as it's being initialized.
629    ///
630    /// The presence of these program accounts will cause Bank to skip adding
631    /// the builtin version of the program, allowing the provided BPF program
632    /// to be used at the designated program ID instead.
633    ///
634    /// See https://github.com/anza-xyz/agave/blob/c038908600b8a1b0080229dea015d7fc9939c418/runtime/src/bank.rs#L5109-L5126.
635    pub fn add_upgradeable_program_to_genesis(
636        &mut self,
637        program_name: &'static str,
638        program_id: &Pubkey,
639    ) {
640        let program_file = find_file(&format!("{program_name}.so"))
641            .expect("Program file data not available for {program_name} ({program_id})");
642        let elf = read_file(program_file);
643        let program_accounts =
644            programs::bpf_loader_upgradeable_program_accounts(program_id, &elf, &Rent::default());
645        for (address, account) in program_accounts {
646            self.add_genesis_account(address, account);
647        }
648    }
649
650    /// Add a SBF program to the test environment.
651    ///
652    /// `program_name` will also be used to locate the SBF shared object in the current or fixtures
653    /// directory.
654    ///
655    /// If `builtin_function` is provided, the natively built-program may be used instead of the
656    /// SBF shared object depending on the `BPF_OUT_DIR` environment variable.
657    pub fn add_program(
658        &mut self,
659        program_name: &'static str,
660        program_id: Pubkey,
661        builtin_function: Option<BuiltinFunctionWithContext>,
662    ) {
663        let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
664            let data = read_file(&program_file);
665            info!(
666                "\"{}\" SBF program from {}{}",
667                program_name,
668                program_file.display(),
669                std::fs::metadata(&program_file)
670                    .map(|metadata| {
671                        metadata
672                            .modified()
673                            .map(|time| {
674                                format!(
675                                    ", modified {}",
676                                    HumanTime::from(time)
677                                        .to_text_en(Accuracy::Precise, Tense::Past)
678                                )
679                            })
680                            .ok()
681                    })
682                    .ok()
683                    .flatten()
684                    .unwrap_or_default()
685            );
686
687            this.add_account(
688                program_id,
689                Account {
690                    lamports: Rent::default().minimum_balance(data.len()).max(1),
691                    data,
692                    owner: clone_solana_sdk::bpf_loader::id(),
693                    executable: true,
694                    rent_epoch: 0,
695                },
696            );
697        };
698
699        let warn_invalid_program_name = || {
700            let valid_program_names = default_shared_object_dirs()
701                .iter()
702                .filter_map(|dir| dir.read_dir().ok())
703                .flat_map(|read_dir| {
704                    read_dir.filter_map(|entry| {
705                        let path = entry.ok()?.path();
706                        if !path.is_file() {
707                            return None;
708                        }
709                        match path.extension()?.to_str()? {
710                            "so" => Some(path.file_stem()?.to_os_string()),
711                            _ => None,
712                        }
713                    })
714                })
715                .collect::<Vec<_>>();
716
717            if valid_program_names.is_empty() {
718                // This should be unreachable as `test-bpf` should guarantee at least one shared
719                // object exists somewhere.
720                warn!("No SBF shared objects found.");
721                return;
722            }
723
724            warn!(
725                "Possible bogus program name. Ensure the program name ({}) \
726                matches one of the following recognizable program names:",
727                program_name,
728            );
729            for name in valid_program_names {
730                warn!(" - {}", name.to_str().unwrap());
731            }
732        };
733
734        let program_file = find_file(&format!("{program_name}.so"));
735        match (self.prefer_bpf, program_file, builtin_function) {
736            // If SBF is preferred (i.e., `test-sbf` is invoked) and a BPF shared object exists,
737            // use that as the program data.
738            (true, Some(file), _) => add_bpf(self, file),
739
740            // If SBF is not required (i.e., we were invoked with `test`), use the provided
741            // processor function as is.
742            (false, _, Some(builtin_function)) => {
743                self.add_builtin_program(program_name, program_id, builtin_function)
744            }
745
746            // Invalid: `test-sbf` invocation with no matching SBF shared object.
747            (true, None, _) => {
748                warn_invalid_program_name();
749                panic!("Program file data not available for {program_name} ({program_id})");
750            }
751
752            // Invalid: regular `test` invocation without a processor.
753            (false, _, None) => {
754                panic!("Program processor not available for {program_name} ({program_id})");
755            }
756        }
757    }
758
759    /// Add a builtin program to the test environment.
760    ///
761    /// Note that builtin programs are responsible for their own `stable_log` output.
762    pub fn add_builtin_program(
763        &mut self,
764        program_name: &'static str,
765        program_id: Pubkey,
766        builtin_function: BuiltinFunctionWithContext,
767    ) {
768        info!("\"{}\" builtin program", program_name);
769        self.builtin_programs.push((
770            program_id,
771            program_name,
772            ProgramCacheEntry::new_builtin(0, program_name.len(), builtin_function),
773        ));
774    }
775
776    /// Deactivate a runtime feature.
777    ///
778    /// Note that all features are activated by default.
779    pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
780        self.deactivate_feature_set.insert(feature_id);
781    }
782
783    fn setup_bank(
784        &mut self,
785    ) -> (
786        Arc<RwLock<BankForks>>,
787        Arc<RwLock<BlockCommitmentCache>>,
788        Hash,
789        GenesisConfigInfo,
790    ) {
791        {
792            use std::sync::Once;
793            static ONCE: Once = Once::new();
794
795            ONCE.call_once(|| {
796                clone_solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
797            });
798        }
799
800        let rent = Rent::default();
801        let fee_rate_governor = FeeRateGovernor {
802            // Initialize with a non-zero fee
803            lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
804            ..FeeRateGovernor::default()
805        };
806        let bootstrap_validator_pubkey = Pubkey::new_unique();
807        let bootstrap_validator_stake_lamports =
808            rent.minimum_balance(VoteState::size_of()) + sol_to_lamports(1_000_000.0);
809
810        let mint_keypair = Keypair::new();
811        let voting_keypair = Keypair::new();
812
813        let mut genesis_config = create_genesis_config_with_leader_ex(
814            sol_to_lamports(1_000_000.0),
815            &mint_keypair.pubkey(),
816            &bootstrap_validator_pubkey,
817            &voting_keypair.pubkey(),
818            &Pubkey::new_unique(),
819            bootstrap_validator_stake_lamports,
820            42,
821            fee_rate_governor,
822            rent.clone(),
823            ClusterType::Development,
824            std::mem::take(&mut self.genesis_accounts),
825        );
826
827        // Remove features tagged to deactivate
828        for deactivate_feature_pk in &self.deactivate_feature_set {
829            if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
830                match genesis_config.accounts.remove(deactivate_feature_pk) {
831                    Some(_) => debug!("Feature for {:?} deactivated", deactivate_feature_pk),
832                    None => warn!(
833                        "Feature {:?} set for deactivation not found in genesis_config account list, ignored.",
834                        deactivate_feature_pk
835                    ),
836                }
837            } else {
838                warn!(
839                    "Feature {:?} set for deactivation is not a known Feature public key",
840                    deactivate_feature_pk
841                );
842            }
843        }
844
845        let target_tick_duration = Duration::from_micros(100);
846        genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
847        debug!("Payer address: {}", mint_keypair.pubkey());
848        debug!("Genesis config: {}", genesis_config);
849
850        let bank = Bank::new_with_paths(
851            &genesis_config,
852            Arc::new(RuntimeConfig {
853                compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
854                    compute_unit_limit: max_units,
855                    ..ComputeBudget::default()
856                }),
857                transaction_account_lock_limit: self.transaction_account_lock_limit,
858                ..RuntimeConfig::default()
859            }),
860            Vec::default(),
861            None,
862            None,
863            false,
864            None,
865            None,
866            None,
867            Arc::default(),
868            None,
869            None,
870        );
871
872        // Add commonly-used SPL programs as a convenience to the user
873        for (program_id, account) in programs::spl_programs(&rent).iter() {
874            bank.store_account(program_id, account);
875        }
876
877        // Add migrated Core BPF programs.
878        for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
879            genesis_config.accounts.contains_key(feature_id)
880        })
881        .iter()
882        {
883            bank.store_account(program_id, account);
884        }
885
886        // User-supplied additional builtins
887        let mut builtin_programs = Vec::new();
888        std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
889        for (program_id, name, builtin) in builtin_programs.into_iter() {
890            bank.add_builtin(program_id, name, builtin);
891        }
892
893        for (address, account) in self.accounts.iter() {
894            if bank.get_account(address).is_some() {
895                info!("Overriding account at {}", address);
896            }
897            bank.store_account(address, account);
898        }
899        bank.set_capitalization();
900        // Advance beyond slot 0 for a slightly more realistic test environment
901        let bank = {
902            let bank = Arc::new(bank);
903            bank.fill_bank_with_ticks_for_tests();
904            let bank = Bank::new_from_parent(bank.clone(), bank.collector_id(), bank.slot() + 1);
905            debug!("Bank slot: {}", bank.slot());
906            bank
907        };
908        let slot = bank.slot();
909        let last_blockhash = bank.last_blockhash();
910        let bank_forks = BankForks::new_rw_arc(bank);
911        let block_commitment_cache = Arc::new(RwLock::new(
912            BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
913        ));
914
915        (
916            bank_forks,
917            block_commitment_cache,
918            last_blockhash,
919            GenesisConfigInfo {
920                genesis_config,
921                mint_keypair,
922                voting_keypair,
923                validator_pubkey: bootstrap_validator_pubkey,
924            },
925        )
926    }
927
928    pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
929        let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
930        let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
931        let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
932        let transport = start_local_server(
933            bank_forks.clone(),
934            block_commitment_cache.clone(),
935            target_tick_duration,
936        )
937        .await;
938        let banks_client = start_client(transport)
939            .await
940            .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
941
942        // Run a simulated PohService to provide the client with new blockhashes.  New blockhashes
943        // are required when sending multiple otherwise identical transactions in series from a
944        // test
945        tokio::spawn(async move {
946            loop {
947                tokio::time::sleep(target_slot_duration).await;
948                bank_forks
949                    .read()
950                    .unwrap()
951                    .working_bank()
952                    .register_unique_recent_blockhash_for_test();
953            }
954        });
955
956        (banks_client, gci.mint_keypair, last_blockhash)
957    }
958
959    /// Start the test client
960    ///
961    /// Returns a `BanksClient` interface into the test environment as well as a payer `Keypair`
962    /// with SOL for sending transactions
963    pub async fn start_with_context(mut self) -> ProgramTestContext {
964        let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
965        let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
966        let transport = start_local_server(
967            bank_forks.clone(),
968            block_commitment_cache.clone(),
969            target_tick_duration,
970        )
971        .await;
972        let banks_client = start_client(transport)
973            .await
974            .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
975
976        ProgramTestContext::new(
977            bank_forks,
978            block_commitment_cache,
979            banks_client,
980            last_blockhash,
981            gci,
982        )
983    }
984}
985
986#[async_trait]
987pub trait ProgramTestBanksClientExt {
988    /// Get a new latest blockhash, similar in spirit to RpcClient::get_latest_blockhash()
989    async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
990}
991
992#[async_trait]
993impl ProgramTestBanksClientExt for BanksClient {
994    async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
995        let mut num_retries = 0;
996        let start = Instant::now();
997        while start.elapsed().as_secs() < 5 {
998            let new_blockhash = self.get_latest_blockhash().await?;
999            if new_blockhash != *blockhash {
1000                return Ok(new_blockhash);
1001            }
1002            debug!("Got same blockhash ({:?}), will retry...", blockhash);
1003
1004            tokio::time::sleep(Duration::from_millis(200)).await;
1005            num_retries += 1;
1006        }
1007
1008        Err(io::Error::new(
1009            io::ErrorKind::Other,
1010            format!(
1011                "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1012                start.elapsed().as_millis(),
1013                num_retries,
1014                blockhash
1015            ),
1016        ))
1017    }
1018}
1019
1020struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1021
1022impl<T> Drop for DroppableTask<T> {
1023    fn drop(&mut self) {
1024        self.0.store(true, Ordering::Relaxed);
1025        trace!(
1026            "stopping task, which is currently {}",
1027            if self.1.is_finished() {
1028                "finished"
1029            } else {
1030                "running"
1031            }
1032        );
1033    }
1034}
1035
1036pub struct ProgramTestContext {
1037    pub banks_client: BanksClient,
1038    pub last_blockhash: Hash,
1039    pub payer: Keypair,
1040    genesis_config: GenesisConfig,
1041    bank_forks: Arc<RwLock<BankForks>>,
1042    block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1043    _bank_task: DroppableTask<()>,
1044}
1045
1046impl ProgramTestContext {
1047    fn new(
1048        bank_forks: Arc<RwLock<BankForks>>,
1049        block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1050        banks_client: BanksClient,
1051        last_blockhash: Hash,
1052        genesis_config_info: GenesisConfigInfo,
1053    ) -> Self {
1054        // Run a simulated PohService to provide the client with new blockhashes.  New blockhashes
1055        // are required when sending multiple otherwise identical transactions in series from a
1056        // test
1057        let running_bank_forks = bank_forks.clone();
1058        let target_tick_duration = genesis_config_info
1059            .genesis_config
1060            .poh_config
1061            .target_tick_duration;
1062        let target_slot_duration =
1063            target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1064        let exit = Arc::new(AtomicBool::new(false));
1065        let bank_task = DroppableTask(
1066            exit.clone(),
1067            tokio::spawn(async move {
1068                loop {
1069                    if exit.load(Ordering::Relaxed) {
1070                        break;
1071                    }
1072                    tokio::time::sleep(target_slot_duration).await;
1073                    running_bank_forks
1074                        .read()
1075                        .unwrap()
1076                        .working_bank()
1077                        .register_unique_recent_blockhash_for_test();
1078                }
1079            }),
1080        );
1081
1082        Self {
1083            banks_client,
1084            last_blockhash,
1085            payer: genesis_config_info.mint_keypair,
1086            genesis_config: genesis_config_info.genesis_config,
1087            bank_forks,
1088            block_commitment_cache,
1089            _bank_task: bank_task,
1090        }
1091    }
1092
1093    pub fn genesis_config(&self) -> &GenesisConfig {
1094        &self.genesis_config
1095    }
1096
1097    /// Manually increment vote credits for the current epoch in the specified vote account to simulate validator voting activity
1098    pub fn increment_vote_account_credits(
1099        &mut self,
1100        vote_account_address: &Pubkey,
1101        number_of_credits: u64,
1102    ) {
1103        let bank_forks = self.bank_forks.read().unwrap();
1104        let bank = bank_forks.working_bank();
1105
1106        // generate some vote activity for rewards
1107        let mut vote_account = bank.get_account(vote_account_address).unwrap();
1108        let mut vote_state = vote_state::from(&vote_account).unwrap();
1109
1110        let epoch = bank.epoch();
1111        for _ in 0..number_of_credits {
1112            vote_state.increment_credits(epoch, 1);
1113        }
1114        let versioned = VoteStateVersions::new_current(vote_state);
1115        vote_state::to(&versioned, &mut vote_account).unwrap();
1116        bank.store_account(vote_account_address, &vote_account);
1117    }
1118
1119    /// Create or overwrite an account, subverting normal runtime checks.
1120    ///
1121    /// This method exists to make it easier to set up artificial situations
1122    /// that would be difficult to replicate by sending individual transactions.
1123    /// Beware that it can be used to create states that would not be reachable
1124    /// by sending transactions!
1125    pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1126        let bank_forks = self.bank_forks.read().unwrap();
1127        let bank = bank_forks.working_bank();
1128        bank.store_account(address, account);
1129    }
1130
1131    /// Create or overwrite a sysvar, subverting normal runtime checks.
1132    ///
1133    /// This method exists to make it easier to set up artificial situations
1134    /// that would be difficult to replicate on a new test cluster. Beware
1135    /// that it can be used to create states that would not be reachable
1136    /// under normal conditions!
1137    pub fn set_sysvar<T: SysvarId + Sysvar>(&self, sysvar: &T) {
1138        let bank_forks = self.bank_forks.read().unwrap();
1139        let bank = bank_forks.working_bank();
1140        bank.set_sysvar_for_tests(sysvar);
1141    }
1142
1143    /// Force the working bank ahead to a new slot
1144    pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1145        let mut bank_forks = self.bank_forks.write().unwrap();
1146        let bank = bank_forks.working_bank();
1147
1148        // Fill ticks until a new blockhash is recorded, otherwise retried transactions will have
1149        // the same signature
1150        bank.fill_bank_with_ticks_for_tests();
1151
1152        // Ensure that we are actually progressing forward
1153        let working_slot = bank.slot();
1154        if warp_slot <= working_slot {
1155            return Err(ProgramTestError::InvalidWarpSlot);
1156        }
1157
1158        // Warp ahead to one slot *before* the desired slot because the bank
1159        // from Bank::warp_from_parent() is frozen. If the desired slot is one
1160        // slot *after* the working_slot, no need to warp at all.
1161        let pre_warp_slot = warp_slot - 1;
1162        let warp_bank = if pre_warp_slot == working_slot {
1163            bank.freeze();
1164            bank
1165        } else {
1166            bank_forks
1167                .insert(Bank::warp_from_parent(
1168                    bank,
1169                    &Pubkey::default(),
1170                    pre_warp_slot,
1171                    // some warping tests cannot use the append vecs because of the sequence of adding roots and flushing
1172                    clone_solana_accounts_db::accounts_db::CalcAccountsHashDataSource::IndexForTests,
1173                ))
1174                .clone_without_scheduler()
1175        };
1176
1177        let (snapshot_request_sender, snapshot_request_receiver) = crossbeam_channel::unbounded();
1178        let abs_request_sender = AbsRequestSender::new(snapshot_request_sender);
1179
1180        bank_forks
1181            .set_root(pre_warp_slot, &abs_request_sender, Some(pre_warp_slot))
1182            .unwrap();
1183
1184        // The call to `set_root()` above will send an EAH request.  Need to intercept and handle
1185        // all EpochAccountsHash requests so future rooted banks do not hang in Bank::freeze()
1186        // waiting for an in-flight EAH calculation to complete.
1187        snapshot_request_receiver
1188            .try_iter()
1189            .filter(|snapshot_request| {
1190                snapshot_request.request_kind == SnapshotRequestKind::EpochAccountsHash
1191            })
1192            .for_each(|snapshot_request| {
1193                snapshot_request
1194                    .snapshot_root_bank
1195                    .rc
1196                    .accounts
1197                    .accounts_db
1198                    .epoch_accounts_hash_manager
1199                    .set_valid(
1200                        EpochAccountsHash::new(Hash::new_unique()),
1201                        snapshot_request.snapshot_root_bank.slot(),
1202                    )
1203            });
1204
1205        // warp_bank is frozen so go forward to get unfrozen bank at warp_slot
1206        bank_forks.insert(Bank::new_from_parent(
1207            warp_bank,
1208            &Pubkey::default(),
1209            warp_slot,
1210        ));
1211
1212        // Update block commitment cache, otherwise banks server will poll at
1213        // the wrong slot
1214        let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1215        // HACK: The root set here should be `pre_warp_slot`, but since we're
1216        // in a testing environment, the root bank never updates after a warp.
1217        // The ticking thread only updates the working bank, and never the root
1218        // bank.
1219        w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1220
1221        let bank = bank_forks.working_bank();
1222        self.last_blockhash = bank.last_blockhash();
1223        Ok(())
1224    }
1225
1226    pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1227        let warp_slot = self
1228            .genesis_config
1229            .epoch_schedule
1230            .get_first_slot_in_epoch(warp_epoch);
1231        self.warp_to_slot(warp_slot)
1232    }
1233
1234    /// warp forward one more slot and force reward interval end
1235    pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1236        let mut bank_forks = self.bank_forks.write().unwrap();
1237        let bank = bank_forks.working_bank();
1238
1239        // Fill ticks until a new blockhash is recorded, otherwise retried transactions will have
1240        // the same signature
1241        bank.fill_bank_with_ticks_for_tests();
1242        let pre_warp_slot = bank.slot();
1243
1244        bank_forks
1245            .set_root(
1246                pre_warp_slot,
1247                &clone_solana_runtime::accounts_background_service::AbsRequestSender::default(),
1248                Some(pre_warp_slot),
1249            )
1250            .unwrap();
1251
1252        // warp_bank is frozen so go forward to get unfrozen bank at warp_slot
1253        let warp_slot = pre_warp_slot + 1;
1254        let mut warp_bank = Bank::new_from_parent(bank, &Pubkey::default(), warp_slot);
1255
1256        warp_bank.force_reward_interval_end_for_tests();
1257        bank_forks.insert(warp_bank);
1258
1259        // Update block commitment cache, otherwise banks server will poll at
1260        // the wrong slot
1261        let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1262        // HACK: The root set here should be `pre_warp_slot`, but since we're
1263        // in a testing environment, the root bank never updates after a warp.
1264        // The ticking thread only updates the working bank, and never the root
1265        // bank.
1266        w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1267
1268        let bank = bank_forks.working_bank();
1269        self.last_blockhash = bank.last_blockhash();
1270        Ok(())
1271    }
1272
1273    /// Get a new latest blockhash, similar in spirit to RpcClient::get_latest_blockhash()
1274    pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1275        let blockhash = self
1276            .banks_client
1277            .get_new_latest_blockhash(&self.last_blockhash)
1278            .await?;
1279        self.last_blockhash = blockhash;
1280        Ok(blockhash)
1281    }
1282
1283    /// record a hard fork slot in working bank; should be in the past
1284    pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1285        self.bank_forks
1286            .read()
1287            .unwrap()
1288            .working_bank()
1289            .register_hard_fork(hard_fork_slot)
1290    }
1291}