v0.2.14-pre.005
This commit is contained in:
138
crates/ksp-program-api/tests/release_completeness.rs
Normal file
138
crates/ksp-program-api/tests/release_completeness.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
// file: crates/ksp-program-api/tests/release_completeness.rs
|
||||
// version: 1
|
||||
|
||||
//! Release-level completeness canaries for the `0.2.14` Program API foundation.
|
||||
|
||||
#[test]
|
||||
fn pre_005_exact_crate_root_export_inventory_is_stable() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for line in crate_root.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("pub use ") {
|
||||
actual.push(trimmed);
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
let mut expected = std::vec![
|
||||
"pub use self::program_instruction_decode::ProgramInstructionDecodeOutcome;",
|
||||
"pub use self::program_instruction_decode::ProgramInstructionRecognition;",
|
||||
"pub use self::program_instruction_decoder::ProgramInstructionDecoder;",
|
||||
"pub use ksp_core_lib::Error;",
|
||||
"pub use ksp_core_lib::ErrorCode;",
|
||||
"pub use ksp_core_lib::ErrorContext;",
|
||||
"pub use ksp_core_lib::Pubkey;",
|
||||
"pub use ksp_core_lib::Result;",
|
||||
"pub use ksp_interface_lib::ProgramAccountMeta;",
|
||||
"pub use ksp_interface_lib::ProgramInstruction;",
|
||||
];
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_production_module_inventory_is_instruction_only() -> std::io::Result<()> {
|
||||
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let entries = match std::fs::read_dir(source_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut names = std::vec::Vec::new();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = match entry.file_name().into_string() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
if name.ends_with(".rs") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, std::vec!["lib.rs", "program_instruction_decode.rs", "program_instruction_decoder.rs"]);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_public_enum_and_trait_inventory_remains_open_world() {
|
||||
let sources = [
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/program_instruction_decode.rs"),
|
||||
include_str!("../src/program_instruction_decoder.rs"),
|
||||
];
|
||||
let mut public_enums = std::vec::Vec::new();
|
||||
let mut public_traits = std::vec::Vec::new();
|
||||
for source in sources {
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("pub enum ") {
|
||||
public_enums.push(trimmed);
|
||||
}
|
||||
if trimmed.starts_with("pub trait ") {
|
||||
public_traits.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
public_enums.sort_unstable();
|
||||
public_traits.sort_unstable();
|
||||
assert_eq!(
|
||||
public_enums,
|
||||
std::vec!["pub enum ProgramInstructionDecodeOutcome<Decoded> {", "pub enum ProgramInstructionRecognition {"]
|
||||
);
|
||||
assert_eq!(public_traits, std::vec!["pub trait ProgramInstructionDecoder: Send + Sync {"]);
|
||||
let decode_source = include_str!("../src/program_instruction_decode.rs");
|
||||
assert!(decode_source.contains("#[non_exhaustive]\npub enum ProgramInstructionRecognition"));
|
||||
assert!(decode_source.contains("#[non_exhaustive]\npub enum ProgramInstructionDecodeOutcome<Decoded>"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_production_sources_have_no_registry_preparer_codec_or_runtime_creep() {
|
||||
let sources = [
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/program_instruction_decode.rs"),
|
||||
include_str!("../src/program_instruction_decoder.rs"),
|
||||
];
|
||||
for source in sources {
|
||||
for forbidden in [
|
||||
"pub enum ProgramKind",
|
||||
"pub struct ProgramRegistry",
|
||||
"pub trait ProgramAccountDecoder",
|
||||
"pub trait ProgramEventDecoder",
|
||||
"pub trait ProgramReturnDataDecoder",
|
||||
"pub trait ProgramExecutionPreparer",
|
||||
"std::any::Any",
|
||||
"serde::",
|
||||
"serde_json::",
|
||||
"borsh::",
|
||||
"bincode::",
|
||||
"wincode::",
|
||||
"ksp_logging_lib::",
|
||||
"tracing::",
|
||||
"reqwest::",
|
||||
"tokio::",
|
||||
"tonic::",
|
||||
"tauri::",
|
||||
"std::env::",
|
||||
"std::fs::",
|
||||
"std::net::",
|
||||
"dyn ProgramInstructionDecoder",
|
||||
"dyn crate::ProgramInstructionDecoder",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "forbidden Program API production surface detected: {forbidden}");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
156
crates/ksp-program-api/tests/security_hardening.rs
Normal file
156
crates/ksp-program-api/tests/security_hardening.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
// file: crates/ksp-program-api/tests/security_hardening.rs
|
||||
// version: 1
|
||||
|
||||
//! Adversarial and bound-safety canaries for the Program API foundation.
|
||||
|
||||
const HOSTILE_MARKER: &str = "PROGRAM-SECRET-CANARY";
|
||||
const MALFORMED_OPCODE: u8 = 0xFF_u8;
|
||||
const PROGRAM_ID_BYTES: [u8; 32] = [0xD1_u8; 32];
|
||||
|
||||
struct BoundsObserved {
|
||||
account_count: usize,
|
||||
data_len: usize,
|
||||
}
|
||||
|
||||
struct BoundedDecoder {
|
||||
program_ids: [ksp_program_api::Pubkey; 1],
|
||||
}
|
||||
|
||||
impl BoundedDecoder {
|
||||
fn new() -> Self {
|
||||
return Self { program_ids: [ksp_program_api::Pubkey::new_from_array(PROGRAM_ID_BYTES)] };
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_program_api::ProgramInstructionDecoder for BoundedDecoder {
|
||||
type Decoded = BoundsObserved;
|
||||
|
||||
fn program_ids(&self) -> &[ksp_program_api::Pubkey] {
|
||||
return &self.program_ids;
|
||||
}
|
||||
|
||||
fn recognize(&self, instruction: &ksp_program_api::ProgramInstruction) -> ksp_program_api::ProgramInstructionRecognition {
|
||||
if instruction.program_id() == &self.program_ids[0] {
|
||||
return ksp_program_api::ProgramInstructionRecognition::ExactMatch;
|
||||
}
|
||||
return ksp_program_api::ProgramInstructionRecognition::NoMatch;
|
||||
}
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
instruction: &ksp_program_api::ProgramInstruction,
|
||||
) -> ksp_program_api::Result<ksp_program_api::ProgramInstructionDecodeOutcome<Self::Decoded>> {
|
||||
if instruction.data().first() == std::option::Option::Some(&MALFORMED_OPCODE) {
|
||||
return std::result::Result::Err(ksp_program_api::Error::new(
|
||||
ksp_program_api::ErrorCode::new("program_test", "malformed_instruction"),
|
||||
"malformed external Program instruction",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(ksp_program_api::ProgramInstructionDecodeOutcome::Decoded(BoundsObserved {
|
||||
account_count: instruction.accounts().len(),
|
||||
data_len: instruction.data().len(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
struct BoundlessDecoded {
|
||||
marker: std::rc::Rc<std::cell::Cell<u8>>,
|
||||
}
|
||||
|
||||
struct BoundlessOutputDecoder;
|
||||
|
||||
impl ksp_program_api::ProgramInstructionDecoder for BoundlessOutputDecoder {
|
||||
type Decoded = BoundlessDecoded;
|
||||
|
||||
fn program_ids(&self) -> &[ksp_program_api::Pubkey] {
|
||||
return &[];
|
||||
}
|
||||
|
||||
fn recognize(&self, _instruction: &ksp_program_api::ProgramInstruction) -> ksp_program_api::ProgramInstructionRecognition {
|
||||
return ksp_program_api::ProgramInstructionRecognition::NoMatch;
|
||||
}
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
_instruction: &ksp_program_api::ProgramInstruction,
|
||||
) -> ksp_program_api::Result<ksp_program_api::ProgramInstructionDecodeOutcome<Self::Decoded>> {
|
||||
return std::result::Result::Ok(ksp_program_api::ProgramInstructionDecodeOutcome::Decoded(BoundlessDecoded {
|
||||
marker: std::rc::Rc::new(std::cell::Cell::new(0x5A_u8)),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_max_interface_instruction_crosses_decoder_boundary_without_new_contract() {
|
||||
let decoder = BoundedDecoder::new();
|
||||
let account = ksp_program_api::ProgramAccountMeta::readonly(ksp_program_api::Pubkey::new_from_array([0xD2_u8; 32]), false);
|
||||
let accounts = std::vec![account; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS];
|
||||
let data = std::vec![0x5A_u8; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN];
|
||||
let instruction = ksp_program_api::ProgramInstruction::try_new(decoder.program_ids[0], accounts, data);
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let outcome = ksp_program_api::ProgramInstructionDecoder::decode(&decoder, &instruction);
|
||||
assert!(outcome.is_ok());
|
||||
let outcome = match outcome {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let observed = match outcome {
|
||||
ksp_program_api::ProgramInstructionDecodeOutcome::Decoded(value) => value,
|
||||
_ => return,
|
||||
};
|
||||
assert_eq!(observed.account_count, ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS);
|
||||
assert_eq!(observed.data_len, ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_malformed_payload_error_path_does_not_gain_automatic_payload_echo() {
|
||||
let decoder = BoundedDecoder::new();
|
||||
let mut payload = std::vec![MALFORMED_OPCODE];
|
||||
payload.extend_from_slice(HOSTILE_MARKER.as_bytes());
|
||||
let instruction = ksp_program_api::ProgramInstruction::try_new(decoder.program_ids[0], std::vec![], payload);
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let outcome = ksp_program_api::ProgramInstructionDecoder::decode(&decoder, &instruction);
|
||||
assert!(outcome.is_err());
|
||||
let error = match outcome {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code().domain(), "program_test");
|
||||
assert_eq!(error.code().code(), "malformed_instruction");
|
||||
assert!(!std::format!("{error}").contains(HOSTILE_MARKER));
|
||||
assert!(!std::format!("{error:?}").contains(HOSTILE_MARKER));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_associated_decoded_type_keeps_no_implicit_debug_clone_send_or_sync_bound() {
|
||||
let decoder = BoundlessOutputDecoder;
|
||||
let instruction = ksp_program_api::ProgramInstruction::try_new(ksp_program_api::Pubkey::new_from_array([0xD3_u8; 32]), std::vec![], std::vec![]);
|
||||
assert!(instruction.is_ok());
|
||||
let instruction = match instruction {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let outcome = ksp_program_api::ProgramInstructionDecoder::decode(&decoder, &instruction);
|
||||
assert!(outcome.is_ok());
|
||||
let outcome = match outcome {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(std::format!("{outcome:?}"), "Decoded");
|
||||
let decoded = match outcome {
|
||||
ksp_program_api::ProgramInstructionDecodeOutcome::Decoded(value) => value,
|
||||
_ => return,
|
||||
};
|
||||
assert_eq!(decoded.marker.get(), 0x5A_u8);
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user