From 39166714d6b582d3e8181f519f1cc1dbf530245a Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:49:18 -0800 Subject: [PATCH 1/6] svm: allow conflicting transactions in entries --- Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + svm/Cargo.toml | 1 + svm/src/account_loader.rs | 134 ++++++++++++++++++++++++++----- svm/src/account_overrides.rs | 2 +- svm/src/transaction_processor.rs | 24 ++++-- 6 files changed, 134 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be294c5cb7f1f2..6289d81ac4ca7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8678,6 +8678,7 @@ dependencies = [ name = "solana-svm" version = "2.2.0" dependencies = [ + "ahash 0.8.11", "assert_matches", "base64 0.22.1", "bincode", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index d705475746e296..4e7e92683a3072 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -7344,6 +7344,7 @@ dependencies = [ name = "solana-svm" version = "2.2.0" dependencies = [ + "ahash 0.8.11", "itertools 0.12.1", "log", "percentage", diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 7cc44fe7b9aa1f..a72d4f7753eecc 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -10,6 +10,7 @@ license = { workspace = true } edition = { workspace = true } [dependencies] +ahash = { workspace = true } itertools = { workspace = true } log = { workspace = true } percentage = { workspace = true } diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 97890f540d1ed4..89a73c9e1913da 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -4,8 +4,10 @@ use { nonce_info::NonceInfo, rollback_accounts::RollbackAccounts, transaction_error_metrics::TransactionErrorMetrics, + transaction_execution_result::ExecutedTransaction, transaction_processing_callback::{AccountState, TransactionProcessingCallback}, }, + ahash::AHashMap, solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, solana_feature_set::{self as feature_set, FeatureSet}, solana_program_runtime::loaded_programs::ProgramCacheForTxBatch, @@ -22,6 +24,7 @@ use { sysvar::{ self, instructions::{construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction}, + slot_history, }, transaction::{Result, TransactionError}, transaction_context::{IndexOfAccount, TransactionAccount}, @@ -97,23 +100,34 @@ pub struct FeesOnlyTransaction { #[cfg_attr(feature = "dev-context-only-utils", derive(Clone))] pub(crate) struct AccountLoader<'a, CB: TransactionProcessingCallback> { - account_overrides: Option<&'a AccountOverrides>, pub(crate) program_cache: ProgramCacheForTxBatch, program_accounts: HashMap, + account_cache: AHashMap, callbacks: &'a CB, pub(crate) feature_set: Arc, } impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { - pub fn new( + pub fn new_with_account_cache_capacity( account_overrides: Option<&'a AccountOverrides>, program_cache: ProgramCacheForTxBatch, program_accounts: HashMap, callbacks: &'a CB, feature_set: Arc, + capacity: usize, ) -> AccountLoader<'a, CB> { + let mut account_cache = AHashMap::with_capacity(capacity); + + // SlotHistory may be overridden for simulation. + // No other uses of AccountOverrides are expected. + if let Some(slot_history) = + account_overrides.and_then(|overrides| overrides.get(&slot_history::id())) + { + account_cache.insert(slot_history::id(), slot_history.clone()); + } + Self { program_cache, - account_overrides, + account_cache, callbacks, program_accounts, feature_set, @@ -131,43 +145,123 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { .feature_set .is_active(&feature_set::disable_account_loader_special_case::id()); - if let Some(account_override) = self - .account_overrides - .and_then(|overrides| overrides.get(account_key)) - { - Some(LoadedTransactionAccount { - loaded_size: account_override.data().len(), - account: account_override.clone(), - rent_collected: 0, - }) - } else if let Some(program) = (use_program_cache && is_invisible_read) + if let Some(program) = (use_program_cache && is_invisible_read) .then_some(()) .and_then(|_| self.program_cache.find(account_key)) { // Optimization to skip loading of accounts which are only used as // programs in top-level instructions and not passed as instruction accounts. - Some(LoadedTransactionAccount { + return Some(LoadedTransactionAccount { loaded_size: program.account_size, account: account_shared_data_from_program(account_key, &self.program_accounts) .ok()?, rent_collected: 0, - }) + }); + } + + let account = if let Some(account) = self.account_cache.get(account_key) { + // Inspect the account prior to collecting rent, since + // rent collection can modify the account. + self.callbacks + .inspect_account(account_key, AccountState::Alive(account), is_writable); + + // If lamports is 0, a previous transaction deallocated this account. + // Return None without inspecting, so it can be recreated. + if account.lamports() == 0 { + None + } else { + Some(account.clone()) + } } else if let Some(account) = self.callbacks.get_account_shared_data(account_key) { // Inspect the account prior to collecting rent, since // rent collection can modify the account. self.callbacks .inspect_account(account_key, AccountState::Alive(&account), is_writable); - Some(LoadedTransactionAccount { - loaded_size: account.data().len(), - account, - rent_collected: 0, - }) + self.account_cache.insert(*account_key, account.clone()); + + Some(account) } else { self.callbacks .inspect_account(account_key, AccountState::Dead, is_writable); None + }; + + account.map(|account| LoadedTransactionAccount { + loaded_size: account.data().len(), + account, + rent_collected: 0, + }) + } + + pub fn update_accounts_for_executed_tx( + &mut self, + message: &impl SVMMessage, + executed_transaction: &ExecutedTransaction, + ) { + if executed_transaction.was_successful() { + self.program_cache + .merge(&executed_transaction.programs_modified_by_tx); + + self.update_accounts_for_successful_tx( + message, + &executed_transaction.loaded_transaction.accounts, + ); + } else { + self.update_accounts_for_failed_tx( + message, + &executed_transaction.loaded_transaction.rollback_accounts, + ); + } + } + + pub fn update_accounts_for_failed_tx( + &mut self, + message: &impl SVMMessage, + rollback_accounts: &RollbackAccounts, + ) { + let fee_payer_address = message.fee_payer(); + match rollback_accounts { + RollbackAccounts::FeePayerOnly { fee_payer_account } => { + self.account_cache + .insert(*fee_payer_address, fee_payer_account.clone()); + } + RollbackAccounts::SameNonceAndFeePayer { nonce } => { + self.account_cache + .insert(*nonce.address(), nonce.account().clone()); + } + RollbackAccounts::SeparateNonceAndFeePayer { + nonce, + fee_payer_account, + } => { + self.account_cache + .insert(*nonce.address(), nonce.account().clone()); + self.account_cache + .insert(*fee_payer_address, fee_payer_account.clone()); + } + } + } + + fn update_accounts_for_successful_tx( + &mut self, + message: &impl SVMMessage, + transaction_accounts: &[TransactionAccount], + ) { + for (i, (address, account)) in (0..message.account_keys().len()).zip(transaction_accounts) { + if !message.is_writable(i) { + continue; + } + + // Accounts that are invoked and also not passed as an instruction + // account to a program don't need to be stored because it's assumed + // to be impossible for a committable transaction to modify an + // invoked account if said account isn't passed to some program. + if message.is_invoked(i) && !message.is_instruction_account(i) { + continue; + } + + self.account_cache.insert(*address, account.clone()); } } } diff --git a/svm/src/account_overrides.rs b/svm/src/account_overrides.rs index 7628b82f85d88e..c31dc5bac16eec 100644 --- a/svm/src/account_overrides.rs +++ b/svm/src/account_overrides.rs @@ -13,7 +13,7 @@ pub struct AccountOverrides { impl AccountOverrides { /// Insert or remove an account with a given pubkey to/from the list of overrides. - pub fn set_account(&mut self, pubkey: &Pubkey, account: Option) { + fn set_account(&mut self, pubkey: &Pubkey, account: Option) { match account { Some(account) => self.accounts.insert(*pubkey, account), None => self.accounts.remove(pubkey), diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 2862f3661ef641..0676b8702fccf0 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -389,12 +389,19 @@ impl TransactionBatchProcessor { program_cache_for_tx_batch }); - let mut account_loader = AccountLoader::new( + // Determine a capacity for the internal account cache. This + // over-allocates but avoids ever reallocating, and spares us from + // deduplicating the account keys lists. + let account_keys_in_batch = sanitized_txs.iter().map(|tx| tx.account_keys().len()).sum(); + + // Create the account loader, which wraps all external account fetching. + let mut account_loader = AccountLoader::new_with_account_cache_capacity( config.account_overrides, program_cache_for_tx_batch, program_accounts_map, callbacks, environment.feature_set.clone(), + account_keys_in_batch, ); let enable_transaction_loading_failure_fees = environment @@ -439,6 +446,10 @@ impl TransactionBatchProcessor { TransactionLoadResult::NotLoaded(err) => Err(err), TransactionLoadResult::FeesOnly(fees_only_tx) => { if enable_transaction_loading_failure_fees { + // Update loaded accounts cache with nonce and fee-payer + account_loader + .update_accounts_for_failed_tx(tx, &fees_only_tx.rollback_accounts); + Ok(ProcessedTransaction::FeesOnly(Box::new(fees_only_tx))) } else { Err(fees_only_tx.load_error) @@ -455,13 +466,10 @@ impl TransactionBatchProcessor { config, ); - // Update batch specific cache of the loaded programs with the modifications - // made by the transaction, if it executed successfully. - if executed_tx.was_successful() { - account_loader - .program_cache - .merge(&executed_tx.programs_modified_by_tx); - } + // Update loaded accounts cache with account states which might have changed. + // Also update local program cache with modifications made by the transaction, + // if it executed successfully. + account_loader.update_accounts_for_executed_tx(tx, &executed_tx); Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) } From cb9ca924ff662a9f1ccd11db8e953a6b842da880 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Tue, 5 Nov 2024 03:08:43 -0800 Subject: [PATCH 2/6] add integration tests back --- svm/tests/integration_test.rs | 1607 +++++++++++++++++++++++++++++++-- 1 file changed, 1552 insertions(+), 55 deletions(-) diff --git a/svm/tests/integration_test.rs b/svm/tests/integration_test.rs index 1e31dc997f7f46..fbb9e3abdce974 100644 --- a/svm/tests/integration_test.rs +++ b/svm/tests/integration_test.rs @@ -4,16 +4,19 @@ use { crate::mock_bank::{ create_custom_loader, deploy_program_with_upgrade_authority, program_address, - register_builtins, MockBankCallback, MockForkGraph, EXECUTION_EPOCH, EXECUTION_SLOT, - WALLCLOCK_TIME, + program_data_size, register_builtins, MockBankCallback, MockForkGraph, EXECUTION_EPOCH, + EXECUTION_SLOT, WALLCLOCK_TIME, }, solana_sdk::{ - account::{AccountSharedData, ReadableAccount, WritableAccount}, + account::{AccountSharedData, ReadableAccount, WritableAccount, PROGRAM_OWNERS}, + bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::Slot, compute_budget::ComputeBudgetInstruction, + entrypoint::MAX_PERMITTED_DATA_INCREASE, feature_set::{self, FeatureSet}, hash::Hash, instruction::{AccountMeta, Instruction}, + native_loader, native_token::LAMPORTS_PER_SOL, nonce::{self, state::DurableNonce}, pubkey::Pubkey, @@ -141,32 +144,72 @@ impl SvmTestEnvironment<'_> { // with SIMD83, an account might change multiple times in the same batch // but it might not exist on all transactions 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]; - for (index, processed_transaction) in batch_output.processing_results.iter().enumerate() { match processed_transaction { Ok(ProcessedTransaction::Executed(executed_transaction)) => { for (pubkey, account_data) in executed_transaction.loaded_transaction.accounts.clone() { - final_accounts_actual.insert(pubkey, account_data); + let is_writable = sanitized_transaction + .account_keys() + .iter() + .position(|key| key == &pubkey) + .map(|index| sanitized_transaction.is_writable(index)) + .unwrap_or(false); + + if is_writable { + update_or_dealloc_account( + &mut final_accounts_actual, + pubkey, + account_data, + ); + } } } Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => { - let fee_payer = transactions[index].fee_payer(); + let fee_payer = sanitized_transaction.fee_payer(); match fees_only_transaction.rollback_accounts.clone() { RollbackAccounts::FeePayerOnly { fee_payer_account } => { - final_accounts_actual.insert(*fee_payer, fee_payer_account); + update_or_dealloc_account( + &mut final_accounts_actual, + *fee_payer, + fee_payer_account, + ); } RollbackAccounts::SameNonceAndFeePayer { nonce } => { - final_accounts_actual.insert(*nonce.address(), nonce.account().clone()); + update_or_dealloc_account( + &mut final_accounts_actual, + *nonce.address(), + nonce.account().clone(), + ); } RollbackAccounts::SeparateNonceAndFeePayer { nonce, fee_payer_account, } => { - final_accounts_actual.insert(*fee_payer, fee_payer_account); - final_accounts_actual.insert(*nonce.address(), nonce.account().clone()); + update_or_dealloc_account( + &mut final_accounts_actual, + *fee_payer, + fee_payer_account, + ); + update_or_dealloc_account( + &mut final_accounts_actual, + *nonce.address(), + nonce.account().clone(), + ); } } } @@ -216,8 +259,9 @@ impl SvmTestEnvironment<'_> { } } + // merge new account states into the bank for multi-batch tests let mut mock_bank_accounts = self.mock_bank.account_shared_data.write().unwrap(); - *mock_bank_accounts = final_accounts_actual; + mock_bank_accounts.extend(final_accounts_actual); } } @@ -276,6 +320,14 @@ impl SvmTestEntry { assert!(self.final_accounts.insert(pubkey, account).is_some()); } + // indicate that an existing account is expected to be deallocated + pub fn drop_expected_account(&mut self, pubkey: Pubkey) { + assert!(self + .final_accounts + .insert(pubkey, AccountSharedData::default()) + .is_some()); + } + // add lamports to an existing expected final account state pub fn increase_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) { self.final_accounts @@ -1083,58 +1135,1490 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V vec![test_entry] } -#[allow(unused)] -#[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, - ) -> 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::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)]) - } - }; +fn simd83_intrabatch_account_reuse(enable_fee_only_transactions: bool) -> Vec { + let mut test_entries = vec![]; + let transfer_amount = LAMPORTS_PER_SOL; + let wallet_rent = Rent::default().minimum_balance(0); - let mut instructions = vec![]; + // batch 0: two successful transfers from the same source + { + let mut test_entry = SvmTestEntry::default(); - if let Some(size) = clamp_data_size { - instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size)); + 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); } - instructions.push(Instruction::new_with_bytes( - program_id, - &instruction_data, - account_metas, + test_entries.push(test_entry); + } + + // batch 1: + // * successful transfer, source left with rent-exempt minimum + // * non-processable transfer due to underfunded fee-payer + { + 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(), )); - Transaction::new_signed_with_payer( - &instructions, - Some(&fee_payer.pubkey()), - &[fee_payer], + 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); + } + + // batch 2: + // * successful transfer to a previously unfunded account + // * successful transfer using the new account as a fee-payer in the same batch + { + 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); + } + + // batch 3: + // * non-processable transfer due to underfunded fee-payer (two signatures) + // * successful transfer with the same fee-payer (one signature) + { + 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); + } + + // batch 4: + // * processable non-executable transaction + // * successful transfer + // this confirms we update the AccountsMap from RollbackAccounts intrabatch + if enable_fee_only_transactions { + 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); + } + + for test_entry in &mut test_entries { + if enable_fee_only_transactions { + test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + } + + test_entries +} + +fn simd83_nonce_reuse( + enable_fee_only_transactions: bool, + fee_paying_nonce: bool, +) -> Vec { + 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::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 initial_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::state::Versions::new(nonce::State::Initialized(initial_nonce_data.clone())), + &system_program::id(), + ) + .unwrap(); + 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.clone(); + 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; + + // batch 0: one transaction that advances the nonce twice + { + 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, + initial_nonce_info.clone(), + ExecutionStatus::ExecutedFailed, + ); + + test_entries.push(test_entry); + } + + // batch 1: + // * a successful nonce transaction + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + 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, initial_nonce_info.clone()); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); } + + // batch 2: + // * an executable failed nonce transaction + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + 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, + initial_nonce_info.clone(), + ExecutionStatus::ExecutedFailed, + ); + + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 3: + // * a processable non-executable nonce transaction, if fee-only transactions are enabled + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + if enable_fee_only_transactions { + 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, + initial_nonce_info.clone(), + ExecutionStatus::ProcessedFailed, + ); + + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + // if the nonce account pays fees, it keeps its new rent epoch, otherwise it resets + if !fee_paying_nonce { + test_entry + .final_accounts + .get_mut(&nonce_pubkey) + .unwrap() + .set_rent_epoch(0); + } + + test_entries.push(test_entry); + } + + // batch 4: + // * a successful blockhash transaction that also advances the nonce + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[ + successful_noop_instruction.clone(), + advance_instruction.clone(), + ], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_nonce_transaction(first_transaction, initial_nonce_info.clone()); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + for test_entry in &mut test_entries { + test_entry.add_initial_program(program_name); + + if enable_fee_only_transactions { + test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + } + + // batch 5: + // * a successful blockhash transaction that closes the nonce + // * a nonce transaction that uses the nonce; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[withdraw_instruction.clone()], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entry.increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL); + + test_entry.drop_expected_account(nonce_pubkey); + + test_entries.push(test_entry); + } + + // batch 6: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that funds the closed account + // * a nonce transaction that uses the account; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[withdraw_instruction.clone()], + 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(), + advanced_nonce_info.clone(), + 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); + } + + // batch 7: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that reopens the account with proper nonce size + // * a nonce transaction that uses the account; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[withdraw_instruction.clone()], + 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(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + let new_nonce_state = AccountSharedData::create( + LAMPORTS_PER_SOL, + 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); + } + + // batch 8: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that reopens the nonce + // * a nonce transaction that uses the nonce; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[withdraw_instruction.clone()], + 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(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + test_entries.push(test_entry); + } + + // batch 9: + // * a successful blockhash noop transaction + // * a nonce transaction that uses a spoofed nonce account; this transaction must be dropped + // check_age would never let such a transaction through validation + // this simulates the case where someone closes a nonce account, then reuses the address in the same batch + // but as a non-system account that parses as an initialized nonce account + 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( + &[successful_noop_instruction.clone()], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + advanced_nonce_info.clone(), + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 10: + // * a successful blockhash transaction that changes the nonce authority + // * a nonce transaction that uses the nonce with the old authority; this transaction must be dropped + 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.clone(), + advanced_nonce_info.clone(), + 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::state::Versions::new(nonce::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); + } + + // batch 11: + // * a successful blockhash transaction that changes the nonce authority + // * a nonce transaction that uses the nonce with the new authority; this transaction succeeds + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + 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.clone(), + ], + Some(&fee_payer), + &[&fee_payer_keypair, &new_authority_keypair], + *advanced_durable.as_hash(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction(second_transaction.clone(), advanced_nonce_info.clone()); + + 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::state::Versions::new(nonce::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); + + if enable_fee_only_transactions { + test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + } + + 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, + ) -> 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::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 { + let mut test_entries = vec![]; + + // batch 0: sanity check, the program actually sets data + // batch 1: removing lamports from account hides it from subsequent in-batch transactions + 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( + Rent::default().minimum_balance(1), + 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(enable_fee_only_transactions: bool) -> Vec { + let mut test_entry = SvmTestEntry::default(); + if enable_fee_only_transactions { + test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + + let program_name = "hello-solana"; + let real_program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + // 0/1: a rent-paying fee-payer goes to zero lamports on an executed transaction, the batch sees it as deallocated + // 2/3: the same, except if fee-only transactions are enabled, it goes to zero lamports from a a fee-only transaction + for do_fee_only_transaction in if enable_fee_only_transactions { + vec![false, true] + } else { + vec![false] + } { + 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); + + // transaction which drains a fee-payer + 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); + + // as noted in `account_deallocate()` we must touch the account to see if anything actually happened + 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); + } + + // 4: a rent-paying non-nonce fee-payer goes to zero on a fee-only nonce transaction, the batch sees it as deallocated + // we test in `simple_nonce()` that nonce fee-payers cannot as a rule be brought below rent-exemption + if enable_fee_only_transactions { + 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 initial_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::state::Versions::new(nonce::State::Initialized(initial_nonce_data.clone())), + &system_program::id(), + ) + .unwrap(); + 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.clone(); + advanced_nonce_info + .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) + .unwrap(); + + 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![]); + + // fee-only nonce transaction which drains a fee-payer + let transaction = Transaction::new_signed_with_payer( + &[advance_instruction, fee_only_noop_instruction], + Some(&dealloc_fee_payer), + &[&dealloc_fee_payer_keypair], + Hash::default(), + ); + test_entry.push_transaction_with_status(transaction, ExecutionStatus::ProcessedFailed); + + test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE); + + // as noted in `account_deallocate()` we must touch the account to see if anything actually happened + 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(enable_fee_only_transactions: bool) -> Vec { + 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); + if enable_fee_only_transactions { + common_test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + + 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( + LAMPORTS_PER_SOL * 10, + 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 print_transaction = WriteProgramInstruction::Print.create_transaction( + program_id, + &fee_payer_keypair, + target, + Some( + (program_size + MAX_PERMITTED_DATA_INCREASE) + .try_into() + .unwrap(), + ), + ); + + common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + let common_test_entry = common_test_entry; + + // batch 0/1: + // * successful realloc up/down + // * change reflected in same batch + 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); + } + + // batch 2: + // * successful large realloc up + // * transaction is aborted based on the new transaction data size post-realloc + { + let mut test_entry = common_test_entry.clone(); + + let new_target_size = target_start_size + MAX_PERMITTED_DATA_INCREASE; + let expected_print_status = if enable_fee_only_transactions { + ExecutionStatus::ProcessedFailed + } else { + test_entry.increase_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + ExecutionStatus::Discarded + }; + + 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.clone(), expected_print_status); + + test_entry.update_expected_account_data(target, &mk_target(new_target_size)); + + test_entries.push(test_entry); + } + + test_entries +} + +fn simd83_bpf_loader_buffer(enable_fee_only_transactions: bool) -> Vec { + let mut test_entries = vec![]; + + let mut common_test_entry = SvmTestEntry::default(); + common_test_entry + .enabled_features + .push(feature_set::enable_account_loader_cache::id()); + if enable_fee_only_transactions { + common_test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + + let program_name = "write-to-account"; + let program_id = program_address(program_name); + 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); + fee_payer_data.set_rent_epoch(u64::MAX); + common_test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let buffer_keypair = Keypair::new(); + let buffer = buffer_keypair.pubkey(); + + let mut buffer_bytes = vec![0; MAX_PERMITTED_DATA_INCREASE]; + bincode::serialize_into( + &mut buffer_bytes, + &UpgradeableLoaderState::Buffer { + authority_address: Some(fee_payer), + }, + ) + .unwrap(); + + // this is used to test non-buffer accounts for all loaders + let mut throwaway_account_owners = vec![None]; + for program_owner in PROGRAM_OWNERS { + throwaway_account_owners.push(Some(*program_owner)); + } + + let initial_buffer_data = AccountSharedData::create( + LAMPORTS_PER_SOL, + buffer_bytes.clone(), + bpf_loader_upgradeable::id(), + false, + u64::MAX, + ); + common_test_entry.add_initial_account(buffer, &initial_buffer_data); + + // bpf_loader_upgradeable does not reassign closed accounts to system + // so reopening a closed buffer under a new owner can only be done in a subsequent transaction + let close_transaction = Transaction::new_signed_with_payer( + &[bpf_loader_upgradeable::close( + &buffer, &fee_payer, &fee_payer, + )], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + // make a transaction to reopen an account with a given size + let reopen = |new_size: usize| { + let reopen_transaction = Transaction::new_signed_with_payer( + &[system_instruction::create_account( + &fee_payer, + &buffer, + LAMPORTS_PER_SOL, + new_size as u64, + &system_program::id(), + )], + Some(&fee_payer), + &[&fee_payer_keypair, &buffer_keypair], + Hash::default(), + ); + + let reopened_buffer_data = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; new_size], + system_program::id(), + false, + u64::MAX, + ); + + (reopen_transaction, reopened_buffer_data) + }; + + let print_transaction = WriteProgramInstruction::Print.create_transaction( + program_id, + &fee_payer_keypair, + buffer, + None, + ); + + let common_test_entry = common_test_entry; + + // batch 0: + // * check buffer size (must be account size) + { + let mut test_entry = common_test_entry.clone(); + + test_entry.push_transaction(print_transaction.clone()); + test_entry.transaction_batch[0] + .asserts + .logs + .push(format!("Program log: account size {}", buffer_bytes.len(),)); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + test_entries.push(test_entry); + } + + // batch 1: + // * close buffer + // * check buffer size (must be zero) + { + let mut test_entry = common_test_entry.clone(); + + test_entry.push_transaction(close_transaction.clone()); + test_entry.push_transaction(print_transaction.clone()); + test_entry.transaction_batch[1] + .asserts + .logs + .push("Program log: account size 0".to_string()); + + test_entry + .increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL - LAMPORTS_PER_SIGNATURE * 2); + test_entry.drop_expected_account(buffer); + + test_entries.push(test_entry); + } + + // batch 2: + // * close buffer + // * reopen as system account of large size + // * check buffer size (must be new size) + { + let mut test_entry = common_test_entry.clone(); + + let (reopen_transaction, reopened_buffer_data) = reopen(MAX_PERMITTED_DATA_INCREASE); + + test_entry.push_transaction(close_transaction.clone()); + test_entry.push_transaction(reopen_transaction); + test_entry.push_transaction(print_transaction); + test_entry.transaction_batch[2].asserts.logs.push(format!( + "Program log: account size {}", + MAX_PERMITTED_DATA_INCREASE, + )); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); + test_entry.update_expected_account_data(buffer, &reopened_buffer_data); + + test_entries.push(test_entry); + } + + // HANA TODO this is kind of nonsense with the new design + // it tests an escape hatch that is no longer there + // however it might retain some merit if its possible to reopen a programdata account + // i asked in discord if it was ever user-settable + // if no one knows i can just pull every program account from mainnet ig + // also remember to update the paragraph below if i change this test + // + // batch 3/4: + // * close buffer + // * reopen as (3: small, 4: large) system account + // * include account as invisible and use compute budget to check size + // + // batch 3 is a sanity check that compute budget failure depends only on the new account size + // it succeeds because we create a 1 byte account with a 10kb transaction data size limit + // + // in batch 4 we expect the third transaction to see a 10kb account + // this fails given a 10kb limit because there are some other accounts of negligible size + // this codepath will find it in the accounts cache, then find it in the program cache + // the program cache check normally substitutes a different transaction size + // this is so read-only non-instruction upgradeable program sizes include programdata + // in this case, however, we do *not* use the program cache size (which is always 0) + // because the program cache does not evict entries, even if the account was closed + // + // the test account must be read-only non-instruction, so we append it to account keys + // we use a system program transfer for our no-op to simplify size math because its only 14 bytes + for sanity_check in [true, false] { + let mut test_entry = common_test_entry.clone(); + + let new_account_size = if sanity_check { + 1 + } else { + MAX_PERMITTED_DATA_INCREASE + }; + + let (reopen_transaction, reopened_buffer_data) = reopen(new_account_size); + + let mut noop_transaction = Transaction::new_with_payer( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_PERMITTED_DATA_INCREASE as u32, + ), + system_instruction::transfer(&fee_payer, &fee_payer, 0), + ], + Some(&fee_payer), + ); + + noop_transaction.message.account_keys.push(buffer); + noop_transaction + .message + .header + .num_readonly_unsigned_accounts += 1; + noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); + + let expected_status = if sanity_check { + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); + ExecutionStatus::Succeeded + } else if enable_fee_only_transactions { + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); + ExecutionStatus::ProcessedFailed + } else { + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 3); + ExecutionStatus::Discarded + }; + + test_entry.push_transaction(close_transaction.clone()); + test_entry.push_transaction(reopen_transaction); + test_entry.push_transaction_with_status(noop_transaction, expected_status); + + test_entry.update_expected_account_data(buffer, &reopened_buffer_data); + + test_entries.push(test_entry); + } + + /* HANA TODO this test changes because of my feature gate + i believe i can just delete this, we always use real sizes for tombstoned cache entries now + i dont think there remains anything to actually test. maybe just test we get real sizes + oh i should actually test a closed program size goes to 36 tho + + // batch 5: buffer from program cache has zero size + // batch 6: same as above but force it into account cache + // batches 7-14: both of above, with an account owned by each loader + for throwaway_account_owner in throwaway_account_owners { + for transaction_count in 1..=2 { + let mut test_entry = common_test_entry.clone(); + + let target = match throwaway_account_owner { + None => buffer, + Some(loader_id) => { + let target = Pubkey::new_unique(); + let throwaway_account_data = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; MAX_PERMITTED_DATA_INCREASE], + loader_id, + false, + u64::MAX, + ); + test_entry.add_initial_account(target, &throwaway_account_data); + + target + } + }; + + // 10kb budget is a comfortable margin to ensure the calculated size of the target is zero + // if the real account size is used, the program sizes push us over this threshold + let mut noop_transaction = Transaction::new_with_payer( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_PERMITTED_DATA_INCREASE as u32, + ), + system_instruction::transfer(&fee_payer, &fee_payer, 0), + ], + Some(&fee_payer), + ); + + noop_transaction.message.account_keys.push(target); + noop_transaction + .message + .header + .num_readonly_unsigned_accounts += 1; + noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); + + for _ in 0..transaction_count { + test_entry.push_transaction(noop_transaction.clone()); + } + + test_entry + .decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * transaction_count); + + test_entries.push(test_entry); + } + } + */ + + // batch 15: non-builtin accounts owned by NativeLoader are never added to the cache + { + let mut test_entry = common_test_entry.clone(); + + let target = Pubkey::new_unique(); + + let throwaway_account_data = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; MAX_PERMITTED_DATA_INCREASE], + native_loader::id(), + false, + u64::MAX, + ); + + test_entry.add_initial_account(target, &throwaway_account_data); + + let mut noop_transaction = Transaction::new_with_payer( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( + MAX_PERMITTED_DATA_INCREASE as u32 / 2, + ), + system_instruction::transfer(&fee_payer, &fee_payer, 0), + ], + Some(&fee_payer), + ); + + noop_transaction.message.account_keys.push(target); + noop_transaction + .message + .header + .num_readonly_unsigned_accounts += 1; + noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); + + let expected_status = if enable_fee_only_transactions { + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + ExecutionStatus::ProcessedFailed + } else { + ExecutionStatus::Discarded + }; + + test_entry.push_transaction_with_status(noop_transaction, expected_status); + + test_entries.push(test_entry); + } + + test_entries } #[test_case(program_medley())] @@ -1144,6 +2628,19 @@ impl WriteProgramInstruction { #[test_case(simple_nonce(true, false))] #[test_case(simple_nonce(false, true))] #[test_case(simple_nonce(true, true))] +#[test_case(simd83_intrabatch_account_reuse(false))] +#[test_case(simd83_intrabatch_account_reuse(true))] +#[test_case(simd83_nonce_reuse(false, false))] +#[test_case(simd83_nonce_reuse(true, false))] +#[test_case(simd83_nonce_reuse(false, true))] +#[test_case(simd83_nonce_reuse(true, true))] +#[test_case(simd83_account_deallocate())] +#[test_case(simd83_fee_payer_deallocate(false))] +#[test_case(simd83_fee_payer_deallocate(true))] +#[test_case(simd83_account_reallocate(false))] +#[test_case(simd83_account_reallocate(true))] +#[test_case(simd83_bpf_loader_buffer(false))] +#[test_case(simd83_bpf_loader_buffer(true))] fn svm_integration(test_entries: Vec) { for test_entry in test_entries { let env = SvmTestEnvironment::create(test_entry); From 1b8a79c66c1b481656c8793527793cc89ba25552 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Tue, 5 Nov 2024 05:19:15 -0800 Subject: [PATCH 3/6] actually these tests are meaningless now --- svm/tests/integration_test.rs | 344 +--------------------------------- 1 file changed, 1 insertion(+), 343 deletions(-) diff --git a/svm/tests/integration_test.rs b/svm/tests/integration_test.rs index fbb9e3abdce974..2d7cc740f57413 100644 --- a/svm/tests/integration_test.rs +++ b/svm/tests/integration_test.rs @@ -8,15 +8,13 @@ use { EXECUTION_SLOT, WALLCLOCK_TIME, }, solana_sdk::{ - account::{AccountSharedData, ReadableAccount, WritableAccount, PROGRAM_OWNERS}, - bpf_loader_upgradeable::{self, UpgradeableLoaderState}, + account::{AccountSharedData, ReadableAccount, WritableAccount}, clock::Slot, compute_budget::ComputeBudgetInstruction, entrypoint::MAX_PERMITTED_DATA_INCREASE, feature_set::{self, FeatureSet}, hash::Hash, instruction::{AccountMeta, Instruction}, - native_loader, native_token::LAMPORTS_PER_SOL, nonce::{self, state::DurableNonce}, pubkey::Pubkey, @@ -2283,344 +2281,6 @@ fn simd83_account_reallocate(enable_fee_only_transactions: bool) -> Vec Vec { - let mut test_entries = vec![]; - - let mut common_test_entry = SvmTestEntry::default(); - common_test_entry - .enabled_features - .push(feature_set::enable_account_loader_cache::id()); - if enable_fee_only_transactions { - common_test_entry - .enabled_features - .push(feature_set::enable_transaction_loading_failure_fees::id()); - } - - let program_name = "write-to-account"; - let program_id = program_address(program_name); - 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); - fee_payer_data.set_rent_epoch(u64::MAX); - common_test_entry.add_initial_account(fee_payer, &fee_payer_data); - - let buffer_keypair = Keypair::new(); - let buffer = buffer_keypair.pubkey(); - - let mut buffer_bytes = vec![0; MAX_PERMITTED_DATA_INCREASE]; - bincode::serialize_into( - &mut buffer_bytes, - &UpgradeableLoaderState::Buffer { - authority_address: Some(fee_payer), - }, - ) - .unwrap(); - - // this is used to test non-buffer accounts for all loaders - let mut throwaway_account_owners = vec![None]; - for program_owner in PROGRAM_OWNERS { - throwaway_account_owners.push(Some(*program_owner)); - } - - let initial_buffer_data = AccountSharedData::create( - LAMPORTS_PER_SOL, - buffer_bytes.clone(), - bpf_loader_upgradeable::id(), - false, - u64::MAX, - ); - common_test_entry.add_initial_account(buffer, &initial_buffer_data); - - // bpf_loader_upgradeable does not reassign closed accounts to system - // so reopening a closed buffer under a new owner can only be done in a subsequent transaction - let close_transaction = Transaction::new_signed_with_payer( - &[bpf_loader_upgradeable::close( - &buffer, &fee_payer, &fee_payer, - )], - Some(&fee_payer), - &[&fee_payer_keypair], - Hash::default(), - ); - - // make a transaction to reopen an account with a given size - let reopen = |new_size: usize| { - let reopen_transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &fee_payer, - &buffer, - LAMPORTS_PER_SOL, - new_size as u64, - &system_program::id(), - )], - Some(&fee_payer), - &[&fee_payer_keypair, &buffer_keypair], - Hash::default(), - ); - - let reopened_buffer_data = AccountSharedData::create( - LAMPORTS_PER_SOL, - vec![0; new_size], - system_program::id(), - false, - u64::MAX, - ); - - (reopen_transaction, reopened_buffer_data) - }; - - let print_transaction = WriteProgramInstruction::Print.create_transaction( - program_id, - &fee_payer_keypair, - buffer, - None, - ); - - let common_test_entry = common_test_entry; - - // batch 0: - // * check buffer size (must be account size) - { - let mut test_entry = common_test_entry.clone(); - - test_entry.push_transaction(print_transaction.clone()); - test_entry.transaction_batch[0] - .asserts - .logs - .push(format!("Program log: account size {}", buffer_bytes.len(),)); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - - test_entries.push(test_entry); - } - - // batch 1: - // * close buffer - // * check buffer size (must be zero) - { - let mut test_entry = common_test_entry.clone(); - - test_entry.push_transaction(close_transaction.clone()); - test_entry.push_transaction(print_transaction.clone()); - test_entry.transaction_batch[1] - .asserts - .logs - .push("Program log: account size 0".to_string()); - - test_entry - .increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL - LAMPORTS_PER_SIGNATURE * 2); - test_entry.drop_expected_account(buffer); - - test_entries.push(test_entry); - } - - // batch 2: - // * close buffer - // * reopen as system account of large size - // * check buffer size (must be new size) - { - let mut test_entry = common_test_entry.clone(); - - let (reopen_transaction, reopened_buffer_data) = reopen(MAX_PERMITTED_DATA_INCREASE); - - test_entry.push_transaction(close_transaction.clone()); - test_entry.push_transaction(reopen_transaction); - test_entry.push_transaction(print_transaction); - test_entry.transaction_batch[2].asserts.logs.push(format!( - "Program log: account size {}", - MAX_PERMITTED_DATA_INCREASE, - )); - - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); - test_entry.update_expected_account_data(buffer, &reopened_buffer_data); - - test_entries.push(test_entry); - } - - // HANA TODO this is kind of nonsense with the new design - // it tests an escape hatch that is no longer there - // however it might retain some merit if its possible to reopen a programdata account - // i asked in discord if it was ever user-settable - // if no one knows i can just pull every program account from mainnet ig - // also remember to update the paragraph below if i change this test - // - // batch 3/4: - // * close buffer - // * reopen as (3: small, 4: large) system account - // * include account as invisible and use compute budget to check size - // - // batch 3 is a sanity check that compute budget failure depends only on the new account size - // it succeeds because we create a 1 byte account with a 10kb transaction data size limit - // - // in batch 4 we expect the third transaction to see a 10kb account - // this fails given a 10kb limit because there are some other accounts of negligible size - // this codepath will find it in the accounts cache, then find it in the program cache - // the program cache check normally substitutes a different transaction size - // this is so read-only non-instruction upgradeable program sizes include programdata - // in this case, however, we do *not* use the program cache size (which is always 0) - // because the program cache does not evict entries, even if the account was closed - // - // the test account must be read-only non-instruction, so we append it to account keys - // we use a system program transfer for our no-op to simplify size math because its only 14 bytes - for sanity_check in [true, false] { - let mut test_entry = common_test_entry.clone(); - - let new_account_size = if sanity_check { - 1 - } else { - MAX_PERMITTED_DATA_INCREASE - }; - - let (reopen_transaction, reopened_buffer_data) = reopen(new_account_size); - - let mut noop_transaction = Transaction::new_with_payer( - &[ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - MAX_PERMITTED_DATA_INCREASE as u32, - ), - system_instruction::transfer(&fee_payer, &fee_payer, 0), - ], - Some(&fee_payer), - ); - - noop_transaction.message.account_keys.push(buffer); - noop_transaction - .message - .header - .num_readonly_unsigned_accounts += 1; - noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); - - let expected_status = if sanity_check { - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); - ExecutionStatus::Succeeded - } else if enable_fee_only_transactions { - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 4); - ExecutionStatus::ProcessedFailed - } else { - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 3); - ExecutionStatus::Discarded - }; - - test_entry.push_transaction(close_transaction.clone()); - test_entry.push_transaction(reopen_transaction); - test_entry.push_transaction_with_status(noop_transaction, expected_status); - - test_entry.update_expected_account_data(buffer, &reopened_buffer_data); - - test_entries.push(test_entry); - } - - /* HANA TODO this test changes because of my feature gate - i believe i can just delete this, we always use real sizes for tombstoned cache entries now - i dont think there remains anything to actually test. maybe just test we get real sizes - oh i should actually test a closed program size goes to 36 tho - - // batch 5: buffer from program cache has zero size - // batch 6: same as above but force it into account cache - // batches 7-14: both of above, with an account owned by each loader - for throwaway_account_owner in throwaway_account_owners { - for transaction_count in 1..=2 { - let mut test_entry = common_test_entry.clone(); - - let target = match throwaway_account_owner { - None => buffer, - Some(loader_id) => { - let target = Pubkey::new_unique(); - let throwaway_account_data = AccountSharedData::create( - LAMPORTS_PER_SOL, - vec![0; MAX_PERMITTED_DATA_INCREASE], - loader_id, - false, - u64::MAX, - ); - test_entry.add_initial_account(target, &throwaway_account_data); - - target - } - }; - - // 10kb budget is a comfortable margin to ensure the calculated size of the target is zero - // if the real account size is used, the program sizes push us over this threshold - let mut noop_transaction = Transaction::new_with_payer( - &[ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - MAX_PERMITTED_DATA_INCREASE as u32, - ), - system_instruction::transfer(&fee_payer, &fee_payer, 0), - ], - Some(&fee_payer), - ); - - noop_transaction.message.account_keys.push(target); - noop_transaction - .message - .header - .num_readonly_unsigned_accounts += 1; - noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); - - for _ in 0..transaction_count { - test_entry.push_transaction(noop_transaction.clone()); - } - - test_entry - .decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * transaction_count); - - test_entries.push(test_entry); - } - } - */ - - // batch 15: non-builtin accounts owned by NativeLoader are never added to the cache - { - let mut test_entry = common_test_entry.clone(); - - let target = Pubkey::new_unique(); - - let throwaway_account_data = AccountSharedData::create( - LAMPORTS_PER_SOL, - vec![0; MAX_PERMITTED_DATA_INCREASE], - native_loader::id(), - false, - u64::MAX, - ); - - test_entry.add_initial_account(target, &throwaway_account_data); - - let mut noop_transaction = Transaction::new_with_payer( - &[ - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - MAX_PERMITTED_DATA_INCREASE as u32 / 2, - ), - system_instruction::transfer(&fee_payer, &fee_payer, 0), - ], - Some(&fee_payer), - ); - - noop_transaction.message.account_keys.push(target); - noop_transaction - .message - .header - .num_readonly_unsigned_accounts += 1; - noop_transaction.sign(&[&fee_payer_keypair], Hash::default()); - - let expected_status = if enable_fee_only_transactions { - test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); - ExecutionStatus::ProcessedFailed - } else { - ExecutionStatus::Discarded - }; - - test_entry.push_transaction_with_status(noop_transaction, expected_status); - - test_entries.push(test_entry); - } - - test_entries -} - #[test_case(program_medley())] #[test_case(simple_transfer(false))] #[test_case(simple_transfer(true))] @@ -2639,8 +2299,6 @@ fn simd83_bpf_loader_buffer(enable_fee_only_transactions: bool) -> Vec) { for test_entry in test_entries { let env = SvmTestEnvironment::create(test_entry); From 9e8eb4bc60b08b10129f6a3eadbac1e57979b896 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Sat, 9 Nov 2024 11:32:20 -0800 Subject: [PATCH 4/6] fix unit tests --- svm/src/account_loader.rs | 12 ++++-- svm/src/transaction_processor.rs | 64 +------------------------------- 2 files changed, 10 insertions(+), 66 deletions(-) diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 89a73c9e1913da..091351d1889b70 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -754,12 +754,13 @@ mod tests { impl<'a> From<&'a TestCallbacks> for AccountLoader<'a, TestCallbacks> { fn from(callbacks: &'a TestCallbacks) -> AccountLoader<'a, TestCallbacks> { - AccountLoader::new( + AccountLoader::new_with_account_cache_capacity( None, ProgramCacheForTxBatch::default(), HashMap::default(), callbacks, Arc::::default(), + 0, ) } } @@ -1098,12 +1099,13 @@ mod tests { accounts_map, ..Default::default() }; - let mut account_loader = AccountLoader::new( + let mut account_loader = AccountLoader::new_with_account_cache_capacity( account_overrides, ProgramCacheForTxBatch::default(), HashMap::default(), &callbacks, Arc::new(FeatureSet::all_enabled()), + 0, ); load_transaction( &mut account_loader, @@ -1545,12 +1547,13 @@ mod tests { )), ); - let mut account_loader = AccountLoader::new( + let mut account_loader = AccountLoader::new_with_account_cache_capacity( None, loaded_programs, program_accounts, &mock_bank, Arc::::default(), + 0, ); let mut error_metrics = TransactionErrorMetrics::default(); @@ -2544,12 +2547,13 @@ mod tests { program_cache.replenish(program2, Arc::new(program2_entry)); let test_transaction_data_size = |transaction, expected_size| { - let mut account_loader = AccountLoader::new( + let mut account_loader = AccountLoader::new_with_account_cache_capacity( None, program_cache.clone(), program_accounts.clone(), &mock_bank, Arc::::default(), + 0, ); let loaded_transaction_accounts = load_transaction_accounts( diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index 0676b8702fccf0..739e703ffc81ab 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -1303,12 +1303,13 @@ mod tests { impl<'a> From<&'a MockBankCallback> for AccountLoader<'a, MockBankCallback> { fn from(callbacks: &'a MockBankCallback) -> AccountLoader<'a, MockBankCallback> { - AccountLoader::new( + AccountLoader::new_with_account_cache_capacity( None, ProgramCacheForTxBatch::default(), HashMap::default(), callbacks, Arc::::default(), + 0, ) } } @@ -2568,67 +2569,6 @@ mod tests { } } - #[test] - fn test_validate_account_override_usage_on_validate_fee() { - /* - The test setups an account override with enough lamport to pass validate fee. - The account_db has the account with minimum rent amount thus would fail the validate_free. - The test verify that the override is used with a passing test of validate fee. - */ - let lamports_per_signature = 5000; - - let message = - new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - - let fee_payer_address = message.fee_payer(); - let transaction_fee = lamports_per_signature; - let rent_collector = RentCollector::default(); - let min_balance = rent_collector.rent.minimum_balance(0); - - let fee_payer_account = AccountSharedData::new(min_balance, 0, &Pubkey::default()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - - let necessary_balance = min_balance + transaction_fee; - let mut account_overrides = AccountOverrides::default(); - let fee_payer_account_override = - AccountSharedData::new(necessary_balance, 0, &Pubkey::default()); - account_overrides.set_account(fee_payer_address, Some(fee_payer_account_override)); - - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; - let mut account_loader = AccountLoader::new( - Some(&account_overrides), - ProgramCacheForTxBatch::default(), - HashMap::default(), - &mock_bank, - Arc::::default(), - ); - - let mut error_counters = TransactionErrorMetrics::default(); - - let result = - TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( - &mut account_loader, - &message, - CheckedTransactionDetails { - nonce: None, - lamports_per_signature, - }, - &Hash::default(), - FeeStructure::default().lamports_per_signature, - &rent_collector, - &mut error_counters, - ); - assert!( - result.is_ok(), - "test_account_override_used: {:?}", - result.err() - ); - } - // Ensure `TransactionProcessingCallback::inspect_account()` is called when // validating the fee payer, since that's when the fee payer account is loaded. #[test] From 96ef843c4ff63e131dbe2047dec3678d807f870f Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:03:51 -0800 Subject: [PATCH 5/6] fix concurrent test --- svm/tests/concurrent_tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/svm/tests/concurrent_tests.rs b/svm/tests/concurrent_tests.rs index 2364a9e760c47d..3eca183c44399a 100644 --- a/svm/tests/concurrent_tests.rs +++ b/svm/tests/concurrent_tests.rs @@ -153,6 +153,8 @@ fn svm_concurrent() { let read_account = Pubkey::new_unique(); let mut account_data = AccountSharedData::default(); account_data.set_data(AMOUNT.to_le_bytes().to_vec()); + account_data.set_rent_epoch(u64::MAX); + account_data.set_lamports(1); mock_bank .account_shared_data .write() From 755375d2666069c616d53c7e2283297cb66855e6 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 15 Nov 2024 07:29:35 -0800 Subject: [PATCH 6/6] improve writable lookup in integration tests --- svm/tests/integration_test.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/svm/tests/integration_test.rs b/svm/tests/integration_test.rs index 2d7cc740f57413..0273be1e32e648 100644 --- a/svm/tests/integration_test.rs +++ b/svm/tests/integration_test.rs @@ -157,21 +157,17 @@ impl SvmTestEnvironment<'_> { match processed_transaction { Ok(ProcessedTransaction::Executed(executed_transaction)) => { - for (pubkey, account_data) in - executed_transaction.loaded_transaction.accounts.clone() + for (index, (pubkey, account_data)) in executed_transaction + .loaded_transaction + .accounts + .iter() + .enumerate() { - let is_writable = sanitized_transaction - .account_keys() - .iter() - .position(|key| key == &pubkey) - .map(|index| sanitized_transaction.is_writable(index)) - .unwrap_or(false); - - if is_writable { + if sanitized_transaction.is_writable(index) { update_or_dealloc_account( &mut final_accounts_actual, - pubkey, - account_data, + *pubkey, + account_data.clone(), ); } }