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