diff --git a/program/Cargo.toml b/program/Cargo.toml new file mode 100644 index 0000000..b304121 --- /dev/null +++ b/program/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "spl-instruction-padding" +version = "0.3.0" +description = "Solana Program Library Instruction Padding Program" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +num_enum = "0.7.3" +solana-account-info = "2.1.0" +solana-cpi = "2.1.0" +solana-instruction = { version = "2.1.0", features = ["std"] } +solana-program-entrypoint = "2.1.0" +solana-program-error = "2.1.0" +solana-pubkey = "2.1.0" + +[dev-dependencies] +solana-program = "2.1.0" +solana-program-test = "2.1.0" +solana-sdk = "2.1.0" +static_assertions = "1.1.0" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/program/README.md b/program/README.md new file mode 100644 index 0000000..5fe411f --- /dev/null +++ b/program/README.md @@ -0,0 +1,29 @@ +# Instruction Pad Program + +A program for padding instructions with additional data or accounts, to be used +for testing larger transactions, either more instruction data, or more accounts. + +The main use-case is with solana-bench-tps, where we can see the impact of larger +transactions through TPS numbers. With that data, we can develop a fair fee model +for large transactions. + +It operates with two instructions: no-op and wrap. + +* No-op: simply an instruction with as much data and as many accounts as desired, +of which none will be used for processing. +* Wrap: before the padding data and accounts, accepts a real instruction and +required accounts, and performs a CPI into the program specified by the instruction + +Both of these modes add the general overhead of calling a BPF program, and +the wrap mode adds the CPI overhead. + +Because of the overhead, it's best to use the instruction padding program with +all large transaction tests, and comparing TPS numbers between: + +* using the program with no padding +* using the program with data and account padding + +## Audit + +The repository [README](https://github.com/solana-labs/solana-program-library#audits) +contains information about program audits. diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs new file mode 100644 index 0000000..d5e799e --- /dev/null +++ b/program/src/entrypoint.rs @@ -0,0 +1,16 @@ +//! Program entrypoint + +#![cfg(not(feature = "no-entrypoint"))] + +use { + solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, +}; + +solana_program_entrypoint::entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process(program_id, accounts, instruction_data) +} diff --git a/program/src/instruction.rs b/program/src/instruction.rs new file mode 100644 index 0000000..68c50b8 --- /dev/null +++ b/program/src/instruction.rs @@ -0,0 +1,169 @@ +//! Instruction creators for large instructions + +use { + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + std::{convert::TryInto, mem::size_of}, +}; + +const MAX_CPI_ACCOUNT_INFOS: usize = 128; +const MAX_CPI_INSTRUCTION_DATA_LEN: u64 = 10 * 1024; + +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_CPI_ACCOUNT_INFOS, + solana_program::syscalls::MAX_CPI_ACCOUNT_INFOS +); +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_CPI_INSTRUCTION_DATA_LEN, + solana_program::syscalls::MAX_CPI_INSTRUCTION_DATA_LEN +); + +/// Instructions supported by the padding program, which takes in additional +/// account data or accounts and does nothing with them. It's meant for testing +/// larger transactions with bench-tps. +#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum PadInstruction { + /// Does no work, but accepts a large amount of data and accounts + Noop, + /// Wraps the provided instruction, calling the provided program via CPI + /// + /// Accounts expected by this instruction: + /// + /// * All accounts required for the inner instruction + /// * The program invoked by the inner instruction + /// * Additional padding accounts + /// + /// Data expected by this instruction: + /// * WrapData + Wrap, +} + +/// Data wrapping any inner instruction +pub struct WrapData<'a> { + /// Number of accounts required by the inner instruction + pub num_accounts: u32, + /// the size of the inner instruction data + pub instruction_size: u32, + /// actual inner instruction data + pub instruction_data: &'a [u8], + // additional padding bytes come after, not captured in this struct +} + +const U32_BYTES: usize = 4; +fn unpack_u32(input: &[u8]) -> Result<(u32, &[u8]), ProgramError> { + let value = input + .get(..U32_BYTES) + .and_then(|slice| slice.try_into().ok()) + .map(u32::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + Ok((value, &input[U32_BYTES..])) +} + +impl<'a> WrapData<'a> { + /// Unpacks instruction data + pub fn unpack(data: &'a [u8]) -> Result { + let (num_accounts, rest) = unpack_u32(data)?; + let (instruction_size, rest) = unpack_u32(rest)?; + + let (instruction_data, _rest) = rest.split_at(instruction_size as usize); + Ok(Self { + num_accounts, + instruction_size, + instruction_data, + }) + } +} + +pub fn noop( + program_id: Pubkey, + padding_accounts: Vec, + padding_data: u32, +) -> Result { + let total_data_size = size_of::().saturating_add(padding_data as usize); + // crude, but can find a potential issue right away + if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize { + return Err(ProgramError::InvalidInstructionData); + } + let mut data = Vec::with_capacity(total_data_size); + data.push(PadInstruction::Noop.into()); + for i in 0..padding_data { + data.push(i.checked_rem(u8::MAX as u32).unwrap() as u8); + } + + let num_accounts = padding_accounts.len().saturating_add(1); + if num_accounts > MAX_CPI_ACCOUNT_INFOS { + return Err(ProgramError::InvalidAccountData); + } + let mut accounts = Vec::with_capacity(num_accounts); + accounts.extend(padding_accounts); + + Ok(Instruction { + program_id, + accounts, + data, + }) +} + +pub fn wrap_instruction( + program_id: Pubkey, + instruction: Instruction, + padding_accounts: Vec, + padding_data: u32, +) -> Result { + let total_data_size = size_of::() + .saturating_add(size_of::()) + .saturating_add(size_of::()) + .saturating_add(instruction.data.len()) + .saturating_add(padding_data as usize); + // crude, but can find a potential issue right away + if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize { + return Err(ProgramError::InvalidInstructionData); + } + let mut data = Vec::with_capacity(total_data_size); + data.push(PadInstruction::Wrap.into()); + let num_accounts: u32 = instruction + .accounts + .len() + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + data.extend(num_accounts.to_le_bytes().iter()); + + let data_size: u32 = instruction + .data + .len() + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + data.extend(data_size.to_le_bytes().iter()); + data.extend(instruction.data); + for i in 0..padding_data { + data.push(i.checked_rem(u8::MAX as u32).unwrap() 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() + .saturating_add(1) + .saturating_add(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); + accounts.push(AccountMeta::new_readonly(instruction.program_id, false)); + accounts.extend(padding_accounts); + + Ok(Instruction { + program_id, + accounts, + data, + }) +} diff --git a/program/src/lib.rs b/program/src/lib.rs new file mode 100644 index 0000000..fc69c9d --- /dev/null +++ b/program/src/lib.rs @@ -0,0 +1,9 @@ +mod entrypoint; +pub mod instruction; +pub mod processor; + +pub use { + solana_account_info, solana_cpi, solana_instruction, solana_program_entrypoint, + solana_program_error, solana_pubkey, +}; +solana_pubkey::declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF"); diff --git a/program/src/processor.rs b/program/src/processor.rs new file mode 100644 index 0000000..37439bd --- /dev/null +++ b/program/src/processor.rs @@ -0,0 +1,54 @@ +use { + crate::instruction::{PadInstruction, WrapData}, + solana_account_info::AccountInfo, + solana_cpi::invoke, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, + std::convert::TryInto, +}; + +pub fn process( + _program_id: &Pubkey, + account_infos: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (tag, rest) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + match (*tag) + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)? + { + PadInstruction::Noop => Ok(()), + PadInstruction::Wrap => { + let WrapData { + num_accounts, + instruction_size, + instruction_data, + } = WrapData::unpack(rest)?; + let mut data = Vec::with_capacity(instruction_size as usize); + data.extend_from_slice(instruction_data); + + let program_id = *account_infos[num_accounts as usize].key; + + let accounts = account_infos + .iter() + .take(num_accounts as usize) + .map(|a| AccountMeta { + pubkey: *a.key, + is_signer: a.is_signer, + is_writable: a.is_writable, + }) + .collect::>(); + + let instruction = Instruction { + program_id, + accounts, + data, + }; + + invoke(&instruction, &account_infos[..num_accounts as usize]) + } + } +} diff --git a/program/tests/noop.rs b/program/tests/noop.rs new file mode 100644 index 0000000..beaba60 --- /dev/null +++ b/program/tests/noop.rs @@ -0,0 +1,38 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_program_test::{processor, tokio, ProgramTest}, + solana_sdk::{ + instruction::AccountMeta, pubkey::Pubkey, signature::Signer, transaction::Transaction, + }, + spl_instruction_padding::{instruction::noop, processor::process}, +}; + +#[tokio::test] +async fn success_with_noop() { + let program_id = Pubkey::new_unique(); + let program_test = ProgramTest::new("spl_instruction_padding", program_id, processor!(process)); + + let context = program_test.start_with_context().await; + + let padding_accounts = vec![ + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + + let padding_data = 800; + + let transaction = Transaction::new_signed_with_payer( + &[noop(program_id, padding_accounts, padding_data).unwrap()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} diff --git a/program/tests/system.rs b/program/tests/system.rs new file mode 100644 index 0000000..335af0b --- /dev/null +++ b/program/tests/system.rs @@ -0,0 +1,62 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_program_test::{processor, tokio, ProgramTest}, + solana_sdk::{ + instruction::AccountMeta, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, + signature::Signer, system_instruction, transaction::Transaction, + }, + spl_instruction_padding::{instruction::wrap_instruction, processor::process}, +}; + +#[tokio::test] +async fn success_with_padded_transfer_data() { + let program_id = Pubkey::new_unique(); + let program_test = ProgramTest::new("spl_instruction_padding", program_id, processor!(process)); + + let context = program_test.start_with_context().await; + let to = Pubkey::new_unique(); + + let transfer_amount = LAMPORTS_PER_SOL; + let transfer_instruction = + system_instruction::transfer(&context.payer.pubkey(), &to, transfer_amount); + + let padding_accounts = vec![ + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + + let padding_data = 800; + + let transaction = Transaction::new_signed_with_payer( + &[wrap_instruction( + program_id, + transfer_instruction, + padding_accounts, + padding_data, + ) + .unwrap()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // make sure the transfer went through + assert_eq!( + transfer_amount, + context + .banks_client + .get_account(to) + .await + .unwrap() + .unwrap() + .lamports + ); +}