diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 339a612..1303355 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,25 @@ on: branches: [main] jobs: + format_and_lint_interface: + name: Format & Lint Interface + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + clippy: true + rustfmt: true + + - name: Format + run: pnpm interface:format + + - name: Lint + run: pnpm interface:lint + format_and_lint_programs: name: Format & Lint Programs runs-on: ubuntu-latest @@ -46,26 +65,6 @@ jobs: - name: Run cargo-audit run: pnpm rust:audit - semver_rust: - name: Check semver Rust - runs-on: ubuntu-latest - steps: - - name: Git Checkout - uses: actions/checkout@v4 - - - name: Setup Environment - uses: ./.github/actions/setup - with: - cargo-cache-key: cargo-semver - - - name: Install cargo-audit - uses: taiki-e/install-action@v2 - with: - tool: cargo-semver-checks - - - name: Run semver checks - run: pnpm rust:semver - spellcheck_rust: name: Spellcheck Rust runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index f3063e5..4ede05d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6870,7 +6870,6 @@ dependencies = [ name = "spl-instruction-padding" version = "0.3.0" dependencies = [ - "num_enum", "solana-account-info", "solana-cpi", "solana-instruction", @@ -6881,6 +6880,19 @@ dependencies = [ "solana-pubkey", "solana-sdk", "solana-system-interface", + "spl-instruction-padding-interface", + "static_assertions", +] + +[[package]] +name = "spl-instruction-padding-interface" +version = "0.1.0" +dependencies = [ + "num_enum", + "solana-instruction", + "solana-program", + "solana-program-error", + "solana-pubkey", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 306730e..85e9a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["program"] +members = ["interface", "program"] [workspace.metadata.cli] solana = "2.3.4" diff --git a/interface/Cargo.toml b/interface/Cargo.toml new file mode 100644 index 0000000..3e23b65 --- /dev/null +++ b/interface/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "spl-instruction-padding-interface" +version = "0.1.0" +description = "Solana Program Library Instruction Padding Interface" +authors = ["Anza Maintainers "] +repository = "https://github.com/solana-program/instruction-padding" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2021" + +[dependencies] +num_enum = "0.7.4" +solana-instruction = { version = "2.2.1", features = ["std"] } +solana-program-error = "2.2.2" +solana-pubkey = "2.2.1" + +[dev-dependencies] +solana-program = "2.3.0" +static_assertions = "1.1.0" + +[lib] +crate-type = ["lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 0000000..fb00f71 --- /dev/null +++ b/interface/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/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 0000000..d26e8b3 --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,3 @@ +pub mod instruction; + +solana_pubkey::declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF"); diff --git a/package.json b/package.json index bea91fd..76824f5 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { "private": true, "scripts": { - "programs:build": "zx ./scripts/program/build.mjs", - "programs:test": "zx ./scripts/program/test.mjs", - "programs:format": "zx ./scripts/program/format.mjs", - "programs:lint": "zx ./scripts/program/lint.mjs", + "interface:format": "zx ./scripts/rust/format.mjs interface", + "interface:lint": "zx ./scripts/rust/lint.mjs interface", + "programs:build": "zx ./scripts/rust/build-sbf.mjs program", + "programs:test": "zx ./scripts/rust/test-sbf.mjs program", + "programs:format": "zx ./scripts/rust/format.mjs program", + "programs:lint": "zx ./scripts/rust/lint.mjs program", "solana:check": "zx ./scripts/check-solana-version.mjs", "solana:link": "zx ./scripts/link-solana-version.mjs", "template:upgrade": "zx ./scripts/upgrade-template.mjs", "rust:spellcheck": "cargo spellcheck --code 1", - "rust:audit": "zx ./scripts/audit-rust.mjs", + "rust:audit": "zx ./scripts/rust/audit.mjs", "rust:semver": "cargo semver-checks" }, "devDependencies": { diff --git a/program/Cargo.toml b/program/Cargo.toml index 4a22654..29c320f 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -13,13 +13,13 @@ no-entrypoint = [] test-sbf = [] [dependencies] -num_enum = "0.7.4" solana-account-info = "2.3.0" solana-cpi = "2.2.1" solana-instruction = { version = "2.2.1", features = ["std"] } solana-program-entrypoint = "2.3.0" solana-program-error = "2.2.2" solana-pubkey = "2.2.1" +spl-instruction-padding-interface = { path = "../interface" } [dev-dependencies] solana-program = "2.3.0" diff --git a/program/src/instruction.rs b/program/src/instruction.rs index fb00f71..2d0553b 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -1,169 +1,3 @@ //! 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, - }) -} +pub use spl_instruction_padding_interface::instruction::*; diff --git a/program/src/lib.rs b/program/src/lib.rs index fc69c9d..01f00fa 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -2,8 +2,8 @@ mod entrypoint; pub mod instruction; pub mod processor; +pub use spl_instruction_padding_interface::{check_id, id, ID}; 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 index 37439bd..ef84af5 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1,10 +1,10 @@ use { - crate::instruction::{PadInstruction, WrapData}, solana_account_info::AccountInfo, solana_cpi::invoke, solana_instruction::{AccountMeta, Instruction}, solana_program_error::{ProgramError, ProgramResult}, solana_pubkey::Pubkey, + spl_instruction_padding_interface::instruction::{PadInstruction, WrapData}, std::convert::TryInto, }; diff --git a/scripts/program/build.mjs b/scripts/program/build.mjs deleted file mode 100644 index 7a07966..0000000 --- a/scripts/program/build.mjs +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - workingDirectory, -} from '../utils.mjs'; - -// Build the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - await $`cargo-build-sbf --manifest-path ${manifestPath} ${cliArguments()}`; -} diff --git a/scripts/program/format.mjs b/scripts/program/format.mjs deleted file mode 100644 index c73ddd4..0000000 --- a/scripts/program/format.mjs +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - getToolchainArgument, - partitionArguments, - popArgument, - workingDirectory, -} from '../utils.mjs'; - -// Configure additional arguments here, e.g.: -// ['--arg1', '--arg2', ...cliArguments()] -const formatArgs = cliArguments(); - -const fix = popArgument(formatArgs, '--fix'); -const [cargoArgs, fmtArgs] = partitionArguments(formatArgs, '--'); -const toolchain = getToolchainArgument('format'); - -// Format the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - - if (fix) { - await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- ${fmtArgs}`; - } else { - await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- --check ${fmtArgs}`; - } -} diff --git a/scripts/program/test.mjs b/scripts/program/test.mjs deleted file mode 100644 index fe85dc6..0000000 --- a/scripts/program/test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - workingDirectory, -} from '../utils.mjs'; - -const hasSolfmt = await which('solfmt', { nothrow: true }); - -// Test the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - - if (hasSolfmt) { - await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${cliArguments()} 2>&1 | solfmt`; - } else { - await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${cliArguments()}`; - } -} diff --git a/scripts/audit-rust.mjs b/scripts/rust/audit.mjs similarity index 100% rename from scripts/audit-rust.mjs rename to scripts/rust/audit.mjs diff --git a/scripts/rust/build-sbf.mjs b/scripts/rust/build-sbf.mjs new file mode 100644 index 0000000..64a80d3 --- /dev/null +++ b/scripts/rust/build-sbf.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...args] = cliArguments(); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); +await $`cargo-build-sbf --manifest-path ${manifestPath} ${args}`; diff --git a/scripts/rust/format.mjs b/scripts/rust/format.mjs new file mode 100644 index 0000000..68dc98c --- /dev/null +++ b/scripts/rust/format.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + getToolchainArgument, + partitionArguments, + popArgument, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...formatArgs] = cliArguments(); + +const fix = popArgument(formatArgs, '--fix'); +const [cargoArgs, fmtArgs] = partitionArguments(formatArgs, '--'); +const toolchain = getToolchainArgument('format'); + +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + +if (fix) { + await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- ${fmtArgs}`; +} else { + await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- --check ${fmtArgs}`; +} diff --git a/scripts/program/lint.mjs b/scripts/rust/lint.mjs similarity index 52% rename from scripts/program/lint.mjs rename to scripts/rust/lint.mjs index e431431..66777eb 100644 --- a/scripts/program/lint.mjs +++ b/scripts/rust/lint.mjs @@ -2,12 +2,13 @@ import 'zx/globals'; import { cliArguments, - getProgramFolders, getToolchainArgument, popArgument, workingDirectory, } from '../utils.mjs'; +const [folder, ...args] = cliArguments(); + // Configure arguments here. const lintArgs = [ '-Zunstable-options', @@ -16,19 +17,16 @@ const lintArgs = [ '--', '--deny=warnings', '--deny=clippy::arithmetic_side_effects', - ...cliArguments(), + ...args, ]; const fix = popArgument(lintArgs, '--fix'); const toolchain = getToolchainArgument('lint'); -// Lint the programs using clippy. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - if (fix) { - await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} --fix ${lintArgs}`; - } else { - await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} ${lintArgs}`; - } +if (fix) { + await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} --fix ${lintArgs}`; +} else { + await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} ${lintArgs}`; } diff --git a/scripts/rust/test-sbf.mjs b/scripts/rust/test-sbf.mjs new file mode 100644 index 0000000..971448c --- /dev/null +++ b/scripts/rust/test-sbf.mjs @@ -0,0 +1,11 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...args] = cliArguments(); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + +await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${args}`;