From 532b806bef8a80fe6c80f063b40cb437d8df378f Mon Sep 17 00:00:00 2001 From: Lucas Steuernagel <38472950+LucasSte@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:04:08 -0300 Subject: [PATCH] Add more unit tests to SVM (#35383) --- svm/src/account_loader.rs | 2 + svm/src/account_overrides.rs | 31 ++++ svm/src/account_rent_state.rs | 67 +++++++ svm/src/transaction_account_state_info.rs | 169 +++++++++++++++++ svm/tests/account_loader.rs | 214 ++++++++++++++++++++++ 5 files changed, 483 insertions(+) create mode 100644 svm/tests/account_loader.rs diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 58bd7c616..126be625e 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -106,6 +106,8 @@ pub fn load_accounts( &loaded_transaction.rent_debits, ) { Ok(nonce) => Some(nonce), + // This error branch is never reached, because `load_transaction_accounts` + // already validates the fee payer account. Err(e) => return (Err(e), None), } } else { diff --git a/svm/src/account_overrides.rs b/svm/src/account_overrides.rs index ee8e7ec9e..c88d77d54 100644 --- a/svm/src/account_overrides.rs +++ b/svm/src/account_overrides.rs @@ -29,3 +29,34 @@ impl AccountOverrides { self.accounts.get(pubkey) } } + +#[cfg(test)] +mod test { + use { + crate::account_overrides::AccountOverrides, + solana_sdk::{account::AccountSharedData, pubkey::Pubkey, sysvar}, + }; + + #[test] + fn test_set_account() { + let mut accounts = AccountOverrides::default(); + let data = AccountSharedData::default(); + let key = Pubkey::new_unique(); + accounts.set_account(&key, Some(data.clone())); + assert_eq!(accounts.get(&key), Some(&data)); + + accounts.set_account(&key, None); + assert!(accounts.get(&key).is_none()); + } + + #[test] + fn test_slot_history() { + let mut accounts = AccountOverrides::default(); + let data = AccountSharedData::default(); + + assert_eq!(accounts.get(&sysvar::slot_history::id()), None); + accounts.set_slot_history(Some(data.clone())); + + assert_eq!(accounts.get(&sysvar::slot_history::id()), Some(&data)); + } +} diff --git a/svm/src/account_rent_state.rs b/svm/src/account_rent_state.rs index 38cda820f..6fae6e903 100644 --- a/svm/src/account_rent_state.rs +++ b/svm/src/account_rent_state.rs @@ -237,4 +237,71 @@ mod tests { }), ); } + + #[test] + fn test_check_rent_state_with_account() { + let pre_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 3, + }; + + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + let account_index = 2 as IndexOfAccount; + let key = Pubkey::new_unique(); + let result = RentState::check_rent_state_with_account( + &pre_rent_state, + &post_rent_state, + &key, + &AccountSharedData::default(), + account_index, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { + account_index: account_index as u8 + }) + ); + + let result = RentState::check_rent_state_with_account( + &pre_rent_state, + &post_rent_state, + &solana_sdk::incinerator::id(), + &AccountSharedData::default(), + account_index, + ); + assert!(result.is_ok()); + } + + #[test] + fn test_check_rent_state() { + let context = TransactionContext::new( + vec![(Pubkey::new_unique(), AccountSharedData::default())], + Rent::default(), + 20, + 20, + ); + + let pre_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 3, + }; + + let post_rent_state = RentState::RentPaying { + data_size: 2, + lamports: 5, + }; + + let result = + RentState::check_rent_state(Some(&pre_rent_state), Some(&post_rent_state), &context, 0); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + + let result = RentState::check_rent_state(None, Some(&post_rent_state), &context, 0); + assert!(result.is_ok()); + } } diff --git a/svm/src/transaction_account_state_info.rs b/svm/src/transaction_account_state_info.rs index 02d6f0228..ff5b93f6a 100644 --- a/svm/src/transaction_account_state_info.rs +++ b/svm/src/transaction_account_state_info.rs @@ -10,6 +10,7 @@ use { }, }; +#[derive(PartialEq, Debug)] pub struct TransactionAccountStateInfo { rent_state: Option, // None: readonly account } @@ -67,3 +68,171 @@ impl TransactionAccountStateInfo { Ok(()) } } + +#[cfg(test)] +mod test { + use { + crate::{ + account_rent_state::RentState, + transaction_account_state_info::TransactionAccountStateInfo, + }, + solana_sdk::{ + account::AccountSharedData, + hash::Hash, + instruction::CompiledInstruction, + message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, + rent::Rent, + signature::{Keypair, Signer}, + transaction::TransactionError, + transaction_context::TransactionContext, + }, + }; + + #[test] + fn test_new() { + let rent = Rent::default(); + 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 transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20); + let result = TransactionAccountStateInfo::new(&rent, &context, &sanitized_message); + assert_eq!( + result, + vec![ + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized) + }, + TransactionAccountStateInfo { rent_state: None }, + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized) + } + ] + ); + } + + #[test] + #[should_panic(expected = "message and transaction context out of sync, fatal")] + fn test_new_panic() { + let rent = Rent::default(); + 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(), key3.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 transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20); + let _result = TransactionAccountStateInfo::new(&rent, &context, &sanitized_message); + } + + #[test] + fn test_verify_changes() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let pre_rent_state = vec![ + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }, + TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }, + ]; + let post_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20); + + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert!(result.is_ok()); + + let pre_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::Uninitialized), + }]; + let post_rent_state = vec![TransactionAccountStateInfo { + rent_state: Some(RentState::RentPaying { + data_size: 2, + lamports: 5, + }), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20); + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + } +} diff --git a/svm/tests/account_loader.rs b/svm/tests/account_loader.rs new file mode 100644 index 000000000..dd4cd0460 --- /dev/null +++ b/svm/tests/account_loader.rs @@ -0,0 +1,214 @@ +use { + crate::mock_bank::MockBankCallback, + solana_program_runtime::loaded_programs::LoadedProgramsForTxBatch, + solana_sdk::{ + account::{AccountSharedData, WritableAccount}, + fee::FeeStructure, + hash::Hash, + instruction::CompiledInstruction, + message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, + native_loader, + nonce_info::{NonceFull, NoncePartial}, + pubkey::Pubkey, + rent_collector::RENT_EXEMPT_RENT_EPOCH, + rent_debits::RentDebits, + signature::{Keypair, Signature, Signer}, + transaction::{SanitizedTransaction, TransactionError}, + }, + solana_svm::{ + account_loader::{load_accounts, LoadedTransaction, TransactionCheckResult}, + transaction_error_metrics::TransactionErrorMetrics, + }, + std::collections::HashMap, +}; + +mod mock_bank; + +#[test] +fn test_load_accounts_success() { + 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 = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + account_data.set_owner(key3.pubkey()); + mock_bank + .account_shared_data + .insert(key1.pubkey(), account_data); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank + .account_shared_data + .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 + .account_shared_data + .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 lock_results = + (Ok(()), Some(NoncePartial::default()), Some(20u64)) as TransactionCheckResult; + + let results = load_accounts( + &mock_bank, + &[sanitized_transaction], + &[lock_results], + &mut error_counter, + &FeeStructure::default(), + None, + &HashMap::new(), + &loaded_programs, + ); + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + + assert_eq!(results.len(), 1); + let (loaded_result, nonce) = results[0].clone(); + assert_eq!( + loaded_result.unwrap(), + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.account_shared_data[&key2.pubkey()].clone() + ), + ( + key1.pubkey(), + mock_bank.account_shared_data[&key1.pubkey()].clone() + ), + (key4.pubkey(), account_data), + ( + key3.pubkey(), + mock_bank.account_shared_data[&key3.pubkey()].clone() + ), + ], + program_indices: vec![vec![3, 1], vec![3, 1]], + rent: 0, + rent_debits: RentDebits::default() + } + ); + + assert_eq!( + nonce.unwrap(), + NonceFull::new( + Pubkey::from([0; 32]), + AccountSharedData::default(), + Some(mock_bank.account_shared_data[&key2.pubkey()].clone()) + ) + ); +} + +#[test] +fn test_load_accounts_error() { + let mock_bank = MockBankCallback::default(); + 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 sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let lock_results = (Ok(()), Some(NoncePartial::default()), None) as TransactionCheckResult; + let fee_structure = FeeStructure::default(); + + let result = load_accounts( + &mock_bank, + &[sanitized_transaction.clone()], + &[lock_results], + &mut TransactionErrorMetrics::default(), + &fee_structure, + None, + &HashMap::new(), + &LoadedProgramsForTxBatch::default(), + ); + + assert_eq!( + result, + vec![(Err(TransactionError::BlockhashNotFound), None)] + ); + + let lock_results = + (Ok(()), Some(NoncePartial::default()), Some(20u64)) as TransactionCheckResult; + + let result = load_accounts( + &mock_bank, + &[sanitized_transaction.clone()], + &[lock_results.clone()], + &mut TransactionErrorMetrics::default(), + &fee_structure, + None, + &HashMap::new(), + &LoadedProgramsForTxBatch::default(), + ); + + assert_eq!(result, vec![(Err(TransactionError::AccountNotFound), None)]); + + let lock_results = ( + Err(TransactionError::InvalidWritableAccount), + Some(NoncePartial::default()), + Some(20u64), + ) as TransactionCheckResult; + + let result = load_accounts( + &mock_bank, + &[sanitized_transaction.clone()], + &[lock_results], + &mut TransactionErrorMetrics::default(), + &fee_structure, + None, + &HashMap::new(), + &LoadedProgramsForTxBatch::default(), + ); + + assert_eq!( + result, + vec![(Err(TransactionError::InvalidWritableAccount), None)] + ); +}