diff --git a/ci/nits.sh b/ci/nits.sh index 963315cf7a2b17..0d8f335fb34420 100755 --- a/ci/nits.sh +++ b/ci/nits.sh @@ -56,9 +56,9 @@ fi # # shellcheck disable=1001 declare useGithubIssueInsteadOf=( - X\XX + #X\XX T\BD - F\IXME + #F\IXME #T\ODO # TODO: Disable TODOs once all other TODOs are purged ) diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index c139ea15130319..7bbf00624e184e 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -5,8 +5,8 @@ use { rollback_accounts::RollbackAccounts, transaction_error_metrics::TransactionErrorMetrics, transaction_processing_callback::{AccountState, TransactionProcessingCallback}, + transaction_processing_result::ProcessedTransaction, }, - itertools::Itertools, solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, solana_feature_set::{self as feature_set, FeatureSet}, solana_program_runtime::loaded_programs::{ProgramCacheEntry, ProgramCacheForTxBatch}, @@ -30,7 +30,10 @@ use { solana_svm_rent_collector::svm_rent_collector::SVMRentCollector, solana_svm_transaction::svm_message::SVMMessage, solana_system_program::{get_system_account_kind, SystemAccountKind}, - std::num::NonZeroU32, + std::{ + collections::{HashMap, HashSet}, + num::NonZeroU32, + }, }; // for the load instructions @@ -53,6 +56,7 @@ pub enum TransactionLoadResult { } #[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] pub struct CheckedTransactionDetails { pub nonce: Option, pub lamports_per_signature: u64, @@ -65,6 +69,16 @@ pub struct ValidatedTransactionDetails { pub compute_budget_limits: ComputeBudgetLimits, pub fee_details: FeeDetails, pub loaded_fee_payer_account: LoadedTransactionAccount, + pub loaded_fee_payer_rent_collected: u64, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +struct LoadedTransactionAccounts { + pub accounts: Vec, + pub program_indices: TransactionProgramIndices, + pub rent: TransactionRent, + pub rent_debits: RentDebits, + pub loaded_accounts_data_size: u32, } #[derive(PartialEq, Eq, Debug, Clone)] @@ -72,7 +86,8 @@ pub struct ValidatedTransactionDetails { pub struct LoadedTransactionAccount { pub(crate) account: AccountSharedData, pub(crate) loaded_size: usize, - pub(crate) rent_collected: u64, + pub(crate) executable_in_batch: bool, + pub(crate) valid_loader: bool, } #[derive(PartialEq, Eq, Debug, Clone)] @@ -95,6 +110,160 @@ pub struct FeesOnlyTransaction { pub fee_details: FeeDetails, } +#[derive(Debug, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] +pub(crate) struct LoadedAccountsMap(HashMap); + +impl LoadedAccountsMap { + pub fn with_capacity(capacity: usize) -> Self { + Self(HashMap::with_capacity(capacity)) + } + + pub fn insert_account( + &mut self, + pubkey: Pubkey, + account: AccountSharedData, + ) -> Option { + let loaded_account = LoadedTransactionAccount { + loaded_size: account.data().len(), + executable_in_batch: account.executable(), + valid_loader: account.executable() + && (native_loader::check_id(&pubkey) || native_loader::check_id(account.owner())), + account, + }; + + self.0.insert(pubkey, loaded_account) + } + + pub fn insert_cached_program( + &mut self, + pubkey: Pubkey, + program: &ProgramCacheEntry, + ) -> Option { + let loaded_account = LoadedTransactionAccount { + loaded_size: program.account_size, + executable_in_batch: true, + valid_loader: native_loader::check_id(&pubkey) + || native_loader::check_id(&program.account_owner()), + account: account_shared_data_from_program(program), + }; + + self.0.insert(pubkey, loaded_account) + } + + pub fn update_account(&mut self, pubkey: &Pubkey, account: &AccountSharedData) { + // accounts which go to zero lamports must be dropped + // we dont insert default so future transactions know theyre creating a new account + // also we need to insert, rather than strictly update, in case the acocunt is newly created + if account.lamports() == 0 { + self.0.remove(pubkey); + } else { + // data size may change between transactions due to realloc + // but an account newly marked executable may never become executable in batch + self.0 + .entry(*pubkey) + .and_modify(|loaded_account| { + loaded_account.loaded_size = account.data().len(); + loaded_account.account = account.clone(); + }) + .or_insert_with(|| LoadedTransactionAccount { + account: account.clone(), + loaded_size: account.data().len(), + executable_in_batch: false, + valid_loader: false, + }); + } + } + + pub fn get_account(&self, pubkey: &Pubkey) -> Option<&AccountSharedData> { + self.0 + .get(pubkey) + .map(|loaded_account| &loaded_account.account) + } + + pub fn get_loaded_account(&self, pubkey: &Pubkey) -> Option<&LoadedTransactionAccount> { + self.0.get(pubkey) + } +} + +// stripped-down version of `collect_accounts_to_store()` from account_saver.rs +pub(crate) fn collect_and_update_loaded_accounts( + loaded_accounts_map: &mut LoadedAccountsMap, + transaction: &T, + processed_transaction: &ProcessedTransaction, +) { + match processed_transaction { + ProcessedTransaction::Executed(executed_tx) => { + if executed_tx.execution_details.status.is_ok() { + update_accounts_for_successful_tx( + loaded_accounts_map, + transaction, + &executed_tx.loaded_transaction.accounts, + ); + } else { + update_accounts_for_failed_tx( + loaded_accounts_map, + transaction, + &executed_tx.loaded_transaction.rollback_accounts, + ); + } + } + ProcessedTransaction::FeesOnly(fees_only_tx) => { + update_accounts_for_failed_tx( + loaded_accounts_map, + transaction, + &fees_only_tx.rollback_accounts, + ); + } + } +} + +fn update_accounts_for_successful_tx( + loaded_accounts_map: &mut LoadedAccountsMap, + transaction: &T, + transaction_accounts: &[TransactionAccount], +) { + // TODO update account saver to be like this after i rebase on master + for (i, (address, account)) in transaction_accounts.iter().enumerate() { + if !transaction.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 transaction.is_invoked(i) && !transaction.is_instruction_account(i) { + continue; + } + + loaded_accounts_map.update_account(address, account); + } +} + +pub(crate) fn update_accounts_for_failed_tx( + loaded_accounts_map: &mut LoadedAccountsMap, + transaction: &T, + rollback_accounts: &RollbackAccounts, +) { + let fee_payer_address = transaction.fee_payer(); + match rollback_accounts { + RollbackAccounts::FeePayerOnly { fee_payer_account } => { + loaded_accounts_map.update_account(fee_payer_address, fee_payer_account); + } + RollbackAccounts::SameNonceAndFeePayer { nonce } => { + loaded_accounts_map.update_account(nonce.address(), nonce.account()); + } + RollbackAccounts::SeparateNonceAndFeePayer { + nonce, + fee_payer_account, + } => { + loaded_accounts_map.update_account(nonce.address(), nonce.account()); + loaded_accounts_map.update_account(fee_payer_address, fee_payer_account); + } + } +} + /// Collect rent from an account if rent is still enabled and regardless of /// whether rent is enabled, set the rent epoch to u64::MAX if the account is /// rent exempt. @@ -181,61 +350,153 @@ pub fn validate_fee_payer( ) } -/// Collect information about accounts used in txs transactions and -/// return vector of tuples, one for each transaction in the -/// batch. Each tuple contains struct of information about accounts as -/// its first element and an optional transaction nonce info as its -/// second element. +#[derive(Debug, Clone)] +struct AccountUsagePattern { + is_writable: bool, + is_instruction_account: bool, +} + pub(crate) fn load_accounts( callbacks: &CB, txs: &[impl SVMMessage], - validation_results: Vec, - error_metrics: &mut TransactionErrorMetrics, + check_results: &Vec, account_overrides: Option<&AccountOverrides>, - feature_set: &FeatureSet, - rent_collector: &dyn SVMRentCollector, - loaded_programs: &ProgramCacheForTxBatch, -) -> Vec { - txs.iter() - .zip(validation_results) - .map(|(transaction, validation_result)| { - load_transaction( - callbacks, - transaction, - validation_result, - error_metrics, - account_overrides, - feature_set, - rent_collector, - loaded_programs, - ) - }) - .collect() + program_cache: &ProgramCacheForTxBatch, +) -> LoadedAccountsMap { + let checked_messages = txs + .iter() + .zip(check_results) + .filter(|(_, tx_details)| tx_details.is_ok()) + .map(|(tx, _)| tx) + .collect::>(); + + let mut account_keys_to_load = HashMap::new(); + let mut all_batch_program_ids = HashSet::new(); + for message in checked_messages { + // first, get all account keys for the batch + // plus whether any transaction uses it as writable or an instruction account + for (account_index, account_key) in message.account_keys().iter().enumerate() { + let is_writable = message.is_writable(account_index); + let is_instruction_account = message.is_instruction_account(account_index); + + account_keys_to_load + .entry(account_key) + .and_modify(|usage_pattern: &mut AccountUsagePattern| { + usage_pattern.is_writable = usage_pattern.is_writable || is_writable; + usage_pattern.is_instruction_account = + usage_pattern.is_instruction_account || is_instruction_account; + }) + .or_insert_with(|| AccountUsagePattern { + is_writable, + is_instruction_account, + }); + } + + // next, get all program ids for the batch, for validation after loading + // we skip adding native loader if present because it does not need to be validated + for (program_id, _) in message.program_instructions_iter() { + if !native_loader::check_id(program_id) { + all_batch_program_ids.insert(*program_id); + } + } + } + + let mut loaded_accounts_map = LoadedAccountsMap::with_capacity(account_keys_to_load.len()); + for (account_key, usage_pattern) in account_keys_to_load { + let AccountUsagePattern { + is_writable, + is_instruction_account, + } = usage_pattern; + + if solana_sdk::sysvar::instructions::check_id(account_key) { + continue; + } else if let Some(account_override) = + account_overrides.and_then(|overrides| overrides.get(account_key)) + { + loaded_accounts_map.insert_account(*account_key, account_override.clone()); + } else if let Some(ref program) = (!is_instruction_account && !is_writable) + .then_some(()) + .and_then(|_| program_cache.find(account_key)) + .filter(|program| !program.is_tombstone()) + { + loaded_accounts_map.insert_cached_program(*account_key, program); + } else if let Some(account) = callbacks.get_account_shared_data(account_key) { + callbacks.inspect_account(account_key, AccountState::Alive(&account), is_writable); + loaded_accounts_map.insert_account(*account_key, account); + } else { + callbacks.inspect_account(account_key, AccountState::Dead, is_writable); + } + } + + // by verifying programs up front, we ensure no transaction can call a program deployed earlier in the batch + // we also preemptively disable execution of any program id not owned by a valid loader + // this way we dont need to validate this for each transaction, because a program may never be released from a loader + for program_id in all_batch_program_ids { + // if we failed to load a program, nothing needs to be done, transaction loading will fail + let Some(loaded_program) = loaded_accounts_map.get_loaded_account(&program_id) else { + continue; + }; + + // if this is a loader, nothing further needs to be done + if loaded_program.valid_loader { + continue; + } + + // NOTE pending a feature gate, we should `continue` if the program is not executable in batch + // however, for our non-executable escape hatch in transaction loading, we must get loaders for invalid programs + + // verify the program owner is a loader and add that loader to the accounts map if we dont already have it + let owner_id = loaded_program.account.owner(); + let owner_is_loader = + if let Some(loaded_owner) = loaded_accounts_map.get_loaded_account(owner_id) { + loaded_owner.valid_loader + } else if let Some(owner_account) = callbacks.get_account_shared_data(owner_id) { + if native_loader::check_id(owner_account.owner()) && owner_account.executable() { + // NOTE pending a feature gate, we will not need to hold onto extra loaders here + // because we wont count them during transaction loading against data size unless theyre used directly + loaded_accounts_map.insert_account(*owner_id, owner_account); + true + } else { + false + } + } else { + false + }; + + // by marking the program as invalid for execution, we dont need to do validation during transaction loading + if !owner_is_loader { + loaded_accounts_map + .0 + .entry(program_id) + .and_modify(|loaded_program| loaded_program.executable_in_batch = false); + } + } + + loaded_accounts_map } -fn load_transaction( - callbacks: &CB, +pub(crate) fn load_transaction( + loaded_accounts_map: &LoadedAccountsMap, message: &impl SVMMessage, validation_result: TransactionValidationResult, error_metrics: &mut TransactionErrorMetrics, - account_overrides: Option<&AccountOverrides>, feature_set: &FeatureSet, rent_collector: &dyn SVMRentCollector, - loaded_programs: &ProgramCacheForTxBatch, + program_cache: &ProgramCacheForTxBatch, ) -> TransactionLoadResult { match validation_result { Err(e) => TransactionLoadResult::NotLoaded(e), Ok(tx_details) => { let load_result = load_transaction_accounts( - callbacks, + loaded_accounts_map, message, tx_details.loaded_fee_payer_account, + tx_details.loaded_fee_payer_rent_collected, &tx_details.compute_budget_limits, error_metrics, - account_overrides, feature_set, rent_collector, - loaded_programs, + program_cache, ); match load_result { @@ -259,141 +520,161 @@ fn load_transaction( } } -#[derive(PartialEq, Eq, Debug, Clone)] -struct LoadedTransactionAccounts { - pub accounts: Vec, - pub program_indices: TransactionProgramIndices, - pub rent: TransactionRent, - pub rent_debits: RentDebits, - pub loaded_accounts_data_size: u32, -} - -fn load_transaction_accounts( - callbacks: &CB, +fn load_transaction_accounts( + loaded_accounts_map: &LoadedAccountsMap, message: &impl SVMMessage, loaded_fee_payer_account: LoadedTransactionAccount, + loaded_fee_payer_rent_collected: u64, compute_budget_limits: &ComputeBudgetLimits, error_metrics: &mut TransactionErrorMetrics, - account_overrides: Option<&AccountOverrides>, feature_set: &FeatureSet, rent_collector: &dyn SVMRentCollector, - loaded_programs: &ProgramCacheForTxBatch, + program_cache: &ProgramCacheForTxBatch, ) -> Result { - let mut tx_rent: TransactionRent = 0; let account_keys = message.account_keys(); + let mut required_programs = HashMap::new(); + let mut accumulated_loaders = HashSet::new(); + let mut accounts = Vec::with_capacity(account_keys.len()); - let mut accounts_found = Vec::with_capacity(account_keys.len()); + let mut tx_rent: TransactionRent = 0; let mut rent_debits = RentDebits::default(); let mut accumulated_accounts_data_size: u32 = 0; - let instruction_accounts = message - .instructions_iter() - .flat_map(|instruction| instruction.accounts) - .unique() - .collect::>(); - - let mut collect_loaded_account = |key, (loaded_account, found)| -> Result<()> { - let LoadedTransactionAccount { - account, - loaded_size, - rent_collected, - } = loaded_account; - - accumulate_and_check_loaded_account_data_size( - &mut accumulated_accounts_data_size, - loaded_size, - compute_budget_limits.loaded_accounts_bytes, - error_metrics, - )?; - - tx_rent += rent_collected; - rent_debits.insert(key, rent_collected, account.lamports()); - - accounts.push((*key, account)); - accounts_found.push(found); - Ok(()) - }; - - // Since the fee payer is always the first account, collect it first. Note - // that account overrides are already applied during fee payer validation so - // it's fine to use the fee payer directly here rather than checking account - // overrides again. - collect_loaded_account(message.fee_payer(), (loaded_fee_payer_account, true))?; - - // Attempt to load and collect remaining non-fee payer accounts - for (account_index, account_key) in account_keys.iter().enumerate().skip(1) { - let (loaded_account, account_found) = load_transaction_account( - callbacks, - message, - account_key, - account_index, - &instruction_accounts[..], - account_overrides, - feature_set, - rent_collector, - loaded_programs, - )?; - collect_loaded_account(account_key, (loaded_account, account_found))?; - } - - let builtins_start_index = accounts.len(); let program_indices = message .instructions_iter() .map(|instruction| { let mut account_indices = Vec::with_capacity(2); let program_index = instruction.program_id_index as usize; - // This command may never return error, because the transaction is sanitized - let (program_id, program_account) = accounts - .get(program_index) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if native_loader::check_id(program_id) { - return Ok(account_indices); - } - let account_found = accounts_found.get(program_index).unwrap_or(&true); - if !account_found { + // This command may never return error, because the transaction is sanitized + let Some(program_id) = account_keys.get(program_index) else { error_metrics.account_not_found += 1; return Err(TransactionError::ProgramAccountNotFound); - } + }; - if !program_account.executable() { - error_metrics.invalid_program_for_execution += 1; - return Err(TransactionError::InvalidProgramForExecution); - } - account_indices.insert(0, program_index as IndexOfAccount); - let owner_id = program_account.owner(); - if native_loader::check_id(owner_id) { - return Ok(account_indices); - } - if !accounts - .get(builtins_start_index..) - .ok_or(TransactionError::ProgramAccountNotFound)? - .iter() - .any(|(key, _)| key == owner_id) - { - if let Some(owner_account) = callbacks.get_account_shared_data(owner_id) { - if !native_loader::check_id(owner_account.owner()) - || !owner_account.executable() - { + // we do not need to validate NativeLoader, and its index is never retained + // otherwise we hold the program id to validate it is executable and owned by a valid loader + // we also retain the index, because we may need to override executable_in_batch if the program was cached + // when this is patched, we do not need to distinguish cached from loaded programs + if !native_loader::check_id(program_id) { + required_programs.insert(program_id, program_index); + account_indices.insert(0, program_index as IndexOfAccount); + + // NOTE to preserve existing behavior, we must count loader size, except NativeLoader, once per transaction + // this is in addition to counting it if used as an instruction account, pending removal by feature gate + // we re-validate here the program is owned by a loader due to interactions with the program cache + // which may cause us to execute programs that are not executable_in_batch + // when this is patched, we can fully rely on loader validation in `load_accounts()` + let Some(loaded_program) = loaded_accounts_map.get_loaded_account(program_id) + else { + error_metrics.account_not_found += 1; + return Err(TransactionError::ProgramAccountNotFound); + }; + + let owner_id = loaded_program.account.owner(); + if !native_loader::check_id(owner_id) && !accumulated_loaders.contains(owner_id) { + let Some(loaded_owner) = loaded_accounts_map.get_loaded_account(owner_id) + else { + error_metrics.invalid_program_for_execution += 1; + return Err(TransactionError::InvalidProgramForExecution); + }; + + if !loaded_owner.valid_loader { error_metrics.invalid_program_for_execution += 1; return Err(TransactionError::InvalidProgramForExecution); } + accumulate_and_check_loaded_account_data_size( &mut accumulated_accounts_data_size, - owner_account.data().len(), + loaded_owner.loaded_size, compute_budget_limits.loaded_accounts_bytes, error_metrics, )?; - accounts.push((*owner_id, owner_account)); - } else { - error_metrics.account_not_found += 1; - return Err(TransactionError::ProgramAccountNotFound); + + accumulated_loaders.insert(owner_id); } } + Ok(account_indices) }) .collect::>>>()?; + let mut collect_loaded_account = + |key, (loaded_account, found, rent_collected): (_, bool, u64)| -> Result<()> { + let LoadedTransactionAccount { + mut account, + mut loaded_size, + executable_in_batch, + valid_loader: _, + } = loaded_account; + + if let Some(program_index) = required_programs.get(key) { + // if this account is a program, we confirm it was found and is executable + if !found { + error_metrics.account_not_found += 1; + return Err(TransactionError::ProgramAccountNotFound); + } + + if !executable_in_batch { + // NOTE in the old loader, executable was not checked for cached, read-only, non-instruction programs + // we preserve this behavior here and must *substitute* the cached program + // pending a feature gate to remove this behavior + // we need to check cache here, even if we allow non-executable programs in `load_accounts()` + // because cache usage there depends on *batch-wide* account usage pattern + // when we fix the executable check, transaction loading no longer needs the cache + let is_writable = message.is_writable(*program_index); + let is_instruction_account = message.is_instruction_account(*program_index); + + if let Some(ref program) = (!is_instruction_account && !is_writable) + .then_some(()) + .and_then(|_| program_cache.find(key)) + { + account = account_shared_data_from_program(program); + loaded_size = program.account_size; + } else { + error_metrics.invalid_program_for_execution += 1; + return Err(TransactionError::InvalidProgramForExecution); + } + } + } + + accumulate_and_check_loaded_account_data_size( + &mut accumulated_accounts_data_size, + loaded_size, + compute_budget_limits.loaded_accounts_bytes, + error_metrics, + )?; + + tx_rent += rent_collected; + rent_debits.insert(key, rent_collected, account.lamports()); + + accounts.push((*key, account)); + Ok(()) + }; + + // Since the fee payer is always the first account, collect it first + collect_loaded_account( + message.fee_payer(), + ( + loaded_fee_payer_account, + true, + loaded_fee_payer_rent_collected, + ), + )?; + + // Attempt to load and collect remaining non-fee payer accounts + for (account_index, account_key) in account_keys.iter().enumerate().skip(1) { + let (loaded_account, account_found, rent_collected) = load_transaction_account( + loaded_accounts_map, + message, + account_key, + account_index, + feature_set, + rent_collector, + )?; + collect_loaded_account(account_key, (loaded_account, account_found, rent_collected))?; + } + Ok(LoadedTransactionAccounts { accounts, program_indices, @@ -403,22 +684,16 @@ fn load_transaction_accounts( }) } -fn load_transaction_account( - callbacks: &CB, +fn load_transaction_account( + loaded_accounts_map: &LoadedAccountsMap, message: &impl SVMMessage, account_key: &Pubkey, account_index: usize, - instruction_accounts: &[&u8], - account_overrides: Option<&AccountOverrides>, feature_set: &FeatureSet, rent_collector: &dyn SVMRentCollector, - loaded_programs: &ProgramCacheForTxBatch, -) -> Result<(LoadedTransactionAccount, bool)> { +) -> Result<(LoadedTransactionAccount, bool, u64)> { let mut account_found = true; - let mut was_inspected = false; - let is_instruction_account = u8::try_from(account_index) - .map(|i| instruction_accounts.contains(&&i)) - .unwrap_or(false); + let mut rent_collected = 0; let is_writable = message.is_writable(account_index); let loaded_account = if solana_sdk::sysvar::instructions::check_id(account_key) { // Since the instructions sysvar is constructed by the SVM and modified @@ -426,61 +701,27 @@ fn load_transaction_account( LoadedTransactionAccount { loaded_size: 0, account: construct_instructions_account(message), - rent_collected: 0, - } - } else if let Some(account_override) = - account_overrides.and_then(|overrides| overrides.get(account_key)) - { - LoadedTransactionAccount { - loaded_size: account_override.data().len(), - account: account_override.clone(), - rent_collected: 0, - } - } else if let Some(program) = (!is_instruction_account && !is_writable) - .then_some(()) - .and_then(|_| loaded_programs.find(account_key)) - { - callbacks - .get_account_shared_data(account_key) - .ok_or(TransactionError::AccountNotFound)?; - // Optimization to skip loading of accounts which are only used as - // programs in top-level instructions and not passed as instruction accounts. - LoadedTransactionAccount { - loaded_size: program.account_size, - account: account_shared_data_from_program(&program), - rent_collected: 0, + executable_in_batch: false, + valid_loader: false, } } else { - callbacks - .get_account_shared_data(account_key) - .map(|mut account| { - let rent_collected = if is_writable { - // Inspect the account prior to collecting rent, since - // rent collection can modify the account. - debug_assert!(!was_inspected); - callbacks.inspect_account( - account_key, - AccountState::Alive(&account), - is_writable, - ); - was_inspected = true; - + loaded_accounts_map + .get_loaded_account(account_key) + .cloned() + .map(|mut loaded_account| { + rent_collected = if is_writable { collect_rent_from_account( feature_set, rent_collector, account_key, - &mut account, + &mut loaded_account.account, ) .rent_amount } else { 0 }; - LoadedTransactionAccount { - loaded_size: account.data().len(), - account, - rent_collected, - } + loaded_account }) .unwrap_or_else(|| { account_found = false; @@ -492,21 +733,13 @@ fn load_transaction_account( LoadedTransactionAccount { loaded_size: default_account.data().len(), account: default_account, - rent_collected: 0, + executable_in_batch: false, + valid_loader: false, } }) }; - if !was_inspected { - let account_state = if account_found { - AccountState::Alive(&loaded_account.account) - } else { - AccountState::Dead - }; - callbacks.inspect_account(account_key, account_state, is_writable); - } - - Ok((loaded_account, account_found)) + Ok((loaded_account, account_found, rent_collected)) } fn account_shared_data_from_program(loaded_program: &ProgramCacheEntry) -> AccountSharedData { @@ -587,16 +820,16 @@ mod tests { solana_program_runtime::loaded_programs::{ProgramCacheEntry, ProgramCacheForTxBatch}, solana_sdk::{ account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, - bpf_loader_upgradeable, + bpf_loader, bpf_loader_upgradeable, epoch_schedule::EpochSchedule, hash::Hash, - instruction::CompiledInstruction, + instruction::{AccountMeta, CompiledInstruction, Instruction}, message::{ v0::{LoadedAddresses, LoadedMessage}, LegacyMessage, Message, MessageHeader, SanitizedMessage, }, native_loader, - native_token::sol_to_lamports, + native_token::{sol_to_lamports, LAMPORTS_PER_SOL}, nonce, pubkey::Pubkey, rent::Rent, @@ -652,7 +885,7 @@ mod tests { rent_collector: &RentCollector, error_metrics: &mut TransactionErrorMetrics, feature_set: &mut FeatureSet, - ) -> Vec { + ) -> TransactionLoadResult { feature_set.deactivate(&feature_set::disable_rent_fees_collection::id()); let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); let fee_payer_account = accounts[0].1.clone(); @@ -664,18 +897,24 @@ mod tests { accounts_map, ..Default::default() }; - load_accounts( + let loaded_accounts_map = load_accounts( &callbacks, - &[sanitized_tx], - vec![Ok(ValidatedTransactionDetails { + &[sanitized_tx.clone()], + &vec![Ok(CheckedTransactionDetails::default())], + None, + &ProgramCacheForTxBatch::default(), + ); + load_transaction( + &loaded_accounts_map, + &sanitized_tx, + Ok(ValidatedTransactionDetails { loaded_fee_payer_account: LoadedTransactionAccount { account: fee_payer_account, ..LoadedTransactionAccount::default() }, ..ValidatedTransactionDetails::default() - })], + }), error_metrics, - None, feature_set, rent_collector, &ProgramCacheForTxBatch::default(), @@ -703,7 +942,7 @@ mod tests { tx: Transaction, accounts: &[TransactionAccount], error_metrics: &mut TransactionErrorMetrics, - ) -> Vec { + ) -> TransactionLoadResult { load_accounts_with_features_and_rent( tx, accounts, @@ -718,7 +957,7 @@ mod tests { accounts: &[TransactionAccount], error_metrics: &mut TransactionErrorMetrics, exclude_features: Option<&[Pubkey]>, - ) -> Vec { + ) -> TransactionLoadResult { load_accounts_with_features_and_rent( tx, accounts, @@ -752,12 +991,11 @@ mod tests { instructions, ); - let load_results = load_accounts_aux_test(tx, &accounts, &mut error_metrics); + let load_result = load_accounts_aux_test(tx, &accounts, &mut error_metrics); assert_eq!(error_metrics.account_not_found, 1); - assert_eq!(load_results.len(), 1); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::FeesOnly(FeesOnlyTransaction { load_error: TransactionError::ProgramAccountNotFound, .. @@ -795,8 +1033,7 @@ mod tests { load_accounts_with_excluded_features(tx, &accounts, &mut error_metrics, None); assert_eq!(error_metrics.account_not_found, 0); - assert_eq!(loaded_accounts.len(), 1); - match &loaded_accounts[0] { + match &loaded_accounts { TransactionLoadResult::Loaded(loaded_transaction) => { assert_eq!(loaded_transaction.accounts.len(), 3); assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); @@ -834,14 +1071,13 @@ mod tests { instructions, ); - let load_results = load_accounts_aux_test(tx, &accounts, &mut error_metrics); + let load_result = load_accounts_aux_test(tx, &accounts, &mut error_metrics); - assert_eq!(error_metrics.account_not_found, 1); - assert_eq!(load_results.len(), 1); + assert_eq!(error_metrics.invalid_program_for_execution, 1); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::ProgramAccountNotFound, + load_error: TransactionError::InvalidProgramForExecution, .. }), )); @@ -871,12 +1107,11 @@ mod tests { instructions, ); - let load_results = load_accounts_aux_test(tx, &accounts, &mut error_metrics); + let load_result = load_accounts_aux_test(tx, &accounts, &mut error_metrics); assert_eq!(error_metrics.invalid_program_for_execution, 1); - assert_eq!(load_results.len(), 1); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::FeesOnly(FeesOnlyTransaction { load_error: TransactionError::InvalidProgramForExecution, .. @@ -926,10 +1161,9 @@ mod tests { load_accounts_with_excluded_features(tx, &accounts, &mut error_metrics, None); assert_eq!(error_metrics.account_not_found, 0); - assert_eq!(loaded_accounts.len(), 1); - match &loaded_accounts[0] { + match &loaded_accounts { TransactionLoadResult::Loaded(loaded_transaction) => { - assert_eq!(loaded_transaction.accounts.len(), 4); + assert_eq!(loaded_transaction.accounts.len(), 3); assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); assert_eq!(loaded_transaction.program_indices.len(), 2); assert_eq!(loaded_transaction.program_indices[0], &[1]); @@ -944,7 +1178,7 @@ mod tests { accounts: &[TransactionAccount], tx: Transaction, account_overrides: Option<&AccountOverrides>, - ) -> Vec { + ) -> TransactionLoadResult { let tx = SanitizedTransaction::from_transaction_for_tests(tx); let mut error_metrics = TransactionErrorMetrics::default(); @@ -956,12 +1190,18 @@ mod tests { accounts_map, ..Default::default() }; - load_accounts( + let loaded_accounts_map = load_accounts( &callbacks, - &[tx], - vec![Ok(ValidatedTransactionDetails::default())], - &mut error_metrics, + &[tx.clone()], + &vec![Ok(CheckedTransactionDetails::default())], account_overrides, + &ProgramCacheForTxBatch::default(), + ); + load_transaction( + &loaded_accounts_map, + &tx, + Ok(ValidatedTransactionDetails::default()), + &mut error_metrics, &FeatureSet::all_enabled(), &RentCollector::default(), &ProgramCacheForTxBatch::default(), @@ -982,10 +1222,9 @@ mod tests { instructions, ); - let load_results = load_accounts_no_store(&[], tx, None); - assert_eq!(load_results.len(), 1); + let load_result = load_accounts_no_store(&[], tx, None); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::FeesOnly(FeesOnlyTransaction { load_error: TransactionError::ProgramAccountNotFound, .. @@ -1015,8 +1254,7 @@ mod tests { let loaded_accounts = load_accounts_no_store(&[(keypair.pubkey(), account)], tx, Some(&account_overrides)); - assert_eq!(loaded_accounts.len(), 1); - match &loaded_accounts[0] { + match &loaded_accounts { TransactionLoadResult::Loaded(loaded_transaction) => { assert_eq!(loaded_transaction.accounts[0].0, keypair.pubkey()); assert_eq!(loaded_transaction.accounts[1].0, slot_history_id); @@ -1228,18 +1466,15 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let fee_payer_balance = 200; let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(fee_payer_balance); - mock_bank - .accounts_map - .insert(fee_payer_address, fee_payer_account.clone()); + loaded_accounts_map.insert_account(fee_payer_address, fee_payer_account.clone()); let fee_payer_rent_debit = 42; let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1247,19 +1482,19 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount { loaded_size: fee_payer_account.data().len(), account: fee_payer_account.clone(), - rent_collected: fee_payer_rent_debit, + ..LoadedTransactionAccount::default() }, + fee_payer_rent_debit, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); let expected_rent_debits = { @@ -1294,18 +1529,13 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); - mock_bank - .accounts_map - .insert(native_loader::id(), AccountSharedData::default()); + let mut loaded_accounts_map = LoadedAccountsMap::default(); + loaded_accounts_map.insert_account(native_loader::id(), AccountSharedData::default()); let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(200); - mock_bank - .accounts_map - .insert(key1.pubkey(), fee_payer_account.clone()); + loaded_accounts_map.insert_account(key1.pubkey(), fee_payer_account.clone()); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1313,18 +1543,18 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount { account: fee_payer_account.clone(), ..LoadedTransactionAccount::default() }, + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); assert_eq!( @@ -1334,7 +1564,10 @@ mod tests { (key1.pubkey(), fee_payer_account), ( native_loader::id(), - mock_bank.accounts_map[&native_loader::id()].clone() + loaded_accounts_map + .get_account(&native_loader::id()) + .unwrap() + .clone() ) ], program_indices: vec![vec![]], @@ -1362,12 +1595,16 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut accounts_map = HashMap::new(); let mut account_data = AccountSharedData::default(); account_data.set_lamports(200); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + accounts_map.insert(key1.pubkey(), account_data); + + let callbacks = TestCallbacks { + accounts_map, + ..Default::default() + }; - let mut error_metrics = TransactionErrorMetrics::default(); let mut loaded_programs = ProgramCacheForTxBatch::default(); loaded_programs.replenish(key2.pubkey(), Arc::new(ProgramCacheEntry::default())); @@ -1376,19 +1613,163 @@ mod tests { vec![Signature::new_unique()], false, ); + + let loaded_accounts_map = load_accounts( + &callbacks, + &[sanitized_transaction.clone()], + &vec![Ok(CheckedTransactionDetails::default())], + None, + &loaded_programs, + ); + + let mut error_metrics = TransactionErrorMetrics::default(); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount::default(), + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), &loaded_programs, ); - assert_eq!(result.err(), Some(TransactionError::AccountNotFound)); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_program_account_executable_bypass() { + let mut loaded_accounts_map = LoadedAccountsMap::default(); + let account_keypair = Keypair::new(); + let program_keypair = Keypair::new(); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + loaded_accounts_map.insert_account(account_keypair.pubkey(), account_data.clone()); + + let mut program_data = AccountSharedData::default(); + program_data.set_lamports(200); + program_data.set_owner(bpf_loader::id()); + loaded_accounts_map.insert_account(program_keypair.pubkey(), program_data); + + let mut loader_data = AccountSharedData::default(); + loader_data.set_lamports(200); + loader_data.set_executable(true); + loader_data.set_owner(native_loader::id()); + loaded_accounts_map.insert_account(bpf_loader::id(), loader_data.clone()); + loaded_accounts_map.insert_account(native_loader::id(), loader_data); + + let mut error_metrics = TransactionErrorMetrics::default(); + let mut loaded_programs = ProgramCacheForTxBatch::default(); + + let transaction = + SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_keypair.pubkey(), + &[], + vec![], + )], + Some(&account_keypair.pubkey()), + &[&account_keypair], + Hash::default(), + )); + + let result = load_transaction_accounts( + &loaded_accounts_map, + transaction.message(), + LoadedTransactionAccount { + account: account_data.clone(), + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &loaded_programs, + ); + + // without cache, program is invalid + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + + loaded_programs.replenish( + program_keypair.pubkey(), + Arc::new(ProgramCacheEntry::default()), + ); + + let mut cached_program = AccountSharedData::default(); + cached_program.set_owner(native_loader::id()); + cached_program.set_executable(true); + + let result = load_transaction_accounts( + &loaded_accounts_map, + transaction.message(), + LoadedTransactionAccount { + account: account_data.clone(), + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &loaded_programs, + ); + + // with cache, executable flag is bypassed + assert_eq!( + result.unwrap(), + LoadedTransactionAccounts { + accounts: vec![ + (account_keypair.pubkey(), account_data.clone()), + (program_keypair.pubkey(), cached_program.clone()), + ], + program_indices: vec![vec![1]], + rent: 0, + rent_debits: RentDebits::default(), + loaded_accounts_data_size: 0, + } + ); + + for account_meta in [AccountMeta::new, AccountMeta::new_readonly] { + let transaction = SanitizedTransaction::from_transaction_for_tests( + Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_keypair.pubkey(), + &[], + vec![account_meta(program_keypair.pubkey(), false)], + )], + Some(&account_keypair.pubkey()), + &[&account_keypair], + Hash::default(), + ), + ); + + let result = load_transaction_accounts( + &loaded_accounts_map, + transaction.message(), + LoadedTransactionAccount { + account: account_data.clone(), + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &loaded_programs, + ); + + // including program as instruction account bypasses executable bypass + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } } #[test] @@ -1408,13 +1789,12 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_lamports(200); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1422,15 +1802,15 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount::default(), + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); @@ -1453,13 +1833,12 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_lamports(200); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1467,15 +1846,15 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount::default(), + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); assert_eq!( @@ -1501,19 +1880,16 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_owner(native_loader::id()); account_data.set_executable(true); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(200); - mock_bank - .accounts_map - .insert(key2.pubkey(), fee_payer_account.clone()); + loaded_accounts_map.insert_account(key2.pubkey(), fee_payer_account.clone()); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1521,18 +1897,18 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount { account: fee_payer_account.clone(), ..LoadedTransactionAccount::default() }, + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); assert_eq!( @@ -1542,7 +1918,10 @@ mod tests { (key2.pubkey(), fee_payer_account), ( key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].clone() + loaded_accounts_map + .get_account(&key1.pubkey()) + .unwrap() + .clone() ), ], program_indices: vec![vec![1]], @@ -1570,16 +1949,15 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_executable(true); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut account_data = AccountSharedData::default(); account_data.set_lamports(200); - mock_bank.accounts_map.insert(key2.pubkey(), account_data); + loaded_accounts_map.insert_account(key2.pubkey(), account_data); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1587,18 +1965,21 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount::default(), + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); } #[test] @@ -1619,21 +2000,18 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_executable(true); account_data.set_owner(key3.pubkey()); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut account_data = AccountSharedData::default(); account_data.set_lamports(200); - mock_bank.accounts_map.insert(key2.pubkey(), account_data); + loaded_accounts_map.insert_account(key2.pubkey(), account_data); - mock_bank - .accounts_map - .insert(key3.pubkey(), AccountSharedData::default()); + loaded_accounts_map.insert_account(key3.pubkey(), AccountSharedData::default()); let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1641,15 +2019,15 @@ mod tests { false, ); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount::default(), + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), ); assert_eq!( @@ -1676,44 +2054,61 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_executable(true); account_data.set_owner(key3.pubkey()); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(200); - mock_bank - .accounts_map - .insert(key2.pubkey(), fee_payer_account.clone()); - - let mut account_data = AccountSharedData::default(); - account_data.set_executable(true); - account_data.set_owner(native_loader::id()); - mock_bank.accounts_map.insert(key3.pubkey(), account_data); - - let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); + loaded_accounts_map.insert_account(key2.pubkey(), fee_payer_account.clone()); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, vec![Signature::new_unique()], false, ); + + // program fails to load without a valid loader + let mut error_metrics = TransactionErrorMetrics::default(); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount { account: fee_payer_account.clone(), ..LoadedTransactionAccount::default() }, + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), + ); + + assert_eq!(error_metrics.invalid_program_for_execution, 1); + assert!(result.is_err()); + + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + loaded_accounts_map.insert_account(key3.pubkey(), account_data); + + let mut error_metrics = TransactionErrorMetrics::default(); + let result = load_transaction_accounts( + &loaded_accounts_map, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &ProgramCacheForTxBatch::default(), ); assert_eq!( @@ -1723,11 +2118,10 @@ mod tests { (key2.pubkey(), fee_payer_account), ( key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].clone() - ), - ( - key3.pubkey(), - mock_bank.accounts_map[&key3.pubkey()].clone() + loaded_accounts_map + .get_account(&key1.pubkey()) + .unwrap() + .clone() ), ], program_indices: vec![vec![1]], @@ -1764,44 +2158,61 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_executable(true); account_data.set_owner(key3.pubkey()); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(200); - mock_bank - .accounts_map - .insert(key2.pubkey(), fee_payer_account.clone()); - - let mut account_data = AccountSharedData::default(); - account_data.set_executable(true); - account_data.set_owner(native_loader::id()); - mock_bank.accounts_map.insert(key3.pubkey(), account_data); - - let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); + loaded_accounts_map.insert_account(key2.pubkey(), fee_payer_account.clone()); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, vec![Signature::new_unique()], false, ); + + // program fails to load without a valid loader + let mut error_metrics = TransactionErrorMetrics::default(); let result = load_transaction_accounts( - &mock_bank, + &loaded_accounts_map, sanitized_transaction.message(), LoadedTransactionAccount { account: fee_payer_account.clone(), ..LoadedTransactionAccount::default() }, + 0, &ComputeBudgetLimits::default(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), + ); + + assert_eq!(error_metrics.invalid_program_for_execution, 1); + assert!(result.is_err()); + + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + loaded_accounts_map.insert_account(key3.pubkey(), account_data); + + let mut error_metrics = TransactionErrorMetrics::default(); + let result = load_transaction_accounts( + &loaded_accounts_map, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &ProgramCacheForTxBatch::default(), ); let mut account_data = AccountSharedData::default(); @@ -1813,13 +2224,12 @@ mod tests { (key2.pubkey(), fee_payer_account), ( key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].clone() + loaded_accounts_map + .get_account(&key1.pubkey()) + .unwrap() + .clone() ), (key4.pubkey(), account_data), - ( - key3.pubkey(), - mock_bank.accounts_map[&key3.pubkey()].clone() - ), ], program_indices: vec![vec![1], vec![1]], rent: 0, @@ -1832,22 +2242,20 @@ mod tests { #[test] fn test_rent_state_list_len() { let mint_keypair = Keypair::new(); - let mut bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let recipient = Pubkey::new_unique(); let last_block_hash = Hash::new_unique(); let mut system_data = AccountSharedData::default(); system_data.set_executable(true); system_data.set_owner(native_loader::id()); - bank.accounts_map - .insert(Pubkey::new_from_array([0u8; 32]), system_data); + loaded_accounts_map.insert_account(Pubkey::new_from_array([0u8; 32]), system_data); let mut mint_data = AccountSharedData::default(); mint_data.set_lamports(2); - bank.accounts_map.insert(mint_keypair.pubkey(), mint_data); + loaded_accounts_map.insert_account(mint_keypair.pubkey(), mint_data); - bank.accounts_map - .insert(recipient, AccountSharedData::default()); + loaded_accounts_map.insert_account(recipient, AccountSharedData::default()); let tx = system_transaction::transfer( &mint_keypair, @@ -1858,18 +2266,17 @@ mod tests { let num_accounts = tx.message().account_keys.len(); let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); let mut error_metrics = TransactionErrorMetrics::default(); - let mut load_results = load_accounts( - &bank, - &[sanitized_tx.clone()], - vec![Ok(ValidatedTransactionDetails::default())], + let load_result = load_transaction( + &loaded_accounts_map, + &sanitized_tx, + Ok(ValidatedTransactionDetails::default()), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), &ProgramCacheForTxBatch::default(), ); - let TransactionLoadResult::Loaded(loaded_transaction) = load_results.swap_remove(0) else { + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { panic!("transaction loading failed"); }; @@ -1921,25 +2328,15 @@ mod tests { }; let sanitized_message = new_unchecked_sanitized_message(message); - let mut mock_bank = TestCallbacks::default(); + let mut loaded_accounts_map = LoadedAccountsMap::default(); let mut account_data = AccountSharedData::default(); account_data.set_executable(true); account_data.set_owner(key3.pubkey()); - mock_bank.accounts_map.insert(key1.pubkey(), account_data); + loaded_accounts_map.insert_account(key1.pubkey(), account_data); let mut fee_payer_account = AccountSharedData::default(); fee_payer_account.set_lamports(200); - mock_bank - .accounts_map - .insert(key2.pubkey(), fee_payer_account.clone()); - - let mut account_data = AccountSharedData::default(); - account_data.set_executable(true); - account_data.set_owner(native_loader::id()); - mock_bank.accounts_map.insert(key3.pubkey(), account_data); - - let mut error_metrics = TransactionErrorMetrics::default(); - let loaded_programs = ProgramCacheForTxBatch::default(); + loaded_accounts_map.insert_account(key2.pubkey(), fee_payer_account.clone()); let sanitized_transaction = SanitizedTransaction::new_for_tests( sanitized_message, @@ -1954,22 +2351,48 @@ mod tests { ..ValidatedTransactionDetails::default() }); - let mut load_results = load_accounts( - &mock_bank, - &[sanitized_transaction], - vec![validation_result], + // program fails to load without a valid loader + let mut error_metrics = TransactionErrorMetrics::default(); + let load_result = load_transaction( + &loaded_accounts_map, + &sanitized_transaction, + validation_result.clone(), &mut error_metrics, - None, &FeatureSet::default(), &RentCollector::default(), - &loaded_programs, + &ProgramCacheForTxBatch::default(), + ); + + assert_eq!(error_metrics.invalid_program_for_execution, 1); + match load_result { + TransactionLoadResult::FeesOnly(_) => {} + TransactionLoadResult::Loaded(_) => { + panic!("transaction loading succeeded unexpectedly") + } + TransactionLoadResult::NotLoaded(_) => panic!("transaction loading failed wrongly"), + }; + + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + loaded_accounts_map.insert_account(key3.pubkey(), account_data); + + let mut error_metrics = TransactionErrorMetrics::default(); + let load_result = load_transaction( + &loaded_accounts_map, + &sanitized_transaction, + validation_result, + &mut error_metrics, + &FeatureSet::default(), + &RentCollector::default(), + &ProgramCacheForTxBatch::default(), ); let mut account_data = AccountSharedData::default(); account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - assert_eq!(load_results.len(), 1); - let TransactionLoadResult::Loaded(loaded_transaction) = load_results.swap_remove(0) else { + assert_eq!(error_metrics, TransactionErrorMetrics::default()); + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { panic!("transaction loading failed"); }; assert_eq!( @@ -1978,17 +2401,19 @@ mod tests { accounts: vec![ ( key2.pubkey(), - mock_bank.accounts_map[&key2.pubkey()].clone() + loaded_accounts_map + .get_account(&key2.pubkey()) + .unwrap() + .clone() ), ( key1.pubkey(), - mock_bank.accounts_map[&key1.pubkey()].clone() + loaded_accounts_map + .get_account(&key1.pubkey()) + .unwrap() + .clone() ), (key4.pubkey(), account_data), - ( - key3.pubkey(), - mock_bank.accounts_map[&key3.pubkey()].clone() - ), ], program_indices: vec![vec![1], vec![1]], fee_details: FeeDetails::default(), @@ -2003,7 +2428,7 @@ mod tests { #[test] fn test_load_accounts_error() { - let mock_bank = TestCallbacks::default(); + let loaded_accounts_map = LoadedAccountsMap::default(); let feature_set = FeatureSet::default(); let rent_collector = RentCollector::default(); @@ -2026,40 +2451,38 @@ mod tests { ); let validation_result = Ok(ValidatedTransactionDetails::default()); - let load_results = load_accounts( - &mock_bank, - &[sanitized_transaction.clone()], - vec![validation_result.clone()], + let load_result = load_transaction( + &loaded_accounts_map, + &sanitized_transaction, + validation_result, &mut TransactionErrorMetrics::default(), - None, &feature_set, &rent_collector, &ProgramCacheForTxBatch::default(), ); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::FeesOnly(FeesOnlyTransaction { - load_error: TransactionError::InvalidProgramForExecution, + load_error: TransactionError::ProgramAccountNotFound, .. }), )); let validation_result = Err(TransactionError::InvalidWritableAccount); - let load_results = load_accounts( - &mock_bank, - &[sanitized_transaction.clone()], - vec![validation_result], + let load_result = load_transaction( + &loaded_accounts_map, + &sanitized_transaction, + validation_result, &mut TransactionErrorMetrics::default(), - None, &feature_set, &rent_collector, &ProgramCacheForTxBatch::default(), ); assert!(matches!( - load_results[0], + load_result, TransactionLoadResult::NotLoaded(TransactionError::InvalidWritableAccount), )); } @@ -2190,21 +2613,11 @@ mod tests { vec![Signature::new_unique()], false, ); - let validation_result = Ok(ValidatedTransactionDetails { - loaded_fee_payer_account: LoadedTransactionAccount { - account: account0.clone(), - ..LoadedTransactionAccount::default() - }, - ..ValidatedTransactionDetails::default() - }); let _load_results = load_accounts( &mock_bank, &[sanitized_transaction], - vec![validation_result], - &mut TransactionErrorMetrics::default(), + &vec![Ok(CheckedTransactionDetails::default())], None, - &FeatureSet::default(), - &RentCollector::default(), &ProgramCacheForTxBatch::default(), ); @@ -2218,7 +2631,7 @@ mod tests { actual_inspected_accounts.sort_unstable_by(|a, b| a.0.cmp(&b.0)); let mut expected_inspected_accounts = vec![ - // *not* key0, since it is loaded during fee payer validation + (address0, vec![(Some(account0), true)]), (address1, vec![(Some(account1), true)]), (address2, vec![(None, true)]), (address3, vec![(Some(account3), false)]), @@ -2227,4 +2640,234 @@ mod tests { assert_eq!(actual_inspected_accounts, expected_inspected_accounts,); } + + #[test] + fn test_load_transaction_accounts_data_sizes() { + let mut loaded_accounts_map = LoadedAccountsMap::default(); + + let native_loader_size = 0x1; + let native_loader = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; native_loader_size as usize], + native_loader::id(), + true, + u64::MAX, + ); + loaded_accounts_map.insert_account(native_loader::id(), native_loader); + + let bpf_loader_size = 0x1 << 1; + let bpf_loader = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; bpf_loader_size as usize], + native_loader::id(), + true, + u64::MAX, + ); + loaded_accounts_map.insert_account(bpf_loader::id(), bpf_loader); + + let upgradeable_loader_size = 0x1 << 2; + let upgradeable_loader = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; upgradeable_loader_size as usize], + native_loader::id(), + true, + u64::MAX, + ); + loaded_accounts_map.insert_account(bpf_loader_upgradeable::id(), upgradeable_loader); + + let program1_keypair = Keypair::new(); + let program1 = program1_keypair.pubkey(); + let program1_size = 0x1 << 3; + let program1_account = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; program1_size as usize], + bpf_loader::id(), + true, + u64::MAX, + ); + loaded_accounts_map.insert_account(program1, program1_account); + + let program2_keypair = Keypair::new(); + let program2 = program2_keypair.pubkey(); + let program2_size = 0x1 << 4; + let program2_account = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; program2_size as usize], + bpf_loader_upgradeable::id(), + true, + u64::MAX, + ); + loaded_accounts_map.insert_account(program2, program2_account); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + let fee_payer_size = 0x1 << 5; + let fee_payer_account = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; fee_payer_size as usize], + system_program::id(), + false, + u64::MAX, + ); + loaded_accounts_map.insert_account(fee_payer, fee_payer_account.clone()); + + let account1_keypair = Keypair::new(); + let account1 = account1_keypair.pubkey(); + let account1_size = 0x1 << 6; + let account1_account = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; account1_size as usize], + program1_keypair.pubkey(), + false, + u64::MAX, + ); + loaded_accounts_map.insert_account(account1, account1_account); + + let account2_keypair = Keypair::new(); + let account2 = account2_keypair.pubkey(); + let account2_size = 0x1 << 7; + let account2_account = AccountSharedData::create( + LAMPORTS_PER_SOL, + vec![0; account2_size as usize], + program2_keypair.pubkey(), + false, + u64::MAX, + ); + loaded_accounts_map.insert_account(account2, account2_account); + + for account_meta in [AccountMeta::new, AccountMeta::new_readonly] { + let test_data_size = |instructions, expected_size| { + let transaction = SanitizedTransaction::from_transaction_for_tests( + Transaction::new_signed_with_payer( + instructions, + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ), + ); + + let loaded_transaction_accounts = load_transaction_accounts( + &loaded_accounts_map, + &transaction, + LoadedTransactionAccount { + account: fee_payer_account.clone(), + loaded_size: fee_payer_size as usize, + ..LoadedTransactionAccount::default() + }, + 0, + &ComputeBudgetLimits::default(), + &mut TransactionErrorMetrics::default(), + &FeatureSet::default(), + &RentCollector::default(), + &ProgramCacheForTxBatch::default(), + ) + .unwrap(); + + assert_eq!( + loaded_transaction_accounts.loaded_accounts_data_size, + expected_size + ); + }; + + // one program plus loader + let ixns = vec![Instruction::new_with_bytes(program1, &[], vec![])]; + test_data_size(&ixns, program1_size + bpf_loader_size + fee_payer_size); + + // two programs, two loaders, two accounts + let ixns = vec![ + Instruction::new_with_bytes(program1, &[], vec![account_meta(account1, false)]), + Instruction::new_with_bytes(program2, &[], vec![account_meta(account2, false)]), + ]; + test_data_size( + &ixns, + account1_size + + account2_size + + program1_size + + program2_size + + bpf_loader_size + + upgradeable_loader_size + + fee_payer_size, + ); + + // ordinary owners not counted + let ixns = vec![Instruction::new_with_bytes( + program1, + &[], + vec![account_meta(account2, false)], + )]; + test_data_size( + &ixns, + account2_size + program1_size + bpf_loader_size + fee_payer_size, + ); + + // program and loader counted once + let ixns = vec![ + Instruction::new_with_bytes(program1, &[], vec![]), + Instruction::new_with_bytes(program1, &[], vec![]), + ]; + test_data_size(&ixns, program1_size + bpf_loader_size + fee_payer_size); + + // native loader not counted if loader + let ixns = vec![Instruction::new_with_bytes(bpf_loader::id(), &[], vec![])]; + test_data_size(&ixns, bpf_loader_size + fee_payer_size); + + // native loader counted if instruction + let ixns = vec![Instruction::new_with_bytes( + bpf_loader::id(), + &[], + vec![account_meta(native_loader::id(), false)], + )]; + test_data_size(&ixns, bpf_loader_size + native_loader_size + fee_payer_size); + + // loader counted twice if included in instruction + let ixns = vec![Instruction::new_with_bytes( + program1, + &[], + vec![account_meta(bpf_loader::id(), false)], + )]; + test_data_size(&ixns, program1_size + bpf_loader_size * 2 + fee_payer_size); + + // cover that case with multiple loaders to be sure + let ixns = vec![ + Instruction::new_with_bytes( + program1, + &[], + vec![ + account_meta(bpf_loader::id(), false), + account_meta(bpf_loader_upgradeable::id(), false), + ], + ), + Instruction::new_with_bytes(program2, &[], vec![account_meta(account1, false)]), + Instruction::new_with_bytes( + bpf_loader_upgradeable::id(), + &[], + vec![account_meta(account1, false)], + ), + ]; + test_data_size( + &ixns, + account1_size + + program1_size + + program2_size + + bpf_loader_size * 2 + + upgradeable_loader_size * 2 + + fee_payer_size, + ); + + // loader counted twice even if included first + let ixns = vec![ + Instruction::new_with_bytes(bpf_loader::id(), &[], vec![]), + Instruction::new_with_bytes(program1, &[], vec![]), + ]; + test_data_size(&ixns, program1_size + bpf_loader_size * 2 + fee_payer_size); + + // fee-payer counted once + let ixns = vec![Instruction::new_with_bytes( + program1, + &[], + vec![account_meta(fee_payer, false)], + )]; + test_data_size(&ixns, program1_size + bpf_loader_size + fee_payer_size); + } + } } diff --git a/svm/src/transaction_error_metrics.rs b/svm/src/transaction_error_metrics.rs index 5b3ec2b7e53d1d..0d14f03d8fa640 100644 --- a/svm/src/transaction_error_metrics.rs +++ b/svm/src/transaction_error_metrics.rs @@ -1,6 +1,7 @@ use solana_sdk::saturating_add_assign; #[derive(Debug, Default)] +#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] pub struct TransactionErrorMetrics { pub total: usize, pub account_in_use: usize, diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index f0c9f681a74955..4f86da239169ee 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -3,9 +3,10 @@ use qualifier_attr::{field_qualifiers, qualifiers}; use { crate::{ account_loader::{ - collect_rent_from_account, load_accounts, validate_fee_payer, - CheckedTransactionDetails, LoadedTransaction, LoadedTransactionAccount, - TransactionCheckResult, TransactionLoadResult, TransactionValidationResult, + collect_and_update_loaded_accounts, collect_rent_from_account, load_accounts, + load_transaction, update_accounts_for_failed_tx, validate_fee_payer, + CheckedTransactionDetails, LoadedAccountsMap, LoadedTransaction, + LoadedTransactionAccount, TransactionCheckResult, TransactionLoadResult, ValidatedTransactionDetails, }, account_overrides::AccountOverrides, @@ -15,7 +16,7 @@ use { transaction_account_state_info::TransactionAccountStateInfo, transaction_error_metrics::TransactionErrorMetrics, transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, - transaction_processing_callback::{AccountState, TransactionProcessingCallback}, + transaction_processing_callback::TransactionProcessingCallback, transaction_processing_result::{ProcessedTransaction, TransactionProcessingResult}, }, log::debug, @@ -40,14 +41,16 @@ use { solana_runtime_transaction::instructions_processor::process_compute_budget_instructions, solana_sdk::{ account::{AccountSharedData, ReadableAccount, PROGRAM_OWNERS}, + account_utils::StateMut, clock::{Epoch, Slot}, fee::{FeeBudgetLimits, FeeStructure}, hash::Hash, inner_instruction::{InnerInstruction, InnerInstructionsList}, instruction::{CompiledInstruction, TRANSACTION_LEVEL_STACK_HEIGHT}, + nonce::state::{DurableNonce, State as NonceState, Versions as NonceVersions}, pubkey::Pubkey, rent_collector::RentCollector, - saturating_add_assign, + saturating_add_assign, system_program, transaction::{self, TransactionError}, transaction_context::{ExecutionRecord, TransactionContext}, }, @@ -239,26 +242,13 @@ impl TransactionBatchProcessor { let mut error_metrics = TransactionErrorMetrics::default(); let mut execute_timings = ExecuteTimings::default(); - let (validation_results, validate_fees_us) = measure_us!(self.validate_fees( - callbacks, - config.account_overrides, - sanitized_txs, - check_results, - &environment.feature_set, - environment - .fee_structure - .unwrap_or(&FeeStructure::default()), - environment - .rent_collector - .unwrap_or(&RentCollector::default()), - &mut error_metrics - )); + let mut processing_results = vec![]; let (mut program_cache_for_tx_batch, program_cache_us) = measure_us!({ let mut program_accounts_map = Self::filter_executable_program_accounts( callbacks, sanitized_txs, - &validation_results, + &check_results, PROGRAM_OWNERS, ); for builtin_program in self.builtin_program_ids.read().unwrap().iter() { @@ -285,56 +275,99 @@ impl TransactionBatchProcessor { program_cache_for_tx_batch }); - let (loaded_transactions, load_accounts_us) = measure_us!(load_accounts( + let (mut loaded_accounts_map, load_accounts_us) = measure_us!(load_accounts( callbacks, sanitized_txs, - validation_results, - &mut error_metrics, + &check_results, config.account_overrides, - &environment.feature_set, - environment - .rent_collector - .unwrap_or(&RentCollector::default()), &program_cache_for_tx_batch, )); let enable_transaction_loading_failure_fees = environment .feature_set .is_active(&enable_transaction_loading_failure_fees::id()); - let (processing_results, execution_us): (Vec, u64) = - measure_us!(loaded_transactions - .into_iter() - .zip(sanitized_txs.iter()) - .map(|(load_result, tx)| match load_result { - TransactionLoadResult::NotLoaded(err) => Err(err), - TransactionLoadResult::FeesOnly(fees_only_tx) => { - if enable_transaction_loading_failure_fees { - Ok(ProcessedTransaction::FeesOnly(Box::new(fees_only_tx))) - } else { - Err(fees_only_tx.load_error) - } - } - TransactionLoadResult::Loaded(loaded_transaction) => { - let executed_tx = self.execute_loaded_transaction( + + let (mut validate_fees_us, mut load_transactions_us, mut execution_us): (u64, u64, u64) = + (0, 0, 0); + + let durable_nonce = DurableNonce::from_blockhash(&environment.blockhash); + for (tx, check_result) in sanitized_txs.iter().zip(check_results) { + let (validate_result, single_validate_fees_us) = + measure_us!(check_result.and_then(|tx_details| { + self.validate_transaction_fee_payer( + &loaded_accounts_map, + tx, + tx_details, + &durable_nonce, + &environment.feature_set, + environment + .fee_structure + .unwrap_or(&FeeStructure::default()), + environment + .rent_collector + .unwrap_or(&RentCollector::default()), + &mut error_metrics, + ) + })); + validate_fees_us = validate_fees_us.saturating_add(single_validate_fees_us); + + let (load_result, single_load_transaction_us) = measure_us!(load_transaction( + &loaded_accounts_map, + tx, + validate_result, + &mut error_metrics, + &environment.feature_set, + environment + .rent_collector + .unwrap_or(&RentCollector::default()), + &program_cache_for_tx_batch, + )); + load_transactions_us = load_transactions_us.saturating_add(single_load_transaction_us); + + let (processing_result, single_execution_us) = measure_us!(match load_result { + 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 + update_accounts_for_failed_tx( + &mut loaded_accounts_map, tx, - loaded_transaction, - &mut execute_timings, - &mut error_metrics, - &mut program_cache_for_tx_batch, - environment, - config, + &fees_only_tx.rollback_accounts, ); - // Update batch specific cache of the loaded programs with the modifications - // made by the transaction, if it executed successfully. - if executed_tx.was_successful() { - program_cache_for_tx_batch.merge(&executed_tx.programs_modified_by_tx); - } + Ok(ProcessedTransaction::FeesOnly(Box::new(fees_only_tx))) + } else { + Err(fees_only_tx.load_error) + } + } + TransactionLoadResult::Loaded(loaded_transaction) => { + let executed_tx = self.execute_loaded_transaction( + tx, + loaded_transaction, + &mut execute_timings, + &mut error_metrics, + &mut program_cache_for_tx_batch, + environment, + config, + ); - Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) + // Update batch specific cache of the loaded programs with the modifications + // made by the transaction, if it executed successfully. + if executed_tx.was_successful() { + program_cache_for_tx_batch.merge(&executed_tx.programs_modified_by_tx); } - }) - .collect()); + + // Update loaded accounts cache with account states which might have changed + let processed_tx = ProcessedTransaction::Executed(Box::new(executed_tx)); + collect_and_update_loaded_accounts(&mut loaded_accounts_map, tx, &processed_tx); + + Ok(processed_tx) + } + }); + execution_us = execution_us.saturating_add(single_execution_us); + + processing_results.push(processing_result); + } // Skip eviction when there's no chance this particular tx batch has increased the size of // ProgramCache entries. Note that loaded_missing is deliberately defined, so that there's @@ -358,11 +391,14 @@ impl TransactionBatchProcessor { sanitized_txs.len(), ); + execute_timings + .saturating_add_in_place(ExecuteTimingType::ProgramCacheUs, program_cache_us); + execute_timings + .saturating_add_in_place(ExecuteTimingType::LoadAccountsUs, load_accounts_us); execute_timings .saturating_add_in_place(ExecuteTimingType::ValidateFeesUs, validate_fees_us); execute_timings - .saturating_add_in_place(ExecuteTimingType::ProgramCacheUs, program_cache_us); - execute_timings.saturating_add_in_place(ExecuteTimingType::LoadUs, load_accounts_us); + .saturating_add_in_place(ExecuteTimingType::LoadTransactionsUs, load_transactions_us); execute_timings.saturating_add_in_place(ExecuteTimingType::ExecuteUs, execution_us); LoadAndExecuteSanitizedTransactionsOutput { @@ -372,47 +408,15 @@ impl TransactionBatchProcessor { } } - fn validate_fees( - &self, - callbacks: &CB, - account_overrides: Option<&AccountOverrides>, - sanitized_txs: &[impl core::borrow::Borrow], - check_results: Vec, - feature_set: &FeatureSet, - fee_structure: &FeeStructure, - rent_collector: &dyn SVMRentCollector, - error_counters: &mut TransactionErrorMetrics, - ) -> Vec { - sanitized_txs - .iter() - .zip(check_results) - .map(|(sanitized_tx, check_result)| { - check_result.and_then(|checked_details| { - let message = sanitized_tx.borrow(); - self.validate_transaction_fee_payer( - callbacks, - account_overrides, - message, - checked_details, - feature_set, - fee_structure, - rent_collector, - error_counters, - ) - }) - }) - .collect() - } - // Loads transaction fee payer, collects rent if necessary, then calculates // transaction fees, and deducts them from the fee payer balance. If the // account is not found or has insufficient funds, an error is returned. - fn validate_transaction_fee_payer( + fn validate_transaction_fee_payer( &self, - callbacks: &CB, - account_overrides: Option<&AccountOverrides>, + loaded_accounts_map: &LoadedAccountsMap, message: &impl SVMMessage, checked_details: CheckedTransactionDetails, + durable_nonce: &DurableNonce, feature_set: &FeatureSet, fee_structure: &FeeStructure, rent_collector: &dyn SVMRentCollector, @@ -426,22 +430,13 @@ impl TransactionBatchProcessor { })?; let fee_payer_address = message.fee_payer(); - - let fee_payer_account = account_overrides - .and_then(|overrides| overrides.get(fee_payer_address).cloned()) - .or_else(|| callbacks.get_account_shared_data(fee_payer_address)); + let fee_payer_account = loaded_accounts_map.get_account(fee_payer_address).cloned(); let Some(mut fee_payer_account) = fee_payer_account else { error_counters.account_not_found += 1; return Err(TransactionError::AccountNotFound); }; - callbacks.inspect_account( - fee_payer_address, - AccountState::Alive(&fee_payer_account), - true, // <-- is_writable - ); - let fee_payer_loaded_rent_epoch = fee_payer_account.rent_epoch(); let fee_payer_rent_debit = collect_rent_from_account( feature_set, @@ -452,7 +447,7 @@ impl TransactionBatchProcessor { .rent_amount; let CheckedTransactionDetails { - nonce, + nonce: advanced_nonce, lamports_per_signature, } = checked_details; @@ -475,10 +470,43 @@ impl TransactionBatchProcessor { fee_details.total_fee(), )?; + // If the nonce has been used in this batch already, we must drop the transaction + // This is the same as if it was used in different batches in the same slot + // If the nonce account was closed in the batch, we error as if the blockhash didn't validate + // We must vaidate the account in case it was reopened, either as a normal system account, or a fake nonce account + if let Some(ref advanced_nonce_info) = advanced_nonce { + let nonces_are_equal = loaded_accounts_map + .get_account(advanced_nonce_info.address()) + .and_then(|current_nonce_account| { + system_program::check_id(current_nonce_account.owner()).then_some(())?; + StateMut::::state(current_nonce_account).ok() + }) + .and_then( + |current_nonce_versions| match current_nonce_versions.state() { + NonceState::Initialized(ref current_nonce_data) => { + Some(¤t_nonce_data.durable_nonce == durable_nonce) + } + _ => None, + }, + ); + + match nonces_are_equal { + Some(false) => (), + Some(true) => { + error_counters.account_not_found += 1; + return Err(TransactionError::AccountNotFound); + } + None => { + error_counters.blockhash_not_found += 1; + return Err(TransactionError::BlockhashNotFound); + } + } + } + // Capture fee-subtracted fee payer account and next nonce account state // to commit if transaction execution fails. let rollback_accounts = RollbackAccounts::new( - nonce, + advanced_nonce, *fee_payer_address, fee_payer_account.clone(), fee_payer_rent_debit, @@ -492,8 +520,10 @@ impl TransactionBatchProcessor { loaded_fee_payer_account: LoadedTransactionAccount { loaded_size: fee_payer_account.data().len(), account: fee_payer_account, - rent_collected: fee_payer_rent_debit, + executable_in_batch: false, + valid_loader: false, }, + loaded_fee_payer_rent_collected: fee_payer_rent_debit, }) } @@ -502,11 +532,11 @@ impl TransactionBatchProcessor { fn filter_executable_program_accounts( callbacks: &CB, txs: &[impl SVMMessage], - validation_results: &[TransactionValidationResult], + check_results: &[TransactionCheckResult], program_owners: &[Pubkey], ) -> HashMap { let mut result: HashMap = HashMap::new(); - validation_results.iter().zip(txs).for_each(|etx| { + check_results.iter().zip(txs).for_each(|etx| { if let (Ok(_), tx) = etx { tx.account_keys() .iter() @@ -998,7 +1028,7 @@ mod tests { super::*, crate::{ account_loader::ValidatedTransactionDetails, nonce_info::NonceInfo, - rollback_accounts::RollbackAccounts, + rollback_accounts::RollbackAccounts, transaction_processing_callback::AccountState, }, solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, solana_feature_set::FeatureSet, @@ -1012,7 +1042,7 @@ mod tests { fee_calculator::FeeCalculator, hash::Hash, message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, - nonce, + nonce::{self, state::DurableNonce}, rent_collector::{RentCollector, RENT_EXEMPT_RENT_EPOCH}, rent_debits::RentDebits, reserved_account_keys::ReservedAccountKeys, @@ -1420,9 +1450,9 @@ mod tests { sanitized_transaction_2.clone(), sanitized_transaction_1, ]; - let validation_results = vec![ - Ok(ValidatedTransactionDetails::default()), - Ok(ValidatedTransactionDetails::default()), + let check_results = vec![ + Ok(CheckedTransactionDetails::default()), + Ok(CheckedTransactionDetails::default()), Err(TransactionError::ProgramAccountNotFound), ]; let owners = vec![owner1, owner2]; @@ -1430,7 +1460,7 @@ mod tests { let result = TransactionBatchProcessor::::filter_executable_program_accounts( &mock_bank, &transactions, - &validation_results, + &check_results, &owners, ); @@ -1513,8 +1543,8 @@ mod tests { &bank, &[sanitized_tx1, sanitized_tx2], &[ - Ok(ValidatedTransactionDetails::default()), - Ok(ValidatedTransactionDetails::default()), + Ok(CheckedTransactionDetails::default()), + Ok(CheckedTransactionDetails::default()), ], owners, ); @@ -1605,15 +1635,15 @@ mod tests { let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); let owners = &[program1_pubkey, program2_pubkey]; - let validation_results = vec![ - Ok(ValidatedTransactionDetails::default()), + let check_results = vec![ + Ok(CheckedTransactionDetails::default()), Err(TransactionError::BlockhashNotFound), ]; let programs = TransactionBatchProcessor::::filter_executable_program_accounts( &bank, &[sanitized_tx1, sanitized_tx2], - &validation_results, + &check_results, owners, ); @@ -1878,23 +1908,19 @@ mod tests { &Pubkey::default(), fee_payer_rent_epoch, ); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &rent_collector, @@ -1923,8 +1949,10 @@ mod tests { loaded_fee_payer_account: LoadedTransactionAccount { loaded_size: fee_payer_account.data().len(), account: post_validation_fee_payer_account, - rent_collected: fee_payer_rent_debit, + executable_in_batch: false, + valid_loader: false, }, + loaded_fee_payer_rent_collected: fee_payer_rent_debit, }) ); } @@ -1956,23 +1984,19 @@ mod tests { .lamports(); assert!(fee_payer_rent_debit > 0); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &rent_collector, @@ -2001,8 +2025,10 @@ mod tests { loaded_fee_payer_account: LoadedTransactionAccount { loaded_size: fee_payer_account.data().len(), account: post_validation_fee_payer_account, - rent_collected: fee_payer_rent_debit, - } + executable_in_batch: false, + valid_loader: false, + }, + loaded_fee_payer_rent_collected: fee_payer_rent_debit, }) ); } @@ -2013,17 +2039,17 @@ mod tests { let message = new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); - let mock_bank = MockBankCallback::default(); + let mock_accounts = LoadedAccountsMap::default(); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &RentCollector::default(), @@ -2041,23 +2067,19 @@ mod tests { new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); let fee_payer_address = message.fee_payer(); let fee_payer_account = AccountSharedData::new(1, 0, &Pubkey::default()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &RentCollector::default(), @@ -2079,23 +2101,19 @@ mod tests { let min_balance = rent_collector.rent.minimum_balance(0); let starting_balance = min_balance + transaction_fee - 1; let fee_payer_account = AccountSharedData::new(starting_balance, 0, &Pubkey::default()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &rent_collector, @@ -2115,23 +2133,19 @@ mod tests { new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); let fee_payer_address = message.fee_payer(); let fee_payer_account = AccountSharedData::new(1_000_000, 0, &Pubkey::new_unique()); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &RentCollector::default(), @@ -2153,17 +2167,17 @@ mod tests { Some(&Pubkey::new_unique()), )); - let mock_bank = MockBankCallback::default(); + let mock_accounts = LoadedAccountsMap::default(); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &FeatureSet::default(), &FeeStructure::default(), &RentCollector::default(), @@ -2208,29 +2222,24 @@ mod tests { ) .unwrap(); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); - let nonce = Some(NonceInfo::new( - *fee_payer_address, - fee_payer_account.clone(), - )); + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let mut future_nonce = NonceInfo::new(*fee_payer_address, fee_payer_account.clone()); + future_nonce.try_advance_nonce(durable_nonce, 0).unwrap(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { - nonce: nonce.clone(), + nonce: Some(future_nonce.clone()), lamports_per_signature, }, + &durable_nonce, &feature_set, &FeeStructure::default(), &rent_collector, @@ -2248,7 +2257,7 @@ mod tests { result, Ok(ValidatedTransactionDetails { rollback_accounts: RollbackAccounts::new( - nonce, + Some(future_nonce), *fee_payer_address, post_validation_fee_payer_account.clone(), 0, // fee_payer_rent_debit @@ -2259,8 +2268,10 @@ mod tests { loaded_fee_payer_account: LoadedTransactionAccount { loaded_size: fee_payer_account.data().len(), account: post_validation_fee_payer_account, - rent_collected: 0, - } + executable_in_batch: false, + valid_loader: false, + }, + loaded_fee_payer_rent_collected: 0, }) ); } @@ -2276,23 +2287,19 @@ mod tests { ) .unwrap(); - let mut mock_accounts = HashMap::new(); - mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); - let mock_bank = MockBankCallback { - account_shared_data: Arc::new(RwLock::new(mock_accounts)), - ..Default::default() - }; + let mut mock_accounts = LoadedAccountsMap::default(); + mock_accounts.insert_account(*fee_payer_address, fee_payer_account.clone()); let mut error_counters = TransactionErrorMetrics::default(); let batch_processor = TransactionBatchProcessor::::default(); let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - None, + &mock_accounts, &message, CheckedTransactionDetails { nonce: None, lamports_per_signature, }, + &DurableNonce::default(), &feature_set, &FeeStructure::default(), &rent_collector, @@ -2303,116 +2310,4 @@ mod tests { assert_eq!(result, Err(TransactionError::InsufficientFundsForFee)); } } - - #[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 error_counters = TransactionErrorMetrics::default(); - let batch_processor = TransactionBatchProcessor::::default(); - - let result = batch_processor.validate_transaction_fee_payer( - &mock_bank, - Some(&account_overrides), - &message, - CheckedTransactionDetails { - nonce: None, - lamports_per_signature, - }, - &FeatureSet::default(), - &FeeStructure::default(), - &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] - fn test_inspect_account_fee_payer() { - let fee_payer_address = Pubkey::new_unique(); - let fee_payer_account = AccountSharedData::new_rent_epoch( - 123_000_000_000, - 0, - &Pubkey::default(), - RENT_EXEMPT_RENT_EPOCH, - ); - let mock_bank = MockBankCallback::default(); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(fee_payer_address, fee_payer_account.clone()); - - let message = new_unchecked_sanitized_message(Message::new_with_blockhash( - &[ - ComputeBudgetInstruction::set_compute_unit_limit(2000u32), - ComputeBudgetInstruction::set_compute_unit_price(1_000_000_000), - ], - Some(&fee_payer_address), - &Hash::new_unique(), - )); - let batch_processor = TransactionBatchProcessor::::default(); - batch_processor - .validate_transaction_fee_payer( - &mock_bank, - None, - &message, - CheckedTransactionDetails { - nonce: None, - lamports_per_signature: 5000, - }, - &FeatureSet::default(), - &FeeStructure::default(), - &RentCollector::default(), - &mut TransactionErrorMetrics::default(), - ) - .unwrap(); - - // ensure the fee payer is an inspected account - let actual_inspected_accounts: Vec<_> = mock_bank - .inspected_accounts - .read() - .unwrap() - .iter() - .map(|(k, v)| (*k, v.clone())) - .collect(); - assert_eq!( - actual_inspected_accounts.as_slice(), - &[(fee_payer_address, vec![(Some(fee_payer_account), true)])], - ); - } } diff --git a/svm/tests/example-programs/write-to-account/Cargo.toml b/svm/tests/example-programs/write-to-account/Cargo.toml new file mode 100644 index 00000000000000..903be78c584f66 --- /dev/null +++ b/svm/tests/example-programs/write-to-account/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "write-to-account" +version = "2.1.0" +edition = "2021" + +[dependencies] +solana-program = { path = "../../../../sdk/program", version = "=2.1.0" } + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/svm/tests/example-programs/write-to-account/src/lib.rs b/svm/tests/example-programs/write-to-account/src/lib.rs new file mode 100644 index 00000000000000..bb7e6065547336 --- /dev/null +++ b/svm/tests/example-programs/write-to-account/src/lib.rs @@ -0,0 +1,62 @@ +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint, + entrypoint::ProgramResult, + incinerator, msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let target_account_info = next_account_info(accounts_iter)?; + match data[0] { + // print account size + 0 => { + msg!( + "account size {}", + target_account_info.try_borrow_data()?.len() + ); + } + // set account data + 1 => { + let mut account_data = target_account_info.try_borrow_mut_data()?; + account_data[0] = 100; + } + // deallocate account + 2 => { + let incinerator_info = next_account_info(accounts_iter)?; + if !incinerator::check_id(incinerator_info.key) { + return Err(ProgramError::InvalidAccountData); + } + + let mut target_lamports = target_account_info.try_borrow_mut_lamports()?; + let mut incinerator_lamports = incinerator_info.try_borrow_mut_lamports()?; + + **incinerator_lamports = incinerator_lamports + .checked_add(**target_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + **target_lamports = target_lamports + .checked_sub(**target_lamports) + .ok_or(ProgramError::InsufficientFunds)?; + } + // reallocate account + 3 => { + let new_size = usize::from_le_bytes(data[1..9].try_into().unwrap()); + target_account_info.realloc(new_size, false)?; + } + // bad ixn + _ => { + return Err(ProgramError::InvalidArgument); + } + } + + Ok(()) +} diff --git a/svm/tests/example-programs/write-to-account/write_to_account_program.so b/svm/tests/example-programs/write-to-account/write_to_account_program.so new file mode 100755 index 00000000000000..e43642631bc688 Binary files /dev/null and b/svm/tests/example-programs/write-to-account/write_to_account_program.so differ diff --git a/svm/tests/integration_test.rs b/svm/tests/integration_test.rs index 1051f5eb651e7e..ac4951facd0c1a 100644 --- a/svm/tests/integration_test.rs +++ b/svm/tests/integration_test.rs @@ -1,14 +1,17 @@ #![cfg(test)] +#![allow(clippy::arithmetic_side_effects)] use { crate::mock_bank::{ create_executable_environment, deploy_program, deploy_program_with_upgrade_authority, - program_address, register_builtins, MockBankCallback, MockForkGraph, EXECUTION_EPOCH, - EXECUTION_SLOT, WALLCLOCK_TIME, + program_address, program_data_size, register_builtins, MockBankCallback, MockForkGraph, + EXECUTION_EPOCH, EXECUTION_SLOT, WALLCLOCK_TIME, }, solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, clock::Slot, + compute_budget::ComputeBudgetInstruction, + entrypoint::MAX_PERMITTED_DATA_INCREASE, feature_set::{self, FeatureSet}, hash::Hash, instruction::{AccountMeta, Instruction}, @@ -27,7 +30,7 @@ use { nonce_info::NonceInfo, rollback_accounts::RollbackAccounts, transaction_execution_result::TransactionExecutionDetails, - transaction_processing_result::ProcessedTransaction, + transaction_processing_result::{ProcessedTransaction, TransactionProcessingResult}, transaction_processor::{ ExecutionRecordingConfig, TransactionBatchProcessor, TransactionProcessingConfig, TransactionProcessingEnvironment, @@ -49,7 +52,7 @@ const LAST_BLOCKHASH: Hash = Hash::new_from_array([7; 32]); // Arbitrary constan pub type AccountsMap = HashMap; // container for a transaction batch and all data needed to run and verify it against svm -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct SvmTestEntry { // features are disabled by default; these will be enabled pub enabled_features: Vec, @@ -156,12 +159,14 @@ impl SvmTestEntry { mut nonce_info: NonceInfo, status: ExecutionStatus, ) { - nonce_info - .try_advance_nonce( - DurableNonce::from_blockhash(&LAST_BLOCKHASH), - LAMPORTS_PER_SIGNATURE, - ) - .unwrap(); + if status != ExecutionStatus::Discarded { + nonce_info + .try_advance_nonce( + DurableNonce::from_blockhash(&LAST_BLOCKHASH), + LAMPORTS_PER_SIGNATURE, + ) + .unwrap(); + } self.transaction_batch.push(TransactionBatchItem { transaction, @@ -321,6 +326,22 @@ impl ExecutionStatus { } } +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), @@ -666,7 +687,11 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V // * true/false: normal nonce account used to pay fees with rent minimum plus 1sol // * false/true: normal nonce account with rent minimum, fee payer doesnt exist // * true/true: same account for both which does not exist - let mk_nonce_transaction = |test_entry: &mut SvmTestEntry, program_id, fake_fee_payer: bool| { + // we also provide a side door to bring a fee-paying nonce account below rent-exemption + 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 { @@ -682,8 +707,12 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V 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); + } else if rent_paying_nonce { + assert!(fee_paying_nonce); + nonce_balance += LAMPORTS_PER_SIGNATURE; + nonce_balance -= 1; } else if fee_paying_nonce { - nonce_balance = nonce_balance.saturating_add(LAMPORTS_PER_SOL); + nonce_balance += LAMPORTS_PER_SOL; } let nonce_initial_hash = DurableNonce::from_blockhash(&Hash::new_unique()); @@ -716,10 +745,11 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V (transaction, fee_payer, nonce_info) }; - // successful nonce transaction, regardless of features + // 0: successful nonce transaction, regardless of features { let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, real_program_id, false); + mk_nonce_transaction(&mut test_entry, real_program_id, false, false); + test_entry.push_nonce_transaction(transaction, nonce_info.clone()); test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); @@ -739,10 +769,10 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V .copy_from_slice(nonce_info.account().data()); } - // non-executing nonce transaction (fee payer doesnt exist) regardless of features + // 1: non-executing nonce transaction (fee payer doesnt exist) regardless of features { let (transaction, _fee_payer, nonce_info) = - mk_nonce_transaction(&mut test_entry, real_program_id, true); + mk_nonce_transaction(&mut test_entry, real_program_id, true, false); test_entry .final_accounts @@ -756,10 +786,11 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V ); } - // failing nonce transaction (bad system instruction) regardless of features + // 2: failing nonce transaction (bad system instruction) regardless of features { let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, system_program::id(), false); + mk_nonce_transaction(&mut test_entry, system_program::id(), false, false); + test_entry.push_nonce_transaction_with_status( transaction, nonce_info.clone(), @@ -783,11 +814,10 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V .copy_from_slice(nonce_info.account().data()); } - // and this (program doesnt exist) will be a non-executing transaction without the feature - // or a fee-only transaction with it. which is identical to failed *except* rent is not updated + // 3: processable non-executable nonce transaction with fee-only enabled, otherwise discarded { let (transaction, fee_payer, mut nonce_info) = - mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false); + mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, false); if enable_fee_only_transactions { test_entry.push_nonce_transaction_with_status( @@ -841,196 +871,1335 @@ fn simple_nonce(enable_fee_only_transactions: bool, fee_paying_nonce: bool) -> V } } - vec![test_entry] -} - -#[test_case(program_medley())] -#[test_case(simple_transfer(false))] -#[test_case(simple_transfer(true))] -#[test_case(simple_nonce(false, false))] -#[test_case(simple_nonce(true, false))] -#[test_case(simple_nonce(false, true))] -#[test_case(simple_nonce(true, true))] -fn svm_integration(test_entries: Vec) { - for test_entry in test_entries { - execute_test_entry(test_entry); - } -} + // 4: safety check that rent-paying nonce fee-payers are verboten (blockhash fee-payers may be below rent-exemption) + // if this situation is ever allowed in the future, the nonce account MUST be hidden for fee-only transactions + // as an aside, nonce accounts closed by WithdrawNonceAccount are safe because they are ordinary executed transactions + // we also dont care whether a non-fee nonce (or any account) pays rent because rent is charged on executed transactions + if fee_paying_nonce { + let (transaction, _, nonce_info) = + mk_nonce_transaction(&mut test_entry, real_program_id, false, true); -fn execute_test_entry(test_entry: SvmTestEntry) { - let mock_bank = MockBankCallback::default(); + test_entry + .final_accounts + .get_mut(nonce_info.address()) + .unwrap() + .set_rent_epoch(0); - for (name, slot, authority) in &test_entry.initial_programs { - deploy_program_with_upgrade_authority(name.to_string(), *slot, &mock_bank, *authority); + test_entry.push_nonce_transaction_with_status( + transaction, + nonce_info.clone(), + ExecutionStatus::Discarded, + ); } - for (pubkey, account) in &test_entry.initial_accounts { - mock_bank - .account_shared_data - .write() + // 5: rent-paying nonce fee-payers are also not charged for fee-only transactions + if enable_fee_only_transactions && fee_paying_nonce { + let (transaction, _, nonce_info) = + mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, true); + + test_entry + .final_accounts + .get_mut(nonce_info.address()) .unwrap() - .insert(*pubkey, account.clone()); + .set_rent_epoch(0); + + test_entry.push_nonce_transaction_with_status( + transaction, + nonce_info.clone(), + ExecutionStatus::Discarded, + ); } - let batch_processor = TransactionBatchProcessor::::new( - EXECUTION_SLOT, - EXECUTION_EPOCH, - HashSet::new(), - ); + vec![test_entry] +} - let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); +fn 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); - create_executable_environment( - fork_graph.clone(), - &mock_bank, - &mut batch_processor.program_cache.write().unwrap(), - ); + // batch 0: two successful transfers from the same source + { + let mut test_entry = SvmTestEntry::default(); - // The sysvars must be put in the cache - batch_processor.fill_missing_sysvar_cache_entries(&mock_bank); - register_builtins(&mock_bank, &batch_processor); + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination1 = Pubkey::new_unique(); + let destination2 = Pubkey::new_unique(); - let processing_config = TransactionProcessingConfig { - recording_config: ExecutionRecordingConfig { - enable_log_recording: true, - enable_return_data_recording: true, - enable_cpi_recording: false, - }, - ..Default::default() - }; + let mut source_data = AccountSharedData::default(); + let destination1_data = AccountSharedData::default(); + let destination2_data = AccountSharedData::default(); - let mut feature_set = FeatureSet::default(); - for feature_id in &test_entry.enabled_features { - feature_set.activate(feature_id, 0); + 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 processing_environment = TransactionProcessingEnvironment { - blockhash: LAST_BLOCKHASH, - feature_set: feature_set.into(), - lamports_per_signature: LAMPORTS_PER_SIGNATURE, - ..TransactionProcessingEnvironment::default() - }; + // batch 1: + // * successful transfer, source left with rent-exempt minimum + // * non-processable transfer due to underfunded fee-payer + { + let mut test_entry = SvmTestEntry::default(); - // execute transaction batch - let (transactions, check_results) = test_entry.prepare_transactions(); - let batch_output = batch_processor.load_and_execute_sanitized_transactions( - &mock_bank, - &transactions, - check_results, - &processing_environment, - &processing_config, - ); + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination = Pubkey::new_unique(); - // build a hashmap of final account states incrementally, starting with all initial states, updating to all final states - // NOTE with SIMD-83 an account may appear multiple times in the same batch - let mut final_accounts_actual = test_entry.initial_accounts.clone(); + let mut source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); - 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); - } - } - Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => { - let fee_payer = transactions[index].fee_payer(); + source_data.set_lamports(transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent); + test_entry.add_initial_account(source, &source_data); - match fees_only_transaction.rollback_accounts.clone() { - RollbackAccounts::FeePayerOnly { fee_payer_account } => { - final_accounts_actual.insert(*fee_payer, fee_payer_account); - } - RollbackAccounts::SameNonceAndFeePayer { nonce } => { - final_accounts_actual.insert(*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()); - } - } - } - Err(_) => {} - } - } + test_entry.push_transaction(system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + )); - // check that all the account states we care about are present and correct - for (pubkey, expected_account_data) in 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 + 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); } - // now run our transaction-by-transaction checks - for (processing_result, test_item_asserts) in batch_output - .processing_results - .iter() - .zip(test_entry.asserts()) + // batch 2: + // * successful transfer to a previously unfunded account + // * successful transfer using the new account as a fee-payer in the same batch { - 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 test_entry = SvmTestEntry::default(); + let first_transfer_amount = transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent; + let second_transfer_amount = transfer_amount; -#[test] -fn svm_inspect_account() { - let mock_bank = MockBankCallback::default(); - let mut expected_inspected_accounts: HashMap<_, Vec<_>> = HashMap::new(); + 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 transfer_program = - deploy_program("simple-transfer".to_string(), DEPLOYMENT_SLOT, &mock_bank); + let mut grandparent_data = AccountSharedData::default(); + let mut parent_data = AccountSharedData::default(); + let mut child_data = AccountSharedData::default(); - let fee_payer_keypair = Keypair::new(); - let sender_keypair = Keypair::new(); + grandparent_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(grandparent, &grandparent_data); - let fee_payer = fee_payer_keypair.pubkey(); - let sender = sender_keypair.pubkey(); - let recipient = Pubkey::new_unique(); - let system = system_program::id(); + test_entry.push_transaction(system_transaction::transfer( + &grandparent_keypair, + &parent, + first_transfer_amount, + Hash::default(), + )); - // Setting up the accounts for the transfer + parent_data + .checked_add_lamports(first_transfer_amount) + .unwrap(); + test_entry.create_expected_account(parent, &parent_data); - // fee payer - let mut fee_payer_account = AccountSharedData::default(); - fee_payer_account.set_lamports(80_020); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(fee_payer, fee_payer_account.clone()); - expected_inspected_accounts - .entry(fee_payer) - .or_default() - .push((Some(fee_payer_account.clone()), true)); + test_entry.decrease_expected_lamports( + &grandparent, + first_transfer_amount + LAMPORTS_PER_SIGNATURE, + ); - // sender - let mut sender_account = AccountSharedData::default(); - sender_account.set_lamports(11_000_000); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(sender, sender_account.clone()); - expected_inspected_accounts - .entry(sender) - .or_default() - .push((Some(sender_account.clone()), true)); + test_entry.push_transaction(system_transaction::transfer( + &parent_keypair, + &child, + second_transfer_amount, + Hash::default(), + )); - // recipient -- initially dead + 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); + } + + if enable_fee_only_transactions { + for test_entry in &mut test_entries { + test_entry + .enabled_features + .push(feature_set::enable_transaction_loading_failure_fees::id()); + } + } + + test_entries +} + +fn 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 + // * a successful blockhash noop transaction that touches the nonce, convenience to see state update + 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.push_transaction(Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes( + program_id, + &[], + vec![AccountMeta::new_readonly(nonce_pubkey, false)], + )], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + + test_entry + .increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL - LAMPORTS_PER_SIGNATURE); + + test_entry.update_expected_account_data(nonce_pubkey, &AccountSharedData::default()); + + 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); + } + + 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 = match self { + Self::Print => vec![0], + Self::Set => vec![1], + Self::Dealloc => vec![2], + Self::Realloc(new_size) => { + let mut vec = vec![3]; + vec.extend_from_slice(&new_size.to_le_bytes()); + vec + } + }; + + let account_metas = if self == Self::Dealloc { + vec![ + AccountMeta::new(target, false), + AccountMeta::new(solana_sdk::incinerator::id(), false), + ] + } else { + 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 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.update_expected_account_data(target, &AccountSharedData::default()); + } + + test_entries.push(test_entry); + } + + test_entries +} + +fn 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.update_expected_account_data(dealloc_fee_payer, &AccountSharedData::default()); + } + + // 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.update_expected_account_data(dealloc_fee_payer, &AccountSharedData::default()); + } + + vec![test_entry] +} + +fn 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(); + + 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); + } + + 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 +} + +#[test_case(program_medley())] +#[test_case(simple_transfer(false))] +#[test_case(simple_transfer(true))] +#[test_case(simple_nonce(false, false))] +#[test_case(simple_nonce(true, false))] +#[test_case(simple_nonce(false, true))] +#[test_case(simple_nonce(true, true))] +#[test_case(intrabatch_account_reuse(false))] +#[test_case(intrabatch_account_reuse(true))] +#[test_case(nonce_reuse(false, false))] +#[test_case(nonce_reuse(true, false))] +#[test_case(nonce_reuse(false, true))] +#[test_case(nonce_reuse(true, true))] +#[test_case(account_deallocate())] +#[test_case(fee_payer_deallocate(false))] +#[test_case(fee_payer_deallocate(true))] +#[test_case(account_reallocate(false))] +#[test_case(account_reallocate(true))] +fn svm_integration(test_entries: Vec) { + for test_entry in test_entries { + execute_test_entry(test_entry); + } +} + +fn execute_test_entry(test_entry: SvmTestEntry) { + 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 batch_processor = TransactionBatchProcessor::::new( + EXECUTION_SLOT, + EXECUTION_EPOCH, + HashSet::new(), + ); + + let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); + + create_executable_environment( + fork_graph.clone(), + &mock_bank, + &mut batch_processor.program_cache.write().unwrap(), + ); + + // The sysvars must be put in the cache + 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, + }, + ..Default::default() + }; + + let mut feature_set = FeatureSet::default(); + for feature_id in &test_entry.enabled_features { + feature_set.activate(feature_id, 0); + } + + let processing_environment = TransactionProcessingEnvironment { + blockhash: LAST_BLOCKHASH, + feature_set: feature_set.into(), + lamports_per_signature: LAMPORTS_PER_SIGNATURE, + ..TransactionProcessingEnvironment::default() + }; + + // execute transaction batch + let (transactions, check_results) = test_entry.prepare_transactions(); + let batch_output = batch_processor.load_and_execute_sanitized_transactions( + &mock_bank, + &transactions, + check_results, + &processing_environment, + &processing_config, + ); + + // build a hashmap of final account states incrementally, starting with all initial states, updating to all final states + // we do it this way because an account may change multiple times in the same batch but may not exist on all transactions + let mut final_accounts_actual = test_entry.initial_accounts.clone(); + + 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); + } + } + Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => { + let fee_payer = transactions[index].fee_payer(); + + match fees_only_transaction.rollback_accounts.clone() { + RollbackAccounts::FeePayerOnly { fee_payer_account } => { + final_accounts_actual.insert(*fee_payer, fee_payer_account); + } + RollbackAccounts::SameNonceAndFeePayer { nonce } => { + final_accounts_actual.insert(*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()); + } + } + } + Err(_) => {} + } + } + + // first assert all transaction states together, it makes test-driven development much less of a headache + let (expected_statuses, actual_statuses): (Vec<_>, Vec<_>) = batch_output + .processing_results + .iter() + .zip(test_entry.asserts()) + .map(|(processing_result, test_item_assert)| { + ( + ExecutionStatus::from(processing_result), + test_item_assert.status, + ) + }) + .unzip(); + assert_eq!(expected_statuses, actual_statuses); + + // check that all the account states we care about are present and correct + for (pubkey, expected_account_data) in 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 + ); + } + + // now run our transaction-by-transaction checks + for (processing_result, test_item_asserts) in batch_output + .processing_results + .iter() + .zip(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()), + } + } +} + +#[test] +fn svm_inspect_account() { + let mock_bank = MockBankCallback::default(); + let mut expected_inspected_accounts: HashMap<_, Vec<_>> = HashMap::new(); + + let transfer_program = + deploy_program("simple-transfer".to_string(), DEPLOYMENT_SLOT, &mock_bank); + + 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 system = system_program::id(); + + // Setting up the accounts for the transfer + + // fee payer + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(80_020); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(fee_payer, fee_payer_account.clone()); + expected_inspected_accounts + .entry(fee_payer) + .or_default() + .push((Some(fee_payer_account.clone()), true)); + + // sender + let mut sender_account = AccountSharedData::default(); + sender_account.set_lamports(11_000_000); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sender, sender_account.clone()); + expected_inspected_accounts + .entry(sender) + .or_default() + .push((Some(sender_account.clone()), true)); + + // recipient -- initially dead expected_inspected_accounts .entry(recipient) .or_default() @@ -1192,26 +2361,18 @@ fn svm_inspect_account() { ); } - // The transfer program account is also loaded during transaction processing, however the - // account state passed to `inspect_account()` is *not* the same as what is held by - // MockBankCallback::account_shared_data. So we check the transfer program differently. - // - // First ensure we have the correct number of inspected accounts, correctly counting the - // transfer program. + // The transfer program account is retreived from the program cache, which does not + // inspect accounts, because they are necessarily read-only. Verify it has not made + // its way into the inspected accounts list. let num_expected_inspected_accounts: usize = expected_inspected_accounts.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 + 2, + num_expected_inspected_accounts, num_actual_inspected_accounts, ); - // And second, ensure the inspected transfer program accounts are alive and not writable. - let actual_transfer_program_accounts = - actual_inspected_accounts.get(&transfer_program).unwrap(); - for actual_transfer_program_account in actual_transfer_program_accounts { - assert!(actual_transfer_program_account.0.is_some()); - assert!(!actual_transfer_program_account.1); - } + assert!(!actual_inspected_accounts.contains_key(&transfer_program)); } diff --git a/svm/tests/mock_bank.rs b/svm/tests/mock_bank.rs index 58a1c155f226a9..b8fe4441124dbe 100644 --- a/svm/tests/mock_bank.rs +++ b/svm/tests/mock_bank.rs @@ -21,7 +21,7 @@ use { account::{AccountSharedData, ReadableAccount, WritableAccount}, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::{Clock, UnixTimestamp}, - native_loader, + compute_budget, native_loader, pubkey::Pubkey, rent::Rent, slot_hashes::Slot, @@ -138,6 +138,11 @@ pub fn program_address(program_name: &str) -> Pubkey { Pubkey::create_with_seed(&Pubkey::default(), program_name, &Pubkey::default()).unwrap() } +#[allow(unused)] +pub fn program_data_size(program_name: &str) -> usize { + load_program(program_name.to_string()).len() +} + #[allow(unused)] pub fn deploy_program(name: String, deployment_slot: Slot, mock_bank: &MockBankCallback) -> Pubkey { deploy_program_with_upgrade_authority(name, deployment_slot, mock_bank, None) @@ -294,6 +299,19 @@ pub fn register_builtins( solana_system_program::system_processor::Entrypoint::vm, ), ); + + // For testing realloc, we need the compute budget program + let compute_budget_program_name = "compute_budget_program"; + batch_processor.add_builtin( + mock_bank, + compute_budget::id(), + compute_budget_program_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + compute_budget_program_name.len(), + solana_compute_budget_program::Entrypoint::vm, + ), + ); } #[allow(unused)] diff --git a/timings/src/lib.rs b/timings/src/lib.rs index 46878559eb25cc..ea2c3f8a640d41 100644 --- a/timings/src/lib.rs +++ b/timings/src/lib.rs @@ -46,7 +46,7 @@ impl ProgramTiming { pub enum ExecuteTimingType { CheckUs, ValidateFeesUs, - LoadUs, + LoadAccountsUs, ExecuteUs, StoreUs, UpdateStakesCacheUs, @@ -57,6 +57,7 @@ pub enum ExecuteTimingType { UpdateTransactionStatuses, ProgramCacheUs, CheckBlockLimitsUs, + LoadTransactionsUs, } pub struct Metrics([u64; ExecuteTimingType::CARDINALITY]); @@ -101,24 +102,31 @@ eager_macro_rules! { $eager_1 i64 ), ( - "validate_fees_us", + "program_cache_us", *$self .metrics - .index(ExecuteTimingType::ValidateFeesUs), + .index(ExecuteTimingType::ProgramCacheUs), i64 ), ( - "program_cache_us", + "load_accounts_us", *$self .metrics - .index(ExecuteTimingType::ProgramCacheUs), + .index(ExecuteTimingType::LoadAccountsUs), + i64 + ), + ( + "validate_fees_us", + *$self + .metrics + .index(ExecuteTimingType::ValidateFeesUs), i64 ), ( - "load_us", + "load_transactions_us", *$self .metrics - .index(ExecuteTimingType::LoadUs), + .index(ExecuteTimingType::LoadTransactionsUs), i64 ), (