Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["program"]
members = ["interface", "program"]

[workspace.metadata.cli]
solana = "2.3.4"
Expand Down
25 changes: 25 additions & 0 deletions interface/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "spl-instruction-padding-interface"
version = "0.1.0"
description = "Solana Program Library Instruction Padding Interface"
authors = ["Anza Maintainers <maintainers@anza.xyz>"]
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"]
169 changes: 169 additions & 0 deletions interface/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -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<Self, ProgramError> {
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<AccountMeta>,
padding_data: u32,
) -> Result<Instruction, ProgramError> {
let total_data_size = size_of::<u8>().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<AccountMeta>,
padding_data: u32,
) -> Result<Instruction, ProgramError> {
let total_data_size = size_of::<u8>()
.saturating_add(size_of::<u32>())
.saturating_add(size_of::<u32>())
.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,
})
}
3 changes: 3 additions & 0 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod instruction;

solana_pubkey::declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF");
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading