From ecc5607fd434ec275d1fa7d40d5ef58f7cc26e67 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 29 Aug 2026 07:51:09 +0200 Subject: [PATCH] v0.3.1-pre.003 --- Cargo.toml | 4 +- crates/ksp-store-api/src/error.rs | 9 + crates/ksp-store-api/src/lib.rs | 43 +- crates/ksp-store-api/src/model.rs | 11 +- .../ksp-store-api/src/model/raw_primitives.rs | 449 ++++++++++++++++++ .../src/model/raw_transaction.rs | 113 +++++ .../tests/dependency_boundary.rs | 46 +- crates/ksp-store-api/tests/public_api.rs | 62 ++- .../unit_tests/model/raw_primitives.rs | 197 ++++++++ .../unit_tests/model/raw_transaction.rs | 85 ++++ deltas/0.3.1/pre.003.md | 204 ++++++++ docs/plans/022-V0_3_1_STORE_RAW_PLAN.md | 46 +- docs/validation/018-V0_3_1_STORE_RAW.md | 39 +- 13 files changed, 1274 insertions(+), 34 deletions(-) create mode 100644 crates/ksp-store-api/src/error.rs create mode 100644 crates/ksp-store-api/src/model/raw_primitives.rs create mode 100644 crates/ksp-store-api/src/model/raw_transaction.rs create mode 100644 crates/ksp-store-api/unit_tests/model/raw_primitives.rs create mode 100644 crates/ksp-store-api/unit_tests/model/raw_transaction.rs create mode 100644 deltas/0.3.1/pre.003.md diff --git a/Cargo.toml b/Cargo.toml index f330ce8..afe5eca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 324 +# version: 325 [workspace] 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.2" +version = "0.3.1-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-api/src/error.rs b/crates/ksp-store-api/src/error.rs new file mode 100644 index 0000000..4ec2a2a --- /dev/null +++ b/crates/ksp-store-api/src/error.rs @@ -0,0 +1,9 @@ +// file: crates/ksp-store-api/src/error.rs +// version: 1 + +/// Error code used when a RAW Store model violates one of its backend-agnostic invariants. +pub const ERROR_CODE_RAW_MODEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_model_invalid"); +/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract. +pub const ERROR_CODE_RAW_PAYLOAD_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_payload_invalid"); +/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. +pub const ERROR_CODE_RAW_PROVENANCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_api", "raw_provenance_invalid"); diff --git a/crates/ksp-store-api/src/lib.rs b/crates/ksp-store-api/src/lib.rs index 03f2a2f..9d6b4ab 100644 --- a/crates/ksp-store-api/src/lib.rs +++ b/crates/ksp-store-api/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/lib.rs -// version: 1 +// version: 2 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -17,8 +17,49 @@ //! dispatch remain outside this crate. mod capability; +mod error; mod model; +/// Error code used when a RAW Store model violates one of its backend-agnostic invariants. +pub use self::error::ERROR_CODE_RAW_MODEL_INVALID; +/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract. +pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID; +/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. +pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID; +/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code. +pub use self::model::raw_primitives::MAX_RAW_CODE_BYTES; +/// Maximum KSP-owned canonical RAW payload admitted by the Store API. +pub use self::model::raw_primitives::MAX_RAW_PAYLOAD_BYTES; +/// Maximum source-wire payload size recorded as acquisition metadata. +pub use self::model::raw_primitives::MAX_RAW_SOURCE_PAYLOAD_BYTES; +/// Maximum supported Unix millisecond timestamp. +pub use self::model::raw_primitives::MAX_RAW_UNIX_MILLIS; +/// Origin category describing why one acquisition was performed. +pub use self::model::raw_primitives::RawAcquisitionOrigin; +/// Safe source-independent acquisition provenance attached to one persisted observation. +pub use self::model::raw_primitives::RawAcquisitionProvenance; +/// Fixed-size digest identifying canonical or source bytes without retaining them. +pub use self::model::raw_primitives::RawContentHash; +/// Bounded identifier of one KSP-owned source-independent RAW persistence format. +pub use self::model::raw_primitives::RawFormatId; +/// Bounded logical network/cluster identifier used in backend-independent Store identities. +pub use self::model::raw_primitives::RawNetworkId; +/// Stable deterministic idempotence key for one persisted acquisition observation. +pub use self::model::raw_primitives::RawObservationKey; +/// Bounded source-independent KSP RAW persistence payload. +pub use self::model::raw_primitives::RawPayload; +/// Bounded logical code used by acquisition provenance fields. +pub use self::model::raw_primitives::RawProvenanceCode; +/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch. +pub use self::model::raw_primitives::RawTimestamp; +/// Canonical 64-byte Solana transaction signature used by Store identities. +pub use self::model::raw_primitives::RawTransactionSignature; +/// Canonical source-independent N1 RAW transaction persisted by Store backends. +pub use self::model::raw_transaction::RawTransaction; +/// Persistable acquisition observation linked to one canonical RAW transaction. +pub use self::model::raw_transaction::RawTransactionObservation; +/// Durable backend-independent identity of one canonical RAW transaction. +pub use self::model::raw_transaction::RawTransactionReference; /// Common KSP error type used by Store-facing contracts. pub use ksp_core_lib::Error; /// Stable structured code identifying a KSP error category and condition. diff --git a/crates/ksp-store-api/src/model.rs b/crates/ksp-store-api/src/model.rs index 038c4c8..d5df2e9 100644 --- a/crates/ksp-store-api/src/model.rs +++ b/crates/ksp-store-api/src/model.rs @@ -1,9 +1,12 @@ // file: crates/ksp-store-api/src/model.rs -// version: 1 +// version: 2 //! Private home for persistent Store models. //! //! The current release is N1 RAW-only. Future N2 STRUCTURAL models are a -//! separate data layer and are not introduced by this scaffold. Persistent -//! models and backend capabilities remain separate even when one capability -//! operates on one or more models. +//! separate data layer and are not introduced here. Persistent models and +//! backend capabilities remain separate even when one capability operates on +//! one or more models. + +pub(crate) mod raw_primitives; +pub(crate) mod raw_transaction; diff --git a/crates/ksp-store-api/src/model/raw_primitives.rs b/crates/ksp-store-api/src/model/raw_primitives.rs new file mode 100644 index 0000000..8014940 --- /dev/null +++ b/crates/ksp-store-api/src/model/raw_primitives.rs @@ -0,0 +1,449 @@ +// file: crates/ksp-store-api/src/model/raw_primitives.rs +// version: 1 + +/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code. +pub const MAX_RAW_CODE_BYTES: usize = 128; +/// Maximum KSP-owned canonical RAW payload admitted by the Store API. +/// +/// This is a Store admission guard, not a Solana protocol-size claim. +pub const MAX_RAW_PAYLOAD_BYTES: usize = 16 * 1024 * 1024; +/// Maximum source-wire payload size recorded as acquisition metadata. +/// +/// The source payload itself is never retained by this metadata field. +pub const MAX_RAW_SOURCE_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024; +/// Maximum supported Unix millisecond timestamp (`9999-12-31T23:59:59.999Z`). +pub const MAX_RAW_UNIX_MILLIS: u64 = 253_402_300_799_999; + +/// Fixed-size digest identifying canonical or source bytes without retaining them. +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct RawContentHash([u8; 32]); + +impl RawContentHash { + /// Creates one opaque 32-byte KSP content digest. + #[must_use] + pub const fn new(bytes: [u8; 32]) -> Self { + return Self(bytes); + } + + /// Returns the exact digest bytes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 32] { + return &self.0; + } +} + +impl std::fmt::Debug for RawContentHash { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("RawContentHash(..)"); + } +} + +/// Stable deterministic idempotence key for one persisted acquisition observation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct RawObservationKey([u8; 32]); + +impl RawObservationKey { + /// Creates one producer-owned deterministic observation key. + #[must_use] + pub const fn new(bytes: [u8; 32]) -> Self { + return Self(bytes); + } + + /// Returns the exact observation-key bytes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 32] { + return &self.0; + } +} + +/// Canonical 64-byte Solana transaction signature used by Store identities. +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawTransactionSignature([u8; 64]); + +impl RawTransactionSignature { + /// Creates one canonical signature from already-decoded Solana signature bytes. + #[must_use] + pub const fn new(bytes: [u8; 64]) -> Self { + return Self(bytes); + } + + /// Returns the exact signature bytes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 64] { + return &self.0; + } +} + +impl std::fmt::Debug for RawTransactionSignature { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.write_str("RawTransactionSignature(..)"); + } +} + +/// Bounded logical network/cluster identifier used in backend-independent Store identities. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawNetworkId(std::string::String); + +impl RawNetworkId { + /// Creates one safe non-empty network/cluster identifier. + pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { + let value = value.into(); + if !valid_raw_code(value.as_str()) { + return std::result::Result::Err(raw_model_error("network")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the network/cluster identifier. + #[must_use] + pub fn as_str(&self) -> &str { + return self.0.as_str(); + } +} + +/// Bounded logical code used by acquisition provenance fields. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawProvenanceCode(std::string::String); + +impl RawProvenanceCode { + /// Creates one safe non-empty provenance code. + pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { + let value = value.into(); + if !valid_raw_code(value.as_str()) { + return std::result::Result::Err(raw_provenance_error("code")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the validated provenance code. + #[must_use] + pub fn as_str(&self) -> &str { + return self.0.as_str(); + } +} + +/// Bounded identifier of one KSP-owned source-independent RAW persistence format. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawFormatId(std::string::String); + +impl RawFormatId { + /// Creates one safe non-empty RAW format identifier. + pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { + let value = value.into(); + if !valid_raw_code(value.as_str()) { + return std::result::Result::Err(raw_payload_error("format_id")); + } + return std::result::Result::Ok(Self(value)); + } + + /// Returns the KSP-owned RAW format identifier. + #[must_use] + pub fn as_str(&self) -> &str { + return self.0.as_str(); + } +} + +/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RawTimestamp { + unix_millis: u64, +} + +impl RawTimestamp { + /// Creates a bounded UTC timestamp from Unix milliseconds. + pub fn from_unix_millis(unix_millis: u64) -> ksp_core_lib::Result { + if unix_millis > crate::MAX_RAW_UNIX_MILLIS { + return std::result::Result::Err(raw_model_error("timestamp")); + } + return std::result::Result::Ok(Self { unix_millis }); + } + + /// Returns whole milliseconds since Unix epoch. + #[must_use] + pub const fn unix_millis(&self) -> u64 { + return self.unix_millis; + } +} + +/// Origin category describing why one acquisition was performed. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RawAcquisitionOrigin { + /// Historical acquisition intended to fill missing durable data. + Backfill, + /// Explicit import from a non-live source controlled by the caller. + Import, + /// Live acquisition from a currently active transport/session. + Live, + /// Explicit repair or reconciliation of previously known data. + Repair, + /// Explicit replay of an already-known source or archived acquisition. + Replay, +} + +/// Safe source-independent acquisition provenance attached to one persisted observation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAcquisitionProvenance { + acquisition_method: crate::RawProvenanceCode, + capture_session_id: std::option::Option, + commitment: std::option::Option, + endpoint_id: std::option::Option, + filter_id: std::option::Option, + observed_at: std::option::Option, + origin: crate::RawAcquisitionOrigin, + protocol: crate::RawProvenanceCode, + provider: crate::RawProvenanceCode, + received_at: crate::RawTimestamp, + source_payload_hash: std::option::Option, + source_payload_size_bytes: std::option::Option, +} + +impl RawAcquisitionProvenance { + /// Creates one successful acquisition provenance record with only mandatory safe metadata. + #[must_use] + pub fn new( + provider: crate::RawProvenanceCode, + protocol: crate::RawProvenanceCode, + acquisition_method: crate::RawProvenanceCode, + origin: crate::RawAcquisitionOrigin, + received_at: crate::RawTimestamp, + ) -> Self { + return Self { + acquisition_method, + capture_session_id: std::option::Option::None, + commitment: std::option::Option::None, + endpoint_id: std::option::Option::None, + filter_id: std::option::Option::None, + observed_at: std::option::Option::None, + origin, + protocol, + provider, + received_at, + source_payload_hash: std::option::Option::None, + source_payload_size_bytes: std::option::Option::None, + }; + } + + /// Attaches one safe logical capture/session identifier. + #[must_use] + pub fn with_capture_session_id(mut self, value: crate::RawProvenanceCode) -> Self { + self.capture_session_id = std::option::Option::Some(value); + return self; + } + + /// Attaches one safe commitment code captured at acquisition. + #[must_use] + pub fn with_commitment(mut self, value: crate::RawProvenanceCode) -> Self { + self.commitment = std::option::Option::Some(value); + return self; + } + + /// Attaches one Config-owned logical endpoint identifier. + #[must_use] + pub fn with_endpoint_id(mut self, value: crate::RawProvenanceCode) -> Self { + self.endpoint_id = std::option::Option::Some(value); + return self; + } + + /// Attaches one safe logical filter identifier. + #[must_use] + pub fn with_filter_id(mut self, value: crate::RawProvenanceCode) -> Self { + self.filter_id = std::option::Option::Some(value); + return self; + } + + /// Attaches the source observation timestamp when it does not follow local receipt. + pub fn try_with_observed_at(mut self, value: crate::RawTimestamp) -> ksp_core_lib::Result { + if value > self.received_at { + return std::result::Result::Err(raw_provenance_error("observed_at")); + } + self.observed_at = std::option::Option::Some(value); + return std::result::Result::Ok(self); + } + + /// Attaches the digest of source-specific bytes without retaining those bytes. + #[must_use] + pub fn with_source_payload_hash(mut self, value: crate::RawContentHash) -> Self { + self.source_payload_hash = std::option::Option::Some(value); + return self; + } + + /// Attaches the bounded source-wire payload size. + pub fn try_with_source_payload_size_bytes(mut self, value: u64) -> ksp_core_lib::Result { + if value > crate::MAX_RAW_SOURCE_PAYLOAD_BYTES { + return std::result::Result::Err(raw_provenance_error("source_payload_size_bytes")); + } + self.source_payload_size_bytes = std::option::Option::Some(value); + return std::result::Result::Ok(self); + } + + /// Returns the logical acquisition method code. + #[must_use] + pub const fn acquisition_method(&self) -> &crate::RawProvenanceCode { + return &self.acquisition_method; + } + + /// Returns the optional logical capture/session identifier. + #[must_use] + pub fn capture_session_id(&self) -> std::option::Option<&crate::RawProvenanceCode> { + return self.capture_session_id.as_ref(); + } + + /// Returns the optional commitment code captured at acquisition. + #[must_use] + pub fn commitment(&self) -> std::option::Option<&crate::RawProvenanceCode> { + return self.commitment.as_ref(); + } + + /// Returns the optional Config-owned logical endpoint identifier. + #[must_use] + pub fn endpoint_id(&self) -> std::option::Option<&crate::RawProvenanceCode> { + return self.endpoint_id.as_ref(); + } + + /// Returns the optional logical filter identifier. + #[must_use] + pub fn filter_id(&self) -> std::option::Option<&crate::RawProvenanceCode> { + return self.filter_id.as_ref(); + } + + /// Returns the optional source observation timestamp when the source supplies one. + #[must_use] + pub const fn observed_at(&self) -> std::option::Option { + return self.observed_at; + } + + /// Returns the acquisition origin category. + #[must_use] + pub const fn origin(&self) -> crate::RawAcquisitionOrigin { + return self.origin; + } + + /// Returns the logical transport/protocol code. + #[must_use] + pub fn protocol(&self) -> &crate::RawProvenanceCode { + return &self.protocol; + } + + /// Returns the safe provider code. + #[must_use] + pub fn provider(&self) -> &crate::RawProvenanceCode { + return &self.provider; + } + + /// Returns the local receipt timestamp. + #[must_use] + pub const fn received_at(&self) -> crate::RawTimestamp { + return self.received_at; + } + + /// Returns the optional digest of source-specific bytes without retaining those bytes. + #[must_use] + pub const fn source_payload_hash(&self) -> std::option::Option { + return self.source_payload_hash; + } + + /// Returns the optional source-wire payload size. + #[must_use] + pub const fn source_payload_size_bytes(&self) -> std::option::Option { + return self.source_payload_size_bytes; + } +} + +/// Bounded source-independent KSP RAW persistence payload. +pub struct RawPayload { + bytes: std::boxed::Box<[u8]>, + content_hash: crate::RawContentHash, + format_id: crate::RawFormatId, + format_version: u32, +} + +impl RawPayload { + /// Creates one canonical RAW payload after enforcing Store-owned admission invariants. + /// + /// The supplied bytes must already use the KSP-owned source-independent format identified + /// by `format_id` and `format_version`; this constructor performs no transport conversion. + pub fn try_new( + format_id: crate::RawFormatId, + format_version: u32, + bytes: std::boxed::Box<[u8]>, + content_hash: crate::RawContentHash, + ) -> ksp_core_lib::Result { + if format_version == 0 { + return std::result::Result::Err(raw_payload_error("format_version")); + } + if bytes.is_empty() || bytes.len() > crate::MAX_RAW_PAYLOAD_BYTES { + return std::result::Result::Err( + raw_payload_error("bytes") + .with_context("actual_len", bytes.len().to_string()) + .with_context("maximum_len", crate::MAX_RAW_PAYLOAD_BYTES.to_string()), + ); + } + return std::result::Result::Ok(Self { bytes, content_hash, format_id, format_version }); + } + + /// Returns the canonical RAW bytes without transport/provider interpretation. + #[must_use] + pub fn bytes(&self) -> &[u8] { + return self.bytes.as_ref(); + } + + /// Returns the deterministic content digest supplied for these exact canonical bytes. + #[must_use] + pub const fn content_hash(&self) -> crate::RawContentHash { + return self.content_hash; + } + + /// Returns the KSP-owned source-independent format identifier. + #[must_use] + pub fn format_id(&self) -> &crate::RawFormatId { + return &self.format_id; + } + + /// Returns the KSP-owned format version. + #[must_use] + pub const fn format_version(&self) -> u32 { + return self.format_version; + } + + /// Returns the canonical payload length in bytes. + #[must_use] + pub fn byte_len(&self) -> usize { + return self.bytes.len(); + } +} + +impl std::fmt::Debug for RawPayload { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("RawPayload") + .field("format_id", &self.format_id) + .field("format_version", &self.format_version) + .field("len", &self.bytes.len()) + .field("content_hash", &self.content_hash) + .finish(); + } +} + +fn raw_model_error(field: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW Store model").with_context("field", field); +} + +fn raw_payload_error(field: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_PAYLOAD_INVALID, "invalid KSP RAW persistence payload").with_context("field", field); +} + +fn raw_provenance_error(field: &'static str) -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_PROVENANCE_INVALID, "invalid RAW acquisition provenance").with_context("field", field); +} + +fn valid_raw_code(value: &str) -> bool { + if value.is_empty() || value.len() > crate::MAX_RAW_CODE_BYTES { + return false; + } + return value.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':')); +} + +#[cfg(test)] +#[path = "../../unit_tests/model/raw_primitives.rs"] +mod tests; diff --git a/crates/ksp-store-api/src/model/raw_transaction.rs b/crates/ksp-store-api/src/model/raw_transaction.rs new file mode 100644 index 0000000..9342e9e --- /dev/null +++ b/crates/ksp-store-api/src/model/raw_transaction.rs @@ -0,0 +1,113 @@ +// file: crates/ksp-store-api/src/model/raw_transaction.rs +// version: 1 + +/// Durable backend-independent identity of one canonical RAW transaction. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RawTransactionReference { + network: crate::RawNetworkId, + signature: crate::RawTransactionSignature, +} + +impl RawTransactionReference { + /// Creates one durable transaction identity from network and canonical Solana signature. + #[must_use] + pub fn new(network: crate::RawNetworkId, signature: crate::RawTransactionSignature) -> Self { + return Self { network, signature }; + } + + /// Returns the logical Solana network/cluster identifier. + #[must_use] + pub fn network(&self) -> &crate::RawNetworkId { + return &self.network; + } + + /// Returns the canonical transaction signature. + #[must_use] + pub const fn signature(&self) -> crate::RawTransactionSignature { + return self.signature; + } +} + +/// Canonical source-independent N1 RAW transaction persisted by Store backends. +/// +/// The payload must contain the complete KSP canonical transaction representation required +/// for future STRUCTURAL replay, including transaction execution metadata and transaction +/// log messages when the canonical format defines them. Provider/transport provenance is +/// deliberately kept in [`crate::RawTransactionObservation`] instead. +#[derive(Debug)] +pub struct RawTransaction { + block_time: std::option::Option, + payload: crate::RawPayload, + reference: crate::RawTransactionReference, + slot: u64, +} + +impl RawTransaction { + /// Creates one complete canonical RAW transaction. + #[must_use] + pub fn new(reference: crate::RawTransactionReference, slot: u64, block_time: std::option::Option, payload: crate::RawPayload) -> Self { + return Self { block_time, payload, reference, slot }; + } + + /// Returns the optional canonical block timestamp. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns the complete KSP-owned canonical RAW payload. + #[must_use] + pub fn payload(&self) -> &crate::RawPayload { + return &self.payload; + } + + /// Returns the durable backend-independent transaction identity. + #[must_use] + pub fn reference(&self) -> &crate::RawTransactionReference { + return &self.reference; + } + + /// Returns the Solana slot containing the transaction. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } +} + +/// Persistable acquisition observation linked to one canonical RAW transaction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawTransactionObservation { + observation_key: crate::RawObservationKey, + provenance: crate::RawAcquisitionProvenance, + transaction: crate::RawTransactionReference, +} + +impl RawTransactionObservation { + /// Creates one successful observation of a complete canonical RAW transaction. + #[must_use] + pub fn new(observation_key: crate::RawObservationKey, transaction: crate::RawTransactionReference, provenance: crate::RawAcquisitionProvenance) -> Self { + return Self { observation_key, provenance, transaction }; + } + + /// Returns the deterministic producer-owned observation idempotence key. + #[must_use] + pub const fn observation_key(&self) -> crate::RawObservationKey { + return self.observation_key; + } + + /// Returns safe source-independent acquisition provenance. + #[must_use] + pub fn provenance(&self) -> &crate::RawAcquisitionProvenance { + return &self.provenance; + } + + /// Returns the durable transaction identity observed by this acquisition. + #[must_use] + pub fn transaction(&self) -> &crate::RawTransactionReference { + return &self.transaction; + } +} + +#[cfg(test)] +#[path = "../../unit_tests/model/raw_transaction.rs"] +mod tests; diff --git a/crates/ksp-store-api/tests/dependency_boundary.rs b/crates/ksp-store-api/tests/dependency_boundary.rs index eccc892..70bd0bc 100644 --- a/crates/ksp-store-api/tests/dependency_boundary.rs +++ b/crates/ksp-store-api/tests/dependency_boundary.rs @@ -1,10 +1,10 @@ // file: crates/ksp-store-api/tests/dependency_boundary.rs -// version: 1 +// version: 2 -//! Dependency canaries for the Store API scaffold. +//! Dependency canaries for the Store API RAW foundation. #[test] -fn pre_002_manifest_has_exact_core_only_runtime_dependency() { +fn pre_003_manifest_keeps_exact_core_only_runtime_dependency() { let manifest = include_str!("../Cargo.toml"); let dependencies_tail = manifest.split("[dependencies]").nth(1); assert!(dependencies_tail.is_some(), "Store API dependencies section must exist"); @@ -49,26 +49,34 @@ fn pre_002_manifest_has_exact_core_only_runtime_dependency() { } #[test] -fn pre_002_source_boundary_separates_models_capabilities_and_backend_runtime() { +fn pre_003_source_boundary_keeps_raw_models_passive_and_backend_free() { let crate_root = include_str!("../src/lib.rs"); + let model_home = include_str!("../src/model.rs"); + let raw_primitives = include_str!("../src/model/raw_primitives.rs"); + let raw_transaction = include_str!("../src/model/raw_transaction.rs"); assert!(crate_root.contains("mod capability;")); + assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod model;")); - assert!(!crate_root.contains("pub mod ")); - for forbidden in [ - "ksp_store_lib", - "ksp_store_postgres_lib", - "ksp_onchain_transport_lib", - "ksp_program_api", - "serde::", - "sqlx::", - "tokio::", - "tokio_postgres::", - "std::env::", - "std::fs::", - "std::net::", - ] { - assert!(!crate_root.contains(forbidden), "forbidden Store API crate-root dependency/surface detected: {forbidden}"); + assert!(model_home.contains("raw_primitives")); + assert!(model_home.contains("raw_transaction")); + for source in [crate_root, model_home, raw_primitives, raw_transaction] { + for forbidden in [ + "ksp_store_lib", + "ksp_store_postgres_lib", + "ksp_onchain_transport_lib", + "ksp_program_api", + "serde::", + "sqlx::", + "tokio::", + "tokio_postgres::", + "std::env::", + "std::fs::", + "std::net::", + ] { + assert!(!source.contains(forbidden), "forbidden Store API dependency/runtime path detected: {forbidden}"); + } } + assert!(!raw_transaction.contains("RawLog")); return; } diff --git a/crates/ksp-store-api/tests/public_api.rs b/crates/ksp-store-api/tests/public_api.rs index 64b3dfa..e5682c3 100644 --- a/crates/ksp-store-api/tests/public_api.rs +++ b/crates/ksp-store-api/tests/public_api.rs @@ -1,14 +1,21 @@ // file: crates/ksp-store-api/tests/public_api.rs -// version: 1 +// version: 2 -//! Integration canaries for the public `ksp-store-api` scaffold. +//! Integration canaries for the public `ksp-store-api` surface. fn consume_result(value: ksp_store_api::Result) -> ksp_store_api::Result { return value; } +fn code(value: &str) -> std::option::Option { + return match ksp_store_api::RawProvenanceCode::new(value.to_owned()) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + #[test] -fn public_pre_002_core_facade_is_available_from_crate_root() { +fn public_pre_002_core_facade_remains_available_from_crate_root() { let pubkey = ksp_store_api::Pubkey::new_from_array([0x31_u8; 32]); let forwarded = consume_result(std::result::Result::Ok(pubkey)); assert!(forwarded.is_ok()); @@ -22,11 +29,54 @@ fn public_pre_002_core_facade_is_available_from_crate_root() { } #[test] -fn public_pre_002_scaffold_exposes_no_private_module_paths_or_backend_types() { +fn public_pre_003_raw_transaction_and_observation_are_constructible_from_crate_root() { + let network = match ksp_store_api::RawNetworkId::new("mainnet-beta".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let reference = ksp_store_api::RawTransactionReference::new(network, ksp_store_api::RawTransactionSignature::new([1_u8; 64])); + 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 = match ksp_store_api::RawPayload::try_new(format, 1, vec![1_u8, 2_u8, 3_u8].into_boxed_slice(), ksp_store_api::RawContentHash::new([2_u8; 32])) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let transaction = ksp_store_api::RawTransaction::new(reference.clone(), 123, std::option::Option::None, payload); + let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_000) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let provider = match code("provider") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let protocol = match code("solana_http") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let method = match code("getTransaction") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_api::RawAcquisitionOrigin::Backfill, received_at); + let observation = ksp_store_api::RawTransactionObservation::new(ksp_store_api::RawObservationKey::new([3_u8; 32]), reference, provenance); + assert_eq!(transaction.slot(), 123); + assert_eq!(transaction.payload().bytes(), &[1_u8, 2_u8, 3_u8]); + assert_eq!(observation.transaction().network().as_str(), "mainnet-beta"); + assert_eq!(observation.provenance().acquisition_method().as_str(), "getTransaction"); + assert_eq!(ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID.domain(), "store_api"); + return; +} + +#[test] +fn public_pre_003_surface_keeps_backend_and_structural_types_out() { let source = include_str!("../src/lib.rs"); assert!(!source.contains("pub mod ")); - for forbidden in ["Postgres", "Sql", "Migration", "ProgramInstruction", "RawTransaction"] { - assert!(!source.contains(forbidden), "forbidden pre.002 Store API public concept detected: {forbidden}"); + for forbidden in ["Postgres", "Sql", "Migration", "StructuralTransaction", "RawLog", "ProgramInstruction"] { + assert!(!source.contains(forbidden), "forbidden pre.003 Store API public concept detected: {forbidden}"); } return; } diff --git a/crates/ksp-store-api/unit_tests/model/raw_primitives.rs b/crates/ksp-store-api/unit_tests/model/raw_primitives.rs new file mode 100644 index 0000000..a195f2c --- /dev/null +++ b/crates/ksp-store-api/unit_tests/model/raw_primitives.rs @@ -0,0 +1,197 @@ +// file: crates/ksp-store-api/unit_tests/model/raw_primitives.rs +// version: 1 + +fn code(value: &str) -> std::option::Option { + return match crate::RawProvenanceCode::new(value.to_owned()) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +fn timestamp(unix_millis: u64) -> std::option::Option { + return match crate::RawTimestamp::from_unix_millis(unix_millis) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +#[test] +fn raw_codes_are_bounded_nonempty_and_reject_url_like_or_control_values() { + let network = crate::RawNetworkId::new("mainnet-beta".to_owned()); + assert!(network.is_ok()); + let format = crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()); + assert!(format.is_ok()); + let maximum = "a".repeat(crate::MAX_RAW_CODE_BYTES); + assert!(crate::RawProvenanceCode::new(maximum).is_ok()); + assert!(crate::RawProvenanceCode::new(std::string::String::new()).is_err()); + assert!(crate::RawProvenanceCode::new("bad value".to_owned()).is_err()); + assert!(crate::RawProvenanceCode::new("https://secret.example".to_owned()).is_err()); + assert!(crate::RawProvenanceCode::new("bad\nvalue".to_owned()).is_err()); + assert!(crate::RawProvenanceCode::new("a".repeat(crate::MAX_RAW_CODE_BYTES + 1)).is_err()); + return; +} + +#[test] +fn raw_timestamp_accepts_documented_maximum_and_rejects_larger_values() { + let maximum = crate::RawTimestamp::from_unix_millis(crate::MAX_RAW_UNIX_MILLIS); + assert!(maximum.is_ok()); + let too_large = crate::RawTimestamp::from_unix_millis(crate::MAX_RAW_UNIX_MILLIS + 1); + assert!(too_large.is_err()); + return; +} + +#[test] +fn raw_payload_is_nonempty_versioned_bounded_and_debug_omits_bytes() { + let format_result = crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()); + assert!(format_result.is_ok()); + let format = match format_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let sentinel = b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER".to_vec().into_boxed_slice(); + let payload_result = crate::RawPayload::try_new(format, 1, sentinel, crate::RawContentHash::new([7_u8; 32])); + assert!(payload_result.is_ok()); + let payload = match payload_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(payload.bytes(), b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER"); + assert_eq!(payload.byte_len(), b"RAW_PAYLOAD_SENTINEL_NEVER_RENDER".len()); + assert_eq!(payload.format_version(), 1); + let debug = format!("{payload:?}"); + assert!(!debug.contains("RAW_PAYLOAD_SENTINEL_NEVER_RENDER")); + assert!(debug.contains("len")); + let empty_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(crate::RawPayload::try_new(empty_format, 1, std::vec::Vec::new().into_boxed_slice(), crate::RawContentHash::new([0_u8; 32])).is_err()); + let zero_version_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(crate::RawPayload::try_new(zero_version_format, 0, vec![1_u8].into_boxed_slice(), crate::RawContentHash::new([0_u8; 32])).is_err()); + let oversized_format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let oversized = vec![0_u8; crate::MAX_RAW_PAYLOAD_BYTES + 1].into_boxed_slice(); + let error = crate::RawPayload::try_new(oversized_format, 1, oversized, crate::RawContentHash::new([0_u8; 32])); + assert!(error.is_err()); + return; +} + +#[test] +fn acquisition_provenance_preserves_safe_metadata_and_validates_time_and_size() { + let received_at = match timestamp(2_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let observed_at = match timestamp(1_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let provider = match code("publicnode") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let protocol = match code("yellowstone_grpc") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let method = match code("transactions") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let endpoint = match code("main") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let commitment = match code("confirmed") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let session = match code("session_1") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let filter = match code("all_transactions") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let provenance = crate::RawAcquisitionProvenance::new(provider, protocol, method, crate::RawAcquisitionOrigin::Live, received_at) + .with_endpoint_id(endpoint) + .with_commitment(commitment) + .with_capture_session_id(session) + .with_filter_id(filter) + .with_source_payload_hash(crate::RawContentHash::new([3_u8; 32])); + let provenance_result = provenance.try_with_observed_at(observed_at); + assert!(provenance_result.is_ok()); + let provenance = match provenance_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let provenance_result = provenance.try_with_source_payload_size_bytes(4_096); + assert!(provenance_result.is_ok()); + let provenance = match provenance_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(provenance.provider().as_str(), "publicnode"); + assert_eq!(provenance.protocol().as_str(), "yellowstone_grpc"); + assert_eq!(provenance.acquisition_method().as_str(), "transactions"); + assert_eq!(provenance.origin(), crate::RawAcquisitionOrigin::Live); + assert_eq!(provenance.observed_at(), std::option::Option::Some(observed_at)); + assert_eq!(provenance.received_at(), received_at); + assert_eq!(provenance.source_payload_size_bytes(), std::option::Option::Some(4_096)); + let reversed_provider = match code("provider") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reversed_protocol = match code("solana_websocket") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reversed_method = match code("transactionSubscribe") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reversed_received = match timestamp(1_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reversed_observed = match timestamp(2_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reversed = + crate::RawAcquisitionProvenance::new(reversed_provider, reversed_protocol, reversed_method, crate::RawAcquisitionOrigin::Live, reversed_received) + .try_with_observed_at(reversed_observed); + assert!(reversed.is_err()); + let oversized_provider = match code("provider") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let oversized_protocol = match code("solana_http") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let oversized_method = match code("getTransaction") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let oversized_received = match timestamp(2_000) { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let oversized_source = crate::RawAcquisitionProvenance::new( + oversized_provider, + oversized_protocol, + oversized_method, + crate::RawAcquisitionOrigin::Backfill, + oversized_received, + ) + .try_with_source_payload_size_bytes(crate::MAX_RAW_SOURCE_PAYLOAD_BYTES + 1); + assert!(oversized_source.is_err()); + return; +} diff --git a/crates/ksp-store-api/unit_tests/model/raw_transaction.rs b/crates/ksp-store-api/unit_tests/model/raw_transaction.rs new file mode 100644 index 0000000..b8d8a7b --- /dev/null +++ b/crates/ksp-store-api/unit_tests/model/raw_transaction.rs @@ -0,0 +1,85 @@ +// file: crates/ksp-store-api/unit_tests/model/raw_transaction.rs +// version: 1 + +fn network() -> std::option::Option { + return match crate::RawNetworkId::new("mainnet-beta".to_owned()) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +fn payload() -> std::option::Option { + let format = match crate::RawFormatId::new("ksp.solana.raw_transaction".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + return match crate::RawPayload::try_new( + format, + 1, + b"canonical transaction including logs".to_vec().into_boxed_slice(), + crate::RawContentHash::new([5_u8; 32]), + ) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => std::option::Option::None, + }; +} + +fn provenance() -> std::option::Option { + let provider = match crate::RawProvenanceCode::new("publicnode".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + let protocol = match crate::RawProvenanceCode::new("yellowstone_grpc".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + let method = match crate::RawProvenanceCode::new("transactions".to_owned()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + let received_at = match crate::RawTimestamp::from_unix_millis(1_000) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + return std::option::Option::Some(crate::RawAcquisitionProvenance::new(provider, protocol, method, crate::RawAcquisitionOrigin::Backfill, received_at)); +} + +#[test] +fn raw_transaction_identity_is_network_plus_signature_and_payload_remains_whole() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let payload = match payload() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let signature = crate::RawTransactionSignature::new([9_u8; 64]); + let reference = crate::RawTransactionReference::new(network, signature); + let transaction = crate::RawTransaction::new(reference, 42, std::option::Option::None, payload); + assert_eq!(transaction.reference().network().as_str(), "mainnet-beta"); + assert_eq!(transaction.reference().signature(), signature); + assert_eq!(transaction.slot(), 42); + assert!(transaction.block_time().is_none()); + assert_eq!(transaction.payload().bytes(), b"canonical transaction including logs"); + return; +} + +#[test] +fn transaction_observation_is_separate_from_canonical_raw_payload() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let provenance = match provenance() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let signature = crate::RawTransactionSignature::new([11_u8; 64]); + let reference = crate::RawTransactionReference::new(network, signature); + let observation = crate::RawTransactionObservation::new(crate::RawObservationKey::new([12_u8; 32]), reference, provenance); + assert_eq!(observation.transaction().signature(), signature); + assert_eq!(observation.observation_key().as_bytes(), &[12_u8; 32]); + assert_eq!(observation.provenance().provider().as_str(), "publicnode"); + return; +} diff --git a/deltas/0.3.1/pre.003.md b/deltas/0.3.1/pre.003.md new file mode 100644 index 0000000..33c92fc --- /dev/null +++ b/deltas/0.3.1/pre.003.md @@ -0,0 +1,204 @@ + + + +# Delta `0.3.1-pre.003` + +## Base requise + +```text +0.3.1-pre.002 +``` + +Le gate opérateur de `pre.002` est fourni vert : audits Rust/Markdown, `cargo check --workspace`, Clippy workspace et `cargo test -p ksp-store-api` passent. + +## Objectif + +Matérialiser les primitives N1 RAW backend-agnostic puis le premier modèle persistant réel `RawTransaction` avec observation d'acquisition séparée, sans introduire encore les matrices HTTP/WS/gRPC de `pre.004`, les capabilities de `pre.005` ni aucun backend/runtime Store. + +## Version + +Le workspace passe à : + +```text +0.3.1-pre.3 +``` + +## Surface ajoutée + +Primitives communes : + +```text +RawNetworkId +RawTransactionSignature +RawTransactionReference +RawFormatId +RawContentHash +RawObservationKey +RawTimestamp +RawAcquisitionOrigin +RawProvenanceCode +RawAcquisitionProvenance +RawPayload +``` + +Première famille N1 : + +```text +RawTransaction +RawTransactionObservation +``` + +Codes d'erreur : + +```text +store_api.raw_model_invalid +store_api.raw_payload_invalid +store_api.raw_provenance_invalid +``` + +## Invariants RAW + +`RawPayload` contient uniquement un format de persistence KSP déjà source-independent : + +```text +format_id +format_version > 0 +bytes non vides +content_hash [u8; 32] +``` + +Le Store API ne choisit ni codec ni algorithme de conversion Transport. Le producer/converter du futur format canonique doit fournir des bytes complets et leur digest déterministe. + +Bornes Store-owned : + +```text +code logique <= 128 bytes +payload RAW canonique <= 16 MiB +source payload size meta <= 64 MiB +Unix timestamp <= 9999-12-31T23:59:59.999Z +``` + +Ces valeurs sont des admission guards internes et ne prétendent pas définir des maxima Solana. + +`RawPayload` et `RawTransaction` ne sont volontairement pas `Clone`, afin d'éviter de rendre triviale la copie de gros documents RAW. + +## Identité transactionnelle + +La référence durable est : + +```text +RawTransactionReference + network + signature [u8; 64] +``` + +Aucune PK SQL/backend ne traverse l'API. `RawTransaction` ajoute : + +```text +slot: u64 +block_time: Option +RawPayload +``` + +Les logs contenus dans la transaction restent à l'intérieur du payload canonique N1. Aucun modèle `RawLog` persistant ni type STRUCTURAL n'est créé. + +## Observation et provenance + +Une observation réussie reste distincte du RAW : + +```text +RawTransactionObservation + observation_key [u8; 32] + transaction reference + provenance +``` + +La provenance peut représenter avec des logical codes sûrs : + +```text +provider +protocol +acquisition method +origin live/backfill/import/replay/repair +endpoint id optionnel +commitment optionnel +capture/session id optionnel +filter id optionnel +observed_at optionnel +received_at +source payload size/hash optionnels +``` + +Elle n'accepte aucun payload source et ses logical codes refusent notamment les espaces, contrôles et formes URL contenant `/`. Le contrat reste explicitement non-secret : un caller ne doit jamais placer une credential dans un logical code. + +`observed_at`, lorsqu'il existe, ne peut pas être postérieur à `received_at`. + +## Tests + +Tests unitaires ajoutés : + +- bornes/validation des codes ; +- borne de timestamp ; +- payload non vide/versionné/borné ; +- `Debug` du payload sans bytes ; +- provenance et ordre temporel ; +- identité `network + signature` ; +- séparation `RawTransaction` / `RawTransactionObservation`. + +Canaris d'intégration mis à jour : + +- surface crate-root des nouveaux modèles ; +- dépendance runtime exacte `ksp-core-lib` ; +- absence de backend, SQL, serde, codec, async runtime, Transport et Program ; +- absence de `RawLog` et de type STRUCTURAL dans la production `pre.003`. + +## Documentation mise à jour + +```text +docs/plans/022-V0_3_1_STORE_RAW_PLAN.md +docs/validation/018-V0_3_1_STORE_RAW.md +``` + +Ils figent les primitives et bornes réellement matérialisées par cette tranche sans avancer les matrices cross-source de `pre.004`. + +## Validations exécutées dans l'environnement de génération + +```text +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 +``` + +## Validations non exécutées dans l'environnement de génération + +`cargo`, `rustc` et `rustfmt` ne sont pas installés dans l'environnement de génération. L'opérateur doit donc exécuter : + +```text +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +``` + +Une commande non exécutée n'est pas déclarée PASS. + +## Hors scope confirmé + +```text +conversion HTTP/WS/gRPC -> RawTransaction canonique +RawAccountState +TransactionStatusObservation +events logs/slot/vote +capabilities read/write +queries/outcomes +retention/tombstone concret +ksp-store-lib +ksp-store-postgres-lib +PostgreSQL/tokio-postgres +Config std.store +N2 STRUCTURAL +N3/N4 +``` + +## Suite + +`0.3.1-pre.004` audite les formes HTTP/WS/gRPC réelles afin de figer la matrice d'admission cross-source et d'introduire seulement les familles N1 supplémentaires dont la sémantique commune est effectivement démontrée, en priorité `RawAccountState`/observation et `TransactionStatusObservation`. 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 af59bbb..7817033 100644 --- a/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md +++ b/docs/plans/022-V0_3_1_STORE_RAW_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.1` — Store API RAW foundation @@ -553,6 +553,46 @@ hashable/idempotent Le Store ne prétend pas que des bytes arbitraires sans format connu sont replayables. +### 7.3 Contrat matérialisé par `pre.003` + +La première surface concrète reste indépendante de tout codec et de tout backend : + +```text +RawPayload + RawFormatId + format_version: u32 non nul + bytes: Box<[u8]> non vide + RawContentHash: [u8; 32] + +RawTransactionReference + RawNetworkId + RawTransactionSignature: [u8; 64] + +RawTransaction + reference + slot: u64 + block_time: Option + payload + +RawTransactionObservation + RawObservationKey: [u8; 32] + transaction reference + RawAcquisitionProvenance +``` + +Les bornes initiales sont des **admission guards Store**, jamais des affirmations sur les maxima du protocole Solana : + +```text +logical code UTF-8 <= 128 bytes +canonical RAW payload <= 16 MiB +source payload size meta <= 64 MiB +Unix timestamp <= 9999-12-31T23:59:59.999Z +``` + +`RawPayload` ne calcule pas lui-même le digest et ne vérifie pas le contenu du format. Le producer/converter propriétaire du format KSP doit fournir des bytes déjà canoniques et leur digest déterministe. Cette séparation permet de conserver `ksp-store-api -> ksp-core-lib` uniquement. + +Le payload et `RawTransaction` ne sont volontairement pas `Clone` dans cette foundation afin de ne pas encourager des copies implicites d'un document RAW potentiellement volumineux. Les références, signatures, digests, timestamps et observations compactes restent clonables/copiables lorsqu'approprié. + ## 8. Future décomposition N1 -> niveau STRUCTURAL Le nom de travail de N2 devient **STRUCTURAL**. `CORE` est abandonné dans le nouveau plan parce qu'il décrivait mal une opération qui consiste principalement à décomposer des données Solana brutes en sous-composants génériques. @@ -627,6 +667,10 @@ payload source complet par simple diagnostic Les codes provider/protocol/method restent ouverts et bornés ; aucun enum provider fermé n'est introduit dans Store API. +`pre.003` matérialise cette politique avec `RawProvenanceCode`, réutilisé pour les codes logiques provider/protocol/method/endpoint/commitment/session/filter. Les valeurs sont non vides, bornées et limitées à un alphabet logique sûr ; une URL contenant `/`, des contrôles ou des espaces ne peut donc pas être stockée accidentellement dans ces champs. Cette validation n'autorise pas le caller à y placer un secret alphanumérique : le contrat reste explicitement « logical code only ». + +`RawAcquisitionProvenance::new(...)` ne prend que les champs obligatoires ; les informations optionnelles sont ajoutées par builders dédiés. `observed_at`, lorsqu'il existe, ne peut pas être postérieur à `received_at`, et le payload source n'est jamais conservé, seulement sa taille bornée et/ou un digest optionnel. + ## 10. Identité et idempotence ### 10.1 Principes diff --git a/docs/validation/018-V0_3_1_STORE_RAW.md b/docs/validation/018-V0_3_1_STORE_RAW.md index a1ec102..515d1c1 100644 --- a/docs/validation/018-V0_3_1_STORE_RAW.md +++ b/docs/validation/018-V0_3_1_STORE_RAW.md @@ -1,5 +1,5 @@ - + # Validation `0.3.1` — Store API RAW foundation @@ -190,6 +190,43 @@ crate/test backend externe | force rehydrate implicite | interdit | `pre.006` | | N2/N3/N4 creep | aucune surface | `pre.007` | +### 8.1 Matérialisation `pre.003` + +La tranche implémente et couvre localement par canaris source/audit : + +```text +RawNetworkId +RawTransactionSignature [u8; 64] +RawTransactionReference +RawFormatId +RawContentHash [u8; 32] +RawObservationKey [u8; 32] +RawTimestamp +RawAcquisitionOrigin +RawProvenanceCode +RawAcquisitionProvenance +RawPayload +RawTransaction +RawTransactionObservation +``` + +Gates matérialisés : + +```text +Core-only dependency firewall conservé +payload KSP source-independent, non vide, version > 0 +payload maximum Store = 16 MiB +source payload metadata maximum = 64 MiB +Debug RawPayload ne rend jamais les bytes +signature/observation key/hash ne dépendent d'aucune PK backend +provenance sans URL/source payload +observed_at <= received_at +logs transactionnels restent dans le payload RawTransaction +aucun RawLog/N2 STRUCTURAL/backend/runtime ajouté +``` + +La complétude sémantique d'une source HTTP/WS/gRPC vers le format canonique n'est pas simulée dans Store API : elle reste le gate d'admission/conversion de `pre.004`. `pre.003` exige seulement qu'un `RawTransaction` reçoive un `RawPayload` déjà canonique complet selon son format KSP déclaré. + ## 9. Gates de fermeture prévus ### Gate technique final `pre.008`