1#![cfg(feature = "agave-unstable-api")]
2#![allow(clippy::arithmetic_side_effects)]
4
5pub use tokio;
7use {
8 agave_feature_set::{
9 FEATURE_NAMES, FeatureSet, increase_cpi_account_info_limit, raise_cpi_nesting_limit_to_8,
10 },
11 async_trait::async_trait,
12 base64::{Engine, prelude::BASE64_STANDARD},
13 chrono_humanize::{Accuracy, HumanTime, Tense},
14 log::*,
15 solana_account::{
16 Account, AccountSharedData, ReadableAccount, create_account_shared_data_for_test,
17 state_traits::StateMut,
18 },
19 solana_account_info::AccountInfo,
20 solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING,
21 solana_banks_client::start_client,
22 solana_banks_server::banks_server::start_local_server,
23 solana_clock::{Clock, Epoch, Slot},
24 solana_cluster_type::ClusterType,
25 solana_compute_budget::compute_budget::{ComputeBudget, SVMTransactionExecutionCost},
26 solana_epoch_rewards::EpochRewards,
27 solana_epoch_schedule::EpochSchedule,
28 solana_fee_calculator::{DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, FeeRateGovernor},
29 solana_genesis_config::GenesisConfig,
30 solana_hash::Hash,
31 solana_instruction::{
32 Instruction,
33 error::{InstructionError, UNSUPPORTED_SYSVAR},
34 },
35 solana_keypair::Keypair,
36 solana_native_token::LAMPORTS_PER_SOL,
37 solana_poh_config::PohConfig,
38 solana_program_binaries as programs,
39 solana_program_entrypoint::{SUCCESS, deserialize},
40 solana_program_error::{ProgramError, ProgramResult},
41 solana_program_runtime::{
42 invoke_context::BuiltinFunctionWithContext, loaded_programs::ProgramCacheEntry,
43 serialization::serialize_parameters, stable_log, sysvar_cache::SysvarCache,
44 },
45 solana_pubkey::Pubkey,
46 solana_rent::Rent,
47 solana_runtime::{
48 bank::Bank,
49 bank_forks::BankForks,
50 commitment::BlockCommitmentCache,
51 genesis_utils::{GenesisConfigInfo, create_genesis_config_with_leader_ex},
52 runtime_config::RuntimeConfig,
53 },
54 solana_signer::Signer,
55 solana_svm_log_collector::ic_msg,
56 solana_svm_timings::ExecuteTimings,
57 solana_sysvar::{SysvarSerialize, last_restart_slot::LastRestartSlot},
58 solana_sysvar_id::SysvarId,
59 solana_vote_program::vote_state::{VoteStateV4, VoteStateVersions},
60 std::{
61 cell::RefCell,
62 collections::{HashMap, HashSet},
63 fs::File,
64 io::{self, Read},
65 mem::transmute,
66 panic::AssertUnwindSafe,
67 path::{Path, PathBuf},
68 ptr,
69 sync::{
70 Arc, RwLock,
71 atomic::{AtomicBool, Ordering},
72 },
73 time::{Duration, Instant},
74 },
75 thiserror::Error,
76 tokio::task::JoinHandle,
77};
78pub 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 vm::{EbpfVm, get_runtime_environment_key},
86 },
87 solana_transaction_context::IndexOfAccount,
88};
89
90#[derive(Error, Debug, PartialEq, Eq)]
92pub enum ProgramTestError {
93 #[error("Warp slot not in the future")]
95 InvalidWarpSlot,
96}
97
98thread_local! {
99 static INVOKE_CONTEXT: RefCell<Option<usize>> = const { RefCell::new(None) };
100}
101fn set_invoke_context(new: &mut InvokeContext) {
102 INVOKE_CONTEXT.with(|invoke_context| unsafe {
103 invoke_context.replace(Some(transmute::<&mut InvokeContext, usize>(new)))
104 });
105}
106fn get_invoke_context<'a, 'b>() -> &'a mut InvokeContext<'b, 'b> {
107 let ptr = INVOKE_CONTEXT.with(|invoke_context| match *invoke_context.borrow() {
108 Some(val) => val,
109 None => panic!("Invoke context not set!"),
110 });
111 unsafe { &mut *ptr::with_exposed_provenance_mut(ptr) }
112}
113
114pub fn invoke_builtin_function(
115 builtin_function: solana_program_entrypoint::ProcessInstruction,
116 invoke_context: &mut InvokeContext,
117) -> Result<u64, Box<dyn std::error::Error>> {
118 set_invoke_context(invoke_context);
119
120 let transaction_context = &invoke_context.transaction_context;
121 let instruction_context = transaction_context.get_current_instruction_context()?;
122 let instruction_account_indices = 0..instruction_context.get_number_of_instruction_accounts();
123
124 invoke_context.consume_checked(1)?;
126
127 let log_collector = invoke_context.get_log_collector();
128 let program_id = instruction_context.get_program_key()?;
129 stable_log::program_invoke(
130 &log_collector,
131 program_id,
132 invoke_context.get_stack_height(),
133 );
134
135 let deduplicated_indices: HashSet<IndexOfAccount> = instruction_account_indices.collect();
137
138 let direct_account_pointers_in_program_input = invoke_context
139 .get_feature_set()
140 .direct_account_pointers_in_program_input;
141
142 let (mut parameter_bytes, _regions, _account_lengths, _instruction_data_offset) =
144 serialize_parameters(
145 &instruction_context,
146 false, false, direct_account_pointers_in_program_input,
149 )?;
150
151 let (program_id, account_infos, input) =
153 unsafe { deserialize(&mut parameter_bytes.as_slice_mut()[0] as *mut u8) };
154
155 match std::panic::catch_unwind(AssertUnwindSafe(|| {
157 builtin_function(program_id, &account_infos, input)
158 })) {
159 Ok(program_result) => {
160 program_result.map_err(|program_error| {
161 let err = InstructionError::from(u64::from(program_error));
162 stable_log::program_failure(&log_collector, program_id, &err);
163 let err: Box<dyn std::error::Error> = Box::new(err);
164 err
165 })?;
166 }
167 Err(_panic_error) => {
168 let err = InstructionError::ProgramFailedToComplete;
169 stable_log::program_failure(&log_collector, program_id, &err);
170 let err: Box<dyn std::error::Error> = Box::new(err);
171 Err(err)?;
172 }
173 };
174
175 stable_log::program_success(&log_collector, program_id);
176
177 let account_info_map: HashMap<_, _> = account_infos.into_iter().map(|a| (a.key, a)).collect();
179
180 let transaction_context = &invoke_context.transaction_context;
183 let instruction_context = transaction_context.get_current_instruction_context()?;
184
185 for i in deduplicated_indices.into_iter() {
187 let mut borrowed_account = instruction_context.try_borrow_instruction_account(i)?;
188 if borrowed_account.is_writable() {
189 if let Some(account_info) = account_info_map.get(borrowed_account.get_key()) {
190 if borrowed_account.get_lamports() != account_info.lamports() {
191 borrowed_account.set_lamports(account_info.lamports())?;
192 }
193
194 if borrowed_account
195 .can_data_be_resized(account_info.data_len())
196 .is_ok()
197 {
198 borrowed_account.set_data_from_slice(&account_info.data.borrow())?;
199 }
200 if borrowed_account.get_owner() != account_info.owner {
201 borrowed_account.set_owner(account_info.owner.as_ref())?;
202 }
203 }
204 }
205 }
206
207 Ok(0)
208}
209
210#[macro_export]
213macro_rules! processor {
214 ($builtin_function:expr) => {
215 Some(|vm, _arg0, _arg1, _arg2, _arg3, _arg4| {
216 let vm = unsafe {
217 &mut *((vm as *mut u64).offset(-($crate::get_runtime_environment_key() as isize))
218 as *mut $crate::EbpfVm<$crate::InvokeContext>)
219 };
220 vm.program_result =
221 $crate::invoke_builtin_function($builtin_function, vm.context_object_pointer)
222 .map_err(|err| $crate::EbpfError::SyscallError(err))
223 .into();
224 })
225 };
226}
227
228fn get_sysvar<T: Default + SysvarSerialize + Sized + serde::de::DeserializeOwned + Clone>(
229 sysvar: Result<Arc<T>, InstructionError>,
230 var_addr: *mut u8,
231) -> u64 {
232 let invoke_context = get_invoke_context();
233 if invoke_context
234 .consume_checked(invoke_context.get_execution_cost().sysvar_base_cost + T::size_of() as u64)
235 .is_err()
236 {
237 panic!("Exceeded compute budget");
238 }
239
240 match sysvar {
241 Ok(sysvar_data) => unsafe {
242 *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
243 SUCCESS
244 },
245 Err(_) => UNSUPPORTED_SYSVAR,
246 }
247}
248
249struct SyscallStubs {}
250
251impl SyscallStubs {
252 fn fetch_and_write_sysvar<T: SysvarSerialize>(
253 &self,
254 var_addr: *mut u8,
255 offset: u64,
256 length: u64,
257 fetch: impl FnOnce(&SysvarCache) -> Result<Arc<T>, InstructionError>,
258 ) -> u64 {
259 let invoke_context = get_invoke_context();
261 let SVMTransactionExecutionCost {
262 sysvar_base_cost,
263 cpi_bytes_per_unit,
264 mem_op_base_cost,
265 ..
266 } = *invoke_context.get_execution_cost();
267
268 let sysvar_id_cost = 32_u64.checked_div(cpi_bytes_per_unit).unwrap_or(0);
269 let sysvar_buf_cost = length.checked_div(cpi_bytes_per_unit).unwrap_or(0);
270
271 if invoke_context
272 .consume_checked(
273 sysvar_base_cost
274 .saturating_add(sysvar_id_cost)
275 .saturating_add(std::cmp::max(sysvar_buf_cost, mem_op_base_cost)),
276 )
277 .is_err()
278 {
279 panic!("Exceeded compute budget");
280 }
281
282 let Ok(sysvar) = fetch(get_invoke_context().get_sysvar_cache()) else {
284 return UNSUPPORTED_SYSVAR;
285 };
286
287 let Ok(expected_length) = bincode::serialized_size(&sysvar) else {
290 return UNSUPPORTED_SYSVAR;
291 };
292
293 if offset.saturating_add(length) > expected_length {
294 return UNSUPPORTED_SYSVAR;
295 }
296
297 if let Ok(serialized) = bincode::serialize(&sysvar) {
299 unsafe {
300 ptr::copy_nonoverlapping(
301 serialized[offset as usize..].as_ptr(),
302 var_addr,
303 length as usize,
304 )
305 };
306 SUCCESS
307 } else {
308 UNSUPPORTED_SYSVAR
309 }
310 }
311}
312impl solana_sysvar::program_stubs::SyscallStubs for SyscallStubs {
313 fn sol_log(&self, message: &str) {
314 let invoke_context = get_invoke_context();
315 ic_msg!(invoke_context, "Program log: {}", message);
316 }
317
318 fn sol_invoke_signed(
319 &self,
320 instruction: &Instruction,
321 account_infos: &[AccountInfo],
322 signers_seeds: &[&[&[u8]]],
323 ) -> ProgramResult {
324 let invoke_context = get_invoke_context();
325 let log_collector = invoke_context.get_log_collector();
326 let transaction_context = &invoke_context.transaction_context;
327 let instruction_context = transaction_context
328 .get_current_instruction_context()
329 .unwrap();
330 let caller = instruction_context.get_program_key().unwrap();
331
332 stable_log::program_invoke(
333 &log_collector,
334 &instruction.program_id,
335 invoke_context.get_stack_height(),
336 );
337
338 let signers = signers_seeds
339 .iter()
340 .map(|seeds| Pubkey::create_program_address(seeds, caller).unwrap())
341 .collect::<Vec<_>>();
342
343 invoke_context
344 .prepare_next_cpi_instruction(instruction.clone(), &signers)
345 .unwrap();
346
347 let transaction_context = &invoke_context.transaction_context;
349 let instruction_context = transaction_context
350 .get_current_instruction_context()
351 .unwrap();
352 let next_instruction_context = transaction_context.get_next_instruction_context().unwrap();
353 let next_instruction_accounts = next_instruction_context.instruction_accounts();
354 let mut account_indices = Vec::with_capacity(next_instruction_accounts.len());
355 for instruction_account in next_instruction_accounts.iter() {
356 let account_key = transaction_context
357 .get_key_of_account_at_index(instruction_account.index_in_transaction)
358 .unwrap();
359 let account_info_index = account_infos
360 .iter()
361 .position(|account_info| account_info.unsigned_key() == account_key)
362 .ok_or(InstructionError::MissingAccount)
363 .unwrap();
364 let account_info = &account_infos[account_info_index];
365 let index_in_caller = instruction_context
366 .get_index_of_account_in_instruction(instruction_account.index_in_transaction)
367 .unwrap();
368 let mut borrowed_account = instruction_context
369 .try_borrow_instruction_account(index_in_caller)
370 .unwrap();
371 if borrowed_account.get_lamports() != account_info.lamports() {
372 borrowed_account
373 .set_lamports(account_info.lamports())
374 .unwrap();
375 }
376 let account_info_data = account_info.try_borrow_data().unwrap();
377 match borrowed_account.can_data_be_resized(account_info_data.len()) {
379 Ok(()) => borrowed_account
380 .set_data_from_slice(&account_info_data)
381 .unwrap(),
382 Err(err) if borrowed_account.get_data() != *account_info_data => {
383 panic!("{err:?}");
384 }
385 _ => {}
386 }
387 if borrowed_account.get_owner() != account_info.owner {
389 borrowed_account
390 .set_owner(account_info.owner.as_ref())
391 .unwrap();
392 }
393 if instruction_account.is_writable() {
394 account_indices
395 .push((instruction_account.index_in_transaction, account_info_index));
396 }
397 }
398
399 let mut compute_units_consumed = 0;
400 invoke_context
401 .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default())
402 .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
403
404 let transaction_context = &invoke_context.transaction_context;
406 let instruction_context = transaction_context
407 .get_current_instruction_context()
408 .unwrap();
409 for (index_in_transaction, account_info_index) in account_indices.into_iter() {
410 let index_in_caller = instruction_context
411 .get_index_of_account_in_instruction(index_in_transaction)
412 .unwrap();
413 let borrowed_account = instruction_context
414 .try_borrow_instruction_account(index_in_caller)
415 .unwrap();
416 let account_info = &account_infos[account_info_index];
417 **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
418 if account_info.owner != borrowed_account.get_owner() {
419 #[allow(clippy::transmute_ptr_to_ptr)]
421 #[allow(mutable_transmutes)]
422 let account_info_mut =
423 unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
424 *account_info_mut = *borrowed_account.get_owner();
425 }
426
427 let new_data = borrowed_account.get_data();
428 let new_len = new_data.len();
429
430 if account_info.data_len() != new_len {
432 account_info.resize(new_len)?;
433 }
434
435 let mut data = account_info.try_borrow_mut_data()?;
437 data.clone_from_slice(new_data);
438 }
439
440 stable_log::program_success(&log_collector, &instruction.program_id);
441 Ok(())
442 }
443
444 fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
445 get_sysvar(
446 get_invoke_context().get_sysvar_cache().get_clock(),
447 var_addr,
448 )
449 }
450
451 fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
452 get_sysvar(
453 get_invoke_context().get_sysvar_cache().get_epoch_schedule(),
454 var_addr,
455 )
456 }
457
458 fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
459 get_sysvar(
460 get_invoke_context().get_sysvar_cache().get_epoch_rewards(),
461 var_addr,
462 )
463 }
464
465 #[allow(deprecated)]
466 fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
467 get_sysvar(get_invoke_context().get_sysvar_cache().get_fees(), var_addr)
468 }
469
470 fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
471 get_sysvar(get_invoke_context().get_sysvar_cache().get_rent(), var_addr)
472 }
473
474 fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
475 get_sysvar(
476 get_invoke_context()
477 .get_sysvar_cache()
478 .get_last_restart_slot(),
479 var_addr,
480 )
481 }
482
483 fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
484 let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
485 Some((*program_id, data.to_vec()))
486 }
487
488 fn sol_set_return_data(&self, data: &[u8]) {
489 let invoke_context = get_invoke_context();
490 let transaction_context = &mut invoke_context.transaction_context;
491 let instruction_context = transaction_context
492 .get_current_instruction_context()
493 .unwrap();
494 let caller = *instruction_context.get_program_key().unwrap();
495 transaction_context
496 .set_return_data(caller, data.to_vec())
497 .unwrap();
498 }
499
500 fn sol_get_stack_height(&self) -> u64 {
501 let invoke_context = get_invoke_context();
502 invoke_context.get_stack_height().try_into().unwrap()
503 }
504
505 fn sol_get_sysvar(
506 &self,
507 sysvar_id_addr: *const u8,
508 var_addr: *mut u8,
509 offset: u64,
510 length: u64,
511 ) -> u64 {
512 let sysvar_id = unsafe { &*(sysvar_id_addr as *const Pubkey) };
513
514 match *sysvar_id {
515 id if id == Clock::id() => self.fetch_and_write_sysvar::<Clock>(
516 var_addr,
517 offset,
518 length,
519 SysvarCache::get_clock,
520 ),
521 id if id == EpochRewards::id() => self.fetch_and_write_sysvar::<EpochRewards>(
522 var_addr,
523 offset,
524 length,
525 SysvarCache::get_epoch_rewards,
526 ),
527 id if id == EpochSchedule::id() => self.fetch_and_write_sysvar::<EpochSchedule>(
528 var_addr,
529 offset,
530 length,
531 SysvarCache::get_epoch_schedule,
532 ),
533 id if id == LastRestartSlot::id() => self.fetch_and_write_sysvar::<LastRestartSlot>(
534 var_addr,
535 offset,
536 length,
537 SysvarCache::get_last_restart_slot,
538 ),
539 id if id == Rent::id() => {
540 self.fetch_and_write_sysvar::<Rent>(var_addr, offset, length, SysvarCache::get_rent)
541 }
542 _ => UNSUPPORTED_SYSVAR,
543 }
544 }
545}
546
547pub fn find_file(filename: &str) -> Option<PathBuf> {
548 for dir in default_shared_object_dirs() {
549 let candidate = dir.join(filename);
550 if candidate.exists() {
551 return Some(candidate);
552 }
553 }
554 None
555}
556
557fn default_shared_object_dirs() -> Vec<PathBuf> {
558 let mut search_path = vec![];
559 if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
560 search_path.push(PathBuf::from(bpf_out_dir));
561 } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
562 search_path.push(PathBuf::from(bpf_out_dir));
563 }
564 search_path.push(PathBuf::from("tests/fixtures"));
565 if let Ok(dir) = std::env::current_dir() {
566 search_path.push(dir);
567 }
568 trace!("SBF .so search path: {search_path:?}");
569 search_path
570}
571
572pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
573 let path = path.as_ref();
574 let mut file = File::open(path)
575 .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
576
577 let mut file_data = Vec::new();
578 file.read_to_end(&mut file_data)
579 .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
580 file_data
581}
582
583pub struct ProgramTest {
584 accounts: Vec<(Pubkey, AccountSharedData)>,
585 genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
586 builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
587 compute_max_units: Option<u64>,
588 prefer_bpf: bool,
589 deactivate_feature_set: HashSet<Pubkey>,
590 transaction_account_lock_limit: Option<usize>,
591}
592
593impl Default for ProgramTest {
594 fn default() -> Self {
607 agave_logger::setup_with_default(
608 "solana_sbpf::vm=debug,solana_runtime::message_processor=debug,\
609 solana_runtime::system_instruction_processor=trace,solana_program_test=info",
610 );
611 let prefer_bpf =
612 std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
613
614 Self {
615 accounts: vec![],
616 genesis_accounts: vec![],
617 builtin_programs: vec![],
618 compute_max_units: None,
619 prefer_bpf,
620 deactivate_feature_set: HashSet::default(),
621 transaction_account_lock_limit: None,
622 }
623 }
624}
625
626impl ProgramTest {
627 pub fn new(
635 program_name: &'static str,
636 program_id: Pubkey,
637 builtin_function: Option<BuiltinFunctionWithContext>,
638 ) -> Self {
639 let mut me = Self::default();
640 me.add_program(program_name, program_id, builtin_function);
641 me
642 }
643
644 pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
646 self.prefer_bpf = prefer_bpf;
647 }
648
649 pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
651 debug_assert!(
652 compute_max_units <= i64::MAX as u64,
653 "Compute unit limit must fit in `i64::MAX`"
654 );
655 self.compute_max_units = Some(compute_max_units);
656 }
657
658 pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
660 self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
661 }
662
663 pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
665 self.genesis_accounts
666 .push((address, AccountSharedData::from(account)));
667 }
668
669 pub fn add_account(&mut self, address: Pubkey, account: Account) {
671 self.accounts
672 .push((address, AccountSharedData::from(account)));
673 }
674
675 pub fn add_account_with_file_data(
677 &mut self,
678 address: Pubkey,
679 lamports: u64,
680 owner: Pubkey,
681 filename: &str,
682 ) {
683 self.add_account(
684 address,
685 Account {
686 lamports,
687 data: read_file(find_file(filename).unwrap_or_else(|| {
688 panic!("Unable to locate {filename}");
689 })),
690 owner,
691 executable: false,
692 rent_epoch: 0,
693 },
694 );
695 }
696
697 pub fn add_account_with_base64_data(
700 &mut self,
701 address: Pubkey,
702 lamports: u64,
703 owner: Pubkey,
704 data_base64: &str,
705 ) {
706 self.add_account(
707 address,
708 Account {
709 lamports,
710 data: BASE64_STANDARD
711 .decode(data_base64)
712 .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
713 owner,
714 executable: false,
715 rent_epoch: 0,
716 },
717 );
718 }
719
720 pub fn add_sysvar_account<S: SysvarSerialize>(&mut self, address: Pubkey, sysvar: &S) {
721 let account = create_account_shared_data_for_test(sysvar);
722 self.add_account(address, account.into());
723 }
724
725 pub fn add_upgradeable_program_to_genesis(
738 &mut self,
739 program_name: &'static str,
740 program_id: &Pubkey,
741 ) {
742 let program_file = find_file(&format!("{program_name}.so")).unwrap_or_else(|| {
743 panic!("Program file data not available for {program_name} ({program_id})")
744 });
745 let elf = read_file(program_file);
746 let program_accounts =
747 programs::bpf_loader_upgradeable_program_accounts(program_id, &elf, &Rent::default());
748 for (address, account) in program_accounts {
749 self.add_genesis_account(address, account);
750 }
751 }
752
753 pub fn add_program(
761 &mut self,
762 program_name: &'static str,
763 program_id: Pubkey,
764 builtin_function: Option<BuiltinFunctionWithContext>,
765 ) {
766 let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
767 let data = read_file(&program_file);
768 info!(
769 "\"{}\" SBF program from {}{}",
770 program_name,
771 program_file.display(),
772 std::fs::metadata(&program_file)
773 .map(|metadata| {
774 metadata
775 .modified()
776 .map(|time| {
777 format!(
778 ", modified {}",
779 HumanTime::from(time)
780 .to_text_en(Accuracy::Precise, Tense::Past)
781 )
782 })
783 .ok()
784 })
785 .ok()
786 .flatten()
787 .unwrap_or_default()
788 );
789
790 this.add_account(
791 program_id,
792 Account {
793 lamports: Rent::default().minimum_balance(data.len()).max(1),
794 data,
795 owner: solana_sdk_ids::bpf_loader::id(),
796 executable: true,
797 rent_epoch: 0,
798 },
799 );
800 };
801
802 let warn_invalid_program_name = || {
803 let valid_program_names = default_shared_object_dirs()
804 .iter()
805 .filter_map(|dir| dir.read_dir().ok())
806 .flat_map(|read_dir| {
807 read_dir.filter_map(|entry| {
808 let path = entry.ok()?.path();
809 if !path.is_file() {
810 return None;
811 }
812 match path.extension()?.to_str()? {
813 "so" => Some(path.file_stem()?.to_os_string()),
814 _ => None,
815 }
816 })
817 })
818 .collect::<Vec<_>>();
819
820 if valid_program_names.is_empty() {
821 warn!("No SBF shared objects found.");
824 return;
825 }
826
827 warn!(
828 "Possible bogus program name. Ensure the program name ({program_name}) matches \
829 one of the following recognizable program names:",
830 );
831 for name in valid_program_names {
832 warn!(" - {}", name.to_str().unwrap());
833 }
834 };
835
836 let program_file = find_file(&format!("{program_name}.so"));
837 match (self.prefer_bpf, program_file, builtin_function) {
838 (true, Some(file), _) => add_bpf(self, file),
841
842 (false, _, Some(builtin_function)) => {
845 self.add_builtin_program(program_name, program_id, builtin_function)
846 }
847
848 (true, None, _) => {
850 warn_invalid_program_name();
851 panic!("Program file data not available for {program_name} ({program_id})");
852 }
853
854 (false, _, None) => {
856 panic!("Program processor not available for {program_name} ({program_id})");
857 }
858 }
859 }
860
861 pub fn add_builtin_program(
865 &mut self,
866 program_name: &'static str,
867 program_id: Pubkey,
868 builtin_function: BuiltinFunctionWithContext,
869 ) {
870 info!("\"{program_name}\" builtin program");
871 self.builtin_programs.push((
872 program_id,
873 program_name,
874 ProgramCacheEntry::new_builtin(0, program_name.len(), builtin_function),
875 ));
876 }
877
878 pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
882 self.deactivate_feature_set.insert(feature_id);
883 }
884
885 fn setup_bank(
886 &mut self,
887 ) -> (
888 Arc<RwLock<BankForks>>,
889 Arc<RwLock<BlockCommitmentCache>>,
890 Hash,
891 GenesisConfigInfo,
892 ) {
893 {
894 use std::sync::Once;
895 static ONCE: Once = Once::new();
896
897 ONCE.call_once(|| {
898 solana_sysvar::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
899 });
900 }
901
902 let rent = Rent::default();
903 let fee_rate_governor = FeeRateGovernor {
904 lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
906 ..FeeRateGovernor::default()
907 };
908 let bootstrap_validator_pubkey = Pubkey::new_unique();
909 let bootstrap_validator_stake_lamports =
910 rent.minimum_balance(VoteStateV4::size_of()) + 1_000_000 * LAMPORTS_PER_SOL;
911
912 let mint_keypair = Keypair::new();
913 let voting_keypair = Keypair::new();
914
915 let mut feature_set = FeatureSet::all_enabled();
917 for deactivate_feature_pk in &self.deactivate_feature_set {
918 if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
919 feature_set.deactivate(deactivate_feature_pk);
920 } else {
921 warn!(
922 "Feature {deactivate_feature_pk:?} set for deactivation is not a known \
923 Feature public key"
924 );
925 }
926 }
927
928 let mut genesis_config = create_genesis_config_with_leader_ex(
929 1_000_000 * LAMPORTS_PER_SOL,
930 &mint_keypair.pubkey(),
931 &bootstrap_validator_pubkey,
932 &voting_keypair.pubkey(),
933 &Pubkey::new_unique(),
934 None,
935 bootstrap_validator_stake_lamports,
936 890_880,
937 fee_rate_governor,
938 rent.clone(),
939 ClusterType::Development,
940 &feature_set,
941 std::mem::take(&mut self.genesis_accounts),
942 );
943
944 let target_tick_duration = Duration::from_micros(100);
945 genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
946 debug!("Payer address: {}", mint_keypair.pubkey());
947 debug!("Genesis config: {genesis_config}");
948
949 let bank = Bank::new_from_genesis(
950 &genesis_config,
951 Arc::new(RuntimeConfig {
952 compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
953 compute_unit_limit: max_units,
954 ..ComputeBudget::new_with_defaults(
955 genesis_config
956 .accounts
957 .contains_key(&raise_cpi_nesting_limit_to_8::id()),
958 genesis_config
959 .accounts
960 .contains_key(&increase_cpi_account_info_limit::id()),
961 )
962 }),
963 transaction_account_lock_limit: self.transaction_account_lock_limit,
964 ..RuntimeConfig::default()
965 }),
966 Vec::default(),
967 None,
968 ACCOUNTS_DB_CONFIG_FOR_TESTING,
969 None,
970 None,
971 Arc::default(),
972 None,
973 None,
974 );
975
976 for (program_id, account) in programs::spl_programs(&rent).iter() {
978 bank.store_account(program_id, account);
979 }
980
981 for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
983 genesis_config.accounts.contains_key(feature_id)
984 })
985 .iter()
986 {
987 bank.store_account(program_id, account);
988 }
989
990 let mut builtin_programs = Vec::new();
992 std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
993 for (program_id, name, builtin) in builtin_programs.into_iter() {
994 bank.add_builtin(program_id, name, builtin);
995 }
996
997 for (address, account) in self.accounts.iter() {
998 if bank.get_account(address).is_some() {
999 info!("Overriding account at {address}");
1000 }
1001 bank.store_account(address, account);
1002 }
1003 bank.set_capitalization_for_tests(bank.calculate_capitalization_for_tests());
1004 let bank = {
1006 let bank = Arc::new(bank);
1007 bank.fill_bank_with_ticks_for_tests();
1008 let bank = Bank::new_from_parent(bank.clone(), bank.leader_id(), bank.slot() + 1);
1009 debug!("Bank slot: {}", bank.slot());
1010 bank
1011 };
1012 let slot = bank.slot();
1013 let last_blockhash = bank.last_blockhash();
1014 let bank_forks = BankForks::new_rw_arc(bank);
1015 let block_commitment_cache = Arc::new(RwLock::new(
1016 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
1017 ));
1018
1019 (
1020 bank_forks,
1021 block_commitment_cache,
1022 last_blockhash,
1023 GenesisConfigInfo {
1024 genesis_config,
1025 mint_keypair,
1026 voting_keypair,
1027 validator_pubkey: bootstrap_validator_pubkey,
1028 },
1029 )
1030 }
1031
1032 pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
1033 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1034 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1035 let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
1036 let transport = start_local_server(
1037 bank_forks.clone(),
1038 block_commitment_cache.clone(),
1039 target_tick_duration,
1040 )
1041 .await;
1042 let banks_client = start_client(transport)
1043 .await
1044 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1045
1046 tokio::spawn(async move {
1050 loop {
1051 tokio::time::sleep(target_slot_duration).await;
1052 bank_forks
1053 .read()
1054 .unwrap()
1055 .working_bank()
1056 .register_unique_recent_blockhash_for_test();
1057 }
1058 });
1059
1060 (banks_client, gci.mint_keypair, last_blockhash)
1061 }
1062
1063 pub async fn start_with_context(mut self) -> ProgramTestContext {
1068 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1069 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1070 let transport = start_local_server(
1071 bank_forks.clone(),
1072 block_commitment_cache.clone(),
1073 target_tick_duration,
1074 )
1075 .await;
1076 let banks_client = start_client(transport)
1077 .await
1078 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1079
1080 ProgramTestContext::new(
1081 bank_forks,
1082 block_commitment_cache,
1083 banks_client,
1084 last_blockhash,
1085 gci,
1086 )
1087 }
1088}
1089
1090#[async_trait]
1091pub trait ProgramTestBanksClientExt {
1092 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
1094}
1095
1096#[async_trait]
1097impl ProgramTestBanksClientExt for BanksClient {
1098 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
1099 let mut num_retries = 0;
1100 let start = Instant::now();
1101 while start.elapsed().as_secs() < 5 {
1102 let new_blockhash = self.get_latest_blockhash().await?;
1103 if new_blockhash != *blockhash {
1104 return Ok(new_blockhash);
1105 }
1106 debug!("Got same blockhash ({blockhash:?}), will retry...");
1107
1108 tokio::time::sleep(Duration::from_millis(200)).await;
1109 num_retries += 1;
1110 }
1111
1112 Err(io::Error::other(format!(
1113 "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1114 start.elapsed().as_millis(),
1115 num_retries,
1116 blockhash
1117 )))
1118 }
1119}
1120
1121struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1122
1123impl<T> Drop for DroppableTask<T> {
1124 fn drop(&mut self) {
1125 self.0.store(true, Ordering::Relaxed);
1126 trace!(
1127 "stopping task, which is currently {}",
1128 if self.1.is_finished() {
1129 "finished"
1130 } else {
1131 "running"
1132 }
1133 );
1134 }
1135}
1136
1137pub struct ProgramTestContext {
1138 pub banks_client: BanksClient,
1139 pub last_blockhash: Hash,
1140 pub payer: Keypair,
1141 genesis_config: GenesisConfig,
1142 bank_forks: Arc<RwLock<BankForks>>,
1143 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1144 _bank_task: DroppableTask<()>,
1145}
1146
1147impl ProgramTestContext {
1148 fn new(
1149 bank_forks: Arc<RwLock<BankForks>>,
1150 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1151 banks_client: BanksClient,
1152 last_blockhash: Hash,
1153 genesis_config_info: GenesisConfigInfo,
1154 ) -> Self {
1155 let running_bank_forks = bank_forks.clone();
1159 let target_tick_duration = genesis_config_info
1160 .genesis_config
1161 .poh_config
1162 .target_tick_duration;
1163 let target_slot_duration =
1164 target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1165 let exit = Arc::new(AtomicBool::new(false));
1166 let bank_task = DroppableTask(
1167 exit.clone(),
1168 tokio::spawn(async move {
1169 loop {
1170 if exit.load(Ordering::Relaxed) {
1171 break;
1172 }
1173 tokio::time::sleep(target_slot_duration).await;
1174 running_bank_forks
1175 .read()
1176 .unwrap()
1177 .working_bank()
1178 .register_unique_recent_blockhash_for_test();
1179 }
1180 }),
1181 );
1182
1183 Self {
1184 banks_client,
1185 last_blockhash,
1186 payer: genesis_config_info.mint_keypair,
1187 genesis_config: genesis_config_info.genesis_config,
1188 bank_forks,
1189 block_commitment_cache,
1190 _bank_task: bank_task,
1191 }
1192 }
1193
1194 pub fn genesis_config(&self) -> &GenesisConfig {
1195 &self.genesis_config
1196 }
1197
1198 pub fn increment_vote_account_credits(
1200 &mut self,
1201 vote_account_address: &Pubkey,
1202 number_of_credits: u64,
1203 ) {
1204 let bank_forks = self.bank_forks.read().unwrap();
1205 let bank = bank_forks.working_bank();
1206
1207 let mut vote_account = bank.get_account(vote_account_address).unwrap();
1209 let mut vote_state =
1210 VoteStateV4::deserialize(vote_account.data(), vote_account_address).unwrap();
1211
1212 let epoch = bank.epoch();
1213 const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
1215 for _ in 0..number_of_credits {
1216 let credits = 1;
1218
1219 if vote_state.epoch_credits.is_empty() {
1221 vote_state.epoch_credits.push((epoch, 0, 0));
1222 } else if epoch != vote_state.epoch_credits.last().unwrap().0 {
1223 let (_, credits_val, prev_credits) = *vote_state.epoch_credits.last().unwrap();
1224
1225 if credits_val != prev_credits {
1226 vote_state
1229 .epoch_credits
1230 .push((epoch, credits_val, credits_val));
1231 } else {
1232 vote_state.epoch_credits.last_mut().unwrap().0 = epoch;
1234 }
1235
1236 if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
1238 vote_state.epoch_credits.remove(0);
1239 }
1240 }
1241
1242 vote_state.epoch_credits.last_mut().unwrap().1 = vote_state
1243 .epoch_credits
1244 .last()
1245 .unwrap()
1246 .1
1247 .saturating_add(credits);
1248 }
1249 let versioned = VoteStateVersions::new_v4(vote_state);
1250 vote_account.set_state(&versioned).unwrap();
1251 bank.store_account(vote_account_address, &vote_account);
1252 }
1253
1254 pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1261 let bank_forks = self.bank_forks.read().unwrap();
1262 let bank = bank_forks.working_bank();
1263 bank.store_account(address, account);
1264 }
1265
1266 pub fn set_sysvar<T: SysvarId + SysvarSerialize>(&self, sysvar: &T) {
1273 let bank_forks = self.bank_forks.read().unwrap();
1274 let bank = bank_forks.working_bank();
1275 bank.set_sysvar_for_tests(sysvar);
1276 }
1277
1278 pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1280 let mut bank_forks = self.bank_forks.write().unwrap();
1281 let bank = bank_forks.working_bank();
1282
1283 bank.fill_bank_with_ticks_for_tests();
1286
1287 let working_slot = bank.slot();
1289 if warp_slot <= working_slot {
1290 return Err(ProgramTestError::InvalidWarpSlot);
1291 }
1292
1293 let pre_warp_slot = warp_slot - 1;
1297 let warp_bank = if pre_warp_slot == working_slot {
1298 bank.freeze();
1299 bank
1300 } else {
1301 bank_forks
1302 .insert(Bank::warp_from_parent(
1303 bank,
1304 &Pubkey::default(),
1305 pre_warp_slot,
1306 ))
1307 .clone_without_scheduler()
1308 };
1309
1310 bank_forks.set_root(
1311 pre_warp_slot,
1312 None, Some(pre_warp_slot),
1314 );
1315
1316 bank_forks.insert(Bank::new_from_parent(
1318 warp_bank,
1319 &Pubkey::default(),
1320 warp_slot,
1321 ));
1322
1323 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1326 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1331
1332 let bank = bank_forks.working_bank();
1333 self.last_blockhash = bank.last_blockhash();
1334 Ok(())
1335 }
1336
1337 pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1338 let warp_slot = self
1339 .genesis_config
1340 .epoch_schedule
1341 .get_first_slot_in_epoch(warp_epoch);
1342 self.warp_to_slot(warp_slot)
1343 }
1344
1345 pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1347 let mut bank_forks = self.bank_forks.write().unwrap();
1348 let bank = bank_forks.working_bank();
1349
1350 bank.fill_bank_with_ticks_for_tests();
1353 let pre_warp_slot = bank.slot();
1354
1355 bank_forks.set_root(
1356 pre_warp_slot,
1357 None, Some(pre_warp_slot),
1359 );
1360
1361 let warp_slot = pre_warp_slot + 1;
1363 let mut warp_bank = Bank::new_from_parent(bank, &Pubkey::default(), warp_slot);
1364
1365 warp_bank.force_reward_interval_end_for_tests();
1366 bank_forks.insert(warp_bank);
1367
1368 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1371 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1376
1377 let bank = bank_forks.working_bank();
1378 self.last_blockhash = bank.last_blockhash();
1379 Ok(())
1380 }
1381
1382 pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1384 let blockhash = self
1385 .banks_client
1386 .get_new_latest_blockhash(&self.last_blockhash)
1387 .await?;
1388 self.last_blockhash = blockhash;
1389 Ok(blockhash)
1390 }
1391
1392 pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1394 self.bank_forks
1395 .read()
1396 .unwrap()
1397 .working_bank()
1398 .register_hard_fork(hard_fork_slot)
1399 }
1400}