From f3fcbdba2908d5ef3bf71a875fd18c802bc2473c Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Thu, 22 Sep 2022 21:37:40 +0200 Subject: [PATCH] bench-tps: Add instruction padding program support (#27813) * bench-tps: Add instruction padding program support * Add ability to customize program id * Improve names and comments --- bench-tps/src/bench.rs | 92 +++++++++++++++--- bench-tps/src/cli.rs | 31 ++++++ .../src/inline_instruction_padding_program.rs | 78 +++++++++++++++ bench-tps/src/lib.rs | 1 + bench-tps/src/main.rs | 10 ++ bench-tps/tests/bench_tps.rs | 65 ++++++++++++- .../tests/fixtures/spl_instruction_padding.so | Bin 0 -> 47184 bytes 7 files changed, 261 insertions(+), 16 deletions(-) create mode 100644 bench-tps/src/inline_instruction_padding_program.rs create mode 100755 bench-tps/tests/fixtures/spl_instruction_padding.so diff --git a/bench-tps/src/bench.rs b/bench-tps/src/bench.rs index c6b333d192..0ee82b8377 100644 --- a/bench-tps/src/bench.rs +++ b/bench-tps/src/bench.rs @@ -2,6 +2,7 @@ use { crate::{ bench_tps_client::*, cli::Config, + inline_instruction_padding_program::{create_padded_instruction, InstructionPaddingConfig}, perf_utils::{sample_txs, SampleStats}, send_batch::*, }, @@ -20,7 +21,7 @@ use { native_token::Sol, pubkey::Pubkey, signature::{Keypair, Signer}, - system_instruction, system_transaction, + system_instruction, timing::{duration_as_ms, duration_as_s, duration_as_us, timestamp}, transaction::Transaction, }, @@ -94,6 +95,7 @@ struct TransactionChunkGenerator<'a, 'b, T: ?Sized> { chunk_index: usize, reclaim_lamports_back_to_source_account: bool, use_randomized_compute_unit_price: bool, + instruction_padding_config: Option, } impl<'a, 'b, T> TransactionChunkGenerator<'a, 'b, T> @@ -106,6 +108,7 @@ where nonce_keypairs: Option<&'b Vec>, chunk_size: usize, use_randomized_compute_unit_price: bool, + instruction_padding_config: Option, ) -> Self { let account_chunks = KeypairChunks::new(gen_keypairs, chunk_size); let nonce_chunks = @@ -118,6 +121,7 @@ where chunk_index: 0, reclaim_lamports_back_to_source_account: false, use_randomized_compute_unit_price, + instruction_padding_config, } } @@ -143,6 +147,7 @@ where source_nonce_chunk, dest_nonce_chunk, self.reclaim_lamports_back_to_source_account, + &self.instruction_padding_config, ) } else { assert!(blockhash.is_some()); @@ -151,6 +156,7 @@ where dest_chunk, self.reclaim_lamports_back_to_source_account, blockhash.unwrap(), + &self.instruction_padding_config, self.use_randomized_compute_unit_price, ) }; @@ -345,6 +351,7 @@ where target_slots_per_epoch, use_randomized_compute_unit_price, use_durable_nonce, + instruction_padding_config, .. } = config; @@ -355,6 +362,7 @@ where nonce_keypairs.as_ref(), tx_count, use_randomized_compute_unit_price, + instruction_padding_config, ); let first_tx_count = loop { @@ -479,6 +487,7 @@ fn generate_system_txs( dest: &VecDeque<&Keypair>, reclaim: bool, blockhash: &Hash, + instruction_padding_config: &Option, use_randomized_compute_unit_price: bool, ) -> Vec { let pairs: Vec<_> = if !reclaim { @@ -500,12 +509,13 @@ fn generate_system_txs( .par_iter() .map(|((from, to), compute_unit_price)| { ( - transfer_with_compute_unit_price( + transfer_with_compute_unit_price_and_padding( from, &to.pubkey(), 1, *blockhash, - **compute_unit_price, + instruction_padding_config, + Some(**compute_unit_price), ), Some(timestamp()), ) @@ -516,7 +526,14 @@ fn generate_system_txs( .par_iter() .map(|(from, to)| { ( - system_transaction::transfer(from, &to.pubkey(), 1, *blockhash), + transfer_with_compute_unit_price_and_padding( + from, + &to.pubkey(), + 1, + *blockhash, + instruction_padding_config, + None, + ), Some(timestamp()), ) }) @@ -524,19 +541,34 @@ fn generate_system_txs( } } -fn transfer_with_compute_unit_price( +fn transfer_with_compute_unit_price_and_padding( from_keypair: &Keypair, to: &Pubkey, lamports: u64, recent_blockhash: Hash, - compute_unit_price: u64, + instruction_padding_config: &Option, + compute_unit_price: Option, ) -> Transaction { let from_pubkey = from_keypair.pubkey(); - let instructions = vec![ - system_instruction::transfer(&from_pubkey, to, lamports), - ComputeBudgetInstruction::set_compute_unit_limit(TRANSFER_TRANSACTION_COMPUTE_UNIT), - ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), - ]; + let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); + let instruction = if let Some(instruction_padding_config) = instruction_padding_config { + create_padded_instruction( + instruction_padding_config.program_id, + transfer_instruction, + vec![], + instruction_padding_config.data_size, + ) + .expect("Could not create padded instruction") + } else { + transfer_instruction + }; + let mut instructions = vec![instruction]; + if let Some(compute_unit_price) = compute_unit_price { + instructions.extend_from_slice(&[ + ComputeBudgetInstruction::set_compute_unit_limit(TRANSFER_TRANSACTION_COMPUTE_UNIT), + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), + ]) + } let message = Message::new(&instructions, Some(&from_pubkey)); Transaction::new(&[from_keypair], message, recent_blockhash) } @@ -601,6 +633,37 @@ fn get_nonce_blockhashes( blockhashes } +fn nonced_transfer_with_padding( + from_keypair: &Keypair, + to: &Pubkey, + lamports: u64, + nonce_account: &Pubkey, + nonce_authority: &Keypair, + nonce_hash: Hash, + instruction_padding_config: &Option, +) -> Transaction { + let from_pubkey = from_keypair.pubkey(); + let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); + let instruction = if let Some(instruction_padding_config) = instruction_padding_config { + create_padded_instruction( + instruction_padding_config.program_id, + transfer_instruction, + vec![], + instruction_padding_config.data_size, + ) + .expect("Could not create padded instruction") + } else { + transfer_instruction + }; + let message = Message::new_with_nonce( + vec![instruction], + Some(&from_pubkey), + nonce_account, + &nonce_authority.pubkey(), + ); + Transaction::new(&[from_keypair, nonce_authority], message, nonce_hash) +} + fn generate_nonced_system_txs( client: Arc, source: &[&Keypair], @@ -608,6 +671,7 @@ fn generate_nonced_system_txs, reclaim: bool, + instruction_padding_config: &Option, ) -> Vec { let length = source.len(); let mut transactions: Vec = Vec::with_capacity(length); @@ -620,13 +684,14 @@ fn generate_nonced_system_txs = get_nonce_blockhashes(&client, &pubkeys); for i in 0..length { transactions.push(( - system_transaction::nonced_transfer( + nonced_transfer_with_padding( source[i], &dest[i].pubkey(), 1, &source_nonce[i].pubkey(), source[i], blockhashes[i], + instruction_padding_config, ), None, )); @@ -637,13 +702,14 @@ fn generate_nonced_system_txs, } impl Default for Config { @@ -85,6 +87,7 @@ impl Default for Config { tpu_connection_pool_size: DEFAULT_TPU_CONNECTION_POOL_SIZE, use_randomized_compute_unit_price: false, use_durable_nonce: false, + instruction_padding_config: None, } } } @@ -318,6 +321,20 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> { .long("use-durable-nonce") .help("Use durable transaction nonce instead of recent blockhash"), ) + .arg( + Arg::with_name("instruction_padding_program_id") + .long("instruction-padding-program-id") + .requires("instruction_padding_data_size") + .takes_value(true) + .value_name("PUBKEY") + .help("If instruction data is padded, optionally specify the padding program id to target"), + ) + .arg( + Arg::with_name("instruction_padding_data_size") + .long("instruction-padding-data-size") + .takes_value(true) + .help("If set, wraps all instructions in the instruction padding program, with the given amount of padding bytes in instruction data."), + ) } /// Parses a clap `ArgMatches` structure into a `Config` @@ -456,5 +473,19 @@ pub fn extract_args(matches: &ArgMatches) -> Config { args.use_durable_nonce = true; } + if let Some(data_size) = matches.value_of("instruction_padding_data_size") { + let program_id = matches + .value_of("instruction_padding_program_id") + .map(|target_str| target_str.parse().unwrap()) + .unwrap_or(inline_instruction_padding_program::ID); + args.instruction_padding_config = Some(InstructionPaddingConfig { + program_id, + data_size: data_size + .to_string() + .parse() + .expect("Can't parse padded instruction data size"), + }); + } + args } diff --git a/bench-tps/src/inline_instruction_padding_program.rs b/bench-tps/src/inline_instruction_padding_program.rs new file mode 100644 index 0000000000..bf7f75929a --- /dev/null +++ b/bench-tps/src/inline_instruction_padding_program.rs @@ -0,0 +1,78 @@ +use { + solana_sdk::{ + declare_id, + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + syscalls::{MAX_CPI_ACCOUNT_INFOS, MAX_CPI_INSTRUCTION_DATA_LEN}, + }, + std::{convert::TryInto, mem::size_of}, +}; + +pub struct InstructionPaddingConfig { + pub program_id: Pubkey, + pub data_size: u32, +} + +declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF"); + +pub fn create_padded_instruction( + program_id: Pubkey, + instruction: Instruction, + padding_accounts: Vec, + padding_data: u32, +) -> Result { + // The format for instruction data goes: + // * 1 byte for the instruction type + // * 4 bytes for the number of accounts required + // * 4 bytes for the size of the data required + // * the actual instruction data + // * additional bytes are all padding + let data_size = size_of::() + + size_of::() + + size_of::() + + instruction.data.len() + + padding_data as usize; + // crude, but can find a potential issue right away + if instruction.data.len() > MAX_CPI_INSTRUCTION_DATA_LEN as usize { + return Err(ProgramError::InvalidInstructionData); + } + let mut data = Vec::with_capacity(data_size); + data.push(1); + let num_accounts: u32 = instruction + .accounts + .len() + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + data.extend(num_accounts.to_le_bytes().into_iter()); + + let data_size: u32 = instruction + .data + .len() + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + data.extend(data_size.to_le_bytes().into_iter()); + data.extend(instruction.data.into_iter()); + for i in 0..padding_data { + data.push((i % u8::MAX as u32) as u8); + } + + // The format for account data goes: + // * accounts required for the CPI + // * program account to call into + // * additional accounts may be included as padding or to test loading / locks + let num_accounts = instruction.accounts.len() + 1 + padding_accounts.len(); + if num_accounts > MAX_CPI_ACCOUNT_INFOS { + return Err(ProgramError::InvalidAccountData); + } + let mut accounts = Vec::with_capacity(num_accounts); + accounts.extend(instruction.accounts.into_iter()); + accounts.push(AccountMeta::new_readonly(instruction.program_id, false)); + accounts.extend(padding_accounts.into_iter()); + + Ok(Instruction { + program_id, + accounts, + data, + }) +} diff --git a/bench-tps/src/lib.rs b/bench-tps/src/lib.rs index 5226b4e56f..ac0f9aba51 100644 --- a/bench-tps/src/lib.rs +++ b/bench-tps/src/lib.rs @@ -2,6 +2,7 @@ pub mod bench; pub mod bench_tps_client; pub mod cli; +pub mod inline_instruction_padding_program; pub mod keypairs; mod perf_utils; pub mod send_batch; diff --git a/bench-tps/src/main.rs b/bench-tps/src/main.rs index a51f49700a..94422c485f 100644 --- a/bench-tps/src/main.rs +++ b/bench-tps/src/main.rs @@ -155,6 +155,7 @@ fn main() { tpu_connection_pool_size, use_randomized_compute_unit_price, use_durable_nonce, + instruction_padding_config, .. } = &cli_config; @@ -223,6 +224,15 @@ fn main() { *num_nodes, *target_node, ); + if let Some(instruction_padding_config) = instruction_padding_config { + info!( + "Checking for existence of instruction padding program: {}", + instruction_padding_config.program_id + ); + client + .get_account(&instruction_padding_config.program_id) + .expect("Instruction padding program must be deployed to this cluster. Deploy the program using `solana program deploy ./bench-tps/tests/fixtures/spl_instruction_padding.so` and pass the resulting program id with `--instruction-padding-program-id`"); + } let keypairs = get_keypairs( client.clone(), id, diff --git a/bench-tps/tests/bench_tps.rs b/bench-tps/tests/bench_tps.rs index b3b5af83f7..a16fbc3645 100644 --- a/bench-tps/tests/bench_tps.rs +++ b/bench-tps/tests/bench_tps.rs @@ -5,6 +5,7 @@ use { solana_bench_tps::{ bench::{do_bench_tps, generate_and_fund_keypairs}, cli::Config, + inline_instruction_padding_program::{self, InstructionPaddingConfig}, send_batch::generate_durable_nonce_accounts, }, solana_core::validator::ValidatorConfig, @@ -16,11 +17,14 @@ use { solana_rpc::rpc::JsonRpcConfig, solana_rpc_client::rpc_client::RpcClient, solana_sdk::{ + account::{Account, AccountSharedData}, commitment_config::CommitmentConfig, + fee_calculator::FeeRateGovernor, + rent::Rent, signature::{Keypair, Signer}, }, solana_streamer::socket::SocketAddrSpace, - solana_test_validator::TestValidator, + solana_test_validator::TestValidatorGenesis, solana_thin_client::thin_client::ThinClient, solana_tpu_client::{ connection_cache::ConnectionCache, @@ -29,8 +33,22 @@ use { std::{sync::Arc, time::Duration}, }; +fn program_account(program_data: &[u8]) -> AccountSharedData { + AccountSharedData::from(Account { + lamports: Rent::default().minimum_balance(program_data.len()).min(1), + data: program_data.to_vec(), + owner: solana_sdk::bpf_loader::id(), + executable: true, + rent_epoch: 0, + }) +} + fn test_bench_tps_local_cluster(config: Config) { let native_instruction_processors = vec![]; + let additional_accounts = vec![( + inline_instruction_padding_program::id(), + program_account(include_bytes!("fixtures/spl_instruction_padding.so")), + )]; solana_logger::setup(); @@ -54,6 +72,7 @@ fn test_bench_tps_local_cluster(config: Config) { NUM_NODES, ), native_instruction_processors, + additional_accounts, ..ClusterConfig::default() }, SocketAddrSpace::Unspecified, @@ -92,8 +111,20 @@ fn test_bench_tps_test_validator(config: Config) { let faucet_addr = run_local_faucet(mint_keypair, None); - let test_validator = - TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified); + let test_validator = TestValidatorGenesis::default() + .fee_rate_governor(FeeRateGovernor::new(0, 0)) + .rent(Rent { + lamports_per_byte_year: 1, + exemption_threshold: 1.0, + ..Rent::default() + }) + .faucet_addr(Some(faucet_addr)) + .add_program( + "spl_instruction_padding", + inline_instruction_padding_program::id(), + ) + .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) + .expect("validator start failed"); let rpc_client = Arc::new(RpcClient::new_with_commitment( test_validator.rpc_url(), @@ -164,3 +195,31 @@ fn test_bench_tps_tpu_client_nonce() { ..Config::default() }); } + +#[test] +#[serial] +fn test_bench_tps_local_cluster_with_padding() { + test_bench_tps_local_cluster(Config { + tx_count: 100, + duration: Duration::from_secs(10), + instruction_padding_config: Some(InstructionPaddingConfig { + program_id: inline_instruction_padding_program::id(), + data_size: 0, + }), + ..Config::default() + }); +} + +#[test] +#[serial] +fn test_bench_tps_tpu_client_with_padding() { + test_bench_tps_test_validator(Config { + tx_count: 100, + duration: Duration::from_secs(10), + instruction_padding_config: Some(InstructionPaddingConfig { + program_id: inline_instruction_padding_program::id(), + data_size: 0, + }), + ..Config::default() + }); +} diff --git a/bench-tps/tests/fixtures/spl_instruction_padding.so b/bench-tps/tests/fixtures/spl_instruction_padding.so new file mode 100755 index 0000000000000000000000000000000000000000..89c9fca5540de01d1d3eb6958899db37497b11bd GIT binary patch literal 47184 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