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