#![cfg(test)]
#![allow(clippy::arithmetic_side_effects)]
use {
crate::mock_bank::{
EXECUTION_EPOCH, EXECUTION_SLOT, MockBankCallback, MockForkGraph, WALLCLOCK_TIME,
create_custom_loader, deploy_program_with_upgrade_authority, load_program, program_address,
program_data_size, register_builtins,
},
solana_account::{AccountSharedData, ReadableAccount, WritableAccount},
solana_clock::Slot,
solana_compute_budget::compute_budget_limits::ComputeBudgetLimits,
solana_compute_budget_interface::ComputeBudgetInstruction,
solana_fee_structure::FeeDetails,
solana_hash::Hash,
solana_instruction::{AccountMeta, Instruction},
solana_keypair::Keypair,
solana_loader_v3_interface::{
get_program_data_address, instruction as loaderv3_instruction,
state::UpgradeableLoaderState,
},
solana_native_token::LAMPORTS_PER_SOL,
solana_nonce::{self as nonce, state::DurableNonce},
solana_program_entrypoint::MAX_PERMITTED_DATA_INCREASE,
solana_program_runtime::{
execution_budget::{
MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, SVMTransactionExecutionAndFeeBudgetLimits,
},
loaded_programs::ProgramRuntimeEnvironments,
},
solana_pubkey::Pubkey,
solana_sdk_ids::{
bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, compute_budget, native_loader,
},
solana_signer::Signer,
solana_svm::{
account_loader::{
CheckedTransactionDetails, TRANSACTION_ACCOUNT_BASE_SIZE, TransactionCheckResult,
},
nonce_info::NonceInfo,
transaction_execution_result::TransactionExecutionDetails,
transaction_processing_result::{
ProcessedTransaction, TransactionProcessingResult,
TransactionProcessingResultExtensions,
},
transaction_processor::{
ExecutionRecordingConfig, LoadAndExecuteSanitizedTransactionsOutput,
TransactionBatchProcessor, TransactionProcessingConfig,
TransactionProcessingEnvironment,
},
},
solana_svm_feature_set::SVMFeatureSet,
solana_svm_transaction::{
instruction::SVMInstruction,
svm_message::{SVMMessage, SVMStaticMessage},
},
solana_svm_type_overrides::sync::{Arc, RwLock},
solana_system_interface::{instruction as system_instruction, program as system_program},
solana_system_transaction as system_transaction,
solana_sysvar::rent::Rent,
solana_transaction::{Transaction, sanitized::SanitizedTransaction},
solana_transaction_context::transaction::TransactionReturnData,
solana_transaction_error::TransactionError,
std::{collections::HashMap, num::NonZeroU32, slice, sync::atomic::Ordering},
test_case::test_case,
};
mod mock_bank;
fn process_test_compute_budget_instructions<'a>(
instructions: impl Iterator<Item = (&'a Pubkey, SVMInstruction<'a>)> + Clone,
) -> Result<ComputeBudgetLimits, TransactionError> {
let mut loaded_accounts_data_size_limit = None;
for (program_id, instruction) in instructions {
if *program_id == compute_budget::id()
&& instruction.data.len() >= 5
&& instruction.data[0] == 4
{
let size = u32::from_le_bytes([
instruction.data[1],
instruction.data[2],
instruction.data[3],
instruction.data[4],
]);
loaded_accounts_data_size_limit = Some(size);
}
}
let loaded_accounts_bytes =
if let Some(requested_loaded_accounts_data_size_limit) = loaded_accounts_data_size_limit {
NonZeroU32::new(requested_loaded_accounts_data_size_limit)
.ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)?
} else {
MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES
}
.min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES);
Ok(ComputeBudgetLimits {
loaded_accounts_bytes,
..Default::default()
})
}
const DEPLOYMENT_SLOT: u64 = 0;
const LAMPORTS_PER_SIGNATURE: u64 = 5000;
const LAST_BLOCKHASH: Hash = Hash::new_from_array([7; 32]);
pub type AccountsMap = HashMap<Pubkey, AccountSharedData>;
pub struct SvmTestEnvironment<'a> {
pub mock_bank: MockBankCallback,
pub fork_graph: Arc<RwLock<MockForkGraph>>,
pub batch_processor: TransactionBatchProcessor<MockForkGraph>,
pub processing_config: TransactionProcessingConfig<'a>,
pub processing_environment: TransactionProcessingEnvironment,
pub test_entry: SvmTestEntry,
}
impl SvmTestEnvironment<'_> {
pub fn create(test_entry: SvmTestEntry) -> Self {
let mock_bank = MockBankCallback::default();
for (name, slot, authority) in &test_entry.initial_programs {
deploy_program_with_upgrade_authority(name.to_string(), *slot, &mock_bank, *authority);
}
for (pubkey, account) in &test_entry.initial_accounts {
mock_bank
.account_shared_data
.write()
.unwrap()
.insert(*pubkey, account.clone());
}
let fork_graph = Arc::new(RwLock::new(MockForkGraph {}));
let batch_processor = TransactionBatchProcessor::new(
EXECUTION_SLOT,
EXECUTION_EPOCH,
Arc::downgrade(&fork_graph),
Some(create_custom_loader()),
);
mock_bank.configure_sysvars();
batch_processor.fill_missing_sysvar_cache_entries(&mock_bank);
register_builtins(&mock_bank, &batch_processor);
let processing_config = TransactionProcessingConfig {
recording_config: ExecutionRecordingConfig {
enable_log_recording: true,
enable_return_data_recording: true,
enable_cpi_recording: false,
enable_transaction_balance_recording: false,
},
drop_on_failure: test_entry.drop_on_failure,
all_or_nothing: test_entry.all_or_nothing,
..Default::default()
};
let processing_environment = TransactionProcessingEnvironment {
blockhash: LAST_BLOCKHASH,
blockhash_lamports_per_signature: LAMPORTS_PER_SIGNATURE,
alpenglow_migration_succeeded: false,
epoch_total_stake: 0,
feature_set: test_entry.feature_set,
program_runtime_environments: ProgramRuntimeEnvironments::new(
batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH),
batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH),
),
rent: test_entry.rent.clone(),
};
Self {
mock_bank,
fork_graph,
batch_processor,
processing_config,
processing_environment,
test_entry,
}
}
pub fn execute(&self) -> LoadAndExecuteSanitizedTransactionsOutput {
let (transactions, check_results) = self.test_entry.prepare_transactions();
let batch_output = self
.batch_processor
.load_and_execute_sanitized_transactions(
&self.mock_bank,
&transactions,
check_results,
&self.processing_environment,
&self.processing_config,
);
let mut final_accounts_actual = self.test_entry.initial_accounts.clone();
let update_or_dealloc_account =
|final_accounts: &mut AccountsMap, pubkey, account: AccountSharedData| {
if account.lamports() == 0 {
final_accounts.insert(pubkey, AccountSharedData::default());
} else {
final_accounts.insert(pubkey, account);
}
};
for (tx_index, processed_transaction) in batch_output.processing_results.iter().enumerate()
{
let sanitized_transaction = &transactions[tx_index];
match processed_transaction {
Ok(ProcessedTransaction::Executed(executed_transaction)) => {
if executed_transaction.was_successful() {
for (index, (pubkey, account_data)) in executed_transaction
.loaded_transaction
.accounts
.iter()
.enumerate()
{
if sanitized_transaction.is_writable(index) {
update_or_dealloc_account(
&mut final_accounts_actual,
*pubkey,
account_data.clone(),
);
}
}
} else {
for (pubkey, account_data) in
&executed_transaction.loaded_transaction.rollback_accounts
{
update_or_dealloc_account(
&mut final_accounts_actual,
*pubkey,
account_data.clone(),
);
}
}
}
Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => {
for (pubkey, account_data) in &fees_only_transaction.rollback_accounts {
update_or_dealloc_account(
&mut final_accounts_actual,
*pubkey,
account_data.clone(),
);
}
}
Err(_) => {}
}
}
let (actual_statuses, expected_statuses): (Vec<_>, Vec<_>) = batch_output
.processing_results
.iter()
.zip(self.test_entry.asserts())
.map(|(processing_result, test_item_assert)| {
(
ExecutionStatus::from(processing_result),
test_item_assert.status,
)
})
.unzip();
assert_eq!(
expected_statuses,
actual_statuses,
"mismatch between expected and actual statuses. execution details:\n{}",
batch_output
.processing_results
.iter()
.enumerate()
.map(|(i, tx)| match tx {
Ok(ProcessedTransaction::Executed(executed)) => {
format!("{} (executed): {:#?}", i, executed.execution_details)
}
Ok(ProcessedTransaction::FeesOnly(fee_only)) => {
format!("{} (fee-only): {:?}", i, fee_only.load_error)
}
Err(e) => format!("{i} (discarded): {e:?}"),
})
.collect::<Vec<_>>()
.join("\n"),
);
for (pubkey, expected_account_data) in self.test_entry.final_accounts.iter() {
let actual_account_data = final_accounts_actual.get(pubkey);
assert_eq!(
Some(expected_account_data),
actual_account_data,
"mismatch on account {pubkey}"
);
}
for (processing_result, test_item_asserts) in batch_output
.processing_results
.iter()
.zip(self.test_entry.asserts())
{
match processing_result {
Ok(ProcessedTransaction::Executed(executed_transaction)) => test_item_asserts
.check_executed_transaction(&executed_transaction.execution_details),
Ok(ProcessedTransaction::FeesOnly(_)) => {
assert!(test_item_asserts.processed());
assert!(!test_item_asserts.executed());
}
Err(_) => assert!(test_item_asserts.discarded()),
}
}
let mut mock_bank_accounts = self.mock_bank.account_shared_data.write().unwrap();
mock_bank_accounts.extend(final_accounts_actual);
for processing_result in batch_output.processing_results.iter() {
if let Some(ProcessedTransaction::Executed(executed_tx)) =
processing_result.processed_transaction()
{
let programs_modified_by_tx = &executed_tx.programs_modified_by_tx;
if executed_tx.was_successful() && !programs_modified_by_tx.is_empty() {
self.batch_processor
.global_program_cache
.write()
.unwrap()
.merge(
&self.batch_processor.program_runtime_environment,
self.batch_processor.slot,
programs_modified_by_tx,
);
}
}
}
batch_output
}
pub fn is_program_blocked(&self, program_id: &Pubkey) -> bool {
let (_, program_cache_entry) = self
.batch_processor
.global_program_cache
.read()
.unwrap()
.get_flattened_entries_for_tests()
.into_iter()
.rev()
.find(|(key, _)| key == program_id)
.unwrap();
program_cache_entry.effective_slot > EXECUTION_SLOT || program_cache_entry.is_tombstone()
}
}
#[derive(Clone)]
pub struct SvmTestEntry {
pub feature_set: SVMFeatureSet,
pub drop_on_failure: bool,
pub all_or_nothing: bool,
pub initial_programs: Vec<(String, Slot, Option<Pubkey>)>,
pub initial_accounts: AccountsMap,
pub transaction_batch: Vec<TransactionBatchItem>,
pub final_accounts: AccountsMap,
pub rent: Rent,
}
impl Default for SvmTestEntry {
fn default() -> Self {
Self {
feature_set: SVMFeatureSet::all_enabled(),
all_or_nothing: false,
drop_on_failure: false,
initial_programs: Vec::new(),
initial_accounts: HashMap::new(),
transaction_batch: Vec::new(),
final_accounts: HashMap::new(),
rent: Rent::default(),
}
}
}
impl SvmTestEntry {
pub fn set_rent_params(&mut self, rent: Rent) {
self.rent = rent;
}
pub fn add_initial_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) {
assert!(
self.initial_accounts
.insert(pubkey, account.clone())
.is_none()
);
self.create_expected_account(pubkey, account);
}
pub fn add_initial_program(&mut self, program_name: &str) {
self.initial_programs
.push((program_name.to_string(), DEPLOYMENT_SLOT, None));
}
pub fn create_expected_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) {
let mut account = account.clone();
account.set_rent_epoch(u64::MAX);
assert!(self.final_accounts.insert(pubkey, account).is_none());
}
pub fn update_expected_account_data(&mut self, pubkey: Pubkey, account: &AccountSharedData) {
let mut account = account.clone();
account.set_rent_epoch(u64::MAX);
assert!(self.final_accounts.insert(pubkey, account).is_some());
}
pub fn drop_expected_account(&mut self, pubkey: Pubkey) {
assert!(
self.final_accounts
.insert(pubkey, AccountSharedData::default())
.is_some()
);
}
pub fn increase_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) {
self.final_accounts
.get_mut(pubkey)
.unwrap()
.checked_add_lamports(lamports)
.unwrap();
}
pub fn decrease_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) {
self.final_accounts
.get_mut(pubkey)
.unwrap()
.checked_sub_lamports(lamports)
.unwrap();
}
pub fn push_transaction(&mut self, transaction: Transaction) {
self.push_transaction_with_status(transaction, ExecutionStatus::Succeeded)
}
pub fn push_transaction_with_status(
&mut self,
transaction: Transaction,
status: ExecutionStatus,
) {
self.transaction_batch.push(TransactionBatchItem {
transaction,
asserts: TransactionBatchItemAsserts {
status,
..TransactionBatchItemAsserts::default()
},
..TransactionBatchItem::default()
});
}
pub fn push_nonce_transaction(&mut self, transaction: Transaction, nonce_address: Pubkey) {
self.push_nonce_transaction_with_status(
transaction,
nonce_address,
ExecutionStatus::Succeeded,
)
}
pub fn push_nonce_transaction_with_status(
&mut self,
transaction: Transaction,
nonce_address: Pubkey,
status: ExecutionStatus,
) {
self.transaction_batch.push(TransactionBatchItem {
transaction,
asserts: TransactionBatchItemAsserts {
status,
..TransactionBatchItemAsserts::default()
},
..TransactionBatchItem::with_nonce(nonce_address)
});
}
fn prepare_transactions(&self) -> (Vec<SanitizedTransaction>, Vec<TransactionCheckResult>) {
self.transaction_batch
.iter()
.cloned()
.map(|item| {
let message = SanitizedTransaction::from_transaction_for_tests(item.transaction);
let check_result = item.check_result.map(|tx_details| {
let compute_budget_limits = process_test_compute_budget_instructions(
SVMStaticMessage::program_instructions_iter(&message),
);
let signature_count = message
.num_transaction_signatures()
.saturating_add(message.num_ed25519_signatures())
.saturating_add(message.num_secp256k1_signatures())
.saturating_add(message.num_secp256r1_signatures());
let compute_budget = compute_budget_limits
.map(|v| {
v.get_compute_budget_and_limits(
v.loaded_accounts_bytes,
FeeDetails::new(
signature_count.saturating_mul(LAMPORTS_PER_SIGNATURE),
v.get_prioritization_fee(),
),
self.feature_set.raise_cpi_nesting_limit_to_8,
)
})
.unwrap();
CheckedTransactionDetails::new(tx_details.nonce_address, compute_budget)
});
(message, check_result)
})
.unzip()
}
fn asserts(&self) -> Vec<TransactionBatchItemAsserts> {
self.transaction_batch
.iter()
.cloned()
.map(|item| item.asserts)
.collect()
}
}
#[derive(Clone, Debug)]
pub struct TransactionBatchItem {
pub transaction: Transaction,
pub check_result: TransactionCheckResult,
pub asserts: TransactionBatchItemAsserts,
}
impl TransactionBatchItem {
fn with_nonce(nonce_address: Pubkey) -> Self {
Self {
check_result: Ok(CheckedTransactionDetails::new(
Some(nonce_address),
SVMTransactionExecutionAndFeeBudgetLimits::default(),
)),
..Self::default()
}
}
}
impl Default for TransactionBatchItem {
fn default() -> Self {
Self {
transaction: Transaction::default(),
check_result: Ok(CheckedTransactionDetails::new(
None,
SVMTransactionExecutionAndFeeBudgetLimits::default(),
)),
asserts: TransactionBatchItemAsserts::default(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TransactionBatchItemAsserts {
pub status: ExecutionStatus,
pub logs: Vec<String>,
pub return_data: ReturnDataAssert,
}
impl TransactionBatchItemAsserts {
pub fn succeeded(&self) -> bool {
self.status.succeeded()
}
pub fn executed(&self) -> bool {
self.status.executed()
}
pub fn processed(&self) -> bool {
self.status.processed()
}
pub fn discarded(&self) -> bool {
self.status.discarded()
}
pub fn check_executed_transaction(&self, execution_details: &TransactionExecutionDetails) {
assert!(self.executed());
assert_eq!(self.succeeded(), execution_details.status.is_ok());
if !self.logs.is_empty() {
let actual_logs = execution_details.log_messages.as_ref().unwrap();
for expected_log in &self.logs {
assert!(actual_logs.contains(expected_log));
}
}
if self.return_data != ReturnDataAssert::Skip {
assert_eq!(
self.return_data,
execution_details.return_data.clone().into()
);
}
}
}
impl From<ExecutionStatus> for TransactionBatchItemAsserts {
fn from(status: ExecutionStatus) -> Self {
Self {
status,
..Self::default()
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExecutionStatus {
Discarded,
ProcessedFailed,
ExecutedFailed,
#[default]
Succeeded,
}
impl ExecutionStatus {
pub fn succeeded(self) -> bool {
self == Self::Succeeded
}
pub fn executed(self) -> bool {
self > Self::ProcessedFailed
}
pub fn processed(self) -> bool {
self != Self::Discarded
}
pub fn discarded(self) -> bool {
self == Self::Discarded
}
}
impl From<&TransactionProcessingResult> for ExecutionStatus {
fn from(processing_result: &TransactionProcessingResult) -> Self {
match processing_result {
Ok(ProcessedTransaction::Executed(executed_transaction)) => {
if executed_transaction.execution_details.status.is_ok() {
ExecutionStatus::Succeeded
} else {
ExecutionStatus::ExecutedFailed
}
}
Ok(ProcessedTransaction::FeesOnly(_)) => ExecutionStatus::ProcessedFailed,
Err(_) => ExecutionStatus::Discarded,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum ReturnDataAssert {
Some(TransactionReturnData),
None,
#[default]
Skip,
}
impl From<Option<TransactionReturnData>> for ReturnDataAssert {
fn from(option_ro_data: Option<TransactionReturnData>) -> Self {
match option_ro_data {
Some(ro_data) => Self::Some(ro_data),
None => Self::None,
}
}
}
fn program_medley(drop_on_failure: bool) -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry {
drop_on_failure,
..Default::default()
};
{
let program_name = "hello-solana";
let program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
));
test_entry.transaction_batch[0]
.asserts
.logs
.push("Program log: Hello, Solana!".to_string());
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
}
{
let program_name = "simple-transfer";
let program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let sender_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let sender = sender_keypair.pubkey();
let recipient = Pubkey::new_unique();
let transfer_amount = 10;
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let mut sender_data = AccountSharedData::default();
sender_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(sender, &sender_data);
let mut recipient_data = AccountSharedData::default();
recipient_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(recipient, &recipient_data);
let instruction = Instruction::new_with_bytes(
program_id,
&u64::to_be_bytes(transfer_amount),
vec![
AccountMeta::new(sender, true),
AccountMeta::new(recipient, false),
AccountMeta::new_readonly(system_program::id(), false),
],
);
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[instruction],
Some(&fee_payer),
&[&fee_payer_keypair, &sender_keypair],
Hash::default(),
));
test_entry.increase_expected_lamports(&recipient, transfer_amount);
test_entry.decrease_expected_lamports(&sender, transfer_amount);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
}
{
let program_name = "clock-sysvar";
let program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
));
let ro_data = TransactionReturnData {
program_id,
data: i64::to_be_bytes(WALLCLOCK_TIME).to_vec(),
};
test_entry.transaction_batch[2].asserts.return_data = ReturnDataAssert::Some(ro_data);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
}
{
let program_id = program_address("simple-transfer");
let fee_payer_keypair = Keypair::new();
let sender_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let sender = sender_keypair.pubkey();
let recipient = Pubkey::new_unique();
let base_amount = 900_000;
let transfer_amount = base_amount + 50;
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
if drop_on_failure {
test_entry.final_accounts.insert(fee_payer, fee_payer_data);
}
let mut sender_data = AccountSharedData::default();
sender_data.set_lamports(base_amount);
sender_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(sender, &sender_data);
if drop_on_failure {
test_entry.final_accounts.insert(sender, sender_data);
}
let mut recipient_data = AccountSharedData::default();
recipient_data.set_lamports(base_amount);
recipient_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(recipient, &recipient_data);
if drop_on_failure {
test_entry.final_accounts.insert(recipient, recipient_data);
}
let instruction = Instruction::new_with_bytes(
program_id,
&u64::to_be_bytes(transfer_amount),
vec![
AccountMeta::new(sender, true),
AccountMeta::new(recipient, false),
AccountMeta::new_readonly(system_program::id(), false),
],
);
test_entry.push_transaction_with_status(
Transaction::new_signed_with_payer(
&[instruction],
Some(&fee_payer),
&[&fee_payer_keypair, &sender_keypair],
Hash::default(),
),
match drop_on_failure {
true => ExecutionStatus::Discarded,
false => ExecutionStatus::ExecutedFailed,
},
);
if !drop_on_failure {
test_entry.transaction_batch[3]
.asserts
.logs
.push("Transfer: insufficient lamports 900000, need 900050".to_string());
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
}
}
{
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
test_entry.transaction_batch.push(TransactionBatchItem {
transaction: Transaction::new_signed_with_payer(
&[],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
),
check_result: Err(TransactionError::BlockhashNotFound),
asserts: ExecutionStatus::Discarded.into(),
});
}
vec![test_entry]
}
fn simple_transfer(drop_on_failure: bool) -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry {
drop_on_failure,
..Default::default()
};
let transfer_amount = LAMPORTS_PER_SOL;
let drop_on_failure_status = |status: ExecutionStatus| match (drop_on_failure, status) {
(true, ExecutionStatus::Succeeded) => ExecutionStatus::Succeeded,
(true, _) => ExecutionStatus::Discarded,
(false, status) => status,
};
{
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let destination = Pubkey::new_unique();
let mut source_data = AccountSharedData::default();
let mut destination_data = AccountSharedData::default();
source_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(source, &source_data);
test_entry.push_transaction(system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
));
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE);
}
{
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let mut source_data = AccountSharedData::default();
source_data.set_lamports(transfer_amount - 1);
source_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(source, &source_data);
if drop_on_failure {
test_entry.final_accounts.insert(source, source_data);
}
test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&Pubkey::new_unique(),
transfer_amount,
Hash::default(),
),
drop_on_failure_status(ExecutionStatus::ExecutedFailed),
);
if !drop_on_failure {
test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE);
}
}
{
test_entry.transaction_batch.push(TransactionBatchItem {
transaction: system_transaction::transfer(
&Keypair::new(),
&Pubkey::new_unique(),
transfer_amount,
Hash::default(),
),
check_result: Err(TransactionError::BlockhashNotFound),
asserts: ExecutionStatus::Discarded.into(),
});
}
{
test_entry.push_transaction_with_status(
system_transaction::transfer(
&Keypair::new(),
&Pubkey::new_unique(),
transfer_amount,
Hash::default(),
),
ExecutionStatus::Discarded,
);
}
{
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let mut source_data = AccountSharedData::default();
source_data.set_lamports(transfer_amount * 10);
test_entry
.initial_accounts
.insert(source, source_data.clone());
test_entry.final_accounts.insert(source, source_data);
let mut instruction =
system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount);
instruction.program_id = Pubkey::new_unique();
if !drop_on_failure {
test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE);
}
test_entry.push_transaction_with_status(
Transaction::new_signed_with_payer(
&[instruction],
Some(&source),
&[&source_keypair],
Hash::default(),
),
drop_on_failure_status(ExecutionStatus::ProcessedFailed),
);
}
vec![test_entry]
}
fn simple_nonce(fee_paying_nonce: bool) -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry::default();
let program_name = "hello-solana";
let real_program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let mk_nonce_transaction = |test_entry: &mut SvmTestEntry,
program_id,
fake_fee_payer: bool,
rent_paying_nonce: bool| {
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let nonce_pubkey = if fee_paying_nonce {
fee_payer
} else {
Pubkey::new_unique()
};
let nonce_size = nonce::state::State::size();
let mut nonce_balance = Rent::default().minimum_balance(nonce_size);
if !fake_fee_payer && !fee_paying_nonce {
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
} else if rent_paying_nonce {
assert!(fee_paying_nonce);
nonce_balance += LAMPORTS_PER_SIGNATURE;
nonce_balance -= 1;
} else if fee_paying_nonce {
nonce_balance += LAMPORTS_PER_SOL;
}
let nonce_initial_hash = DurableNonce::from_blockhash(&Hash::new_unique());
let nonce_data =
nonce::state::Data::new(fee_payer, nonce_initial_hash, LAMPORTS_PER_SIGNATURE);
let mut nonce_account = AccountSharedData::new_data(
nonce_balance,
&nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())),
&system_program::id(),
)
.unwrap();
nonce_account.set_rent_epoch(u64::MAX);
let nonce_info = NonceInfo::new(nonce_pubkey, nonce_account.clone());
if !(fake_fee_payer && fee_paying_nonce) {
test_entry.add_initial_account(nonce_pubkey, &nonce_account);
}
let instructions = vec![
system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer),
Instruction::new_with_bytes(program_id, &[], vec![]),
];
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&fee_payer),
&[&fee_payer_keypair],
nonce_data.blockhash(),
);
(transaction, fee_payer, nonce_info)
};
{
let (transaction, fee_payer, mut nonce_info) =
mk_nonce_transaction(&mut test_entry, real_program_id, false, false);
test_entry.push_nonce_transaction(transaction, *nonce_info.address());
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
nonce_info
.try_advance_nonce(
DurableNonce::from_blockhash(&LAST_BLOCKHASH),
LAMPORTS_PER_SIGNATURE,
)
.unwrap();
test_entry
.final_accounts
.get_mut(nonce_info.address())
.unwrap()
.data_as_mut_slice()
.copy_from_slice(nonce_info.account().data());
}
{
let (transaction, _fee_payer, nonce_info) =
mk_nonce_transaction(&mut test_entry, real_program_id, true, false);
test_entry.push_nonce_transaction_with_status(
transaction,
*nonce_info.address(),
ExecutionStatus::Discarded,
);
}
{
let (transaction, fee_payer, mut nonce_info) =
mk_nonce_transaction(&mut test_entry, system_program::id(), false, false);
test_entry.push_nonce_transaction_with_status(
transaction,
*nonce_info.address(),
ExecutionStatus::ExecutedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
nonce_info
.try_advance_nonce(
DurableNonce::from_blockhash(&LAST_BLOCKHASH),
LAMPORTS_PER_SIGNATURE,
)
.unwrap();
test_entry
.final_accounts
.get_mut(nonce_info.address())
.unwrap()
.data_as_mut_slice()
.copy_from_slice(nonce_info.account().data());
}
{
let (transaction, fee_payer, mut nonce_info) =
mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, false);
test_entry.push_nonce_transaction_with_status(
transaction,
*nonce_info.address(),
ExecutionStatus::ProcessedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
nonce_info
.try_advance_nonce(
DurableNonce::from_blockhash(&LAST_BLOCKHASH),
LAMPORTS_PER_SIGNATURE,
)
.unwrap();
test_entry
.final_accounts
.get_mut(nonce_info.address())
.unwrap()
.data_as_mut_slice()
.copy_from_slice(nonce_info.account().data());
}
if fee_paying_nonce {
let (transaction, _, nonce_info) =
mk_nonce_transaction(&mut test_entry, real_program_id, false, true);
test_entry.push_nonce_transaction_with_status(
transaction,
*nonce_info.address(),
ExecutionStatus::Discarded,
);
}
if fee_paying_nonce {
let (transaction, _, nonce_info) =
mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, true);
test_entry.push_nonce_transaction_with_status(
transaction,
*nonce_info.address(),
ExecutionStatus::Discarded,
);
}
vec![test_entry]
}
fn simd83_intrabatch_account_reuse() -> Vec<SvmTestEntry> {
let mut test_entries = vec![];
let transfer_amount = LAMPORTS_PER_SOL;
let wallet_rent = Rent::default().minimum_balance(0);
{
let mut test_entry = SvmTestEntry::default();
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let destination1 = Pubkey::new_unique();
let destination2 = Pubkey::new_unique();
let mut source_data = AccountSharedData::default();
let destination1_data = AccountSharedData::default();
let destination2_data = AccountSharedData::default();
source_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(source, &source_data);
for (destination, mut destination_data) in [
(destination1, destination1_data),
(destination2, destination2_data),
] {
test_entry.push_transaction(system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
));
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry
.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE);
}
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let destination = Pubkey::new_unique();
let mut source_data = AccountSharedData::default();
let mut destination_data = AccountSharedData::default();
source_data.set_lamports(transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent);
test_entry.add_initial_account(source, &source_data);
test_entry.push_transaction(system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
));
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE);
test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
),
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
let first_transfer_amount = transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent;
let second_transfer_amount = transfer_amount;
let grandparent_keypair = Keypair::new();
let grandparent = grandparent_keypair.pubkey();
let parent_keypair = Keypair::new();
let parent = parent_keypair.pubkey();
let child = Pubkey::new_unique();
let mut grandparent_data = AccountSharedData::default();
let mut parent_data = AccountSharedData::default();
let mut child_data = AccountSharedData::default();
grandparent_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(grandparent, &grandparent_data);
test_entry.push_transaction(system_transaction::transfer(
&grandparent_keypair,
&parent,
first_transfer_amount,
Hash::default(),
));
parent_data
.checked_add_lamports(first_transfer_amount)
.unwrap();
test_entry.create_expected_account(parent, &parent_data);
test_entry.decrease_expected_lamports(
&grandparent,
first_transfer_amount + LAMPORTS_PER_SIGNATURE,
);
test_entry.push_transaction(system_transaction::transfer(
&parent_keypair,
&child,
second_transfer_amount,
Hash::default(),
));
child_data
.checked_add_lamports(second_transfer_amount)
.unwrap();
test_entry.create_expected_account(child, &child_data);
test_entry
.decrease_expected_lamports(&parent, second_transfer_amount + LAMPORTS_PER_SIGNATURE);
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
let feepayer_keypair = Keypair::new();
let feepayer = feepayer_keypair.pubkey();
let separate_source_keypair = Keypair::new();
let separate_source = separate_source_keypair.pubkey();
let destination = Pubkey::new_unique();
let mut feepayer_data = AccountSharedData::default();
let mut separate_source_data = AccountSharedData::default();
let mut destination_data = AccountSharedData::default();
feepayer_data.set_lamports(1 + LAMPORTS_PER_SIGNATURE + wallet_rent);
test_entry.add_initial_account(feepayer, &feepayer_data);
separate_source_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(separate_source, &separate_source_data);
test_entry.push_transaction_with_status(
Transaction::new_signed_with_payer(
&[system_instruction::transfer(
&separate_source,
&destination,
1,
)],
Some(&feepayer),
&[&feepayer_keypair, &separate_source_keypair],
Hash::default(),
),
ExecutionStatus::Discarded,
);
test_entry.push_transaction(system_transaction::transfer(
&feepayer_keypair,
&destination,
1,
Hash::default(),
));
destination_data.checked_add_lamports(1).unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry.decrease_expected_lamports(&feepayer, 1 + LAMPORTS_PER_SIGNATURE);
}
{
let mut test_entry = SvmTestEntry::default();
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let destination = Pubkey::new_unique();
let mut source_data = AccountSharedData::default();
let mut destination_data = AccountSharedData::default();
source_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(source, &source_data);
let mut load_program_fail_instruction =
system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount);
load_program_fail_instruction.program_id = Pubkey::new_unique();
test_entry.push_transaction_with_status(
Transaction::new_signed_with_payer(
&[load_program_fail_instruction],
Some(&source),
&[&source_keypair],
Hash::default(),
),
ExecutionStatus::ProcessedFailed,
);
test_entry.push_transaction(system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
));
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry
.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE * 2);
test_entries.push(test_entry);
}
test_entries
}
fn simd83_nonce_reuse(fee_paying_nonce: bool) -> Vec<SvmTestEntry> {
let mut test_entries = vec![];
let program_name = "hello-solana";
let program_id = program_address(program_name);
let fee_payer_keypair = Keypair::new();
let non_fee_nonce_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let nonce_pubkey = if fee_paying_nonce {
fee_payer
} else {
non_fee_nonce_keypair.pubkey()
};
let nonce_size = nonce::state::State::size();
let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique());
let initial_nonce_data =
nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE);
let mut initial_nonce_account = AccountSharedData::new_data(
LAMPORTS_PER_SOL,
&nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)),
&system_program::id(),
)
.unwrap();
initial_nonce_account.set_rent_epoch(u64::MAX);
let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone());
let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH);
let mut advanced_nonce_info = initial_nonce_info;
advanced_nonce_info
.try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE)
.unwrap();
let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer);
let withdraw_instruction = system_instruction::withdraw_nonce_account(
&nonce_pubkey,
&fee_payer,
&fee_payer,
LAMPORTS_PER_SOL,
);
let successful_noop_instruction = Instruction::new_with_bytes(program_id, &[], vec![]);
let failing_noop_instruction = Instruction::new_with_bytes(system_program::id(), &[], vec![]);
let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
let second_transaction = Transaction::new_signed_with_payer(
&[
advance_instruction.clone(),
successful_noop_instruction.clone(),
],
Some(&fee_payer),
&[&fee_payer_keypair],
*advanced_durable.as_hash(),
);
let mut common_test_entry = SvmTestEntry::default();
common_test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account);
if !fee_paying_nonce {
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
common_test_entry.add_initial_account(fee_payer, &fee_payer_data);
}
common_test_entry
.final_accounts
.get_mut(&nonce_pubkey)
.unwrap()
.data_as_mut_slice()
.copy_from_slice(advanced_nonce_info.account().data());
common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let common_test_entry = common_test_entry;
{
let mut test_entry = common_test_entry.clone();
let transaction = Transaction::new_signed_with_payer(
&[advance_instruction.clone(), advance_instruction.clone()],
Some(&fee_payer),
&[&fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction_with_status(
transaction,
nonce_pubkey,
ExecutionStatus::ExecutedFailed,
);
test_entries.push(test_entry);
}
{
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
&[
advance_instruction.clone(),
successful_noop_instruction.clone(),
],
Some(&fee_payer),
&[&fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction(first_transaction, nonce_pubkey);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
{
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
&[advance_instruction.clone(), failing_noop_instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction_with_status(
first_transaction,
nonce_pubkey,
ExecutionStatus::ExecutedFailed,
);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
{
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
&[advance_instruction.clone(), fee_only_noop_instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction_with_status(
first_transaction,
nonce_pubkey,
ExecutionStatus::ProcessedFailed,
);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
{
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
&[successful_noop_instruction.clone(), advance_instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
slice::from_ref(&withdraw_instruction),
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entry.increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL);
test_entry.drop_expected_account(nonce_pubkey);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
slice::from_ref(&withdraw_instruction),
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
let middle_transaction = system_transaction::transfer(
&fee_payer_keypair,
&nonce_pubkey,
LAMPORTS_PER_SOL,
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_transaction(middle_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let mut new_nonce_state = AccountSharedData::default();
new_nonce_state.set_lamports(LAMPORTS_PER_SOL);
test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
slice::from_ref(&withdraw_instruction),
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
let middle_transaction = system_transaction::create_account(
&fee_payer_keypair,
&non_fee_nonce_keypair,
Hash::default(),
LAMPORTS_PER_SOL,
nonce_size as u64,
&system_program::id(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_transaction(middle_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
let new_nonce_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(vec![0; nonce_size]),
system_program::id(),
false,
u64::MAX,
);
test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
let first_transaction = Transaction::new_signed_with_payer(
slice::from_ref(&withdraw_instruction),
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
let create_instructions = system_instruction::create_nonce_account(
&fee_payer,
&nonce_pubkey,
&fee_payer,
LAMPORTS_PER_SOL,
);
let middle_transaction = Transaction::new_signed_with_payer(
&create_instructions,
Some(&fee_payer),
&[&fee_payer_keypair, &non_fee_nonce_keypair],
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_transaction(middle_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
test_entry.initial_accounts.remove(&nonce_pubkey);
test_entry.final_accounts.remove(&nonce_pubkey);
let mut fake_nonce_account = initial_nonce_account.clone();
fake_nonce_account.set_rent_epoch(u64::MAX);
fake_nonce_account.set_owner(Pubkey::new_unique());
test_entry.add_initial_account(nonce_pubkey, &fake_nonce_account);
let first_transaction = Transaction::new_signed_with_payer(
slice::from_ref(&successful_noop_instruction),
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction.clone(),
nonce_pubkey,
ExecutionStatus::Discarded,
);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry.clone();
let new_authority = Pubkey::new_unique();
let first_transaction = Transaction::new_signed_with_payer(
&[system_instruction::authorize_nonce_account(
&nonce_pubkey,
&fee_payer,
&new_authority,
)],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_nonce_transaction_with_status(
second_transaction,
nonce_pubkey,
ExecutionStatus::Discarded,
);
let final_nonce_data =
nonce::state::Data::new(new_authority, initial_durable, LAMPORTS_PER_SIGNATURE);
let final_nonce_account = AccountSharedData::new_data(
LAMPORTS_PER_SOL,
&nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)),
&system_program::id(),
)
.unwrap();
test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account);
test_entries.push(test_entry);
}
if !fee_paying_nonce {
let mut test_entry = common_test_entry;
let new_authority_keypair = Keypair::new();
let new_authority = new_authority_keypair.pubkey();
let first_transaction = Transaction::new_signed_with_payer(
&[system_instruction::authorize_nonce_account(
&nonce_pubkey,
&fee_payer,
&new_authority,
)],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
let second_transaction = Transaction::new_signed_with_payer(
&[
system_instruction::advance_nonce_account(&nonce_pubkey, &new_authority),
successful_noop_instruction,
],
Some(&fee_payer),
&[&fee_payer_keypair, &new_authority_keypair],
*initial_durable.as_hash(),
);
test_entry.push_transaction(first_transaction);
test_entry.push_nonce_transaction(second_transaction, nonce_pubkey);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
let final_nonce_data =
nonce::state::Data::new(new_authority, advanced_durable, LAMPORTS_PER_SIGNATURE);
let final_nonce_account = AccountSharedData::new_data(
LAMPORTS_PER_SOL,
&nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)),
&system_program::id(),
)
.unwrap();
test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account);
test_entries.push(test_entry);
}
for test_entry in &mut test_entries {
test_entry.add_initial_program(program_name);
}
test_entries
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WriteProgramInstruction {
Print,
Set,
Dealloc,
Realloc(usize),
}
impl WriteProgramInstruction {
fn create_transaction(
self,
program_id: Pubkey,
fee_payer: &Keypair,
target: Pubkey,
clamp_data_size: Option<u32>,
) -> Transaction {
let (instruction_data, account_metas) = match self {
Self::Print => (vec![0], vec![AccountMeta::new_readonly(target, false)]),
Self::Set => (vec![1], vec![AccountMeta::new(target, false)]),
Self::Dealloc => (
vec![2],
vec![
AccountMeta::new(target, false),
AccountMeta::new(solana_sdk_ids::incinerator::id(), false),
],
),
Self::Realloc(new_size) => {
let mut instruction_data = vec![3];
instruction_data.extend_from_slice(&new_size.to_le_bytes());
(instruction_data, vec![AccountMeta::new(target, false)])
}
};
let mut instructions = vec![];
if let Some(size) = clamp_data_size {
instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size));
}
instructions.push(Instruction::new_with_bytes(
program_id,
&instruction_data,
account_metas,
));
Transaction::new_signed_with_payer(
&instructions,
Some(&fee_payer.pubkey()),
&[fee_payer],
Hash::default(),
)
}
}
fn simd83_account_deallocate() -> Vec<SvmTestEntry> {
let mut test_entries = vec![];
for remove_lamports in [false, true] {
let mut test_entry = SvmTestEntry::default();
let program_name = "write-to-account";
let program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target = Pubkey::new_unique();
let mut target_data = AccountSharedData::create_from_existing_shared_data(
Rent::default().minimum_balance(1),
Arc::new(vec![0]),
program_id,
false,
u64::MAX,
);
test_entry.add_initial_account(target, &target_data);
let set_data_transaction = WriteProgramInstruction::Set.create_transaction(
program_id,
&fee_payer_keypair,
target,
None,
);
test_entry.push_transaction(set_data_transaction);
target_data.data_as_mut_slice()[0] = 100;
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
test_entry.update_expected_account_data(target, &target_data);
if remove_lamports {
let dealloc_transaction = WriteProgramInstruction::Dealloc.create_transaction(
program_id,
&fee_payer_keypair,
target,
None,
);
test_entry.push_transaction(dealloc_transaction);
let print_transaction = WriteProgramInstruction::Print.create_transaction(
program_id,
&fee_payer_keypair,
target,
None,
);
test_entry.push_transaction(print_transaction);
test_entry.transaction_batch[2]
.asserts
.logs
.push("Program log: account size 0".to_string());
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
test_entry.drop_expected_account(target);
}
test_entries.push(test_entry);
}
test_entries
}
fn simd83_fee_payer_deallocate() -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry::default();
let program_name = "hello-solana";
let real_program_id = program_address(program_name);
test_entry.add_initial_program(program_name);
let rent = Rent {
lamports_per_byte: LAMPORTS_PER_SIGNATURE / solana_rent::ACCOUNT_STORAGE_OVERHEAD,
..Rent::default()
};
test_entry.set_rent_params(rent);
for do_fee_only_transaction in [false, true] {
let dealloc_fee_payer_keypair = Keypair::new();
let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey();
let mut dealloc_fee_payer_data = AccountSharedData::default();
dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE);
test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data);
let stable_fee_payer_keypair = Keypair::new();
let stable_fee_payer = stable_fee_payer_keypair.pubkey();
let mut stable_fee_payer_data = AccountSharedData::default();
stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data);
let instruction = Instruction::new_with_bytes(
if do_fee_only_transaction {
Pubkey::new_unique()
} else {
real_program_id
},
&[],
vec![],
);
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&dealloc_fee_payer),
&[&dealloc_fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction_with_status(
transaction,
if do_fee_only_transaction {
ExecutionStatus::ProcessedFailed
} else {
ExecutionStatus::Succeeded
},
);
test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE);
let instruction = Instruction::new_with_bytes(
real_program_id,
&[],
vec![AccountMeta::new_readonly(dealloc_fee_payer, false)],
);
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[instruction],
Some(&stable_fee_payer),
&[&stable_fee_payer_keypair],
Hash::default(),
));
test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE);
test_entry.drop_expected_account(dealloc_fee_payer);
}
{
let dealloc_fee_payer_keypair = Keypair::new();
let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey();
let mut dealloc_fee_payer_data = AccountSharedData::default();
dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE);
dealloc_fee_payer_data.set_rent_epoch(u64::MAX - 1);
test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data);
let stable_fee_payer_keypair = Keypair::new();
let stable_fee_payer = stable_fee_payer_keypair.pubkey();
let mut stable_fee_payer_data = AccountSharedData::default();
stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data);
let nonce_pubkey = Pubkey::new_unique();
let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique());
let initial_nonce_data =
nonce::state::Data::new(dealloc_fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE);
let mut initial_nonce_account = AccountSharedData::new_data(
LAMPORTS_PER_SOL,
&nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)),
&system_program::id(),
)
.unwrap();
initial_nonce_account.set_rent_epoch(u64::MAX);
let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone());
let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH);
let mut advanced_nonce_info = initial_nonce_info;
advanced_nonce_info
.try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE)
.unwrap();
test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account);
let advance_instruction =
system_instruction::advance_nonce_account(&nonce_pubkey, &dealloc_fee_payer);
let fee_only_noop_instruction =
Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
let transaction = Transaction::new_signed_with_payer(
&[advance_instruction, fee_only_noop_instruction],
Some(&dealloc_fee_payer),
&[&dealloc_fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction_with_status(
transaction,
nonce_pubkey,
ExecutionStatus::ProcessedFailed,
);
test_entry
.final_accounts
.get_mut(&nonce_pubkey)
.unwrap()
.data_as_mut_slice()
.copy_from_slice(advanced_nonce_info.account().data());
test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE);
let instruction = Instruction::new_with_bytes(
real_program_id,
&[],
vec![AccountMeta::new_readonly(dealloc_fee_payer, false)],
);
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[instruction],
Some(&stable_fee_payer),
&[&stable_fee_payer_keypair],
Hash::default(),
));
test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE);
test_entry.drop_expected_account(dealloc_fee_payer);
}
vec![test_entry]
}
fn simd83_account_reallocate() -> Vec<SvmTestEntry> {
let mut test_entries = vec![];
let program_name = "write-to-account";
let program_id = program_address(program_name);
let program_size = program_data_size(program_name);
let mut common_test_entry = SvmTestEntry::default();
common_test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
common_test_entry.add_initial_account(fee_payer, &fee_payer_data);
let mk_target = |size| {
AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL * 10,
Arc::new(vec![0; size]),
program_id,
false,
u64::MAX,
)
};
let target = Pubkey::new_unique();
let target_start_size = 100;
common_test_entry.add_initial_account(target, &mk_target(target_start_size));
let size_budget = Some((program_size + MAX_PERMITTED_DATA_INCREASE) as u32);
let print_transaction = WriteProgramInstruction::Print.create_transaction(
program_id,
&fee_payer_keypair,
target,
size_budget,
);
common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
let common_test_entry = common_test_entry;
for new_target_size in [target_start_size + 1, target_start_size - 1] {
let mut test_entry = common_test_entry.clone();
let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size)
.create_transaction(program_id, &fee_payer_keypair, target, None);
test_entry.push_transaction(realloc_transaction);
test_entry.push_transaction(print_transaction.clone());
test_entry.transaction_batch[1]
.asserts
.logs
.push(format!("Program log: account size {new_target_size}"));
test_entry.update_expected_account_data(target, &mk_target(new_target_size));
test_entries.push(test_entry);
}
{
let mut test_entry = common_test_entry;
let new_target_size = target_start_size + MAX_PERMITTED_DATA_INCREASE;
let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size)
.create_transaction(program_id, &fee_payer_keypair, target, None);
test_entry.push_transaction(realloc_transaction);
test_entry
.push_transaction_with_status(print_transaction, ExecutionStatus::ProcessedFailed);
test_entry.update_expected_account_data(target, &mk_target(new_target_size));
test_entries.push(test_entry);
}
test_entries
}
fn simd0392_balance_checks() -> Vec<SvmTestEntry> {
let mut test_entries = vec![];
let program_name = "write-to-account";
let program_id = program_address(program_name);
let base_rent = Rent::default();
let bumped_rent = Rent {
lamports_per_byte: base_rent.lamports_per_byte.saturating_mul(4),
..base_rent
};
let target_size = 8;
let old_min_balance = base_rent.minimum_balance(target_size);
let new_min_balance = bumped_rent.minimum_balance(target_size);
assert!(new_min_balance > old_min_balance + LAMPORTS_PER_SIGNATURE);
let mk_program_account = |lamports, size| {
AccountSharedData::create_from_existing_shared_data(
lamports,
Arc::new(vec![0; size]),
program_id,
false,
u64::MAX,
)
};
let mk_system_account = |lamports, size| {
AccountSharedData::create_from_existing_shared_data(
lamports,
Arc::new(vec![0; size]),
system_program::id(),
false,
u64::MAX,
)
};
{
let mut test_entry = SvmTestEntry::default();
test_entry.set_rent_params(bumped_rent.clone());
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target = Pubkey::new_unique();
let target_data = mk_program_account(old_min_balance, target_size);
test_entry.add_initial_account(target, &target_data);
let set_data_transaction = WriteProgramInstruction::Set.create_transaction(
program_id,
&fee_payer_keypair,
target,
None,
);
test_entry.push_transaction(set_data_transaction);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let mut expected_target = target_data;
expected_target.data_as_mut_slice()[0] = 100;
test_entry.update_expected_account_data(target, &expected_target);
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
test_entry.set_rent_params(bumped_rent.clone());
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target = Pubkey::new_unique();
let target_data = mk_program_account(old_min_balance, target_size);
test_entry.add_initial_account(target, &target_data);
let new_target_size = target_size - 1;
let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size)
.create_transaction(program_id, &fee_payer_keypair, target, None);
test_entry.push_transaction(realloc_transaction);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let expected_target = mk_program_account(old_min_balance, new_target_size);
test_entry.update_expected_account_data(target, &expected_target);
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
test_entry.set_rent_params(bumped_rent.clone());
test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target = Pubkey::new_unique();
let target_data = mk_program_account(old_min_balance, target_size);
test_entry.add_initial_account(target, &target_data);
let new_target_size = target_size + 1;
let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size)
.create_transaction(program_id, &fee_payer_keypair, target, None);
test_entry
.push_transaction_with_status(realloc_transaction, ExecutionStatus::ExecutedFailed);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
test_entries.push(test_entry);
}
{
let mut test_entry = SvmTestEntry::default();
test_entry.set_rent_params(bumped_rent.clone());
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
fee_payer_data.set_rent_epoch(u64::MAX);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target_keypair = Keypair::new();
let target = target_keypair.pubkey();
let target_data = mk_system_account(old_min_balance, target_size);
test_entry.add_initial_account(target, &target_data);
let new_owner = Pubkey::new_unique();
let transaction = Transaction::new_signed_with_payer(
&[system_instruction::assign(&target, &new_owner)],
Some(&fee_payer),
&[&fee_payer_keypair, &target_keypair],
Hash::default(),
);
test_entry.push_transaction_with_status(transaction, ExecutionStatus::ExecutedFailed);
test_entry.decrease_expected_lamports(&fee_payer, 2 * LAMPORTS_PER_SIGNATURE);
test_entries.push(test_entry);
}
test_entries
}
enum AbortReason {
None,
Unprocessable,
DropOnFailure,
}
fn all_or_nothing(abort: AbortReason) -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry {
all_or_nothing: true,
drop_on_failure: matches!(abort, AbortReason::DropOnFailure),
..Default::default()
};
let transfer_amount = LAMPORTS_PER_SOL;
{
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let destination = Pubkey::new_unique();
let mut source_data = AccountSharedData::default();
let mut destination_data = AccountSharedData::default();
source_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(source, &source_data);
let status = match abort {
AbortReason::None => {
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
test_entry.create_expected_account(destination, &destination_data);
test_entry
.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE);
ExecutionStatus::Succeeded
}
AbortReason::Unprocessable | AbortReason::DropOnFailure => {
test_entry.final_accounts.insert(source, source_data);
ExecutionStatus::Discarded
}
};
test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
),
status,
);
}
if matches!(abort, AbortReason::DropOnFailure) {
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let mut source_data = AccountSharedData::default();
source_data.set_lamports(transfer_amount - 1);
test_entry.add_initial_account(source, &source_data);
test_entry.final_accounts.insert(source, source_data);
test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&Pubkey::new_unique(),
transfer_amount,
Hash::default(),
),
ExecutionStatus::Discarded,
);
}
if matches!(abort, AbortReason::Unprocessable) {
test_entry.transaction_batch.push(TransactionBatchItem {
transaction: system_transaction::transfer(
&Keypair::new(),
&Pubkey::new_unique(),
transfer_amount,
Hash::default(),
),
check_result: Err(TransactionError::BlockhashNotFound),
asserts: ExecutionStatus::Discarded.into(),
});
}
vec![test_entry]
}
fn drop_on_failure_batch(statuses: &[bool]) -> Vec<SvmTestEntry> {
let mut test_entry = SvmTestEntry {
drop_on_failure: true,
..Default::default()
};
let transfer_amount = LAMPORTS_PER_SOL;
let source_keypair = Keypair::new();
let source = source_keypair.pubkey();
let mut source_data = AccountSharedData::default();
source_data.set_lamports(LAMPORTS_PER_SOL * 100);
test_entry.add_initial_account(source, &source_data);
let destination = Pubkey::new_unique();
let mut destination_data = AccountSharedData::default();
println!("source: {source}");
println!("destination: {destination}");
for success in statuses {
match success {
true => {
test_entry
.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE);
destination_data
.checked_add_lamports(transfer_amount)
.unwrap();
destination_data.set_rent_epoch(u64::MAX);
test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&destination,
transfer_amount,
Hash::default(),
),
ExecutionStatus::Succeeded,
);
}
false => test_entry.push_transaction_with_status(
system_transaction::transfer(
&source_keypair,
&destination,
source_data.lamports() + 1,
Hash::default(),
),
ExecutionStatus::Discarded,
),
}
}
if statuses.iter().all(|success| !*success) {
test_entry
.final_accounts
.get_mut(&source)
.unwrap()
.set_rent_epoch(0);
}
if statuses.iter().any(|success| *success) {
assert!(
test_entry
.final_accounts
.insert(destination, destination_data)
.is_none()
);
}
vec![test_entry]
}
#[test_case(program_medley(false))]
#[test_case(program_medley(true))]
#[test_case(simple_transfer(false))]
#[test_case(simple_transfer(true))]
#[test_case(simple_nonce(false))]
#[test_case(simple_nonce(true))]
#[test_case(simd83_intrabatch_account_reuse())]
#[test_case(simd83_nonce_reuse(false))]
#[test_case(simd83_nonce_reuse(true))]
#[test_case(simd83_account_deallocate())]
#[test_case(simd83_fee_payer_deallocate())]
#[test_case(simd83_account_reallocate())]
#[test_case(simd0392_balance_checks())]
#[test_case(all_or_nothing(AbortReason::None))]
#[test_case(all_or_nothing(AbortReason::Unprocessable))]
#[test_case(all_or_nothing(AbortReason::DropOnFailure))]
#[test_case(drop_on_failure_batch(&[false]))]
#[test_case(drop_on_failure_batch(&[true]))]
#[test_case(drop_on_failure_batch(&[false, false]))]
#[test_case(drop_on_failure_batch(&[true, true]))]
#[test_case(drop_on_failure_batch(&[false, false, true]))]
#[test_case(drop_on_failure_batch(&[true, true, false]))]
#[test_case(drop_on_failure_batch(&[false, true, false]))]
#[test_case(drop_on_failure_batch(&[true, false, true]))]
fn svm_integration(test_entries: Vec<SvmTestEntry>) {
for test_entry in test_entries {
let env = SvmTestEnvironment::create(test_entry);
env.execute();
}
}
#[test]
fn program_cache_create_account() {
let supported_loaders = [
bpf_loader_upgradeable::id(),
bpf_loader::id(),
bpf_loader_deprecated::id(),
];
for loader_id in &supported_loaders {
let mut test_entry = SvmTestEntry::default();
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let new_account_keypair = Keypair::new();
let program_id = new_account_keypair.pubkey();
let create_transaction = system_transaction::create_account(
&fee_payer_keypair,
&new_account_keypair,
Hash::default(),
LAMPORTS_PER_SOL,
0,
loader_id,
);
test_entry.push_transaction(create_transaction);
test_entry
.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SOL + LAMPORTS_PER_SIGNATURE * 2);
let invoke_transaction = Transaction::new_signed_with_payer(
&[Instruction::new_with_bytes(program_id, &[], vec![])],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
test_entry.push_transaction_with_status(
invoke_transaction.clone(),
ExecutionStatus::ExecutedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let mut env = SvmTestEnvironment::create(test_entry);
env.execute();
let mut test_entry = SvmTestEntry {
initial_accounts: env.test_entry.final_accounts.clone(),
final_accounts: env.test_entry.final_accounts.clone(),
..SvmTestEntry::default()
};
test_entry
.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
env.test_entry = test_entry;
env.execute();
}
}
#[test_case(false, false; "close::scan_only")]
#[test_case(false, true; "close::invoke")]
#[test_case(true, false; "upgrade::scan_only")]
#[test_case(true, true; "upgrade::invoke")]
fn program_cache_loaderv3_update_tombstone(upgrade_program: bool, invoke_changed_program: bool) {
let mut test_entry = SvmTestEntry::default();
let program_name = "hello-solana";
let program_id = program_address(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
test_entry
.initial_programs
.push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer)));
let buffer_address = Pubkey::new_unique();
let change_instruction = if upgrade_program {
let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer {
authority_address: Some(fee_payer),
})
.unwrap();
let mut program_bytecode = load_program(program_name.to_string());
data.append(&mut program_bytecode);
let buffer_account = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(data),
bpf_loader_upgradeable::id(),
true,
u64::MAX,
);
test_entry.add_initial_account(buffer_address, &buffer_account);
test_entry.drop_expected_account(buffer_address);
loaderv3_instruction::upgrade(
&program_id,
&buffer_address,
&fee_payer,
&Pubkey::new_unique(),
)
} else {
loaderv3_instruction::close_any(
&get_program_data_address(&program_id),
&Pubkey::new_unique(),
Some(&fee_payer),
Some(&program_id),
)
};
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[change_instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
));
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
let invoke_transaction = Transaction::new_signed_with_payer(
&[Instruction::new_with_bytes(program_id, &[], vec![])],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
if invoke_changed_program {
test_entry.push_transaction_with_status(
invoke_transaction.clone(),
ExecutionStatus::ExecutedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
}
let mut env = SvmTestEnvironment::create(test_entry);
env.execute();
assert!(env.is_program_blocked(&program_id));
let mut test_entry = SvmTestEntry {
initial_accounts: env.test_entry.final_accounts.clone(),
final_accounts: env.test_entry.final_accounts.clone(),
..SvmTestEntry::default()
};
test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
env.test_entry = test_entry;
env.execute();
assert!(env.is_program_blocked(&program_id));
}
#[test_case(false; "upgrade::scan_only")]
#[test_case(true; "upgrade::invoke")]
fn program_cache_loaderv3_buffer_swap(invoke_changed_program: bool) {
let mut test_entry = SvmTestEntry::default();
let program_name = "hello-solana";
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
let target_keypair = Keypair::new();
let target = target_keypair.pubkey();
let programdata_address = get_program_data_address(&target);
let deploy_keypair = Keypair::new();
let deploy = deploy_keypair.pubkey();
let mut buffer_data = bincode::serialize(&UpgradeableLoaderState::Buffer {
authority_address: Some(fee_payer),
})
.unwrap();
let mut program_bytecode = load_program(program_name.to_string());
buffer_data.append(&mut program_bytecode);
let buffer_account = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(buffer_data.clone()),
bpf_loader_upgradeable::id(),
true,
u64::MAX,
);
test_entry.add_initial_account(target, &buffer_account);
test_entry.add_initial_account(deploy, &buffer_account);
let program_data = bincode::serialize(&UpgradeableLoaderState::Program {
programdata_address,
})
.unwrap();
let program_account = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(program_data),
bpf_loader_upgradeable::id(),
true,
u64::MAX,
);
test_entry.update_expected_account_data(target, &program_account);
test_entry.drop_expected_account(deploy);
let close_instruction =
loaderv3_instruction::close_any(&target, &Pubkey::new_unique(), Some(&fee_payer), None);
#[allow(deprecated)]
let deploy_instruction = loaderv3_instruction::deploy_with_max_program_len(
&fee_payer,
&target,
&deploy,
&fee_payer,
LAMPORTS_PER_SOL,
buffer_data.len(),
)
.unwrap();
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[close_instruction],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
));
test_entry.push_transaction(Transaction::new_signed_with_payer(
&deploy_instruction,
Some(&fee_payer),
&[&fee_payer_keypair, &target_keypair],
Hash::default(),
));
test_entry.decrease_expected_lamports(
&fee_payer,
Rent::default().minimum_balance(
UpgradeableLoaderState::size_of_programdata_metadata() + buffer_data.len(),
) + LAMPORTS_PER_SIGNATURE * 3,
);
let invoke_transaction = Transaction::new_signed_with_payer(
&[Instruction::new_with_bytes(target, &[], vec![])],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
);
if invoke_changed_program {
test_entry.push_transaction_with_status(
invoke_transaction.clone(),
ExecutionStatus::ExecutedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
}
let mut env = SvmTestEnvironment::create(test_entry);
env.execute();
assert!(env.is_program_blocked(&target));
let mut test_entry = SvmTestEntry {
initial_accounts: env.test_entry.final_accounts.clone(),
final_accounts: env.test_entry.final_accounts.clone(),
..SvmTestEntry::default()
};
test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
env.test_entry = test_entry;
env.execute();
assert!(env.is_program_blocked(&target));
}
#[test]
fn program_cache_stats() {
let mut test_entry = SvmTestEntry::default();
let program_name = "hello-solana";
let noop_program = program_address(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let mut fee_payer_data = AccountSharedData::default();
fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 100);
test_entry.add_initial_account(fee_payer, &fee_payer_data);
test_entry
.initial_programs
.push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer)));
let missing_program = Pubkey::new_unique();
let buffer_address = Pubkey::new_unique();
{
let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer {
authority_address: Some(fee_payer),
})
.unwrap();
let mut program_bytecode = load_program(program_name.to_string());
data.append(&mut program_bytecode);
let buffer_account = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(data),
bpf_loader_upgradeable::id(),
true,
u64::MAX,
);
test_entry.add_initial_account(buffer_address, &buffer_account);
}
let make_transaction = |instructions: &[Instruction]| {
Transaction::new_signed_with_payer(
instructions,
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
)
};
let successful_noop_instruction = Instruction::new_with_bytes(noop_program, &[], vec![]);
let successful_transfer_instruction =
system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL);
let failing_transfer_instruction =
system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL * 1000);
let fee_only_noop_instruction = Instruction::new_with_bytes(missing_program, &[], vec![]);
let mut noop_tx_usage = 0;
let mut system_tx_usage = 0;
let mut successful_transfers = 0;
test_entry.push_transaction(make_transaction(slice::from_ref(
&successful_noop_instruction,
)));
noop_tx_usage += 1;
test_entry.push_transaction(make_transaction(slice::from_ref(
&successful_transfer_instruction,
)));
system_tx_usage += 1;
successful_transfers += 1;
test_entry.push_transaction_with_status(
make_transaction(slice::from_ref(&failing_transfer_instruction)),
ExecutionStatus::ExecutedFailed,
);
system_tx_usage += 1;
test_entry.push_transaction(make_transaction(&[
successful_noop_instruction.clone(),
successful_noop_instruction.clone(),
successful_transfer_instruction.clone(),
successful_transfer_instruction.clone(),
successful_noop_instruction.clone(),
]));
noop_tx_usage += 1;
system_tx_usage += 1;
successful_transfers += 2;
test_entry.push_transaction_with_status(
make_transaction(&[
failing_transfer_instruction,
successful_noop_instruction.clone(),
successful_transfer_instruction.clone(),
]),
ExecutionStatus::ExecutedFailed,
);
noop_tx_usage += 1;
system_tx_usage += 1;
test_entry.push_transaction_with_status(
make_transaction(&[
successful_noop_instruction.clone(),
fee_only_noop_instruction,
]),
ExecutionStatus::ProcessedFailed,
);
test_entry.decrease_expected_lamports(
&fee_payer,
LAMPORTS_PER_SIGNATURE * test_entry.transaction_batch.len() as u64
+ LAMPORTS_PER_SOL * successful_transfers,
);
test_entry.transaction_batch.push(TransactionBatchItem {
transaction: make_transaction(slice::from_ref(&successful_transfer_instruction)),
check_result: Err(TransactionError::BlockhashNotFound),
asserts: ExecutionStatus::Discarded.into(),
});
let mut env = SvmTestEnvironment::create(test_entry);
env.execute();
let global_program_cache = env
.batch_processor
.global_program_cache
.read()
.unwrap()
.get_flattened_entries_for_tests()
.into_iter()
.rev()
.collect::<Vec<_>>();
let (_, noop_entry) = global_program_cache
.iter()
.find(|(pubkey, _)| *pubkey == noop_program)
.unwrap();
assert_eq!(
noop_entry.stats.uses.load(Ordering::Relaxed),
noop_tx_usage,
"noop_tx_usage matches"
);
let (_, system_entry) = global_program_cache
.iter()
.find(|(pubkey, _)| *pubkey == system_program::id())
.unwrap();
assert_eq!(
system_entry.stats.uses.load(Ordering::Relaxed),
system_tx_usage,
"system_tx_usage matches"
);
assert!(
!global_program_cache
.iter()
.any(|(pubkey, _)| *pubkey == missing_program),
"missing_program is missing"
);
let mut test_entry = SvmTestEntry {
initial_accounts: env.test_entry.final_accounts.clone(),
final_accounts: env.test_entry.final_accounts.clone(),
..SvmTestEntry::default()
};
test_entry.push_transaction(Transaction::new_signed_with_payer(
&[loaderv3_instruction::upgrade(
&noop_program,
&buffer_address,
&fee_payer,
&Pubkey::new_unique(),
)],
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
));
noop_tx_usage += 1;
test_entry.drop_expected_account(buffer_address);
test_entry.push_transaction_with_status(
make_transaction(slice::from_ref(&successful_noop_instruction)),
ExecutionStatus::ExecutedFailed,
);
noop_tx_usage += 1;
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
env.test_entry = test_entry;
env.execute();
let (_, noop_entry) = env
.batch_processor
.global_program_cache
.read()
.unwrap()
.get_flattened_entries_for_tests()
.into_iter()
.rev()
.find(|(pubkey, _)| *pubkey == noop_program)
.unwrap();
assert_eq!(
noop_entry.stats.uses.load(Ordering::Relaxed),
noop_tx_usage,
"noop_tx_usage matches"
);
let mut test_entry = SvmTestEntry {
initial_accounts: env.test_entry.final_accounts.clone(),
final_accounts: env.test_entry.final_accounts.clone(),
..SvmTestEntry::default()
};
test_entry.push_transaction_with_status(
make_transaction(slice::from_ref(&successful_noop_instruction)),
ExecutionStatus::ExecutedFailed,
);
noop_tx_usage += 1;
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
env.test_entry = test_entry;
env.execute();
let (_, noop_entry) = env
.batch_processor
.global_program_cache
.read()
.unwrap()
.get_flattened_entries_for_tests()
.into_iter()
.rev()
.find(|(pubkey, _)| *pubkey == noop_program)
.unwrap();
assert_eq!(
noop_entry.stats.uses.load(Ordering::Relaxed),
noop_tx_usage,
"noop_tx_usage matches"
);
}
#[derive(Clone, PartialEq, Eq)]
enum Inspect<'a> {
LiveRead(&'a AccountSharedData),
LiveWrite(&'a AccountSharedData),
#[allow(dead_code)]
DeadRead,
DeadWrite,
}
impl From<Inspect<'_>> for (Option<AccountSharedData>, bool) {
fn from(inspect: Inspect) -> Self {
match inspect {
Inspect::LiveRead(account) => (Some(account.clone()), false),
Inspect::LiveWrite(account) => (Some(account.clone()), true),
Inspect::DeadRead => (None, false),
Inspect::DeadWrite => (None, true),
}
}
}
#[derive(Clone, Default)]
struct InspectedAccounts(pub HashMap<Pubkey, Vec<(Option<AccountSharedData>, bool)>>);
impl InspectedAccounts {
fn inspect(&mut self, pubkey: Pubkey, inspect: Inspect) {
self.0.entry(pubkey).or_default().push(inspect.into())
}
}
#[test_case(false; "separate_nonce::old")]
#[test_case(true; "fee_paying_nonce::old")]
fn svm_inspect_nonce_load_failure(fee_paying_nonce: bool) {
let mut test_entry = SvmTestEntry::default();
let mut expected_inspected_accounts = InspectedAccounts::default();
let fee_payer_keypair = Keypair::new();
let separate_nonce_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let nonce_pubkey = if fee_paying_nonce {
fee_payer
} else {
separate_nonce_keypair.pubkey()
};
let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique());
let initial_nonce_data =
nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE);
let mut initial_nonce_account = AccountSharedData::new_data(
LAMPORTS_PER_SOL,
&nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)),
&system_program::id(),
)
.unwrap();
initial_nonce_account.set_rent_epoch(u64::MAX);
let initial_nonce_account = initial_nonce_account;
let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone());
let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH);
let mut advanced_nonce_info = initial_nonce_info;
advanced_nonce_info
.try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE)
.unwrap();
let compute_instruction = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(1);
let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer);
let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]);
test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account);
let mut separate_fee_payer_account = AccountSharedData::default();
separate_fee_payer_account.set_lamports(LAMPORTS_PER_SOL);
let separate_fee_payer_account = separate_fee_payer_account;
expected_inspected_accounts.inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account));
if fee_paying_nonce {
expected_inspected_accounts
.inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account));
} else {
test_entry.add_initial_account(fee_payer, &separate_fee_payer_account);
expected_inspected_accounts
.inspect(fee_payer, Inspect::LiveWrite(&separate_fee_payer_account));
}
let transaction = Transaction::new_signed_with_payer(
&[
advance_instruction,
compute_instruction,
fee_only_noop_instruction,
],
Some(&fee_payer),
&[&fee_payer_keypair],
*initial_durable.as_hash(),
);
test_entry.push_nonce_transaction_with_status(
transaction,
nonce_pubkey,
ExecutionStatus::ProcessedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE);
test_entry
.final_accounts
.get_mut(&nonce_pubkey)
.unwrap()
.data_as_mut_slice()
.copy_from_slice(advanced_nonce_info.account().data());
let env = SvmTestEnvironment::create(test_entry.clone());
env.execute();
let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone();
for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 {
let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap();
assert_eq!(
expected_account, actual_account,
"pubkey: {expected_pubkey}",
);
}
}
#[test]
fn svm_inspect_account() {
let mut initial_test_entry = SvmTestEntry::default();
let mut expected_inspected_accounts = InspectedAccounts::default();
let fee_payer_keypair = Keypair::new();
let sender_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let sender = sender_keypair.pubkey();
let recipient = Pubkey::new_unique();
let mut fee_payer_account = AccountSharedData::default();
fee_payer_account.set_lamports(10_000_000);
fee_payer_account.set_rent_epoch(u64::MAX);
initial_test_entry.add_initial_account(fee_payer, &fee_payer_account);
expected_inspected_accounts.inspect(fee_payer, Inspect::LiveWrite(&fee_payer_account));
let mut sender_account = AccountSharedData::default();
sender_account.set_lamports(11_000_000);
sender_account.set_rent_epoch(u64::MAX);
initial_test_entry.add_initial_account(sender, &sender_account);
expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&sender_account));
expected_inspected_accounts.inspect(recipient, Inspect::DeadWrite);
let system_account = AccountSharedData::create_from_existing_shared_data(
5000,
Arc::new("system_program".as_bytes().to_vec()),
native_loader::id(),
true,
0,
);
expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account));
let transfer_amount = 1_000_000;
let transaction = Transaction::new_signed_with_payer(
&[system_instruction::transfer(
&sender,
&recipient,
transfer_amount,
)],
Some(&fee_payer),
&[&fee_payer_keypair, &sender_keypair],
Hash::default(),
);
initial_test_entry.push_transaction(transaction);
let mut recipient_account = AccountSharedData::default();
recipient_account.set_lamports(transfer_amount);
initial_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
initial_test_entry.decrease_expected_lamports(&sender, transfer_amount);
initial_test_entry.create_expected_account(recipient, &recipient_account);
let initial_test_entry = initial_test_entry;
let mut env = SvmTestEnvironment::create(initial_test_entry.clone());
env.execute();
let intermediate_fee_payer_account = initial_test_entry
.final_accounts
.get(&fee_payer)
.cloned()
.unwrap();
expected_inspected_accounts.inspect(
fee_payer,
Inspect::LiveWrite(&intermediate_fee_payer_account),
);
let intermediate_sender_account = initial_test_entry
.final_accounts
.get(&sender)
.cloned()
.unwrap();
expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&intermediate_sender_account));
let intermediate_recipient_account = initial_test_entry
.final_accounts
.get(&recipient)
.cloned()
.unwrap();
expected_inspected_accounts.inspect(
recipient,
Inspect::LiveWrite(&intermediate_recipient_account),
);
expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account));
let mut final_test_entry = SvmTestEntry {
initial_accounts: initial_test_entry.final_accounts.clone(),
final_accounts: initial_test_entry.final_accounts,
..SvmTestEntry::default()
};
let transfer_amount = 456;
let transaction = Transaction::new_signed_with_payer(
&[system_instruction::transfer(
&sender,
&recipient,
transfer_amount,
)],
Some(&fee_payer),
&[&fee_payer_keypair, &sender_keypair],
Hash::default(),
);
final_test_entry.push_transaction(transaction);
final_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
final_test_entry.decrease_expected_lamports(&sender, transfer_amount);
final_test_entry.increase_expected_lamports(&recipient, transfer_amount);
env.test_entry = final_test_entry;
env.execute();
let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone();
for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 {
let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap();
assert_eq!(
expected_account, actual_account,
"pubkey: {expected_pubkey}",
);
}
let num_expected_inspected_accounts: usize =
expected_inspected_accounts.0.values().map(Vec::len).sum();
let num_actual_inspected_accounts: usize =
actual_inspected_accounts.values().map(Vec::len).sum();
assert_eq!(
num_expected_inspected_accounts,
num_actual_inspected_accounts,
);
}
#[test_case(false; "old_fee_only")]
#[test_case(true; "simd186_fee_only")]
fn fee_only_loaded_transaction_data_size(define_ltds_fee_only_semantics: bool) {
let mut common_test_entry = SvmTestEntry::default();
common_test_entry.feature_set.define_ltds_fee_only_semantics = define_ltds_fee_only_semantics;
let program_name = "hello-solana";
let program_id = program_address(program_name);
let loaded_program_size = (UpgradeableLoaderState::size_of_program()
+ program_data_size(program_name)
+ TRANSACTION_ACCOUNT_BASE_SIZE * 2) as u32;
common_test_entry.add_initial_program(program_name);
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let loaded_fee_payer_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32;
let fee_payer_data =
AccountSharedData::new_rent_epoch(LAMPORTS_PER_SOL, 0, &Pubkey::default(), u64::MAX);
common_test_entry.add_initial_account(fee_payer, &fee_payer_data);
let mut loaded_account_sizes = vec![];
for i in 9..=13 {
let base_size = 2_usize.pow(i);
let pubkey = Pubkey::new_unique();
let account_data = AccountSharedData::new_rent_epoch(
LAMPORTS_PER_SOL,
base_size,
&Pubkey::default(),
u64::MAX,
);
common_test_entry.add_initial_account(pubkey, &account_data);
loaded_account_sizes.push((pubkey, base_size + TRANSACTION_ACCOUNT_BASE_SIZE));
}
let common_test_entry = common_test_entry;
let transaction = |program_id: Pubkey, accounts: &[Pubkey], loaded_data_limit: Option<u32>| {
let account_metas = accounts
.iter()
.map(|pubkey| AccountMeta {
pubkey: *pubkey,
..AccountMeta::default()
})
.collect::<Vec<_>>();
let mut instructions = vec![];
if let Some(size) = loaded_data_limit {
instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size));
}
instructions.push(Instruction::new_with_bytes(program_id, &[], account_metas));
Transaction::new_signed_with_payer(
&instructions,
Some(&fee_payer),
&[&fee_payer_keypair],
Hash::default(),
)
};
for count in 0..loaded_account_sizes.len() {
let mut test_entry = common_test_entry.clone();
let (account_keys, other_accounts_size) =
&loaded_account_sizes[..count]
.iter()
.fold((vec![], 0), |mut acc, (pubkey, size)| {
acc.0.push(*pubkey);
acc.1 += *size as u32;
acc
});
let success_transaction = transaction(program_id, account_keys, None);
test_entry.push_transaction_with_status(success_transaction, ExecutionStatus::Succeeded);
let size_limit = (other_accounts_size / 2).max(1);
let fail_limit_transaction = transaction(program_id, account_keys, Some(size_limit));
test_entry
.push_transaction_with_status(fail_limit_transaction, ExecutionStatus::ProcessedFailed);
let fail_program_id_transaction = transaction(Pubkey::new_unique(), account_keys, None);
test_entry.push_transaction_with_status(
fail_program_id_transaction,
ExecutionStatus::ProcessedFailed,
);
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 3);
let env = SvmTestEnvironment::create(test_entry);
let output = env.execute();
let success_loaded_size = output.processing_results[0]
.as_ref()
.unwrap()
.loaded_accounts_data_size();
assert_eq!(
loaded_fee_payer_size + loaded_program_size + other_accounts_size,
success_loaded_size,
);
let fail_limit_loaded_size = output.processing_results[1]
.as_ref()
.unwrap()
.loaded_accounts_data_size();
assert_eq!(
if define_ltds_fee_only_semantics {
size_limit
} else {
0
},
fail_limit_loaded_size,
);
let fail_program_id_loaded_size = output.processing_results[2]
.as_ref()
.unwrap()
.loaded_accounts_data_size();
assert_eq!(
if define_ltds_fee_only_semantics {
loaded_fee_payer_size + other_accounts_size
} else {
0
},
fail_program_id_loaded_size,
);
}
}
#[test]
fn svm_metrics_accumulation() {
for test_entry in program_medley(false) {
let env = SvmTestEnvironment::create(test_entry);
let (transactions, check_results) = env.test_entry.prepare_transactions();
let result = env.batch_processor.load_and_execute_sanitized_transactions(
&env.mock_bank,
&transactions,
check_results,
&env.processing_environment,
&env.processing_config,
);
#[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))]
{
assert_ne!(
result
.execute_timings
.details
.create_executor_jit_compile_us
.0,
0
);
}
assert_ne!(
result.execute_timings.details.create_executor_load_elf_us.0,
0
);
assert_ne!(
result
.execute_timings
.details
.create_executor_verify_code_us
.0,
0
);
}
}
mod balance_collector {
use {
super::*,
rand::prelude::*,
solana_program_pack::Pack,
spl_generic_token::token_2022,
spl_token_interface::state::{
Account as TokenAccount, AccountState as TokenAccountState, Mint,
},
test_case::test_case,
};
const STARTING_BALANCE: u64 = LAMPORTS_PER_SOL * 100;
#[derive(Debug, Default)]
struct Transfer {
from: Pubkey,
to: Pubkey,
amount: u64,
}
impl Transfer {
fn new_rand(users: &[Pubkey]) -> Self {
let mut rng = rand::rng();
let [from_idx, to_idx] = (0..users.len()).choose_multiple(&mut rng, 2)[..] else {
unreachable!()
};
let from = users[from_idx];
let to = users[to_idx];
let amount = rng.random_range(1..STARTING_BALANCE / 100);
Self { from, to, amount }
}
fn to_system_instruction(&self) -> Instruction {
system_instruction::transfer(&self.from, &self.to, self.amount)
}
fn to_token_instruction(&self, fee_payer: &Pubkey) -> Instruction {
let mut instruction = spl_token_interface::instruction::transfer(
&spl_token_interface::id(),
&self.from,
&self.to,
fee_payer,
&[],
self.amount,
)
.unwrap();
instruction.accounts[0].is_signer = true;
instruction
}
fn to_instruction(&self, fee_payer: &Pubkey, use_tokens: bool) -> Instruction {
if use_tokens {
self.to_token_instruction(fee_payer)
} else {
self.to_system_instruction()
}
}
}
#[test_case(false; "native")]
#[test_case(true; "token")]
fn svm_collect_balances(use_tokens: bool) {
let mut rng = rand::rng();
let fee_payer_keypair = Keypair::new();
let fake_fee_payer_keypair = Keypair::new();
let alice_keypair = Keypair::new();
let bob_keypair = Keypair::new();
let charlie_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();
let fake_fee_payer = fake_fee_payer_keypair.pubkey();
let mint = Pubkey::new_unique();
let alice = alice_keypair.pubkey();
let bob = bob_keypair.pubkey();
let charlie = charlie_keypair.pubkey();
let native_state = AccountSharedData::create_from_existing_shared_data(
STARTING_BALANCE,
Arc::new(vec![]),
system_program::id(),
false,
u64::MAX,
);
let mut mint_buf = vec![0; Mint::get_packed_len()];
Mint {
decimals: 9,
is_initialized: true,
..Mint::default()
}
.pack_into_slice(&mut mint_buf);
let mint_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(mint_buf),
spl_token_interface::id(),
false,
u64::MAX,
);
let token_account_for_tests = || TokenAccount {
mint,
owner: fee_payer,
amount: STARTING_BALANCE,
state: TokenAccountState::Initialized,
..TokenAccount::default()
};
let mut token_buf = vec![0; TokenAccount::get_packed_len()];
token_account_for_tests().pack_into_slice(&mut token_buf);
let token_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(token_buf),
spl_token_interface::id(),
false,
u64::MAX,
);
let mut program_accounts =
solana_program_binaries::by_id(&spl_token_interface::id(), &Rent::default()).unwrap();
let (_, spl_token) = program_accounts.swap_remove(0);
let (program_data_key, program_data) = program_accounts.swap_remove(0);
for _ in 0..100 {
let mut test_entry = SvmTestEntry::default();
test_entry.add_initial_account(fee_payer, &native_state.clone());
if use_tokens {
test_entry.add_initial_account(spl_token_interface::id(), &spl_token);
test_entry.add_initial_account(program_data_key, &program_data);
test_entry.add_initial_account(mint, &mint_state);
test_entry.add_initial_account(alice, &token_state);
test_entry.add_initial_account(bob, &token_state);
test_entry.add_initial_account(charlie, &token_state);
} else {
test_entry.add_initial_account(alice, &native_state);
test_entry.add_initial_account(bob, &native_state);
test_entry.add_initial_account(charlie, &native_state);
}
let mut transaction_discards = vec![];
let mut user_balances = HashMap::new();
user_balances.insert(alice, STARTING_BALANCE);
user_balances.insert(bob, STARTING_BALANCE);
user_balances.insert(charlie, STARTING_BALANCE);
let mut user_balance_history = vec![(Transfer::default(), user_balances.clone())];
for _ in 0..50 {
let expected_status = match rng.random::<f64>() {
n if n < 0.85 => ExecutionStatus::Succeeded,
n if n < 0.90 => ExecutionStatus::ExecutedFailed,
n if n < 0.95 => ExecutionStatus::ProcessedFailed,
_ => ExecutionStatus::Discarded,
};
transaction_discards.push(expected_status == ExecutionStatus::Discarded);
let mut transfer = Transfer::new_rand(&[alice, bob, charlie]);
let from_signer = vec![&alice_keypair, &bob_keypair, &charlie_keypair]
.into_iter()
.find(|k| k.pubkey() == transfer.from)
.unwrap();
let instructions = match expected_status {
ExecutionStatus::Succeeded => {
user_balances
.entry(transfer.from)
.and_modify(|v| *v -= transfer.amount);
user_balances
.entry(transfer.to)
.and_modify(|v| *v += transfer.amount);
vec![transfer.to_instruction(&fee_payer, use_tokens)]
}
ExecutionStatus::ExecutedFailed => {
transfer.amount = u64::MAX / 2;
let instruction = transfer.to_instruction(&fee_payer, use_tokens);
transfer.amount = 0;
vec![instruction]
}
ExecutionStatus::ProcessedFailed => {
let mut instruction = transfer.to_instruction(&fee_payer, use_tokens);
instruction.program_id = token_2022::id();
transfer.amount = 0;
vec![instruction]
}
ExecutionStatus::Discarded => {
let mut instruction = transfer.to_instruction(&fee_payer, use_tokens);
if use_tokens {
instruction.accounts[2].pubkey = fake_fee_payer;
}
transfer.amount = 0;
vec![instruction]
}
};
let transaction = if expected_status.discarded() {
Transaction::new_signed_with_payer(
&instructions,
Some(&fake_fee_payer),
&[&fake_fee_payer_keypair, from_signer],
Hash::default(),
)
} else {
test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
Transaction::new_signed_with_payer(
&instructions,
Some(&fee_payer),
&[&fee_payer_keypair, from_signer],
Hash::default(),
)
};
test_entry.push_transaction_with_status(transaction, expected_status);
user_balance_history.push((transfer, user_balances.clone()));
}
if use_tokens {
let mut token_account = token_account_for_tests();
let mut token_buf = vec![0; TokenAccount::get_packed_len()];
token_account.amount = *user_balances.get(&alice).unwrap();
token_account.pack_into_slice(&mut token_buf);
let final_token_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(token_buf.clone()),
spl_token_interface::id(),
false,
u64::MAX,
);
test_entry.update_expected_account_data(alice, &final_token_state);
token_account.amount = *user_balances.get(&bob).unwrap();
token_account.pack_into_slice(&mut token_buf);
let final_token_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(token_buf.clone()),
spl_token_interface::id(),
false,
u64::MAX,
);
test_entry.update_expected_account_data(bob, &final_token_state);
token_account.amount = *user_balances.get(&charlie).unwrap();
token_account.pack_into_slice(&mut token_buf);
let final_token_state = AccountSharedData::create_from_existing_shared_data(
LAMPORTS_PER_SOL,
Arc::new(token_buf.clone()),
spl_token_interface::id(),
false,
u64::MAX,
);
test_entry.update_expected_account_data(charlie, &final_token_state);
} else {
let mut alice_final_state = native_state.clone();
alice_final_state.set_lamports(*user_balances.get(&alice).unwrap());
test_entry.update_expected_account_data(alice, &alice_final_state);
let mut bob_final_state = native_state.clone();
bob_final_state.set_lamports(*user_balances.get(&bob).unwrap());
test_entry.update_expected_account_data(bob, &bob_final_state);
let mut charlie_final_state = native_state.clone();
charlie_final_state.set_lamports(*user_balances.get(&charlie).unwrap());
test_entry.update_expected_account_data(charlie, &charlie_final_state);
}
let mut env = SvmTestEnvironment::create(test_entry);
env.processing_config
.recording_config
.enable_transaction_balance_recording = true;
let batch_output = env.execute();
let (pre_lamport_vecs, post_lamport_vecs, pre_token_vecs, post_token_vecs) =
batch_output.balance_collector.unwrap().into_vecs();
let mut running_fee_payer_balance = STARTING_BALANCE;
for (pre_bal, post_bal, was_discarded) in pre_lamport_vecs
.iter()
.zip(post_lamport_vecs.clone())
.zip(transaction_discards)
.map(|((pres, posts), discard)| (pres[0], posts[0], discard))
{
if was_discarded {
assert_eq!(pre_bal, 0);
assert_eq!(post_bal, 0);
continue;
}
let expected_post_balance = running_fee_payer_balance - LAMPORTS_PER_SIGNATURE * 2;
assert_eq!(pre_bal, running_fee_payer_balance);
assert_eq!(post_bal, expected_post_balance);
running_fee_payer_balance = expected_post_balance;
}
let (batch_pre, batch_post) = if use_tokens {
let pre_tupls: Vec<_> = pre_token_vecs
.iter()
.map(|bals| (bals[0].amount, bals[1].amount))
.collect();
let post_tupls: Vec<_> = post_token_vecs
.iter()
.map(|bals| (bals[0].amount, bals[1].amount))
.collect();
(pre_tupls, post_tupls)
} else {
let pre_tupls: Vec<_> = pre_lamport_vecs
.iter()
.map(|bals| (bals[1], bals[2]))
.collect();
let post_tupls: Vec<_> = post_lamport_vecs
.iter()
.map(|bals| (bals[1], bals[2]))
.collect();
(pre_tupls, post_tupls)
};
assert_eq!(user_balance_history.len(), batch_pre.len() + 1);
assert_eq!(user_balance_history.len(), batch_post.len() + 1);
for (i, (svm_pre_balances, svm_post_balances)) in
batch_pre.into_iter().zip(batch_post).enumerate()
{
let (_, ref expected_pre_balances) = user_balance_history[i];
let (ref transfer, ref expected_post_balances) = user_balance_history[i + 1];
assert_eq!(
svm_pre_balances.0,
*expected_pre_balances.get(&transfer.from).unwrap()
);
assert_eq!(
svm_pre_balances.1,
*expected_pre_balances.get(&transfer.to).unwrap()
);
assert_eq!(
svm_post_balances.0,
*expected_post_balances.get(&transfer.from).unwrap()
);
assert_eq!(
svm_post_balances.1,
*expected_post_balances.get(&transfer.to).unwrap()
);
}
}
}
}