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