v0.3.1-pre.004

This commit is contained in:
2026-08-29 08:20:16 +02:00
parent afd4c770f9
commit 28ca5bdac5
11 changed files with 843 additions and 70 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 325 # version: 326
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.1-pre.3-fix.1" version = "0.3.1-pre.4"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/lib.rs // file: crates/ksp-store-api/src/lib.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![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; pub use self::error::ERROR_CODE_RAW_PAYLOAD_INVALID;
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent. /// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
pub use self::error::ERROR_CODE_RAW_PROVENANCE_INVALID; 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. /// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
pub use self::model::raw_primitives::MAX_RAW_CODE_BYTES; pub use self::model::raw_primitives::MAX_RAW_CODE_BYTES;
/// Maximum KSP-owned canonical RAW payload admitted by the Store API. /// Maximum KSP-owned canonical RAW payload admitted by the Store API.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/src/model.rs // file: crates/ksp-store-api/src/model.rs
// version: 2 // version: 3
//! Private home for persistent Store models. //! Private home for persistent Store models.
//! //!
@@ -8,5 +8,6 @@
//! backend capabilities remain separate even when one capability operates on //! backend capabilities remain separate even when one capability operates on
//! one or more models. //! one or more models.
pub(crate) mod raw_account;
pub(crate) mod raw_primitives; pub(crate) mod raw_primitives;
pub(crate) mod raw_transaction; pub(crate) mod raw_transaction;

View File

@@ -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<Self> {
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<bool>,
observation_key: crate::RawObservationKey,
provenance: crate::RawAcquisitionProvenance,
transaction_signature: std::option::Option<crate::RawTransactionSignature>,
write_version: std::option::Option<u64>,
}
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<bool> {
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<crate::RawTransactionSignature> {
return self.transaction_signature;
}
/// Returns the optional source-specific account write version.
#[must_use]
pub const fn write_version(&self) -> std::option::Option<u64> {
return self.write_version;
}
}
#[cfg(test)]
#[path = "../../unit_tests/model/raw_account.rs"]
mod tests;

View File

@@ -1,6 +1,10 @@
// file: crates/ksp-store-api/src/model/raw_primitives.rs // 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. /// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
pub const MAX_RAW_CODE_BYTES: usize = 128; pub const MAX_RAW_CODE_BYTES: usize = 128;
/// Maximum KSP-owned canonical RAW payload admitted by the Store API. /// Maximum KSP-owned canonical RAW payload admitted by the Store API.

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-store-api/tests/dependency_boundary.rs // file: crates/ksp-store-api/tests/dependency_boundary.rs
// version: 2 // version: 3
//! Dependency canaries for the Store API RAW foundation. //! Dependency canaries for the Store API RAW foundation.
#[test] #[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 manifest = include_str!("../Cargo.toml");
let dependencies_tail = manifest.split("[dependencies]").nth(1); let dependencies_tail = manifest.split("[dependencies]").nth(1);
assert!(dependencies_tail.is_some(), "Store API dependencies section must exist"); 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] #[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 crate_root = include_str!("../src/lib.rs");
let model_home = include_str!("../src/model.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_primitives = include_str!("../src/model/raw_primitives.rs");
let raw_transaction = include_str!("../src/model/raw_transaction.rs"); let raw_transaction = include_str!("../src/model/raw_transaction.rs");
assert!(crate_root.contains("mod capability;")); assert!(crate_root.contains("mod capability;"));
assert!(crate_root.contains("mod error;")); assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("mod model;")); assert!(crate_root.contains("mod model;"));
assert!(model_home.contains("raw_account"));
assert!(model_home.contains("raw_primitives")); assert!(model_home.contains("raw_primitives"));
assert!(model_home.contains("raw_transaction")); 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 [ for forbidden in [
"ksp_store_lib", "ksp_store_lib",
"ksp_store_postgres_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")); 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; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-api/tests/public_api.rs // file: crates/ksp-store-api/tests/public_api.rs
// version: 2 // version: 3
//! Integration canaries for the public `ksp-store-api` surface. //! 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; 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;
}

View File

