1#![cfg(feature = "agave-unstable-api")]
2#![allow(clippy::arithmetic_side_effects)]
4
5pub use tokio;
7use {
8 agave_feature_set::{FEATURE_NAMES, FeatureSet, raise_cpi_nesting_limit_to_8},
9 async_trait::async_trait,
10 base64::{Engine, prelude::BASE64_STANDARD},
11 chrono_humanize::{Accuracy, HumanTime, Tense},
12 log::*,
13 solana_account::{
14 Account, AccountSharedData, ReadableAccount, create_account_shared_data_for_test,
15 state_traits::StateMut,
16 },
17 solana_account_info::AccountInfo,
18 solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING,
19 solana_address::Address,
20 solana_banks_client::start_client,
21 solana_banks_server::banks_server::start_local_server,
22 solana_clock::{Clock, Epoch, Slot},
23 solana_cluster_type::ClusterType,
24 solana_compute_budget::compute_budget::{ComputeBudget, SVMTransactionExecutionCost},
25 solana_epoch_rewards::EpochRewards,
26 solana_epoch_schedule::EpochSchedule,
27 solana_fee_calculator::{DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, FeeRateGovernor},
28 solana_genesis_config::GenesisConfig,
29 solana_hash::Hash,
30 solana_instruction::{
31 Instruction,
32 error::{InstructionError, UNSUPPORTED_SYSVAR},
33 },
34 solana_keypair::Keypair,
35 solana_native_token::LAMPORTS_PER_SOL,
36 solana_poh_config::PohConfig,
37 solana_program_binaries as programs,
38 solana_program_entrypoint::{SUCCESS, deserialize},
39 solana_program_error::{ProgramError, ProgramResult},
40 solana_program_runtime::{
41 invoke_context::BuiltinFunctionRegisterer, program_cache_entry::ProgramCacheEntry,
42 serialization::serialize_parameters, stable_log, sysvar_cache::SysvarCache,
43 },
44 solana_pubkey::Pubkey,
45 solana_rent::Rent,
46 solana_runtime::{
47 bank::Bank,
48 bank_forks::BankForks,
49 commitment::BlockCommitmentCache,
50 genesis_utils::{GenesisConfigInfo, create_genesis_config_with_leader_ex},
51 runtime_config::RuntimeConfig,
52 },
53 solana_signer::Signer,
54 solana_svm_log_collector::ic_msg,
55 solana_svm_timings::ExecuteTimings,
56 solana_sysvar::{SysvarSerialize, last_restart_slot::LastRestartSlot},
57 solana_sysvar_id::SysvarId,
58 solana_vote_program::vote_state::{VoteStateV4, VoteStateVersions},
59 std::{
60 cell::RefCell,
61 collections::{HashMap, HashSet},
62 fs::File,
63 io::{self, Read},
64 mem::transmute,
65 panic::AssertUnwindSafe,
66 path::{Path, PathBuf},
67 ptr,
68 sync::{
69 Arc, RwLock,
70 atomic::{AtomicBool, Ordering},
71 },
72 time::{Duration, Instant},
73 },
74 thiserror::Error,
75 tokio::task::JoinHandle,
76};
77pub use {
79 solana_banks_client::{BanksClient, BanksClientError},
80 solana_banks_interface::BanksTransactionResultWithMetadata,
81 solana_program_runtime::invoke_context::InvokeContext,
82 solana_sbpf::{
83 error::EbpfError,
84 memory_region::MemoryMapping,
85 program::BuiltinFunctionDefinition,
86 vm::{EbpfVm, EncryptedHostAddressToEbpfVm, 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.compute_meter.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 struct Converter;
217 impl $crate::BuiltinFunctionDefinition<$crate::InvokeContext<'_, '_>> for Converter {
218 type Error = Box<dyn std::error::Error>;
219 fn rust(
220 _: &mut $crate::InvokeContext<'_, '_>,
221 _: u64,
222 _: u64,
223 _: u64,
224 _: u64,
225 _: u64,
226 ) -> Result<u64, Box<dyn std::error::Error>> {
227 unreachable!()
228 }
229 fn vm(
230 mut vm: $crate::EncryptedHostAddressToEbpfVm<$crate::InvokeContext>,
231 _: u64,
232 _: u64,
233 _: u64,
234 _: u64,
235 _: u64,
236 ) {
237 unsafe {
238 vm.with_vm(|vm| {
239 vm.program_result =
240 $crate::invoke_builtin_function($builtin_function, vm.context())
241 .map_err(|err| $crate::EbpfError::SyscallError(err))
242 .into();
243 });
244 }
245 }
246 };
247 Some(<Converter as $crate::BuiltinFunctionDefinition<_>>::register)
248 }};
249}
250
251fn get_sysvar<T: Default + SysvarSerialize + Sized + serde::de::DeserializeOwned + Clone>(
252 sysvar: Result<Arc<T>, InstructionError>,
253 var_addr: *mut u8,
254) -> u64 {
255 let invoke_context = get_invoke_context();
256 if invoke_context
257 .compute_meter
258 .consume_checked(invoke_context.get_execution_cost().sysvar_base_cost + T::size_of() as u64)
259 .is_err()
260 {
261 panic!("Exceeded compute budget");
262 }
263
264 match sysvar {
265 Ok(sysvar_data) => unsafe {
266 *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
267 SUCCESS
268 },
269 Err(_) => UNSUPPORTED_SYSVAR,
270 }
271}
272
273struct SyscallStubs {}
274
275impl SyscallStubs {
276 fn fetch_and_write_sysvar<T: SysvarSerialize>(
277 &self,
278 var_addr: *mut u8,
279 offset: u64,
280 length: u64,
281 fetch: impl FnOnce(&SysvarCache) -> Result<Arc<T>, InstructionError>,
282 ) -> u64 {
283 let invoke_context = get_invoke_context();
285 let SVMTransactionExecutionCost {
286 sysvar_base_cost,
287 cpi_bytes_per_unit,
288 mem_op_base_cost,
289 ..
290 } = *invoke_context.get_execution_cost();
291
292 let sysvar_id_cost = 32_u64.checked_div(cpi_bytes_per_unit).unwrap_or(0);
293 let sysvar_buf_cost = length.checked_div(cpi_bytes_per_unit).unwrap_or(0);
294
295 if invoke_context
296 .compute_meter
297 .consume_checked(
298 sysvar_base_cost
299 .saturating_add(sysvar_id_cost)
300 .saturating_add(std::cmp::max(sysvar_buf_cost, mem_op_base_cost)),
301 )
302 .is_err()
303 {
304 panic!("Exceeded compute budget");
305 }
306
307 let Ok(sysvar) = fetch(get_invoke_context().environment_config.sysvar_cache()) else {
309 return UNSUPPORTED_SYSVAR;
310 };
311
312 let Ok(expected_length) = bincode::serialized_size(&sysvar) else {
315 return UNSUPPORTED_SYSVAR;
316 };
317
318 if offset.saturating_add(length) > expected_length {
319 return UNSUPPORTED_SYSVAR;
320 }
321
322 if let Ok(serialized) = bincode::serialize(&sysvar) {
324 unsafe {
325 ptr::copy_nonoverlapping(
326 serialized[offset as usize..].as_ptr(),
327 var_addr,
328 length as usize,
329 )
330 };
331 SUCCESS
332 } else {
333 UNSUPPORTED_SYSVAR
334 }
335 }
336}
337impl solana_sysvar::program_stubs::SyscallStubs for SyscallStubs {
338 fn sol_log(&self, message: &str) {
339 let invoke_context = get_invoke_context();
340 ic_msg!(invoke_context, "Program log: {}", message);
341 }
342
343 fn sol_invoke_signed(
344 &self,
345 instruction: &Instruction,
346 account_infos: &[AccountInfo],
347 signers_seeds: &[&[&[u8]]],
348 ) -> ProgramResult {
349 let invoke_context = get_invoke_context();
350 let log_collector = invoke_context.get_log_collector();
351 let transaction_context = &invoke_context.transaction_context;
352 let instruction_context = transaction_context
353 .get_current_instruction_context()
354 .unwrap();
355 let caller = instruction_context.get_program_key().unwrap();
356
357 stable_log::program_invoke(
358 &log_collector,
359 &instruction.program_id,
360 invoke_context.get_stack_height(),
361 );
362
363 let signers = signers_seeds
364 .iter()
365 .map(|seeds| Pubkey::create_program_address(seeds, caller).unwrap())
366 .collect::<Vec<_>>();
367
368 invoke_context
369 .prepare_next_cpi_instruction(instruction.clone(), &signers)
370 .unwrap();
371
372 let transaction_context = &invoke_context.transaction_context;
374 let instruction_context = transaction_context
375 .get_current_instruction_context()
376 .unwrap();
377 let next_instruction_context = transaction_context.get_next_instruction_context().unwrap();
378 let next_instruction_accounts = next_instruction_context.instruction_accounts();
379 let mut account_indices = Vec::with_capacity(next_instruction_accounts.len());
380 for instruction_account in next_instruction_accounts.iter() {
381 let account_key = transaction_context
382 .get_key_of_account_at_index(instruction_account.index_in_transaction)
383 .unwrap();
384 let account_info_index = account_infos
385 .iter()
386 .position(|account_info| account_info.unsigned_key() == account_key)
387 .ok_or(InstructionError::MissingAccount)
388 .unwrap();
389 let account_info = &account_infos[account_info_index];
390 let index_in_caller = instruction_context
391 .get_index_of_account_in_instruction(instruction_account.index_in_transaction)
392 .unwrap();
393 let mut borrowed_account = instruction_context
394 .try_borrow_instruction_account(index_in_caller)
395 .unwrap();
396 if borrowed_account.get_lamports() != account_info.lamports() {
397 borrowed_account
398 .set_lamports(account_info.lamports())
399 .unwrap();
400 }
401 let account_info_data = account_info.try_borrow_data().unwrap();
402 match borrowed_account.can_data_be_resized(account_info_data.len()) {
404 Ok(()) => borrowed_account
405 .set_data_from_slice(&account_info_data)
406 .unwrap(),
407 Err(err) if borrowed_account.get_data() != *account_info_data => {
408 panic!("{err:?}");
409 }
410 _ => {}
411 }
412 if borrowed_account.get_owner() != account_info.owner {
414 borrowed_account
415 .set_owner(account_info.owner.as_ref())
416 .unwrap();
417 }
418 if instruction_account.is_writable() {
419 account_indices
420 .push((instruction_account.index_in_transaction, account_info_index));
421 }
422 }
423
424 let mut compute_units_consumed = 0;
425 invoke_context
426 .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default())
427 .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
428
429 let transaction_context = &invoke_context.transaction_context;
431 let instruction_context = transaction_context
432 .get_current_instruction_context()
433 .unwrap();
434 for (index_in_transaction, account_info_index) in account_indices.into_iter() {
435 let index_in_caller = instruction_context
436 .get_index_of_account_in_instruction(index_in_transaction)
437 .unwrap();
438 let borrowed_account = instruction_context
439 .try_borrow_instruction_account(index_in_caller)
440 .unwrap();
441 let account_info = &account_infos[account_info_index];
442 **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
443 if account_info.owner != borrowed_account.get_owner() {
444 #[allow(clippy::transmute_ptr_to_ptr)]
446 #[allow(mutable_transmutes)]
447 let account_info_mut =
448 unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
449 *account_info_mut = *borrowed_account.get_owner();
450 }
451
452 let new_data = borrowed_account.get_data();
453 let new_len = new_data.len();
454
455 if account_info.data_len() != new_len {
457 account_info.resize(new_len)?;
458 }
459
460 let mut data = account_info.try_borrow_mut_data()?;
462 data.clone_from_slice(new_data);
463 }
464
465 stable_log::program_success(&log_collector, &instruction.program_id);
466 Ok(())
467 }
468
469 fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
470 get_sysvar(
471 get_invoke_context()
472 .environment_config
473 .sysvar_cache()
474 .get_clock(),
475 var_addr,
476 )
477 }
478
479 fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
480 get_sysvar(
481 get_invoke_context()
482 .environment_config
483 .sysvar_cache()
484 .get_epoch_schedule(),
485 var_addr,
486 )
487 }
488
489 fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
490 get_sysvar(
491 get_invoke_context()
492 .environment_config
493 .sysvar_cache()
494 .get_epoch_rewards(),
495 var_addr,
496 )
497 }
498
499 #[allow(deprecated)]
500 fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
501 get_sysvar(
502 get_invoke_context()
503 .environment_config
504 .sysvar_cache()
505 .get_fees(),
506 var_addr,
507 )
508 }
509
510 fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
511 get_sysvar(
512 get_invoke_context()
513 .environment_config
514 .sysvar_cache()
515 .get_rent(),
516 var_addr,
517 )
518 }
519
520 fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
521 get_sysvar(
522 get_invoke_context()
523 .environment_config
524 .sysvar_cache()
525 .get_last_restart_slot(),
526 var_addr,
527 )
528 }
529
530 fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
531 let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
532 Some((*program_id, data.to_vec()))
533 }
534
535 fn sol_set_return_data(&self, data: &[u8]) {
536 let invoke_context = get_invoke_context();
537 let transaction_context = &mut invoke_context.transaction_context;
538 let instruction_context = transaction_context
539 .get_current_instruction_context()
540 .unwrap();
541 let caller = *instruction_context.get_program_key().unwrap();
542 transaction_context
543 .set_return_data(caller, data.to_vec())
544 .unwrap();
545 }
546
547 fn sol_get_stack_height(&self) -> u64 {
548 let invoke_context = get_invoke_context();
549 invoke_context.get_stack_height().try_into().unwrap()
550 }
551
552 fn sol_get_sysvar(
553 &self,
554 sysvar_id_addr: *const u8,
555 var_addr: *mut u8,
556 offset: u64,
557 length: u64,
558 ) -> u64 {
559 let sysvar_id = unsafe { &*(sysvar_id_addr as *const Pubkey) };
560
561 match *sysvar_id {
562 id if id == Clock::id() => self.fetch_and_write_sysvar::<Clock>(
563 var_addr,
564 offset,
565 length,
566 SysvarCache::get_clock,
567 ),
568 id if id == EpochRewards::id() => self.fetch_and_write_sysvar::<EpochRewards>(
569 var_addr,
570 offset,
571 length,
572 SysvarCache::get_epoch_rewards,
573 ),
574 id if id == EpochSchedule::id() => self.fetch_and_write_sysvar::<EpochSchedule>(
575 var_addr,
576 offset,
577 length,
578 SysvarCache::get_epoch_schedule,
579 ),
580 id if id == LastRestartSlot::id() => self.fetch_and_write_sysvar::<LastRestartSlot>(
581 var_addr,
582 offset,
583 length,
584 SysvarCache::get_last_restart_slot,
585 ),
586 id if id == Rent::id() => {
587 self.fetch_and_write_sysvar::<Rent>(var_addr, offset, length, SysvarCache::get_rent)
588 }
589 _ => UNSUPPORTED_SYSVAR,
590 }
591 }
592}
593
594pub fn find_file(filename: &str) -> Option<PathBuf> {
595 for dir in default_shared_object_dirs() {
596 let candidate = dir.join(filename);
597 if candidate.exists() {
598 return Some(candidate);
599 }
600 }
601 None
602}
603
604fn default_shared_object_dirs() -> Vec<PathBuf> {
605 let mut search_path = vec![];
606 if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
607 search_path.push(PathBuf::from(bpf_out_dir));
608 } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
609 search_path.push(PathBuf::from(bpf_out_dir));
610 }
611 search_path.push(PathBuf::from("tests/fixtures"));
612 if let Ok(dir) = std::env::current_dir() {
613 search_path.push(dir);
614 }
615 trace!("SBF .so search path: {search_path:?}");
616 search_path
617}
618
619pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
620 let path = path.as_ref();
621 let mut file = File::open(path)
622 .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
623
624 let mut file_data = Vec::new();
625 file.read_to_end(&mut file_data)
626 .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
627 file_data
628}
629
630pub struct ProgramTest {
631 accounts: Vec<(Pubkey, AccountSharedData)>,
632 genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
633 builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
634 compute_max_units: Option<u64>,
635 prefer_bpf: bool,
636 deactivate_feature_set: HashSet<Pubkey>,
637 transaction_account_lock_limit: Option<usize>,
638}
639
640impl Default for ProgramTest {
641 fn default() -> Self {
654 agave_logger::setup_with_default(
655 "solana_sbpf::vm=debug,solana_runtime::message_processor=debug,\
656 solana_runtime::system_instruction_processor=trace,solana_program_test=info",
657 );
658 let prefer_bpf =
659 std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
660
661 Self {
662 accounts: vec![],
663 genesis_accounts: vec![],
664 builtin_programs: vec![],
665 compute_max_units: None,
666 prefer_bpf,
667 deactivate_feature_set: HashSet::default(),
668 transaction_account_lock_limit: None,
669 }
670 }
671}
672
673impl ProgramTest {
674 pub fn new(
682 program_name: &'static str,
683 program_id: Pubkey,
684 builtin: Option<BuiltinFunctionRegisterer>,
685 ) -> Self {
686 let mut me = Self::default();
687 me.add_program(program_name, program_id, builtin);
688 me
689 }
690
691 pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
693 self.prefer_bpf = prefer_bpf;
694 }
695
696 pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
698 debug_assert!(
699 compute_max_units <= i64::MAX as u64,
700 "Compute unit limit must fit in `i64::MAX`"
701 );
702 self.compute_max_units = Some(compute_max_units);
703 }
704
705 pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
707 self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
708 }
709
710 pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
712 self.genesis_accounts
713 .push((address, AccountSharedData::from(account)));
714 }
715
716 pub fn add_account(&mut self, address: Pubkey, account: Account) {
718 self.accounts
719 .push((address, AccountSharedData::from(account)));
720 }
721
722 pub fn add_account_with_file_data(
724 &mut self,
725 address: Pubkey,
726 lamports: u64,
727 owner: Pubkey,
728 filename: &str,
729 ) {
730 self.add_account(
731 address,
732 Account {
733 lamports,
734 data: read_file(find_file(filename).unwrap_or_else(|| {
735 panic!("Unable to locate {filename}");
736 })),
737 owner,
738 executable: false,
739 rent_epoch: 0,
740 },
741 );
742 }
743
744 pub fn add_account_with_base64_data(
747 &mut self,
748 address: Pubkey,
749 lamports: u64,
750 owner: Pubkey,
751 data_base64: &str,
752 ) {
753 self.add_account(
754 address,
755 Account {
756 lamports,
757 data: BASE64_STANDARD
758 .decode(data_base64)
759 .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
760 owner,
761 executable: false,
762 rent_epoch: 0,
763 },
764 );
765 }
766
767 pub fn add_sysvar_account<S: SysvarSerialize>(&mut self, address: Pubkey, sysvar: &S) {
768 let account = create_account_shared_data_for_test(sysvar);
769 self.add_account(address, account.into());
770 }
771
772 pub fn add_upgradeable_program_to_genesis(
785 &mut self,
786 program_name: &'static str,
787 program_id: &Pubkey,
788 ) {
789 let program_file = find_file(&format!("{program_name}.so")).unwrap_or_else(|| {
790 panic!("Program file data not available for {program_name} ({program_id})")
791 });
792 let elf = read_file(program_file);
793 let program_accounts =
794 programs::bpf_loader_upgradeable_program_accounts(program_id, &elf, &Rent::default());
795 for (address, account) in program_accounts {
796 self.add_genesis_account(address, account);
797 }
798 }
799
800 pub fn add_program(
808 &mut self,
809 program_name: &'static str,
810 program_id: Pubkey,
811 builtin_function: Option<BuiltinFunctionRegisterer>,
812 ) {
813 let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
814 let data = read_file(&program_file);
815 info!(
816 "\"{}\" SBF program from {}{}",
817 program_name,
818 program_file.display(),
819 std::fs::metadata(&program_file)
820 .map(|metadata| {
821 metadata
822 .modified()
823 .map(|time| {
824 format!(
825 ", modified {}",
826 HumanTime::from(time)
827 .to_text_en(Accuracy::Precise, Tense::Past)
828 )
829 })
830 .ok()
831 })
832 .ok()
833 .flatten()
834 .unwrap_or_default()
835 );
836
837 this.add_account(
838 program_id,
839 Account {
840 lamports: Rent::default().minimum_balance(data.len()).max(1),
841 data,
842 owner: solana_sdk_ids::bpf_loader::id(),
843 executable: true,
844 rent_epoch: 0,
845 },
846 );
847 };
848
849 let warn_invalid_program_name = || {
850 let valid_program_names = default_shared_object_dirs()
851 .iter()
852 .filter_map(|dir| dir.read_dir().ok())
853 .flat_map(|read_dir| {
854 read_dir.filter_map(|entry| {
855 let path = entry.ok()?.path();
856 if !path.is_file() {
857 return None;
858 }
859 match path.extension()?.to_str()? {
860 "so" => Some(path.file_stem()?.to_os_string()),
861 _ => None,
862 }
863 })
864 })
865 .collect::<Vec<_>>();
866
867 if valid_program_names.is_empty() {
868 warn!("No SBF shared objects found.");
871 return;
872 }
873
874 warn!(
875 "Possible bogus program name. Ensure the program name ({program_name}) matches \
876 one of the following recognizable program names:",
877 );
878 for name in valid_program_names {
879 warn!(" - {}", name.to_str().unwrap());
880 }
881 };
882
883 let program_file = find_file(&format!("{program_name}.so"));
884 match (self.prefer_bpf, program_file, builtin_function) {
885 (true, Some(file), _) => add_bpf(self, file),
888
889 (false, _, Some(builtin_function)) => {
892 self.add_builtin_program(program_name, program_id, builtin_function)
893 }
894
895 (true, None, _) => {
897 warn_invalid_program_name();
898 panic!("Program file data not available for {program_name} ({program_id})");
899 }
900
901 (false, _, None) => {
903 panic!("Program processor not available for {program_name} ({program_id})");
904 }
905 }
906 }
907
908 pub fn add_builtin_program(
912 &mut self,
913 program_name: &'static str,
914 program_id: Pubkey,
915 builtin: BuiltinFunctionRegisterer,
916 ) {
917 info!("\"{program_name}\" builtin program");
918 self.builtin_programs.push((
919 program_id,
920 program_name,
921 ProgramCacheEntry::new_builtin(0, program_name.len(), builtin),
922 ));
923 }
924
925 pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
929 self.deactivate_feature_set.insert(feature_id);
930 }
931
932 fn setup_bank(
933 &mut self,
934 ) -> (
935 Arc<RwLock<BankForks>>,
936 Arc<RwLock<BlockCommitmentCache>>,
937 Hash,
938 GenesisConfigInfo,
939 ) {
940 {
941 use std::sync::Once;
942 static ONCE: Once = Once::new();
943
944 ONCE.call_once(|| {
945 solana_sysvar::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
946 });
947 }
948
949 let rent = Rent::default();
950 let fee_rate_governor = FeeRateGovernor {
951 lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
953 ..FeeRateGovernor::default()
954 };
955 let bootstrap_validator_pubkey = Pubkey::new_unique();
956 let bootstrap_validator_stake_lamports =
957 rent.minimum_balance(VoteStateV4::size_of()) + 1_000_000 * LAMPORTS_PER_SOL;
958
959 let mint_keypair = Keypair::new();
960 let voting_keypair = Keypair::new();
961
962 let mut feature_set = FeatureSet::all_enabled();
964 for deactivate_feature_pk in &self.deactivate_feature_set {
965 if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
966 feature_set.deactivate(deactivate_feature_pk);
967 } else {
968 warn!(
969 "Feature {deactivate_feature_pk:?} set for deactivation is not a known \
970 Feature public key"
971 );
972 }
973 }
974
975 let mut genesis_config = create_genesis_config_with_leader_ex(
976 1_000_000 * LAMPORTS_PER_SOL,
977 &mint_keypair.pubkey(),
978 &bootstrap_validator_pubkey,
979 &voting_keypair.pubkey(),
980 &Pubkey::new_unique(),
981 None,
982 bootstrap_validator_stake_lamports,
983 890_880,
984 fee_rate_governor,
985 rent.clone(),
986 ClusterType::Development,
987 &feature_set,
988 std::mem::take(&mut self.genesis_accounts),
989 );
990
991 let target_tick_duration = Duration::from_micros(100);
992 genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
993 debug!("Payer address: {}", mint_keypair.pubkey());
994 debug!("Genesis config: {genesis_config}");
995
996 let bank = Bank::new_from_genesis(
997 &genesis_config,
998 Arc::new(RuntimeConfig {
999 compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
1000 compute_unit_limit: max_units,
1001 ..ComputeBudget::new_with_defaults(
1002 genesis_config
1003 .accounts
1004 .contains_key(&raise_cpi_nesting_limit_to_8::id()),
1005 )
1006 }),
1007 transaction_account_lock_limit: self.transaction_account_lock_limit,
1008 ..RuntimeConfig::default()
1009 }),
1010 Vec::default(),
1011 None,
1012 ACCOUNTS_DB_CONFIG_FOR_TESTING,
1013 None,
1014 None,
1015 Arc::default(),
1016 None,
1017 None,
1018 );
1019
1020 for (program_id, account) in programs::spl_programs(&rent).iter() {
1022 bank.store_account(program_id, account);
1023 }
1024
1025 for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
1027 genesis_config.accounts.contains_key(feature_id)
1028 })
1029 .iter()
1030 {
1031 bank.store_account(program_id, account);
1032 }
1033
1034 let mut builtin_programs = Vec::new();
1036 std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
1037 for (program_id, name, builtin) in builtin_programs.into_iter() {
1038 bank.add_builtin(program_id, name, builtin);
1039 }
1040
1041 for (address, account) in self.accounts.iter() {
1042 if bank.get_account(address).is_some() {
1043 info!("Overriding account at {address}");
1044 }
1045 bank.store_account(address, account);
1046 }
1047 bank.set_capitalization_for_tests(bank.calculate_capitalization_for_tests());
1048 bank.fill_bank_with_ticks_for_tests();
1052 let bank_forks = BankForks::new_rw_arc(bank);
1053 let bank0 = bank_forks.read().unwrap().root_bank();
1054 let bank1 = Bank::new_from_parent(bank0.clone(), *bank0.leader(), bank0.slot() + 1);
1055 let bank1 = {
1056 let mut bf = bank_forks.write().unwrap();
1057 bf.insert(bank1);
1058 bf.working_bank()
1059 };
1060 debug!("Bank slot: {}", bank1.slot());
1061 let slot = bank1.slot();
1062 let last_blockhash = bank1.last_blockhash();
1063 let block_commitment_cache = Arc::new(RwLock::new(
1064 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
1065 ));
1066
1067 (
1068 bank_forks,
1069 block_commitment_cache,
1070 last_blockhash,
1071 GenesisConfigInfo {
1072 genesis_config,
1073 mint_keypair,
1074 voting_keypair,
1075 validator_pubkey: bootstrap_validator_pubkey,
1076 },
1077 )
1078 }
1079
1080 pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
1081 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1082 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1083 let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
1084 let transport = start_local_server(
1085 bank_forks.clone(),
1086 block_commitment_cache.clone(),
1087 target_tick_duration,
1088 )
1089 .await;
1090 let banks_client = start_client(transport)
1091 .await
1092 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1093
1094 tokio::spawn(async move {
1098 loop {
1099 tokio::time::sleep(target_slot_duration).await;
1100 bank_forks
1101 .read()
1102 .unwrap()
1103 .working_bank()
1104 .register_unique_recent_blockhash_for_test();
1105 }
1106 });
1107
1108 (banks_client, gci.mint_keypair, last_blockhash)
1109 }
1110
1111 pub async fn start_with_context(mut self) -> ProgramTestContext {
1116 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1117 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1118 let transport = start_local_server(
1119 bank_forks.clone(),
1120 block_commitment_cache.clone(),
1121 target_tick_duration,
1122 )
1123 .await;
1124 let banks_client = start_client(transport)
1125 .await
1126 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1127
1128 ProgramTestContext::new(
1129 bank_forks,
1130 block_commitment_cache,
1131 banks_client,
1132 last_blockhash,
1133 gci,
1134 )
1135 }
1136}
1137
1138#[async_trait]
1139pub trait ProgramTestBanksClientExt {
1140 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
1142}
1143
1144#[async_trait]
1145impl ProgramTestBanksClientExt for BanksClient {
1146 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
1147 let mut num_retries = 0;
1148 let start = Instant::now();
1149 while start.elapsed().as_secs() < 5 {
1150 let new_blockhash = self.get_latest_blockhash().await?;
1151 if new_blockhash != *blockhash {
1152 return Ok(new_blockhash);
1153 }
1154 debug!("Got same blockhash ({blockhash:?}), will retry...");
1155
1156 tokio::time::sleep(Duration::from_millis(200)).await;
1157 num_retries += 1;
1158 }
1159
1160 Err(io::Error::other(format!(
1161 "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1162 start.elapsed().as_millis(),
1163 num_retries,
1164 blockhash
1165 )))
1166 }
1167}
1168
1169struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1170
1171impl<T> Drop for DroppableTask<T> {
1172 fn drop(&mut self) {
1173 self.0.store(true, Ordering::Relaxed);
1174 trace!(
1175 "stopping task, which is currently {}",
1176 if self.1.is_finished() {
1177 "finished"
1178 } else {
1179 "running"
1180 }
1181 );
1182 }
1183}
1184
1185pub struct ProgramTestContext {
1186 pub banks_client: BanksClient,
1187 pub last_blockhash: Hash,
1188 pub payer: Keypair,
1189 genesis_config: GenesisConfig,
1190 bank_forks: Arc<RwLock<BankForks>>,
1191 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1192 _bank_task: DroppableTask<()>,
1193}
1194
1195impl ProgramTestContext {
1196 fn new(
1197 bank_forks: Arc<RwLock<BankForks>>,
1198 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1199 banks_client: BanksClient,
1200 last_blockhash: Hash,
1201 genesis_config_info: GenesisConfigInfo,
1202 ) -> Self {
1203 let running_bank_forks = bank_forks.clone();
1207 let target_tick_duration = genesis_config_info
1208 .genesis_config
1209 .poh_config
1210 .target_tick_duration;
1211 let target_slot_duration =
1212 target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1213 let exit = Arc::new(AtomicBool::new(false));
1214 let bank_task = DroppableTask(
1215 exit.clone(),
1216 tokio::spawn(async move {
1217 loop {
1218 if exit.load(Ordering::Relaxed) {
1219 break;
1220 }
1221 tokio::time::sleep(target_slot_duration).await;
1222 running_bank_forks
1223 .read()
1224 .unwrap()
1225 .working_bank()
1226 .register_unique_recent_blockhash_for_test();
1227 }
1228 }),
1229 );
1230
1231 Self {
1232 banks_client,
1233 last_blockhash,
1234 payer: genesis_config_info.mint_keypair,
1235 genesis_config: genesis_config_info.genesis_config,
1236 bank_forks,
1237 block_commitment_cache,
1238 _bank_task: bank_task,
1239 }
1240 }
1241
1242 pub fn genesis_config(&self) -> &GenesisConfig {
1243 &self.genesis_config
1244 }
1245
1246 pub fn is_active(&self, feature: &Address) -> bool {
1247 self.bank_forks
1248 .read()
1249 .unwrap()
1250 .root_bank()
1251 .feature_set
1252 .is_active(feature)
1253 }
1254
1255 pub fn increment_vote_account_credits(
1257 &mut self,
1258 vote_account_address: &Pubkey,
1259 number_of_credits: u64,
1260 ) {
1261 let bank_forks = self.bank_forks.read().unwrap();
1262 let bank = bank_forks.working_bank();
1263
1264 let mut vote_account = bank.get_account(vote_account_address).unwrap();
1266 let mut vote_state =
1267 VoteStateV4::deserialize(vote_account.data(), vote_account_address).unwrap();
1268
1269 let epoch = bank.epoch();
1270 const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
1272 for _ in 0..number_of_credits {
1273 let credits = 1;
1275
1276 if vote_state.epoch_credits.is_empty() {
1278 vote_state.epoch_credits.push((epoch, 0, 0));
1279 } else if epoch != vote_state.epoch_credits.last().unwrap().0 {
1280 let (_, credits_val, prev_credits) = *vote_state.epoch_credits.last().unwrap();
1281
1282 if credits_val != prev_credits {
1283 vote_state
1286 .epoch_credits
1287 .push((epoch, credits_val, credits_val));
1288 } else {
1289 vote_state.epoch_credits.last_mut().unwrap().0 = epoch;
1291 }
1292
1293 if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
1295 vote_state.epoch_credits.remove(0);
1296 }
1297 }
1298
1299 vote_state.epoch_credits.last_mut().unwrap().1 = vote_state
1300 .epoch_credits
1301 .last()
1302 .unwrap()
1303 .1
1304 .saturating_add(credits);
1305 }
1306 let versioned = VoteStateVersions::new_v4(vote_state);
1307 vote_account.set_state(&versioned).unwrap();
1308 bank.store_account(vote_account_address, &vote_account);
1309 }
1310
1311 pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1318 let bank_forks = self.bank_forks.read().unwrap();
1319 let bank = bank_forks.working_bank();
1320 bank.store_account(address, account);
1321 }
1322
1323 pub fn set_sysvar<T: SysvarId + SysvarSerialize>(&self, sysvar: &T) {
1330 let bank_forks = self.bank_forks.read().unwrap();
1331 let bank = bank_forks.working_bank();
1332 bank.set_sysvar_for_tests(sysvar);
1333 }
1334
1335 pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1337 let bank = self.bank_forks.read().unwrap().working_bank();
1338 let leader = *bank.leader();
1339
1340 bank.fill_bank_with_ticks_for_tests();
1343
1344 let working_slot = bank.slot();
1346 if warp_slot <= working_slot {
1347 return Err(ProgramTestError::InvalidWarpSlot);
1348 }
1349
1350 let pre_warp_slot = warp_slot - 1;
1354 let warp_bank = if pre_warp_slot == working_slot {
1355 bank.freeze();
1356 bank
1357 } else {
1358 let warped = Bank::warp_from_parent(bank, leader, pre_warp_slot);
1359 self.bank_forks
1360 .write()
1361 .unwrap()
1362 .insert(warped)
1363 .clone_without_scheduler()
1364 };
1365
1366 self.bank_forks.write().unwrap().set_root(
1367 pre_warp_slot,
1368 None, Some(pre_warp_slot),
1370 );
1371
1372 let bank_at_warp_slot = Bank::new_from_parent(warp_bank, leader, warp_slot);
1374 self.bank_forks.write().unwrap().insert(bank_at_warp_slot);
1375
1376 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1379 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1384
1385 let bank = self.bank_forks.read().unwrap().working_bank();
1386 self.last_blockhash = bank.last_blockhash();
1387 Ok(())
1388 }
1389
1390 pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1391 let warp_slot = self
1392 .genesis_config
1393 .epoch_schedule
1394 .get_first_slot_in_epoch(warp_epoch);
1395 self.warp_to_slot(warp_slot)
1396 }
1397
1398 pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1400 let bank = self.bank_forks.read().unwrap().working_bank();
1401 let leader = *bank.leader();
1402
1403 bank.fill_bank_with_ticks_for_tests();
1406 let pre_warp_slot = bank.slot();
1407
1408 self.bank_forks.write().unwrap().set_root(
1409 pre_warp_slot,
1410 None, Some(pre_warp_slot),
1412 );
1413
1414 let warp_slot = pre_warp_slot + 1;
1416 let mut warp_bank = Bank::new_from_parent(bank, leader, warp_slot);
1417
1418 warp_bank.force_reward_interval_end_for_tests();
1419 self.bank_forks.write().unwrap().insert(warp_bank);
1420
1421 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1424 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1429
1430 let bank = self.bank_forks.read().unwrap().working_bank();
1431 self.last_blockhash = bank.last_blockhash();
1432 Ok(())
1433 }
1434
1435 pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1437 let blockhash = self
1438 .banks_client
1439 .get_new_latest_blockhash(&self.last_blockhash)
1440 .await?;
1441 self.last_blockhash = blockhash;
1442 Ok(blockhash)
1443 }
1444
1445 pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1447 self.bank_forks
1448 .read()
1449 .unwrap()
1450 .working_bank()
1451 .register_hard_fork(hard_fork_slot)
1452 }
1453}