v0.3.1-pre.007

This commit is contained in:
2026-08-29 09:29:58 +02:00
parent c43d4d7c38
commit 47b12e14e6
10 changed files with 580 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/capability/raw_retention.rs
// version: 1
// version: 2
/// Read capability for canonical RAW transaction retention metadata.
pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {
@@ -20,6 +20,10 @@ pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {
///
/// This contract applies a transition selected by an external worker/job policy;
/// the Store backend never decides by itself that higher processing is complete.
/// Implementations must compare the persisted state with `transition.expected()`
/// atomically with the update. A concurrent state change must return
/// [`crate::RawRetentionWriteOutcome::ExpectedStateMismatch`] unless the entity
/// is already at the requested target.
pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {
/// Applies one atomic forward lifecycle transition chosen by the caller.
fn transition_raw_transaction_retention<'a>(

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/model/raw_retention.rs
// version: 1
// version: 2
/// Logical availability state of one canonical RAW payload.
///
@@ -131,6 +131,11 @@ pub enum RawRetentionWriteOutcome {
Applied,
/// The entity was already at the requested target state.
AlreadyAtTarget,
/// The current state matched neither the caller's expected state nor the requested target.
///
/// This outcome makes a lost compare-and-transition race observable without
/// treating concurrent policy progress as malformed input.
ExpectedStateMismatch,
}
fn allowed_transition(expected: crate::RawRetentionState, target: crate::RawRetentionState) -> bool {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/public_api.rs
// version: 5
// version: 6
//! Integration canaries for the public `ksp-store-api` surface.
@@ -179,3 +179,10 @@ fn public_pre_006_query_outcome_and_retention_contracts_are_available_from_crate
assert_eq!(ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID.domain(), "store_api");
return;
}
#[test]
fn public_pre_007_retention_race_outcome_is_available_from_crate_root() {
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::Applied, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
return;
}

View File

@@ -0,0 +1,228 @@
// file: crates/ksp-store-api/tests/release_completeness.rs
// version: 1
//! Release-level boundary and completeness canaries for the `0.3.1` Store API RAW surface.
#[test]
fn pre_007_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 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 self::capability::StoreApiFuture;",
"pub use self::capability::raw_account::RawAccountObservationRead;",
"pub use self::capability::raw_account::RawAccountObservationWrite;",
"pub use self::capability::raw_account::RawAccountStateRead;",
"pub use self::capability::raw_account::RawAccountStateWrite;",
"pub use self::capability::raw_retention::RawTransactionRetentionRead;",
"pub use self::capability::raw_retention::RawTransactionRetentionWrite;",
"pub use self::capability::raw_transaction::RawTransactionObservationRead;",
"pub use self::capability::raw_transaction::RawTransactionObservationWrite;",
"pub use self::capability::raw_transaction::RawTransactionRead;",
"pub use self::capability::raw_transaction::RawTransactionWrite;",
"pub use self::error::ERROR_CODE_RAW_CONFLICT;",
"pub use self::error::ERROR_CODE_RAW_MODEL_INVALID;",
"pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID;",
"pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID;",
"pub use self::error::ERROR_CODE_RAW_QUERY_INVALID;",
"pub use self::error::ERROR_CODE_RAW_RETENTION_INVALID;",
"pub use self::model::raw_account::RawAccountObservation;",
"pub use self::model::raw_account::RawAccountState;",
"pub use self::model::raw_account::RawAccountStateReference;",
"pub use self::model::raw_outcome::RawAcquisitionWriteOutcome;",
"pub use self::model::raw_outcome::RawEntityWriteOutcome;",
"pub use self::model::raw_outcome::RawObservationWriteOutcome;",
"pub use self::model::raw_pagination::MAX_RAW_PAGE_CURSOR_BYTES;",
"pub use self::model::raw_pagination::RawAccountStateQuery;",
"pub use self::model::raw_pagination::RawPage;",
"pub use self::model::raw_pagination::RawPageCursor;",
"pub use self::model::raw_pagination::RawPageLimit;",
"pub use self::model::raw_pagination::RawPageRequest;",
"pub use self::model::raw_pagination::RawSlotRange;",
"pub use self::model::raw_pagination::RawSortDirection;",
"pub use self::model::raw_pagination::RawTransactionQuery;",
"pub use self::model::raw_primitives::MAX_RAW_ACCOUNT_DATA_BYTES;",
"pub use self::model::raw_primitives::MAX_RAW_CODE_BYTES;",
"pub use self::model::raw_primitives::MAX_RAW_PAYLOAD_BYTES;",
"pub use self::model::raw_primitives::MAX_RAW_SOURCE_PAYLOAD_BYTES;",
"pub use self::model::raw_primitives::MAX_RAW_UNIX_MILLIS;",
"pub use self::model::raw_primitives::RawAcquisitionOrigin;",
"pub use self::model::raw_primitives::RawAcquisitionProvenance;",
"pub use self::model::raw_primitives::RawContentHash;",
"pub use self::model::raw_primitives::RawFormatId;",
"pub use self::model::raw_primitives::RawNetworkId;",
"pub use self::model::raw_primitives::RawObservationKey;",
"pub use self::model::raw_primitives::RawPayload;",
"pub use self::model::raw_primitives::RawProvenanceCode;",
"pub use self::model::raw_primitives::RawTimestamp;",
"pub use self::model::raw_primitives::RawTransactionSignature;",
"pub use self::model::raw_retention::RawRetentionState;",
"pub use self::model::raw_retention::RawRetentionWriteOutcome;",
"pub use self::model::raw_retention::RawTransactionAcquisitionMode;",
"pub use self::model::raw_retention::RawTransactionRetentionTransition;",
"pub use self::model::raw_retention::RawTransactionTombstone;",
"pub use self::model::raw_transaction::RawTransaction;",
"pub use self::model::raw_transaction::RawTransactionObservation;",
"pub use self::model::raw_transaction::RawTransactionReference;",
];
expected.sort_unstable();
assert_eq!(actual, expected);
assert!(!crate_root.contains("pub mod "));
return;
}
#[test]
fn pre_007_exact_production_module_inventory_is_raw_only() -> std::io::Result<()> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
assert_eq!(rust_file_names(root.as_path())?, std::vec!["capability.rs", "error.rs", "lib.rs", "model.rs"]);
assert_eq!(
rust_file_names(root.join("model").as_path())?,
std::vec!["raw_account.rs", "raw_outcome.rs", "raw_pagination.rs", "raw_primitives.rs", "raw_retention.rs", "raw_transaction.rs"]
);
assert_eq!(rust_file_names(root.join("capability").as_path())?, std::vec!["raw_account.rs", "raw_retention.rs", "raw_transaction.rs"]);
return std::result::Result::Ok(());
}
#[test]
fn pre_007_public_evolutive_enums_remain_non_exhaustive() {
let sources = [
include_str!("../src/model/raw_outcome.rs"),
include_str!("../src/model/raw_pagination.rs"),
include_str!("../src/model/raw_primitives.rs"),
include_str!("../src/model/raw_retention.rs"),
];
for enum_name in [
"RawAcquisitionOrigin",
"RawEntityWriteOutcome",
"RawObservationWriteOutcome",
"RawRetentionState",
"RawRetentionWriteOutcome",
"RawSortDirection",
"RawTransactionAcquisitionMode",
] {
assert_non_exhaustive(&sources, enum_name);
}
return;
}
#[test]
fn pre_007_interface_store_ownership_and_negative_scope_remain_explicit() {
let interface_root = include_str!("../../ksp-interface-lib/src/lib.rs");
let store_sources = [
include_str!("../src/lib.rs"),
include_str!("../src/model.rs"),
include_str!("../src/model/raw_account.rs"),
include_str!("../src/model/raw_outcome.rs"),
include_str!("../src/model/raw_pagination.rs"),
include_str!("../src/model/raw_primitives.rs"),
include_str!("../src/model/raw_retention.rs"),
include_str!("../src/model/raw_transaction.rs"),
include_str!("../src/capability.rs"),
include_str!("../src/capability/raw_account.rs"),
include_str!("../src/capability/raw_retention.rs"),
include_str!("../src/capability/raw_transaction.rs"),
];
for persistent_name in ["RawTransaction", "RawAccountState", "RawRetentionState", "RawTransactionTombstone"] {
assert!(!interface_root.contains(persistent_name), "persistent Store model leaked into Interface crate root: {persistent_name}");
}
for source in store_sources {
for forbidden in [
"RawLogNotification",
"RawSlotEvent",
"RawVoteEvent",
"YellowstoneEntry",
"StructuralInstruction",
"StructuralTransaction",
"DecodedInstruction",
"DomainRecord",
"ProgramInstructionDecoder",
"PostgresStore",
"MySqlStore",
"StoreBackend",
] {
assert!(!source.contains(forbidden), "deferred or backend-specific Store surface detected: {forbidden}");
}
}
return;
}
#[test]
fn pre_007_capability_inventory_stays_fine_grained_without_runtime_facade() {
let sources = [
include_str!("../src/capability/raw_account.rs"),
include_str!("../src/capability/raw_retention.rs"),
include_str!("../src/capability/raw_transaction.rs"),
];
let mut traits = std::vec::Vec::new();
for source in sources {
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("pub trait ") {
traits.push(trimmed);
}
}
}
traits.sort_unstable();
let mut expected = std::vec![
"pub trait RawAccountObservationRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountObservationWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountStateRead: std::marker::Send + std::marker::Sync {",
"pub trait RawAccountStateWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionObservationRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionObservationWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRetentionRead: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionRetentionWrite: std::marker::Send + std::marker::Sync {",
"pub trait RawTransactionWrite: std::marker::Send + std::marker::Sync {",
];
expected.sort_unstable();
assert_eq!(traits, expected);
return;
}
fn assert_non_exhaustive(sources: &[&str], enum_name: &str) {
let needle = "#[non_exhaustive]\n";
let declaration = std::format!("pub enum {enum_name}");
let mut found = false;
for source in sources {
if let std::option::Option::Some(position) = source.find(declaration.as_str()) {
let prefix = &source[..position];
let attribute_position = prefix.rfind(needle);
assert!(attribute_position.is_some(), "public evolutive enum is missing #[non_exhaustive]: {enum_name}");
found = true;
}
}
assert!(found, "public evolutive enum not found: {enum_name}");
return;
}
fn rust_file_names(directory: &std::path::Path) -> std::io::Result<std::vec::Vec<std::string::String>> {
let entries = std::fs::read_dir(directory)?;
let mut names = std::vec::Vec::new();
for entry in entries {
let entry = entry?;
if !entry.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();
return std::result::Result::Ok(names);
}

View File

@@ -0,0 +1,124 @@
// file: crates/ksp-store-api/tests/security_hardening.rs
// version: 1
//! Adversarial and retention-race canaries for the Store API RAW foundation.
const HOSTILE_MARKER: &str = "STORE-RAW-SECRET-CANARY";
#[test]
fn pre_007_hostile_model_query_and_provenance_errors_never_echo_input_material() {
let hostile_code = std::format!("https://example.invalid/{HOSTILE_MARKER}");
let network = ksp_store_api::RawNetworkId::new(hostile_code.clone());
assert!(network.is_err());
let network_error = match network {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(network_error.code(), &ksp_store_api::ERROR_CODE_RAW_MODEL_INVALID);
assert!(!std::format!("{network_error}").contains(HOSTILE_MARKER));
assert!(!std::format!("{network_error:?}").contains(HOSTILE_MARKER));
let provenance = ksp_store_api::RawProvenanceCode::new(hostile_code.clone());
assert!(provenance.is_err());
let provenance_error = match provenance {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(provenance_error.code(), &ksp_store_api::ERROR_CODE_RAW_PROVENANCE_INVALID);
assert!(!std::format!("{provenance_error}").contains(HOSTILE_MARKER));
assert!(!std::format!("{provenance_error:?}").contains(HOSTILE_MARKER));
let format = ksp_store_api::RawFormatId::new(hostile_code);
assert!(format.is_err());
let format_error = match format {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(format_error.code(), &ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID);
assert!(!std::format!("{format_error}").contains(HOSTILE_MARKER));
assert!(!std::format!("{format_error:?}").contains(HOSTILE_MARKER));
let cursor = ksp_store_api::RawPageCursor::try_new(std::vec![0x41_u8; ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES + 1].into_boxed_slice());
assert!(cursor.is_err());
let cursor_error = match cursor {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(cursor_error.code(), &ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID);
assert!(!std::format!("{cursor_error}").contains(HOSTILE_MARKER));
assert!(!std::format!("{cursor_error:?}").contains(HOSTILE_MARKER));
return;
}
#[test]
fn pre_007_raw_debug_surfaces_do_not_render_payload_hash_signature_or_account_bytes() {
let marker_bytes = HOSTILE_MARKER.as_bytes().to_vec().into_boxed_slice();
let format = match ksp_store_api::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let payload = ksp_store_api::RawPayload::try_new(format, 1, marker_bytes, ksp_store_api::RawContentHash::new([0x53_u8; 32]));
assert!(payload.is_ok());
let payload = match payload {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(!std::format!("{payload:?}").contains(HOSTILE_MARKER));
assert_eq!(std::format!("{:?}", payload.content_hash()), "RawContentHash(..)");
let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let signature = ksp_store_api::RawTransactionSignature::new([0x54_u8; 64]);
assert_eq!(std::format!("{signature:?}"), "RawTransactionSignature(..)");
let reference = ksp_store_api::RawTransactionReference::new(network.clone(), signature);
let transaction = ksp_store_api::RawTransaction::new(reference, 7, std::option::Option::None, payload);
assert!(!std::format!("{transaction:?}").contains(HOSTILE_MARKER));
let account_reference = ksp_store_api::RawAccountStateReference::new(
network,
ksp_store_api::Pubkey::new_from_array([0x55_u8; 32]),
8,
ksp_store_api::RawContentHash::new([0x56_u8; 32]),
);
let account = ksp_store_api::RawAccountState::try_new(
account_reference,
1,
ksp_store_api::Pubkey::new_from_array([0x57_u8; 32]),
false,
0,
HOSTILE_MARKER.as_bytes().to_vec().into_boxed_slice(),
);
assert!(account.is_ok());
let account = match account {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(!std::format!("{account:?}").contains(HOSTILE_MARKER));
return;
}
#[test]
fn pre_007_cursor_boundary_and_page_limit_preserve_security_without_executor_policy() {
let cursor = ksp_store_api::RawPageCursor::try_new(std::vec![0x5A_u8; ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES].into_boxed_slice());
assert!(cursor.is_ok());
let cursor = match cursor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(cursor.as_bytes().len(), ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES);
assert!(!std::format!("{cursor:?}").contains("ZZZZ"));
assert!(ksp_store_api::RawPageCursor::try_new(std::vec![0x5A_u8; ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES + 1].into_boxed_slice()).is_err());
assert!(ksp_store_api::RawPageLimit::new(0).is_err());
let unlimited_policy = ksp_store_api::RawPageLimit::new(u64::MAX);
assert!(unlimited_policy.is_ok());
let unlimited_policy = match unlimited_policy {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(unlimited_policy.get(), u64::MAX);
return;
}
#[test]
fn pre_007_retention_outcome_distinguishes_lost_compare_and_transition_race() {
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::Applied, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
assert_ne!(ksp_store_api::RawRetentionWriteOutcome::AlreadyAtTarget, ksp_store_api::RawRetentionWriteOutcome::ExpectedStateMismatch);
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/unit_tests/model/raw_retention.rs
// version: 1
// version: 2
fn reference() -> std::option::Option<crate::RawTransactionReference> {
let network = match crate::RawNetworkId::new("mainnet-beta".to_owned()) {
@@ -51,3 +51,10 @@ fn tombstone_keeps_minimal_identity_without_raw_payload() {
assert!(crate::RawTransactionTombstone::try_new(tombstone.reference().clone(), tombstone.slot(), second_format, 0, tombstone.content_hash()).is_err());
return;
}
#[test]
fn retention_outcome_distinguishes_target_idempotence_from_lost_compare_and_transition() {
assert_ne!(crate::RawRetentionWriteOutcome::AlreadyAtTarget, crate::RawRetentionWriteOutcome::ExpectedStateMismatch);
assert_ne!(crate::RawRetentionWriteOutcome::Applied, crate::RawRetentionWriteOutcome::ExpectedStateMismatch);
return;
}