solana_program_test/
lib.rs

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