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 serde::Serialize,
14 solana_account::{
15 Account, AccountSharedData, ReadableAccount, state_traits::StateMutWincode as _,
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_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};
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 && let Some(account_info) = account_info_map.get(borrowed_account.get_key())
191 {
192 if borrowed_account.get_lamports() != account_info.lamports() {
193 borrowed_account.set_lamports(account_info.lamports())?;
194 }
195
196 if borrowed_account
197 .can_data_be_resized(account_info.data_len())
198 .is_ok()
199 {
200 borrowed_account.set_data_from_slice(&account_info.data.borrow())?;
201 }
202 if borrowed_account.get_owner() != account_info.owner {
203 borrowed_account.set_owner(account_info.owner.as_ref())?;
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: Clone>(
252 sysvar: Result<Arc<T>, InstructionError>,
253 var_addr: *mut u8,
254 sysvar_size: usize,
255) -> u64 {
256 let invoke_context = get_invoke_context();
257 if invoke_context
258 .compute_meter
259 .consume_checked(invoke_context.get_execution_cost().sysvar_base_cost + sysvar_size as u64)
260 .is_err()
261 {
262 panic!("Exceeded compute budget");
263 }
264
265 match sysvar {
266 Ok(sysvar_data) => unsafe {
267 *(var_addr as *mut _ as *mut T) = T::clone(&sysvar_data);
268 SUCCESS
269 },
270 Err(_) => UNSUPPORTED_SYSVAR,
271 }
272}
273
274pub fn sol_get_clock_sysvar(var_addr: *mut u8) -> u64 {
276 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_clock_sysvar(
277 &SyscallStubs {},
278 var_addr,
279 )
280}
281
282pub fn sol_get_epoch_schedule_sysvar(var_addr: *mut u8) -> u64 {
284 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_epoch_schedule_sysvar(
285 &SyscallStubs {},
286 var_addr,
287 )
288}
289
290pub fn sol_get_epoch_rewards_sysvar(var_addr: *mut u8) -> u64 {
292 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_epoch_rewards_sysvar(
293 &SyscallStubs {},
294 var_addr,
295 )
296}
297
298pub fn sol_get_fees_sysvar(var_addr: *mut u8) -> u64 {
300 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_fees_sysvar(
301 &SyscallStubs {},
302 var_addr,
303 )
304}
305
306pub fn sol_get_rent_sysvar(var_addr: *mut u8) -> u64 {
308 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_rent_sysvar(
309 &SyscallStubs {},
310 var_addr,
311 )
312}
313
314pub fn sol_get_last_restart_slot(var_addr: *mut u8) -> u64 {
316 <SyscallStubs as solana_sysvar::program_stubs::SyscallStubs>::sol_get_last_restart_slot(
317 &SyscallStubs {},
318 var_addr,
319 )
320}
321
322struct SyscallStubs {}
323
324impl SyscallStubs {
325 fn fetch_and_write_sysvar<T: Serialize>(
326 &self,
327 var_addr: *mut u8,
328 offset: u64,
329 length: u64,
330 fetch: impl FnOnce(&SysvarCache) -> Result<Arc<T>, InstructionError>,
331 ) -> u64 {
332 let invoke_context = get_invoke_context();
334 let SVMTransactionExecutionCost {
335 sysvar_base_cost,
336 cpi_bytes_per_unit,
337 mem_op_base_cost,
338 ..
339 } = *invoke_context.get_execution_cost();
340
341 let sysvar_id_cost = 32_u64.checked_div(cpi_bytes_per_unit).unwrap_or(0);
342 let sysvar_buf_cost = length.checked_div(cpi_bytes_per_unit).unwrap_or(0);
343
344 if invoke_context
345 .compute_meter
346 .consume_checked(
347 sysvar_base_cost
348 .saturating_add(sysvar_id_cost)
349 .saturating_add(std::cmp::max(sysvar_buf_cost, mem_op_base_cost)),
350 )
351 .is_err()
352 {
353 panic!("Exceeded compute budget");
354 }
355
356 let Ok(sysvar) = fetch(get_invoke_context().environment_config.sysvar_cache()) else {
358 return UNSUPPORTED_SYSVAR;
359 };
360
361 let Ok(expected_length) = bincode::serialized_size(&sysvar) else {
364 return UNSUPPORTED_SYSVAR;
365 };
366
367 if offset.saturating_add(length) > expected_length {
368 return UNSUPPORTED_SYSVAR;
369 }
370
371 if let Ok(serialized) = bincode::serialize(&sysvar) {
373 unsafe {
374 ptr::copy_nonoverlapping(
375 serialized[offset as usize..].as_ptr(),
376 var_addr,
377 length as usize,
378 )
379 };
380 SUCCESS
381 } else {
382 UNSUPPORTED_SYSVAR
383 }
384 }
385}
386impl solana_sysvar::program_stubs::SyscallStubs for SyscallStubs {
387 fn sol_log(&self, message: &str) {
388 let invoke_context = get_invoke_context();
389 ic_msg!(invoke_context, "Program log: {}", message);
390 }
391
392 fn sol_invoke_signed(
393 &self,
394 instruction: &Instruction,
395 account_infos: &[AccountInfo],
396 signers_seeds: &[&[&[u8]]],
397 ) -> ProgramResult {
398 let invoke_context = get_invoke_context();
399 let log_collector = invoke_context.get_log_collector();
400
401 stable_log::program_invoke(
402 &log_collector,
403 &instruction.program_id,
404 invoke_context.get_stack_height(),
405 );
406
407 let transaction_context = &invoke_context.transaction_context;
412 let instruction_context = transaction_context
413 .get_current_instruction_context()
414 .unwrap();
415 let mut account_indices = Vec::with_capacity(instruction.accounts.len());
416 for account_meta in instruction.accounts.iter() {
417 let index_in_transaction = transaction_context
418 .find_index_of_account(&account_meta.pubkey)
419 .ok_or(InstructionError::MissingAccount)
420 .unwrap();
421 let account_info_index = account_infos
422 .iter()
423 .position(|account_info| account_info.unsigned_key() == &account_meta.pubkey)
424 .ok_or(InstructionError::MissingAccount)
425 .unwrap();
426 let account_info = &account_infos[account_info_index];
427 let index_in_caller = instruction_context
428 .get_index_of_account_in_instruction(index_in_transaction)
429 .unwrap();
430 let mut borrowed_account = instruction_context
431 .try_borrow_instruction_account(index_in_caller)
432 .unwrap();
433 if borrowed_account.get_lamports() != account_info.lamports() {
434 borrowed_account
435 .set_lamports(account_info.lamports())
436 .unwrap();
437 }
438 let account_info_data = account_info.try_borrow_data().unwrap();
439 match borrowed_account.can_data_be_resized(account_info_data.len()) {
441 Ok(()) => borrowed_account
442 .set_data_from_slice(&account_info_data)
443 .unwrap(),
444 Err(err) if borrowed_account.get_data() != *account_info_data => {
445 panic!("{err:?}");
446 }
447 _ => {}
448 }
449 if borrowed_account.get_owner() != account_info.owner {
451 borrowed_account
452 .set_owner(account_info.owner.as_ref())
453 .unwrap();
454 }
455 if account_meta.is_writable {
456 account_indices.push((index_in_transaction, account_info_index));
457 }
458 }
459
460 invoke_context
461 .native_invoke_signed(instruction.clone(), signers_seeds)
462 .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
463
464 let transaction_context = &invoke_context.transaction_context;
466 let instruction_context = transaction_context
467 .get_current_instruction_context()
468 .unwrap();
469 for (index_in_transaction, account_info_index) in account_indices.into_iter() {
470 let index_in_caller = instruction_context
471 .get_index_of_account_in_instruction(index_in_transaction)
472 .unwrap();
473 let borrowed_account = instruction_context
474 .try_borrow_instruction_account(index_in_caller)
475 .unwrap();
476 let account_info = &account_infos[account_info_index];
477 **account_info.try_borrow_mut_lamports().unwrap() = borrowed_account.get_lamports();
478 if account_info.owner != borrowed_account.get_owner() {
479 #[allow(clippy::transmute_ptr_to_ptr)]
481 #[allow(mutable_transmutes)]
482 let account_info_mut =
483 unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
484 *account_info_mut = *borrowed_account.get_owner();
485 }
486
487 let new_data = borrowed_account.get_data();
488 let new_len = new_data.len();
489
490 if account_info.data_len() != new_len {
492 account_info.resize(new_len)?;
493 }
494
495 let mut data = account_info.try_borrow_mut_data()?;
497 data.clone_from_slice(new_data);
498 }
499
500 stable_log::program_success(&log_collector, &instruction.program_id);
501 Ok(())
502 }
503
504 fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
505 get_sysvar(
506 get_invoke_context()
507 .environment_config
508 .sysvar_cache()
509 .get_clock(),
510 var_addr,
511 solana_clock::SIZE,
512 )
513 }
514
515 fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
516 get_sysvar(
517 get_invoke_context()
518 .environment_config
519 .sysvar_cache()
520 .get_epoch_schedule(),
521 var_addr,
522 solana_epoch_schedule::SIZE,
523 )
524 }
525
526 fn sol_get_epoch_rewards_sysvar(&self, var_addr: *mut u8) -> u64 {
527 get_sysvar(
528 get_invoke_context()
529 .environment_config
530 .sysvar_cache()
531 .get_epoch_rewards(),
532 var_addr,
533 solana_epoch_rewards::SIZE,
534 )
535 }
536
537 #[allow(deprecated)]
538 fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
539 get_sysvar(
540 get_invoke_context()
541 .environment_config
542 .sysvar_cache()
543 .get_fees(),
544 var_addr,
545 solana_sysvar::fees::SIZE,
546 )
547 }
548
549 fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
550 get_sysvar(
551 get_invoke_context()
552 .environment_config
553 .sysvar_cache()
554 .get_rent(),
555 var_addr,
556 solana_rent::SIZE,
557 )
558 }
559
560 fn sol_get_last_restart_slot(&self, var_addr: *mut u8) -> u64 {
561 get_sysvar(
562 get_invoke_context()
563 .environment_config
564 .sysvar_cache()
565 .get_last_restart_slot(),
566 var_addr,
567 solana_sysvar::last_restart_slot::SIZE,
568 )
569 }
570
571 fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
572 let (program_id, data) = get_invoke_context().transaction_context.get_return_data();
573 Some((*program_id, data.to_vec()))
574 }
575
576 fn sol_set_return_data(&self, data: &[u8]) {
577 let invoke_context = get_invoke_context();
578 let transaction_context = &mut invoke_context.transaction_context;
579 let instruction_context = transaction_context
580 .get_current_instruction_context()
581 .unwrap();
582 let caller = *instruction_context.get_program_key().unwrap();
583 transaction_context
584 .set_return_data(caller, data.to_vec())
585 .unwrap();
586 }
587
588 fn sol_get_stack_height(&self) -> u64 {
589 let invoke_context = get_invoke_context();
590 invoke_context.get_stack_height().try_into().unwrap()
591 }
592
593 fn sol_get_sysvar(
594 &self,
595 sysvar_id_addr: *const u8,
596 var_addr: *mut u8,
597 offset: u64,
598 length: u64,
599 ) -> u64 {
600 let sysvar_id = unsafe { &*(sysvar_id_addr as *const Pubkey) };
601
602 match *sysvar_id {
603 id if id == Clock::id() => self.fetch_and_write_sysvar::<Clock>(
604 var_addr,
605 offset,
606 length,
607 SysvarCache::get_clock,
608 ),
609 id if id == EpochRewards::id() => self.fetch_and_write_sysvar::<EpochRewards>(
610 var_addr,
611 offset,
612 length,
613 SysvarCache::get_epoch_rewards,
614 ),
615 id if id == EpochSchedule::id() => self.fetch_and_write_sysvar::<EpochSchedule>(
616 var_addr,
617 offset,
618 length,
619 SysvarCache::get_epoch_schedule,
620 ),
621 id if id == LastRestartSlot::id() => self.fetch_and_write_sysvar::<LastRestartSlot>(
622 var_addr,
623 offset,
624 length,
625 SysvarCache::get_last_restart_slot,
626 ),
627 id if id == Rent::id() => {
628 self.fetch_and_write_sysvar::<Rent>(var_addr, offset, length, SysvarCache::get_rent)
629 }
630 _ => UNSUPPORTED_SYSVAR,
631 }
632 }
633}
634
635#[allow(deprecated)]
636fn canonical_sysvar_data_len(sysvar_id: &Pubkey) -> Option<usize> {
637 match *sysvar_id {
638 sysvar::clock::ID => Some(solana_clock::SIZE),
639 sysvar::epoch_rewards::ID => Some(solana_epoch_rewards::SIZE),
640 sysvar::epoch_schedule::ID => Some(solana_epoch_schedule::SIZE),
641 sysvar::fees::ID => Some(solana_sysvar::fees::SIZE),
642 sysvar::last_restart_slot::ID => Some(solana_sysvar::last_restart_slot::SIZE),
643 sysvar::recent_blockhashes::ID => Some(solana_sysvar::recent_blockhashes::SIZE),
644 sysvar::rent::ID => Some(solana_rent::SIZE),
645 sysvar::rewards::ID => Some(solana_sysvar::rewards::SIZE),
646 sysvar::slot_hashes::ID => Some(solana_sysvar::slot_hashes::SIZE),
647 sysvar::slot_history::ID => Some(solana_sysvar::slot_history::SIZE),
648 sysvar::stake_history::ID => Some(solana_sysvar::stake_history::SIZE),
649 _ => None,
650 }
651}
652
653fn required_sysvar_data_len(sysvar_id: &Pubkey, serialized_len: usize) -> usize {
657 canonical_sysvar_data_len(sysvar_id)
658 .unwrap_or(serialized_len)
659 .max(serialized_len)
660}
661
662fn create_sysvar_account<T: SysvarId + Serialize>(sysvar: &T) -> Account {
663 let serialized_len = bincode::serialized_size(sysvar).unwrap() as usize;
664 let data_len = required_sysvar_data_len(&T::id(), serialized_len);
665 let mut account = Account::new(1, data_len, &sysvar::id());
666 bincode::serialize_into(account.data.as_mut_slice(), sysvar).unwrap();
667 account
668}
669
670pub fn find_file(filename: &str) -> Option<PathBuf> {
671 for dir in default_shared_object_dirs() {
672 let candidate = dir.join(filename);
673 if candidate.exists() {
674 return Some(candidate);
675 }
676 }
677 None
678}
679
680fn default_shared_object_dirs() -> Vec<PathBuf> {
681 let mut search_path = vec![];
682 if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
683 search_path.push(PathBuf::from(bpf_out_dir));
684 } else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
685 search_path.push(PathBuf::from(bpf_out_dir));
686 }
687 search_path.push(PathBuf::from("tests/fixtures"));
688 if let Ok(dir) = std::env::current_dir() {
689 search_path.push(dir);
690 }
691 trace!("SBF .so search path: {search_path:?}");
692 search_path
693}
694
695pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
696 let path = path.as_ref();
697 let mut file = File::open(path)
698 .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
699
700 let mut file_data = Vec::new();
701 file.read_to_end(&mut file_data)
702 .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
703 file_data
704}
705
706pub struct ProgramTest {
707 accounts: Vec<(Pubkey, AccountSharedData)>,
708 genesis_accounts: Vec<(Pubkey, AccountSharedData)>,
709 builtin_programs: Vec<(Pubkey, &'static str, ProgramCacheEntry)>,
710 compute_max_units: Option<u64>,
711 prefer_bpf: bool,
712 deactivate_feature_set: HashSet<Pubkey>,
713 transaction_account_lock_limit: Option<usize>,
714}
715
716impl Default for ProgramTest {
717 fn default() -> Self {
730 agave_logger::setup_with_default(
731 "solana_sbpf::vm=debug,solana_runtime::message_processor=debug,\
732 solana_runtime::system_instruction_processor=trace,solana_program_test=info",
733 );
734 let prefer_bpf =
735 std::env::var("BPF_OUT_DIR").is_ok() || std::env::var("SBF_OUT_DIR").is_ok();
736
737 Self {
738 accounts: vec![],
739 genesis_accounts: vec![],
740 builtin_programs: vec![],
741 compute_max_units: None,
742 prefer_bpf,
743 deactivate_feature_set: HashSet::default(),
744 transaction_account_lock_limit: None,
745 }
746 }
747}
748
749impl ProgramTest {
750 pub fn new(
758 program_name: &'static str,
759 program_id: Pubkey,
760 builtin: Option<BuiltinFunctionRegisterer>,
761 ) -> Self {
762 let mut me = Self::default();
763 me.add_program(program_name, program_id, builtin);
764 me
765 }
766
767 pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
769 self.prefer_bpf = prefer_bpf;
770 }
771
772 pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
774 debug_assert!(
775 compute_max_units <= i64::MAX as u64,
776 "Compute unit limit must fit in `i64::MAX`"
777 );
778 self.compute_max_units = Some(compute_max_units);
779 }
780
781 pub fn set_transaction_account_lock_limit(&mut self, transaction_account_lock_limit: usize) {
783 self.transaction_account_lock_limit = Some(transaction_account_lock_limit);
784 }
785
786 pub fn add_genesis_account(&mut self, address: Pubkey, account: Account) {
788 self.genesis_accounts
789 .push((address, AccountSharedData::from(account)));
790 }
791
792 pub fn add_account(&mut self, address: Pubkey, account: Account) {
794 self.accounts
795 .push((address, AccountSharedData::from(account)));
796 }
797
798 pub fn add_account_with_file_data(
800 &mut self,
801 address: Pubkey,
802 lamports: u64,
803 owner: Pubkey,
804 filename: &str,
805 ) {
806 self.add_account(
807 address,
808 Account {
809 lamports,
810 data: read_file(find_file(filename).unwrap_or_else(|| {
811 panic!("Unable to locate {filename}");
812 })),
813 owner,
814 executable: false,
815 rent_epoch: 0,
816 },
817 );
818 }
819
820 pub fn add_account_with_base64_data(
823 &mut self,
824 address: Pubkey,
825 lamports: u64,
826 owner: Pubkey,
827 data_base64: &str,
828 ) {
829 self.add_account(
830 address,
831 Account {
832 lamports,
833 data: BASE64_STANDARD
834 .decode(data_base64)
835 .unwrap_or_else(|err| panic!("Failed to base64 decode: {err}")),
836 owner,
837 executable: false,
838 rent_epoch: 0,
839 },
840 );
841 }
842
843 pub fn add_sysvar_account<S: SysvarId + Serialize>(&mut self, address: Pubkey, sysvar: &S) {
844 self.add_account(address, create_sysvar_account(sysvar));
845 }
846
847 pub fn add_upgradeable_program_to_genesis(
860 &mut self,
861 program_name: &'static str,
862 program_id: &Pubkey,
863 ) {
864 let program_file = find_file(&format!("{program_name}.so")).unwrap_or_else(|| {
865 panic!("Program file data not available for {program_name} ({program_id})")
866 });
867 let elf = read_file(program_file);
868 let program_accounts =
869 programs::bpf_loader_upgradeable_program_accounts(program_id, &elf, &Rent::default());
870 for (address, account) in program_accounts {
871 self.add_genesis_account(address, account);
872 }
873 }
874
875 pub fn add_program(
883 &mut self,
884 program_name: &'static str,
885 program_id: Pubkey,
886 builtin_function: Option<BuiltinFunctionRegisterer>,
887 ) {
888 let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
889 let data = read_file(&program_file);
890 info!(
891 "\"{}\" SBF program from {}{}",
892 program_name,
893 program_file.display(),
894 std::fs::metadata(&program_file)
895 .map(|metadata| {
896 metadata
897 .modified()
898 .map(|time| {
899 format!(
900 ", modified {}",
901 HumanTime::from(time)
902 .to_text_en(Accuracy::Precise, Tense::Past)
903 )
904 })
905 .ok()
906 })
907 .ok()
908 .flatten()
909 .unwrap_or_default()
910 );
911
912 this.add_account(
913 program_id,
914 Account {
915 lamports: Rent::default().minimum_balance(data.len()).max(1),
916 data,
917 owner: solana_sdk_ids::bpf_loader::id(),
918 executable: true,
919 rent_epoch: 0,
920 },
921 );
922 };
923
924 let warn_invalid_program_name = || {
925 let valid_program_names = default_shared_object_dirs()
926 .iter()
927 .filter_map(|dir| dir.read_dir().ok())
928 .flat_map(|read_dir| {
929 read_dir.filter_map(|entry| {
930 let path = entry.ok()?.path();
931 if !path.is_file() {
932 return None;
933 }
934 match path.extension()?.to_str()? {
935 "so" => Some(path.file_stem()?.to_os_string()),
936 _ => None,
937 }
938 })
939 })
940 .collect::<Vec<_>>();
941
942 if valid_program_names.is_empty() {
943 warn!("No SBF shared objects found.");
946 return;
947 }
948
949 warn!(
950 "Possible bogus program name. Ensure the program name ({program_name}) matches \
951 one of the following recognizable program names:",
952 );
953 for name in valid_program_names {
954 warn!(" - {}", name.to_str().unwrap());
955 }
956 };
957
958 let program_file = find_file(&format!("{program_name}.so"));
959 match (self.prefer_bpf, program_file, builtin_function) {
960 (true, Some(file), _) => add_bpf(self, file),
963
964 (false, _, Some(builtin_function)) => {
967 self.add_builtin_program(program_name, program_id, builtin_function)
968 }
969
970 (true, None, _) => {
972 warn_invalid_program_name();
973 panic!("Program file data not available for {program_name} ({program_id})");
974 }
975
976 (false, _, None) => {
978 panic!("Program processor not available for {program_name} ({program_id})");
979 }
980 }
981 }
982
983 pub fn add_builtin_program(
987 &mut self,
988 program_name: &'static str,
989 program_id: Pubkey,
990 builtin: BuiltinFunctionRegisterer,
991 ) {
992 info!("\"{program_name}\" builtin program");
993 self.builtin_programs.push((
994 program_id,
995 program_name,
996 ProgramCacheEntry::new_builtin(0, builtin),
997 ));
998 }
999
1000 pub fn deactivate_feature(&mut self, feature_id: Pubkey) {
1004 self.deactivate_feature_set.insert(feature_id);
1005 }
1006
1007 fn setup_bank(
1008 &mut self,
1009 ) -> (
1010 Arc<RwLock<BankForks>>,
1011 Arc<RwLock<BlockCommitmentCache>>,
1012 Hash,
1013 GenesisConfigInfo,
1014 ) {
1015 {
1016 use std::sync::Once;
1017 static ONCE: Once = Once::new();
1018
1019 ONCE.call_once(|| {
1020 solana_sysvar::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
1021 });
1022 }
1023
1024 let rent = Rent::default();
1025 let fee_rate_governor = FeeRateGovernor {
1026 lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
1028 ..FeeRateGovernor::default()
1029 };
1030 let bootstrap_validator_pubkey = Pubkey::new_unique();
1031 let bootstrap_validator_stake_lamports =
1032 rent.minimum_balance(VoteStateV4::size_of()) + 1_000_000 * LAMPORTS_PER_SOL;
1033
1034 let mint_keypair = Keypair::new();
1035 let voting_keypair = Keypair::new();
1036
1037 let mut feature_set = FeatureSet::all_enabled();
1039 for deactivate_feature_pk in &self.deactivate_feature_set {
1040 if FEATURE_NAMES.contains_key(deactivate_feature_pk) {
1041 feature_set.deactivate(deactivate_feature_pk);
1042 } else {
1043 warn!(
1044 "Feature {deactivate_feature_pk:?} set for deactivation is not a known \
1045 Feature public key"
1046 );
1047 }
1048 }
1049
1050 let mut genesis_config = create_genesis_config_with_leader_ex(
1051 1_000_000 * LAMPORTS_PER_SOL,
1052 &mint_keypair.pubkey(),
1053 &bootstrap_validator_pubkey,
1054 &voting_keypair.pubkey(),
1055 &Pubkey::new_unique(),
1056 None,
1057 bootstrap_validator_stake_lamports,
1058 890_880,
1059 fee_rate_governor,
1060 rent.clone(),
1061 ClusterType::Development,
1062 &feature_set,
1063 std::mem::take(&mut self.genesis_accounts),
1064 );
1065
1066 let target_tick_duration = Duration::from_micros(100);
1067 genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
1068 debug!("Payer address: {}", mint_keypair.pubkey());
1069 debug!("Genesis config: {genesis_config}");
1070
1071 let bank = Bank::new_from_genesis(
1072 &genesis_config,
1073 Arc::new(RuntimeConfig {
1074 compute_budget: self.compute_max_units.map(|max_units| ComputeBudget {
1075 compute_unit_limit: max_units,
1076 ..ComputeBudget::new_with_defaults(
1077 genesis_config
1078 .accounts
1079 .contains_key(&raise_cpi_nesting_limit_to_8::id()),
1080 )
1081 }),
1082 transaction_account_lock_limit: self.transaction_account_lock_limit,
1083 ..RuntimeConfig::default()
1084 }),
1085 Vec::default(),
1086 None,
1087 ACCOUNTS_DB_CONFIG_FOR_TESTING,
1088 None,
1089 None,
1090 Arc::default(),
1091 None,
1092 None,
1093 );
1094
1095 for (program_id, account) in programs::spl_programs(&rent).iter() {
1097 bank.store_account(program_id, account);
1098 }
1099
1100 for (program_id, account) in programs::core_bpf_programs(&rent, |feature_id| {
1102 genesis_config.accounts.contains_key(feature_id)
1103 })
1104 .iter()
1105 {
1106 bank.store_account(program_id, account);
1107 }
1108
1109 let mut builtin_programs = Vec::new();
1111 std::mem::swap(&mut self.builtin_programs, &mut builtin_programs);
1112 for (program_id, name, builtin) in builtin_programs.into_iter() {
1113 bank.add_builtin(program_id, name, builtin);
1114 }
1115
1116 for (address, account) in self.accounts.iter() {
1117 if bank.get_account(address).is_some() {
1118 info!("Overriding account at {address}");
1119 }
1120 bank.store_account(address, account);
1121 }
1122 bank.set_capitalization_for_tests(bank.calculate_capitalization_for_tests());
1123 bank.fill_bank_with_ticks_for_tests();
1127 let bank_forks = BankForks::new_rw_arc(bank);
1128 let bank0 = bank_forks.read().unwrap().root_bank();
1129 let bank1 = Bank::new_from_parent(bank0.clone(), *bank0.leader(), bank0.slot() + 1);
1130 let bank1 = {
1131 let mut bf = bank_forks.write().unwrap();
1132 bf.insert(bank1);
1133 bf.working_bank()
1134 };
1135 debug!("Bank slot: {}", bank1.slot());
1136 let slot = bank1.slot();
1137 let last_blockhash = bank1.last_blockhash();
1138 let block_commitment_cache = Arc::new(RwLock::new(
1139 BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
1140 ));
1141
1142 (
1143 bank_forks,
1144 block_commitment_cache,
1145 last_blockhash,
1146 GenesisConfigInfo {
1147 genesis_config,
1148 mint_keypair,
1149 voting_keypair,
1150 validator_pubkey: bootstrap_validator_pubkey,
1151 },
1152 )
1153 }
1154
1155 pub async fn start(mut self) -> (BanksClient, Keypair, Hash) {
1156 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1157 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1158 let target_slot_duration = target_tick_duration * gci.genesis_config.ticks_per_slot as u32;
1159 let transport = start_local_server(
1160 bank_forks.clone(),
1161 block_commitment_cache.clone(),
1162 target_tick_duration,
1163 )
1164 .await;
1165 let banks_client = start_client(transport)
1166 .await
1167 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1168
1169 tokio::spawn(async move {
1173 loop {
1174 tokio::time::sleep(target_slot_duration).await;
1175 bank_forks
1176 .read()
1177 .unwrap()
1178 .working_bank()
1179 .register_unique_recent_blockhash_for_test();
1180 }
1181 });
1182
1183 (banks_client, gci.mint_keypair, last_blockhash)
1184 }
1185
1186 pub async fn start_with_context(mut self) -> ProgramTestContext {
1191 let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
1192 let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
1193 let transport = start_local_server(
1194 bank_forks.clone(),
1195 block_commitment_cache.clone(),
1196 target_tick_duration,
1197 )
1198 .await;
1199 let banks_client = start_client(transport)
1200 .await
1201 .unwrap_or_else(|err| panic!("Failed to start banks client: {err}"));
1202
1203 ProgramTestContext::new(
1204 bank_forks,
1205 block_commitment_cache,
1206 banks_client,
1207 last_blockhash,
1208 gci,
1209 )
1210 }
1211}
1212
1213#[async_trait]
1214pub trait ProgramTestBanksClientExt {
1215 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
1217}
1218
1219#[async_trait]
1220impl ProgramTestBanksClientExt for BanksClient {
1221 async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
1222 let mut num_retries = 0;
1223 let start = Instant::now();
1224 while start.elapsed().as_secs() < 5 {
1225 let new_blockhash = self.get_latest_blockhash().await?;
1226 if new_blockhash != *blockhash {
1227 return Ok(new_blockhash);
1228 }
1229 debug!("Got same blockhash ({blockhash:?}), will retry...");
1230
1231 tokio::time::sleep(Duration::from_millis(200)).await;
1232 num_retries += 1;
1233 }
1234
1235 Err(io::Error::other(format!(
1236 "Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
1237 start.elapsed().as_millis(),
1238 num_retries,
1239 blockhash
1240 )))
1241 }
1242}
1243
1244struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
1245
1246impl<T> Drop for DroppableTask<T> {
1247 fn drop(&mut self) {
1248 self.0.store(true, Ordering::Relaxed);
1249 trace!(
1250 "stopping task, which is currently {}",
1251 if self.1.is_finished() {
1252 "finished"
1253 } else {
1254 "running"
1255 }
1256 );
1257 }
1258}
1259
1260pub struct ProgramTestContext {
1261 pub banks_client: BanksClient,
1262 pub last_blockhash: Hash,
1263 pub payer: Keypair,
1264 genesis_config: GenesisConfig,
1265 bank_forks: Arc<RwLock<BankForks>>,
1266 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1267 _bank_task: DroppableTask<()>,
1268}
1269
1270impl ProgramTestContext {
1271 fn new(
1272 bank_forks: Arc<RwLock<BankForks>>,
1273 block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
1274 banks_client: BanksClient,
1275 last_blockhash: Hash,
1276 genesis_config_info: GenesisConfigInfo,
1277 ) -> Self {
1278 let running_bank_forks = bank_forks.clone();
1282 let target_tick_duration = genesis_config_info
1283 .genesis_config
1284 .poh_config
1285 .target_tick_duration;
1286 let target_slot_duration =
1287 target_tick_duration * genesis_config_info.genesis_config.ticks_per_slot as u32;
1288 let exit = Arc::new(AtomicBool::new(false));
1289 let bank_task = DroppableTask(
1290 exit.clone(),
1291 tokio::spawn(async move {
1292 loop {
1293 if exit.load(Ordering::Relaxed) {
1294 break;
1295 }
1296 tokio::time::sleep(target_slot_duration).await;
1297 running_bank_forks
1298 .read()
1299 .unwrap()
1300 .working_bank()
1301 .register_unique_recent_blockhash_for_test();
1302 }
1303 }),
1304 );
1305
1306 Self {
1307 banks_client,
1308 last_blockhash,
1309 payer: genesis_config_info.mint_keypair,
1310 genesis_config: genesis_config_info.genesis_config,
1311 bank_forks,
1312 block_commitment_cache,
1313 _bank_task: bank_task,
1314 }
1315 }
1316
1317 pub fn genesis_config(&self) -> &GenesisConfig {
1318 &self.genesis_config
1319 }
1320
1321 pub fn is_active(&self, feature: &Address) -> bool {
1322 self.bank_forks
1323 .read()
1324 .unwrap()
1325 .root_bank()
1326 .feature_set
1327 .is_active(feature)
1328 }
1329
1330 pub fn increment_vote_account_credits(
1332 &mut self,
1333 vote_account_address: &Pubkey,
1334 number_of_credits: u64,
1335 ) {
1336 let bank_forks = self.bank_forks.read().unwrap();
1337 let bank = bank_forks.working_bank();
1338
1339 let mut vote_account = bank.get_account(vote_account_address).unwrap();
1341 let mut vote_state =
1342 VoteStateV4::deserialize(vote_account.data(), vote_account_address).unwrap();
1343
1344 let epoch = bank.epoch();
1345 const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
1347 for _ in 0..number_of_credits {
1348 let credits = 1;
1350
1351 if vote_state.epoch_credits.is_empty() {
1353 vote_state.epoch_credits.push((epoch, 0, 0));
1354 } else if epoch != vote_state.epoch_credits.last().unwrap().0 {
1355 let (_, credits_val, prev_credits) = *vote_state.epoch_credits.last().unwrap();
1356
1357 if credits_val != prev_credits {
1358 vote_state
1361 .epoch_credits
1362 .push((epoch, credits_val, credits_val));
1363 } else {
1364 vote_state.epoch_credits.last_mut().unwrap().0 = epoch;
1366 }
1367
1368 if vote_state.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
1370 vote_state.epoch_credits.remove(0);
1371 }
1372 }
1373
1374 vote_state.epoch_credits.last_mut().unwrap().1 = vote_state
1375 .epoch_credits
1376 .last()
1377 .unwrap()
1378 .1
1379 .saturating_add(credits);
1380 }
1381 let versioned = VoteStateVersions::new_v4(vote_state);
1382 vote_account.set_state(&versioned).unwrap();
1383 bank.store_account(vote_account_address, &vote_account);
1384 }
1385
1386 pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
1393 let bank_forks = self.bank_forks.read().unwrap();
1394 let bank = bank_forks.working_bank();
1395 bank.store_account(address, account);
1396 }
1397
1398 pub fn set_sysvar<T: SysvarId + Serialize>(&self, sysvar: &T) {
1405 let bank_forks = self.bank_forks.read().unwrap();
1406 let bank = bank_forks.working_bank();
1407 bank.set_sysvar_for_tests(sysvar);
1408 }
1409
1410 pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
1412 let bank = self.bank_forks.read().unwrap().working_bank();
1413 let leader = *bank.leader();
1414
1415 bank.fill_bank_with_ticks_for_tests();
1418
1419 let working_slot = bank.slot();
1421 if warp_slot <= working_slot {
1422 return Err(ProgramTestError::InvalidWarpSlot);
1423 }
1424
1425 let pre_warp_slot = warp_slot - 1;
1429 let warp_bank = if pre_warp_slot == working_slot {
1430 bank.freeze();
1431 bank
1432 } else {
1433 let warped = Bank::warp_from_parent(bank, leader, pre_warp_slot);
1434 self.bank_forks
1435 .write()
1436 .unwrap()
1437 .insert(warped)
1438 .clone_without_scheduler()
1439 };
1440
1441 self.bank_forks.write().unwrap().set_root(
1442 pre_warp_slot,
1443 None, Some(pre_warp_slot),
1445 );
1446
1447 let bank_at_warp_slot = Bank::new_from_parent(warp_bank, leader, warp_slot);
1449 self.bank_forks.write().unwrap().insert(bank_at_warp_slot);
1450
1451 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1454 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1459
1460 let bank = self.bank_forks.read().unwrap().working_bank();
1461 self.last_blockhash = bank.last_blockhash();
1462 Ok(())
1463 }
1464
1465 pub fn warp_to_epoch(&mut self, warp_epoch: Epoch) -> Result<(), ProgramTestError> {
1466 let warp_slot = self
1467 .genesis_config
1468 .epoch_schedule
1469 .get_first_slot_in_epoch(warp_epoch);
1470 self.warp_to_slot(warp_slot)
1471 }
1472
1473 pub fn warp_forward_force_reward_interval_end(&mut self) -> Result<(), ProgramTestError> {
1475 let bank = self.bank_forks.read().unwrap().working_bank();
1476 let leader = *bank.leader();
1477
1478 bank.fill_bank_with_ticks_for_tests();
1481 let pre_warp_slot = bank.slot();
1482
1483 self.bank_forks.write().unwrap().set_root(
1484 pre_warp_slot,
1485 None, Some(pre_warp_slot),
1487 );
1488
1489 let warp_slot = pre_warp_slot + 1;
1491 let mut warp_bank = Bank::new_from_parent(bank, leader, warp_slot);
1492
1493 warp_bank.force_reward_interval_end_for_tests();
1494 self.bank_forks.write().unwrap().insert(warp_bank);
1495
1496 let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
1499 w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
1504
1505 let bank = self.bank_forks.read().unwrap().working_bank();
1506 self.last_blockhash = bank.last_blockhash();
1507 Ok(())
1508 }
1509
1510 pub async fn get_new_latest_blockhash(&mut self) -> io::Result<Hash> {
1512 let blockhash = self
1513 .banks_client
1514 .get_new_latest_blockhash(&self.last_blockhash)
1515 .await?;
1516 self.last_blockhash = blockhash;
1517 Ok(blockhash)
1518 }
1519
1520 pub fn register_hard_fork(&mut self, hard_fork_slot: Slot) {
1522 self.bank_forks
1523 .read()
1524 .unwrap()
1525 .working_bank()
1526 .register_hard_fork(hard_fork_slot)
1527 }
1528}