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_sbpf::{
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_sbpf::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.clone(),
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).iter() {
870            bank.store_account(program_id, account);
871        }
872
873        // Add migrated Core BPF programs.
874        for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
875            genesis_config.accounts.contains_key(feature_id)
876        })
877        .iter()
878        {
879            bank.store_account(program_id, account);
880        }
881
882        // User-supplied additional builtins
883        let mut builtin_programs = Vec::new();
884        std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
885        for (program_id, name, builtin) in builtin_programs.into_iter() {
886            bank.add_builtin(program_id, name, builtin);
887        }
888
889        for (address, account) in self.accounts.iter() {
890            if bank.get_account(address).is_some() {
891                info!("Overriding account at {}", address);
892            }
893            bank.store_account(address, account);
894        }
895        bank.set_capitalization();
896        // Advance beyond slot 0 for a slightly more realistic test environment
897        let bank = {
898            let bank = Arc::new(bank);
899            bank.fill_bank_with_ticks_for_tests();
900            let bank = Bank::new_from_parent(bank.clone(), bank.collector_id(), bank.slot() + 1);
901            debug!("Bank slot: {}", bank.slot());
902            bank
903        };
904        let slot = bank.slot();
905        let last_blockhash = bank.last_blockhash();
906        let bank_forks = BankForks::new_rw_arc(bank);
907        let block_commitment_cache = Arc::new(RwLock::new(
908            BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
909        ));
910
911        (
912            bank_forks,
913            block_commitment_cache,
914            last_blockhash,
915            GenesisConfigInfo {
916                genesis_config,
917                mint_keypair,
918                voting_keypair,
919                validator_pubkey: bootstrap_validator_pubkey,
920            },
921        )
922    }
923
924    pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
925        let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
926        let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
927        let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
928        let transport = start_local_server(
929            bank_forks.clone(),
930            block_commitment_cache.clone(),
931            target_tick_duration,
932        )
933        .await;
934        let banks_client = start_client(transport)
935            .await
936            .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
937
938        // Run a simulated PohService to provide the client with new blockhashes.  New blockhashes
939        // are required when sending multiple otherwise identical transactions in series from a
940        // test
941        tokio::spawn(async move {
942            loop {
943                tokio::time::sleep(target_slot_duration).await;
944                bank_forks
945                    .read()
946                    .unwrap()
947                    .working_bank()
948                    .register_unique_recent_blockhash_for_test();
949            }
950        });
951
952        (banks_client, gci.mint_keypair, last_blockhash)
953    }
954
955    /// Start the test client
956    ///
957    /// Returns a `BanksClient` interface into the test environment as well as a payer `Keypair`
958    /// with SOL for sending transactions
959    pub async fn start_with_context(mut self) -> ProgramTestContext {
960        let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
961        let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
962        let transport = start_local_server(
963            bank_forks.clone(),
964            block_commitment_cache.clone(),
965            target_tick_duration,
966        )
967        .await;
968        let banks_client = start_client(transport)
969            .await
970            .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
971
972        ProgramTestContext::new(
973            bank_forks,
974            block_commitment_cache,
975            banks_client,
976            last_blockhash,
977            gci,
978        )
979    }
980}
981
982#[async_trait]
983pub trait ProgramTestBanksClientExt {
984    /// Get a new latest blockhash, similar in spirit to RpcClient::get_latest_blockhash()
985    async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
986}
987
988#[async_trait]
989impl ProgramTestBanksClientExt for BanksClient {
990    async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
991        let mut num_retries = 0;
992        let start = Instant::now();
993        while start.elapsed().as_secs() < 5 {
994            let new_blockhash = self.get_latest_blockhash().await?;
995            if new_blockhash != *blockhash {
996                return Ok(new_blockhash);
997            }
998            debug!("Got same blockhash ({:?}), will retry...", blockhash);
999
1000            tokio::time::sleep(Duration::from_millis(200)).await;
1001            num_retries += 1;
1002        }
1003
1004        Err(io::Error::new(
1005            io::ErrorKind::Other,
1006            format!(
1007                "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1008                start.elapsed().as_millis(),
1009                num_retries,
1010                blockhash
1011            ),
1012        ))
1013    }
1014}
1015
1016struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1017
1018impl<T> Drop for DroppableTask<T> {
1019    fn drop(&mut self) {
1020        self.0.store(true, Ordering::Relaxed);
1021        trace!(
1022            "stopping task, which is currently {}",
1023            if self.1.is_finished() {
1024                "finished"
1025            } else {
1026                "running"
1027            }
1028        );
1029    }
1030}
1031
1032pub struct ProgramTestContext {
1033    pub banks_client: BanksClient,
1034    pub last_blockhash: Hash,
1035    pub payer: Keypair,
1036    genesis_config: GenesisConfig,
1037    bank_forks: Arc<RwLock<BankForks>>,
1038    block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1039    _bank_task: DroppableTask<()>,
1040}
1041
1042impl ProgramTestContext {
1043    fn new(
1044        bank_forks: Arc<RwLock<BankForks>>,
1045        block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1046        banks_client: BanksClient,
1047        last_blockhash: Hash,
1048        genesis_config_info: GenesisConfigInfo,
1049    ) -> Self {
1050        // Run a simulated PohService to provide the client with new blockhashes.  New blockhashes
1051        // are required when sending multiple otherwise identical transactions in series from a
1052        // test
1053        let running_bank_forks = bank_forks.clone();
1054        let target_tick_duration = genesis_config_info
1055            .genesis_config
1056            .poh_config
1057            .target_tick_duration;
1058        let target_slot_duration =
1059            target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1060        let exit = Arc::new(AtomicBool::new(false));
1061        let bank_task = DroppableTask(
1062            exit.clone(),
1063            tokio::spawn(async move {
1064                loop {
1065                    if exit.load(Ordering::Relaxed) {
1066                        break;
1067                    }
1068                    tokio::time::sleep(target_slot_duration).await;
1069                    running_bank_forks
1070                        .read()
1071                        .unwrap()
1072                        .working_bank()
1073                        .register_unique_recent_blockhash_for_test();
1074                }
1075            }),
1076        );
1077
1078        Self {
1079            banks_client,
1080            last_blockhash,
1081            payer: genesis_config_info.mint_keypair,
1082            genesis_config: genesis_config_info.genesis_config,
1083            bank_forks,
1084            block_commitment_cache,
1085            _bank_task: bank_task,
1086        }
1087    }
1088
1089    pub fn genesis_config(&self) -> &GenesisConfig {
1090        &self.genesis_config
1091    }
1092
1093    /// Manually increment vote credits for the current epoch in the specified vote account to simulate validator voting activity
1094    pub fn increment_vote_account_credits(
1095        &mut self,
1096        vote_account_address: &Pubkey,
1097        number_of_credits: u64,
1098    ) {
1099        let bank_forks = self.bank_forks.read().unwrap();
1100        let bank = bank_forks.working_bank();
1101
1102        // generate some vote activity for rewards
1103        let mut vote_account = bank.get_account(vote_account_address).unwrap();
1104        let mut vote_state = vote_state::from(&vote_account).unwrap();
1105
1106        let epoch = bank.epoch();
1107        for _ in 0..number_of_credits {
1108            vote_state.increment_credits(epoch, 1);
1109        }
1110        let versioned = VoteStateVersions::new_current(vote_state);
1111        vote_state::to(&versioned, &mut vote_account).unwrap();
1112        bank.store_account(vote_account_address, &vote_account);
1113    }
1114
1115    /// Create or overwrite an account, subverting normal runtime checks.
1116    ///
1117    /// This method exists to make it easier to set up artificial situations
1118    /// that would be difficult to replicate by sending individual transactions.
1119    /// Beware that it can be used to create states that would not be reachable
1120    /// by sending transactions!
1121    pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1122        let bank_forks = self.bank_forks.read().unwrap();
1123        let bank = bank_forks.working_bank();
1124        bank.store_account(address, account);
1125    }
1126
1127    /// Create or overwrite a sysvar, subverting normal runtime checks.
1128    ///
1129    /// This method exists to make it easier to set up artificial situations
1130    /// that would be difficult to replicate on a new test cluster. Beware
1131    /// that it can be used to create states that would not be reachable
1132    /// under normal conditions!
1133    pub fn set_sysvar<T: SysvarId + Sysvar>(&self, sysvar: &T) {
1134        let bank_forks = self.bank_forks.read().unwrap();
1135        let bank = bank_forks.working_bank();
1136        bank.set_sysvar_for_tests(sysvar);
1137    }
1138
1139    /// Force the working bank ahead to a new slot
1140    pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1141        let mut bank_forks = self.bank_forks.write().unwrap();
1142        let bank = bank_forks.working_bank();
1143
1144        // Fill ticks until a new blockhash is recorded, otherwise retried transactions will have
1145        // the same signature
1146        bank.fill_bank_with_ticks_for_tests();
1147
1148        // Ensure that we are actually progressing forward
1149        let working_slot = bank.slot();
1150        if warp_slot <= working_slot {
1151            return Err(ProgramTestError::InvalidWarpSlot);
1152        }
1153
1154        // Warp ahead to one slot *before* the desired slot because the bank
1155        // from Bank::warp_from_parent() is frozen. If the desired slot is one
1156        // slot *after* the working_slot, no need to warp at all.
1157        let pre_warp_slot = warp_slot - 1;
1158        let warp_bank = if pre_warp_slot == working_slot {
1159            bank.freeze();
1160            bank
1161        } else {
1162            bank_forks
1163                .insert(Bank::warp_from_parent(
1164                    bank,
1165                    &Pubkey::default(),
1166                    pre_warp_slot,
1167                    // some warping tests cannot use the append vecs because of the sequence of adding roots and flushing
1168                    solana_accounts_db::accounts_db::CalcAccountsHashDataSource::IndexForTests,
1169                ))
1170                .clone_without_scheduler()
1171        };
1172
1173        let (snapshot_request_sender, snapshot_request_receiver) = crossbeam_channel::unbounded();
1174        let abs_request_sender = AbsRequestSender::new(snapshot_request_sender);
1175
1176        bank_forks
1177            .set_root(pre_warp_slot, &abs_request_sender, Some(pre_warp_slot))
1178            .unwrap();
1179
1180        // The call to `set_root()` above will send an EAH request.  Need to intercept and handle
1181        // all EpochAccountsHash requests so future rooted banks do not hang in Bank::freeze()
1182        // waiting for an in-flight EAH calculation to complete.
1183        snapshot_request_receiver
1184            .try_iter()
1185            .filter(|snapshot_request| {
1186                snapshot_request.request_kind == SnapshotRequestKind::EpochAccountsHash
1187            })
1188            .for_each(|snapshot_request| {
1189                snapshot_request
1190                    .snapshot_root_bank
1191                    .rc
1192                    .accounts
1193                    .accounts_db
1194                    .epoch_accounts_hash_manager
1195                    .set_valid(
1196                        EpochAccountsHash::new(Hash::new_unique()),
1197                        snapshot_request.snapshot_root_bank.slot(),
1198                    )
1199            });
1200
1201        // warp_bank is frozen so go forward to get unfrozen bank at warp_slot
1202        bank_forks.insert(Bank::new_from_parent(
1203            warp_bank,
1204            &Pubkey::default(),
1205            warp_slot,
1206        ));
1207
1208        // Update block commitment cache, otherwise banks server will poll at
1209        // the wrong slot
1210        let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1211        // HACK: The root set here should be `pre_warp_slot`, but since we're
1212        // in a testing environment, the root bank never updates after a warp.
1213        // The ticking thread only updates the working bank, and never the root
1214        // bank.
1215        w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1216
1217        let bank = bank_forks.working_bank();
1218        self.last_blockhash = bank.last_blockhash();
1219        Ok(())
1220    }
1221
1222    pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1223        let warp_slot = self
1224            .genesis_config
1225            .epoch_schedule
1226            .get_first_slot_in_epoch(warp_epoch);
1227        self.warp_to_slot(warp_slot)
1228    }
1229
1230    /// warp forward one more slot and force reward interval end
1231    pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1232        let mut bank_forks = self.bank_forks.write().unwrap();
1233        let bank = bank_forks.working_bank();
1234
1235        // Fill ticks until a new blockhash is recorded, otherwise retried transactions will have
1236        // the same signature
1237        bank.fill_bank_with_ticks_for_tests();
1238        let pre_warp_slot = bank.slot();
1239
1240        bank_forks
1241            .set_root(
1242                pre_warp_slot,
1243                &solana_runtime::accounts_background_service::AbsRequestSender::default(),
1244                Some(pre_warp_slot),
1245            )
1246            .unwrap();
1247
1248        // warp_bank is frozen so go forward to get unfrozen bank at warp_slot
1249        let warp_slot = pre_warp_slot + 1;
1250        let mut warp_bank = Bank::new_from_parent(bank, &Pubkey::default(), warp_slot);
1251
1252        warp_bank.force_reward_interval_end_for_tests();
1253        bank_forks.insert(warp_bank);
1254
1255        // Update block commitment cache, otherwise banks server will poll at
1256        // the wrong slot
1257        let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1258        // HACK: The root set here should be `pre_warp_slot`, but since we're
1259        // in a testing environment, the root bank never updates after a warp.
1260        // The ticking thread only updates the working bank, and never the root
1261        // bank.
1262        w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1263
1264        let bank = bank_forks.working_bank();
1265        self.last_blockhash = bank.last_blockhash();
1266        Ok(())
1267    }
1268
1269    /// Get a new latest blockhash, similar in spirit to RpcClient::get_latest_blockhash()
1270    pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1271        let blockhash = self
1272            .banks_client
1273            .get_new_latest_blockhash(&self.last_blockhash)
1274            .await?;
1275        self.last_blockhash = blockhash;
1276        Ok(blockhash)
1277    }
1278
1279    /// record a hard fork slot in working bank; should be in the past
1280    pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1281        self.bank_forks
1282            .read()
1283            .unwrap()
1284            .working_bank()
1285            .register_hard_fork(hard_fork_slot)
1286    }
1287}