v0.3.1-pre.007
This commit is contained in:
@@ -6,7 +6,7 @@ resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1-pre.6-fix.1"
|
||||
version = "0.3.1-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
228
crates/ksp-store-api/tests/release_completeness.rs
Normal file
228
crates/ksp-store-api/tests/release_completeness.rs
Normal 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);
|
||||
}
|
||||
124
crates/ksp-store-api/tests/security_hardening.rs
Normal file
124
crates/ksp-store-api/tests/security_hardening.rs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
166
deltas/0.3.1/pre.007.md
Normal file
166
deltas/0.3.1/pre.007.md
Normal file
@@ -0,0 +1,166 @@
|
||||
<!-- file: deltas/0.3.1/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.1-pre.007` — hardening adversarial et complétude Store API
|
||||
|
||||
## Base
|
||||
|
||||
```text
|
||||
0.3.1-pre.6-fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur de `pre.006-fix.001` est intégralement propre :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.1
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
```
|
||||
|
||||
Résultat Store API observé : 14 tests unitaires, 2 dependency-boundary, 1 external-backend, 6 public-api et doc-tests propres.
|
||||
|
||||
## Objet
|
||||
|
||||
Fermer la tranche de hardening/completeness de `ksp-store-api` sans ouvrir une nouvelle famille N1 ni une couche supérieure :
|
||||
|
||||
```text
|
||||
payload/cursor/provenance adversariaux
|
||||
retention compare-and-transition race
|
||||
exports/modules/capabilities exacts
|
||||
frontière ksp-interface-lib / ksp-store-api
|
||||
scope négatif event-only + N2/N3/N4
|
||||
backend/dependency firewall
|
||||
```
|
||||
|
||||
## Race de rétention
|
||||
|
||||
`RawTransactionRetentionTransition` transporte déjà :
|
||||
|
||||
```text
|
||||
expected
|
||||
reference
|
||||
target
|
||||
```
|
||||
|
||||
Le résultat manquait toutefois d'un état distinct lorsque l'état durable avait changé concurremment. `RawRetentionWriteOutcome` ajoute donc :
|
||||
|
||||
```text
|
||||
ExpectedStateMismatch
|
||||
```
|
||||
|
||||
Sémantique requise pour un backend :
|
||||
|
||||
```text
|
||||
persisted == expected
|
||||
-> compare-and-transition atomique
|
||||
-> Applied si succès
|
||||
|
||||
persisted == target
|
||||
-> AlreadyAtTarget
|
||||
|
||||
persisted != expected && persisted != target
|
||||
-> ExpectedStateMismatch
|
||||
```
|
||||
|
||||
Cette course n'est ni un input invalide ni une autorisation pour écraser le nouvel état. La décision de retry/reload reste au caller/worker policy.
|
||||
|
||||
## Hardening adversarial
|
||||
|
||||
Nouveau canari :
|
||||
|
||||
```text
|
||||
crates/ksp-store-api/tests/security_hardening.rs
|
||||
```
|
||||
|
||||
Il vérifie notamment :
|
||||
|
||||
```text
|
||||
URL/marker hostile rejeté sans echo dans Error Display/Debug
|
||||
RawPayload Debug sans bytes
|
||||
RawContentHash Debug opaque
|
||||
RawTransactionSignature Debug opaque
|
||||
RawTransaction Debug sans payload bytes
|
||||
RawAccountState Debug sans account bytes
|
||||
cursor exact à la borne accepté
|
||||
cursor au-dessus de la borne rejeté
|
||||
page limit 0 rejeté
|
||||
page limit u64::MAX accepté sans policy Store artificielle
|
||||
ExpectedStateMismatch distinct de Applied/AlreadyAtTarget
|
||||
```
|
||||
|
||||
## Complétude et frontières
|
||||
|
||||
Nouveau canari :
|
||||
|
||||
```text
|
||||
crates/ksp-store-api/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
Il verrouille :
|
||||
|
||||
```text
|
||||
60 exports crate-root exacts
|
||||
aucun pub mod
|
||||
inventaire exact src/model/capability
|
||||
public enums évolutifs #[non_exhaustive]
|
||||
10 traits capability fins exacts
|
||||
aucun Store/StoreBackend runtime monolithique
|
||||
aucun RawLogNotification/RawSlotEvent/RawVoteEvent public
|
||||
aucun YellowstoneEntry
|
||||
aucun Structural*/Decoded*/Domain* public
|
||||
aucun backend PostgreSQL/MySQL dans l'API
|
||||
aucun modèle persistant Store réexporté par ksp-interface-lib
|
||||
```
|
||||
|
||||
La frontière durable reste :
|
||||
|
||||
```text
|
||||
ksp-interface-lib
|
||||
-> contrats/wires/event-only inter-composants lorsque réellement nécessaires
|
||||
|
||||
ksp-store-api
|
||||
-> modèles persistants/replayables/queryables + capabilities backend-agnostic
|
||||
```
|
||||
|
||||
Aucun modèle event-only nouveau n'est créé par cette tranche.
|
||||
|
||||
## Hors scope
|
||||
|
||||
```text
|
||||
ksp-store-lib
|
||||
ksp-store-postgres-lib
|
||||
SQL/migrations
|
||||
Config std.store
|
||||
Transport -> RAW adapter
|
||||
RawLogNotification / RawSlotEvent / RawVoteEvent
|
||||
TransactionStatusObservation
|
||||
RawBlock / Yellowstone Entry
|
||||
N2 STRUCTURAL
|
||||
N3 DECODED
|
||||
N4 DOMAIN
|
||||
processing ledger
|
||||
worker/executor policy
|
||||
compression/archive physiques
|
||||
```
|
||||
|
||||
## Version Cargo
|
||||
|
||||
La tranche suivante normale porte :
|
||||
|
||||
```text
|
||||
0.3.1-pre.7
|
||||
```
|
||||
|
||||
## Validations attendues
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.1
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-store-api
|
||||
```
|
||||
@@ -1426,6 +1426,8 @@ Finaliser reads/list cursorisés sans plafond métier arbitraire, outcomes idemp
|
||||
|
||||
Payload/cursor/provenance hostile, Interface/Store ownership canaries, admission matrix, retention races, exact exports/modules, external backend, dependency firewall et scope négatif N2/N3/N4.
|
||||
|
||||
La tranche matérialise aussi le résultat distinct `ExpectedStateMismatch` pour une compare-and-transition de rétention perdue : un backend ne doit confondre ni une course concurrente, ni un état déjà à la cible, ni une transition effectivement appliquée. Les canaris de complétude figent la surface publique N1, les inventaires de modules/capabilities et l'absence de modèles event-only ou STRUCTURAL/DECODED/DOMAIN dans `ksp-store-api`.
|
||||
|
||||
### `pre.008` — Gate technique final
|
||||
|
||||
Workspace complet, tests Store API, graphes Cargo. Aucun développement fonctionnel nouveau.
|
||||
|
||||
@@ -425,17 +425,46 @@ tombstone minimal sans payload
|
||||
retention policy choisie hors Store par worker/job/maintenance
|
||||
```
|
||||
|
||||
### 8.5 Matérialisation `pre.007`
|
||||
|
||||
La tranche ferme le hardening de surface sans ajouter de nouvelle famille N1 :
|
||||
|
||||
```text
|
||||
security_hardening.rs
|
||||
release_completeness.rs
|
||||
RawRetentionWriteOutcome::ExpectedStateMismatch
|
||||
```
|
||||
|
||||
Gates matérialisés :
|
||||
|
||||
```text
|
||||
erreurs model/query/provenance hostiles sans echo de valeur
|
||||
Debug payload/hash/signature/account sans bytes sensibles
|
||||
cursor exact 4 KiB accepté, dépassement rejeté
|
||||
RawPageLimit u64::MAX reste représentable
|
||||
compare-and-transition perdue distincte de Applied/AlreadyAtTarget
|
||||
exports crate-root exacts
|
||||
inventaire exact des modules production RAW
|
||||
10 capabilities fines exactes, aucun StoreBackend/Store runtime
|
||||
Interface ne réexporte pas les modèles persistants Store
|
||||
Store API ne publie aucun event-only logs/slot/vote
|
||||
aucune surface STRUCTURAL/DECODED/DOMAIN
|
||||
aucune fuite PostgreSQL/MySQL/backend runtime
|
||||
```
|
||||
|
||||
La course de rétention est un état normal de concurrence : si l'état durable ne correspond plus à `expected` et n'est pas déjà `target`, le backend doit retourner `ExpectedStateMismatch`. Cette sémantique évite d'utiliser une erreur de validation pour une course et impose une compare-and-transition atomique à l'implémentation future.
|
||||
|
||||
## 11. État initial des tranches
|
||||
|
||||
| Tranche | Objet | État |
|
||||
|-----------|------------------------------------------|-----------------------|
|
||||
| `pre.001` | audit/design/taxonomie/split | PRÊT après gate local |
|
||||
| `pre.002` | scaffold + taxonomie Store API | À FAIRE |
|
||||
| `pre.003` | primitives + RawTransaction | À FAIRE |
|
||||
| `pre.002` | scaffold + taxonomie Store API | PRÊT après gate local |
|
||||
| `pre.003` | primitives + RawTransaction | PRÊT après gate local |
|
||||
| `pre.004` | admission matrix + account/status models | PRÊT après gate local |
|
||||
| `pre.005` | backend contracts/capabilities | PRÊT après gate local |
|
||||
| `pre.006` | queries/outcomes/retention/tombstone | PRÊT après gate local |
|
||||
| `pre.007` | boundary/adversarial/completeness | À FAIRE |
|
||||
| `pre.007` | boundary/adversarial/completeness | PRÊT après gate local |
|
||||
| `pre.008` | gate technique final | À FAIRE |
|
||||
| `pre.009` | réconciliation documentaire | À FAIRE |
|
||||
| `pre.010` | préparation publication | À FAIRE |
|
||||
|
||||
Reference in New Issue
Block a user