Skip to content

Commit

Permalink
Implement concurrent test for the SVM (anza-xyz#2610)
Browse files Browse the repository at this point in the history
* Implement concurrent test for the SVM

* Remove debug print and optimize lock
  • Loading branch information
LucasSte authored and ray-kast committed Nov 27, 2024
1 parent a94e71a commit 88443e4
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 180 deletions.
192 changes: 189 additions & 3 deletions svm/tests/concurrent_tests.rs
Original file line number Diff line number Diff line change
@@ -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::<MockForkGraph>::new(5, 5, HashSet::new());
Expand Down Expand Up @@ -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::<MockForkGraph>::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,
);
}
12 changes: 12 additions & 0 deletions svm/tests/example-programs/transfer-from-account/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
28 changes: 28 additions & 0 deletions svm/tests/example-programs/transfer-from-account/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Binary file not shown.
Loading

0 comments on commit 88443e4

Please sign in to comment.