Skip to main content

solana_program_test/
lib.rs

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