1#![allow(clippy::arithmetic_side_effects)]
3
4pub use tokio;
6use {
7 agave_feature_set::{raise_cpi_nesting_limit_to_8, FEATURE_NAMES},
8 async_trait::async_trait,
9 base64::{prelude::BASE64_STANDARD, Engine},
10 chrono_humanize::{Accuracy, HumanTime, Tense},
11 log::*,
12 solana_account::{create_account_shared_data_for_test, Account, AccountSharedData},
13 solana_account_info::AccountInfo,
14 solana_banks_client::start_client,
15 solana_banks_server::banks_server::start_local_server,
16 solana_clock::{Epoch, Slot},
17 solana_cluster_type::ClusterType,
18 solana_compute_budget::compute_budget::ComputeBudget,
19 solana_fee_calculator::{FeeRateGovernor, DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE},
20 solana_genesis_config::GenesisConfig,
21 solana_hash::Hash,
22 solana_instruction::{
23 error::{InstructionError, UNSUPPORTED_SYSVAR},
24 Instruction,
25 },
26 solana_keypair::Keypair,
27 solana_native_token::LAMPORTS_PER_SOL,
28 solana_poh_config::PohConfig,
29 solana_program_entrypoint::{deserialize, SUCCESS},
30 solana_program_error::{ProgramError, ProgramResult},
31 solana_program_runtime::{
32 invoke_context::BuiltinFunctionWithContext, loaded_programs::ProgramCacheEntry,
33 serialization::serialize_parameters, stable_log,
34 },
35 solana_pubkey::Pubkey,
36 solana_rent::Rent,
37 solana_runtime::{
38 bank::Bank,
39 bank_forks::BankForks,
40 commitment::BlockCommitmentCache,
41 genesis_utils::{create_genesis_config_with_leader_ex, GenesisConfigInfo},
42 runtime_config::RuntimeConfig,
43 },
44 solana_signer::Signer,
45 solana_svm_log_collector::ic_msg,
46 solana_svm_timings::ExecuteTimings,
47 solana_sysvar::SysvarSerialize,
48 solana_sysvar_id::SysvarId,
49 solana_vote_program::vote_state::{self, VoteStateV3, VoteStateVersions},
50 std::{
51 cell::RefCell,
52 collections::{HashMap, HashSet},
53 convert::TryFrom,
54 fs::File,
55 io::{self, Read},
56 mem::transmute,
57 panic::AssertUnwindSafe,
58 path::{Path, PathBuf},
59 sync::{
60 atomic::{AtomicBool, Ordering},
61 Arc, RwLock,
62 },
63 time::{Duration, Instant},
64 },
65 thiserror::Error,
66 tokio::task::JoinHandle,
67};
68pub use {
70 solana_banks_client::{BanksClient, BanksClientError},
71 solana_banks_interface::BanksTransactionResultWithMetadata,
72 solana_program_runtime::invoke_context::InvokeContext,
73 solana_sbpf::{
74 error::EbpfError,
75 vm::{get_runtime_environment_key, EbpfVm},
76 },
77 solana_transaction_context::IndexOfAccount,
78};
79
80pub mod programs;
81
82#[derive(Error, Debug, PartialEq, Eq)]
84pub enum ProgramTestError {
85 #[error("Warp slot not in the future")]
87 InvalidWarpSlot,
88}
89
90thread_local! {
91 static INVOKE_CONTEXT: RefCell<Option<usize>> = const { RefCell::new(None) };
92}
93fn set_invoke_context(new: &mut InvokeContext) {
94 INVOKE_CONTEXT.with(|invoke_context| unsafe {
95 invoke_context.replace(Some(transmute::<&mut InvokeContext, usize>(new)))
96 });
97}
98fn get_invoke_context<'a, 'b>() -> &'a mut InvokeContext<'b> {
99 let ptr = INVOKE_CONTEXT.with(|invoke_context| match *invoke_context.borrow() {
100 Some(val) => val,
101 None => panic!("Invoke context not set!"),
102 });
103 unsafe { transmute::<usize, &mut InvokeContext>(ptr) }
104}
105
106pub fn invoke_builtin_function(
107 builtin_function: solana_program_entrypoint::ProcessInstruction,
108 invoke_context: &mut InvokeContext,
109) -> Result<u64, Box<dyn std::error::Error>> {
110 set_invoke_context(invoke_context);
111
112 let transaction_context = &invoke_context.transaction_context;
113 let instruction_context = transaction_context.get_current_instruction_context()?;
114 let instruction_account_indices = 0..instruction_context.get_number_of_instruction_accounts();
115
116 invoke_context.consume_checked(1)?;
118
119 let log_collector = invoke_context.get_log_collector();
120 let program_id = instruction_context.get_program_key()?;
121 stable_log::program_invoke(
122 &log_collector,
123 program_id,
124 invoke_context.get_stack_height(),
125 );
126
127 let deduplicated_indices: HashSet<IndexOfAccount> = instruction_account_indices.collect();
129
130 let mask_out_rent_epoch_in_vm_serialization = invoke_context
132 .get_feature_set()
133 .mask_out_rent_epoch_in_vm_serialization;
134 let (mut parameter_bytes, _regions, _account_lengths) = serialize_parameters(
135 &instruction_context,
136 false, false, mask_out_rent_epoch_in_vm_serialization,
139 )?;
140
141 let (program_id, account_infos, input) =
143 unsafe { deserialize(&mut parameter_bytes.as_slice_mut()[0] as *mut u8) };
144
145 match std::panic::catch_unwind(AssertUnwindSafe(|| {
147 builtin_function(program_id, &account_infos, input)
148 })) {
149 Ok(program_result) => {
150 program_result.map_err(|program_error| {
151 let err = InstructionError::from(u64::from(program_error));
152 stable_log::program_failure(&log_collector, program_id, &err);
153 let err: Box<dyn std::error::Error> = Box::new(err);
154 err
155 })?;
156 }
157 Err(_panic_error) => {
158 let err = InstructionError::ProgramFailedToComplete;
159 stable_log::program_failure(&log_collector, program_id, &err);
160 let err: Box<dyn std::error::Error> = Box::new(err);
161 Err(err)?;
162 }
163 };
164
165 stable_log::program_success(&log_collector, program_id);
166
167 let account_info_map: HashMap<_, _> = account_infos.into_iter().map(|a| (a.key, a)).collect();
169
170 let transaction_context = &invoke_context.transaction_context;
173 let instruction_context = transaction_context.get_current_instruction_context()?;
174
175 for i in deduplicated_indices.into_iter() {
177 let mut borrowed_account = instruction_context.try_borrow_instruction_account(i)?;
178 if borrowed_account.is_writable() {
179 if let Some(account_info) = account_info_map.get(borrowed_account.get_key()) {
180 if borrowed_account.get_lamports() != account_info.lamports() {
181 borrowed_account.set_lamports(account_info.lamports())?;
182 }
183
184 if borrowed_account
185 .can_data_be_resized(account_info.data_len())
186 .is_ok()
187 {
188 borrowed_account.set_data_from_slice(&account_info.data.borrow())?;
189 }
190 if borrowed_account.get_owner() != account_info.owner {
191 borrowed_account.set_owner(account_info.owner.as_ref())?;
192 }
193 }
194 }
195 }
196
197 Ok(0)
198}
199
200#[macro_export]
203macro_rules! processor {
204 ($builtin_function:expr) => {
205 Some(|vm, _arg0, _arg1, _arg2, _arg3, _arg4| {
206 let vm = unsafe {
207 &mut *((vm as *mut u64).offset(-($crate::get_runtime_environment_key() as isize))
208 as *mut $crate::EbpfVm<$crate::InvokeContext>)
209 };
210 vm.program_result =
211 $crate::invoke_builtin_function($builtin_function, vm.context_object_pointer)
212 .map_err(|err| $crate::EbpfError::SyscallError(err))
213 .into();
214 })
215 };
216}
217
218fn get_sysvar<T: Default + SysvarSerialize + Sized + serde::de::DeserializeOwned + Clone>(
219 sysvar: Result<Arc<T>, InstructionError>,
220 var_addr: *mut u8,
221) -> u64 {
222 let invoke_context = get_invoke_context();
223 if invoke_context
224 .consume_checked(invoke_context.get_execution_cost().sysvar_base_cost + T::size_of() as u64)
225 .is_err()
226 {
227 panic!("Exceeded compute budget");
228 }
229
230 match sysvar {
231 Ok(sysvar_data) => unsafe {
232 *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
233 SUCCESS
234 },
235 Err(_) => UNSUPPORTED_SYSVAR,
236 }
237}
238
239struct SyscallStubs {}
240impl solana_sysvar::program_stubs::SyscallStubs for SyscallStubs {
241 fn sol_log(&self, message: &str) {
242 let invoke_context = get_invoke_context();
243 ic_msg!(invoke_context, "Program log: {}", message);
244 }
245
246 fn sol_invoke_signed(
247 &self,
248 instruction: &Instruction,
249 account_infos: &[AccountInfo],
250 signers_seeds: &[&[&[u8]]],
251 ) -> ProgramResult {
252 let invoke_context = get_invoke_context();
253 let log_collector = invoke_context.get_log_collector();
254 let transaction_context = &invoke_context.transaction_context;
255 let instruction_context = transaction_context
256 .get_current_instruction_context()
257 .unwrap();
258 let caller = instruction_context.get_program_key().unwrap();
259
260 stable_log::program_invoke(
261 &log_collector,
262 &instruction.program_id,
263 invoke_context.get_stack_height(),
264 );
265
266 let signers = signers_seeds
267 .iter()
268 .map(|seeds| Pubkey::create_program_address(seeds, caller).unwrap())
269 .collect::<Vec<_>>();
270
271 invoke_context
272 .prepare_next_instruction(instruction, &signers)
273 .unwrap();
274
275 let transaction_context = &invoke_context.transaction_context;
277 let instruction_context = transaction_context
278 .get_current_instruction_context()
279 .unwrap();
280 let next_instruction_context = transaction_context.get_next_instruction_context().unwrap();
281 let next_instruction_accounts = next_instruction_context.instruction_accounts();
282 let mut account_indices = Vec::with_capacity(next_instruction_accounts.len());
283 for instruction_account in next_instruction_accounts.iter() {
284 let account_key = transaction_context
285 .get_key_of_account_at_index(instruction_account.index_in_transaction)
286 .unwrap();
287 let account_info_index = account_infos
288 .iter()
289 .position(|account_info| account_info.unsigned_key() == account_key)
290 .ok_or(InstructionError::MissingAccount)
291 .unwrap();
292 let account_info = &account_infos[account_info_index];
293 let index_in_caller = instruction_context
294 .get_index_of_account_in_instruction(instruction_account.index_in_transaction)
295 .unwrap();
296 let mut borrowed_account = instruction_context
297 .try_borrow_instruction_account(index_in_caller)
298 .unwrap();
299 if borrowed_account.get_lamports() != account_info.lamports() {
300 borrowed_account
301 .set_lamports(account_info.lamports())
302 .unwrap();
303 }
304 let account_info_data = account_info.try_borrow_data().unwrap();
305 match borrowed_account.can_data_be_resized(account_info_data.len()) {
307 Ok(()) => borrowed_account
308 .set_data_from_slice(&account_info_data)
309 .unwrap(),
310 Err(err) if borrowed_account.get_data() != *account_info_data => {
311 panic!("{err:?}");
312 }
313 _ => {}
314 }
315 if borrowed_account.get_owner() != account_info.owner {
317 borrowed_account
318 .set_owner(account_info.owner.as_ref())
319 .unwrap();
320 }
321 if instruction_account.is_writable() {
322 account_indices
323 .push((instruction_account.index_in_transaction, account_info_index));
324 }
325 }
326
327 let mut compute_units_consumed = 0;
328 invoke_context
329 .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default())
330 .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
331
332 let transaction_context = &invoke_context.transaction_context;
334 let instruction_context = transaction_context
335 .get_current_instruction_context()
336 .unwrap();
337 for (index_in_transaction, account_info_index) in account_indices.into_iter() {
338 let index_in_caller = instruction_context
339 .get_index_of_account_in_instruction(index_in_transaction)
340 .unwrap();
341 let borrowed_account = instruction_context
342 .try_borrow_instruction_account(index_in_caller)
343 .unwrap();
344 let account_info = &account_infos[account_info_index];
345 **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
346 if account_info.owner != borrowed_account.get_owner() {
347 #[allow(clippy::transmute_ptr_to_ptr)]
349 #[allow(mutable_transmutes)]
350 let account_info_mut =
351 unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
352 *account_info_mut = *borrowed_account.get_owner();
353 }
354
355 let new_data = borrowed_account.get_data();
356 let new_len = new_data.len();
357
358 if account_info.data_len() != new_len {
360 account_info.resize(new_len)?;
361 }
362
363 let mut data = account_info.try_borrow_mut_data()?;
365 data.clone_from_slice(new_data);
366 }
367
368 stable_log::program_success(&log_collector, &instruction.program_id);
369 Ok(())
370 }
371
372 fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
373 get_sysvar(
374 get_invoke_context().get_sysvar_cache().get_clock(),
375 var_addr,
376 )
377 }
378
379 fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
380 get_sysvar(
381 get_invoke_context().get_sysvar_cache().get_epoch_schedule(),
382 var_addr,
383 )
384 }
385
386 fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
387 get_sysvar(
388 get_invoke_context().get_sysvar_cache().get_epoch_rewards(),
389 var_addr,
390 )
391 }
392
393 #[allow(deprecated)]
394 fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
395 get_sysvar(get_invoke_context().get_sysvar_cache().get_fees(), var_addr)
396 }
397
398 fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
399 get_sysvar(get_invoke_context().get_sysvar_cache().get_rent(), var_addr)
400 }
401
402 fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
403 get_sysvar(
404 get_invoke_context()
405 .get_sysvar_cache()
406 .get_last_restart_slot(),
407 var_addr,
408 )
409 }
410
411 fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
412 let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
413 Some((*program_id, data.to_vec()))
414 }
415
416 fn sol_set_return_data(&self, data: &[u8]) {
417 let invoke_context = get_invoke_context();
418 let transaction_context = &mut invoke_context.transaction_context;
419 let instruction_context = transaction_context
420 .get_current_instruction_context()
421 .unwrap();
422 let caller = *instruction_context.get_program_key().unwrap();
423 transaction_context
424 .set_return_data(caller, data.to_vec())
425 .unwrap();
426 }
427
428 fn sol_get_stack_height(&self) -> u64 {
429 let invoke_context = get_invoke_context();
430 invoke_context.get_stack_height().try_into().unwrap()
431 }
432}
433
434pub fn find_file(filename: &str) -> Option<PathBuf> {
435 for dir in default_shared_object_dirs() {
436 let candidate = dir.join(filename);
437 if candidate.exists() {
438 return Some(candidate);
439 }
440 }
441 None
442}
443
444fn default_shared_object_dirs() -> Vec<PathBuf> {
445 let mut search_path = vec![];
446 if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
447 search_path.push(PathBuf::from(bpf_out_dir));
448 } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
449 search_path.push(PathBuf::from(bpf_out_dir));
450 }
451 search_path.push(PathBuf::from("tests/fixtures"));
452 if let Ok(dir) = std::env::current_dir() {
453 search_path.push(dir);
454 }
455 trace!("SBF .so search path: {search_path:?}");
456 search_path
457}
458
459pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
460 let path = path.as_ref();
461 let mut file = File::open(path)
462 .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
463
464 let mut file_data = Vec::new();
465 file.read_to_end(&mut file_data)
466 .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
467 file_data
468}
469
470pub struct ProgramTest {
471 accounts: Vec<(Pubkey, AccountSharedData)>,
472 genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
473 builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
474 compute_max_units: Option<u64>,
475 prefer_bpf: bool,
476 deactivate_feature_set: HashSet<Pubkey>,
477 transaction_account_lock_limit: Option<usize>,
478}
479
480impl Default for ProgramTest {
481 fn default() -> Self {
494 solana_logger::setup_with_default(
495 "solana_sbpf::vm=debug,solana_runtime::message_processor=debug,\
496 solana_runtime::system_instruction_processor=trace,solana_program_test=info",
497 );
498 let prefer_bpf =
499 std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
500
501 Self {
502 accounts: vec![],
503 genesis_accounts: vec![],
504 builtin_programs: vec![],
505 compute_max_units: None,
506 prefer_bpf,
507 deactivate_feature_set: HashSet::default(),
508 transaction_account_lock_limit: None,
509 }
510 }
511}
512
513impl ProgramTest {
514 pub fn new(
522 program_name: &'static str,
523 program_id: Pubkey,
524 builtin_function: Option<BuiltinFunctionWithContext>,
525 ) -> Self {
526 let mut me = Self::default();
527 me.add_program(program_name, program_id, builtin_function);
528 me
529 }
530
531 pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
533 self.prefer_bpf = prefer_bpf;
534 }
535
536 pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
538 debug_assert!(
539 compute_max_units <= i64::MAX as u64,
540 "Compute unit limit must fit in `i64::MAX`"
541 );
542 self.compute_max_units = Some(compute_max_units);
543 }
544
545 pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
547 self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
548 }
549
550 pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
552 self.genesis_accounts
553 .push((address, AccountSharedData::from(account)));
554 }
555
556 pub fn add_account(&mut self, address: Pubkey, account: Account) {
558 self.accounts
559 .push((address, AccountSharedData::from(account)));
560 }
561
562 pub fn add_account_with_file_data(
564 &mut self,
565 address: Pubkey,
566 lamports: u64,
567 owner: Pubkey,
568 filename: &str,
569 ) {
570 self.add_account(
571 address,
572 Account {
573 lamports,
574 data: read_file(find_file(filename).unwrap_or_else(|| {
575 panic!("Unable to locate {filename}");
576 })),
577 owner,
578 executable: false,
579 rent_epoch: 0,
580 },
581 );
582 }
583
584 pub fn add_account_with_base64_data(
587 &mut self,
588 address: Pubkey,
589 lamports: u64,
590 owner: Pubkey,
591 data_base64: &str,
592 ) {
593 self.add_account(
594 address,
595 Account {
596 lamports,
597 data: BASE64_STANDARD
598 .decode(data_base64)
599 .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
600 owner,
601 executable: false,
602 rent_epoch: 0,
603 },
604 );
605 }
606
607 pub fn add_sysvar_account<S: SysvarSerialize>(&mut self, address: Pubkey, sysvar: &S) {
608 let account = create_account_shared_data_for_test(sysvar);
609 self.add_account(address, account.into());
610 }
611
612 pub fn add_upgradeable_program_to_genesis(
625 &mut self,
626 program_name: &'static str,
627 program_id: &Pubkey,
628 ) {
629 let program_file = find_file(&format!("{program_name}.so"))
630 .expect("Program file data not available for {program_name} ({program_id})");
631 let elf = read_file(program_file);
632 let program_accounts =
633 programs::bpf_loader_upgradeable_program_accounts(program_id, &elf, &Rent::default());
634 for (address, account) in program_accounts {
635 self.add_genesis_account(address, account);
636 }
637 }
638
639 pub fn add_program(
647 &mut self,
648 program_name: &'static str,
649 program_id: Pubkey,
650 builtin_function: Option<BuiltinFunctionWithContext>,
651 ) {
652 let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
653 let data = read_file(&program_file);
654 info!(
655 "\"{}\" SBF program from {}{}",
656 program_name,
657 program_file.display(),
658 std::fs::metadata(&program_file)
659 .map(|metadata| {
660 metadata
661 .modified()
662 .map(|time| {
663 format!(
664 ", modified {}",
665 HumanTime::from(time)
666 .to_text_en(Accuracy::Precise, Tense::Past)
667 )
668 })
669 .ok()
670 })
671 .ok()
672 .flatten()
673 .unwrap_or_default()
674 );
675
676 this.add_account(
677 program_id,
678 Account {
679 lamports: Rent::default().minimum_balance(data.len()).max(1),
680 data,
681 owner: solana_sdk_ids::bpf_loader::id(),
682 executable: true,
683 rent_epoch: 0,
684 },
685 );
686 };
687
688 let warn_invalid_program_name = || {
689 let valid_program_names = default_shared_object_dirs()
690 .iter()
691 .filter_map(|dir| dir.read_dir().ok())
692 .flat_map(|read_dir| {
693 read_dir.filter_map(|entry| {
694 let path = entry.ok()?.path();
695 if !path.is_file() {
696 return None;
697 }
698 match path.extension()?.to_str()? {
699 "so" => Some(path.file_stem()?.to_os_string()),
700 _ => None,
701 }
702 })
703 })
704 .collect::<Vec<_>>();
705
706 if valid_program_names.is_empty() {
707 warn!("No SBF shared objects found.");
710 return;
711 }
712
713 warn!(
714 "Possible bogus program name. Ensure the program name ({program_name}) matches \
715 one of the following recognizable program names:",
716 );
717 for name in valid_program_names {
718 warn!(" - {}", name.to_str().unwrap());
719 }
720 };
721
722 let program_file = find_file(&format!("{program_name}.so"));
723 match (self.prefer_bpf, program_file, builtin_function) {
724 (true, Some(file), _) => add_bpf(self, file),
727
728 (false, _, Some(builtin_function)) => {
731 self.add_builtin_program(program_name, program_id, builtin_function)
732 }
733
734 (true, None, _) => {
736 warn_invalid_program_name();
737 panic!("Program file data not available for {program_name} ({program_id})");
738 }
739
740 (false, _, None) => {
742 panic!("Program processor not available for {program_name} ({program_id})");
743 }
744 }
745 }
746
747 pub fn add_builtin_program(
751 &mut self,
752 program_name: &'static str,
753 program_id: Pubkey,
754 builtin_function: BuiltinFunctionWithContext,
755 ) {
756 info!("\"{program_name}\" builtin program");
757 self.builtin_programs.push((
758 program_id,
759 program_name,
760 ProgramCacheEntry::new_builtin(0, program_name.len(), builtin_function),
761 ));
762 }
763
764 pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
768 self.deactivate_feature_set.insert(feature_id);
769 }
770
771 fn setup_bank(
772 &mut self,
773 ) -> (
774 Arc<RwLock<BankForks>>,
775 Arc<RwLock<BlockCommitmentCache>>,
776 Hash,
777 GenesisConfigInfo,
778 ) {
779 {
780 use std::sync::Once;
781 static ONCE: Once = Once::new();
782
783 ONCE.call_once(|| {
784 solana_sysvar::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
785 });
786 }
787
788 let rent = Rent::default();
789 let fee_rate_governor = FeeRateGovernor {
790 lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
792 ..FeeRateGovernor::default()
793 };
794 let bootstrap_validator_pubkey = Pubkey::new_unique();
795 let bootstrap_validator_stake_lamports =
796 rent.minimum_balance(VoteStateV3::size_of()) + 1_000_000 * LAMPORTS_PER_SOL;
797
798 let mint_keypair = Keypair::new();
799 let voting_keypair = Keypair::new();
800
801 let mut genesis_config = create_genesis_config_with_leader_ex(
802 1_000_000 * LAMPORTS_PER_SOL,
803 &mint_keypair.pubkey(),
804 &bootstrap_validator_pubkey,
805 &voting_keypair.pubkey(),
806 &Pubkey::new_unique(),
807 bootstrap_validator_stake_lamports,
808 42,
809 fee_rate_governor,
810 rent.clone(),
811 ClusterType::Development,
812 std::mem::take(&mut self.genesis_accounts),
813 );
814
815 for deactivate_feature_pk in &self.deactivate_feature_set {
817 if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
818 match genesis_config.accounts.remove(deactivate_feature_pk) {
819 Some(_) => debug!("Feature for {deactivate_feature_pk:?} deactivated"),
820 None => warn!(
821 "Feature {deactivate_feature_pk:?} set for deactivation not found in \
822 genesis_config account list, ignored."
823 ),
824 }
825 } else {
826 warn!(
827 "Feature {deactivate_feature_pk:?} set for deactivation is not a known \
828 Feature public key"
829 );
830 }
831 }
832
833 let target_tick_duration = Duration::from_micros(100);
834 genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
835 debug!("Payer address: {}", mint_keypair.pubkey());
836 debug!("Genesis config: {genesis_config}");
837
838 let bank = Bank::new_with_paths(
839 &genesis_config,
840 Arc::new(RuntimeConfig {
841 compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
842 compute_unit_limit: max_units,
843 ..ComputeBudget::new_with_defaults(
844 genesis_config
845 .accounts
846 .contains_key(&raise_cpi_nesting_limit_to_8::id()),
847 )
848 }),
849 transaction_account_lock_limit: self.transaction_account_lock_limit,
850 ..RuntimeConfig::default()
851 }),
852 Vec::default(),
853 None,
854 None,
855 false,
856 None,
857 None,
858 None,
859 Arc::default(),
860 None,
861 None,
862 );
863
864 for (program_id, account) in programs::spl_programs(&rent).iter() {
866 bank.store_account(program_id, account);
867 }
868
869 for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
871 genesis_config.accounts.contains_key(feature_id)
872 })
873 .iter()
874 {
875 bank.store_account(program_id, account);
876 }
877
878 let mut builtin_programs = Vec::new();
880 std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
881 for (program_id, name, builtin) in builtin_programs.into_iter() {
882 bank.add_builtin(program_id, name, builtin);
883 }
884
885 for (address, account) in self.accounts.iter() {
886 if bank.get_account(address).is_some() {
887 info!("Overriding account at {address}");
888 }
889 bank.store_account(address, account);
890 }
891 bank.set_capitalization_for_tests(bank.calculate_capitalization_for_tests());
892 let bank = {
894 let bank = Arc::new(bank);
895 bank.fill_bank_with_ticks_for_tests();
896 let bank = Bank::new_from_parent(bank.clone(), bank.collector_id(), bank.slot() + 1);
897 debug!("Bank slot: {}", bank.slot());
898 bank
899 };
900 let slot = bank.slot();
901 let last_blockhash = bank.last_blockhash();
902 let bank_forks = BankForks::new_rw_arc(bank);
903 let block_commitment_cache = Arc::new(RwLock::new(
904 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
905 ));
906
907 (
908 bank_forks,
909 block_commitment_cache,
910 last_blockhash,
911 GenesisConfigInfo {
912 genesis_config,
913 mint_keypair,
914 voting_keypair,
915 validator_pubkey: bootstrap_validator_pubkey,
916 },
917 )
918 }
919
920 pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
921 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
922 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
923 let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
924 let transport = start_local_server(
925 bank_forks.clone(),
926 block_commitment_cache.clone(),
927 target_tick_duration,
928 )
929 .await;
930 let banks_client = start_client(transport)
931 .await
932 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
933
934 tokio::spawn(async move {
938 loop {
939 tokio::time::sleep(target_slot_duration).await;
940 bank_forks
941 .read()
942 .unwrap()
943 .working_bank()
944 .register_unique_recent_blockhash_for_test();
945 }
946 });
947
948 (banks_client, gci.mint_keypair, last_blockhash)
949 }
950
951 pub async fn start_with_context(mut self) -> ProgramTestContext {
956 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
957 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
958 let transport = start_local_server(
959 bank_forks.clone(),
960 block_commitment_cache.clone(),
961 target_tick_duration,
962 )
963 .await;
964 let banks_client = start_client(transport)
965 .await
966 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
967
968 ProgramTestContext::new(
969 bank_forks,
970 block_commitment_cache,
971 banks_client,
972 last_blockhash,
973 gci,
974 )
975 }
976}
977
978#[async_trait]
979pub trait ProgramTestBanksClientExt {
980 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
982}
983
984#[async_trait]
985impl ProgramTestBanksClientExt for BanksClient {
986 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
987 let mut num_retries = 0;
988 let start = Instant::now();
989 while start.elapsed().as_secs() < 5 {
990 let new_blockhash = self.get_latest_blockhash().await?;
991 if new_blockhash != *blockhash {
992 return Ok(new_blockhash);
993 }
994 debug!("Got same blockhash ({blockhash:?}), will retry...");
995
996 tokio::time::sleep(Duration::from_millis(200)).await;
997 num_retries += 1;
998 }
999
1000 Err(io::Error::other(format!(
1001 "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1002 start.elapsed().as_millis(),
1003 num_retries,
1004 blockhash
1005 )))
1006 }
1007}
1008
1009struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1010
1011impl<T> Drop for DroppableTask<T> {
1012 fn drop(&mut self) {
1013 self.0.store(true, Ordering::Relaxed);
1014 trace!(
1015 "stopping task, which is currently {}",
1016 if self.1.is_finished() {
1017 "finished"
1018 } else {
1019 "running"
1020 }
1021 );
1022 }
1023}
1024
1025pub struct ProgramTestContext {
1026 pub banks_client: BanksClient,
1027 pub last_blockhash: Hash,
1028 pub payer: Keypair,
1029 genesis_config: GenesisConfig,
1030 bank_forks: Arc<RwLock<BankForks>>,
1031 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1032 _bank_task: DroppableTask<()>,
1033}
1034
1035impl ProgramTestContext {
1036 fn new(
1037 bank_forks: Arc<RwLock<BankForks>>,
1038 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1039 banks_client: BanksClient,
1040 last_blockhash: Hash,
1041 genesis_config_info: GenesisConfigInfo,
1042 ) -> Self {
1043 let running_bank_forks = bank_forks.clone();
1047 let target_tick_duration = genesis_config_info
1048 .genesis_config
1049 .poh_config
1050 .target_tick_duration;
1051 let target_slot_duration =
1052 target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1053 let exit = Arc::new(AtomicBool::new(false));
1054 let bank_task = DroppableTask(
1055 exit.clone(),
1056 tokio::spawn(async move {
1057 loop {
1058 if exit.load(Ordering::Relaxed) {
1059 break;
1060 }
1061 tokio::time::sleep(target_slot_duration).await;
1062 running_bank_forks
1063 .read()
1064 .unwrap()
1065 .working_bank()
1066 .register_unique_recent_blockhash_for_test();
1067 }
1068 }),
1069 );
1070
1071 Self {
1072 banks_client,
1073 last_blockhash,
1074 payer: genesis_config_info.mint_keypair,
1075 genesis_config: genesis_config_info.genesis_config,
1076 bank_forks,
1077 block_commitment_cache,
1078 _bank_task: bank_task,
1079 }
1080 }
1081
1082 pub fn genesis_config(&self) -> &GenesisConfig {
1083 &self.genesis_config
1084 }
1085
1086 pub fn increment_vote_account_credits(
1088 &mut self,
1089 vote_account_address: &Pubkey,
1090 number_of_credits: u64,
1091 ) {
1092 let bank_forks = self.bank_forks.read().unwrap();
1093 let bank = bank_forks.working_bank();
1094
1095 let mut vote_account = bank.get_account(vote_account_address).unwrap();
1097 let mut vote_state = vote_state::from(&vote_account).unwrap();
1098
1099 let epoch = bank.epoch();
1100 for _ in 0..number_of_credits {
1101 vote_state.increment_credits(epoch, 1);
1102 }
1103 let versioned = VoteStateVersions::new_v3(vote_state);
1104 vote_state::to(&versioned, &mut vote_account).unwrap();
1105 bank.store_account(vote_account_address, &vote_account);
1106 }
1107
1108 pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1115 let bank_forks = self.bank_forks.read().unwrap();
1116 let bank = bank_forks.working_bank();
1117 bank.store_account(address, account);
1118 }
1119
1120 pub fn set_sysvar<T: SysvarId + SysvarSerialize>(&self, sysvar: &T) {
1127 let bank_forks = self.bank_forks.read().unwrap();
1128 let bank = bank_forks.working_bank();
1129 bank.set_sysvar_for_tests(sysvar);
1130 }
1131
1132 pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1134 let mut bank_forks = self.bank_forks.write().unwrap();
1135 let bank = bank_forks.working_bank();
1136
1137 bank.fill_bank_with_ticks_for_tests();
1140
1141 let working_slot = bank.slot();
1143 if warp_slot <= working_slot {
1144 return Err(ProgramTestError::InvalidWarpSlot);
1145 }
1146
1147 let pre_warp_slot = warp_slot - 1;
1151 let warp_bank = if pre_warp_slot == working_slot {
1152 bank.freeze();
1153 bank
1154 } else {
1155 bank_forks
1156 .insert(Bank::warp_from_parent(
1157 bank,
1158 &Pubkey::default(),
1159 pre_warp_slot,
1160 ))
1161 .clone_without_scheduler()
1162 };
1163
1164 bank_forks
1165 .set_root(
1166 pre_warp_slot,
1167 None, Some(pre_warp_slot),
1169 )
1170 .unwrap();
1171
1172 bank_forks.insert(Bank::new_from_parent(
1174 warp_bank,
1175 &Pubkey::default(),
1176 warp_slot,
1177 ));
1178
1179 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1182 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1187
1188 let bank = bank_forks.working_bank();
1189 self.last_blockhash = bank.last_blockhash();
1190 Ok(())
1191 }
1192
1193 pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1194 let warp_slot = self
1195 .genesis_config
1196 .epoch_schedule
1197 .get_first_slot_in_epoch(warp_epoch);
1198 self.warp_to_slot(warp_slot)
1199 }
1200
1201 pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1203 let mut bank_forks = self.bank_forks.write().unwrap();
1204 let bank = bank_forks.working_bank();
1205
1206 bank.fill_bank_with_ticks_for_tests();
1209 let pre_warp_slot = bank.slot();
1210
1211 bank_forks
1212 .set_root(
1213 pre_warp_slot,
1214 None, Some(pre_warp_slot),
1216 )
1217 .unwrap();
1218
1219 let warp_slot = pre_warp_slot + 1;
1221 let mut warp_bank = Bank::new_from_parent(bank, &Pubkey::default(), warp_slot);
1222
1223 warp_bank.force_reward_interval_end_for_tests();
1224 bank_forks.insert(warp_bank);
1225
1226 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1229 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1234
1235 let bank = bank_forks.working_bank();
1236 self.last_blockhash = bank.last_blockhash();
1237 Ok(())
1238 }
1239
1240 pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1242 let blockhash = self
1243 .banks_client
1244 .get_new_latest_blockhash(&self.last_blockhash)
1245 .await?;
1246 self.last_blockhash = blockhash;
1247 Ok(blockhash)
1248 }
1249
1250 pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1252 self.bank_forks
1253 .read()
1254 .unwrap()
1255 .working_bank()
1256 .register_hard_fork(hard_fork_slot)
1257 }
1258}