diff --git a/svm/tests/concurrent_tests.rs b/svm/tests/concurrent_tests.rs index cfe9f2233afceb..e11019be16a346 100644 --- a/svm/tests/concurrent_tests.rs +++ b/svm/tests/concurrent_tests.rs @@ -1,20 +1,40 @@ #![cfg(feature = "shuttle-test")] use { - crate::mock_bank::{deploy_program, MockForkGraph}, + crate::{ + mock_bank::{ + create_executable_environment, deploy_program, register_builtins, MockForkGraph, + }, + transaction_builder::SanitizedTransactionBuilder, + }, mock_bank::MockBankCallback, shuttle::{ sync::{Arc, RwLock}, thread, Runner, }, solana_program_runtime::loaded_programs::ProgramCacheEntryType, - solana_sdk::pubkey::Pubkey, - solana_svm::transaction_processor::TransactionBatchProcessor, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount, WritableAccount}, + hash::Hash, + instruction::AccountMeta, + pubkey::Pubkey, + signature::Signature, + }, + solana_svm::{ + account_loader::{CheckedTransactionDetails, TransactionCheckResult}, + transaction_processing_result::TransactionProcessingResultExtensions, + transaction_processor::{ + ExecutionRecordingConfig, TransactionBatchProcessor, TransactionProcessingConfig, + TransactionProcessingEnvironment, + }, + }, std::collections::{HashMap, HashSet}, }; mod mock_bank; +mod transaction_builder; + fn program_cache_execution(threads: usize) { let mut mock_bank = MockBankCallback::default(); let batch_processor = TransactionBatchProcessor::::new(5, 5, HashSet::new()); @@ -97,3 +117,169 @@ fn test_program_cache_with_exhaustive_scheduler() { let runner = Runner::new(scheduler, Default::default()); runner.run(move || program_cache_execution(4)); } + +// This test executes multiple transactions in parallel where all read from the same data account, +// but write to different accounts. Given that there are no locks in this case, SVM must behave +// correctly. +fn svm_concurrent() { + let mock_bank = Arc::new(MockBankCallback::default()); + let batch_processor = Arc::new(TransactionBatchProcessor::::new( + 5, + 2, + 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(), + ); + + batch_processor.fill_missing_sysvar_cache_entries(&*mock_bank); + register_builtins(&mock_bank, &batch_processor); + + let mut transaction_builder = SanitizedTransactionBuilder::default(); + let program_id = deploy_program("transfer-from-account".to_string(), 0, &mock_bank); + + const THREADS: usize = 4; + const TRANSACTIONS_PER_THREAD: usize = 3; + const AMOUNT: u64 = 50; + const CAPACITY: usize = THREADS * TRANSACTIONS_PER_THREAD; + const BALANCE: u64 = 500000; + + let mut transactions = vec![Vec::new(); THREADS]; + let mut check_data = vec![Vec::new(); THREADS]; + let read_account = Pubkey::new_unique(); + let mut account_data = AccountSharedData::default(); + account_data.set_data(AMOUNT.to_le_bytes().to_vec()); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(read_account, account_data); + + #[derive(Clone)] + struct CheckTxData { + sender: Pubkey, + recipient: Pubkey, + fee_payer: Pubkey, + } + + for idx in 0..CAPACITY { + let sender = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + let fee_payer = Pubkey::new_unique(); + let system_account = Pubkey::from([0u8; 32]); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(BALANCE); + + { + let shared_data = &mut mock_bank.account_shared_data.write().unwrap(); + shared_data.insert(sender, account_data.clone()); + shared_data.insert(recipient, account_data.clone()); + shared_data.insert(fee_payer, account_data); + } + + transaction_builder.create_instruction( + program_id, + vec![ + AccountMeta { + pubkey: sender, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: recipient, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: read_account, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: system_account, + is_signer: false, + is_writable: false, + }, + ], + HashMap::from([(sender, Signature::new_unique())]), + vec![0], + ); + + let sanitized_transaction = + transaction_builder.build(Hash::default(), (fee_payer, Signature::new_unique()), true); + transactions[idx % THREADS].push(sanitized_transaction.unwrap()); + check_data[idx % THREADS].push(CheckTxData { + fee_payer, + recipient, + sender, + }); + } + + let ths: Vec<_> = (0..THREADS) + .map(|idx| { + let local_batch = batch_processor.clone(); + let local_bank = mock_bank.clone(); + let th_txs = std::mem::take(&mut transactions[idx]); + let check_results = vec![ + Ok(CheckedTransactionDetails { + nonce: None, + lamports_per_signature: 20 + }) as TransactionCheckResult; + TRANSACTIONS_PER_THREAD + ]; + let processing_config = TransactionProcessingConfig { + recording_config: ExecutionRecordingConfig { + enable_log_recording: true, + enable_return_data_recording: false, + enable_cpi_recording: false, + }, + ..Default::default() + }; + let check_tx_data = std::mem::take(&mut check_data[idx]); + + thread::spawn(move || { + let result = local_batch.load_and_execute_sanitized_transactions( + &*local_bank, + &th_txs, + check_results, + &TransactionProcessingEnvironment::default(), + &processing_config, + ); + + for (idx, item) in result.processing_results.iter().enumerate() { + assert!(item.was_processed()); + let inserted_accounts = &check_tx_data[idx]; + for (key, account_data) in &item.as_ref().unwrap().loaded_transaction.accounts { + if *key == inserted_accounts.fee_payer { + assert_eq!(account_data.lamports(), BALANCE - 10000); + } else if *key == inserted_accounts.sender { + assert_eq!(account_data.lamports(), BALANCE - AMOUNT); + } else if *key == inserted_accounts.recipient { + assert_eq!(account_data.lamports(), BALANCE + AMOUNT); + } + } + } + }) + }) + .collect(); + + for th in ths { + th.join().unwrap(); + } +} + +#[test] +fn test_svm_with_probabilistic_scheduler() { + shuttle::check_pct( + move || { + svm_concurrent(); + }, + 300, + 5, + ); +} diff --git a/svm/tests/example-programs/transfer-from-account/Cargo.toml b/svm/tests/example-programs/transfer-from-account/Cargo.toml new file mode 100644 index 00000000000000..3b05d81523837c --- /dev/null +++ b/svm/tests/example-programs/transfer-from-account/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "transfer-from-account" +version = "2.1.0" +edition = "2021" + +[dependencies] +solana-program = { path = "../../../../sdk/program", version = "=2.1.0" } + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] \ No newline at end of file diff --git a/svm/tests/example-programs/transfer-from-account/src/lib.rs b/svm/tests/example-programs/transfer-from-account/src/lib.rs new file mode 100644 index 00000000000000..08494a13ce0395 --- /dev/null +++ b/svm/tests/example-programs/transfer-from-account/src/lib.rs @@ -0,0 +1,28 @@ +use solana_program::{ + account_info::{AccountInfo, next_account_info}, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, + program::invoke, system_instruction, +}; + +entrypoint!(process_instruction); + + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _data: &[u8] +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let payer = next_account_info(accounts_iter)?; + let recipient = next_account_info(accounts_iter)?; + let data_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + let amount = u64::from_le_bytes(data_account.data.borrow()[0..8].try_into().unwrap()); + + invoke( + &system_instruction::transfer(payer.key, recipient.key, amount), + &[payer.clone(), recipient.clone(), system_program.clone()], + )?; + + Ok(()) +} \ No newline at end of file diff --git a/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so b/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so new file mode 100755 index 00000000000000..a3ef926d3747ef Binary files /dev/null and b/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so differ diff --git a/svm/tests/integration_test.rs b/svm/tests/integration_test.rs index 5070bca08e0907..f35f023e54ae2a 100644 --- a/svm/tests/integration_test.rs +++ b/svm/tests/integration_test.rs @@ -2,28 +2,15 @@ use { crate::{ - mock_bank::{deploy_program, MockBankCallback}, - transaction_builder::SanitizedTransactionBuilder, - }, - solana_bpf_loader_program::syscalls::{ - SyscallAbort, SyscallGetClockSysvar, SyscallInvokeSignedRust, SyscallLog, SyscallMemcpy, - SyscallMemset, SyscallSetReturnData, - }, - solana_compute_budget::compute_budget::ComputeBudget, - solana_program_runtime::{ - invoke_context::InvokeContext, - loaded_programs::{ - BlockRelation, ForkGraph, ProgramCache, ProgramCacheEntry, ProgramRuntimeEnvironments, - }, - solana_rbpf::{ - program::{BuiltinFunction, BuiltinProgram, FunctionRegistry}, - vm::Config, + mock_bank::{ + create_executable_environment, deploy_program, register_builtins, MockBankCallback, + MockForkGraph, }, + transaction_builder::SanitizedTransactionBuilder, }, solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, - bpf_loader_upgradeable::{self}, - clock::{Clock, Epoch, Slot, UnixTimestamp}, + clock::Clock, hash::Hash, instruction::AccountMeta, pubkey::Pubkey, @@ -40,165 +27,20 @@ use { TransactionProcessingEnvironment, }, }, - std::{ - cmp::Ordering, - collections::{HashMap, HashSet}, - sync::{Arc, RwLock}, - time::{SystemTime, UNIX_EPOCH}, - }, + solana_type_overrides::sync::{Arc, RwLock}, + std::collections::{HashMap, HashSet}, }; // This module contains the implementation of TransactionProcessingCallback mod mock_bank; mod transaction_builder; -const BPF_LOADER_NAME: &str = "solana_bpf_loader_upgradeable_program"; -const SYSTEM_PROGRAM_NAME: &str = "system_program"; const DEPLOYMENT_SLOT: u64 = 0; const EXECUTION_SLOT: u64 = 5; // The execution slot must be greater than the deployment slot -const DEPLOYMENT_EPOCH: u64 = 0; const EXECUTION_EPOCH: u64 = 2; // The execution epoch must be greater than the deployment epoch -struct MockForkGraph {} - -impl ForkGraph for MockForkGraph { - fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { - match a.cmp(&b) { - Ordering::Less => BlockRelation::Ancestor, - Ordering::Equal => BlockRelation::Equal, - Ordering::Greater => BlockRelation::Descendant, - } - } - - fn slot_epoch(&self, _slot: Slot) -> Option { - Some(0) - } -} - -fn create_custom_environment<'a>() -> BuiltinProgram> { - let compute_budget = ComputeBudget::default(); - let vm_config = Config { - max_call_depth: compute_budget.max_call_depth, - stack_frame_size: compute_budget.stack_frame_size, - enable_address_translation: true, - enable_stack_frame_gaps: true, - instruction_meter_checkpoint_distance: 10000, - enable_instruction_meter: true, - enable_instruction_tracing: true, - enable_symbol_and_section_labels: true, - reject_broken_elfs: true, - noop_instruction_rate: 256, - sanitize_user_provided_values: true, - external_internal_function_hash_collision: false, - reject_callx_r10: false, - enable_sbpf_v1: true, - enable_sbpf_v2: false, - optimize_rodata: false, - aligned_memory_mapping: true, - }; - - // These functions are system calls the compile contract calls during execution, so they - // need to be registered. - let mut function_registry = FunctionRegistry::>::default(); - function_registry - .register_function_hashed(*b"abort", SyscallAbort::vm) - .expect("Registration failed"); - function_registry - .register_function_hashed(*b"sol_log_", SyscallLog::vm) - .expect("Registration failed"); - function_registry - .register_function_hashed(*b"sol_memcpy_", SyscallMemcpy::vm) - .expect("Registration failed"); - function_registry - .register_function_hashed(*b"sol_memset_", SyscallMemset::vm) - .expect("Registration failed"); - - function_registry - .register_function_hashed(*b"sol_invoke_signed_rust", SyscallInvokeSignedRust::vm) - .expect("Registration failed"); - - function_registry - .register_function_hashed(*b"sol_set_return_data", SyscallSetReturnData::vm) - .expect("Registration failed"); - - function_registry - .register_function_hashed(*b"sol_get_clock_sysvar", SyscallGetClockSysvar::vm) - .expect("Registration failed"); - - BuiltinProgram::new_loader(vm_config, function_registry) -} - -fn create_executable_environment( - fork_graph: Arc>, - mock_bank: &mut MockBankCallback, - program_cache: &mut ProgramCache, -) { - program_cache.environments = ProgramRuntimeEnvironments { - program_runtime_v1: Arc::new(create_custom_environment()), - // We are not using program runtime v2 - program_runtime_v2: Arc::new(BuiltinProgram::new_loader( - Config::default(), - FunctionRegistry::default(), - )), - }; - - program_cache.fork_graph = Some(Arc::downgrade(&fork_graph)); - - // We must fill in the sysvar cache entries - let time_now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as i64; - let clock = Clock { - slot: DEPLOYMENT_SLOT, - epoch_start_timestamp: time_now.saturating_sub(10) as UnixTimestamp, - epoch: DEPLOYMENT_EPOCH, - leader_schedule_epoch: DEPLOYMENT_EPOCH, - unix_timestamp: time_now as UnixTimestamp, - }; - - let mut account_data = AccountSharedData::default(); - account_data.set_data(bincode::serialize(&clock).unwrap()); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(Clock::id(), account_data); -} - -fn register_builtins( - mock_bank: &MockBankCallback, - batch_processor: &TransactionBatchProcessor, -) { - // We must register the bpf loader account as a loadable account, otherwise programs - // won't execute. - batch_processor.add_builtin( - mock_bank, - bpf_loader_upgradeable::id(), - BPF_LOADER_NAME, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - BPF_LOADER_NAME.len(), - solana_bpf_loader_program::Entrypoint::vm, - ), - ); - - // In order to perform a transference of native tokens using the system instruction, - // the system program builtin must be registered. - batch_processor.add_builtin( - mock_bank, - solana_system_program::id(), - SYSTEM_PROGRAM_NAME, - ProgramCacheEntry::new_builtin( - DEPLOYMENT_SLOT, - SYSTEM_PROGRAM_NAME.len(), - solana_system_program::system_processor::Entrypoint::vm, - ), - ); -} - fn prepare_transactions( - mock_bank: &mut MockBankCallback, + mock_bank: &MockBankCallback, ) -> (Vec, Vec) { let mut transaction_builder = SanitizedTransactionBuilder::default(); let mut all_transactions = Vec::new(); @@ -392,8 +234,8 @@ fn prepare_transactions( #[test] fn svm_integration() { - let mut mock_bank = MockBankCallback::default(); - let (transactions, check_results) = prepare_transactions(&mut mock_bank); + let mock_bank = MockBankCallback::default(); + let (transactions, check_results) = prepare_transactions(&mock_bank); let batch_processor = TransactionBatchProcessor::::new( EXECUTION_SLOT, EXECUTION_EPOCH, @@ -404,7 +246,7 @@ fn svm_integration() { create_executable_environment( fork_graph.clone(), - &mut mock_bank, + &mock_bank, &mut batch_processor.program_cache.write().unwrap(), ); diff --git a/svm/tests/mock_bank.rs b/svm/tests/mock_bank.rs index 355f9f0ce8898a..169ac63cf8854b 100644 --- a/svm/tests/mock_bank.rs +++ b/svm/tests/mock_bank.rs @@ -1,15 +1,33 @@ use { - solana_program_runtime::loaded_programs::{BlockRelation, ForkGraph}, + solana_bpf_loader_program::syscalls::{ + SyscallAbort, SyscallGetClockSysvar, SyscallInvokeSignedRust, SyscallLog, SyscallMemcpy, + SyscallMemset, SyscallSetReturnData, + }, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::{ + invoke_context::InvokeContext, + loaded_programs::{ + BlockRelation, ForkGraph, ProgramCache, ProgramCacheEntry, ProgramRuntimeEnvironments, + }, + solana_rbpf::{ + program::{BuiltinFunction, BuiltinProgram, FunctionRegistry}, + vm::Config, + }, + }, solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, - clock::Epoch, + clock::{Clock, Epoch, UnixTimestamp}, feature_set::FeatureSet, native_loader, pubkey::Pubkey, slot_hashes::Slot, + sysvar::SysvarId, + }, + solana_svm::{ + transaction_processing_callback::TransactionProcessingCallback, + transaction_processor::TransactionBatchProcessor, }, - solana_svm::transaction_processing_callback::TransactionProcessingCallback, solana_type_overrides::sync::{Arc, RwLock}, std::{ cmp::Ordering, @@ -17,6 +35,7 @@ use { env, fs::{self, File}, io::Read, + time::{SystemTime, UNIX_EPOCH}, }, }; @@ -97,11 +116,7 @@ fn load_program(name: String) -> Vec { } #[allow(unused)] -pub fn deploy_program( - name: String, - deployment_slot: Slot, - mock_bank: &mut MockBankCallback, -) -> Pubkey { +pub fn deploy_program(name: String, deployment_slot: Slot, mock_bank: &MockBankCallback) -> Pubkey { let program_account = Pubkey::new_unique(); let program_data_account = Pubkey::new_unique(); let state = UpgradeableLoaderState::Program { @@ -144,3 +159,134 @@ pub fn deploy_program( program_account } + +#[allow(unused)] +pub fn create_executable_environment( + fork_graph: Arc>, + mock_bank: &MockBankCallback, + program_cache: &mut ProgramCache, +) { + const DEPLOYMENT_EPOCH: u64 = 0; + const DEPLOYMENT_SLOT: u64 = 0; + + program_cache.environments = ProgramRuntimeEnvironments { + program_runtime_v1: Arc::new(create_custom_environment()), + // We are not using program runtime v2 + program_runtime_v2: Arc::new(BuiltinProgram::new_loader( + Config::default(), + FunctionRegistry::default(), + )), + }; + + program_cache.fork_graph = Some(Arc::downgrade(&fork_graph)); + + // We must fill in the sysvar cache entries + let time_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as i64; + let clock = Clock { + slot: DEPLOYMENT_SLOT, + epoch_start_timestamp: time_now.saturating_sub(10) as UnixTimestamp, + epoch: DEPLOYMENT_EPOCH, + leader_schedule_epoch: DEPLOYMENT_EPOCH, + unix_timestamp: time_now as UnixTimestamp, + }; + + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&clock).unwrap()); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(Clock::id(), account_data); +} + +#[allow(unused)] +pub fn register_builtins( + mock_bank: &MockBankCallback, + batch_processor: &TransactionBatchProcessor, +) { + const DEPLOYMENT_SLOT: u64 = 0; + // We must register the bpf loader account as a loadable account, otherwise programs + // won't execute. + let bpf_loader_name = "solana_bpf_loader_upgradeable_program"; + batch_processor.add_builtin( + mock_bank, + bpf_loader_upgradeable::id(), + bpf_loader_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + bpf_loader_name.len(), + solana_bpf_loader_program::Entrypoint::vm, + ), + ); + + // In order to perform a transference of native tokens using the system instruction, + // the system program builtin must be registered. + let system_program_name = "system_program"; + batch_processor.add_builtin( + mock_bank, + solana_system_program::id(), + system_program_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + system_program_name.len(), + solana_system_program::system_processor::Entrypoint::vm, + ), + ); +} + +#[allow(unused)] +fn create_custom_environment<'a>() -> BuiltinProgram> { + let compute_budget = ComputeBudget::default(); + let vm_config = Config { + max_call_depth: compute_budget.max_call_depth, + stack_frame_size: compute_budget.stack_frame_size, + enable_address_translation: true, + enable_stack_frame_gaps: true, + instruction_meter_checkpoint_distance: 10000, + enable_instruction_meter: true, + enable_instruction_tracing: true, + enable_symbol_and_section_labels: true, + reject_broken_elfs: true, + noop_instruction_rate: 256, + sanitize_user_provided_values: true, + external_internal_function_hash_collision: false, + reject_callx_r10: false, + enable_sbpf_v1: true, + enable_sbpf_v2: false, + optimize_rodata: false, + aligned_memory_mapping: true, + }; + + // These functions are system calls the compile contract calls during execution, so they + // need to be registered. + let mut function_registry = FunctionRegistry::>::default(); + function_registry + .register_function_hashed(*b"abort", SyscallAbort::vm) + .expect("Registration failed"); + function_registry + .register_function_hashed(*b"sol_log_", SyscallLog::vm) + .expect("Registration failed"); + function_registry + .register_function_hashed(*b"sol_memcpy_", SyscallMemcpy::vm) + .expect("Registration failed"); + function_registry + .register_function_hashed(*b"sol_memset_", SyscallMemset::vm) + .expect("Registration failed"); + + function_registry + .register_function_hashed(*b"sol_invoke_signed_rust", SyscallInvokeSignedRust::vm) + .expect("Registration failed"); + + function_registry + .register_function_hashed(*b"sol_set_return_data", SyscallSetReturnData::vm) + .expect("Registration failed"); + + function_registry + .register_function_hashed(*b"sol_get_clock_sysvar", SyscallGetClockSysvar::vm) + .expect("Registration failed"); + + BuiltinProgram::new_loader(vm_config, function_registry) +}