v0.2.13-pre.004
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-interface-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
mod error;
|
||||
mod program_account_meta;
|
||||
mod program_instruction;
|
||||
|
||||
/// Error code used when an Interface-owned Program instruction admission limit is exceeded.
|
||||
pub use self::error::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED;
|
||||
@@ -21,5 +22,9 @@ pub use self::error::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED;
|
||||
pub use self::program_account_meta::MAX_PROGRAM_INSTRUCTION_ACCOUNTS;
|
||||
/// Passive account metadata attached to one Program instruction.
|
||||
pub use self::program_account_meta::ProgramAccountMeta;
|
||||
/// Maximum opaque data payload admitted by one passive Program instruction contract.
|
||||
pub use self::program_instruction::MAX_PROGRAM_INSTRUCTION_DATA_LEN;
|
||||
/// Passive, bounded Program instruction wire contract.
|
||||
pub use self::program_instruction::ProgramInstruction;
|
||||
/// Canonical Solana account address primitive owned by `ksp-core-lib`.
|
||||
pub use ksp_core_lib::Pubkey;
|
||||
|
||||
81
crates/ksp-interface-lib/src/program_instruction.rs
Normal file
81
crates/ksp-interface-lib/src/program_instruction.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// file: crates/ksp-interface-lib/src/program_instruction.rs
|
||||
// version: 1
|
||||
|
||||
/// Maximum opaque data payload admitted by one passive Program instruction contract.
|
||||
pub const MAX_PROGRAM_INSTRUCTION_DATA_LEN: usize = 10 * 1024;
|
||||
|
||||
/// Passive, bounded Program instruction wire contract.
|
||||
///
|
||||
/// The instruction preserves the caller-provided Program identity, ordered
|
||||
/// account metas and opaque data bytes without applying Program-specific
|
||||
/// semantics. Construction enforces only the Interface-owned admission bounds.
|
||||
pub struct ProgramInstruction {
|
||||
program_id: crate::Pubkey,
|
||||
accounts: std::vec::Vec<crate::ProgramAccountMeta>,
|
||||
data: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
impl ProgramInstruction {
|
||||
/// Creates one passive Program instruction after enforcing Interface admission bounds.
|
||||
///
|
||||
/// The provided vectors are consumed directly. Their account order and duplicates
|
||||
/// are preserved exactly when construction succeeds.
|
||||
pub fn try_new(program_id: crate::Pubkey, accounts: std::vec::Vec<crate::ProgramAccountMeta>, data: std::vec::Vec<u8>) -> ksp_core_lib::Result<Self> {
|
||||
if accounts.len() > crate::MAX_PROGRAM_INSTRUCTION_ACCOUNTS {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED,
|
||||
"Program instruction account count exceeds the Interface admission limit",
|
||||
)
|
||||
.with_context("field", "accounts")
|
||||
.with_context("actual_len", accounts.len().to_string())
|
||||
.with_context("maximum_len", crate::MAX_PROGRAM_INSTRUCTION_ACCOUNTS.to_string()),
|
||||
);
|
||||
}
|
||||
if data.len() > crate::MAX_PROGRAM_INSTRUCTION_DATA_LEN {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED,
|
||||
"Program instruction data length exceeds the Interface admission limit",
|
||||
)
|
||||
.with_context("field", "data")
|
||||
.with_context("actual_len", data.len().to_string())
|
||||
.with_context("maximum_len", crate::MAX_PROGRAM_INSTRUCTION_DATA_LEN.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(Self { program_id, accounts, data });
|
||||
}
|
||||
|
||||
/// Returns the ordered account metas exactly as admitted at construction.
|
||||
#[must_use]
|
||||
pub fn accounts(&self) -> &[crate::ProgramAccountMeta] {
|
||||
return self.accounts.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the opaque Program instruction data bytes.
|
||||
#[must_use]
|
||||
pub fn data(&self) -> &[u8] {
|
||||
return self.data.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the Program identity.
|
||||
#[must_use]
|
||||
pub const fn program_id(&self) -> &crate::Pubkey {
|
||||
return &self.program_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ProgramInstruction {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("ProgramInstruction")
|
||||
.field("program_id", &self.program_id)
|
||||
.field("account_count", &self.accounts.len())
|
||||
.field("data_len", &self.data.len())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/program_instruction.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-interface-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Dependency and passive-surface canaries for the Interface foundation.
|
||||
|
||||
@@ -45,15 +45,20 @@ fn pre_002_manifest_has_exact_core_only_runtime_dependency() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_surface_remains_passive_without_instruction_or_runtime_logging() {
|
||||
fn pre_004_surface_remains_passive_without_codecs_or_runtime_logging() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("ProgramAccountMeta"));
|
||||
assert!(crate_root.contains("MAX_PROGRAM_INSTRUCTION_ACCOUNTS"));
|
||||
assert!(crate_root.contains("ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED"));
|
||||
assert!(!crate_root.contains("ProgramInstruction"));
|
||||
assert!(crate_root.contains("ProgramInstruction"));
|
||||
assert!(crate_root.contains("MAX_PROGRAM_INSTRUCTION_DATA_LEN"));
|
||||
assert!(!crate_root.contains("TRACING_TARGET"));
|
||||
assert!(!crate_root.contains("ksp_logging_lib"));
|
||||
assert!(!std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/constants.rs").exists());
|
||||
let instruction_source = include_str!("../src/program_instruction.rs");
|
||||
for forbidden in ["serde", "borsh", "bincode", "wincode", "solana_instruction", "ksp_logging_lib", "TRACING_TARGET"] {
|
||||
assert!(!instruction_source.contains(forbidden), "forbidden Interface surface detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-interface-lib/tests/public_api.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Integration canaries for the public `ksp-interface-lib` foundation.
|
||||
|
||||
@@ -36,3 +36,20 @@ fn public_pre_003_interface_error_code_is_stable_and_core_owned() {
|
||||
assert_eq!(code.code(), "program_instruction_limit_exceeded");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_004_program_instruction_contract_is_available_from_crate_root() {
|
||||
let program_id = ksp_interface_lib::Pubkey::new_from_array([4_u8; 32]);
|
||||
let account = ksp_interface_lib::ProgramAccountMeta::readonly(ksp_interface_lib::Pubkey::new_from_array([5_u8; 32]), true);
|
||||
let instruction = ksp_interface_lib::ProgramInstruction::try_new(program_id, std::vec![account], std::vec![1_u8, 2, 3]);
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(instruction.program_id(), &program_id);
|
||||
assert_eq!(instruction.accounts(), &[account]);
|
||||
assert_eq!(instruction.data(), &[1_u8, 2, 3]);
|
||||
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN, 10_240);
|
||||
return;
|
||||
}
|
||||
|
||||
107
crates/ksp-interface-lib/unit_tests/program_instruction.rs
Normal file
107
crates/ksp-interface-lib/unit_tests/program_instruction.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
// file: crates/ksp-interface-lib/unit_tests/program_instruction.rs
|
||||
// version: 1
|
||||
|
||||
fn meta(byte: u8, writable: bool) -> crate::ProgramAccountMeta {
|
||||
let pubkey = crate::Pubkey::new_from_array([byte; 32]);
|
||||
if writable {
|
||||
return crate::ProgramAccountMeta::writable(pubkey, false);
|
||||
}
|
||||
return crate::ProgramAccountMeta::readonly(pubkey, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_accepts_empty_accounts_and_data_with_opaque_program_id() {
|
||||
let program_id = crate::Pubkey::new_from_array([0xD3_u8; 32]);
|
||||
let instruction = crate::ProgramInstruction::try_new(program_id, std::vec::Vec::new(), std::vec::Vec::new());
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(instruction.program_id(), &program_id);
|
||||
assert!(instruction.accounts().is_empty());
|
||||
assert!(instruction.data().is_empty());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_preserves_account_order_duplicates_and_opaque_data() {
|
||||
let program_id = crate::Pubkey::new_from_array([0x41_u8; 32]);
|
||||
let first = meta(1, false);
|
||||
let duplicate = meta(2, true);
|
||||
let accounts = std::vec![first, duplicate, first];
|
||||
let data = std::vec![0_u8, 1, 2, 0xFF];
|
||||
let instruction = crate::ProgramInstruction::try_new(program_id, accounts, data.clone());
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(instruction.accounts(), &[first, duplicate, first]);
|
||||
assert_eq!(instruction.data(), data.as_slice());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_accepts_account_limit_and_rejects_one_above_it() {
|
||||
let program_id = crate::Pubkey::new_from_array([0x51_u8; 32]);
|
||||
let account = meta(7, false);
|
||||
let accepted = crate::ProgramInstruction::try_new(program_id, std::vec![account; crate::MAX_PROGRAM_INSTRUCTION_ACCOUNTS], std::vec::Vec::new());
|
||||
assert!(accepted.is_ok());
|
||||
let rejected = crate::ProgramInstruction::try_new(program_id, std::vec![account; crate::MAX_PROGRAM_INSTRUCTION_ACCOUNTS + 1], std::vec::Vec::new());
|
||||
assert!(rejected.is_err());
|
||||
let error = match rejected {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED);
|
||||
assert_eq!(error.message(), "Program instruction account count exceeds the Interface admission limit");
|
||||
assert_eq!(error.context().len(), 3);
|
||||
assert_eq!(error.context()[0].key(), "field");
|
||||
assert_eq!(error.context()[0].value(), "accounts");
|
||||
assert_eq!(error.context()[1].key(), "actual_len");
|
||||
assert_eq!(error.context()[1].value(), "256");
|
||||
assert_eq!(error.context()[2].key(), "maximum_len");
|
||||
assert_eq!(error.context()[2].value(), "255");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_accepts_data_limit_and_rejects_one_byte_above_it() {
|
||||
let program_id = crate::Pubkey::new_from_array([0x61_u8; 32]);
|
||||
let accepted = crate::ProgramInstruction::try_new(program_id, std::vec::Vec::new(), std::vec![0xA5_u8; crate::MAX_PROGRAM_INSTRUCTION_DATA_LEN]);
|
||||
assert!(accepted.is_ok());
|
||||
let rejected = crate::ProgramInstruction::try_new(program_id, std::vec::Vec::new(), std::vec![0x5A_u8; crate::MAX_PROGRAM_INSTRUCTION_DATA_LEN + 1]);
|
||||
assert!(rejected.is_err());
|
||||
let error = match rejected {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED);
|
||||
assert_eq!(error.message(), "Program instruction data length exceeds the Interface admission limit");
|
||||
assert_eq!(error.context().len(), 3);
|
||||
assert_eq!(error.context()[0].value(), "data");
|
||||
assert_eq!(error.context()[1].value(), "10241");
|
||||
assert_eq!(error.context()[2].value(), "10240");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_debug_is_bounded_and_omits_accounts_and_payload_bytes() {
|
||||
let program_id = crate::Pubkey::new_from_array([0x71_u8; 32]);
|
||||
let payload = b"PAYLOAD_SENTINEL_NEVER_RENDER".to_vec();
|
||||
let instruction = crate::ProgramInstruction::try_new(program_id, std::vec![meta(8, false), meta(9, true)], payload);
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let rendered = std::format!("{instruction:?}");
|
||||
assert!(rendered.contains("ProgramInstruction"));
|
||||
assert!(rendered.contains("account_count: 2"));
|
||||
assert!(rendered.contains("data_len: 29"));
|
||||
assert!(!rendered.contains("PAYLOAD_SENTINEL_NEVER_RENDER"));
|
||||
assert!(!rendered.contains("is_signer"));
|
||||
assert!(!rendered.contains("is_writable"));
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user