1091 lines
39 KiB
Rust
1091 lines
39 KiB
Rust
//! The solana-program-test provides a BanksClient-based test framework BPF programs
|
|
#![allow(clippy::integer_arithmetic)]
|
|
|
|
#[allow(deprecated)]
|
|
use solana_sdk::sysvar::fees::Fees;
|
|
// Export tokio for test clients
|
|
pub use tokio;
|
|
use {
|
|
async_trait::async_trait,
|
|
chrono_humanize::{Accuracy, HumanTime, Tense},
|
|
log::*,
|
|
solana_banks_client::start_client,
|
|
solana_banks_server::banks_server::start_local_server,
|
|
solana_program_runtime::{ic_msg, invoke_context::ProcessInstructionWithContext, stable_log},
|
|
solana_runtime::{
|
|
bank::{Bank, ExecuteTimings},
|
|
bank_forks::BankForks,
|
|
builtins::Builtin,
|
|
commitment::BlockCommitmentCache,
|
|
genesis_utils::{create_genesis_config_with_leader_ex, GenesisConfigInfo},
|
|
},
|
|
solana_sdk::{
|
|
account::{Account, AccountSharedData, ReadableAccount, WritableAccount},
|
|
account_info::AccountInfo,
|
|
clock::{Clock, Slot},
|
|
compute_budget::ComputeBudget,
|
|
entrypoint::{ProgramResult, SUCCESS},
|
|
epoch_schedule::EpochSchedule,
|
|
fee_calculator::{FeeCalculator, FeeRateGovernor},
|
|
genesis_config::{ClusterType, GenesisConfig},
|
|
hash::Hash,
|
|
instruction::{Instruction, InstructionError},
|
|
native_token::sol_to_lamports,
|
|
poh_config::PohConfig,
|
|
program_error::{ProgramError, ACCOUNT_BORROW_FAILED, UNSUPPORTED_SYSVAR},
|
|
pubkey::Pubkey,
|
|
rent::Rent,
|
|
signature::{Keypair, Signer},
|
|
sysvar::{
|
|
clock, epoch_schedule,
|
|
fees::{self},
|
|
rent, Sysvar, SysvarId,
|
|
},
|
|
},
|
|
solana_vote_program::vote_state::{VoteState, VoteStateVersions},
|
|
std::{
|
|
cell::RefCell,
|
|
collections::HashSet,
|
|
convert::TryFrom,
|
|
fs::File,
|
|
io::{self, Read},
|
|
mem::transmute,
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, RwLock,
|
|
},
|
|
time::{Duration, Instant},
|
|
},
|
|
thiserror::Error,
|
|
tokio::task::JoinHandle,
|
|
};
|
|
// Export types so test clients can limit their solana crate dependencies
|
|
pub use {solana_banks_client::BanksClient, solana_program_runtime::invoke_context::InvokeContext};
|
|
|
|
pub mod programs;
|
|
|
|
#[macro_use]
|
|
extern crate solana_bpf_loader_program;
|
|
|
|
/// Errors from the program test environment
|
|
#[derive(Error, Debug, PartialEq)]
|
|
pub enum ProgramTestError {
|
|
/// The chosen warp slot is not in the future, so warp is not performed
|
|
#[error("Warp slot not in the future")]
|
|
InvalidWarpSlot,
|
|
}
|
|
|
|
thread_local! {
|
|
static INVOKE_CONTEXT: RefCell<Option<usize>> = RefCell::new(None);
|
|
}
|
|
fn set_invoke_context(new: &mut InvokeContext) {
|
|
INVOKE_CONTEXT
|
|
.with(|invoke_context| unsafe { invoke_context.replace(Some(transmute::<_, usize>(new))) });
|
|
}
|
|
fn get_invoke_context<'a, 'b>() -> &'a mut InvokeContext<'b> {
|
|
let ptr = INVOKE_CONTEXT.with(|invoke_context| match *invoke_context.borrow() {
|
|
Some(val) => val,
|
|
None => panic!("Invoke context not set!"),
|
|
});
|
|
unsafe { transmute::<usize, &mut InvokeContext>(ptr) }
|
|
}
|
|
|
|
pub fn builtin_process_instruction(
|
|
process_instruction: solana_sdk::entrypoint::ProcessInstruction,
|
|
_first_instruction_account: usize,
|
|
input: &[u8],
|
|
invoke_context: &mut InvokeContext,
|
|
) -> Result<(), InstructionError> {
|
|
set_invoke_context(invoke_context);
|
|
|
|
let transaction_context = &invoke_context.transaction_context;
|
|
let instruction_context = transaction_context.get_current_instruction_context()?;
|
|
let indices_in_instruction = instruction_context.get_number_of_program_accounts()
|
|
..instruction_context.get_number_of_accounts();
|
|
|
|
let log_collector = invoke_context.get_log_collector();
|
|
let program_id = transaction_context.get_program_key()?;
|
|
stable_log::program_invoke(&log_collector, program_id, invoke_context.invoke_depth());
|
|
|
|
// Copy indices_in_instruction into a HashSet to ensure there are no duplicates
|
|
let deduplicated_indices: HashSet<usize> = indices_in_instruction.clone().collect();
|
|
|
|
// Create copies of the accounts
|
|
let mut account_copies = deduplicated_indices
|
|
.iter()
|
|
.map(|index_in_instruction| {
|
|
let borrowed_account = instruction_context
|
|
.try_borrow_account(transaction_context, *index_in_instruction)?;
|
|
Ok((
|
|
*borrowed_account.get_key(),
|
|
*borrowed_account.get_owner(),
|
|
borrowed_account.get_lamports(),
|
|
borrowed_account.get_data().to_vec(),
|
|
))
|
|
})
|
|
.collect::<Result<Vec<_>, InstructionError>>()?;
|
|
|
|
// Create shared references to account_copies
|
|
let account_refs: Vec<_> = account_copies
|
|
.iter_mut()
|
|
.map(|(key, owner, lamports, data)| {
|
|
(
|
|
key,
|
|
owner,
|
|
Rc::new(RefCell::new(lamports)),
|
|
Rc::new(RefCell::new(data.as_mut())),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
// Create AccountInfos
|
|
let account_infos = indices_in_instruction
|
|
.map(|index_in_instruction| {
|
|
let account_copy_index = deduplicated_indices
|
|
.iter()
|
|
.position(|index| *index == index_in_instruction)
|
|
.unwrap();
|
|
let (key, owner, lamports, data) = &account_refs[account_copy_index];
|
|
let borrowed_account = instruction_context
|
|
.try_borrow_account(transaction_context, index_in_instruction)?;
|
|
Ok(AccountInfo {
|
|
key,
|
|
is_signer: borrowed_account.is_signer(),
|
|
is_writable: borrowed_account.is_writable(),
|
|
lamports: lamports.clone(),
|
|
data: data.clone(),
|
|
owner,
|
|
executable: borrowed_account.is_executable(),
|
|
rent_epoch: borrowed_account.get_rent_epoch(),
|
|
})
|
|
})
|
|
.collect::<Result<Vec<AccountInfo>, InstructionError>>()?;
|
|
|
|
// Execute the program
|
|
process_instruction(program_id, &account_infos, input).map_err(|err| {
|
|
let err = u64::from(err);
|
|
stable_log::program_failure(&log_collector, program_id, &err.into());
|
|
err
|
|
})?;
|
|
stable_log::program_success(&log_collector, program_id);
|
|
|
|
// Commit AccountInfo changes back into KeyedAccounts
|
|
for (index_in_instruction, (_key, _owner, lamports, data)) in deduplicated_indices
|
|
.into_iter()
|
|
.zip(account_copies.into_iter())
|
|
{
|
|
let mut borrowed_account =
|
|
instruction_context.try_borrow_account(transaction_context, index_in_instruction)?;
|
|
if borrowed_account.is_writable() {
|
|
borrowed_account.set_lamports(lamports)?;
|
|
borrowed_account.set_data(&data)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Converts a `solana-program`-style entrypoint into the runtime's entrypoint style, for
|
|
/// use with `ProgramTest::add_program`
|
|
#[macro_export]
|
|
macro_rules! processor {
|
|
($process_instruction:expr) => {
|
|
Some(
|
|
|first_instruction_account: usize,
|
|
input: &[u8],
|
|
invoke_context: &mut solana_program_test::InvokeContext| {
|
|
$crate::builtin_process_instruction(
|
|
$process_instruction,
|
|
first_instruction_account,
|
|
input,
|
|
invoke_context,
|
|
)
|
|
},
|
|
)
|
|
};
|
|
}
|
|
|
|
fn get_sysvar<T: Default + Sysvar + Sized + serde::de::DeserializeOwned>(
|
|
id: &Pubkey,
|
|
var_addr: *mut u8,
|
|
) -> u64 {
|
|
let invoke_context = get_invoke_context();
|
|
if invoke_context
|
|
.get_compute_meter()
|
|
.try_borrow_mut()
|
|
.map_err(|_| ACCOUNT_BORROW_FAILED)
|
|
.unwrap()
|
|
.consume(invoke_context.get_compute_budget().sysvar_base_cost + T::size_of() as u64)
|
|
.is_err()
|
|
{
|
|
panic!("Exceeded compute budget");
|
|
}
|
|
|
|
match invoke_context.get_sysvar::<T>(id) {
|
|
Ok(sysvar_data) => unsafe {
|
|
*(var_addr as *mut _ as *mut T) = sysvar_data;
|
|
SUCCESS
|
|
},
|
|
Err(_) => UNSUPPORTED_SYSVAR,
|
|
}
|
|
}
|
|
|
|
struct SyscallStubs {}
|
|
impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs {
|
|
fn sol_log(&self, message: &str) {
|
|
let invoke_context = get_invoke_context();
|
|
ic_msg!(invoke_context, "Program log: {}", message);
|
|
}
|
|
|
|
fn sol_invoke_signed(
|
|
&self,
|
|
instruction: &Instruction,
|
|
account_infos: &[AccountInfo],
|
|
signers_seeds: &[&[&[u8]]],
|
|
) -> ProgramResult {
|
|
let invoke_context = get_invoke_context();
|
|
let log_collector = invoke_context.get_log_collector();
|
|
|
|
let caller = *invoke_context
|
|
.transaction_context
|
|
.get_program_key()
|
|
.unwrap();
|
|
|
|
stable_log::program_invoke(
|
|
&log_collector,
|
|
&instruction.program_id,
|
|
invoke_context.invoke_depth(),
|
|
);
|
|
|
|
let signers = signers_seeds
|
|
.iter()
|
|
.map(|seeds| Pubkey::create_program_address(seeds, &caller).unwrap())
|
|
.collect::<Vec<_>>();
|
|
let (instruction_accounts, program_indices) = invoke_context
|
|
.prepare_instruction(instruction, &signers)
|
|
.unwrap();
|
|
|
|
// Copy caller's account_info modifications into invoke_context accounts
|
|
let mut account_indices = Vec::with_capacity(instruction_accounts.len());
|
|
for instruction_account in instruction_accounts.iter() {
|
|
let account_key = invoke_context
|
|
.transaction_context
|
|
.get_key_of_account_at_index(instruction_account.index_in_transaction);
|
|
let account_info_index = account_infos
|
|
.iter()
|
|
.position(|account_info| account_info.unsigned_key() == account_key)
|
|
.ok_or(InstructionError::MissingAccount)
|
|
.unwrap();
|
|
let account_info = &account_infos[account_info_index];
|
|
let mut account = invoke_context
|
|
.transaction_context
|
|
.get_account_at_index(instruction_account.index_in_transaction)
|
|
.borrow_mut();
|
|
account.copy_into_owner_from_slice(account_info.owner.as_ref());
|
|
account.set_data_from_slice(&account_info.try_borrow_data().unwrap());
|
|
account.set_lamports(account_info.lamports());
|
|
account.set_executable(account_info.executable);
|
|
account.set_rent_epoch(account_info.rent_epoch);
|
|
if instruction_account.is_writable {
|
|
account_indices
|
|
.push((instruction_account.index_in_transaction, account_info_index));
|
|
}
|
|
}
|
|
|
|
let mut compute_units_consumed = 0;
|
|
invoke_context
|
|
.process_instruction(
|
|
&instruction.data,
|
|
&instruction_accounts,
|
|
&program_indices,
|
|
&mut compute_units_consumed,
|
|
)
|
|
.map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?;
|
|
|
|
// Copy invoke_context accounts modifications into caller's account_info
|
|
for (index_in_transaction, account_info_index) in account_indices.into_iter() {
|
|
let account = invoke_context
|
|
.transaction_context
|
|
.get_account_at_index(index_in_transaction)
|
|
.borrow_mut();
|
|
let account_info = &account_infos[account_info_index];
|
|
**account_info.try_borrow_mut_lamports().unwrap() = account.lamports();
|
|
let mut data = account_info.try_borrow_mut_data()?;
|
|
let new_data = account.data();
|
|
if account_info.owner != account.owner() {
|
|
// TODO Figure out a better way to allow the System Program to set the account owner
|
|
#[allow(clippy::transmute_ptr_to_ptr)]
|
|
#[allow(mutable_transmutes)]
|
|
let account_info_mut =
|
|
unsafe { transmute::<&Pubkey, &mut Pubkey>(account_info.owner) };
|
|
*account_info_mut = *account.owner();
|
|
}
|
|
// TODO: Figure out how to allow the System Program to resize the account data
|
|
assert!(
|
|
data.len() == new_data.len(),
|
|
"Account data resizing not supported yet: {} -> {}. \
|
|
Consider making this test conditional on `#[cfg(feature = \"test-bpf\")]`",
|
|
data.len(),
|
|
new_data.len()
|
|
);
|
|
data.clone_from_slice(new_data);
|
|
}
|
|
|
|
stable_log::program_success(&log_collector, &instruction.program_id);
|
|
Ok(())
|
|
}
|
|
|
|
fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 {
|
|
get_sysvar::<Clock>(&clock::id(), var_addr)
|
|
}
|
|
|
|
fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 {
|
|
get_sysvar::<EpochSchedule>(&epoch_schedule::id(), var_addr)
|
|
}
|
|
|
|
#[allow(deprecated)]
|
|
fn sol_get_fees_sysvar(&self, var_addr: *mut u8) -> u64 {
|
|
get_sysvar::<Fees>(&fees::id(), var_addr)
|
|
}
|
|
|
|
fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 {
|
|
get_sysvar::<Rent>(&rent::id(), var_addr)
|
|
}
|
|
}
|
|
|
|
pub fn find_file(filename: &str) -> Option<PathBuf> {
|
|
for dir in default_shared_object_dirs() {
|
|
let candidate = dir.join(&filename);
|
|
if candidate.exists() {
|
|
return Some(candidate);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn default_shared_object_dirs() -> Vec<PathBuf> {
|
|
let mut search_path = vec![];
|
|
if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
|
|
search_path.push(PathBuf::from(bpf_out_dir));
|
|
}
|
|
search_path.push(PathBuf::from("tests/fixtures"));
|
|
if let Ok(dir) = std::env::current_dir() {
|
|
search_path.push(dir);
|
|
}
|
|
trace!("BPF .so search path: {:?}", search_path);
|
|
search_path
|
|
}
|
|
|
|
pub fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
|
|
let path = path.as_ref();
|
|
let mut file = File::open(path)
|
|
.unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err));
|
|
|
|
let mut file_data = Vec::new();
|
|
file.read_to_end(&mut file_data)
|
|
.unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err));
|
|
file_data
|
|
}
|
|
|
|
fn setup_fees(bank: Bank) -> Bank {
|
|
// Realistic fees part 1: Fake a single signature by calling
|
|
// `bank.commit_transactions()` so that the fee in the child bank will be
|
|
// initialized with a non-zero fee.
|
|
assert_eq!(bank.signature_count(), 0);
|
|
bank.commit_transactions(
|
|
&[], // transactions
|
|
&mut [], // loaded accounts
|
|
vec![], // transaction execution results
|
|
0, // tx count
|
|
1, // signature count
|
|
&mut ExecuteTimings::default(),
|
|
);
|
|
assert_eq!(bank.signature_count(), 1);
|
|
|
|
// Advance beyond slot 0 for a slightly more realistic test environment
|
|
let bank = Arc::new(bank);
|
|
let bank = Bank::new_from_parent(&bank, bank.collector_id(), bank.slot() + 1);
|
|
debug!("Bank slot: {}", bank.slot());
|
|
|
|
// Realistic fees part 2: Tick until a new blockhash is produced to pick up the
|
|
// non-zero fees
|
|
let last_blockhash = bank.last_blockhash();
|
|
while last_blockhash == bank.last_blockhash() {
|
|
bank.register_tick(&Hash::new_unique());
|
|
}
|
|
|
|
// Make sure a fee is now required
|
|
let lamports_per_signature = bank.get_lamports_per_signature();
|
|
assert_ne!(lamports_per_signature, 0);
|
|
|
|
bank
|
|
}
|
|
|
|
pub struct ProgramTest {
|
|
accounts: Vec<(Pubkey, AccountSharedData)>,
|
|
builtins: Vec<Builtin>,
|
|
compute_max_units: Option<u64>,
|
|
prefer_bpf: bool,
|
|
use_bpf_jit: bool,
|
|
}
|
|
|
|
impl Default for ProgramTest {
|
|
/// Initialize a new ProgramTest
|
|
///
|
|
/// If the `BPF_OUT_DIR` environment variable is defined, BPF programs will be preferred over
|
|
/// over a native instruction processor. The `ProgramTest::prefer_bpf()` method may be
|
|
/// used to override this preference at runtime. `cargo test-bpf` will set `BPF_OUT_DIR`
|
|
/// automatically.
|
|
///
|
|
/// BPF program shared objects and account data files are searched for in
|
|
/// * the value of the `BPF_OUT_DIR` environment variable
|
|
/// * the `tests/fixtures` sub-directory
|
|
/// * the current working directory
|
|
///
|
|
fn default() -> Self {
|
|
solana_logger::setup_with_default(
|
|
"solana_rbpf::vm=debug,\
|
|
solana_runtime::message_processor=debug,\
|
|
solana_runtime::system_instruction_processor=trace,\
|
|
solana_program_test=info",
|
|
);
|
|
let prefer_bpf = std::env::var("BPF_OUT_DIR").is_ok();
|
|
|
|
Self {
|
|
accounts: vec![],
|
|
builtins: vec![],
|
|
compute_max_units: None,
|
|
prefer_bpf,
|
|
use_bpf_jit: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ProgramTest {
|
|
/// Create a `ProgramTest`.
|
|
///
|
|
/// This is a wrapper around [`default`] and [`add_program`]. See their documentation for more
|
|
/// details.
|
|
///
|
|
/// [`default`]: #method.default
|
|
/// [`add_program`]: #method.add_program
|
|
pub fn new(
|
|
program_name: &str,
|
|
program_id: Pubkey,
|
|
process_instruction: Option<ProcessInstructionWithContext>,
|
|
) -> Self {
|
|
let mut me = Self::default();
|
|
me.add_program(program_name, program_id, process_instruction);
|
|
me
|
|
}
|
|
|
|
/// Override default BPF program selection
|
|
pub fn prefer_bpf(&mut self, prefer_bpf: bool) {
|
|
self.prefer_bpf = prefer_bpf;
|
|
}
|
|
|
|
/// Override the default maximum compute units
|
|
pub fn set_compute_max_units(&mut self, compute_max_units: u64) {
|
|
self.compute_max_units = Some(compute_max_units);
|
|
}
|
|
|
|
/// Override the BPF compute budget
|
|
#[allow(deprecated)]
|
|
#[deprecated(since = "1.8.0", note = "please use `set_compute_max_units` instead")]
|
|
pub fn set_bpf_compute_max_units(&mut self, bpf_compute_max_units: u64) {
|
|
self.compute_max_units = Some(bpf_compute_max_units);
|
|
}
|
|
|
|
/// Execute the BPF program with JIT if true, interpreted if false
|
|
pub fn use_bpf_jit(&mut self, use_bpf_jit: bool) {
|
|
self.use_bpf_jit = use_bpf_jit;
|
|
}
|
|
|
|
/// Add an account to the test environment
|
|
pub fn add_account(&mut self, address: Pubkey, account: Account) {
|
|
self.accounts
|
|
.push((address, AccountSharedData::from(account)));
|
|
}
|
|
|
|
/// Add an account to the test environment with the account data in the provided `filename`
|
|
pub fn add_account_with_file_data(
|
|
&mut self,
|
|
address: Pubkey,
|
|
lamports: u64,
|
|
owner: Pubkey,
|
|
filename: &str,
|
|
) {
|
|
self.add_account(
|
|
address,
|
|
Account {
|
|
lamports,
|
|
data: read_file(find_file(filename).unwrap_or_else(|| {
|
|
panic!("Unable to locate {}", filename);
|
|
})),
|
|
owner,
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Add an account to the test environment with the account data in the provided as a base 64
|
|
/// string
|
|
pub fn add_account_with_base64_data(
|
|
&mut self,
|
|
address: Pubkey,
|
|
lamports: u64,
|
|
owner: Pubkey,
|
|
data_base64: &str,
|
|
) {
|
|
self.add_account(
|
|
address,
|
|
Account {
|
|
lamports,
|
|
data: base64::decode(data_base64)
|
|
.unwrap_or_else(|err| panic!("Failed to base64 decode: {}", err)),
|
|
owner,
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Add a BPF program to the test environment.
|
|
///
|
|
/// `program_name` will also be used to locate the BPF shared object in the current or fixtures
|
|
/// directory.
|
|
///
|
|
/// If `process_instruction` is provided, the natively built-program may be used instead of the
|
|
/// BPF shared object depending on the `BPF_OUT_DIR` environment variable.
|
|
pub fn add_program(
|
|
&mut self,
|
|
program_name: &str,
|
|
program_id: Pubkey,
|
|
process_instruction: Option<ProcessInstructionWithContext>,
|
|
) {
|
|
let add_bpf = |this: &mut ProgramTest, program_file: PathBuf| {
|
|
let data = read_file(&program_file);
|
|
info!(
|
|
"\"{}\" BPF program from {}{}",
|
|
program_name,
|
|
program_file.display(),
|
|
std::fs::metadata(&program_file)
|
|
.map(|metadata| {
|
|
metadata
|
|
.modified()
|
|
.map(|time| {
|
|
format!(
|
|
", modified {}",
|
|
HumanTime::from(time)
|
|
.to_text_en(Accuracy::Precise, Tense::Past)
|
|
)
|
|
})
|
|
.ok()
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or_else(|| "".to_string())
|
|
);
|
|
|
|
this.add_account(
|
|
program_id,
|
|
Account {
|
|
lamports: Rent::default().minimum_balance(data.len()).min(1),
|
|
data,
|
|
owner: solana_sdk::bpf_loader::id(),
|
|
executable: true,
|
|
rent_epoch: 0,
|
|
},
|
|
);
|
|
};
|
|
|
|
let add_native = |this: &mut ProgramTest, process_fn: ProcessInstructionWithContext| {
|
|
info!("\"{}\" program loaded as native code", program_name);
|
|
this.builtins
|
|
.push(Builtin::new(program_name, program_id, process_fn));
|
|
};
|
|
|
|
let warn_invalid_program_name = || {
|
|
let valid_program_names = default_shared_object_dirs()
|
|
.iter()
|
|
.filter_map(|dir| dir.read_dir().ok())
|
|
.flat_map(|read_dir| {
|
|
read_dir.filter_map(|entry| {
|
|
let path = entry.ok()?.path();
|
|
if !path.is_file() {
|
|
return None;
|
|
}
|
|
match path.extension()?.to_str()? {
|
|
"so" => Some(path.file_stem()?.to_os_string()),
|
|
_ => None,
|
|
}
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if valid_program_names.is_empty() {
|
|
// This should be unreachable as `test-bpf` should guarantee at least one shared
|
|
// object exists somewhere.
|
|
warn!("No BPF shared objects found.");
|
|
return;
|
|
}
|
|
|
|
warn!(
|
|
"Possible bogus program name. Ensure the program name ({}) \
|
|
matches one of the following recognizable program names:",
|
|
program_name,
|
|
);
|
|
for name in valid_program_names {
|
|
warn!(" - {}", name.to_str().unwrap());
|
|
}
|
|
};
|
|
|
|
let program_file = find_file(&format!("{}.so", program_name));
|
|
match (self.prefer_bpf, program_file, process_instruction) {
|
|
// If BPF is preferred (i.e., `test-bpf` is invoked) and a BPF shared object exists,
|
|
// use that as the program data.
|
|
(true, Some(file), _) => add_bpf(self, file),
|
|
|
|
// If BPF is not required (i.e., we were invoked with `test`), use the provided
|
|
// processor function as is.
|
|
//
|
|
// TODO: figure out why tests hang if a processor panics when running native code.
|
|
(false, _, Some(process)) => add_native(self, process),
|
|
|
|
// Invalid: `test-bpf` invocation with no matching BPF shared object.
|
|
(true, None, _) => {
|
|
warn_invalid_program_name();
|
|
panic!(
|
|
"Program file data not available for {} ({})",
|
|
program_name, program_id
|
|
);
|
|
}
|
|
|
|
// Invalid: regular `test` invocation without a processor.
|
|
(false, _, None) => {
|
|
panic!(
|
|
"Program processor not available for {} ({})",
|
|
program_name, program_id
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add a builtin program to the test environment.
|
|
///
|
|
/// Note that builtin programs are responsible for their own `stable_log` output.
|
|
pub fn add_builtin_program(
|
|
&mut self,
|
|
program_name: &str,
|
|
program_id: Pubkey,
|
|
process_instruction: ProcessInstructionWithContext,
|
|
) {
|
|
info!("\"{}\" builtin program", program_name);
|
|
self.builtins
|
|
.push(Builtin::new(program_name, program_id, process_instruction));
|
|
}
|
|
|
|
fn setup_bank(
|
|
&self,
|
|
) -> (
|
|
Arc<RwLock<BankForks>>,
|
|
Arc<RwLock<BlockCommitmentCache>>,
|
|
Hash,
|
|
GenesisConfigInfo,
|
|
) {
|
|
{
|
|
use std::sync::Once;
|
|
static ONCE: Once = Once::new();
|
|
|
|
ONCE.call_once(|| {
|
|
solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {}));
|
|
});
|
|
}
|
|
|
|
let rent = Rent::default();
|
|
let fee_rate_governor = FeeRateGovernor::default();
|
|
let bootstrap_validator_pubkey = Pubkey::new_unique();
|
|
let bootstrap_validator_stake_lamports =
|
|
rent.minimum_balance(VoteState::size_of()) + sol_to_lamports(1_000_000.0);
|
|
|
|
let mint_keypair = Keypair::new();
|
|
let voting_keypair = Keypair::new();
|
|
|
|
let mut genesis_config = create_genesis_config_with_leader_ex(
|
|
sol_to_lamports(1_000_000.0),
|
|
&mint_keypair.pubkey(),
|
|
&bootstrap_validator_pubkey,
|
|
&voting_keypair.pubkey(),
|
|
&Pubkey::new_unique(),
|
|
bootstrap_validator_stake_lamports,
|
|
42,
|
|
fee_rate_governor,
|
|
rent,
|
|
ClusterType::Development,
|
|
vec![],
|
|
);
|
|
let target_tick_duration = Duration::from_micros(100);
|
|
genesis_config.poh_config = PohConfig::new_sleep(target_tick_duration);
|
|
debug!("Payer address: {}", mint_keypair.pubkey());
|
|
debug!("Genesis config: {}", genesis_config);
|
|
|
|
let mut bank = Bank::new_for_tests(&genesis_config);
|
|
|
|
// Add loaders
|
|
macro_rules! add_builtin {
|
|
($b:expr) => {
|
|
bank.add_builtin(&$b.0, &$b.1, $b.2)
|
|
};
|
|
}
|
|
add_builtin!(solana_bpf_loader_deprecated_program!());
|
|
if self.use_bpf_jit {
|
|
add_builtin!(solana_bpf_loader_program_with_jit!());
|
|
add_builtin!(solana_bpf_loader_upgradeable_program_with_jit!());
|
|
} else {
|
|
add_builtin!(solana_bpf_loader_program!());
|
|
add_builtin!(solana_bpf_loader_upgradeable_program!());
|
|
}
|
|
|
|
// Add commonly-used SPL programs as a convenience to the user
|
|
for (program_id, account) in programs::spl_programs(&Rent::default()).iter() {
|
|
bank.store_account(program_id, account);
|
|
}
|
|
|
|
// User-supplied additional builtins
|
|
for builtin in self.builtins.iter() {
|
|
bank.add_builtin(
|
|
&builtin.name,
|
|
&builtin.id,
|
|
builtin.process_instruction_with_context,
|
|
);
|
|
}
|
|
|
|
for (address, account) in self.accounts.iter() {
|
|
if bank.get_account(address).is_some() {
|
|
info!("Overriding account at {}", address);
|
|
}
|
|
bank.store_account(address, account);
|
|
}
|
|
bank.set_capitalization();
|
|
if let Some(max_units) = self.compute_max_units {
|
|
bank.set_compute_budget(Some(ComputeBudget {
|
|
max_units,
|
|
..ComputeBudget::default()
|
|
}));
|
|
}
|
|
let bank = setup_fees(bank);
|
|
let slot = bank.slot();
|
|
let last_blockhash = bank.last_blockhash();
|
|
let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
|
|
let block_commitment_cache = Arc::new(RwLock::new(
|
|
BlockCommitmentCache::new_for_tests_with_slots(slot, slot),
|
|
));
|
|
|
|
(
|
|
bank_forks,
|
|
block_commitment_cache,
|
|
last_blockhash,
|
|
GenesisConfigInfo {
|
|
genesis_config,
|
|
mint_keypair,
|
|
voting_keypair,
|
|
validator_pubkey: bootstrap_validator_pubkey,
|
|
},
|
|
)
|
|
}
|
|
|
|
pub async fn start(self) -> (BanksClient, Keypair, Hash) {
|
|
let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
|
|
let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
|
|
let transport = start_local_server(
|
|
bank_forks.clone(),
|
|
block_commitment_cache.clone(),
|
|
target_tick_duration,
|
|
)
|
|
.await;
|
|
let banks_client = start_client(transport)
|
|
.await
|
|
.unwrap_or_else(|err| panic!("Failed to start banks client: {}", err));
|
|
|
|
// Run a simulated PohService to provide the client with new blockhashes. New blockhashes
|
|
// are required when sending multiple otherwise identical transactions in series from a
|
|
// test
|
|
tokio::spawn(async move {
|
|
loop {
|
|
bank_forks
|
|
.read()
|
|
.unwrap()
|
|
.working_bank()
|
|
.register_tick(&Hash::new_unique());
|
|
tokio::time::sleep(target_tick_duration).await;
|
|
}
|
|
});
|
|
|
|
(banks_client, gci.mint_keypair, last_blockhash)
|
|
}
|
|
|
|
/// Start the test client
|
|
///
|
|
/// Returns a `BanksClient` interface into the test environment as well as a payer `Keypair`
|
|
/// with SOL for sending transactions
|
|
pub async fn start_with_context(self) -> ProgramTestContext {
|
|
let (bank_forks, block_commitment_cache, last_blockhash, gci) = self.setup_bank();
|
|
let target_tick_duration = gci.genesis_config.poh_config.target_tick_duration;
|
|
let transport = start_local_server(
|
|
bank_forks.clone(),
|
|
block_commitment_cache.clone(),
|
|
target_tick_duration,
|
|
)
|
|
.await;
|
|
let banks_client = start_client(transport)
|
|
.await
|
|
.unwrap_or_else(|err| panic!("Failed to start banks client: {}", err));
|
|
|
|
ProgramTestContext::new(
|
|
bank_forks,
|
|
block_commitment_cache,
|
|
banks_client,
|
|
last_blockhash,
|
|
gci,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait ProgramTestBanksClientExt {
|
|
/// Get a new blockhash, similar in spirit to RpcClient::get_new_blockhash()
|
|
///
|
|
/// This probably should eventually be moved into BanksClient proper in some form
|
|
#[deprecated(
|
|
since = "1.9.0",
|
|
note = "Please use `get_new_latest_blockhash `instead"
|
|
)]
|
|
async fn get_new_blockhash(&mut self, blockhash: &Hash) -> io::Result<(Hash, FeeCalculator)>;
|
|
/// Get a new latest blockhash, similar in spirit to RpcClient::get_latest_blockhash()
|
|
async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash>;
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ProgramTestBanksClientExt for BanksClient {
|
|
async fn get_new_blockhash(&mut self, blockhash: &Hash) -> io::Result<(Hash, FeeCalculator)> {
|
|
let mut num_retries = 0;
|
|
let start = Instant::now();
|
|
while start.elapsed().as_secs() < 5 {
|
|
#[allow(deprecated)]
|
|
if let Ok((fee_calculator, new_blockhash, _slot)) = self.get_fees().await {
|
|
if new_blockhash != *blockhash {
|
|
return Ok((new_blockhash, fee_calculator));
|
|
}
|
|
}
|
|
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
|
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
num_retries += 1;
|
|
}
|
|
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
format!(
|
|
"Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
|
|
start.elapsed().as_millis(),
|
|
num_retries,
|
|
blockhash
|
|
),
|
|
))
|
|
}
|
|
|
|
async fn get_new_latest_blockhash(&mut self, blockhash: &Hash) -> io::Result<Hash> {
|
|
let mut num_retries = 0;
|
|
let start = Instant::now();
|
|
while start.elapsed().as_secs() < 5 {
|
|
let new_blockhash = self.get_latest_blockhash().await?;
|
|
if new_blockhash != *blockhash {
|
|
return Ok(new_blockhash);
|
|
}
|
|
debug!("Got same blockhash ({:?}), will retry...", blockhash);
|
|
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
num_retries += 1;
|
|
}
|
|
|
|
Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
format!(
|
|
"Unable to get new blockhash after {}ms (retried {} times), stuck at {}",
|
|
start.elapsed().as_millis(),
|
|
num_retries,
|
|
blockhash
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
struct DroppableTask<T>(Arc<AtomicBool>, JoinHandle<T>);
|
|
|
|
impl<T> Drop for DroppableTask<T> {
|
|
fn drop(&mut self) {
|
|
self.0.store(true, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
pub struct ProgramTestContext {
|
|
pub banks_client: BanksClient,
|
|
pub last_blockhash: Hash,
|
|
pub payer: Keypair,
|
|
genesis_config: GenesisConfig,
|
|
bank_forks: Arc<RwLock<BankForks>>,
|
|
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
|
_bank_task: DroppableTask<()>,
|
|
}
|
|
|
|
impl ProgramTestContext {
|
|
fn new(
|
|
bank_forks: Arc<RwLock<BankForks>>,
|
|
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
|
|
banks_client: BanksClient,
|
|
last_blockhash: Hash,
|
|
genesis_config_info: GenesisConfigInfo,
|
|
) -> Self {
|
|
// Run a simulated PohService to provide the client with new blockhashes. New blockhashes
|
|
// are required when sending multiple otherwise identical transactions in series from a
|
|
// test
|
|
let running_bank_forks = bank_forks.clone();
|
|
let target_tick_duration = genesis_config_info
|
|
.genesis_config
|
|
.poh_config
|
|
.target_tick_duration;
|
|
let exit = Arc::new(AtomicBool::new(false));
|
|
let bank_task = DroppableTask(
|
|
exit.clone(),
|
|
tokio::spawn(async move {
|
|
loop {
|
|
if exit.load(Ordering::Relaxed) {
|
|
break;
|
|
}
|
|
running_bank_forks
|
|
.read()
|
|
.unwrap()
|
|
.working_bank()
|
|
.register_tick(&Hash::new_unique());
|
|
tokio::time::sleep(target_tick_duration).await;
|
|
}
|
|
}),
|
|
);
|
|
|
|
Self {
|
|
banks_client,
|
|
last_blockhash,
|
|
payer: genesis_config_info.mint_keypair,
|
|
genesis_config: genesis_config_info.genesis_config,
|
|
bank_forks,
|
|
block_commitment_cache,
|
|
_bank_task: bank_task,
|
|
}
|
|
}
|
|
|
|
pub fn genesis_config(&self) -> &GenesisConfig {
|
|
&self.genesis_config
|
|
}
|
|
|
|
/// Manually increment vote credits for the current epoch in the specified vote account to simulate validator voting activity
|
|
pub fn increment_vote_account_credits(
|
|
&mut self,
|
|
vote_account_address: &Pubkey,
|
|
number_of_credits: u64,
|
|
) {
|
|
let bank_forks = self.bank_forks.read().unwrap();
|
|
let bank = bank_forks.working_bank();
|
|
|
|
// generate some vote activity for rewards
|
|
let mut vote_account = bank.get_account(vote_account_address).unwrap();
|
|
let mut vote_state = VoteState::from(&vote_account).unwrap();
|
|
|
|
let epoch = bank.epoch();
|
|
for _ in 0..number_of_credits {
|
|
vote_state.increment_credits(epoch);
|
|
}
|
|
let versioned = VoteStateVersions::new_current(vote_state);
|
|
VoteState::to(&versioned, &mut vote_account).unwrap();
|
|
bank.store_account(vote_account_address, &vote_account);
|
|
}
|
|
|
|
/// Create or overwrite an account, subverting normal runtime checks.
|
|
///
|
|
/// This method exists to make it easier to set up artificial situations
|
|
/// that would be difficult to replicate by sending individual transactions.
|
|
/// Beware that it can be used to create states that would not be reachable
|
|
/// by sending transactions!
|
|
pub fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) {
|
|
let bank_forks = self.bank_forks.read().unwrap();
|
|
let bank = bank_forks.working_bank();
|
|
bank.store_account(address, account);
|
|
}
|
|
|
|
/// Create or overwrite a sysvar, subverting normal runtime checks.
|
|
///
|
|
/// This method exists to make it easier to set up artificial situations
|
|
/// that would be difficult to replicate on a new test cluster. Beware
|
|
/// that it can be used to create states that would not be reachable
|
|
/// under normal conditions!
|
|
pub fn set_sysvar<T: SysvarId + Sysvar>(&self, sysvar: &T) {
|
|
let bank_forks = self.bank_forks.read().unwrap();
|
|
let bank = bank_forks.working_bank();
|
|
bank.set_sysvar_for_tests(sysvar);
|
|
}
|
|
|
|
/// Force the working bank ahead to a new slot
|
|
pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> {
|
|
let mut bank_forks = self.bank_forks.write().unwrap();
|
|
let bank = bank_forks.working_bank();
|
|
|
|
// Force ticks until a new blockhash, otherwise retried transactions will have
|
|
// the same signature
|
|
let last_blockhash = bank.last_blockhash();
|
|
while last_blockhash == bank.last_blockhash() {
|
|
bank.register_tick(&Hash::new_unique());
|
|
}
|
|
|
|
// warp ahead to one slot *before* the desired slot because the warped
|
|
// bank is frozen
|
|
let working_slot = bank.slot();
|
|
if warp_slot <= working_slot {
|
|
return Err(ProgramTestError::InvalidWarpSlot);
|
|
}
|
|
|
|
let pre_warp_slot = warp_slot - 1;
|
|
let warp_bank = bank_forks.insert(Bank::warp_from_parent(
|
|
&bank,
|
|
&Pubkey::default(),
|
|
pre_warp_slot,
|
|
));
|
|
bank_forks.set_root(
|
|
pre_warp_slot,
|
|
&solana_runtime::accounts_background_service::AbsRequestSender::default(),
|
|
Some(pre_warp_slot),
|
|
);
|
|
|
|
// warp bank is frozen, so go forward one slot from it
|
|
bank_forks.insert(Bank::new_from_parent(
|
|
&warp_bank,
|
|
&Pubkey::default(),
|
|
warp_slot,
|
|
));
|
|
|
|
// Update block commitment cache, otherwise banks server will poll at
|
|
// the wrong slot
|
|
let mut w_block_commitment_cache = self.block_commitment_cache.write().unwrap();
|
|
// HACK: The root set here should be `pre_warp_slot`, but since we're
|
|
// in a testing environment, the root bank never updates after a warp.
|
|
// The ticking thread only updates the working bank, and never the root
|
|
// bank.
|
|
w_block_commitment_cache.set_all_slots(warp_slot, warp_slot);
|
|
|
|
let bank = bank_forks.working_bank();
|
|
self.last_blockhash = bank.last_blockhash();
|
|
Ok(())
|
|
}
|
|
}
|