From 47b12e14e6a6e2cb0c2975ca7189d06873540684 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 29 Aug 2026 09:29:58 +0200 Subject: [PATCH] v0.3.1-pre.007 --- Cargo.toml | 2 +- .../src/capability/raw_retention.rs | 6 +- .../ksp-store-api/src/model/raw_retention.rs | 7 +- crates/ksp-store-api/tests/public_api.rs | 9 +- .../tests/release_completeness.rs | 228 ++++++++++++++++++ .../ksp-store-api/tests/security_hardening.rs | 124 ++++++++++ .../unit_tests/model/raw_retention.rs | 9 +- deltas/0.3.1/pre.007.md | 166 +++++++++++++ docs/plans/022-V0_3_1_STORE_RAW_PLAN.md | 2 + docs/validation/018-V0_3_1_STORE_RAW.md | 35 ++- 10 files changed, 580 insertions(+), 8 deletions(-) create mode 100644 crates/ksp-store-api/tests/release_completeness.rs create mode 100644 crates/ksp-store-api/tests/security_hardening.rs create mode 100644 deltas/0.3.1/pre.007.md diff --git a/Cargo.toml b/Cargo.toml index c1220b9..7bc8778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/ksp-store-api/src/capability/raw_retention.rs b/crates/ksp-store-api/src/capability/raw_retention.rs index ede4982..edb3f0d 100644 --- a/crates/ksp-store-api/src/capability/raw_retention.rs +++ b/crates/ksp-store-api/src/capability/raw_retention.rs @@ -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>( diff --git a/crates/ksp-store-api/src/model/raw_retention.rs b/crates/ksp-store-api/src/model/raw_retention.rs index 47b218a..be2cc65 100644 --- a/crates/ksp-store-api/src/model/raw_retention.rs +++ b/crates/ksp-store-api/src/model/raw_retention.rs @@ -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 { diff --git a/crates/ksp-store-api/tests/public_api.rs b/crates/ksp-store-api/tests/public_api.rs index 900710f..1cdd718 100644 --- a/crates/ksp-store-api/tests/public_api.rs +++ b/crates/ksp-store-api/tests/public_api.rs @@ -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; +} diff --git a/crates/ksp-store-api/tests/release_completeness.rs b/crates/ksp-store-api/tests/release_completeness.rs new file mode 100644 index 0000000..3a4ba76 --- /dev/null +++ b/crates/ksp-store-api/tests/release_completeness.rs @@ -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> { + 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); +} diff --git a/crates/ksp-store-api/tests/security_hardening.rs b/crates/ksp-store-api/tests/security_hardening.rs new file mode 100644 index 0000000..627bbfa --- /dev/null +++ b/crates/ksp-store-api/tests/security_hardening.rs @@ -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; +} diff --git a/crates/ksp-store-api/unit_tests/model/raw_retention.rs b/crates/ksp-store-api/unit_tests/model/raw_retention.rs index 5dde6f1..71087d3 100644 --- a/crates/ksp-store-api/unit_tests/model/raw_retention.rs +++ b/crates/ksp-store-api/unit_tests/model/raw_retention.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/unit_tests/model/raw_retention.rs -// version: 1 +// version: 2 fn reference() -> std::option::Option { 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; +} diff --git a/deltas/0.3.1/pre.007.md b/deltas/0.3.1/pre.007.md new file mode 100644 index 0000000..6913813 --- /dev/null +++ b/deltas/0.3.1/pre.007.md @@ -0,0 +1,166 @@ + + + +# 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 +``` diff --git a/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md b/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md index d4b03a6..bc96e68 100644 --- a/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md +++ b/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md @@ -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. diff --git a/docs/validation/018-V0_3_1_STORE_RAW.md b/docs/validation/018-V0_3_1_STORE_RAW.md index d637ac8..9afeb9f 100644 --- a/docs/validation/018-V0_3_1_STORE_RAW.md +++ b/docs/validation/018-V0_3_1_STORE_RAW.md @@ -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 |