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