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