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