From 8f3e960640aa35737dc8570042b500ef6e953e24 Mon Sep 17 00:00:00 2001 From: Lucas Steuernagel <38472950+LucasSte@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:17:45 -0300 Subject: [PATCH] Add tests for private functions in SVM `account_loader.rs` (#35334) --- sdk/src/transaction/sanitized.rs | 14 + svm/src/account_loader.rs | 676 ++++++++++++++++++++++++++++++- 2 files changed, 685 insertions(+), 5 deletions(-) diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index 4189f1b64..b7383b4a0 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -271,6 +271,20 @@ impl SanitizedTransaction { Ok(()) } } + + #[cfg(feature = "dev-context-only-utils")] + pub fn new_for_tests( + message: SanitizedMessage, + signatures: Vec, + is_simple_vote_tx: bool, + ) -> SanitizedTransaction { + SanitizedTransaction { + message, + message_hash: Hash::new_unique(), + signatures, + is_simple_vote_tx, + } + } } #[cfg(test)] diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 334ad7679..197d46250 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -276,6 +276,7 @@ fn load_transaction_accounts( .map(|instruction| { let mut account_indices = Vec::new(); let mut 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)?; @@ -452,7 +453,7 @@ pub fn validate_fee_payer( ) } -pub fn construct_instructions_account(message: &SanitizedMessage) -> AccountSharedData { +fn construct_instructions_account(message: &SanitizedMessage) -> AccountSharedData { AccountSharedData::from(Account { data: construct_instructions_data(&message.decompile_instructions()), owner: sysvar::id(), @@ -464,31 +465,38 @@ pub fn construct_instructions_account(message: &SanitizedMessage) -> AccountShar mod tests { use { super::*, + crate::transaction_processor::TransactionProcessingCallback, nonce::state::Versions as NonceVersions, solana_program_runtime::{ compute_budget_processor, + loaded_programs::LoadedProgram, prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType}, }, solana_sdk::{ - account::{AccountSharedData, WritableAccount}, + account::{AccountSharedData, ReadableAccount, WritableAccount}, bpf_loader_upgradeable, compute_budget::ComputeBudgetInstruction, epoch_schedule::EpochSchedule, feature_set::FeatureSet, hash::Hash, instruction::CompiledInstruction, - message::{Message, SanitizedMessage}, + message::{ + v0::{LoadedAddresses, LoadedMessage}, + LegacyMessage, Message, MessageHeader, SanitizedMessage, + }, nonce, + pubkey::Pubkey, rent::Rent, rent_collector::RentCollector, - signature::{Keypair, Signer}, + signature::{Keypair, Signature, Signer}, system_program, sysvar, transaction::{Result, Transaction, TransactionError}, transaction_context::TransactionAccount, }, - std::{convert::TryFrom, sync::Arc}, + std::{borrow::Cow, convert::TryFrom, sync::Arc}, }; + #[derive(Default)] struct TestCallbacks { accounts_map: HashMap, rent_collector: RentCollector, @@ -1380,4 +1388,662 @@ mod tests { &rent_collector, ); } + + #[test] + fn test_construct_instructions_account() { + let loaded_message = LoadedMessage { + message: Cow::Owned(solana_sdk::message::v0::Message::default()), + loaded_addresses: Cow::Owned(LoadedAddresses::default()), + is_writable_account_cache: vec![false], + }; + let message = SanitizedMessage::V0(loaded_message); + let shared_data = construct_instructions_account(&message); + let expected = AccountSharedData::from(Account { + data: construct_instructions_data(&message.decompile_instructions()), + owner: sysvar::id(), + ..Account::default() + }); + assert_eq!(shared_data, expected); + } + + #[test] + fn test_account_shared_data_from_program() { + let key = Keypair::new().pubkey(); + let other_key = Keypair::new().pubkey(); + + let mut accounts: HashMap = HashMap::new(); + + let result = account_shared_data_from_program(&key, &accounts); + assert_eq!(result.err(), Some(TransactionError::AccountNotFound)); + + accounts.insert(key, (&other_key, 32)); + + let result = account_shared_data_from_program(&key, &accounts); + let mut expected = AccountSharedData::default(); + expected.set_owner(other_key); + expected.set_executable(true); + expected.set_data_from_slice(create_executable_meta(&other_key)); + + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_load_transaction_accounts_failure() { + let message = Message::default(); + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mock_bank = TestCallbacks::default(); + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = + SanitizedTransaction::new_for_tests(sanitized_message, vec![], false); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + + assert_eq!(result.err(), Some(TransactionError::MissingSignatureForFee)); + } + + #[test] + fn test_load_transaction_accounts_fail_to_validate_fee_payer() { + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mock_bank = TestCallbacks::default(); + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + + assert_eq!(result.err(), Some(TransactionError::AccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_native_loader() { + let key1 = Keypair::new(); + let message = Message { + account_keys: vec![key1.pubkey(), native_loader::id()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::default(); + mock_bank + .accounts_map + .insert(native_loader::id(), AccountSharedData::default()); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), account_data); + + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key1.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + assert_eq!( + result.unwrap(), + LoadedTransaction { + accounts: vec![ + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].clone() + ), + ( + native_loader::id(), + mock_bank.accounts_map[&native_loader::id()].clone() + ) + ], + program_indices: vec![vec![]], + rent: 0, + rent_debits: RentDebits::default() + } + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_not_found_but_loaded() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), account_data); + + let mut error_counter = TransactionErrorMetrics::default(); + let mut loaded_programs = LoadedProgramsForTxBatch::default(); + loaded_programs.replenish(key2.pubkey(), Arc::new(LoadedProgram::default())); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + + assert_eq!(result.err(), Some(TransactionError::AccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_program_account_no_data() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), account_data); + + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_invalid_program_for_execution() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key1.pubkey(), account_data); + + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_native_loader_owner() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::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); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), account_data); + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key2.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + assert_eq!( + result.unwrap(), + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.accounts_map[&key2.pubkey()].clone() + ), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].clone() + ), + ], + program_indices: vec![vec![1]], + rent: 0, + rent_debits: RentDebits::default() + } + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_not_found_after_all_checks() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + mock_bank.accounts_map.insert(key1.pubkey(), account_data); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), account_data); + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key2.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_program_account_invalid_program_for_execution_last_check() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::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); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), account_data); + + mock_bank + .accounts_map + .insert(key3.pubkey(), AccountSharedData::default()); + let mut error_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key2.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_program_success_complete() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::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); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), account_data); + + 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_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key2.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + assert_eq!( + result.unwrap(), + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.accounts_map[&key2.pubkey()].clone() + ), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].clone() + ), + ( + key3.pubkey(), + mock_bank.accounts_map[&key3.pubkey()].clone() + ), + ], + program_indices: vec![vec![2, 1]], + rent: 0, + rent_debits: RentDebits::default() + } + ); + } + + #[test] + fn test_load_transaction_accounts_program_builtin_saturating_add() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + let key4 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key4.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let legacy = LegacyMessage::new(message); + let sanitized_message = SanitizedMessage::Legacy(legacy); + let mut mock_bank = TestCallbacks::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); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank.accounts_map.insert(key2.pubkey(), account_data); + + 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_counter = TransactionErrorMetrics::default(); + let loaded_programs = LoadedProgramsForTxBatch::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mock_bank, + &sanitized_transaction, + 32, + &mut error_counter, + None, + &HashMap::new(), + &loaded_programs, + ); + mock_bank + .accounts_map + .get_mut(&key2.pubkey()) + .unwrap() + .set_lamports(200 - 32); + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + assert_eq!( + result.unwrap(), + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.accounts_map[&key2.pubkey()].clone() + ), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].clone() + ), + (key4.pubkey(), account_data), + ( + key3.pubkey(), + mock_bank.accounts_map[&key3.pubkey()].clone() + ), + ], + program_indices: vec![vec![3, 1], vec![3, 1]], + rent: 0, + rent_debits: RentDebits::default() + } + ); + } }