diff --git a/Cargo.toml b/Cargo.toml index e74590d..99c0534 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 325 +# version: 326 [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.3-fix.1" +version = "0.3.1-pre.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-api/src/lib.rs b/crates/ksp-store-api/src/lib.rs index 9d6b4ab..32b8c4f 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: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -26,6 +26,14 @@ pub use self::error::ERROR_CODE_RAW_MODEL_INVALID; 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; +/// Persistable acquisition observation linked to one complete canonical RAW account state. +pub use self::model::raw_account::RawAccountObservation; +/// Canonical complete N1 RAW account state independent from acquisition transport. +pub use self::model::raw_account::RawAccountState; +/// Durable backend-independent identity of one canonical RAW account state. +pub use self::model::raw_account::RawAccountStateReference; +/// Maximum complete RAW account-data length admitted by the Store API. +pub use self::model::raw_primitives::MAX_RAW_ACCOUNT_DATA_BYTES; /// 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. diff --git a/crates/ksp-store-api/src/model.rs b/crates/ksp-store-api/src/model.rs index d5df2e9..ce051bb 100644 --- a/crates/ksp-store-api/src/model.rs +++ b/crates/ksp-store-api/src/model.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-api/src/model.rs -// version: 2 +// version: 3 //! Private home for persistent Store models. //! @@ -8,5 +8,6 @@ //! backend capabilities remain separate even when one capability operates on //! one or more models. +pub(crate) mod raw_account; pub(crate) mod raw_primitives; pub(crate) mod raw_transaction; diff --git a/crates/ksp-store-api/src/model/raw_account.rs b/crates/ksp-store-api/src/model/raw_account.rs new file mode 100644 index 0000000..56560a7 --- /dev/null +++ b/crates/ksp-store-api/src/model/raw_account.rs @@ -0,0 +1,231 @@ +// file: crates/ksp-store-api/src/model/raw_account.rs +// version: 1 + +/// Durable backend-independent identity of one canonical RAW account state. +/// +/// The content hash is part of the identity because one account can be written more than once +/// inside the same slot while standard HTTP/WebSocket surfaces do not expose Yellowstone's +/// `write_version`. Multiple observations of the same complete state therefore converge on the +/// same reference without making a provider-specific write ordinal part of the common model. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct RawAccountStateReference { + network: crate::RawNetworkId, + pubkey: ksp_core_lib::Pubkey, + slot: u64, + state_hash: crate::RawContentHash, +} + +impl RawAccountStateReference { + /// Creates one durable account-state identity from network, account, slot and canonical state digest. + #[must_use] + pub fn new(network: crate::RawNetworkId, pubkey: ksp_core_lib::Pubkey, slot: u64, state_hash: crate::RawContentHash) -> Self { + return Self { network, pubkey, slot, state_hash }; + } + + /// Returns the logical Solana network/cluster identifier. + #[must_use] + pub fn network(&self) -> &crate::RawNetworkId { + return &self.network; + } + + /// Returns the account public key. + #[must_use] + pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { + return &self.pubkey; + } + + /// Returns the slot associated with this complete account state. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns the producer-supplied digest of the complete canonical account state. + #[must_use] + pub const fn state_hash(&self) -> crate::RawContentHash { + return self.state_hash; + } +} + +/// Canonical complete N1 RAW account state independent from HTTP, WebSocket or gRPC acquisition. +/// +/// Only complete raw account bytes are admissible. A transport response using `jsonParsed`, a +/// request-side data slice, or a response without a durable slot context must be normalized or +/// reacquired before this model is constructed. +pub struct RawAccountState { + data: std::boxed::Box<[u8]>, + executable: bool, + lamports: u64, + owner: ksp_core_lib::Pubkey, + reference: crate::RawAccountStateReference, + rent_epoch: u64, +} + +impl RawAccountState { + /// Creates one complete canonical RAW account state after Store-owned admission checks. + pub fn try_new( + reference: crate::RawAccountStateReference, + lamports: u64, + owner: ksp_core_lib::Pubkey, + executable: bool, + rent_epoch: u64, + data: std::boxed::Box<[u8]>, + ) -> ksp_core_lib::Result { + if data.len() > crate::MAX_RAW_ACCOUNT_DATA_BYTES { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_MODEL_INVALID, "invalid backend-agnostic RAW Store model") + .with_context("field", "account_data") + .with_context("actual_len", data.len().to_string()) + .with_context("maximum_len", crate::MAX_RAW_ACCOUNT_DATA_BYTES.to_string()), + ); + } + return std::result::Result::Ok(Self { data, executable, lamports, owner, reference, rent_epoch }); + } + + /// Returns the exact complete account bytes used by future decoders. + #[must_use] + pub fn data(&self) -> &[u8] { + return self.data.as_ref(); + } + + /// Returns the complete account-data length in bytes. + #[must_use] + pub fn data_len(&self) -> usize { + return self.data.len(); + } + + /// Returns whether the account is executable. + #[must_use] + pub const fn executable(&self) -> bool { + return self.executable; + } + + /// Returns the account lamport balance. + #[must_use] + pub const fn lamports(&self) -> u64 { + return self.lamports; + } + + /// Returns the account owner program public key. + #[must_use] + pub const fn owner(&self) -> &ksp_core_lib::Pubkey { + return &self.owner; + } + + /// Returns the durable source-independent account-state identity. + #[must_use] + pub fn reference(&self) -> &crate::RawAccountStateReference { + return &self.reference; + } + + /// Returns the rent epoch reported for this account state. + #[must_use] + pub const fn rent_epoch(&self) -> u64 { + return self.rent_epoch; + } +} + +impl std::fmt::Debug for RawAccountState { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("RawAccountState") + .field("reference", &self.reference) + .field("lamports", &self.lamports) + .field("owner", &self.owner) + .field("executable", &self.executable) + .field("rent_epoch", &self.rent_epoch) + .field("data_len", &self.data.len()) + .finish(); + } +} + +/// Persistable acquisition observation linked to one complete canonical RAW account state. +/// +/// Yellowstone-only metadata remains optional observation detail and never changes the canonical +/// account state itself. HTTP/WS acquisitions therefore use the same observation type without +/// inventing a `write_version`, transaction signature or startup flag. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawAccountObservation { + account: crate::RawAccountStateReference, + is_startup: std::option::Option, + observation_key: crate::RawObservationKey, + provenance: crate::RawAcquisitionProvenance, + transaction_signature: std::option::Option, + write_version: std::option::Option, +} + +impl RawAccountObservation { + /// Creates one successful observation of a complete canonical RAW account state. + #[must_use] + pub fn new(observation_key: crate::RawObservationKey, account: crate::RawAccountStateReference, provenance: crate::RawAcquisitionProvenance) -> Self { + return Self { + account, + is_startup: std::option::Option::None, + observation_key, + provenance, + transaction_signature: std::option::Option::None, + write_version: std::option::Option::None, + }; + } + + /// Attaches a provider-reported startup/replay marker when the source exposes one. + #[must_use] + pub fn with_is_startup(mut self, value: bool) -> Self { + self.is_startup = std::option::Option::Some(value); + return self; + } + + /// Attaches the transaction signature associated with the account write when exposed by the source. + #[must_use] + pub fn with_transaction_signature(mut self, value: crate::RawTransactionSignature) -> Self { + self.transaction_signature = std::option::Option::Some(value); + return self; + } + + /// Attaches the source-specific account write version when the source exposes one. + #[must_use] + pub fn with_write_version(mut self, value: u64) -> Self { + self.write_version = std::option::Option::Some(value); + return self; + } + + /// Returns the durable account-state identity observed by this acquisition. + #[must_use] + pub fn account(&self) -> &crate::RawAccountStateReference { + return &self.account; + } + + /// Returns the optional source-reported startup/replay marker. + #[must_use] + pub const fn is_startup(&self) -> std::option::Option { + return self.is_startup; + } + + /// 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 optional transaction signature associated with this account write. + #[must_use] + pub const fn transaction_signature(&self) -> std::option::Option { + return self.transaction_signature; + } + + /// Returns the optional source-specific account write version. + #[must_use] + pub const fn write_version(&self) -> std::option::Option { + return self.write_version; + } +} + +#[cfg(test)] +#[path = "../../unit_tests/model/raw_account.rs"] +mod tests; diff --git a/crates/ksp-store-api/src/model/raw_primitives.rs b/crates/ksp-store-api/src/model/raw_primitives.rs index 7258333..eb18b3a 100644 --- a/crates/ksp-store-api/src/model/raw_primitives.rs +++ b/crates/ksp-store-api/src/model/raw_primitives.rs @@ -1,6 +1,10 @@ // file: crates/ksp-store-api/src/model/raw_primitives.rs -// version: 1 +// version: 2 +/// Maximum complete RAW account-data length admitted by the Store API. +/// +/// This is a Store admission guard, not a Solana protocol-size claim. +pub const MAX_RAW_ACCOUNT_DATA_BYTES: usize = 16 * 1024 * 1024; /// 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. diff --git a/crates/ksp-store-api/tests/dependency_boundary.rs b/crates/ksp-store-api/tests/dependency_boundary.rs index 70bd0bc..40745b8 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: 2 +// version: 3 //! Dependency canaries for the Store API RAW foundation. #[test] -fn pre_003_manifest_keeps_exact_core_only_runtime_dependency() { +fn pre_004_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,17 +49,19 @@ fn pre_003_manifest_keeps_exact_core_only_runtime_dependency() { } #[test] -fn pre_003_source_boundary_keeps_raw_models_passive_and_backend_free() { +fn pre_004_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_account = include_str!("../src/model/raw_account.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!(model_home.contains("raw_account")); assert!(model_home.contains("raw_primitives")); assert!(model_home.contains("raw_transaction")); - for source in [crate_root, model_home, raw_primitives, raw_transaction] { + for source in [crate_root, model_home, raw_account, raw_primitives, raw_transaction] { for forbidden in [ "ksp_store_lib", "ksp_store_postgres_lib", @@ -77,6 +79,9 @@ fn pre_003_source_boundary_keeps_raw_models_passive_and_backend_free() { } } assert!(!raw_transaction.contains("RawLog")); + for forbidden in ["TransactionStatusObservation", "RawLogNotification", "RawSlotEvent", "RawVoteEvent", "RawBlock", "YellowstoneEntry"] { + assert!(!crate_root.contains(forbidden), "deferred pre.004 model leaked into Store API surface: {forbidden}"); + } return; } diff --git a/crates/ksp-store-api/tests/public_api.rs b/crates/ksp-store-api/tests/public_api.rs index e5682c3..17aabe2 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: 2 +// version: 3 //! Integration canaries for the public `ksp-store-api` surface. @@ -80,3 +80,48 @@ fn public_pre_003_surface_keeps_backend_and_structural_types_out() { } return; } + +#[test] +fn public_pre_004_raw_account_state_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::RawAccountStateReference::new( + network, + ksp_store_api::Pubkey::new_from_array([21_u8; 32]), + 55, + ksp_store_api::RawContentHash::new([22_u8; 32]), + ); + let state = ksp_store_api::RawAccountState::try_new( + reference.clone(), + 123, + ksp_store_api::Pubkey::new_from_array([23_u8; 32]), + false, + 9, + vec![1_u8, 2_u8].into_boxed_slice(), + ); + assert!(state.is_ok()); + let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(2_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("getAccountInfo") { + 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::RawAccountObservation::new(ksp_store_api::RawObservationKey::new([24_u8; 32]), reference, provenance); + assert_eq!(observation.account().slot(), 55); + assert!(observation.write_version().is_none()); + assert_eq!(ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES, 16 * 1024 * 1024); + return; +} diff --git a/crates/ksp-store-api/unit_tests/model/raw_account.rs b/crates/ksp-store-api/unit_tests/model/raw_account.rs new file mode 100644 index 0000000..95e2c19 --- /dev/null +++ b/crates/ksp-store-api/unit_tests/model/raw_account.rs @@ -0,0 +1,108 @@ +// file: crates/ksp-store-api/unit_tests/model/raw_account.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 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("accounts".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::Live, received_at)); +} + +#[test] +fn raw_account_state_preserves_complete_common_fields_and_redacts_data_debug() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let pubkey = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]); + let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]); + let reference = crate::RawAccountStateReference::new(network, pubkey, 42, crate::RawContentHash::new([3_u8; 32])); + let data = b"ACCOUNT_DATA_SENTINEL_NEVER_RENDER".to_vec().into_boxed_slice(); + let state_result = crate::RawAccountState::try_new(reference.clone(), 500, owner, false, 7, data); + assert!(state_result.is_ok()); + let state = match state_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(state.reference(), &reference); + assert_eq!(state.lamports(), 500); + assert!(!state.executable()); + assert_eq!(state.rent_epoch(), 7); + assert_eq!(state.data(), b"ACCOUNT_DATA_SENTINEL_NEVER_RENDER"); + let debug = format!("{state:?}"); + assert!(!debug.contains("ACCOUNT_DATA_SENTINEL_NEVER_RENDER")); + assert!(debug.contains("data_len")); + return; +} + +#[test] +fn raw_account_state_rejects_only_oversized_data_and_allows_empty_accounts() { + let first_network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let first_reference = + crate::RawAccountStateReference::new(first_network, ksp_core_lib::Pubkey::new_from_array([4_u8; 32]), 1, crate::RawContentHash::new([5_u8; 32])); + let empty = crate::RawAccountState::try_new( + first_reference, + 0, + ksp_core_lib::Pubkey::new_from_array([6_u8; 32]), + false, + 0, + std::vec::Vec::new().into_boxed_slice(), + ); + assert!(empty.is_ok()); + let second_network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let second_reference = + crate::RawAccountStateReference::new(second_network, ksp_core_lib::Pubkey::new_from_array([7_u8; 32]), 2, crate::RawContentHash::new([8_u8; 32])); + let oversized = vec![0_u8; crate::MAX_RAW_ACCOUNT_DATA_BYTES + 1].into_boxed_slice(); + let rejected = crate::RawAccountState::try_new(second_reference, 0, ksp_core_lib::Pubkey::new_from_array([9_u8; 32]), false, 0, oversized); + assert!(rejected.is_err()); + return; +} + +#[test] +fn raw_account_observation_keeps_yellowstone_specific_metadata_optional() { + let network = match network() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let reference = + crate::RawAccountStateReference::new(network, ksp_core_lib::Pubkey::new_from_array([10_u8; 32]), 99, crate::RawContentHash::new([11_u8; 32])); + let provenance = match provenance() { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let observation = crate::RawAccountObservation::new(crate::RawObservationKey::new([12_u8; 32]), reference.clone(), provenance) + .with_write_version(17) + .with_transaction_signature(crate::RawTransactionSignature::new([13_u8; 64])) + .with_is_startup(false); + assert_eq!(observation.account(), &reference); + assert_eq!(observation.write_version(), std::option::Option::Some(17)); + assert_eq!(observation.is_startup(), std::option::Option::Some(false)); + assert_eq!(observation.transaction_signature(), std::option::Option::Some(crate::RawTransactionSignature::new([13_u8; 64]))); + return; +} diff --git a/deltas/0.3.1/pre.004.md b/deltas/0.3.1/pre.004.md new file mode 100644 index 0000000..74d74b4 --- /dev/null +++ b/deltas/0.3.1/pre.004.md @@ -0,0 +1,252 @@ + + + +# Delta `0.3.1-pre.004` — admission cross-source + account state N1 + +## Base requise + +```text +0.3.1-pre.3-fix.1 +``` + +Le gate opérateur de `pre.003-fix.001` est fourni vert : `cargo fmt --all`, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace et `cargo test -p ksp-store-api` passent. + +## Objectif + +Auditer les formes HTTP/WS/gRPC déjà possédées par `ksp-onchain-transport-lib` avant d'ajouter une nouvelle famille N1, puis matérialiser uniquement le modèle dont la sémantique commune est démontrée. + +La tranche : + +- admet `RawAccountState`/`RawAccountObservation` avec bytes complets + slot durable ; +- sépare les enrichissements Yellowstone de l'état canonique commun ; +- refuse conceptuellement les réponses account partielles/parsées comme états persistants ; +- diffère `TransactionStatusObservation` parce que snapshot HTTP, transition WS et update Yellowstone ne représentent pas encore le même fait ; +- ferme la classification logs/slot/vote/block/Entry sans créer de modèles Store prématurés. + +## Version + +Le workspace passe à : + +```text +0.3.1-pre.4 +``` + +## Audit account cross-source + +Surfaces KSP relues : + +```text +HTTP + getAccountInfo + getMultipleAccounts + getProgramAccounts + +WebSocket + accountSubscribe + programSubscribe + Helius standard account/program reuse + +Yellowstone gRPC + Account / AccountInfo +``` + +Champs communs retenus pour un état complet : + +```text +network +pubkey +slot +lamports +owner +executable +rent_epoch +complete account data bytes +canonical state hash +``` + +Règles d'admission : + +```text +getAccountInfo/getMultipleAccounts + -> admissibles avec account non-null + bytes complets + +getProgramAccounts/programSubscribe + -> slot/context obligatoire + +accountSubscribe + -> admissible avec bytes complets + +Yellowstone Account + -> admissible seulement sans accounts_data_slice tronquant les bytes + +jsonParsed/dataSlice/bare program notification + -X-> RawAccountState persistant +``` + +`space` n'est pas une vérité stockée séparément lorsque les bytes complets sont présents : `data.len()` est déterministe. + +## Modèles ajoutés + +```text +RawAccountStateReference + network + pubkey + slot + state_hash + +RawAccountState + reference + lamports + owner + executable + rent_epoch + complete data bytes + +RawAccountObservation + observation_key + account reference + provenance + optional write_version + optional transaction_signature + optional is_startup +``` + +La référence inclut `state_hash` parce que plusieurs écritures d'une account peuvent survenir dans le même slot alors que HTTP/WS standards ne possèdent pas le `write_version` Yellowstone. Plusieurs sources observant le même état complet peuvent donc converger sans faire de l'ordinal Yellowstone un identifiant commun. + +Le calcul du digest reste producer/converter-owned. `ksp-store-api` n'ajoute aucun codec ni algorithme de hash. + +## Borne account data + +Nouvelle admission guard Store-owned : + +```text +MAX_RAW_ACCOUNT_DATA_BYTES = 16 MiB +``` + +Les bytes vides restent valides pour une account vide. La borne n'est pas présentée comme une limite protocolaire Solana. + +`RawAccountState` n'implémente pas `Clone` et son `Debug` ne rend jamais les bytes. + +## Observation account + +Les informations présentes uniquement sur certaines sources restent observation-only : + +```text +Yellowstone write_version +Yellowstone transaction signature +Yellowstone is_startup +``` + +HTTP/WS utilisent le même `RawAccountObservation` sans inventer ces valeurs. + +Provider/protocol/method/endpoint/commitment/timing restent dans `RawAcquisitionProvenance` conformément à `pre.003`. + +## Transaction status différé + +Surfaces auditées : + +```text +getSignatureStatuses + = snapshot interrogé avec confirmations/confirmationStatus + +signatureSubscribe + = event one-shot de réception/commitment demandé + +Yellowstone TransactionStatus + = update avec slot/signature/is_vote/index/error, sans commitment commun +``` + +La tranche n'introduit donc aucun `TransactionStatusObservation` générique rempli d'options. La future conception devra distinguer snapshot durable et event realtime et auditer l'ownership `ksp-interface-lib` des événements passifs. + +## Classification fermée + +```text +logsSubscribe + -> event-only candidat ; pas de RawLog Store + +transaction logMessages + -> restent dans RawTransaction jusqu'à N2 STRUCTURAL + +slot/root/slotsUpdates + -> event-only candidat + +vote + -> event-only candidat après compatibilité utile + +getBlock/Yellowstone Block + -> conteneur d'acquisition RawTransaction ; RawBlock reste IDEA + +Yellowstone Entry + -> explicitement non retenu +``` + +Le Store runtime ne publie aucune notification ; workers/analyzers/runtime possèdent les futurs déclenchements. + +## Tests ajoutés/mis à jour + +Unitaires account : + +- état complet et champs communs ; +- account data vide accepté ; +- account data oversized rejeté ; +- `Debug` sans bytes ; +- metadata Yellowstone optionnelle sur observation. + +Canaris d'intégration : + +- surface crate-root `RawAccountState*`/`RawAccountObservation` ; +- dépendance runtime toujours exactement Core-only ; +- absence de Transport/backend/runtime/codec ; +- absence de `TransactionStatusObservation`, `RawLogNotification`, `RawSlotEvent`, `RawVoteEvent`, `RawBlock` et `YellowstoneEntry` publics. + +## Documentation mise à jour + +```text +docs/plans/022-V0_3_1_STORE_RAW_PLAN.md +docs/validation/018-V0_3_1_STORE_RAW.md +``` + +La matrice cross-source est désormais explicite et `pre.004` ne prétend pas que toutes les réponses on-chain constituent des modèles Store. + +## 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 +source converter HTTP/WS/gRPC concret +TransactionStatus model commun +logs/slot/vote event model +RawBlock persistence +Yellowstone Entry persistence +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.005` introduit les capabilities backend extensibles et object-safe pour les modèles persistants réellement matérialisés, sans façade runtime `Store`, sans backend PostgreSQL et sans obliger un backend à supporter toutes les familles N1. 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 7817033..11f23da 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 @@ -434,61 +434,123 @@ logsSubscribe Le Store n'envoie lui-même aucun événement. -### 6.4 `RawAccountState` et observation +### 6.4 `RawAccountState` et observation — matrice `pre.004` -Les réponses account complètes provenant de HTTP, WS ou Yellowstone peuvent potentiellement converger vers un même état canonique : +L'audit de la surface KSP réelle confirme qu'un **état complet de compte** possède une sémantique commune entre HTTP, WebSocket et Yellowstone lorsque la source fournit les bytes complets et un slot durable. Le Store ne conserve pas la forme d'encodage réseau : base58/base64/base64+zstd et protobuf doivent être décodés avant construction du modèle commun. + +Le modèle commun matérialisé est : ```text -pubkey -slot/context utile -lamports -owner -executable -rent_epoch -data bytes exacts +RawAccountStateReference + network + pubkey + slot + state_hash + +RawAccountState + reference + lamports + owner + executable + rent_epoch + complete data bytes + +RawAccountObservation + observation_key + account reference + provenance + optional write_version + optional transaction_signature + optional is_startup ``` -Les détails propres à l'acquisition (`write_version`, signature source, startup, provider, transport, timing, etc.) appartiennent à `RawAccountObservation` lorsqu'ils sont utiles et disponibles. +`state_hash` fait partie de la référence car une même account peut subir plusieurs écritures dans un slot, alors que les surfaces HTTP/WS standards n'exposent pas le `write_version` Yellowstone. Le digest permet à plusieurs sources observant **le même état complet** de converger sans promouvoir un ordinal provider-specific dans l'identité commune. Le producer/converter possède le calcul déterministe du digest ; Store API ne choisit pas l'algorithme de hash. -`RawAccountState`/`RawAccountObservation` doivent être **prévus dans `ksp-store-api`** après validation de la matrice de compatibilité. Leur persistence concrète PostgreSQL peut rester non implémentée au début de `0.3.2`. +La matrice d'admission courante est : -Un compte demandé sous une forme déjà interprétée par le provider ne doit pas remplacer arbitrairement les bytes canoniques nécessaires à un futur decoder. +| Source KSP | État complet commun | Slot durable | Admission `RawAccountState` | +|------------------------------------|---------------------|---------------|-------------------------------------------------------------------------------------| +| HTTP `getAccountInfo` | oui | oui | oui si account non-null, bytes complets et aucun `dataSlice`/`jsonParsed` | +| HTTP `getMultipleAccounts` | oui | oui | oui par position non-null si bytes complets ; pubkey reprise depuis la requête | +| HTTP `getProgramAccounts` | oui | conditionnel | oui seulement avec résultat contextualisé + bytes complets ; forme bare refusée | +| WS `accountSubscribe` | oui | oui | oui si bytes complets ; pubkey reprise depuis l'identité de subscription | +| WS `programSubscribe` | oui | conditionnel | oui seulement pour la forme contextualisée + bytes complets ; forme bare event-only | +| Helius standard account/program WS | oui | idem standard | mêmes règles que le wire Solana standard réutilisé | +| Yellowstone `Account` | oui | oui | oui si `accounts_data_slice` n'a pas tronqué les bytes | -### 6.5 Transaction status - -Les surfaces signature/status peuvent représenter un fait distinct d'une transaction complète : +Règles négatives : ```text -signatureSubscribe -getSignatureStatuses -Yellowstone TransactionStatus -provider equivalent +jsonParsed +request-side data slice +Yellowstone accounts_data_slice +program account sans context/slot +account absent/null + -X-> RawAccountState persistant incomplet ``` -Le candidat `TransactionStatusObservation` doit être étudié et, si la sémantique commune est démontrée, prévu dans l'API même si sa persistence n'est pas immédiatement implémentée. +`space` n'est pas conservé comme vérité indépendante : lorsqu'on possède les bytes complets, leur longueur est déterministe. Les enrichissements Yellowstone `write_version`, `txn_signature` et `is_startup` appartiennent à `RawAccountObservation`. Le timestamp serveur et les filters/capture ids restent de la provenance lorsque le converter peut les représenter sans perte utile. -Il peut plus tard servir à un worker/analyser pour détecter des transitions de commitment/status. Ce déclenchement n'est pas une responsabilité du Store. - -### 6.6 Slot, vote, block et Yellowstone Entry - -Classification actuelle : +La borne initiale Store-owned est : ```text +complete account data <= 16 MiB +``` + +Elle est un admission guard KSP, pas une affirmation sur la limite protocolaire Solana. + +### 6.5 Transaction status — convergence insuffisante pour un modèle Store unique + +L'audit `pre.004` conclut que les trois surfaces candidates ne représentent pas encore exactement le même fait : + +| Source | Sémantique principale | Conclusion `0.3.1` | +|-------------------------------|---------------------------------------------------------------------------|-----------------------------------------------------| +| HTTP `getSignatureStatuses` | snapshot interrogé : slot, confirmations, error, confirmation status | candidat snapshot durable, non figé | +| WS `signatureSubscribe` | event one-shot : received puis/ou commitment demandé atteint | event runtime, ownership Interface/worker à étudier | +| Yellowstone TransactionStatus | update d'exécution : slot, signature, vote, index, error, sans commitment | event/status provider-neutral potentiel, non figé | + +Créer maintenant un `TransactionStatusObservation` rempli d'options ferait perdre la distinction entre **snapshot interrogé**, **transition de commitment** et **update d'exécution**. Aucun modèle Store n'est donc ajouté en `pre.004`. + +TODO avant matérialisation : + +```text +séparer explicitement snapshot durable vs event realtime +étudier l'ownership ksp-interface-lib des events passifs +prouver la correspondance des états/commitments +prévoir un éventuel wake-up worker/analyser sans notification émise par Store +``` + +### 6.6 Logs, slot, vote, block et Yellowstone Entry — classification fermée `pre.004` + +Classification actuelle après audit : + +```text +logsSubscribe + -> event realtime passif distinct + -> signature + error + ordered log lines + context slot + -> pas de RawLog Store + -> ownership ksp-interface-lib/worker à préciser + slot/root/slotsUpdates - -> event-only candidat ; persistence non justifiée actuellement + -> event-only candidat + -> aucune persistence N1 démontrée vote - -> event-only candidat si la forme est suffisamment commune entre les ledgers/providers qui l'exposent + -> event-only candidat si la forme commune utile est prouvée + -> aucune persistence N1 par défaut getBlock / Yellowstone Block - -> source/conteneur d'acquisition de RawTransaction par défaut - RawBlock persistant seulement si un besoin block-level non reconstructible est démontré + -> source/conteneur d'acquisition de RawTransaction + -> RawBlock persistant reste IDEA uniquement Yellowstone Entry - -> examiné et non retenu actuellement : trop bas niveau et aucune destination replay/decomposition/event métier suffisante identifiée + -> transport-only + -> explicitement non retenu actuellement ``` -`RawBlock` reste une IDEA, pas un modèle actif. Recréer le ledger bloc par bloc sans besoin supplémentaire irait à l'encontre de l'objectif KSP de transformer la donnée blockchain en unités directement exploitables. +Les logs contenus dans `RawTransaction` restent distincts de `logsSubscribe` : les premiers sont de la matière replayable de transaction et seront extraits en N2 STRUCTURAL ; le second est un événement realtime léger pouvant éventuellement déclencher l'hydratation de la transaction complète. + +`RawBlock` ne doit être rouvert que si un besoin block-level non reconstructible apporte une valeur concrète au pipeline. Recréer le ledger bloc par bloc sans besoin supplémentaire irait à l'encontre de l'objectif KSP de produire des unités directement exploitables. ### 6.7 Modèles et capabilities sont indépendants @@ -702,9 +764,25 @@ La signature ne doit pas être représentée comme un identifiant SQL `i64` dans Chaque observation possède une clé d'idempotence déterministe fournie par le producer/composition selon un contrat documenté. Deux acquisitions légitimes distinctes peuvent donc être conservées même si elles pointent vers le même RAW. -### 10.4 Événements et autres familles +### 10.4 Account states -Les événements realtime non persistés n'ont pas d'identité Store à inventer. Lorsqu'une nouvelle famille persistante est admise (`RawAccountState`, status durable, etc.), son identité d'idempotence doit être définie avec le modèle concret et ne jamais dépendre d'une primary key backend. +La référence matérialisée par `pre.004` est : + +```text +RawAccountStateReference + network + pubkey + slot + canonical state hash +``` + +Le hash couvre conceptuellement l'état canonique complet et sert à distinguer/converger les écritures multiples possibles dans un même slot sans dépendre de `write_version`. Le Store API ne calcule pas ce digest et ne transforme pas les bytes de transport. + +Une `RawAccountObservation` possède sa propre `RawObservationKey`; plusieurs acquisitions HTTP/WS/gRPC peuvent donc viser la même référence d'état sans être fusionnées comme observations. + +### 10.5 Événements et autres familles + +Les événements realtime non persistés n'ont pas d'identité Store à inventer. Si une autre famille persistante est admise ultérieurement, son identité d'idempotence doit être définie avec le modèle concret et ne jamais dépendre d'une primary key backend. Aucune promesse exactly-once distribuée n'est faite. @@ -1293,7 +1371,7 @@ Introduire payload/reference/provenance/idempotence/timestamps bornés puis `Raw ### `pre.004` — Matrice cross-source + familles N1 prévues -Auditer HTTP/WS/gRPC pour `RawAccountState`/observation et `TransactionStatusObservation`; introduire les modèles communs seulement lorsque la sémantique converge. Classer explicitement logsSubscribe/slot/vote/block/Entry en event, IDEA ou rejet. +Audit HTTP/WS/gRPC matérialisé. `RawAccountState`/observation sont admis avec bytes complets + slot et enrichissements source-specific séparés. `TransactionStatusObservation` est différé car snapshot HTTP, transition WS et update Yellowstone ne convergent pas encore assez. `logsSubscribe`/slot/vote restent event-only candidats, block reste conteneur/IDEA et Entry reste rejeté. ### `pre.005` — Capabilities backend extensibles diff --git a/docs/validation/018-V0_3_1_STORE_RAW.md b/docs/validation/018-V0_3_1_STORE_RAW.md index 515d1c1..25989e0 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 @@ -16,7 +16,7 @@ contrats/capabilities backend externes cycle de rétention logique + tombstone ``` -`RawAccountState`/observation et `TransactionStatusObservation` doivent être audités cross-source et prévus dans l'API lorsque leur sémantique commune est prouvée. Les notifications de logs/slot/vote sont classées séparément comme candidats event-only, avec ownership Interface préféré lorsqu'elles ne sont pas persistées. +`pre.004` prouve la convergence de `RawAccountState`/observation entre HTTP/WS/gRPC sous admission stricte bytes complets + slot. `TransactionStatusObservation` reste différé : snapshot HTTP, transition WS et update Yellowstone ne sont pas encore un fait unique. Les notifications logs/slot/vote restent event-only candidates, avec ownership Interface préféré lorsqu'elles ne sont pas persistées. ## 2. Gate `pre.001` @@ -118,8 +118,8 @@ Toute divergence exige un usage public réel et une révision du plan. |--------------------------------|--------------------------|-------------------|----------------|--------------------------------------------------------------------| | transaction complète | N1 RAW | oui | STRUCTURAL oui | implémenter modèle + observation | | logs contenus dans transaction | partie de RawTransaction | via transaction | STRUCTURAL oui | préserver lossless, ne pas dupliquer en `RawLog` | -| account state complet | N1 RAW candidat | futur oui | à déterminer | audit HTTP/WS/gRPC puis prévoir modèle/observation | -| transaction status | observation candidat | optionnelle | non | audit signature/status sources | +| account state complet | N1 RAW | oui à terme | pas démontré | modèle + observation matérialisés en `pre.004` | +| transaction status | familles distinctes | non figée | non | différer : snapshot HTTP != transition WS != update Yellowstone | | `logsSubscribe` notification | event-only candidat | non par défaut | non | ownership Interface/worker à figer | | slot/root/slotsUpdates | event-only candidat | non | non | TODO use-case + compatibilité | | vote | event-only candidat | non | non | TODO seulement si forme commune utile | @@ -169,26 +169,30 @@ crate/test backend externe ## 8. Threat/API gates futurs -| Gate | Attendu | Statut initial | -|----------------------------------|-------------------------------------------------|----------------| -| oversized RAW payload | rejet avant allocation pathologique | `pre.003` | -| payload Debug | aucun bytes brut | `pre.003` | -| partial source -> RawTransaction | interdit ; contrat complet exigé | `pre.003/004` | -| HTTP/WS/gRPC semantic mismatch | détecté par admission matrix | `pre.004` | -| duplicate same content | `AlreadyPresent` | `pre.006` | -| duplicate divergent content | Conflict stable | `pre.006` | -| partial transaction+observation | interdit par atomic acquisition | `pre.005` | -| event-only -> Store capability | absent par défaut | `pre.004/007` | -| Interface/Store duplicate model | absent | `pre.007` | -| page limit 0/>500 | rejet | `pre.006` | -| SQL/backend cursor leak | absent | `pre.006/007` | -| external backend | implémente API sans Store lib | `pre.005` | -| processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` | -| purge sans policy/evidence | impossible par contrat | `pre.006/007` | -| tombstone supprimé avec payload | interdit | `pre.006` | -| rebackfill normal après purge | skip | `pre.006` | -| force rehydrate implicite | interdit | `pre.006` | -| N2/N3/N4 creep | aucune surface | `pre.007` | +| Gate | Attendu | Statut initial | +|---------------------------------------------|-------------------------------------------------|----------------| +| oversized RAW payload | rejet avant allocation pathologique | `pre.003` | +| payload Debug | aucun bytes brut | `pre.003` | +| partial source -> RawTransaction | interdit ; contrat complet exigé | `pre.003/004` | +| HTTP/WS/gRPC semantic mismatch | détecté par admission matrix | `pre.004` PASS | +| account bytes partiels/parsés | refusés avant `RawAccountState` | `pre.004` PASS | +| account sans slot durable | refusé comme état persistant | `pre.004` PASS | +| account data Debug | aucun bytes brut | `pre.004` PASS | +| status surfaces artificiellement fusionnées | aucun modèle commun prématuré | `pre.004` PASS | +| duplicate same content | `AlreadyPresent` | `pre.006` | +| duplicate divergent content | Conflict stable | `pre.006` | +| partial transaction+observation | interdit par atomic acquisition | `pre.005` | +| event-only -> Store capability | absent par défaut | `pre.004/007` | +| Interface/Store duplicate model | absent | `pre.007` | +| page limit 0/>500 | rejet | `pre.006` | +| SQL/backend cursor leak | absent | `pre.006/007` | +| external backend | implémente API sans Store lib | `pre.005` | +| processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` | +| purge sans policy/evidence | impossible par contrat | `pre.006/007` | +| tombstone supprimé avec payload | interdit | `pre.006` | +| rebackfill normal après purge | skip | `pre.006` | +| force rehydrate implicite | interdit | `pre.006` | +| N2/N3/N4 creep | aucune surface | `pre.007` | ### 8.1 Matérialisation `pre.003` @@ -227,6 +231,43 @@ 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é. +### 8.2 Matérialisation `pre.004` + +La tranche ajoute : + +```text +MAX_RAW_ACCOUNT_DATA_BYTES +RawAccountStateReference +RawAccountState +RawAccountObservation +``` + +Invariants vérifiés par modèle/canaris : + +```text +référence = network + pubkey + slot + canonical state hash +account data complet <= 16 MiB +empty account data autorisé +Debug RawAccountState ne rend jamais les bytes +write_version/transaction_signature/is_startup restent observation-only optionnels +HTTP/WS/gRPC source details n'entrent pas dans RawAccountState +aucun TransactionStatusObservation artificiel +aucun RawLogNotification/RawSlotEvent/RawVoteEvent/RawBlock/YellowstoneEntry public +``` + +Matrice d'admission validée architecturalement : + +```text +getAccountInfo/getMultipleAccounts -> oui avec bytes complets +getProgramAccounts -> contexte obligatoire +accountSubscribe -> oui avec bytes complets +programSubscribe -> contexte obligatoire +Yellowstone Account -> aucun accounts_data_slice +jsonParsed/dataSlice/bare program -> non +``` + +La conversion source -> modèle reste hors `ksp-store-api`; la crate ne dépend toujours que de `ksp-core-lib`. + ## 9. Gates de fermeture prévus ### Gate technique final `pre.008` @@ -317,7 +358,7 @@ sécurité | `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.004` | admission matrix + account/status models | À FAIRE | +| `pre.004` | admission matrix + account/status models | PRÊT après gate local | | `pre.005` | backend contracts/capabilities | À FAIRE | | `pre.006` | queries/outcomes/retention/tombstone | À FAIRE | | `pre.007` | boundary/adversarial/completeness | À FAIRE |