solana_program_test/
lib.rs

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