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