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