Skip to content

Commit 86a56dc

Browse files
authored
interface: Introduce lightweight crate for instruction + id (#67)
* interface: Introduce lightweight crate for instruction + id #### Problem In order to publish the v3 SDK crates and have them usable in Agave, we also need to have SPL crates using the v3 SDK crates. However, we have a circular dependency between Agave and SPL which currently makes this impossible. The overall plan is to have Agave only use "interface" crates from SPL, which have no dependencies on Agave crates. You can see more info about the project at https://github.com/orgs/anza-xyz/projects/27 Instruction-padding is pretty simple, since it just exposes an instruction creator and id to Agave. #### Summary of changes Create the new interface crate with the instruction and id. The Rust scripts needed to be adapted to having more than one crate, so I copied all the scripts from token-2022 and updated the commands in package.json accordingly. * Remove semver check, will be added to publish script later * Remove unneeded ignore
1 parent 5ba5978 commit 86a56dc

File tree

19 files changed

+293
-269
lines changed

19 files changed

+293
-269
lines changed

.github/workflows/main.yml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ on:
77
branches: [main]
88

99
jobs:
10+
format_and_lint_interface:
11+
name: Format & Lint Interface
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Git Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Environment
18+
uses: ./.github/actions/setup
19+
with:
20+
clippy: true
21+
rustfmt: true
22+
23+
- name: Format
24+
run: pnpm interface:format
25+
26+
- name: Lint
27+
run: pnpm interface:lint
28+
1029
format_and_lint_programs:
1130
name: Format & Lint Programs
1231
runs-on: ubuntu-latest
@@ -46,26 +65,6 @@ jobs:
4665
- name: Run cargo-audit
4766
run: pnpm rust:audit
4867

49-
semver_rust:
50-
name: Check semver Rust
51-
runs-on: ubuntu-latest
52-
steps:
53-
- name: Git Checkout
54-
uses: actions/checkout@v4
55-
56-
- name: Setup Environment
57-
uses: ./.github/actions/setup
58-
with:
59-
cargo-cache-key: cargo-semver
60-
61-
- name: Install cargo-audit
62-
uses: taiki-e/install-action@v2
63-
with:
64-
tool: cargo-semver-checks
65-
66-
- name: Run semver checks
67-
run: pnpm rust:semver
68-
6968
spellcheck_rust:
7069
name: Spellcheck Rust
7170
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["program"]
3+
members = ["interface", "program"]
44

55
[workspace.metadata.cli]
66
solana = "2.3.4"

interface/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "spl-instruction-padding-interface"
3+
version = "0.1.0"
4+
description = "Solana Program Library Instruction Padding Interface"
5+
authors = ["Anza Maintainers <[email protected]>"]
6+
repository = "https://github.com/solana-program/instruction-padding"
7+
license = "Apache-2.0"
8+
homepage = "https://solana.com/"
9+
edition = "2021"
10+
11+
[dependencies]
12+
num_enum = "0.7.4"
13+
solana-instruction = { version = "2.2.1", features = ["std"] }
14+
solana-program-error = "2.2.2"
15+
solana-pubkey = "2.2.1"
16+
17+
[dev-dependencies]
18+
solana-program = "2.3.0"
19+
static_assertions = "1.1.0"
20+
21+
[lib]
22+
crate-type = ["lib"]
23+
24+
[package.metadata.docs.rs]
25+
targets = ["x86_64-unknown-linux-gnu"]

interface/src/instruction.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! Instruction creators for large instructions
2+
3+
use {
4+
num_enum::{IntoPrimitive, TryFromPrimitive},
5+
solana_instruction::{AccountMeta, Instruction},
6+
solana_program_error::ProgramError,
7+
solana_pubkey::Pubkey,
8+
std::{convert::TryInto, mem::size_of},
9+
};
10+
11+
const MAX_CPI_ACCOUNT_INFOS: usize = 128;
12+
const MAX_CPI_INSTRUCTION_DATA_LEN: u64 = 10 * 1024;
13+
14+
#[cfg(test)]
15+
static_assertions::const_assert_eq!(
16+
MAX_CPI_ACCOUNT_INFOS,
17+
solana_program::syscalls::MAX_CPI_ACCOUNT_INFOS
18+
);
19+
#[cfg(test)]
20+
static_assertions::const_assert_eq!(
21+
MAX_CPI_INSTRUCTION_DATA_LEN,
22+
solana_program::syscalls::MAX_CPI_INSTRUCTION_DATA_LEN
23+
);
24+
25+
/// Instructions supported by the padding program, which takes in additional
26+
/// account data or accounts and does nothing with them. It's meant for testing
27+
/// larger transactions with bench-tps.
28+
#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)]
29+
#[repr(u8)]
30+
pub enum PadInstruction {
31+
/// Does no work, but accepts a large amount of data and accounts
32+
Noop,
33+
/// Wraps the provided instruction, calling the provided program via CPI
34+
///
35+
/// Accounts expected by this instruction:
36+
///
37+
/// * All accounts required for the inner instruction
38+
/// * The program invoked by the inner instruction
39+
/// * Additional padding accounts
40+
///
41+
/// Data expected by this instruction:
42+
/// * `WrapData`
43+
Wrap,
44+
}
45+
46+
/// Data wrapping any inner instruction
47+
pub struct WrapData<'a> {
48+
/// Number of accounts required by the inner instruction
49+
pub num_accounts: u32,
50+
/// the size of the inner instruction data
51+
pub instruction_size: u32,
52+
/// actual inner instruction data
53+
pub instruction_data: &'a [u8],
54+
// additional padding bytes come after, not captured in this struct
55+
}
56+
57+
const U32_BYTES: usize = 4;
58+
fn unpack_u32(input: &[u8]) -> Result<(u32, &[u8]), ProgramError> {
59+
let value = input
60+
.get(..U32_BYTES)
61+
.and_then(|slice| slice.try_into().ok())
62+
.map(u32::from_le_bytes)
63+
.ok_or(ProgramError::InvalidInstructionData)?;
64+
Ok((value, &input[U32_BYTES..]))
65+
}
66+
67+
impl<'a> WrapData<'a> {
68+
/// Unpacks instruction data
69+
pub fn unpack(data: &'a [u8]) -> Result<Self, ProgramError> {
70+
let (num_accounts, rest) = unpack_u32(data)?;
71+
let (instruction_size, rest) = unpack_u32(rest)?;
72+
73+
let (instruction_data, _rest) = rest.split_at(instruction_size as usize);
74+
Ok(Self {
75+
num_accounts,
76+
instruction_size,
77+
instruction_data,
78+
})
79+
}
80+
}
81+
82+
pub fn noop(
83+
program_id: Pubkey,
84+
padding_accounts: Vec<AccountMeta>,
85+
padding_data: u32,
86+
) -> Result<Instruction, ProgramError> {
87+
let total_data_size = size_of::<u8>().saturating_add(padding_data as usize);
88+
// crude, but can find a potential issue right away
89+
if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize {
90+
return Err(ProgramError::InvalidInstructionData);
91+
}
92+
let mut data = Vec::with_capacity(total_data_size);
93+
data.push(PadInstruction::Noop.into());
94+
for i in 0..padding_data {
95+
data.push(i.checked_rem(u8::MAX as u32).unwrap() as u8);
96+
}
97+
98+
let num_accounts = padding_accounts.len().saturating_add(1);
99+
if num_accounts > MAX_CPI_ACCOUNT_INFOS {
100+
return Err(ProgramError::InvalidAccountData);
101+
}
102+
let mut accounts = Vec::with_capacity(num_accounts);
103+
accounts.extend(padding_accounts);
104+
105+
Ok(Instruction {
106+
program_id,
107+
accounts,
108+
data,
109+
})
110+
}
111+
112+
pub fn wrap_instruction(
113+
program_id: Pubkey,
114+
instruction: Instruction,
115+
padding_accounts: Vec<AccountMeta>,
116+
padding_data: u32,
117+
) -> Result<Instruction, ProgramError> {
118+
let total_data_size = size_of::<u8>()
119+
.saturating_add(size_of::<u32>())
120+
.saturating_add(size_of::<u32>())
121+
.saturating_add(instruction.data.len())
122+
.saturating_add(padding_data as usize);
123+
// crude, but can find a potential issue right away
124+
if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize {
125+
return Err(ProgramError::InvalidInstructionData);
126+
}
127+
let mut data = Vec::with_capacity(total_data_size);
128+
data.push(PadInstruction::Wrap.into());
129+
let num_accounts: u32 = instruction
130+
.accounts
131+
.len()
132+
.try_into()
133+
.map_err(|_| ProgramError::InvalidInstructionData)?;
134+
data.extend(num_accounts.to_le_bytes().iter());
135+
136+
let data_size: u32 = instruction
137+
.data
138+
.len()
139+
.try_into()
140+
.map_err(|_| ProgramError::InvalidInstructionData)?;
141+
data.extend(data_size.to_le_bytes().iter());
142+
data.extend(instruction.data);
143+
for i in 0..padding_data {
144+
data.push(i.checked_rem(u8::MAX as u32).unwrap() as u8);
145+
}
146+
147+
// The format for account data goes:
148+
// * accounts required for the CPI
149+
// * program account to call into
150+
// * additional accounts may be included as padding or to test loading / locks
151+
let num_accounts = instruction
152+
.accounts
153+
.len()
154+
.saturating_add(1)
155+
.saturating_add(padding_accounts.len());
156+
if num_accounts > MAX_CPI_ACCOUNT_INFOS {
157+
return Err(ProgramError::InvalidAccountData);
158+
}
159+
let mut accounts = Vec::with_capacity(num_accounts);
160+
accounts.extend(instruction.accounts);
161+
accounts.push(AccountMeta::new_readonly(instruction.program_id, false));
162+
accounts.extend(padding_accounts);
163+
164+
Ok(Instruction {
165+
program_id,
166+
accounts,
167+
data,
168+
})
169+
}

