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