@@ -0,0 +1,108 @@
// file: crates/ksp-store-api/unit_tests/model/raw_account.rs
// version: 1
fn network() -> std::option::Option<crate::RawNetworkId> {
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<crate::RawAcquisitionProvenance> {
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;
}

252
deltas/0.3.1/pre.004.md Normal file
View File

@@ -0,0 +1,252 @@
<!-- file: deltas/0.3.1/pre.004.md -->
<!-- version: 1 -->
# 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.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/022-V0_3_1_STORE_RAW_PLAN.md --> <!-- file: docs/plans/022-V0_3_1_STORE_RAW_PLAN.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Plan `0.3.1` — Store API RAW foundation # Plan `0.3.1` — Store API RAW foundation
@@ -434,61 +434,123 @@ logsSubscribe
Le Store n'envoie lui-même aucun événement. 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 ```text
pubkey RawAccountStateReference
slot/context utile network
lamports pubkey
owner slot
executable state_hash
rent_epoch
data bytes exacts 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 Règles négatives :
Les surfaces signature/status peuvent représenter un fait distinct d'une transaction complète :
```text ```text
signatureSubscribe jsonParsed
getSignatureStatuses request-side data slice
Yellowstone TransactionStatus Yellowstone accounts_data_slice
provider equivalent 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. La borne initiale Store-owned est :
### 6.6 Slot, vote, block et Yellowstone Entry
Classification actuelle :
```text ```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 slot/root/slotsUpdates
-> event-only candidat ; persistence non justifiée actuellement -> event-only candidat
-> aucune persistence N1 démontrée
vote 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 getBlock / Yellowstone Block
-> source/conteneur d'acquisition de RawTransaction par défaut -> source/conteneur d'acquisition de RawTransaction
RawBlock persistant seulement si un besoin block-level non reconstructible est démontré -> RawBlock persistant reste IDEA uniquement
Yellowstone Entry 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 ### 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. 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. 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 ### `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 ### `pre.005` — Capabilities backend extensibles

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/018-V0_3_1_STORE_RAW.md --> <!-- file: docs/validation/018-V0_3_1_STORE_RAW.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Validation `0.3.1` — Store API RAW foundation # Validation `0.3.1` — Store API RAW foundation
@@ -16,7 +16,7 @@ contrats/capabilities backend externes
cycle de rétention logique + tombstone 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` ## 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 | | 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` | | 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 | à terminer | audit HTTP/WS/gRPC puis prévoir modèle/observation | | account state complet | N1 RAW | oui à terme | pas démontré | modèle + observation matérialisés en `pre.004` |
| transaction status | observation candidat | optionnelle | non | audit signature/status sources | | 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 | | `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é | | slot/root/slotsUpdates | event-only candidat | non | non | TODO use-case + compatibilité |
| vote | event-only candidat | non | non | TODO seulement si forme commune utile | | 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 ## 8. Threat/API gates futurs
| Gate | Attendu | Statut initial | | Gate | Attendu | Statut initial |
|----------------------------------|-------------------------------------------------|----------------| |---------------------------------------------|-------------------------------------------------|----------------|
| oversized RAW payload | rejet avant allocation pathologique | `pre.003` | | oversized RAW payload | rejet avant allocation pathologique | `pre.003` |
| payload Debug | aucun bytes brut | `pre.003` | | payload Debug | aucun bytes brut | `pre.003` |
| partial source -> RawTransaction | interdit ; contrat complet exigé | `pre.003/004` | | partial source -> RawTransaction | interdit ; contrat complet exigé | `pre.003/004` |
| HTTP/WS/gRPC semantic mismatch | détecté par admission matrix | `pre.004` | | HTTP/WS/gRPC semantic mismatch | détecté par admission matrix | `pre.004` PASS |
| duplicate same content | `AlreadyPresent` | `pre.006` | | account bytes partiels/parsés | refusés avant `RawAccountState` | `pre.004` PASS |
| duplicate divergent content | Conflict stable | `pre.006` | | account sans slot durable | refusé comme état persistant | `pre.004` PASS |
| partial transaction+observation | interdit par atomic acquisition | `pre.005` | | account data Debug | aucun bytes brut | `pre.004` PASS |
| event-only -> Store capability | absent par défaut | `pre.004/007` | | status surfaces artificiellement fusionnées | aucun modèle commun prématuré | `pre.004` PASS |
| Interface/Store duplicate model | absent | `pre.007` | | duplicate same content | `AlreadyPresent` | `pre.006` |
| page limit 0/>500 | rejet | `pre.006` | | duplicate divergent content | Conflict stable | `pre.006` |
| SQL/backend cursor leak | absent | `pre.006/007` | | partial transaction+observation | interdit par atomic acquisition | `pre.005` |
| external backend | implémente API sans Store lib | `pre.005` | | event-only -> Store capability | absent par défaut | `pre.004/007` |
| processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` | | Interface/Store duplicate model | absent | `pre.007` |
| purge sans policy/evidence | impossible par contrat | `pre.006/007` | | page limit 0/>500 | rejet | `pre.006` |
| tombstone supprimé avec payload | interdit | `pre.006` | | SQL/backend cursor leak | absent | `pre.006/007` |
| rebackfill normal après purge | skip | `pre.006` | | external backend | implémente API sans Store lib | `pre.005` |
| force rehydrate implicite | interdit | `pre.006` | | processing `bool` comme vérité | absent ; future preuve version-aware documentée | `pre.006/007` |
| N2/N3/N4 creep | aucune surface | `pre.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` ### 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é. 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 ## 9. Gates de fermeture prévus
### Gate technique final `pre.008` ### Gate technique final `pre.008`
@@ -317,7 +358,7 @@ sécurité
| `pre.001` | audit/design/taxonomie/split | PRÊT après gate local | | `pre.001` | audit/design/taxonomie/split | PRÊT après gate local |
| `pre.002` | scaffold + taxonomie Store API | À FAIRE | | `pre.002` | scaffold + taxonomie Store API | À FAIRE |
| `pre.003` | primitives + RawTransaction | À 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.005` | backend contracts/capabilities | À FAIRE |
| `pre.006` | queries/outcomes/retention/tombstone | À FAIRE | | `pre.006` | queries/outcomes/retention/tombstone | À FAIRE |
| `pre.007` | boundary/adversarial/completeness | À FAIRE | | `pre.007` | boundary/adversarial/completeness | À FAIRE |