interface/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod instruction;
2+
3+
solana_pubkey::declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF");

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
{
22
"private": true,
33
"scripts": {
4-
"programs:build": "zx ./scripts/program/build.mjs",
5-
"programs:test": "zx ./scripts/program/test.mjs",
6-
"programs:format": "zx ./scripts/program/format.mjs",
7-
"programs:lint": "zx ./scripts/program/lint.mjs",
4+
"interface:format": "zx ./scripts/rust/format.mjs interface",
5+
"interface:lint": "zx ./scripts/rust/lint.mjs interface",
6+
"programs:build": "zx ./scripts/rust/build-sbf.mjs program",
7+
"programs:test": "zx ./scripts/rust/test-sbf.mjs program",
8+
"programs:format": "zx ./scripts/rust/format.mjs program",
9+
"programs:lint": "zx ./scripts/rust/lint.mjs program",
810
"solana:check": "zx ./scripts/check-solana-version.mjs",
911
"solana:link": "zx ./scripts/link-solana-version.mjs",
1012
"template:upgrade": "zx ./scripts/upgrade-template.mjs",
1113
"rust:spellcheck": "cargo spellcheck --code 1",
12-
"rust:audit": "zx ./scripts/audit-rust.mjs",
14+
"rust:audit": "zx ./scripts/rust/audit.mjs",
1315
"rust:semver": "cargo semver-checks"
1416
},
1517
"devDependencies": {

program/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ no-entrypoint = []
1313
test-sbf = []
1414

1515
[dependencies]
16-
num_enum = "0.7.4"
1716
solana-account-info = "2.3.0"
1817
solana-cpi = "2.2.1"
1918
solana-instruction = { version = "2.2.1", features = ["std"] }
2019
solana-program-entrypoint = "2.3.0"
2120
solana-program-error = "2.2.2"
2221
solana-pubkey = "2.2.1"
22+
spl-instruction-padding-interface = { path = "../interface" }
2323

2424
[dev-dependencies]
2525
solana-program = "2.3.0"

0 commit comments

Comments
 (0)