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, state_traits::StateMutWincode as _,
15 },
16 solana_account_info::AccountInfo,
17 solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING,
18 solana_address::Address,
19 solana_banks_client::start_client,
20 solana_banks_server::banks_server::start_local_server,
21 solana_clock::{Clock, Epoch, Slot},
22 solana_cluster_type::ClusterType,
23 solana_compute_budget::compute_budget::{ComputeBudget, SVMTransactionExecutionCost},
24 solana_epoch_rewards::EpochRewards,
25 solana_epoch_schedule::EpochSchedule,
26 solana_fee_calculator::{DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, FeeRateGovernor},
27 solana_genesis_config::GenesisConfig,
28 solana_hash::Hash,
29 solana_instruction::Instruction,
30 solana_instruction_error::{InstructionError, UNSUPPORTED_SYSVAR},
31 solana_keypair::Keypair,
32 solana_native_token::LAMPORTS_PER_SOL,
33 solana_poh_config::PohConfig,
34 solana_program_binaries as programs,
35 solana_program_entrypoint::{SUCCESS, deserialize},
36 solana_program_error::{ProgramError, ProgramResult},
37 solana_program_runtime::{
38 invoke_context::BuiltinFunctionRegisterer,
39 program_cache_entry::ProgramCacheEntry,
40 serialization::{SerializationResult, serialize_parameters},
41 stable_log,
42 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_sdk_ids::sysvar,
54 solana_signer::Signer,
55 solana_svm_log_collector::ic_msg,
56 solana_sysvar::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 wincode::Serialize,
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 memory_region::MemoryMapping,
86 program::BuiltinFunctionDefinition,
87 vm::{EbpfVm, EncryptedHostAddressToEbpfVm, get_runtime_environment_key},
88 },
89 solana_transaction_context::IndexOfAccount,
90};
91
92#[derive(Error, Debug, PartialEq, Eq)]
94pub enum ProgramTestError {
95 #[error("Warp slot not in the future")]
97 InvalidWarpSlot,
98}
99
100thread_local! {
101 static INVOKE_CONTEXT: RefCell<Option<usize>> = const { RefCell::new(None) };
102}
103fn set_invoke_context(new: &mut InvokeContext) {
104 INVOKE_CONTEXT.with(|invoke_context| unsafe {
105 invoke_context.replace(Some(transmute::<&mut InvokeContext, usize>(new)))
106 });
107}
108fn get_invoke_context<'a, 'b>() -> &'a mut InvokeContext<'b, 'b> {
109 let ptr = INVOKE_CONTEXT.with(|invoke_context| match *invoke_context.borrow() {
110 Some(val) => val,
111 None => panic!("Invoke context not set!"),
112 });
113 unsafe { &mut *ptr::with_exposed_provenance_mut(ptr) }
114}
115
116pub fn invoke_builtin_function(
117 builtin_function: solana_program_entrypoint::ProcessInstruction,
118 invoke_context: &mut InvokeContext,
119) -> Result<u64, Box<dyn std::error::Error>> {
120 set_invoke_context(invoke_context);
121
122 let transaction_context = &invoke_context.transaction_context;
123 let instruction_context = transaction_context.get_current_instruction_context()?;
124 let instruction_account_indices = 0..instruction_context.get_number_of_instruction_accounts();
125
126 invoke_context.consume_checked(1)?;
128
129 let log_collector = invoke_context.get_log_collector();
130 let program_id = instruction_context.get_program_key()?;
131 stable_log::program_invoke(
132 &log_collector,
133 program_id,
134 invoke_context.get_stack_height(),
135 );
136
137 let deduplicated_indices: HashSet<IndexOfAccount> = instruction_account_indices.collect();
139
140 let direct_account_pointers_in_program_input = invoke_context
141 .get_feature_set()
142 .direct_account_pointers_in_program_input;
143
144 let SerializationResult {
146 mut parameter_bytes,
147 ..
148 } = serialize_parameters(
149 &instruction_context,
150 false, false, direct_account_pointers_in_program_input,
153 )?;
154
155 let (program_id, account_infos, input) =
157 unsafe { deserialize(&mut parameter_bytes.as_slice_mut()[0] as *mut u8) };
158
159 match std::panic::catch_unwind(AssertUnwindSafe(|| {
161 builtin_function(program_id, &account_infos, input)
162 })) {
163 Ok(program_result) => {
164 program_result.map_err(|program_error| {
165 let err = InstructionError::from(u64::from(program_error));
166 stable_log::program_failure(&log_collector, program_id, &err);
167 let err: Box<dyn std::error::Error> = Box::new(err);
168 err
169 })?;
170 }
171 Err(_panic_error) => {
172 let err = InstructionError::ProgramFailedToComplete;
173 stable_log::program_failure(&log_collector, program_id, &err);
174 let err: Box<dyn std::error::Error> = Box::new(err);
175 Err(err)?;
176 }
177 };
178
179 stable_log::program_success(&log_collector, program_id);
180
181 let account_info_map: HashMap<_, _> = account_infos.into_iter().map(|a| (a.key, a)).collect();
183
184 let transaction_context = &invoke_context.transaction_context;
187 let instruction_context = transaction_context.get_current_instruction_context()?;
188
189 for i in deduplicated_indices.into_iter() {
191 let mut borrowed_account = instruction_context.try_borrow_instruction_account(i)?;
192 if borrowed_account.is_writable()
193 && let Some(account_info) = account_info_map.get(borrowed_account.get_key())
194 {
195 if borrowed_account.get_lamports() != account_info.lamports() {
196 borrowed_account.set_lamports(account_info.lamports())?;
197 }
198
199 if borrowed_account
200 .can_data_be_resized(account_info.data_len())
201 .is_ok()
202 {
203 borrowed_account.set_data_from_slice(&account_info.data.borrow())?;
204 }
205 if borrowed_account.get_owner() != account_info.owner {
206 borrowed_account.set_owner(account_info.owner.as_ref())?;
207 }
208 }
209 }
210
211 Ok(0)
212}
213
214#[macro_export]
217macro_rules! processor {
218 ($builtin_function:expr) => {{
219 struct Converter;
220 impl $crate::BuiltinFunctionDefinition<$crate::InvokeContext<'_, '_>> for Converter {
221 type Error = Box<dyn std::error::Error>;
222 fn rust(
223 _: &mut $crate::InvokeContext<'_, '_>,
224 _: u64,
225 _: u64,
226 _: u64,
227 _: u64,
228 _: u64,
229 ) -> Result<u64, Box<dyn std::error::Error>> {
230 unreachable!()
231 }
232 fn vm(
233 mut vm: $crate::EncryptedHostAddressToEbpfVm<$crate::InvokeContext>,
234 _: u64,
235 _: u64,
236 _: u64,
237 _: u64,
238 _: u64,
239 ) {
240 unsafe {
241 vm.with_vm(|vm| {
242 vm.program_result =
243 $crate::invoke_builtin_function($builtin_function, vm.context())
244 .map_err(|err| $crate::EbpfError::SyscallError(err))
245 .into();
246 });
247 }
248 }
249 };
250 Some(<Converter as $crate::BuiltinFunctionDefinition<_>>::register)
251 }};
252}
253
254fn get_sysvar<T: Clone>(
255 sysvar: Result<Arc<T>, InstructionError>,
256 var_addr: *mut u8,
257 sysvar_size: usize,
258) -> u64 {
259 let invoke_context = get_invoke_context();
260 if invoke_context
261 .consume_checked(invoke_context.get_execution_cost().sysvar_base_cost + sysvar_size as u64)
262 .is_err()
263 {
264 panic!("Exceeded compute budget");
265 }
266
267 match sysvar {
268 Ok(sysvar_data) => unsafe {
269 *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
270 SUCCESS
271 },
272 Err(_) => UNSUPPORTED_SYSVAR,
273 }
274}
275
276pub fn sol_get_clock_sysvar(var_addr: *mut u8) -> u64 {
278 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_clock_sysvar(
279 &SyscallStubs {},
280 var_addr,
281 )
282}
283
284pub fn sol_get_epoch_schedule_sysvar(var_addr: *mut u8) -> u64 {
286 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_epoch_schedule_sysvar(
287 &SyscallStubs {},
288 var_addr,
289 )
290}
291
292pub fn sol_get_epoch_rewards_sysvar(var_addr: *mut u8) -> u64 {
294 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_epoch_rewards_sysvar(
295 &SyscallStubs {},
296 var_addr,
297 )
298}
299
300pub fn sol_get_fees_sysvar(var_addr: *mut u8) -> u64 {
302 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_fees_sysvar(
303 &SyscallStubs {},
304 var_addr,
305 )
306}
307
308pub fn sol_get_rent_sysvar(var_addr: *mut u8) -> u64 {
310 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_rent_sysvar(
311 &SyscallStubs {},
312 var_addr,
313 )
314}
315
316pub fn sol_get_last_restart_slot(var_addr: *mut u8) -> u64 {
318 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_last_restart_slot(
319 &SyscallStubs {},
320 var_addr,
321 )
322}
323
324struct SyscallStubs {}
325
326impl SyscallStubs {
327 fn fetch_and_write_sysvar<T: Serialize<Src = T>>(
328 &self,
329 var_addr: *mut u8,
330 offset: u64,
331 length: u64,
332 fetch: impl FnOnce(&SysvarCache) -> Result<Arc<T>, InstructionError>,
333 ) -> u64 {
334 let invoke_context = get_invoke_context();
336 let SVMTransactionExecutionCost {
337 sysvar_base_cost,
338 cpi_bytes_per_unit,
339 mem_op_base_cost,
340 ..
341 } = *invoke_context.get_execution_cost();
342
343 let sysvar_id_cost = 32_u64.checked_div(cpi_bytes_per_unit).unwrap_or(0);
344 let sysvar_buf_cost = length.checked_div(cpi_bytes_per_unit).unwrap_or(0);
345
346 if invoke_context
347 .consume_checked(
348 sysvar_base_cost
349 .saturating_add(sysvar_id_cost)
350 .saturating_add(std::cmp::max(sysvar_buf_cost, mem_op_base_cost)),
351 )
352 .is_err()
353 {
354 panic!("Exceeded compute budget");
355 }
356
357 let Ok(sysvar) = fetch(get_invoke_context().environment_config.sysvar_cache()) else {
359 return UNSUPPORTED_SYSVAR;
360 };
361
362 let Ok(expected_length) = wincode::serialized_size(sysvar.as_ref()) else {
365 return UNSUPPORTED_SYSVAR;
366 };
367
368 if offset.saturating_add(length) > expected_length {
369 return UNSUPPORTED_SYSVAR;
370 }
371
372 if let Ok(serialized) = wincode::serialize(sysvar.as_ref()) {
374 unsafe {
375 ptr::copy_nonoverlapping(
376 serialized[offset as usize..].as_ptr(),
377 var_addr,
378 length as usize,
379 )
380 };
381 SUCCESS
382 } else {
383 UNSUPPORTED_SYSVAR
384 }
385 }
386}
387impl solana_sysvar::program_stubs::SyscallStubs for SyscallStubs {
388 fn sol_log(&self, message: &str) {
389 let invoke_context = get_invoke_context();
390 ic_msg!(invoke_context, "Program log: {}", message);
391 }
392
393 fn sol_invoke_signed(
394 &self,
395 instruction: &Instruction,
396 account_infos: &[AccountInfo],
397 signers_seeds: &[&[&[u8]]],
398 ) -> ProgramResult {
399 let invoke_context = get_invoke_context();
400 let log_collector = invoke_context.get_log_collector();
401
402 stable_log::program_invoke(
403 &log_collector,
404 &instruction.program_id,
405 invoke_context.get_stack_height(),
406 );
407
408 let transaction_context = &invoke_context.transaction_context;
413 let instruction_context = transaction_context
414 .get_current_instruction_context()
415 .unwrap();
416 let mut account_indices = Vec::with_capacity(instruction.accounts.len());
417 for account_meta in instruction.accounts.iter() {
418 let index_in_transaction = transaction_context
419 .find_index_of_account(&account_meta.pubkey)
420 .ok_or(InstructionError::MissingAccount)
421 .unwrap();
422 let account_info_index = account_infos
423 .iter()
424 .position(|account_info| account_info.unsigned_key() == &account_meta.pubkey)
425 .ok_or(InstructionError::MissingAccount)
426 .unwrap();
427 let account_info = &account_infos[account_info_index];
428 let index_in_caller = instruction_context
429 .get_index_of_account_in_instruction(index_in_transaction)
430 .unwrap();
431 let mut borrowed_account = instruction_context
432 .try_borrow_instruction_account(index_in_caller)
433 .unwrap();
434 if borrowed_account.get_lamports() != account_info.lamports() {
435 borrowed_account
436 .set_lamports(account_info.lamports())
437 .unwrap();
438 }
439 let account_info_data = account_info.try_borrow_data().unwrap();
440 match borrowed_account.can_data_be_resized(account_info_data.len()) {
442 Ok(()) => borrowed_account
443 .set_data_from_slice(&account_info_data)
444 .unwrap(),
445 Err(err) if borrowed_account.get_data() != *account_info_data => {
446 panic!("{err:?}");
447 }
448 _ => {}
449 }
450 if borrowed_account.get_owner() != account_info.owner {
452 borrowed_account
453 .set_owner(account_info.owner.as_ref())
454 .unwrap();
455 }
456 if account_meta.is_writable {
457 account_indices.push((index_in_transaction, account_info_index));
458 }
459 }
460
461 invoke_context
462 .native_invoke_signed(instruction.clone(), signers_seeds)
463 .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
464
465 let transaction_context = &invoke_context.transaction_context;
467 let instruction_context = transaction_context
468 .get_current_instruction_context()
469 .unwrap();
470 for (index_in_transaction, account_info_index) in account_indices.into_iter() {
471 let index_in_caller = instruction_context
472 .get_index_of_account_in_instruction(index_in_transaction)
473 .unwrap();
474 let borrowed_account = instruction_context
475 .try_borrow_instruction_account(index_in_caller)
476 .unwrap();
477 let account_info = &account_infos[account_info_index];
478 **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
479 if account_info.owner != borrowed_account.get_owner() {
480 #[allow(clippy::transmute_ptr_to_ptr)]
482 #[allow(mutable_transmutes)]
483 let account_info_mut =
484 unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
485 *account_info_mut = *borrowed_account.get_owner();
486 }
487
488 let new_data = borrowed_account.get_data();
489 let new_len = new_data.len();
490
491 if account_info.data_len() != new_len {
493 account_info.resize(new_len)?;
494 }
495
496 let mut data = account_info.try_borrow_mut_data()?;
498 data.clone_from_slice(new_data);
499 }
500
501 stable_log::program_success(&log_collector, &instruction.program_id);
502 Ok(())
503 }
504
505 fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
506 get_sysvar(
507 get_invoke_context()
508 .environment_config
509 .sysvar_cache()
510 .get_clock(),
511 var_addr,
512 solana_clock::SIZE,
513 )
514 }
515
516 fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
517 get_sysvar(
518 get_invoke_context()
519 .environment_config
520 .sysvar_cache()
521 .get_epoch_schedule(),
522 var_addr,
523 solana_epoch_schedule::SIZE,
524 )
525 }
526
527 fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
528 get_sysvar(
529 get_invoke_context()
530 .environment_config
531 .sysvar_cache()
532 .get_epoch_rewards(),
533 var_addr,
534 solana_epoch_rewards::SIZE,
535 )
536 }
537
538 #[allow(deprecated)]
539 fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
540 get_sysvar(
541 get_invoke_context()
542 .environment_config
543 .sysvar_cache()
544 .get_fees(),
545 var_addr,
546 solana_sysvar::fees::SIZE,
547 )
548 }
549
550 fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
551 get_sysvar(
552 get_invoke_context()
553 .environment_config
554 .sysvar_cache()
555 .get_rent(),
556 var_addr,
557 solana_rent::SIZE,
558 )
559 }
560
561 fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
562 get_sysvar(
563 get_invoke_context()
564 .environment_config
565 .sysvar_cache()
566 .get_last_restart_slot(),
567 var_addr,
568 solana_sysvar::last_restart_slot::SIZE,
569 )
570 }
571
572 fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
573 let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
574 Some((*program_id, data.to_vec()))
575 }
576
577 fn sol_set_return_data(&self, data: &[u8]) {
578 let invoke_context = get_invoke_context();
579 let transaction_context = &mut invoke_context.transaction_context;
580 let instruction_context = transaction_context
581 .get_current_instruction_context()
582 .unwrap();
583 let caller = *instruction_context.get_program_key().unwrap();
584 transaction_context
585 .set_return_data(caller, data.to_vec())
586 .unwrap();
587 }
588
589 fn sol_get_stack_height(&self) -> u64 {
590 let invoke_context = get_invoke_context();
591 invoke_context.get_stack_height().try_into().unwrap()
592 }
593
594 fn sol_get_sysvar(
595 &self,
596 sysvar_id_addr: *const u8,
597 var_addr: *mut u8,
598 offset: u64,
599 length: u64,
600 ) -> u64 {
601 let sysvar_id = unsafe { &*(sysvar_id_addr as *const Pubkey) };
602
603 match *sysvar_id {
604 id if id == Clock::id() => self.fetch_and_write_sysvar::<Clock>(
605 var_addr,
606 offset,
607 length,
608 SysvarCache::get_clock,
609 ),
610 id if id == EpochRewards::id() => self.fetch_and_write_sysvar::<EpochRewards>(
611 var_addr,
612 offset,
613 length,
614 SysvarCache::get_epoch_rewards,
615 ),
616 id if id == EpochSchedule::id() => self.fetch_and_write_sysvar::<EpochSchedule>(
617 var_addr,
618 offset,
619 length,
620 SysvarCache::get_epoch_schedule,
621 ),
622 id if id == LastRestartSlot::id() => self.fetch_and_write_sysvar::<LastRestartSlot>(
623 var_addr,
624 offset,
625 length,
626 SysvarCache::get_last_restart_slot,
627 ),
628 id if id == Rent::id() => {
629 self.fetch_and_write_sysvar::<Rent>(var_addr, offset, length, SysvarCache::get_rent)
630 }
631 _ => UNSUPPORTED_SYSVAR,
632 }
633 }
634}
635
636#[allow(deprecated)]
637fn canonical_sysvar_data_len(sysvar_id: &Pubkey) -> Option<usize> {
638 match *sysvar_id {
639 sysvar::clock::ID => Some(solana_clock::SIZE),
640 sysvar::epoch_rewards::ID => Some(solana_epoch_rewards::SIZE),
641 sysvar::epoch_schedule::ID => Some(solana_epoch_schedule::SIZE),
642 sysvar::fees::ID => Some(solana_sysvar::fees::SIZE),
643 sysvar::last_restart_slot::ID => Some(solana_sysvar::last_restart_slot::SIZE),
644 sysvar::recent_blockhashes::ID => Some(solana_sysvar::recent_blockhashes::SIZE),
645 sysvar::rent::ID => Some(solana_rent::SIZE),
646 sysvar::rewards::ID => Some(solana_sysvar::rewards::SIZE),
647 sysvar::slot_hashes::ID => Some(solana_sysvar::slot_hashes::SIZE),
648 sysvar::slot_history::ID => Some(solana_sysvar::slot_history::SIZE),
649 sysvar::stake_history::ID => Some(solana_sysvar::stake_history::SIZE),
650 _ => None,
651 }
652}
653
654fn required_sysvar_data_len(sysvar_id: &Pubkey, serialized_len: usize) -> usize {
658 canonical_sysvar_data_len(sysvar_id)
659 .unwrap_or(serialized_len)
660 .max(serialized_len)
661}
662
663fn create_sysvar_account<T: SysvarId + Serialize<Src = T>>(sysvar: &T) -> Account {
664 let serialized_len = wincode::serialized_size(sysvar).unwrap() as usize;
665 let data_len = required_sysvar_data_len(&T::id(), serialized_len);
666 let mut account = Account::new(1, data_len, &sysvar::id());
667 wincode::serialize_into(account.data.as_mut_slice(), sysvar).unwrap();
668 account
669}
670
671pub fn find_file(filename: &str) -> Option<PathBuf> {
672 for dir in default_shared_object_dirs() {
673 let candidate = dir.join(filename);
674 if candidate.exists() {
675 return Some(candidate);
676 }
677 }
678 None
679}
680
681fn default_shared_object_dirs() -> Vec<PathBuf> {
682 let mut search_path = vec![];
683 if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
684 search_path.push(PathBuf::from(bpf_out_dir));
685 } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
686 search_path.push(PathBuf::from(bpf_out_dir));
687 }
688 search_path.push(PathBuf::from("tests/fixtures"));
689 if let Ok(dir) = std::env::current_dir() {
690 search_path.push(dir);
691 }
692 trace!("SBF .so search path: {search_path:?}");
693 search_path
694}
695
696pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
697 let path = path.as_ref();
698 let mut file = File::open(path)
699 .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
700
701 let mut file_data = Vec::new();
702 file.read_to_end(&mut file_data)
703 .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
704 file_data
705}
706
707pub struct ProgramTest {
708 accounts: Vec<(Pubkey, AccountSharedData)>,
709 genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
710 builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
711 compute_max_units: Option<u64>,
712 prefer_bpf: bool,
713 deactivate_feature_set: HashSet<Pubkey>,
714 transaction_account_lock_limit: Option<usize>,
715}
716
717impl Default for ProgramTest {
718 fn default() -> Self {
731 agave_logger::setup_with_default(
732 "solana_sbpf::vm=debug,solana_runtime::message_processor=debug,\
733 solana_runtime::system_instruction_processor=trace,solana_program_test=info",
734 );
735 let prefer_bpf =
736 std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
737
738 Self {
739 accounts: vec![],
740 genesis_accounts: vec![],
741 builtin_programs: vec![],
742 compute_max_units: None,
743 prefer_bpf,
744 deactivate_feature_set: HashSet::default(),
745 transaction_account_lock_limit: None,
746 }
747 }
748}
749
750impl ProgramTest {
751 pub fn new(
759 program_name: &'static str,
760 program_id: Pubkey,
761 builtin: Option<BuiltinFunctionRegisterer>,
762 ) -> Self {
763 let mut me = Self::default();
764 me.add_program(program_name, program_id, builtin);
765 me
766 }
767
768 pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
770 self.prefer_bpf = prefer_bpf;
771 }
772
773 pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
775 debug_assert!(
776 compute_max_units <= i64::MAX as u64,
777 "Compute unit limit must fit in `i64::MAX`"
778 );
779 self.compute_max_units = Some(compute_max_units);
780 }
781
782 pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
784 self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
785 }
786
787 pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
789 self.genesis_accounts
790 .push((address, AccountSharedData::from(account)));
791 }
792
793 pub fn add_account(&mut self, address: Pubkey, account: Account) {
795 self.accounts
796 .push((address, AccountSharedData::from(account)));
797 }
798
799 pub fn add_account_with_file_data(
801 &mut self,
802 address: Pubkey,
803 lamports: u64,
804 owner: Pubkey,
805 filename: &str,
806 ) {
807 self.add_account(
808 address,
809 Account {
810 lamports,
811 data: read_file(find_file(filename).unwrap_or_else(|| {
812 panic!("Unable to locate {filename}");
813 })),
814 owner,
815 executable: false,
816 rent_epoch: 0,
817 },
818 );
819 }
820
821 pub fn add_account_with_base64_data(
824 &mut self,
825 address: Pubkey,
826 lamports: u64,
827 owner: Pubkey,
828 data_base64: &str,
829 ) {
830 self.add_account(
831 address,
832 Account {
833 lamports,
834 data: BASE64_STANDARD
835 .decode(data_base64)
836 .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
837 owner,
838 executable: false,
839 rent_epoch: 0,
840 },
841 );
842 }
843
844 pub fn add_sysvar_account<S: SysvarId + Serialize<Src = S>>(
845 &mut self,
846 address: Pubkey,
847 sysvar: &S,
848 ) {
849 self.add_account(address, create_sysvar_account(sysvar));
850 }
851
852 pub fn add_upgradeable_program_to_genesis(
865 &mut self,
866 program_name: &'static str,
867 program_id: &Pubkey,
868 ) {
869 let program_file = find_file(&format!("{program_name}.so")).unwrap_or_else(|| {
870 panic!("Program file data not available for {program_name} ({program_id})")
871 });
872 let elf = read_file(program_file);
873 let program_accounts = programs::bpf_loader_upgradeable_program_accounts(
874 program_id,
875 &elf,
876 &Pubkey::default(),
877 &Rent::default(),
878 );
879 for (address, account) in program_accounts {
880 self.add_genesis_account(address, account);
881 }
882 }
883
884 pub fn add_program(
892 &mut self,
893 program_name: &'static str,
894 program_id: Pubkey,
895 builtin_function: Option<BuiltinFunctionRegisterer>,
896 ) {
897 let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
898 let data = read_file(&program_file);
899 info!(
900 "\"{}\" SBF program from {}{}",
901 program_name,
902 program_file.display(),
903 std::fs::metadata(&program_file)
904 .map(|metadata| {
905 metadata
906 .modified()
907 .map(|time| {
908 format!(
909 ", modified {}",
910 HumanTime::from(time)
911 .to_text_en(Accuracy::Precise, Tense::Past)
912 )
913 })
914 .ok()
915 })
916 .ok()
917 .flatten()
918 .unwrap_or_default()
919 );
920
921 this.add_account(
922 program_id,
923 Account {
924 lamports: Rent::default().minimum_balance(data.len()).max(1),
925 data,
926 owner: solana_sdk_ids::bpf_loader::id(),
927 executable: true,
928 rent_epoch: 0,
929 },
930 );
931 };
932
933 let warn_invalid_program_name = || {
934 let valid_program_names = default_shared_object_dirs()
935 .iter()
936 .filter_map(|dir| dir.read_dir().ok())
937 .flat_map(|read_dir| {
938 read_dir.filter_map(|entry| {
939 let path = entry.ok()?.path();
940 if !path.is_file() {
941 return None;
942 }
943 match path.extension()?.to_str()? {
944 "so" => Some(path.file_stem()?.to_os_string()),
945 _ => None,
946 }
947 })
948 })
949 .collect::<Vec<_>>();
950
951 if valid_program_names.is_empty() {
952 warn!("No SBF shared objects found.");
955 return;
956 }
957
958 warn!(
959 "Possible bogus program name. Ensure the program name ({program_name}) matches \
960 one of the following recognizable program names:",
961 );
962 for name in valid_program_names {
963 warn!(" - {}", name.to_str().unwrap());
964 }
965 };
966
967 let program_file = find_file(&format!("{program_name}.so"));
968 match (self.prefer_bpf, program_file, builtin_function) {
969 (true, Some(file), _) => add_bpf(self, file),
972
973 (false, _, Some(builtin_function)) => {
976 self.add_builtin_program(program_name, program_id, builtin_function)
977 }
978
979 (true, None, _) => {
981 warn_invalid_program_name();
982 panic!("Program file data not available for {program_name} ({program_id})");
983 }
984
985 (false, _, None) => {
987 panic!("Program processor not available for {program_name} ({program_id})");
988 }
989 }
990 }
991
992 pub fn add_builtin_program(
996 &mut self,
997 program_name: &'static str,
998 program_id: Pubkey,
999 builtin: BuiltinFunctionRegisterer,
1000 ) {
1001 info!("\"{program_name}\" builtin program");
1002 self.builtin_programs.push((
1003 program_id,
1004 program_name,
1005 ProgramCacheEntry::new_builtin(builtin),
1006 ));
1007 }
1008
1009 pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
1013 self.deactivate_feature_set.insert(feature_id);
1014 }
1015
1016 fn setup_bank(
1017 &mut self,
1018 ) -> (
1019 Arc<RwLock<BankForks>>,
1020 Arc<RwLock<BlockCommitmentCache>>,
1021 Hash,
1022 GenesisConfigInfo,
1023 ) {
1024 {
1025 use std::sync::Once;
1026 static ONCE: Once = Once::new();
1027
1028 ONCE.call_once(|| {
1029 solana_sysvar::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
1030 });
1031 }
1032
1033 let rent = Rent::default();
1034 let fee_rate_governor = FeeRateGovernor {
1035 lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
1037 ..FeeRateGovernor::default()
1038 };
1039 let bootstrap_validator_pubkey = Pubkey::new_unique();
1040 let bootstrap_validator_stake_lamports =
1041 rent.minimum_balance(VoteStateV4::size_of()) + 1_000_000 * LAMPORTS_PER_SOL;
1042
1043 let mint_keypair = Keypair::new();
1044 let voting_keypair = Keypair::new();
1045
1046 let mut feature_set = FeatureSet::all_enabled();
1048 for deactivate_feature_pk in &self.deactivate_feature_set {
1049 if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
1050 feature_set.deactivate(deactivate_feature_pk);
1051 } else {
1052 warn!(
1053 "Feature {deactivate_feature_pk:?} set for deactivation is not a known \
1054 Feature public key"
1055 );
1056 }
1057 }
1058
1059 let mut genesis_config = create_genesis_config_with_leader_ex(
1060 1_000_000 * LAMPORTS_PER_SOL,
1061 &mint_keypair.pubkey(),
1062 &bootstrap_validator_pubkey,
1063 &voting_keypair.pubkey(),
1064 &Pubkey::new_unique(),
1065 None,
1066 bootstrap_validator_stake_lamports,
1067 890_880,
1068 fee_rate_governor,
1069 rent.clone(),
1070 ClusterType::Development,
1071 &feature_set,
1072 std::mem::take(&mut self.genesis_accounts),
1073 );
1074
1075 let target_tick_duration = Duration::from_micros(100);
1076 genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
1077 debug!("Payer address: {}", mint_keypair.pubkey());
1078 debug!("Genesis config: {genesis_config}");
1079
1080 let bank = Bank::new_from_genesis(
1081 &genesis_config,
1082 Arc::new(RuntimeConfig {
1083 compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
1084 compute_unit_limit: max_units,
1085 ..ComputeBudget::new_with_defaults(
1086 genesis_config
1087 .accounts
1088 .contains_key(&raise_cpi_nesting_limit_to_8::id()),
1089 )
1090 }),
1091 transaction_account_lock_limit: self.transaction_account_lock_limit,
1092 ..RuntimeConfig::default()
1093 }),
1094 Vec::default(),
1095 None,
1096 ACCOUNTS_DB_CONFIG_FOR_TESTING,
1097 None,
1098 None,
1099 Arc::default(),
1100 None,
1101 None,
1102 );
1103
1104 for (program_id, account) in programs::spl_programs(&rent).iter() {
1106 bank.store_account(program_id, account);
1107 }
1108
1109 for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
1111 genesis_config.accounts.contains_key(feature_id)
1112 })
1113 .iter()
1114 {
1115 bank.store_account(program_id, account);
1116 }
1117
1118 let mut builtin_programs = Vec::new();
1120 std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
1121 for (program_id, name, builtin) in builtin_programs.into_iter() {
1122 bank.add_builtin(program_id, name, builtin);
1123 }
1124
1125 for (address, account) in self.accounts.iter() {
1126 if bank.get_account(address).is_some() {
1127 info!("Overriding account at {address}");
1128 }
1129 bank.store_account(address, account);
1130 }
1131 bank.set_capitalization_for_tests(bank.calculate_capitalization_for_tests());
1132 bank.fill_bank_with_ticks_for_tests();
1136 let bank_forks = BankForks::new_rw_arc(bank);
1137 let bank0 = bank_forks.read().unwrap().root_bank();
1138 let bank1 = Bank::new_from_parent(bank0.clone(), *bank0.leader(), bank0.slot() + 1);
1139 let bank1 = {
1140 let mut bf = bank_forks.write().unwrap();
1141 bf.insert(bank1);
1142 bf.working_bank()
1143 };
1144 debug!("Bank slot: {}", bank1.slot());
1145 let slot = bank1.slot();
1146 let last_blockhash = bank1.last_blockhash();
1147 let block_commitment_cache = Arc::new(RwLock::new(
1148 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
1149 ));
1150
1151 (
1152 bank_forks,
1153 block_commitment_cache,
1154 last_blockhash,
1155 GenesisConfigInfo {
1156 genesis_config,
1157 mint_keypair,
1158 voting_keypair,
1159 validator_pubkey: bootstrap_validator_pubkey,
1160 },
1161 )
1162 }
1163
1164 pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
1165 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1166 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1167 let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
1168 let transport = start_local_server(
1169 bank_forks.clone(),
1170 block_commitment_cache.clone(),
1171 target_tick_duration,
1172 )
1173 .await;
1174 let banks_client = start_client(transport)
1175 .await
1176 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1177
1178 tokio::spawn(async move {
1182 loop {
1183 tokio::time::sleep(target_slot_duration).await;
1184 bank_forks
1185 .read()
1186 .unwrap()
1187 .working_bank()
1188 .register_unique_recent_blockhash_for_test();
1189 }
1190 });
1191
1192 (banks_client, gci.mint_keypair, last_blockhash)
1193 }
1194
1195 pub async fn start_with_context(mut self) -> ProgramTestContext {
1200 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1201 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1202 let transport = start_local_server(
1203 bank_forks.clone(),
1204 block_commitment_cache.clone(),
1205 target_tick_duration,
1206 )
1207 .await;
1208 let banks_client = start_client(transport)
1209 .await
1210 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1211
1212 ProgramTestContext::new(
1213 bank_forks,
1214 block_commitment_cache,
1215 banks_client,
1216 last_blockhash,
1217 gci,
1218 )
1219 }
1220}
1221
1222#[async_trait]
1223pub trait ProgramTestBanksClientExt {
1224 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
1226}
1227
1228#[async_trait]
1229impl ProgramTestBanksClientExt for BanksClient {
1230 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
1231 let mut num_retries = 0;
1232 let start = Instant::now();
1233 while start.elapsed().as_secs() < 5 {
1234 let new_blockhash = self.get_latest_blockhash().await?;
1235 if new_blockhash != *blockhash {
1236 return Ok(new_blockhash);
1237 }
1238 debug!("Got same blockhash ({blockhash:?}), will retry...");
1239
1240 tokio::time::sleep(Duration::from_millis(200)).await;
1241 num_retries += 1;
1242 }
1243
1244 Err(io::Error::other(format!(
1245 "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1246 start.elapsed().as_millis(),
1247 num_retries,
1248 blockhash
1249 )))
1250 }
1251}
1252
1253struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1254
1255impl<T> Drop for DroppableTask<T> {
1256 fn drop(&mut self) {
1257 self.0.store(true, Ordering::Relaxed);
1258 trace!(
1259 "stopping task, which is currently {}",
1260 if self.1.is_finished() {
1261 "finished"
1262 } else {
1263 "running"
1264 }
1265 );
1266 }
1267}
1268
1269pub struct ProgramTestContext {
1270 pub banks_client: BanksClient,
1271 pub last_blockhash: Hash,
1272 pub payer: Keypair,
1273 genesis_config: GenesisConfig,
1274 bank_forks: Arc<RwLock<BankForks>>,
1275 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1276 _bank_task: DroppableTask<()>,
1277}
1278
1279impl ProgramTestContext {
1280 fn new(
1281 bank_forks: Arc<RwLock<BankForks>>,
1282 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1283 banks_client: BanksClient,
1284 last_blockhash: Hash,
1285 genesis_config_info: GenesisConfigInfo,
1286 ) -> Self {
1287 let running_bank_forks = bank_forks.clone();
1291 let target_tick_duration = genesis_config_info
1292 .genesis_config
1293 .poh_config
1294 .target_tick_duration;
1295 let target_slot_duration =
1296 target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1297 let exit = Arc::new(AtomicBool::new(false));
1298 let bank_task = DroppableTask(
1299 exit.clone(),
1300 tokio::spawn(async move {
1301 loop {
1302 if exit.load(Ordering::Relaxed) {
1303 break;
1304 }
1305 tokio::time::sleep(target_slot_duration).await;
1306 running_bank_forks
1307 .read()
1308 .unwrap()
1309 .working_bank()
1310 .register_unique_recent_blockhash_for_test();
1311 }
1312 }),
1313 );
1314
1315 Self {
1316 banks_client,
1317 last_blockhash,
1318 payer: genesis_config_info.mint_keypair,
1319 genesis_config: genesis_config_info.genesis_config,
1320 bank_forks,
1321 block_commitment_cache,
1322 _bank_task: bank_task,
1323 }
1324 }
1325
1326 pub fn genesis_config(&self) -> &GenesisConfig {
1327 &self.genesis_config
1328 }
1329
1330 pub fn is_active(&self, feature: &Address) -> bool {
1331 self.bank_forks
1332 .read()
1333 .unwrap()
1334 .root_bank()
1335 .feature_set
1336 .is_active(feature)
1337 }
1338
1339 pub fn increment_vote_account_credits(
1341 &mut self,
1342 vote_account_address: &Pubkey,
1343 number_of_credits: u64,
1344 ) {
1345 let bank_forks = self.bank_forks.read().unwrap();
1346 let bank = bank_forks.working_bank();
1347
1348 let mut vote_account = bank.get_account(vote_account_address).unwrap();
1350 let mut vote_state =
1351 VoteStateV4::deserialize(vote_account.data(), vote_account_address).unwrap();
1352
1353 let epoch = bank.epoch();
1354 const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
1356 for _ in 0..number_of_credits {
1357 let credits = 1;
1359
1360 if vote_state.epoch_credits.is_empty() {
1362 vote_state.epoch_credits.push((epoch, 0, 0));
1363 } else if epoch != vote_state.epoch_credits.last().unwrap().0 {
1364 let (_, credits_val, prev_credits) = *vote_state.epoch_credits.last().unwrap();
1365
1366 if credits_val != prev_credits {
1367 vote_state
1370 .epoch_credits
1371 .push((epoch, credits_val, credits_val));
1372 } else {
1373 vote_state.epoch_credits.last_mut().unwrap().0 = epoch;
1375 }
1376
1377 if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
1379 vote_state.epoch_credits.remove(0);
1380 }
1381 }
1382
1383 vote_state.epoch_credits.last_mut().unwrap().1 = vote_state
1384 .epoch_credits
1385 .last()
1386 .unwrap()
1387 .1
1388 .saturating_add(credits);
1389 }
1390 let versioned = VoteStateVersions::new_v4(vote_state);
1391 vote_account.set_state(&versioned).unwrap();
1392 bank.store_account(vote_account_address, &vote_account);
1393 }
1394
1395 pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1402 let bank_forks = self.bank_forks.read().unwrap();
1403 let bank = bank_forks.working_bank();
1404 bank.store_account(address, account);
1405 }
1406
1407 pub fn set_sysvar<T>(&self, sysvar: &T)
1414 where
1415 T: wincode::SchemaWrite<solana_account::WincodeConfig, Src = T>
1416 + for<'de> wincode::SchemaRead<'de, solana_account::WincodeConfig, Dst = T>
1417 + SysvarId,
1418 {
1419 let bank_forks = self.bank_forks.read().unwrap();
1420 let bank = bank_forks.working_bank();
1421 bank.set_sysvar_for_tests(sysvar);
1422 }
1423
1424 pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1426 let bank = self.bank_forks.read().unwrap().working_bank();
1427 let leader = *bank.leader();
1428
1429 bank.fill_bank_with_ticks_for_tests();
1432
1433 let working_slot = bank.slot();
1435 if warp_slot <= working_slot {
1436 return Err(ProgramTestError::InvalidWarpSlot);
1437 }
1438
1439 let pre_warp_slot = warp_slot - 1;
1443 let warp_bank = if pre_warp_slot == working_slot {
1444 bank.freeze();
1445 bank
1446 } else {
1447 let warped = Bank::warp_from_parent(bank, leader, pre_warp_slot);
1448 self.bank_forks
1449 .write()
1450 .unwrap()
1451 .insert(warped)
1452 .clone_without_scheduler()
1453 };
1454
1455 self.bank_forks.write().unwrap().set_root(
1456 pre_warp_slot,
1457 None, Some(pre_warp_slot),
1459 );
1460
1461 let bank_at_warp_slot = Bank::new_from_parent(warp_bank, leader, warp_slot);
1463 self.bank_forks.write().unwrap().insert(bank_at_warp_slot);
1464
1465 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1468 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1473
1474 let bank = self.bank_forks.read().unwrap().working_bank();
1475 self.last_blockhash = bank.last_blockhash();
1476 Ok(())
1477 }
1478
1479 pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1480 let warp_slot = self
1481 .genesis_config
1482 .epoch_schedule
1483 .get_first_slot_in_epoch(warp_epoch);
1484 self.warp_to_slot(warp_slot)
1485 }
1486
1487 pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1489 let bank = self.bank_forks.read().unwrap().working_bank();
1490 let leader = *bank.leader();
1491
1492 bank.fill_bank_with_ticks_for_tests();
1495 let pre_warp_slot = bank.slot();
1496
1497 self.bank_forks.write().unwrap().set_root(
1498 pre_warp_slot,
1499 None, Some(pre_warp_slot),
1501 );
1502
1503 let warp_slot = pre_warp_slot + 1;
1505 let mut warp_bank = Bank::new_from_parent(bank, leader, warp_slot);
1506
1507 warp_bank.force_reward_interval_end_for_tests();
1508 self.bank_forks.write().unwrap().insert(warp_bank);
1509
1510 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1513 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1518
1519 let bank = self.bank_forks.read().unwrap().working_bank();
1520 self.last_blockhash = bank.last_blockhash();
1521 Ok(())
1522 }
1523
1524 pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1526 let blockhash = self
1527 .banks_client
1528 .get_new_latest_blockhash(&self.last_blockhash)
1529 .await?;
1530 self.last_blockhash = blockhash;
1531 Ok(blockhash)
1532 }
1533
1534 pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1536 self.bank_forks
1537 .read()
1538 .unwrap()
1539 .working_bank()
1540 .register_hard_fork(hard_fork_slot)
1541 }
1542}