20 Commits

Author SHA1 Message Date
c7cfb9557d v0.3.4-rel.001 2026-08-31 00:52:56 +02:00
c27428d6c0 v0.3.4-pre.013 2026-08-31 00:46:52 +02:00
dfa01b1381 v0.3.4-pre.012 2026-08-31 00:33:51 +02:00
5fc7f65b2c v0.3.4-pre.011 2026-08-30 23:56:11 +02:00
bbf804b7fc v0.3.4-pre.010 2026-08-30 23:46:37 +02:00
13687ec2fe v0.3.4-pre.009 2026-08-30 23:39:43 +02:00
b756da5149 v0.3.4-pre.008-fix.001 2026-08-30 23:25:12 +02:00
04234926ce v0.3.4-pre.008 2026-08-30 23:15:48 +02:00
bfc69f5631 v0.3.4-pre.007-fix.001 2026-08-30 22:54:53 +02:00
902b5fba99 v0.3.4-pre.007 2026-08-30 22:47:33 +02:00
fccb7d876c v0.3.4-pre.006 2026-08-30 22:31:02 +02:00
50d4142797 v0.3.4-pre.005 2026-08-30 22:17:47 +02:00
eb6dbc31e8 v0.3.4-pre.004-fix.001 2026-08-30 22:06:24 +02:00
fb8b585aa2 v0.3.4-pre.004 2026-08-30 21:57:23 +02:00
dad18c16d5 v0.3.4-pre.003-fix.001 2026-08-30 21:45:30 +02:00
a704a5722e v0.3.4-pre.003 2026-08-30 21:37:02 +02:00
dfa3723a7f v0.3.4-pre.002-fix.001 2026-08-30 21:17:38 +02:00
6bd593f467 v0.3.4-pre.002 2026-08-30 21:11:23 +02:00
03ad0ba063 v0.3.4-pre.001-fix.001 2026-08-30 19:29:05 +02:00
d95b1095e5 v0.3.4-pre.001 2026-08-30 19:21:15 +02:00
82 changed files with 11286 additions and 171 deletions

View File

@@ -1,8 +1,20 @@
<!-- file: CHANGELOG.md -->
<!-- version: 22 -->
<!-- version: 23 -->
# Changelog KSP
## 0.3.4 — Store/PostgreSQL RawAccountState + complétude RAW — 2026-08-31
`0.3.4` complète la seconde vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` et ferme la conformance PostgreSQL des **10 capabilities** backend-agnostic de `ksp-store-api` : les six capabilities `RawTransaction*` acquises en `0.3.3` restent intactes et les quatre capabilities `RawAccountStateRead`, `RawAccountStateWrite`, `RawAccountObservationRead` et `RawAccountObservationWrite` sont désormais implémentées par `PostgresBackend` puis dispatchées par la façade `Store`. La séparation reste stricte : les consommateurs ordinaires passent par `ksp-store-lib`, le backend PostgreSQL conserve SQL/driver/pool/TLS/migrations privés, et la façade reste compilable/testable sans backend via `--no-default-features`.
La migration additive V002 introduit `ksp_raw_account_states` et `ksp_raw_account_observations` au-dessus de V000/V001 sans modifier leurs bytes. L'identité canonique account est `(pubkey, slot, state_hash)` : plusieurs états d'un même compte dans un même slot restent représentables lorsque `state_hash` diffère, tandis qu'une collision sur la référence complète déclenche une comparaison exacte de `lamports`, `owner`, `executable`, `rent_epoch` et `data` avant de conclure à l'idempotence ou à `store_api.raw_conflict`. Les `u64` physiques utilisent `NUMERIC(20,0)`, les clés/hashes/signatures utilisent des `BYTEA` de largeur contrainte, et les bytes account restent complets jusqu'à la borne KSP de 16 MiB. V002 contient exactement 32 ressources gérées et son checksum final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`; V000 et V001 restent respectivement `d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450` et `31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51`.
L'acquisition `RawAccountState + RawAccountObservation` est transactionnelle : `INSERT ... ON CONFLICT DO NOTHING`, lecture/verrouillage du canonical en collision, comparaison exacte et rollback complet lorsque l'observation diverge. Une observation supplémentaire vérifie la référence existante sous transaction et ne crée jamais implicitement son state. Les métadonnées Yellowstone `is_startup`, `transaction_signature` et `write_version` restent optionnelles et observation-only ; aucune FK transaction n'est inventée. La navigation account utilise une keyset `(slot, pubkey, state_hash)` ASC/DESC, avec filtre pubkey optionnel et cursor KSPA V1 opaque de 109 octets lié au réseau, au filtre, au range, à la direction et à la famille afin d'empêcher les replays cross-query/cross-family. Aucun `OFFSET`, plafond métier de batch, index owner/provider/time ou lifecycle destructif account n'est introduit.
Les gates de clôture valident audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config, `cargo test --workspace`, façade sans feature PostgreSQL et graphes Cargo. Le live `postgres_raw_account_live` passe sur **PostgreSQL 17** avec bootstrap V000/V001/V002, round-trip des bytes et du domaine `u64`, idempotence/conflit, états distincts même pubkey+slot, observations/metadata optionnelles, pagination/cursors, concurrence, annulation/rollback et coexistence `RawTransaction`. La réconciliation documentaire finale est ensuite validée sans réouvrir code ni migrations.
`prompts/024-V0_3_5_START_PROMPT.md` ouvre `0.3.5` sur `ksp-interface-lib` uniquement. Cette release doit réauditer les surfaces d'acquisition actuelles et matérialiser seulement les modèles passifs/event-only réellement partagés, sans recopier `RawTransaction`/`RawAccountState`, sans créer un event bus et sans déplacer les DTOs provider/transport. Les candidats logs, slot/root/slotsUpdates, transaction status et vote sont traités par matrice sémantique ; une famille reste reportée si la convergence ou le consumer réel n'est pas démontré. L'archive historique kbot3 n'est pas requise pour ce gate : les sources de vérité sont la base KSP stable et les contrats officiels actuels des transports concernés.
## 0.3.3 — Store/PostgreSQL RawTransaction vertical slice — 2026-08-30
`0.3.3` complète la première vertical slice RAW physique sur le couple `ksp-store-lib` / `ksp-store-postgres-lib` sans modifier les contrats backend-agnostic acquis dans `ksp-store-api`. `PostgresBackend` et la façade `Store` implémentent désormais les six capabilities `RawTransactionRead`, `RawTransactionWrite`, `RawTransactionObservationRead`, `RawTransactionObservationWrite`, `RawTransactionRetentionRead` et `RawTransactionRetentionWrite`. Une base PostgreSQL reste liée à un unique `RawNetworkId` par `ksp_store_identity`; le mauvais réseau est refusé avant I/O, les slots `u64` sont conservés exactement en `NUMERIC(20,0)`, et la migration logique V001 reste découpée en ressources tables/contraintes/indexes avec vérification de compatibilité du schéma effectif.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 363
# version: 382
[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-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.3.3"
version = "0.3.4"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 97 -->
<!-- version: 98 -->
# Roadmap KSP
@@ -96,8 +96,8 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
- [X] `0.3.1``ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
- [X] `0.3.2``ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
- [ ] `0.3.4`Étendre le même couple avec `RawAccountState` + `RawAccountObservation` : quatre capabilities account, migration additive au-dessus de V000/V001, acquisition state+observation atomique, idempotence/conflit, get/list cursorisé, puis complétude des dix capabilities RAW, indexes justifiés par les queries et hardening PostgreSQL cross-family final.
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/events réellement partagés par les premiers consumers dacquisition, sans dupliquer les modèles persistants de `ksp-store-api`.
- [X] `0.3.4`Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
- [ ] `0.3.5` — Étendre `ksp-interface-lib` uniquement avec les modèles passifs/event-only dont une matrice des surfaces HTTP/WS/Yellowstone/Helius démontre la sémantique réellement partagée et le besoin consumer ; auditer notamment logs, slot/root/slotsUpdates, transaction status et vote, sans dupliquer `RawTransaction`/`RawAccountState`, sans event bus et sans dépendance Interface vers Transport/Store.
- [ ] `0.3.6` — Introduire `ksp-job-api` et un premier job de backfill historique concret consommant `ksp-store-lib`, avec policy/batch-size/progression possédés par le job et non par Store.
- [ ] `0.3.7` — Introduire une application spécialisée de backfill/inspection RAW.
- [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils dexploitation réellement nécessaires avant de passer à la couche de normalisation générique suivante.
@@ -105,7 +105,7 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
### TODO/IDEAS — taxonomie N1, processing et rétention
- [ ] **TODO** — maintenir la matrice dadmission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique reste réservée à `0.3.4`.
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
- [ ] **TODO**`TransactionStatusObservation` : réauditer `signatureSubscribe`, `getSignatureStatuses`, Yellowstone TransactionStatus et extensions provider lorsquun consumer réel apparaît ; ne pas fusionner snapshot, transition et update dans un modèle Option-soup.
- [ ] **TODO** — logs realtime : conserver `logMessages` dans `RawTransaction` jusquà la décomposition STRUCTURAL ; traiter `logsSubscribe` comme event-only candidat et décider son contrat passif dans `ksp-interface-lib`, sans table Store par défaut. Le format canonique dun wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé quavec un publisher/consumer réel.
- [ ] **TODO** — slot/root/slotsUpdates et vote : ne créer un modèle passif commun que si un consumer realtime réel et une sémantique cross-ledger/provider justifient le contrat ; aucune persistence Store par défaut.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-lib/README.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# ksp-store-lib
@@ -19,7 +19,7 @@ Elle expose aux consumers une surface backend-neutral, réexporte les contrats R
- `Store::health().await` pour la readiness portable et bornée ;
- `Store::close(self).await` pour la fermeture explicite bornée ;
- le mapping des erreurs backend vers des codes Store stables sans exposer les erreurs physiques ;
- les six capabilities `RawTransaction*` dispatchées vers le backend compilé ;
- les dix capabilities RAW de `ksp-store-api` dispatchées vers le backend compilé : six `RawTransaction*` et quatre `RawAccount*` ;
- une validation réseau backend-neutral avant dispatch pour toutes les opérations qui portent explicitement un réseau ;
- les réexports crate-root de `ksp-store-api` nécessaires aux consumers ordinaires.
@@ -63,9 +63,9 @@ Store ne lit ni `.env`, ni variables `KSP_*` / `KSPB_*`, ni variables/fichiers i
`ksp-config-lib` possède `std.store`, la résolution des secrets et la sélection du target. Il construit ensuite un `StoreSettings` backend-neutral. L'URI PostgreSQL reste nécessaire au runtime mais n'a aucun getter public dans `ksp-store-lib` et son `Debug` est redacted.
## Surface RawTransaction
## Surface RAW
`Store` implémente les six capabilities transactionnelles acquises dans `ksp-store-api` :
`Store` implémente exactement dix capabilities RAW backend-neutral :
```text
RawTransactionRead
@@ -74,24 +74,29 @@ RawTransactionObservationRead
RawTransactionObservationWrite
RawTransactionRetentionRead
RawTransactionRetentionWrite
RawAccountStateRead
RawAccountStateWrite
RawAccountObservationRead
RawAccountObservationWrite
```
Le consumer manipule uniquement les modèles et outcomes backend-neutral. Les erreurs physiques PostgreSQL sont projetées vers des codes Store stables sans exposer le backend.
Pour `RawTransaction`, la façade fournit la lecture canonique et des observations, l'acquisition atomique transaction+observation, l'ajout idempotent d'observations, la pagination keyset et les transitions de rétention demandées par le caller.
La façade fournit ainsi :
Pour `RawAccountState`, elle fournit :
- lecture d'une transaction canonique, de ses observations et de sa rétention ;
- écriture atomique transaction + observation ;
- ajout idempotent d'observations ;
- pagination keyset déterministe par cursor opaque ;
- application de transitions de rétention demandées par le caller ;
- validation réseau avant dispatch lorsqu'un input porte explicitement son réseau.
- lecture d'un état complet par référence durable `(network, pubkey, slot, state_hash)` ;
- pagination keyset des références dans l'ordre total `(slot, pubkey, state_hash)`, avec filtre pubkey optionnel ;
- écriture atomique état+observation avec idempotence exacte et conflit sur contenu divergent ;
- lecture et ajout d'observations account ;
- validation réseau avant dispatch pour les opérations dont l'input porte explicitement un réseau.
Les cursors restent opaques et propres à leur famille. Store ne leur attribue aucune sémantique de batch, priorité ou scheduling.
## Hors périmètre
Sont volontairement hors de cette surface :
- persistence/query/rétention PostgreSQL de `RawAccountState` ;
- rétention, archivage, purge, delete ou compaction de `RawAccountState` ;
- batch-size, priorité, backlog ou policy de worker/job ;
- transport d'acquisition, Program decoding et materialization ;
- exposition publique de SQL, pool, client, row, statement ou transaction PostgreSQL.
@@ -105,4 +110,6 @@ Sont volontairement hors de cette surface :
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — plan de fondation ;
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — validation de fondation ;
- [`../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md`](../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md) — plan `RawTransaction` ;
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction`.
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction` ;
- [`../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md`](../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md) — design `RawAccountState` et complétude RAW ;
- [`../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md`](../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md) — validation `RawAccountState` et conformance 10/10.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-lib/USAGE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Utilisation de ksp-store-lib
@@ -268,7 +268,98 @@ async fn archive_transaction(
Le backend PostgreSQL supporte physiquement `Full -> Archived -> Purged`. Une transition impliquant `Compacted` est rejetée par ce backend tant qu'aucune représentation compactée réelle n'est implémentée.
## 11. Diagnostics et erreurs
## 11. Lire et paginer les états account RAW
Importer `RawAccountStateRead` donne accès à la lecture par référence et à la navigation déterministe.
```rust
use ksp_store_lib::RawAccountStateRead;
async fn read_account_state(
store: &ksp_store_lib::Store,
reference: &ksp_store_lib::RawAccountStateReference,
) -> ksp_store_lib::Result<std::option::Option<ksp_store_lib::RawAccountState>> {
return store.get_raw_account_state(reference).await;
}
```
Pour une première page :
```rust
use ksp_store_lib::RawAccountStateRead;
async fn first_account_page(
store: &ksp_store_lib::Store,
network: ksp_store_lib::RawNetworkId,
pubkey: std::option::Option<ksp_store_lib::Pubkey>,
) -> ksp_store_lib::Result<ksp_store_lib::RawPage<ksp_store_lib::RawAccountStateReference>> {
let limit = ksp_store_lib::RawPageLimit::new(100);
let limit = match limit {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slots = ksp_store_lib::RawSlotRange::new(std::option::Option::None, std::option::Option::None);
let slots = match slots {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let query = ksp_store_lib::RawAccountStateQuery::new(
network,
pubkey,
slots,
ksp_store_lib::RawSortDirection::Ascending,
ksp_store_lib::RawPageRequest::first(limit),
);
return store.list_raw_account_states(&query).await;
}
```
La continuation utilise uniquement `RawPage::next_cursor()` puis `RawPageRequest::after`. Le cursor account est opaque et lié au réseau, au filtre pubkey, à la direction et aux bornes de slots de la query qui l'a produit.
## 12. Persister une acquisition account
L'état canonique et son observation initiale sont une seule opération atomique.
```rust
use ksp_store_lib::RawAccountStateWrite;
async fn persist_account_acquisition(
store: &ksp_store_lib::Store,
state: ksp_store_lib::RawAccountState,
observation: ksp_store_lib::RawAccountObservation,
) -> ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome> {
return store.persist_raw_account_acquisition(state, observation).await;
}
```
`state.reference()` et `observation.account()` doivent désigner exactement le même état et le réseau du `Store`. Une répétition byte-identique est idempotente ; un contenu divergent sous la même identité retourne `ERROR_CODE_RAW_CONFLICT`.
## 13. Lire et ajouter une observation account
```rust
use ksp_store_lib::RawAccountObservationRead;
use ksp_store_lib::RawAccountObservationWrite;
async fn use_account_observation(
store: &ksp_store_lib::Store,
key: &ksp_store_lib::RawObservationKey,
observation: ksp_store_lib::RawAccountObservation,
) -> ksp_store_lib::Result<ksp_store_lib::RawObservationWriteOutcome> {
let existing = store.get_raw_account_observation(key).await;
if let std::result::Result::Err(error) = existing {
return std::result::Result::Err(error);
}
return store.record_raw_account_observation(observation).await;
}
```
`record_raw_account_observation` ne crée jamais implicitement l'état canonique ; une référence absente retourne `ERROR_CODE_RAW_REFERENCE_NOT_FOUND`. `RawObservationKey` ne contient pas de réseau : la lecture par clé reste liée au backend mono-réseau déjà ouvert.
## 14. Diagnostics et erreurs
Les snapshots et erreurs de façade n'exposent ni URI, host, user, database, SQL, handle backend, valeur de bind ni texte d'erreur PostgreSQL.
@@ -286,8 +377,8 @@ store.postgres_retention_compaction_unsupported
Les conflits et queries invalides utilisent les codes backend-neutral `store_api.raw_conflict` et `store_api.raw_query_invalid`.
## 12. Limites de la façade
## 15. Limites de la façade
La façade ne fournit pas d'accès public au SQL, au pool, aux clients ou transactions PostgreSQL. Les capabilities `RawAccount*` réexportées par l'API commune ne sont pas encore dispatchées par `Store`.
La façade ne fournit pas d'accès public au SQL, au pool, aux clients ou transactions PostgreSQL. Elle dispatch les dix capabilities RAW de l'API commune, mais ne fournit aucune capability de rétention, archivage, purge, delete ou compaction account.
La taille de page est une primitive de navigation. Les décisions de batch, priorité, backlog et scheduling appartiennent aux workers/jobs, pas à Store.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs
// version: 8
// version: 9
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -8,9 +8,10 @@
//! Common backend-neutral Store runtime facade for KSP.
//!
//! The runtime facade owns backend selection, lifecycle, safe diagnostics and
//! backend-neutral capability dispatch. `0.3.3-pre.008` completes the PostgreSQL
//! `RawTransaction` vertical slice by implementing the six transaction capabilities
//! on both the physical backend and this common facade without exposing physical types.
//! backend-neutral capability dispatch. `0.3.3-pre.008` completed the six PostgreSQL
//! `RawTransaction` capabilities. `0.3.4-pre.008` adds the four `RawAccount*` capabilities
//! on both the physical backend and this common facade, completing the RAW inventory at
//! ten capabilities without exposing physical types.
//!
//! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/store.rs
// version: 6
// version: 7
/// Opaque common Store runtime facade.
///
@@ -116,6 +116,156 @@ impl std::fmt::Debug for Store {
}
}
impl ksp_store_api::RawAccountObservationRead for Store {
fn get_raw_account_observation<'a>(
&'a self,
observation_key: &'a ksp_store_api::RawObservationKey,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountObservation>>> {
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_account_observation(observation_key).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = observation_key;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawAccountObservationWrite for Store {
fn record_raw_account_observation<'a>(
&'a self,
observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
let network_check = validate_operation_network(&self.network, observation.account().network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.record_raw_account_observation(observation).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = observation;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawAccountStateRead for Store {
fn get_raw_account_state<'a>(
&'a self,
reference: &'a ksp_store_api::RawAccountStateReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountState>>> {
let network_check = validate_operation_network(&self.network, reference.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.get_raw_account_state(reference).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = reference;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
fn list_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>>> {
let network_check = validate_operation_network(&self.network, query.network(), self.backend_kind);
if let std::result::Result::Err(error) = network_check {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.list_raw_account_states(query).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = query;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawAccountStateWrite for Store {
fn persist_raw_account_acquisition<'a>(
&'a self,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
let state_network = validate_operation_network(&self.network, state.reference().network(), self.backend_kind);
if let std::result::Result::Err(error) = state_network {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
let observation_network = validate_operation_network(&self.network, observation.account().network(), self.backend_kind);
if let std::result::Result::Err(error) = observation_network {
return std::boxed::Box::pin(async move {
return std::result::Result::Err(error);
});
}
return std::boxed::Box::pin(async move {
#[cfg(feature = "postgres")]
{
return match &self.runtime {
StoreRuntime::Postgres(backend) => {
let result = backend.persist_raw_account_acquisition(state, observation).await;
result.map_err(|error| return map_postgres_error(error, self.backend_kind, self.network.as_str()))
},
};
}
#[cfg(not(feature = "postgres"))]
{
let _ = state;
let _ = observation;
return std::result::Result::Err(unavailable_runtime_error(self.backend_kind));
}
});
}
}
impl ksp_store_api::RawTransactionRead for Store {
fn get_raw_transaction<'a>(
&'a self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 7
// version: 8
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -60,34 +60,28 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
}
#[test]
fn pre_008_facade_dispatches_six_raw_transaction_capabilities_without_physical_leak() {
fn pre_008_facade_dispatches_exact_ten_raw_capabilities_without_physical_leak() {
let store = include_str!("../src/store.rs");
for required in [
"impl ksp_store_api::RawTransactionRead for Store",
"impl ksp_store_api::RawTransactionWrite for Store",
"impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionObservationRead for Store",
"impl ksp_store_api::RawTransactionObservationWrite for Store",
"impl ksp_store_api::RawTransactionRead for Store",
"impl ksp_store_api::RawTransactionRetentionRead for Store",
"impl ksp_store_api::RawTransactionRetentionWrite for Store",
"impl ksp_store_api::RawTransactionWrite for Store",
"validate_operation_network",
"StoreRuntime::Postgres(backend)",
"map_postgres_error",
] {
assert!(store.contains(required), "missing pre.008 Store capability dispatch contract: {required}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store",
"tokio_postgres::",
"deadpool_postgres::",
"CREATE TABLE",
"INSERT INTO",
"UPDATE ksp_",
"DELETE FROM",
] {
assert!(!store.contains(forbidden), "pre.008 facade leaked physical or RawAccount scope: {forbidden}");
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10);
for forbidden in ["tokio_postgres::", "deadpool_postgres::", "CREATE TABLE", "INSERT INTO", "UPDATE ksp_", "DELETE FROM"] {
assert!(!store.contains(forbidden), "pre.008 facade leaked physical backend material: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/hardening_completeness.rs
// version: 4
// version: 6
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -84,6 +84,27 @@ fn manifest_dependency_names(source: &str) -> std::vec::Vec<&str> {
return names;
}
fn raw_capability_trait_names<'a>(source: &'a str, implementor: &str) -> std::vec::Vec<&'a str> {
let mut names = std::vec::Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("impl ksp_store_api::Raw") || !trimmed.contains(implementor) {
continue;
}
let trait_tail = match trimmed.strip_prefix("impl ksp_store_api::") {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let trait_name = match trait_tail.split(" for ").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
names.push(trait_name);
}
names.sort_unstable();
return names;
}
#[test]
fn pre_009_facade_modules_and_crate_root_exports_are_exact() {
let crate_root = include_str!("../src/lib.rs");
@@ -263,27 +284,40 @@ fn pre_009_facade_production_sources_keep_config_env_physical_sql_and_backend_ha
}
#[test]
fn pre_010_facade_raw_transaction_capability_inventory_is_exact_and_raw_account_scope_stays_closed() {
fn pre_010_facade_raw_capability_inventory_is_exactly_ten() {
let store = include_str!("../src/store.rs");
let capability_impls = [
"impl ksp_store_api::RawTransactionRead for Store",
"impl ksp_store_api::RawTransactionWrite for Store",
"impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store",
"impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawTransactionObservationRead for Store",
"impl ksp_store_api::RawTransactionObservationWrite for Store",
"impl ksp_store_api::RawTransactionRead for Store",
"impl ksp_store_api::RawTransactionRetentionRead for Store",
"impl ksp_store_api::RawTransactionRetentionWrite for Store",
"impl ksp_store_api::RawTransactionWrite for Store",
];
for implementation in capability_impls {
assert_eq!(store.matches(implementation).count(), 1, "unexpected Store capability implementation inventory: {implementation}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for Store",
"impl ksp_store_api::RawAccountStateWrite for Store",
"impl ksp_store_api::RawAccountObservationRead for Store",
"impl ksp_store_api::RawAccountObservationWrite for Store",
] {
assert!(!store.contains(forbidden), "RawAccountState scope opened in Store during RawTransaction hardening: {forbidden}");
}
assert_eq!(store.matches("validate_operation_network(").count(), 9);
assert_eq!(store.matches("impl ksp_store_api::Raw").count(), 10);
assert_eq!(store.matches("validate_operation_network(").count(), 14);
return;
}
#[test]
fn pre_010_facade_and_backend_raw_capability_sets_match_exactly_without_account_retention() {
let store = include_str!("../src/store.rs");
let backend = include_str!("../../ksp-store-postgres-lib/src/runtime.rs");
let store_traits = raw_capability_trait_names(store, " for Store");
let backend_traits = raw_capability_trait_names(backend, " for PostgresBackend");
assert_eq!(store_traits.len(), 10);
assert_eq!(backend_traits.len(), 10);
assert_eq!(store_traits, backend_traits);
for forbidden in ["RawAccountRetentionRead", "RawAccountRetentionWrite", "RawAccountDelete", "RawAccountCompaction"] {
assert!(!store_traits.contains(&forbidden), "unexpected account capability added to Store: {forbidden}");
assert!(!backend_traits.contains(&forbidden), "unexpected account capability added to PostgreSQL backend: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/public_api.rs
// version: 7
// version: 8
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -74,21 +74,25 @@ fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts()
return;
}
fn assert_raw_transaction_capabilities<T>()
fn assert_raw_capabilities<T>()
where
T: ksp_store_lib::RawTransactionRead
+ ksp_store_lib::RawTransactionWrite
T: ksp_store_lib::RawAccountObservationRead
+ ksp_store_lib::RawAccountObservationWrite
+ ksp_store_lib::RawAccountStateRead
+ ksp_store_lib::RawAccountStateWrite
+ ksp_store_lib::RawTransactionObservationRead
+ ksp_store_lib::RawTransactionObservationWrite
+ ksp_store_lib::RawTransactionRead
+ ksp_store_lib::RawTransactionRetentionRead
+ ksp_store_lib::RawTransactionRetentionWrite,
+ ksp_store_lib::RawTransactionRetentionWrite
+ ksp_store_lib::RawTransactionWrite,
{
let _marker = std::marker::PhantomData::<T>;
return;
}
#[test]
fn pre_008_store_facade_implements_all_six_raw_transaction_capabilities() {
assert_raw_transaction_capabilities::<ksp_store_lib::Store>();
fn pre_008_store_facade_implements_exact_raw_capability_set_10_of_10() {
assert_raw_capabilities::<ksp_store_lib::Store>();
return;
}

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/README.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# ksp-store-postgres-lib
@@ -18,7 +18,7 @@ La crate possède seule pour PostgreSQL :
- le bootstrap et le moteur de migrations privé KSP ;
- la table metadata `ksp_store_schema_migrations` ;
- l'advisory transaction lock borné des migrations ;
- le schéma physique et les statements privés `RawTransaction` ;
- les schémas physiques et statements privés `RawTransaction` et `RawAccountState` ;
- les snapshots runtime/health sûrs destinés au bridge de façade ;
- la fermeture explicite du pool et son fallback `Drop` best-effort ;
- la classification d'erreurs backend sans conserver le texte d'erreur PostgreSQL.
@@ -63,7 +63,7 @@ VerifyFull
Le moteur de migrations embarqué vérifie version logique, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
Le bootstrap metadata est conservé comme migration V000. La migration logique V001 matérialise le schéma `RawTransaction` en ressources séparées `tables/`, `constraints/` et `indexes/` afin que le backend puisse vérifier leur compatibilité effective sans transformer les fichiers SQL en parser généraliste.
Le bootstrap metadata est conservé comme migration V000. La migration logique V001 matérialise le schéma `RawTransaction` et V002 le schéma `RawAccountState`, chacune en ressources séparées `tables/`, `constraints/` et `indexes/` afin que le backend puisse vérifier leur compatibilité effective sans transformer les fichiers SQL en parser généraliste.
La base est liée à un seul `RawNetworkId` via `ksp_store_identity`. Une migration enregistrée mais physiquement divergente est un mismatch ; les réparations additives sûres dépendent de `schema_autoupdate`.
@@ -130,11 +130,43 @@ Le backend verrouille la ligne canonique avec `FOR UPDATE`, compare l'état cour
Toute transition impliquant `Compacted` est rejetée avec `RetentionCompactionUnsupported` tant qu'aucune représentation compactée réelle n'est implémentée.
## Lectures RAW account
Le backend expose :
```text
get_raw_account_state
get_raw_account_observation
```
`get_raw_account_state` reconstruit l'état complet à partir de `(pubkey, slot, state_hash)` sans narrowing du domaine `u64`. Les bytes `pubkey`, `owner` et `state_hash` sont revalidés à leur largeur exacte et `data` reste un `BYTEA` complet, vide autorisé, borné par le contrat Store API.
`get_raw_account_observation` reconstruit la provenance commune et les métadonnées account optionnelles, notamment `is_startup`, `transaction_signature` et `write_version`. La signature est une metadata fixed-width et ne crée aucune FK vers la famille transaction.
## Écritures RAW account
Le backend expose :
```text
persist_raw_account_acquisition
record_raw_account_observation
```
L'acquisition état+observation est transactionnelle. Les inserts utilisent `ON CONFLICT ... DO NOTHING`, puis verrouillent et comparent le contenu gagnant avant de conclure `AlreadyPresent` ou `Conflict`; aucun `DO UPDATE` n'est utilisé. Une collision divergente d'observation fait échouer toute l'acquisition et rollback un éventuel nouvel état.
L'ajout d'une observation vérifie que l'état référencé existe déjà et ne crée jamais implicitement cet état.
## Pagination RAW account
`list_raw_account_states` parcourt les références selon l'ordre total `(slot, pubkey, state_hash)`, en ASC ou DESC, avec filtre pubkey optionnel. La continuation est keyset, sans `OFFSET`.
Le cursor `KSPA` est opaque et lié au réseau, au filtre pubkey, à la direction, aux bornes de slots et à la dernière clé complète. Il est distinct du cursor transaction `KSPT`.
## Hors périmètre
La crate ne contient :
- aucune implémentation PostgreSQL des capabilities `RawAccount*` ;
- aucune rétention, archive, purge, suppression ou compaction account ;
- aucune orchestration worker/job ;
- aucun transport d'acquisition ou decoder Program ;
- aucune policy autonome de batch, priorité ou rétention.
@@ -147,4 +179,6 @@ La crate ne contient :
- [`../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md`](../../docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md) — décisions pool/TLS/migrations ;
- [`../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md`](../../docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md) — validation de fondation ;
- [`../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md`](../../docs/plans/024-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION_PLAN.md) — design `RawTransaction` ;
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction`.
- [`../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md`](../../docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md) — validation `RawTransaction` ;
- [`../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md`](../../docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md) — design `RawAccountState` et complétude RAW ;
- [`../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md`](../../docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md) — validation `RawAccountState` et conformance RAW.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
<!-- version: 10 -->
<!-- version: 11 -->
# Utilisation de ksp-store-postgres-lib
@@ -202,7 +202,58 @@ Le backend applique la transition choisie par le caller ; il ne décide pas de l
Une transition impliquant `Compacted` est refusée avec `PostgresBackendErrorKind::RetentionCompactionUnsupported` tant qu'aucune représentation compactée réelle n'est disponible.
## 11. Classifier les erreurs sans fuite
## 11. Lire et paginer les états account
```rust
async fn read_account_state(
backend: &ksp_store_postgres_lib::PostgresBackend,
reference: &ksp_store_api::RawAccountStateReference,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, ksp_store_postgres_lib::PostgresBackendError> {
return backend.get_raw_account_state(reference).await;
}
```
Pour la navigation, construire un `RawAccountStateQuery` puis appeler :
```rust
async fn list_account_states(
backend: &ksp_store_postgres_lib::PostgresBackend,
query: &ksp_store_api::RawAccountStateQuery,
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, ksp_store_postgres_lib::PostgresBackendError> {
return backend.list_raw_account_states(query).await;
}
```
La pagination est keyset sur `(slot, pubkey, state_hash)` avec filtre pubkey optionnel. Le cursor `KSPA` est opaque, lié au contexte de query et distinct du cursor transaction.
## 12. Persister une acquisition account
```rust
async fn persist_account_acquisition(
backend: &ksp_store_postgres_lib::PostgresBackend,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
return backend.persist_raw_account_acquisition(state, observation).await;
}
```
Le backend exige le même réseau et la même référence complète entre l'état et l'observation avant l'I/O métier. L'opération est atomique et idempotente par comparaison exacte du contenu persistant ; un contenu divergent produit `PostgresBackendErrorKind::Conflict` sans overwrite silencieux.
## 13. Lire et ajouter une observation account
```rust
async fn record_account_observation(
backend: &ksp_store_postgres_lib::PostgresBackend,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, ksp_store_postgres_lib::PostgresBackendError> {
return backend.record_raw_account_observation(observation).await;
}
```
La lecture correspondante utilise `get_raw_account_observation`. L'ajout exige un état déjà durable et retourne `ReferenceNotFound` lorsqu'il manque. Les métadonnées Yellowstone optionnelles sont conservées sans créer de couplage physique vers `RawTransaction`.
## 14. Classifier les erreurs sans fuite
```rust
fn classify(error: &ksp_store_postgres_lib::PostgresBackendError) {
@@ -234,8 +285,8 @@ fn classify(error: &ksp_store_postgres_lib::PostgresBackendError) {
`PostgresBackendError` conserve uniquement une classification KSP et une phase statique. Ne pas reconstruire de diagnostic utilisateur à partir d'une erreur brute PostgreSQL.
## 12. Limites du backend direct
## 15. Limites du backend direct
Le backend ne lit aucune variable d'environnement et ne possède aucune sélection de target Config. Les applications, jobs et workers doivent normalement passer par `ksp-store-lib`.
Les capabilities `RawAccount*` ne sont pas implémentées par ce backend. Les décisions de batch, priorité, backlog, scheduling et policy de rétention restent hors de sa responsabilité.
Les dix capabilities RAW communes sont implémentées par ce backend. Les décisions de batch, priorité, backlog, scheduling et policy de rétention restent hors de sa responsabilité ; aucune rétention/archivage/purge account n'est fournie.

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_raw_account_states'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT pk_ksp_raw_account_states PRIMARY KEY (pubkey, slot, state_hash);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'pk_ksp_raw_account_observations'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT pk_ksp_raw_account_observations PRIMARY KEY (observation_key);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,14 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_ksp_raw_account_observations_state'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations
ADD CONSTRAINT fk_ksp_raw_account_observations_state FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) ON DELETE RESTRICT;
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_pubkey'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_pubkey CHECK (octet_length(pubkey) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_slot'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_slot CHECK (slot >= 0 AND slot <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_state_hash'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_state_hash CHECK (octet_length(state_hash) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_lamports'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_lamports CHECK (lamports >= 0 AND lamports <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_owner'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_owner CHECK (octet_length(owner) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_rent_epoch'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_rent_epoch CHECK (rent_epoch >= 0 AND rent_epoch <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_data'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_data CHECK (octet_length(data) <= 16777216);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_key'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_key CHECK (octet_length(observation_key) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_pubkey'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_pubkey CHECK (octet_length(account_pubkey) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_slot'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_slot CHECK (account_slot >= 0 AND account_slot <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_state_hash'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_state_hash CHECK (octet_length(account_state_hash) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_provider'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_provider CHECK (
octet_length(provider) >= 1 AND octet_length(provider) <= 128
AND provider ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_protocol'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_protocol CHECK (
octet_length(protocol) >= 1 AND octet_length(protocol) <= 128
AND protocol ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_method'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_method CHECK (
octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128
AND acquisition_method ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_origin'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_origin CHECK (origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay');
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_received_at'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_received_at CHECK (received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_capture_session'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_capture_session CHECK (
capture_session_id IS NULL
OR (
octet_length(capture_session_id) >= 1 AND octet_length(capture_session_id) <= 128
AND capture_session_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_commitment'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_commitment CHECK (
commitment IS NULL
OR (
octet_length(commitment) >= 1 AND octet_length(commitment) <= 128
AND commitment ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_endpoint'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_endpoint CHECK (
endpoint_id IS NULL
OR (
octet_length(endpoint_id) >= 1 AND octet_length(endpoint_id) <= 128
AND endpoint_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_filter'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_filter CHECK (
filter_id IS NULL
OR (
octet_length(filter_id) >= 1 AND octet_length(filter_id) <= 128
AND filter_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_observed_at'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_observed_at CHECK (
observed_at_unix_millis IS NULL
OR observed_at_unix_millis >= 0 AND observed_at_unix_millis <= 253402300799999
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_time_order'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_time_order CHECK (observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_source_hash'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_hash CHECK (
source_payload_hash IS NULL
OR octet_length(source_payload_hash) = 32
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_source_size'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_size CHECK (
source_payload_size_bytes IS NULL
OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_transaction_signature'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_transaction_signature CHECK (
transaction_signature IS NULL
OR octet_length(transaction_signature) = 64
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_write_version'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_write_version CHECK (
write_version IS NULL
OR write_version >= 0 AND write_version <= 18446744073709551615
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash
ON ksp_raw_account_states (slot, pubkey, state_hash);

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS ksp_raw_account_states (
pubkey BYTEA NOT NULL,
slot NUMERIC(20, 0) NOT NULL,
state_hash BYTEA NOT NULL,
lamports NUMERIC(20, 0) NOT NULL,
owner BYTEA NOT NULL,
executable BOOLEAN NOT NULL,
rent_epoch NUMERIC(20, 0) NOT NULL,
data BYTEA NOT NULL
);
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS pubkey BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS slot NUMERIC(20, 0) NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS state_hash BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS lamports NUMERIC(20, 0) NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS owner BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS executable BOOLEAN NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS rent_epoch NUMERIC(20, 0) NOT NULL;
ALTER TABLE ksp_raw_account_states ADD COLUMN IF NOT EXISTS data BYTEA NOT NULL;

View File

@@ -0,0 +1,41 @@
CREATE TABLE IF NOT EXISTS ksp_raw_account_observations (
observation_key BYTEA NOT NULL,
account_pubkey BYTEA NOT NULL,
account_slot NUMERIC(20, 0) NOT NULL,
account_state_hash BYTEA NOT NULL,
provider TEXT NOT NULL,
protocol TEXT NOT NULL,
acquisition_method TEXT NOT NULL,
origin TEXT NOT NULL,
received_at_unix_millis BIGINT NOT NULL,
capture_session_id TEXT NULL,
commitment TEXT NULL,
endpoint_id TEXT NULL,
filter_id TEXT NULL,
observed_at_unix_millis BIGINT NULL,
source_payload_hash BYTEA NULL,
source_payload_size_bytes BIGINT NULL,
is_startup BOOLEAN NULL,
transaction_signature BYTEA NULL,
write_version NUMERIC(20, 0) NULL
);
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS observation_key BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_pubkey BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_slot NUMERIC(20, 0) NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS account_state_hash BYTEA NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS protocol TEXT NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS acquisition_method TEXT NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS origin TEXT NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS received_at_unix_millis BIGINT NOT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS capture_session_id TEXT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS commitment TEXT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS endpoint_id TEXT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS filter_id TEXT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS observed_at_unix_millis BIGINT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS source_payload_hash BYTEA NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS source_payload_size_bytes BIGINT NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS is_startup BOOLEAN NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS transaction_signature BYTEA NULL;
ALTER TABLE ksp_raw_account_observations ADD COLUMN IF NOT EXISTS write_version NUMERIC(20, 0) NULL;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 14
// version: 21
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -21,6 +21,18 @@
//! with compare-and-transition outcomes and explicit rejection of `Compacted`.
//! `0.3.3-pre.008` implements all six `RawTransaction*` capabilities directly on
//! `PostgresBackend` while preserving the existing narrow backend bridge.
//! `0.3.4-pre.002` registers additive V002 and its two minimal RAW account
//! tables with canonical state/observation PKs and the observation-state FK.
//! `0.3.4-pre.003` completes V002 with exact physical bounds, one unfiltered
//! navigation index, external-schema compatibility and the bounded prerelease
//! checksum transition from the provisional `pre.002` schema. `0.3.4-pre.004`
//! adds backend-private RAW account state/observation read mapping and hostile-row
//! guards. `0.3.4-pre.005` adds atomic account state+observation acquisition writes
//! with exact idempotence/conflict classification. `0.3.4-pre.006` adds additional
//! account observations guarded by the existing state reference. `0.3.4-pre.007` adds
//! deterministic account keyset pagination with the fixed `KSPA` cursor. `0.3.4-pre.008`
//! implements the four `RawAccount*` capabilities directly on `PostgresBackend`, completing
//! the backend RAW capability inventory at ten without exposing physical PostgreSQL types.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
@@ -30,6 +42,7 @@ mod constants;
mod error;
mod health;
mod migration;
mod raw_account;
mod raw_transaction;
mod runtime;
mod schema;
@@ -59,6 +72,22 @@ pub(crate) use self::health::probe_health;
pub(crate) use self::migration::bootstrap;
/// Current embedded migration version consumed by the private health probe.
pub(crate) use self::migration::current_migration_version;
/// Private RAW account cursor decoder consumed by the physical RAW account module.
pub(crate) use self::raw_account::cursor::decode_raw_account_cursor;
/// Private RAW account cursor encoder consumed by the physical RAW account module.
pub(crate) use self::raw_account::cursor::encode_raw_account_cursor;
/// Private physical account page-limit converter consumed by the physical RAW account module.
pub(crate) use self::raw_account::cursor::raw_account_physical_page_limit;
/// Private RAW account observation reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::get_raw_account_observation;
/// Private RAW account state reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::get_raw_account_state;
/// Private RAW account-state list reader consumed by the physical backend runtime.
pub(crate) use self::raw_account::list_raw_account_states;
/// Private atomic RAW account acquisition writer consumed by the physical backend runtime.
pub(crate) use self::raw_account::persist_raw_account_acquisition;
/// Private additional RAW account observation writer consumed by the physical backend runtime.
pub(crate) use self::raw_account::record_raw_account_observation;
/// Private RAW transaction cursor decoder consumed by the physical RAW module.
pub(crate) use self::raw_transaction::cursor::decode_raw_transaction_cursor;
/// Private RAW transaction cursor encoder consumed by the physical RAW module.
@@ -93,11 +122,15 @@ pub(crate) use self::schema::SchemaResourceState;
pub(crate) use self::schema::V000_RESOURCES;
/// Private V001 schema resource inventory consumed by the migration engine.
pub(crate) use self::schema::V001_RESOURCES;
/// Private V002 schema resource inventory consumed by the migration engine.
pub(crate) use self::schema::V002_RESOURCES;
/// Private physical schema resource inspector consumed by the migration engine.
pub(crate) use self::schema::inspect_resource;
/// Private V001 adoption probe consumed by the migration engine.
pub(crate) use self::schema::managed_v001_objects_exist;
/// Private external-schema compatibility gate consumed by the migration engine.
/// Private managed-schema adoption probe consumed by the migration engine.
pub(crate) use self::schema::managed_schema_objects_exist;
/// Private V001 external-schema compatibility gate consumed by the migration engine.
pub(crate) use self::schema::verify_v001_external_compatibility;
/// Private V002 external-schema compatibility gate consumed by the migration engine.
pub(crate) use self::schema::verify_v002_external_compatibility;
const _: &str = crate::TRACING_TARGET;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/migration.rs
// version: 7
// version: 10
use sha2::Digest; // rust-rules: trait-import
@@ -9,6 +9,7 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
checksum: MigrationChecksum::LegacySql(include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql")),
hook: MigrationHook::None,
name: "bootstrap",
previous_checksums: &[],
resources: crate::V000_RESOURCES,
version: 0,
},
@@ -16,13 +17,23 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
checksum: MigrationChecksum::Resources,
hook: MigrationHook::StoreIdentity,
name: "raw_transaction",
previous_checksums: &[],
resources: crate::V001_RESOURCES,
version: 1,
},
EmbeddedMigration {
checksum: MigrationChecksum::Resources,
hook: MigrationHook::None,
name: "raw_account_state",
previous_checksums: &[V002_PROVISIONAL_CHECKSUM_PRE_002],
resources: crate::V002_RESOURCES,
version: 2,
},
];
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
const HISTORY_UPDATE_CHECKSUM_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = $2 AND name = $3 AND checksum = $4";
const IDENTITY_INSERT_SQL: &str = "INSERT INTO ksp_store_identity (singleton, network) VALUES (1, $1)";
const IDENTITY_LOAD_SQL: &str = "SELECT singleton, network FROM ksp_store_identity ORDER BY singleton LIMIT 2";
const LOCK_POLL_INTERVAL_MS: u64 = 25;
@@ -33,6 +44,7 @@ const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
AND table_type = 'BASE TABLE'
)"#;
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
const V002_PROVISIONAL_CHECKSUM_PRE_002: &str = "30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55";
struct AppliedMigration {
checksum: std::string::String,
@@ -51,6 +63,7 @@ struct EmbeddedMigration {
checksum: MigrationChecksum,
hook: MigrationHook,
name: &'static str,
previous_checksums: &'static [&'static str],
resources: &'static [crate::SchemaResource],
version: i64,
}
@@ -133,7 +146,7 @@ async fn bootstrap_inner(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (next_index, mutation_mode) = if metadata_exists {
let (next_index, mutation_mode, applied_history) = if metadata_exists {
let metadata_result = crate::inspect_resource(&transaction, &crate::V000_RESOURCES[0]).await;
match metadata_result {
std::result::Result::Ok(crate::SchemaResourceState::Compatible) => {},
@@ -148,18 +161,18 @@ async fn bootstrap_inner(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS);
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS, schema_autoupdate);
let index = match validation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
(index, SchemaMutationMode::Update)
(index, SchemaMutationMode::Update, std::option::Option::Some(history))
} else {
if !schema_autocreate {
log_schema_block("schema_autocreate_disabled");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_autocreate_disabled"));
}
let managed_result = crate::managed_v001_objects_exist(&transaction).await;
let managed_result = crate::managed_schema_objects_exist(&transaction).await;
let managed_objects_exist = match managed_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -168,12 +181,18 @@ async fn bootstrap_inner(
log_schema_block("schema_adoption_disabled");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_adoption_disabled"));
}
(0, SchemaMutationMode::Create)
(0, SchemaMutationMode::Create, std::option::Option::None)
};
let existing_schema_result = verify_or_repair_applied_migrations(&transaction, next_index, schema_autoupdate).await;
if let std::result::Result::Err(error) = existing_schema_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(history) = applied_history.as_deref() {
let checksum_result = reconcile_applied_history_checksums(&transaction, history, next_index, schema_autoupdate).await;
if let std::result::Result::Err(error) = checksum_result {
return std::result::Result::Err(error);
}
}
let existing_hook_result = run_applied_migration_hooks(&transaction, network, next_index).await;
if let std::result::Result::Err(error) = existing_hook_result {
return std::result::Result::Err(error);
@@ -521,12 +540,14 @@ async fn set_statement_timeout(
}
async fn verify_migration_contract(transaction: &deadpool_postgres::Transaction<'_>, version: i64) -> std::result::Result<(), crate::PostgresBackendError> {
if version == 1 {
let result = crate::verify_v001_external_compatibility(transaction).await;
if let std::result::Result::Err(error) = result {
log_schema_block(error.phase());
return std::result::Result::Err(error);
}
let result = match version {
1 => crate::verify_v001_external_compatibility(transaction).await,
2 => crate::verify_v002_external_compatibility(transaction).await,
_ => std::result::Result::Ok(()),
};
if let std::result::Result::Err(error) = result {
log_schema_block(error.phase());
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
@@ -575,6 +596,41 @@ async fn verify_or_repair_applied_migrations(
return std::result::Result::Ok(());
}
async fn reconcile_applied_history_checksums(
transaction: &deadpool_postgres::Transaction<'_>,
history: &[AppliedMigration],
applied_count: usize,
schema_autoupdate: bool,
) -> std::result::Result<(), crate::PostgresBackendError> {
if !schema_autoupdate {
return std::result::Result::Ok(());
}
for (index, applied) in history.iter().take(applied_count).enumerate() {
let migration = &EMBEDDED_MIGRATIONS[index];
let expected_checksum = migration_checksum(migration);
if applied.checksum == expected_checksum {
continue;
}
if !migration.previous_checksums.contains(&applied.checksum.as_str()) {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
let result = transaction.execute(HISTORY_UPDATE_CHECKSUM_SQL, &[&expected_checksum, &migration.version, &migration.name, &applied.checksum]).await;
match result {
std::result::Result::Ok(1) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
migration_version = migration.version,
"upgraded known prerelease PostgreSQL Store migration checksum after schema reconciliation"
);
},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_checksum_update"));
},
}
}
return std::result::Result::Ok(());
}
fn schema_autoupdate_disabled_error() -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "schema_autoupdate_disabled");
}
@@ -608,7 +664,11 @@ fn validate_embedded_registry(migrations: &[EmbeddedMigration]) -> std::result::
return std::result::Result::Ok(());
}
fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigration]) -> std::result::Result<usize, crate::PostgresBackendError> {
fn validate_history(
history: &[AppliedMigration],
migrations: &[EmbeddedMigration],
allow_previous_checksums: bool,
) -> std::result::Result<usize, crate::PostgresBackendError> {
if history.is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
}
@@ -623,7 +683,8 @@ fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigratio
}
let expected = &migrations[index];
let expected_checksum = migration_checksum(expected);
if applied.version != expected.version || applied.name != expected.name || applied.checksum != expected_checksum {
let previous_checksum_matches = allow_previous_checksums && expected.previous_checksums.contains(&applied.checksum.as_str());
if applied.version != expected.version || applied.name != expected.name || (applied.checksum != expected_checksum && !previous_checksum_matches) {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
index += 1;

View File

@@ -0,0 +1,962 @@
// file: crates/ksp-store-postgres-lib/src/raw_account.rs
// version: 5
pub(crate) mod cursor;
const GET_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1";
const GET_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3";
const INSERT_ACCOUNT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_account_observations (observation_key, account_pubkey, account_slot, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version) VALUES ($1, $2, $3::TEXT::NUMERIC, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::TEXT::NUMERIC) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
const INSERT_ACCOUNT_STATE_SQL: &str = "INSERT INTO ksp_raw_account_states (pubkey, slot, state_hash, lamports, owner, executable, rent_epoch, data) VALUES ($1, $2::TEXT::NUMERIC, $3, $4::TEXT::NUMERIC, $5, $6, $7::TEXT::NUMERIC, $8) ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING pubkey";
const LIST_ACCOUNT_STATES_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) > ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $6";
const LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) > ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot ASC, pubkey ASC, state_hash ASC LIMIT $7";
const LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE pubkey = $1 AND ($2::TEXT IS NULL OR slot >= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR slot <= $3::TEXT::NUMERIC) AND ($4::TEXT IS NULL OR (slot, pubkey, state_hash) < ($4::TEXT::NUMERIC, $5::BYTEA, $6::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $7";
const LIST_ACCOUNT_STATES_DESC_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash FROM ksp_raw_account_states WHERE ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, pubkey, state_hash) < ($3::TEXT::NUMERIC, $4::BYTEA, $5::BYTEA)) ORDER BY slot DESC, pubkey DESC, state_hash DESC LIMIT $6";
const LOCK_ACCOUNT_OBSERVATION_SQL: &str = "SELECT observation_key, account_pubkey, account_slot::text AS account_slot_text, account_state_hash, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes, is_startup, transaction_signature, write_version::text AS write_version_text FROM ksp_raw_account_observations WHERE observation_key = $1 FOR UPDATE";
const LOCK_ACCOUNT_REFERENCE_SQL: &str =
"SELECT 1 FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR KEY SHARE";
const LOCK_ACCOUNT_STATE_SQL: &str = "SELECT pubkey, slot::text AS slot_text, state_hash, lamports::text AS lamports_text, owner, executable, rent_epoch::text AS rent_epoch_text, data FROM ksp_raw_account_states WHERE pubkey = $1 AND slot = $2::TEXT::NUMERIC AND state_hash = $3 FOR UPDATE";
struct RawAccountListDbRow {
pubkey: std::vec::Vec<u8>,
slot_text: std::string::String,
state_hash: std::vec::Vec<u8>,
}
struct RawAccountObservationDbRow {
account_pubkey: std::vec::Vec<u8>,
account_slot_text: std::string::String,
account_state_hash: std::vec::Vec<u8>,
acquisition_method: std::string::String,
capture_session_id: std::option::Option<std::string::String>,
commitment: std::option::Option<std::string::String>,
endpoint_id: std::option::Option<std::string::String>,
filter_id: std::option::Option<std::string::String>,
is_startup: std::option::Option<bool>,
observation_key: std::vec::Vec<u8>,
observed_at_unix_millis: std::option::Option<i64>,
origin: std::string::String,
protocol: std::string::String,
provider: std::string::String,
received_at_unix_millis: i64,
source_payload_hash: std::option::Option<std::vec::Vec<u8>>,
source_payload_size_bytes: std::option::Option<i64>,
transaction_signature: std::option::Option<std::vec::Vec<u8>>,
write_version_text: std::option::Option<std::string::String>,
}
struct RawAccountStateDbRow {
data: std::vec::Vec<u8>,
executable: bool,
lamports_text: std::string::String,
owner: std::vec::Vec<u8>,
pubkey: std::vec::Vec<u8>,
rent_epoch_text: std::string::String,
slot_text: std::string::String,
state_hash: std::vec::Vec<u8>,
}
/// Reads one complete canonical RAW account state from the physical PostgreSQL backend.
pub(crate) async fn get_raw_account_state(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
reference: &ksp_store_api::RawAccountStateReference,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
let network_result = ensure_network(network, reference, "raw_account_state_network");
if let std::result::Result::Err(error) = network_result {
return std::result::Result::Err(error);
}
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
let slot_text = reference.slot().to_string();
let state_hash = reference.state_hash();
let state_hash_bytes: &[u8] = state_hash.as_bytes();
let rows_result = client.query(GET_ACCOUNT_STATE_SQL, &[&pubkey_bytes, &slot_text, &state_hash_bytes]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_state_query"));
},
};
if rows.is_empty() {
return std::result::Result::Ok(std::option::Option::None);
}
if rows.len() != 1 {
return std::result::Result::Err(data_invalid("raw_account_state_cardinality"));
}
let row = match rows.first() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_state_cardinality")),
};
let physical = match raw_account_state_db_row(row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let decoded = match decode_raw_account_state_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if decoded.reference() != reference {
return std::result::Result::Err(data_invalid("raw_account_state_reference"));
}
return std::result::Result::Ok(std::option::Option::Some(decoded));
}
/// Lists deterministic canonical RAW account-state references using PostgreSQL keyset pagination.
pub(crate) async fn list_raw_account_states(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
query: &ksp_store_api::RawAccountStateQuery,
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, crate::PostgresBackendError> {
if query.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, "raw_account_list_network"));
}
let (requested_usize, sql_limit) = match crate::raw_account_physical_page_limit(query.page().limit().get()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let decoded_cursor = match query.page().cursor() {
std::option::Option::Some(value) => match crate::decode_raw_account_cursor(query, value) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => std::option::Option::None,
};
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let slots = query.slots();
let start_text = slots.start_inclusive().map(|value| return value.to_string());
let end_text = slots.end_inclusive().map(|value| return value.to_string());
let cursor_slot_text = decoded_cursor.as_ref().map(|value| return value.last_slot.to_string());
let cursor_pubkey = decoded_cursor.as_ref().map(|value| return value.last_pubkey.to_vec());
let cursor_state_hash = decoded_cursor.as_ref().map(|value| return value.last_state_hash.to_vec());
let rows_result = match query.pubkey() {
std::option::Option::Some(pubkey) => {
let pubkey_bytes: &[u8] = pubkey.as_ref();
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_list_direction",
));
},
};
client.query(sql, &[&pubkey_bytes, &start_text, &end_text, &cursor_slot_text, &cursor_pubkey, &cursor_state_hash, &sql_limit]).await
},
std::option::Option::None => {
let sql = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => LIST_ACCOUNT_STATES_ASC_SQL,
ksp_store_api::RawSortDirection::Descending => LIST_ACCOUNT_STATES_DESC_SQL,
_ => {
return std::result::Result::Err(crate::PostgresBackendError::new(
crate::PostgresBackendErrorKind::QueryInvalid,
"raw_account_list_direction",
));
},
};
client.query(sql, &[&start_text, &end_text, &cursor_slot_text, &cursor_pubkey, &cursor_state_hash, &sql_limit]).await
},
};
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_list_query"));
},
};
let mut decoded = std::vec::Vec::with_capacity(rows.len());
for row in rows {
let physical = match raw_account_list_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let item = match decode_raw_account_list_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
decoded.push(item);
}
let has_more = decoded.len() > requested_usize;
if has_more {
decoded.truncate(requested_usize);
}
let next_cursor = if has_more {
let last = match decoded.last() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_list_page")),
};
match crate::encode_raw_account_cursor(query, last.0, last.1.pubkey(), &last.1.state_hash()) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
std::option::Option::None
};
let items = decoded.into_iter().map(|value| return value.1).collect();
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
}
/// Reads one persisted RAW account observation by producer-owned idempotence key.
pub(crate) async fn get_raw_account_observation(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
observation_key: &ksp_store_api::RawObservationKey,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountObservation>, crate::PostgresBackendError> {
let client_result = pool.get().await;
let client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let observation_key_bytes: &[u8] = observation_key.as_bytes();
let rows_result = client.query(GET_ACCOUNT_OBSERVATION_SQL, &[&observation_key_bytes]).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_account_observation_query"));
},
};
if rows.is_empty() {
return std::result::Result::Ok(std::option::Option::None);
}
if rows.len() != 1 {
return std::result::Result::Err(data_invalid("raw_account_observation_cardinality"));
}
let row = match rows.first() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_account_observation_cardinality")),
};
let physical = match raw_account_observation_db_row(row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let decoded = match decode_raw_account_observation_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if decoded.observation_key() != *observation_key {
return std::result::Result::Err(data_invalid("raw_account_observation_key"));
}
return std::result::Result::Ok(std::option::Option::Some(decoded));
}
/// Persists one canonical RAW account state and its acquisition observation atomically.
pub(crate) async fn persist_raw_account_acquisition(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
let input_result = ensure_acquisition_inputs(network, &state, &observation);
if let std::result::Result::Err(error) = input_result {
return std::result::Result::Err(error);
}
let client_result = pool.get().await;
let mut client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let sql_transaction_result = client.transaction().await;
let sql_transaction = match sql_transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_begin")),
};
let insert_result = insert_account_state(&sql_transaction, &state).await;
let inserted = match insert_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entity_outcome = if inserted {
ksp_store_api::RawEntityWriteOutcome::Inserted
} else {
let locked_result = load_locked_account_state(&sql_transaction, network, state.reference()).await;
let locked = match locked_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_acquisition_conflict_missing")),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !raw_account_states_equal(&locked, &state) {
return std::result::Result::Err(conflict("raw_account_acquisition_content_conflict"));
}
ksp_store_api::RawEntityWriteOutcome::AlreadyPresent
};
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
let observation_outcome = match observation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let commit_result = sql_transaction.commit().await;
if commit_result.is_err() {
return std::result::Result::Err(write_failed("raw_account_acquisition_commit"));
}
return std::result::Result::Ok(ksp_store_api::RawAcquisitionWriteOutcome::new(entity_outcome, observation_outcome));
}
/// Persists one additional acquisition observation for an already durable RAW account state.
pub(crate) async fn record_raw_account_observation(
pool: &deadpool_postgres::Pool,
network: &ksp_store_api::RawNetworkId,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
let input_result = ensure_observation_write_input(network, &observation);
if let std::result::Result::Err(error) = input_result {
return std::result::Result::Err(error);
}
let client_result = pool.get().await;
let mut client = match client_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
};
let sql_transaction_result = client.transaction().await;
let sql_transaction = match sql_transaction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_begin")),
};
let account = observation.account();
let account_pubkey_bytes: &[u8] = account.pubkey().as_ref();
let account_slot_text = account.slot().to_string();
let account_state_hash = account.state_hash();
let account_state_hash_bytes: &[u8] = account_state_hash.as_bytes();
let reference_result = sql_transaction
.query_opt(LOCK_ACCOUNT_REFERENCE_SQL, &[&account_pubkey_bytes, &account_slot_text.as_str(), &account_state_hash_bytes])
.await;
match reference_result {
std::result::Result::Ok(std::option::Option::Some(_)) => {},
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(reference_not_found("raw_account_observation_reference"));
},
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_lock_reference")),
}
let observation_result = persist_account_observation_row(&sql_transaction, network, &observation).await;
let outcome = match observation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let commit_result = sql_transaction.commit().await;
if commit_result.is_err() {
return std::result::Result::Err(write_failed("raw_account_observation_commit"));
}
return std::result::Result::Ok(outcome);
}
fn raw_account_list_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountListDbRow, crate::PostgresBackendError> {
let pubkey = match row.try_get::<_, std::vec::Vec<u8>>("pubkey") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
};
let slot_text = match row.try_get::<_, std::string::String>("slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
};
let state_hash = match row.try_get::<_, std::vec::Vec<u8>>("state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_list_decode")),
};
return std::result::Result::Ok(RawAccountListDbRow { pubkey, slot_text, state_hash });
}
fn decode_raw_account_list_row(
network: &ksp_store_api::RawNetworkId,
row: RawAccountListDbRow,
) -> std::result::Result<(u64, ksp_store_api::RawAccountStateReference), crate::PostgresBackendError> {
let pubkey = match fixed_bytes::<32>(row.pubkey, "raw_account_list_pubkey") {
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(row.slot_text.as_str(), "raw_account_list_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash = match fixed_bytes::<32>(row.state_hash, "raw_account_list_state_hash") {
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
return std::result::Result::Ok((slot, reference));
}
fn raw_account_observation_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountObservationDbRow, crate::PostgresBackendError> {
let account_pubkey: std::vec::Vec<u8> = match row.try_get("account_pubkey") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let account_slot_text: std::string::String = match row.try_get("account_slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let account_state_hash: std::vec::Vec<u8> = match row.try_get("account_state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let acquisition_method: std::string::String = match row.try_get("acquisition_method") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let capture_session_id: std::option::Option<std::string::String> = match row.try_get("capture_session_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let commitment: std::option::Option<std::string::String> = match row.try_get("commitment") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let endpoint_id: std::option::Option<std::string::String> = match row.try_get("endpoint_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let filter_id: std::option::Option<std::string::String> = match row.try_get("filter_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let is_startup: std::option::Option<bool> = match row.try_get("is_startup") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let observation_key: std::vec::Vec<u8> = match row.try_get("observation_key") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let observed_at_unix_millis: std::option::Option<i64> = match row.try_get("observed_at_unix_millis") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let origin: std::string::String = match row.try_get("origin") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let protocol: std::string::String = match row.try_get("protocol") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let provider: std::string::String = match row.try_get("provider") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let received_at_unix_millis: i64 = match row.try_get("received_at_unix_millis") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let source_payload_hash: std::option::Option<std::vec::Vec<u8>> = match row.try_get("source_payload_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let source_payload_size_bytes: std::option::Option<i64> = match row.try_get("source_payload_size_bytes") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let transaction_signature: std::option::Option<std::vec::Vec<u8>> = match row.try_get("transaction_signature") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
let write_version_text: std::option::Option<std::string::String> = match row.try_get("write_version_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_decode")),
};
return std::result::Result::Ok(RawAccountObservationDbRow {
account_pubkey,
account_slot_text,
account_state_hash,
acquisition_method,
capture_session_id,
commitment,
endpoint_id,
filter_id,
is_startup,
observation_key,
observed_at_unix_millis,
origin,
protocol,
provider,
received_at_unix_millis,
source_payload_hash,
source_payload_size_bytes,
transaction_signature,
write_version_text,
});
}
fn raw_account_state_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawAccountStateDbRow, crate::PostgresBackendError> {
let data: std::vec::Vec<u8> = match row.try_get("data") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let executable: bool = match row.try_get("executable") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let lamports_text: std::string::String = match row.try_get("lamports_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let owner: std::vec::Vec<u8> = match row.try_get("owner") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let pubkey: std::vec::Vec<u8> = match row.try_get("pubkey") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let rent_epoch_text: std::string::String = match row.try_get("rent_epoch_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let slot_text: std::string::String = match row.try_get("slot_text") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
let state_hash: std::vec::Vec<u8> = match row.try_get("state_hash") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_state_decode")),
};
return std::result::Result::Ok(RawAccountStateDbRow { data, executable, lamports_text, owner, pubkey, rent_epoch_text, slot_text, state_hash });
}
fn decode_raw_account_observation_row(
network: &ksp_store_api::RawNetworkId,
row: RawAccountObservationDbRow,
) -> std::result::Result<ksp_store_api::RawAccountObservation, crate::PostgresBackendError> {
let observation_key = match fixed_bytes::<32>(row.observation_key, "raw_account_observation_key") {
std::result::Result::Ok(value) => ksp_store_api::RawObservationKey::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pubkey = match fixed_bytes::<32>(row.account_pubkey, "raw_account_observation_pubkey") {
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(row.account_slot_text.as_str(), "raw_account_observation_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash = match fixed_bytes::<32>(row.account_state_hash, "raw_account_observation_state_hash") {
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider = match decode_provenance_code(row.provider) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let protocol = match decode_provenance_code(row.protocol) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let acquisition_method = match decode_provenance_code(row.acquisition_method) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let origin = match decode_origin(row.origin.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let received_at = match decode_timestamp_i64(row.received_at_unix_millis, "raw_account_observation_received_at") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, acquisition_method, origin, received_at);
provenance = match row.capture_session_id {
std::option::Option::Some(value) => match decode_provenance_code(value) {
std::result::Result::Ok(code) => provenance.with_capture_session_id(code),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => provenance,
};
provenance = match row.commitment {
std::option::Option::Some(value) => match decode_provenance_code(value) {
std::result::Result::Ok(code) => provenance.with_commitment(code),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => provenance,
};
provenance = match row.endpoint_id {
std::option::Option::Some(value) => match decode_provenance_code(value) {
std::result::Result::Ok(code) => provenance.with_endpoint_id(code),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => provenance,
};
provenance = match row.filter_id {
std::option::Option::Some(value) => match decode_provenance_code(value) {
std::result::Result::Ok(code) => provenance.with_filter_id(code),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => provenance,
};
provenance = match row.observed_at_unix_millis {
std::option::Option::Some(value) => {
let timestamp = match decode_timestamp_i64(value, "raw_account_observation_observed_at") {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match provenance.try_with_observed_at(timestamp) {
std::result::Result::Ok(updated) => updated,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_time_order")),
}
},
std::option::Option::None => provenance,
};
provenance = match row.source_payload_hash {
std::option::Option::Some(value) => {
let hash = match fixed_bytes::<32>(value, "raw_account_observation_source_hash") {
std::result::Result::Ok(decoded) => ksp_store_api::RawContentHash::new(decoded),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
provenance.with_source_payload_hash(hash)
},
std::option::Option::None => provenance,
};
provenance = match row.source_payload_size_bytes {
std::option::Option::Some(value) => {
let size = match decode_u64_i64(value, "raw_account_observation_source_size") {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match provenance.try_with_source_payload_size_bytes(size) {
std::result::Result::Ok(updated) => updated,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_source_size")),
}
},
std::option::Option::None => provenance,
};
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
let mut observation = ksp_store_api::RawAccountObservation::new(observation_key, reference, provenance);
observation = match row.is_startup {
std::option::Option::Some(value) => observation.with_is_startup(value),
std::option::Option::None => observation,
};
observation = match row.transaction_signature {
std::option::Option::Some(value) => {
let signature = match fixed_bytes::<64>(value, "raw_account_observation_transaction_signature") {
std::result::Result::Ok(decoded) => ksp_store_api::RawTransactionSignature::new(decoded),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
observation.with_transaction_signature(signature)
},
std::option::Option::None => observation,
};
observation = match row.write_version_text {
std::option::Option::Some(value) => match decode_u64_decimal(value.as_str(), "raw_account_observation_write_version") {
std::result::Result::Ok(decoded) => observation.with_write_version(decoded),
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
std::option::Option::None => observation,
};
return std::result::Result::Ok(observation);
}
fn decode_raw_account_state_row(
network: &ksp_store_api::RawNetworkId,
row: RawAccountStateDbRow,
) -> std::result::Result<ksp_store_api::RawAccountState, crate::PostgresBackendError> {
let pubkey = match fixed_bytes::<32>(row.pubkey, "raw_account_state_pubkey") {
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let slot = match decode_u64_decimal(row.slot_text.as_str(), "raw_account_state_slot") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state_hash = match fixed_bytes::<32>(row.state_hash, "raw_account_state_hash") {
std::result::Result::Ok(value) => ksp_store_api::RawContentHash::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let lamports = match decode_u64_decimal(row.lamports_text.as_str(), "raw_account_state_lamports") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match fixed_bytes::<32>(row.owner, "raw_account_state_owner") {
std::result::Result::Ok(value) => ksp_store_api::Pubkey::new_from_array(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_epoch = match decode_u64_decimal(row.rent_epoch_text.as_str(), "raw_account_state_rent_epoch") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let reference = ksp_store_api::RawAccountStateReference::new(network.clone(), pubkey, slot, state_hash);
return match ksp_store_api::RawAccountState::try_new(reference, lamports, owner, row.executable, rent_epoch, row.data.into_boxed_slice()) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_state_model")),
};
}
async fn insert_account_state(
sql_transaction: &deadpool_postgres::Transaction<'_>,
state: &ksp_store_api::RawAccountState,
) -> std::result::Result<bool, crate::PostgresBackendError> {
let reference = state.reference();
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
let slot_text = reference.slot().to_string();
let state_hash = reference.state_hash();
let state_hash_bytes: &[u8] = state_hash.as_bytes();
let lamports_text = state.lamports().to_string();
let owner_bytes: &[u8] = state.owner().as_ref();
let executable = state.executable();
let rent_epoch_text = state.rent_epoch().to_string();
let data = state.data();
let row_result = sql_transaction
.query_opt(
INSERT_ACCOUNT_STATE_SQL,
&[&pubkey_bytes, &slot_text.as_str(), &state_hash_bytes, &lamports_text.as_str(), &owner_bytes, &executable, &rent_epoch_text.as_str(), &data],
)
.await;
return match row_result {
std::result::Result::Ok(std::option::Option::Some(_)) => std::result::Result::Ok(true),
std::result::Result::Ok(std::option::Option::None) => std::result::Result::Ok(false),
std::result::Result::Err(_) => std::result::Result::Err(write_failed("raw_account_acquisition_insert_state")),
};
}
async fn load_locked_account_state(
sql_transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
reference: &ksp_store_api::RawAccountStateReference,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
let pubkey_bytes: &[u8] = reference.pubkey().as_ref();
let slot_text = reference.slot().to_string();
let state_hash = reference.state_hash();
let state_hash_bytes: &[u8] = state_hash.as_bytes();
let row_result = sql_transaction.query_opt(LOCK_ACCOUNT_STATE_SQL, &[&pubkey_bytes, &slot_text.as_str(), &state_hash_bytes]).await;
let row = match row_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Ok(std::option::Option::None),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_acquisition_lock_state")),
};
let physical = match raw_account_state_db_row(&row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match decode_raw_account_state_row(network, physical) {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
async fn persist_account_observation_row(
sql_transaction: &deadpool_postgres::Transaction<'_>,
network: &ksp_store_api::RawNetworkId,
observation: &ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
let provenance = observation.provenance();
let observation_key = observation.observation_key();
let observation_key_bytes: &[u8] = observation_key.as_bytes();
let account = observation.account();
let account_pubkey_bytes: &[u8] = account.pubkey().as_ref();
let account_slot_text = account.slot().to_string();
let account_state_hash = account.state_hash();
let account_state_hash_bytes: &[u8] = account_state_hash.as_bytes();
let origin = match encode_origin(provenance.origin()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let received_at = match i64::try_from(provenance.received_at().unix_millis()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_received_at_encode")),
};
let observed_at = match provenance.observed_at() {
std::option::Option::Some(value) => match i64::try_from(value.unix_millis()) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_observed_at_encode")),
},
std::option::Option::None => std::option::Option::None,
};
let source_payload_size = match provenance.source_payload_size_bytes() {
std::option::Option::Some(value) => match i64::try_from(value) {
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_account_observation_source_size_encode")),
},
std::option::Option::None => std::option::Option::None,
};
let capture_session_id = provenance.capture_session_id().map(|value| return value.as_str());
let commitment = provenance.commitment().map(|value| return value.as_str());
let endpoint_id = provenance.endpoint_id().map(|value| return value.as_str());
let filter_id = provenance.filter_id().map(|value| return value.as_str());
let source_payload_hash = provenance.source_payload_hash();
let source_payload_hash_bytes: std::option::Option<&[u8]> = source_payload_hash.as_ref().map(|value| return &value.as_bytes()[..]);
let is_startup = observation.is_startup();
let transaction_signature = observation.transaction_signature();
let transaction_signature_bytes: std::option::Option<&[u8]> = transaction_signature.as_ref().map(|value| return &value.as_bytes()[..]);
let write_version_text = observation.write_version().map(|value| return value.to_string());
let row_result = sql_transaction
.query_opt(
INSERT_ACCOUNT_OBSERVATION_SQL,
&[
&observation_key_bytes,
&account_pubkey_bytes,
&account_slot_text.as_str(),
&account_state_hash_bytes,
&provenance.provider().as_str(),
&provenance.protocol().as_str(),
&provenance.acquisition_method().as_str(),
&origin,
&received_at,
&capture_session_id,
&commitment,
&endpoint_id,
&filter_id,
&observed_at,
&source_payload_hash_bytes,
&source_payload_size,
&is_startup,
&transaction_signature_bytes,
&write_version_text,
],
)
.await;
let inserted = match row_result {
std::result::Result::Ok(std::option::Option::Some(_)) => true,
std::result::Result::Ok(std::option::Option::None) => false,
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_insert")),
};
if inserted {
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::Inserted);
}
let existing_result = sql_transaction.query_opt(LOCK_ACCOUNT_OBSERVATION_SQL, &[&observation_key_bytes]).await;
let existing_row = match existing_result {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(data_invalid("raw_account_observation_conflict_missing")),
std::result::Result::Err(_) => return std::result::Result::Err(write_failed("raw_account_observation_conflict_query")),
};
let physical = match raw_account_observation_db_row(&existing_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let stored = match decode_raw_account_observation_row(network, physical) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if stored.eq(observation) {
return std::result::Result::Ok(ksp_store_api::RawObservationWriteOutcome::AlreadyPresent);
}
return std::result::Result::Err(conflict("raw_account_observation_content_conflict"));
}
fn ensure_acquisition_inputs(
network: &ksp_store_api::RawNetworkId,
state: &ksp_store_api::RawAccountState,
observation: &ksp_store_api::RawAccountObservation,
) -> std::result::Result<(), crate::PostgresBackendError> {
let state_network_result = ensure_network(network, state.reference(), "raw_account_acquisition_state_network");
if let std::result::Result::Err(error) = state_network_result {
return std::result::Result::Err(error);
}
let observation_network_result = ensure_network(network, observation.account(), "raw_account_acquisition_observation_network");
if let std::result::Result::Err(error) = observation_network_result {
return std::result::Result::Err(error);
}
if observation.account() != state.reference() {
return std::result::Result::Err(conflict("raw_account_acquisition_reference_mismatch"));
}
return std::result::Result::Ok(());
}
fn ensure_observation_write_input(
network: &ksp_store_api::RawNetworkId,
observation: &ksp_store_api::RawAccountObservation,
) -> std::result::Result<(), crate::PostgresBackendError> {
return ensure_network(network, observation.account(), "raw_account_observation_write_network");
}
fn raw_account_states_equal(left: &ksp_store_api::RawAccountState, right: &ksp_store_api::RawAccountState) -> bool {
return left.reference() == right.reference()
&& left.lamports() == right.lamports()
&& left.owner() == right.owner()
&& left.executable() == right.executable()
&& left.rent_epoch() == right.rent_epoch()
&& left.data() == right.data();
}
fn encode_origin(origin: ksp_store_api::RawAcquisitionOrigin) -> std::result::Result<&'static str, crate::PostgresBackendError> {
return match origin {
ksp_store_api::RawAcquisitionOrigin::Backfill => std::result::Result::Ok("backfill"),
ksp_store_api::RawAcquisitionOrigin::Import => std::result::Result::Ok("import"),
ksp_store_api::RawAcquisitionOrigin::Live => std::result::Result::Ok("live"),
ksp_store_api::RawAcquisitionOrigin::Repair => std::result::Result::Ok("repair"),
ksp_store_api::RawAcquisitionOrigin::Replay => std::result::Result::Ok("replay"),
_ => std::result::Result::Err(data_invalid("raw_account_observation_origin_encode")),
};
}
fn conflict(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::Conflict, phase);
}
fn decode_origin(value: &str) -> std::result::Result<ksp_store_api::RawAcquisitionOrigin, crate::PostgresBackendError> {
return match value {
"backfill" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Backfill),
"import" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Import),
"live" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Live),
"repair" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Repair),
"replay" => std::result::Result::Ok(ksp_store_api::RawAcquisitionOrigin::Replay),
_ => std::result::Result::Err(data_invalid("raw_account_observation_origin")),
};
}
fn decode_provenance_code(value: std::string::String) -> std::result::Result<ksp_store_api::RawProvenanceCode, crate::PostgresBackendError> {
return match ksp_store_api::RawProvenanceCode::new(value) {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid("raw_account_provenance_code")),
};
}
fn decode_timestamp_i64(value: i64, phase: &'static str) -> std::result::Result<ksp_store_api::RawTimestamp, crate::PostgresBackendError> {
let unsigned = match u64::try_from(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid(phase)),
};
return match ksp_store_api::RawTimestamp::from_unix_millis(unsigned) {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
};
}
fn decode_u64_decimal(value: &str, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
return match value.parse::<u64>() {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
};
}
fn decode_u64_i64(value: i64, phase: &'static str) -> std::result::Result<u64, crate::PostgresBackendError> {
return match u64::try_from(value) {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
};
}
fn fixed_bytes<const N: usize>(value: std::vec::Vec<u8>, phase: &'static str) -> std::result::Result<[u8; N], crate::PostgresBackendError> {
return match <[u8; N]>::try_from(value.as_slice()) {
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
std::result::Result::Err(_) => std::result::Result::Err(data_invalid(phase)),
};
}
fn ensure_network(
network: &ksp_store_api::RawNetworkId,
reference: &ksp_store_api::RawAccountStateReference,
phase: &'static str,
) -> std::result::Result<(), crate::PostgresBackendError> {
if reference.network() != network {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, phase));
}
return std::result::Result::Ok(());
}
fn data_invalid(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::DataInvalid, phase);
}
fn reference_not_found(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReferenceNotFound, phase);
}
fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
}
#[cfg(test)]
#[path = "../unit_tests/raw_account.rs"]
mod tests;

View File

@@ -0,0 +1,230 @@
// file: crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
// version: 1
use sha2::Digest; // rust-rules: trait-import
const CURSOR_BYTES: usize = 109;
const CURSOR_DIGEST_OFFSET: usize = 77;
const CURSOR_DOMAIN: &[u8] = b"KSP/raw-account-state-cursor/v1";
const CURSOR_MAGIC: &[u8; 4] = b"KSPA";
const CURSOR_VERSION: u8 = 1;
const MAX_POSTGRES_PAGE_LIMIT: u64 = 9_223_372_036_854_775_806;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Decoded backend-private continuation key extracted from one validated RAW account cursor.
pub(crate) struct RawAccountDecodedCursor {
/// Last canonical account pubkey returned by the previous page.
pub(crate) last_pubkey: [u8; 32],
/// Last canonical account-state hash returned by the previous page.
pub(crate) last_state_hash: [u8; 32],
/// Last canonical account slot returned by the previous page.
pub(crate) last_slot: u64,
}
/// Decodes and validates one backend-private RAW account cursor against its query binding.
pub(crate) fn decode_raw_account_cursor(
query: &ksp_store_api::RawAccountStateQuery,
cursor: &ksp_store_api::RawPageCursor,
) -> std::result::Result<RawAccountDecodedCursor, crate::PostgresBackendError> {
let bytes = cursor.as_bytes();
if bytes.len() != CURSOR_BYTES {
return std::result::Result::Err(query_invalid("raw_account_cursor_size"));
}
if bytes.get(0..4) != std::option::Option::Some(CURSOR_MAGIC.as_ref()) {
return std::result::Result::Err(query_invalid("raw_account_cursor_magic"));
}
if bytes.get(4).copied() != std::option::Option::Some(CURSOR_VERSION) {
return std::result::Result::Err(query_invalid("raw_account_cursor_version"));
}
let slot_bytes = match bytes.get(5..13) {
std::option::Option::Some(value) => match <[u8; 8]>::try_from(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_slot")),
},
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_slot")),
};
let last_slot = u64::from_be_bytes(slot_bytes);
let last_pubkey = match bytes.get(13..45) {
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
},
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
};
let last_state_hash = match bytes.get(45..CURSOR_DIGEST_OFFSET) {
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_state_hash")),
},
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_state_hash")),
};
let range_result = validate_slot_in_range(query.slots(), last_slot);
if let std::result::Result::Err(error) = range_result {
return std::result::Result::Err(error);
}
let pubkey_result = validate_pubkey_filter(query.pubkey(), &last_pubkey);
if let std::result::Result::Err(error) = pubkey_result {
return std::result::Result::Err(error);
}
let expected_digest = match binding_digest(query, last_slot, &last_pubkey, &last_state_hash) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let stored_digest = match bytes.get(CURSOR_DIGEST_OFFSET..CURSOR_BYTES) {
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_digest")),
},
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_account_cursor_digest")),
};
if stored_digest != expected_digest {
return std::result::Result::Err(query_invalid("raw_account_cursor_binding"));
}
return std::result::Result::Ok(RawAccountDecodedCursor { last_pubkey, last_state_hash, last_slot });
}
/// Encodes one backend-private RAW account cursor bound to the supplied query context.
pub(crate) fn encode_raw_account_cursor(
query: &ksp_store_api::RawAccountStateQuery,
last_slot: u64,
last_pubkey: &ksp_store_api::Pubkey,
last_state_hash: &ksp_store_api::RawContentHash,
) -> std::result::Result<ksp_store_api::RawPageCursor, crate::PostgresBackendError> {
let range_result = validate_slot_in_range(query.slots(), last_slot);
if let std::result::Result::Err(error) = range_result {
return std::result::Result::Err(error);
}
let pubkey_bytes = match <[u8; 32]>::try_from(last_pubkey.as_ref()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey")),
};
let pubkey_result = validate_pubkey_filter(query.pubkey(), &pubkey_bytes);
if let std::result::Result::Err(error) = pubkey_result {
return std::result::Result::Err(error);
}
let digest = match binding_digest(query, last_slot, &pubkey_bytes, last_state_hash.as_bytes()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut bytes = std::vec::Vec::with_capacity(CURSOR_BYTES);
bytes.extend_from_slice(CURSOR_MAGIC);
bytes.push(CURSOR_VERSION);
bytes.extend_from_slice(&last_slot.to_be_bytes());
bytes.extend_from_slice(&pubkey_bytes);
bytes.extend_from_slice(last_state_hash.as_bytes());
bytes.extend_from_slice(&digest);
if bytes.len() != CURSOR_BYTES {
return std::result::Result::Err(query_invalid("raw_account_cursor_encode_size"));
}
return match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(query_invalid("raw_account_cursor_encode")),
};
}
/// Converts one caller page size into the exact PostgreSQL LIMIT+1 representation.
pub(crate) fn raw_account_physical_page_limit(requested: u64) -> std::result::Result<(usize, i64), crate::PostgresBackendError> {
if requested > MAX_POSTGRES_PAGE_LIMIT {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
}
let requested_usize = match usize::try_from(requested) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
},
};
let requested_plus_one = match requested.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
},
};
let sql_limit = match i64::try_from(requested_plus_one) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_account_page_limit"));
},
};
return std::result::Result::Ok((requested_usize, sql_limit));
}
fn binding_digest(
query: &ksp_store_api::RawAccountStateQuery,
last_slot: u64,
last_pubkey: &[u8; 32],
last_state_hash: &[u8; 32],
) -> std::result::Result<[u8; 32], crate::PostgresBackendError> {
let direction = match query.direction() {
ksp_store_api::RawSortDirection::Ascending => 0_u8,
ksp_store_api::RawSortDirection::Descending => 1_u8,
_ => return std::result::Result::Err(query_invalid("raw_account_cursor_direction")),
};
let network_len = match u64::try_from(query.network().as_str().len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_account_cursor_network")),
};
let mut hasher = sha2::Sha256::new();
hasher.update(CURSOR_DOMAIN);
hasher.update(network_len.to_be_bytes());
hasher.update(query.network().as_str().as_bytes());
hash_optional_pubkey(&mut hasher, query.pubkey());
hasher.update([direction]);
hash_optional_slot(&mut hasher, query.slots().start_inclusive());
hash_optional_slot(&mut hasher, query.slots().end_inclusive());
hasher.update(last_slot.to_be_bytes());
hasher.update(last_pubkey);
hasher.update(last_state_hash);
let finalized = hasher.finalize();
let mut digest = [0_u8; 32];
digest.copy_from_slice(finalized.as_ref());
return std::result::Result::Ok(digest);
}
fn hash_optional_pubkey(hasher: &mut sha2::Sha256, value: std::option::Option<&ksp_store_api::Pubkey>) {
match value {
std::option::Option::Some(pubkey) => {
hasher.update([1_u8]);
hasher.update(pubkey.as_ref());
},
std::option::Option::None => hasher.update([0_u8]),
}
return;
}
fn hash_optional_slot(hasher: &mut sha2::Sha256, value: std::option::Option<u64>) {
match value {
std::option::Option::Some(slot) => {
hasher.update([1_u8]);
hasher.update(slot.to_be_bytes());
},
std::option::Option::None => hasher.update([0_u8]),
}
return;
}
fn query_invalid(phase: &'static str) -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, phase);
}
fn validate_pubkey_filter(filter: std::option::Option<&ksp_store_api::Pubkey>, last_pubkey: &[u8; 32]) -> std::result::Result<(), crate::PostgresBackendError> {
if let std::option::Option::Some(pubkey) = filter
&& pubkey.as_ref() != last_pubkey.as_slice()
{
return std::result::Result::Err(query_invalid("raw_account_cursor_pubkey_filter"));
}
return std::result::Result::Ok(());
}
fn validate_slot_in_range(range: ksp_store_api::RawSlotRange, slot: u64) -> std::result::Result<(), crate::PostgresBackendError> {
if let std::option::Option::Some(start) = range.start_inclusive()
&& slot < start
{
return std::result::Result::Err(query_invalid("raw_account_cursor_slot_range"));
}
if let std::option::Option::Some(end) = range.end_inclusive()
&& slot > end
{
return std::result::Result::Err(query_invalid("raw_account_cursor_slot_range"));
}
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/runtime.rs
// version: 10
// version: 15
const APPLICATION_NAME: &str = "ksp-store";
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
@@ -332,6 +332,47 @@ impl PostgresBackend {
return crate::probe_health(&self.pool).await;
}
/// Reads one complete canonical RAW account state without exposing physical PostgreSQL row types.
pub async fn get_raw_account_state(
&self,
reference: &ksp_store_api::RawAccountStateReference,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountState>, crate::PostgresBackendError> {
return crate::get_raw_account_state(&self.pool, &self.network, reference).await;
}
/// Lists deterministic canonical RAW account-state references with a backend-owned opaque continuation cursor.
pub async fn list_raw_account_states(
&self,
query: &ksp_store_api::RawAccountStateQuery,
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>, crate::PostgresBackendError> {
return crate::list_raw_account_states(&self.pool, &self.network, query).await;
}
/// Reads one persisted RAW account observation by producer-owned idempotence key.
pub async fn get_raw_account_observation(
&self,
observation_key: &ksp_store_api::RawObservationKey,
) -> std::result::Result<std::option::Option<ksp_store_api::RawAccountObservation>, crate::PostgresBackendError> {
return crate::get_raw_account_observation(&self.pool, &self.network, observation_key).await;
}
/// Persists one complete RAW account state and its acquisition observation atomically.
pub async fn persist_raw_account_acquisition(
&self,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawAcquisitionWriteOutcome, crate::PostgresBackendError> {
return crate::persist_raw_account_acquisition(&self.pool, &self.network, state, observation).await;
}
/// Persists one additional RAW account observation for an already durable account state.
pub async fn record_raw_account_observation(
&self,
observation: ksp_store_api::RawAccountObservation,
) -> std::result::Result<ksp_store_api::RawObservationWriteOutcome, crate::PostgresBackendError> {
return crate::record_raw_account_observation(&self.pool, &self.network, observation).await;
}
/// Reads one canonical RAW transaction without exposing physical PostgreSQL row types.
pub async fn get_raw_transaction(
&self,
@@ -428,6 +469,65 @@ impl std::fmt::Debug for PostgresBackend {
}
}
impl ksp_store_api::RawAccountObservationRead for PostgresBackend {
fn get_raw_account_observation<'a>(
&'a self,
observation_key: &'a ksp_store_api::RawObservationKey,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountObservation>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::get_raw_account_observation(self, observation_key).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountObservationWrite for PostgresBackend {
fn record_raw_account_observation<'a>(
&'a self,
observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawObservationWriteOutcome>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::record_raw_account_observation(self, observation).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountStateRead for PostgresBackend {
fn get_raw_account_state<'a>(
&'a self,
reference: &'a ksp_store_api::RawAccountStateReference,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<std::option::Option<ksp_store_api::RawAccountState>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::get_raw_account_state(self, reference).await;
return result.map_err(map_capability_error);
});
}
fn list_raw_account_states<'a>(
&'a self,
query: &'a ksp_store_api::RawAccountStateQuery,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawPage<ksp_store_api::RawAccountStateReference>>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::list_raw_account_states(self, query).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawAccountStateWrite for PostgresBackend {
fn persist_raw_account_acquisition<'a>(
&'a self,
state: ksp_store_api::RawAccountState,
observation: ksp_store_api::RawAccountObservation,
) -> ksp_store_api::StoreApiFuture<'a, ksp_store_api::Result<ksp_store_api::RawAcquisitionWriteOutcome>> {
return std::boxed::Box::pin(async move {
let result = PostgresBackend::persist_raw_account_acquisition(self, state, observation).await;
return result.map_err(map_capability_error);
});
}
}
impl ksp_store_api::RawTransactionRead for PostgresBackend {
fn get_raw_transaction<'a>(
&'a self,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/schema.rs
// version: 7
// version: 9
/// Immutable V000 physical schema resource inventory.
pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource {
@@ -370,7 +370,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
access_method: "btree",
key_fragment: "slot,signature",
name: "ix_ksp_raw_transactions_slot_signature",
predicate_fragment: "retention_state<>'purged'",
predicate_fragment: std::option::Option::Some("retention_state<>'purged'"),
table: "ksp_raw_transactions",
unique: false,
}),
@@ -378,6 +378,308 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
sql: include_str!("../migrations/v001_raw_transaction/indexes/001_ix_ksp_raw_transactions_slot_signature.sql"),
},
];
/// Final V002 physical schema resource inventory for RAW account state persistence.
pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
SchemaResource {
id: "tables/001_ksp_raw_account_states.sql",
object: SchemaObjectContract::Table(TableContract {
columns: RAW_ACCOUNT_STATES_COLUMNS,
name: "ksp_raw_account_states",
primary_key_columns: std::option::Option::None,
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql"),
},
SchemaResource {
id: "tables/002_ksp_raw_account_observations.sql",
object: SchemaObjectContract::Table(TableContract {
columns: RAW_ACCOUNT_OBSERVATIONS_COLUMNS,
name: "ksp_raw_account_observations",
primary_key_columns: std::option::Option::None,
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql"),
},
SchemaResource {
id: "constraints/001_pk_ksp_raw_account_states.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "p", name: "pk_ksp_raw_account_states", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/001_pk_ksp_raw_account_states.sql"),
},
SchemaResource {
id: "constraints/002_pk_ksp_raw_account_observations.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "p",
name: "pk_ksp_raw_account_observations",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/002_pk_ksp_raw_account_observations.sql"),
},
SchemaResource {
id: "constraints/003_fk_ksp_raw_account_observations_state.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "f",
name: "fk_ksp_raw_account_observations_state",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql"),
},
SchemaResource {
id: "constraints/004_ck_ksp_raw_account_states_pubkey.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_pubkey", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql"),
},
SchemaResource {
id: "constraints/005_ck_ksp_raw_account_states_slot.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_slot", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql"),
},
SchemaResource {
id: "constraints/006_ck_ksp_raw_account_states_state_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_states_state_hash",
table: "ksp_raw_account_states",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql"),
},
SchemaResource {
id: "constraints/007_ck_ksp_raw_account_states_lamports.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_lamports", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql"),
},
SchemaResource {
id: "constraints/008_ck_ksp_raw_account_states_owner.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_owner", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql"),
},
SchemaResource {
id: "constraints/009_ck_ksp_raw_account_states_rent_epoch.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_states_rent_epoch",
table: "ksp_raw_account_states",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql"),
},
SchemaResource {
id: "constraints/010_ck_ksp_raw_account_states_data.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_data", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql"),
},
SchemaResource {
id: "constraints/011_ck_ksp_raw_account_observations_key.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_key",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql"),
},
SchemaResource {
id: "constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_pubkey",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql"),
},
SchemaResource {
id: "constraints/013_ck_ksp_raw_account_observations_account_slot.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_slot",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql"),
},
SchemaResource {
id: "constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_state_hash",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql"),
},
SchemaResource {
id: "constraints/015_ck_ksp_raw_account_observations_provider.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_provider",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql"),
},
SchemaResource {
id: "constraints/016_ck_ksp_raw_account_observations_protocol.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_protocol",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql"),
},
SchemaResource {
id: "constraints/017_ck_ksp_raw_account_observations_method.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_method",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql"),
},
SchemaResource {
id: "constraints/018_ck_ksp_raw_account_observations_origin.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_origin",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql"),
},
SchemaResource {
id: "constraints/019_ck_ksp_raw_account_observations_received_at.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_received_at",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql"),
},
SchemaResource {
id: "constraints/020_ck_ksp_raw_account_observations_capture_session.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_capture_session",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql"),
},
SchemaResource {
id: "constraints/021_ck_ksp_raw_account_observations_commitment.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_commitment",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql"),
},
SchemaResource {
id: "constraints/022_ck_ksp_raw_account_observations_endpoint.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_endpoint",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql"),
},
SchemaResource {
id: "constraints/023_ck_ksp_raw_account_observations_filter.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_filter",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql"),
},
SchemaResource {
id: "constraints/024_ck_ksp_raw_account_observations_observed_at.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_observed_at",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql"),
},
SchemaResource {
id: "constraints/025_ck_ksp_raw_account_observations_time_order.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_time_order",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql"),
},
SchemaResource {
id: "constraints/026_ck_ksp_raw_account_observations_source_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_source_hash",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql"),
},
SchemaResource {
id: "constraints/027_ck_ksp_raw_account_observations_source_size.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_source_size",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql"),
},
SchemaResource {
id: "constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_transaction_signature",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql"),
},
SchemaResource {
id: "constraints/029_ck_ksp_raw_account_observations_write_version.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_write_version",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql"),
},
SchemaResource {
id: "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql",
object: SchemaObjectContract::Index(IndexContract {
access_method: "btree",
key_fragment: "slot,pubkey,state_hash",
name: "ix_ksp_raw_account_states_slot_pubkey_state_hash",
predicate_fragment: std::option::Option::None,
table: "ksp_raw_account_states",
unique: false,
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql"),
},
];
const COLUMN_LOAD_SQL: &str = r#"SELECT column_name::TEXT, udt_name::TEXT, (is_nullable = 'YES') AS nullable, numeric_precision::INTEGER, numeric_scale::INTEGER, column_default::TEXT, is_identity::TEXT, is_generated::TEXT
FROM information_schema.columns
@@ -404,7 +706,7 @@ WHERE ns.nspname = current_schema() AND table_rel.relname = $1 AND index_rel.rel
const MANAGED_OBJECT_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads')
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads', 'ksp_raw_account_states', 'ksp_raw_account_observations')
AND table_type = 'BASE TABLE'
)"#;
const PRIMARY_KEY_LOAD_SQL: &str = r#"SELECT string_agg(att.attname::TEXT, ',' ORDER BY key_part.ord)::TEXT
@@ -464,7 +766,7 @@ struct IndexContract {
access_method: &'static str,
key_fragment: &'static str,
name: &'static str,
predicate_fragment: &'static str,
predicate_fragment: std::option::Option<&'static str>,
table: &'static str,
unique: bool,
}
@@ -710,6 +1012,199 @@ const RAW_TRANSACTION_OBSERVATIONS_COLUMNS: &[ColumnContract] = &[
udt_name: "int8",
},
];
const RAW_ACCOUNT_STATES_COLUMNS: &[ColumnContract] = &[
ColumnContract {
name: "pubkey",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "slot",
nullable: false,
numeric_precision: std::option::Option::Some(20),
numeric_scale: std::option::Option::Some(0),
udt_name: "numeric",
},
ColumnContract {
name: "state_hash",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "lamports",
nullable: false,
numeric_precision: std::option::Option::Some(20),
numeric_scale: std::option::Option::Some(0),
udt_name: "numeric",
},
ColumnContract {
name: "owner",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "executable",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bool",
},
ColumnContract {
name: "rent_epoch",
nullable: false,
numeric_precision: std::option::Option::Some(20),
numeric_scale: std::option::Option::Some(0),
udt_name: "numeric",
},
ColumnContract {
name: "data",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
];
const RAW_ACCOUNT_OBSERVATIONS_COLUMNS: &[ColumnContract] = &[
ColumnContract {
name: "observation_key",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "account_pubkey",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "account_slot",
nullable: false,
numeric_precision: std::option::Option::Some(20),
numeric_scale: std::option::Option::Some(0),
udt_name: "numeric",
},
ColumnContract {
name: "account_state_hash",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "provider",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "protocol",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "acquisition_method",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "origin",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "received_at_unix_millis",
nullable: false,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "int8",
},
ColumnContract {
name: "capture_session_id",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "commitment",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "endpoint_id",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "filter_id",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "text",
},
ColumnContract {
name: "observed_at_unix_millis",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "int8",
},
ColumnContract {
name: "source_payload_hash",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "source_payload_size_bytes",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "int8",
},
ColumnContract {
name: "is_startup",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bool",
},
ColumnContract {
name: "transaction_signature",
nullable: true,
numeric_precision: std::option::Option::None,
numeric_scale: std::option::Option::None,
udt_name: "bytea",
},
ColumnContract {
name: "write_version",
nullable: true,
numeric_precision: std::option::Option::Some(20),
numeric_scale: std::option::Option::Some(0),
udt_name: "numeric",
},
];
const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
ColumnContract {
name: "signature",
@@ -727,6 +1222,7 @@ const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
},
];
const V001_TABLE_NAMES: &[&str] = &["ksp_store_identity", "ksp_raw_transactions", "ksp_raw_transaction_observations", "ksp_raw_transaction_archive_payloads"];
const V002_TABLE_NAMES: &[&str] = &["ksp_raw_account_states", "ksp_raw_account_observations"];
/// Inspects one embedded schema resource against the effective PostgreSQL catalog.
pub(crate) async fn inspect_resource(
@@ -740,8 +1236,8 @@ pub(crate) async fn inspect_resource(
};
}
/// Returns whether any V001-managed base table already exists in the active schema.
pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
/// Returns whether any KSP-managed Store base table already exists in the active schema.
pub(crate) async fn managed_schema_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
let result = transaction.query_one(MANAGED_OBJECT_EXISTS_SQL, &[]).await;
let row = match result {
std::result::Result::Ok(value) => value,
@@ -757,7 +1253,22 @@ pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::
pub(crate) async fn verify_v001_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
) -> std::result::Result<(), crate::PostgresBackendError> {
for table in V001_TABLE_NAMES {
return verify_external_compatibility(transaction, V001_TABLE_NAMES, V001_RESOURCES).await;
}
/// Rejects external V002 schema extensions that can constrain or mutate KSP writes.
pub(crate) async fn verify_v002_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
) -> std::result::Result<(), crate::PostgresBackendError> {
return verify_external_compatibility(transaction, V002_TABLE_NAMES, V002_RESOURCES).await;
}
async fn verify_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
tables: &[&str],
resources: &[SchemaResource],
) -> std::result::Result<(), crate::PostgresBackendError> {
for table in tables {
let table = *table;
let constraint_rows = transaction.query(UNEXPECTED_CONSTRAINTS_SQL, &[&table]).await;
let constraint_rows = match constraint_rows {
@@ -782,11 +1293,11 @@ pub(crate) async fn verify_v001_external_compatibility(
) => (name, kind, validated, deferrable, deferred, definition),
_ => return schema_query_error("schema_constraint_inventory_decode"),
};
if is_expected_constraint(table, name.as_str()) {
if is_expected_constraint(resources, table, name.as_str()) {
continue;
}
let definition = normalize_catalog_sql(definition.as_str());
if !validated || deferrable || deferred || !matches_expected_constraint_definition(table, kind.as_str(), definition.as_str()) {
if !validated || deferrable || deferred || !matches_expected_constraint_definition(resources, table, kind.as_str(), definition.as_str()) {
return schema_incompatible("schema_external_constraint");
}
}
@@ -887,9 +1398,10 @@ async fn inspect_index(
};
let definition = normalize_catalog_sql(definition.as_str());
let predicate = predicate.map(|value| return normalize_catalog_sql(value.as_str()));
let predicate_matches = match predicate.as_deref() {
std::option::Option::Some(value) => value.contains(contract.predicate_fragment),
std::option::Option::None => false,
let predicate_matches = match (contract.predicate_fragment, predicate.as_deref()) {
(std::option::Option::Some(expected), std::option::Option::Some(value)) => value.contains(expected),
(std::option::Option::None, std::option::Option::None) => true,
(std::option::Option::Some(_), std::option::Option::None) | (std::option::Option::None, std::option::Option::Some(_)) => false,
};
if unique != contract.unique || access_method != contract.access_method || !definition.contains(contract.key_fragment) || !predicate_matches {
return std::result::Result::Ok(SchemaResourceState::Incompatible);
@@ -1105,8 +1617,8 @@ fn expected_constraint_definition(resource_sql: &str, name: &str) -> std::option
return std::option::Option::Some(normalize_catalog_sql(definition));
}
fn is_expected_constraint(table: &str, name: &str) -> bool {
for resource in V001_RESOURCES {
fn is_expected_constraint(resources: &[SchemaResource], table: &str, name: &str) -> bool {
for resource in resources {
let contract = match resource.object {
SchemaObjectContract::Constraint(value) => value,
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
@@ -1118,8 +1630,8 @@ fn is_expected_constraint(table: &str, name: &str) -> bool {
return false;
}
fn matches_expected_constraint_definition(table: &str, kind: &str, definition: &str) -> bool {
for resource in V001_RESOURCES {
fn matches_expected_constraint_definition(resources: &[SchemaResource], table: &str, kind: &str, definition: &str) -> bool {
for resource in resources {
let contract = match resource.object {
SchemaObjectContract::Constraint(value) => value,
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 14
// version: 24
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -23,6 +23,7 @@ fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_faca
let bootstrap_sql = include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql");
assert!(migration.contains("crate::V000_RESOURCES"));
assert!(migration.contains("crate::V001_RESOURCES"));
assert!(migration.contains("crate::V002_RESOURCES"));
assert!(schema.contains("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql"));
assert!(schema.contains("../migrations/v001_raw_transaction/tables/001_ksp_store_identity.sql"));
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
@@ -42,6 +43,7 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private()
assert!(crate_root.contains("mod runtime;"));
assert!(crate_root.contains("mod schema;"));
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
let runtime = include_str!("../src/runtime.rs");
for forbidden in [
"pub mod ",
"ksp_store_lib",
@@ -53,7 +55,6 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private()
] {
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
}
let runtime = include_str!("../src/runtime.rs");
for forbidden in [
"std::env",
"dotenv",
@@ -113,7 +114,7 @@ fn pre_003_fix_001_migration_engine_uses_split_schema_contract_and_binds_network
assert!(observation_table.contains("CREATE TABLE IF NOT EXISTS ksp_raw_transaction_observations"));
assert!(archive_table.contains("CREATE TABLE IF NOT EXISTS ksp_raw_transaction_archive_payloads"));
assert!(index.contains("CREATE INDEX IF NOT EXISTS ix_ksp_raw_transactions_slot_signature"));
for forbidden in ["impl ksp_store_api::RawTransaction", "repository", "sqlx", "RawAccountState"] {
for forbidden in ["impl ksp_store_api::RawTransaction", "impl ksp_store_api::RawAccount", "repository", "sqlx"] {
assert!(!migration.contains(forbidden), "repository/cross-scope implementation leaked into migration engine: {forbidden}");
assert!(!schema.contains(forbidden), "repository/cross-scope implementation leaked into schema contract: {forbidden}");
}
@@ -124,6 +125,110 @@ fn pre_003_fix_001_migration_engine_uses_split_schema_contract_and_binds_network
return;
}
#[test]
fn pre_003_v002_schema_is_complete_and_keeps_capability_implementation_out_of_schema_layers() {
let migration = include_str!("../src/migration.rs");
let schema = include_str!("../src/schema.rs");
let states = include_str!("../migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql");
let observations = include_str!("../migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql");
let slot_check = include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql");
let index = include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
assert!(migration.contains("name: \"raw_account_state\""));
assert!(migration.contains("resources: crate::V002_RESOURCES"));
assert!(migration.contains("V002_PROVISIONAL_CHECKSUM_PRE_002"));
assert!(migration.contains("HISTORY_UPDATE_CHECKSUM_SQL"));
assert!(migration.contains("reconcile_applied_history_checksums"));
assert!(schema.contains("pub(crate) const V002_RESOURCES"));
assert!(schema.contains("verify_v002_external_compatibility"));
assert!(schema.contains("ksp_raw_account_states', 'ksp_raw_account_observations"));
assert!(states.contains("ksp_raw_account_states"));
assert!(observations.contains("ksp_raw_account_observations"));
assert!(slot_check.contains("slot >= 0 AND slot <= 18446744073709551615"));
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
assert!(!index.contains("WHERE"));
assert!(!migration.contains("impl ksp_store_api::RawAccount"));
assert!(!schema.contains("impl ksp_store_api::RawAccount"));
return;
}
#[test]
fn pre_005_raw_account_acquisition_is_atomic_idempotent_and_keeps_trait_impls_out_of_sql_module() {
let crate_root = include_str!("../src/lib.rs");
let raw = include_str!("../src/raw_account.rs");
let runtime = include_str!("../src/runtime.rs");
assert!(crate_root.contains("mod raw_account;"));
assert!(!crate_root.contains("pub mod raw_account"));
for required in [
"GET_ACCOUNT_STATE_SQL",
"GET_ACCOUNT_OBSERVATION_SQL",
"INSERT_ACCOUNT_STATE_SQL",
"INSERT_ACCOUNT_OBSERVATION_SQL",
"LOCK_ACCOUNT_STATE_SQL",
"LOCK_ACCOUNT_OBSERVATION_SQL",
"ON CONFLICT (pubkey, slot, state_hash) DO NOTHING",
"ON CONFLICT (observation_key) DO NOTHING",
"FOR UPDATE",
"persist_raw_account_acquisition",
"raw_account_states_equal",
"RawAccountState::try_new",
"RawAccountObservation::new",
"PostgresBackendErrorKind::Conflict",
"PostgresBackendErrorKind::DataInvalid",
"PostgresBackendErrorKind::WrongNetwork",
"PostgresBackendErrorKind::WriteFailed",
] {
assert!(raw.contains(required), "missing private RAW account acquisition contract: {required}");
}
assert!(runtime.contains("pub async fn persist_raw_account_acquisition"));
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] {
assert!(!raw.contains(forbidden), "pre.005 account module contains later/destructive scope: {forbidden}");
}
for implementation in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] {
assert!(!raw.contains(implementation), "account capability implementation leaked into SQL/mapping module: {implementation}");
assert_eq!(runtime.matches(implementation).count(), 1, "missing or duplicated account runtime bridge implementation: {implementation}");
}
for forbidden in ["std::env", "dotenv", "ksp_store_lib", "ksp_config_lib", "sqlx::", "SELECT *"] {
assert!(!raw.contains(forbidden), "RAW account module contains forbidden ownership/query material: {forbidden}");
}
return;
}
#[test]
fn pre_006_raw_account_additional_observation_is_reference_guarded_cancellation_safe_and_keeps_destructive_scope_closed() {
let raw = include_str!("../src/raw_account.rs");
let runtime = include_str!("../src/runtime.rs");
for required in [
"LOCK_ACCOUNT_REFERENCE_SQL",
"FOR KEY SHARE",
"record_raw_account_observation",
"reference_not_found(\"raw_account_observation_reference\")",
"persist_account_observation_row",
"RawObservationWriteOutcome",
"raw_account_observation_commit",
] {
assert!(raw.contains(required), "missing pre.006 additional-observation contract: {required}");
}
assert!(runtime.contains("pub async fn record_raw_account_observation"));
for forbidden in ["UPDATE ", "DELETE FROM", "ON CONFLICT DO UPDATE", " OFFSET "] {
assert!(!raw.contains(forbidden), "pre.006 account module contains later/destructive scope: {forbidden}");
}
for implementation in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] {
assert!(!raw.contains(implementation), "account capability implementation leaked into SQL/mapping module: {implementation}");
assert_eq!(runtime.matches(implementation).count(), 1, "missing or duplicated account runtime bridge implementation: {implementation}");
}
return;
}
#[test]
fn pre_004_raw_read_sql_and_mapping_remain_backend_private() {
let crate_root = include_str!("../src/lib.rs");
@@ -217,6 +322,59 @@ fn pre_006_raw_pagination_is_keyset_cursor_bound_and_policy_free() {
return;
}
#[test]
fn pre_007_raw_account_pagination_is_keyset_cursor_bound_and_policy_free() {
let raw = include_str!("../src/raw_account.rs");
let cursor = include_str!("../src/raw_account/cursor.rs");
let index = include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
for required in [
"LIST_ACCOUNT_STATES_ASC_SQL",
"LIST_ACCOUNT_STATES_DESC_SQL",
"LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL",
"LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL",
"(slot, pubkey, state_hash) >",
"(slot, pubkey, state_hash) <",
"ORDER BY slot ASC, pubkey ASC, state_hash ASC",
"ORDER BY slot DESC, pubkey DESC, state_hash DESC",
"list_raw_account_states",
] {
assert!(raw.contains(required), "missing pre.007 account keyset pagination contract: {required}");
}
for required in [
"CURSOR_BYTES: usize = 109",
"b\"KSPA\"",
"CURSOR_VERSION: u8 = 1",
"KSP/raw-account-state-cursor/v1",
"sha2::Sha256",
"query.network().as_str()",
"query.pubkey()",
"query.direction()",
"query.slots().start_inclusive()",
"query.slots().end_inclusive()",
"last_slot",
"last_pubkey",
"last_state_hash",
"PageLimitUnsupported",
"9_223_372_036_854_775_806",
] {
assert!(cursor.contains(required), "missing pre.007 account cursor/binding contract: {required}");
}
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000"] {
assert!(!raw.contains(forbidden), "pre.007 account source contains forbidden pagination/policy material: {forbidden}");
assert!(!cursor.contains(forbidden), "pre.007 account cursor contains forbidden pagination/policy material: {forbidden}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] {
assert!(!raw.contains(forbidden), "account trait implementation leaked into raw account SQL/mapping module: {forbidden}");
}
return;
}
#[test]
fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compaction() {
let raw = include_str!("../src/raw_transaction.rs");
@@ -250,25 +408,22 @@ fn pre_007_raw_retention_is_atomic_compare_and_transition_without_fake_compactio
}
#[test]
fn pre_008_backend_trait_implementations_stay_in_runtime_bridge_and_raw_account_scope_stays_closed() {
fn pre_008_backend_trait_implementations_cover_exact_ten_raw_capabilities_in_runtime_bridge() {
let runtime = include_str!("../src/runtime.rs");
for required in [
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend",
] {
assert!(runtime.contains(required), "missing pre.008 PostgreSQL capability implementation: {required}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
for implementation in [
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
] {
assert!(!runtime.contains(forbidden), "pre.008 opened RawAccount capability scope prematurely: {forbidden}");
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL RAW capability inventory: {implementation}");
}
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 10);
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
// version: 11
// version: 17
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -109,7 +109,8 @@ fn assert_pre_io_rejection(connection_uri: &str, tls_mode: ksp_store_postgres_li
#[test]
fn pre_009_backend_modules_exports_and_manifest_dependencies_are_exact() {
let crate_root = include_str!("../src/lib.rs");
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod raw_transaction;", "mod runtime;", "mod schema;"] {
for required in ["mod constants;", "mod error;", "mod health;", "mod migration;", "mod raw_account;", "mod raw_transaction;", "mod runtime;", "mod schema;"]
{
assert!(crate_root.contains(required), "missing PostgreSQL backend module: {required}");
}
assert!(!crate_root.contains("pub mod "));
@@ -172,6 +173,8 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
runtime,
include_str!("../src/migration.rs"),
include_str!("../src/health.rs"),
include_str!("../src/raw_account.rs"),
include_str!("../src/raw_account/cursor.rs"),
include_str!("../src/raw_transaction.rs"),
include_str!("../src/raw_transaction/cursor.rs"),
] {
@@ -183,7 +186,7 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
}
#[test]
fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_implementation() {
fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_and_keeps_account_impls_in_runtime_bridge() {
let production = std::format!(
"{}
{}
@@ -192,11 +195,13 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
{}
{}
{}
{}
{}",
include_str!("../src/error.rs"),
include_str!("../src/health.rs"),
include_str!("../src/lib.rs"),
include_str!("../src/migration.rs"),
include_str!("../src/raw_account.rs"),
include_str!("../src/raw_transaction.rs"),
include_str!("../src/raw_transaction/cursor.rs"),
include_str!("../src/runtime.rs"),
@@ -219,9 +224,8 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
"ksp_store_lib",
"ksp_config_lib",
"sqlx::",
"impl ksp_store_api::RawAccount",
] {
assert!(!production.contains(forbidden), "forbidden backend ownership/reverse-edge/RawAccount material detected: {forbidden}");
assert!(!production.contains(forbidden), "forbidden backend ownership/reverse-edge material detected: {forbidden}");
}
let bootstrap_sql = include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql");
assert!(bootstrap_sql.contains("ksp_store_schema_migrations"));
@@ -231,6 +235,41 @@ fn pre_009_backend_has_no_env_bypass_reverse_facade_edge_or_raw_account_trait_im
return;
}
#[test]
fn pre_009_live_raw_account_proof_is_opt_in_isolated_secret_safe_and_cross_family() {
let live = include_str!("postgres_raw_account_live.rs");
for required in [
"#[ignore = \"opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin\"]",
"std::io::stdin().read_line",
"managed_schema_preexisting_refusal",
"prove_schema_update_policy",
"prove_state_extremes",
"MAX_RAW_ACCOUNT_DATA_BYTES",
"u64::MAX",
"prove_atomic_idempotence_and_conflicts",
"prove_additional_observations",
"ReferenceNotFound",
"prove_pagination_and_cursors",
"KSPT",
"prove_cross_family_coexistence",
"persist_raw_transaction_acquisition",
"prove_concurrent_identical_insert",
"prove_concurrent_divergent_insert",
"prove_cancellation_rollback",
"task.abort()",
"cleanup_verification",
"ksp_raw_account_states",
"ksp_raw_account_observations",
"ksp_raw_transactions",
] {
assert!(live.contains(required), "missing pre.009 RawAccount live proof guard/scenario: {required}");
}
for forbidden in ["std::env", "KSP_SECRET_", "PGPASSWORD", "connection_uri = %", "connection_uri = ?", "println!(uri", "eprintln!(uri"] {
assert!(!live.contains(forbidden), "pre.009 RawAccount live proof contains forbidden secret/environment material: {forbidden}");
}
return;
}
#[test]
fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
let live = include_str!("postgres_raw_transaction_live.rs");
@@ -247,6 +286,8 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
"prove_cancellation_rollback",
"task.abort()",
"cleanup_verification",
"ksp_raw_account_states",
"ksp_raw_account_observations",
] {
assert!(live.contains(required), "missing pre.009 live proof guard/scenario: {required}");
}
@@ -257,29 +298,28 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
}
#[test]
fn pre_010_raw_transaction_capability_implementation_inventory_is_exact_and_raw_account_scope_stays_closed() {
fn pre_010_raw_capability_implementation_inventory_is_exactly_ten() {
let runtime = include_str!("../src/runtime.rs");
let capability_impls = [
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationRead for PostgresBackend",
"impl ksp_store_api::RawTransactionObservationWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionRead for PostgresBackend",
"impl ksp_store_api::RawTransactionRetentionWrite for PostgresBackend",
"impl ksp_store_api::RawTransactionWrite for PostgresBackend",
];
for implementation in capability_impls {
assert_eq!(runtime.matches(implementation).count(), 1, "unexpected PostgreSQL capability implementation inventory: {implementation}");
}
for forbidden in [
"impl ksp_store_api::RawAccountStateRead for PostgresBackend",
"impl ksp_store_api::RawAccountStateWrite for PostgresBackend",
"impl ksp_store_api::RawAccountObservationRead for PostgresBackend",
"impl ksp_store_api::RawAccountObservationWrite for PostgresBackend",
] {
assert!(!runtime.contains(forbidden), "RawAccountState scope opened during RawTransaction hardening: {forbidden}");
}
assert_eq!(runtime.matches("impl ksp_store_api::Raw").count(), 10);
let migration = include_str!("../src/migration.rs");
assert!(!migration.contains("ksp_raw_account"));
assert!(migration.contains("raw_account_state"));
assert!(migration.contains("crate::V002_RESOURCES"));
assert!(!runtime.contains("mod raw_account"));
return;
}
@@ -302,3 +342,73 @@ fn pre_010_raw_transaction_private_sql_keeps_keyset_navigation_and_bounded_state
}
return;
}
#[test]
fn pre_010_raw_account_private_sql_is_non_destructive_keyset_and_family_local() {
let source = include_str!("../src/raw_account.rs");
let cursor = include_str!("../src/raw_account/cursor.rs");
for required in [
"ON CONFLICT (pubkey, slot, state_hash) DO NOTHING",
"ON CONFLICT (observation_key) DO NOTHING",
"FOR UPDATE",
"FOR KEY SHARE",
"ORDER BY slot ASC, pubkey ASC, state_hash ASC",
"ORDER BY slot DESC, pubkey DESC, state_hash DESC",
"(slot, pubkey, state_hash) >",
"(slot, pubkey, state_hash) <",
] {
assert!(source.contains(required), "required hardened RawAccount SQL contract missing: {required}");
}
for forbidden in [
" OFFSET ",
"SELECT *",
"ON CONFLICT DO UPDATE",
"UPDATE ksp_raw_account",
"DELETE FROM ksp_raw_account",
"TRUNCATE",
"batch_size",
"priority",
] {
assert!(!source.contains(forbidden), "forbidden RawAccount scope/policy SQL detected: {forbidden}");
}
for required in ["b\"KSPA\"", "KSP/raw-account-state-cursor/v1", "CURSOR_BYTES: usize = 109"] {
assert!(cursor.contains(required), "missing hardened RawAccount cursor family contract: {required}");
}
assert!(!cursor.contains("b\"KSPT\""));
let state_fk = include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql");
let transaction_signature = include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql");
assert!(state_fk.contains("REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)"));
assert!(!state_fk.contains("ksp_raw_transactions"));
assert!(transaction_signature.contains("octet_length(transaction_signature) = 64"));
assert!(!transaction_signature.contains("REFERENCES"));
assert!(!transaction_signature.contains("ksp_raw_transactions"));
return;
}
#[test]
fn pre_010_v002_schema_inventory_is_exact_family_local_and_query_justified() {
let schema = include_str!("../src/schema.rs");
let v002_tail = match schema.split("pub(crate) const V002_RESOURCES: &[SchemaResource] = &[").nth(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("V002 resource inventory missing"),
};
let v002 = match v002_tail.split("\n];").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("V002 resource inventory terminator missing"),
};
assert_eq!(v002.matches("SchemaResource {").count(), 32);
assert_eq!(v002.matches("SchemaObjectContract::Table(").count(), 2);
assert_eq!(v002.matches("kind: \"p\"").count(), 2);
assert_eq!(v002.matches("kind: \"f\"").count(), 1);
assert_eq!(v002.matches("kind: \"c\"").count(), 26);
assert_eq!(v002.matches("SchemaObjectContract::Index(").count(), 1);
assert_eq!(v002.matches("predicate_fragment: std::option::Option::None").count(), 1);
assert!(v002.contains("key_fragment: \"slot,pubkey,state_hash\""));
assert!(!v002.contains("ksp_raw_transactions"));
assert!(!v002.contains("ksp_raw_transaction_observations"));
for forbidden in ["owner", "provider", "received_at_unix_millis", "acquisition_method"] {
let index_marker = std::format!("key_fragment: \"{forbidden}");
assert!(!v002.contains(index_marker.as_str()), "unjustified V002 business index detected: {forbidden}");
}
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
// version: 3
// version: 4
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -153,7 +153,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
}
*owns_schema = true;
let initial_health = initial.health().await;
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(1) || initial_health.pending_migration_count() != 0 {
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(2) || initial_health.pending_migration_count() != 0 {
return std::result::Result::Err(LiveFailure::new("initial_health"));
}
let initial_close = close_backend(initial).await;
@@ -167,7 +167,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
};
let idempotent_health = idempotent.health().await;
if !idempotent_health.is_ready()
|| idempotent_health.migration_version() != std::option::Option::Some(1)
|| idempotent_health.migration_version() != std::option::Option::Some(2)
|| idempotent_health.pending_migration_count() != 0
{
return std::result::Result::Err(LiveFailure::new("idempotent_health"));
@@ -250,7 +250,7 @@ async fn run_foundation_scenario(admin: &mut tokio_postgres::Client, uri: &str,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_health = final_backend.health().await;
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(1) || final_health.pending_migration_count() != 0 {
if !final_health.is_ready() || final_health.migration_version() != std::option::Option::Some(2) || final_health.pending_migration_count() != 0 {
return std::result::Result::Err(LiveFailure::new("final_health"));
}
return close_backend(final_backend).await;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
// version: 3
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -18,7 +18,9 @@ const LIVE_INDEX_EXISTS_SQL: &str = r#"SELECT EXISTS (
AND indexname = 'ix_ksp_raw_transactions_slot_signature'
)"#;
const LIVE_LOCK_OBSERVATION_SQL: &str = "SELECT observation_key FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE";
const LIVE_MANAGED_SCHEMA_DROP_SQL: &str = r#"DROP TABLE IF EXISTS ksp_raw_transaction_observations;
const LIVE_MANAGED_SCHEMA_DROP_SQL: &str = r#"DROP TABLE IF EXISTS ksp_raw_account_observations;
DROP TABLE IF EXISTS ksp_raw_account_states;
DROP TABLE IF EXISTS ksp_raw_transaction_observations;
DROP TABLE IF EXISTS ksp_raw_transaction_archive_payloads;
DROP TABLE IF EXISTS ksp_raw_transactions;
DROP TABLE IF EXISTS ksp_store_identity;
@@ -31,7 +33,9 @@ const LIVE_MANAGED_SCHEMA_EXISTS_SQL: &str = r#"SELECT EXISTS (
'ksp_store_identity',
'ksp_raw_transactions',
'ksp_raw_transaction_observations',
'ksp_raw_transaction_archive_payloads'
'ksp_raw_transaction_archive_payloads',
'ksp_raw_account_states',
'ksp_raw_account_observations'
)
AND table_type = 'BASE TABLE'
)"#;
@@ -150,7 +154,7 @@ async fn run_raw_transaction_scenario(admin: &mut tokio_postgres::Client, uri: &
};
*owns_schema = true;
let initial_health = initial.health().await;
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(1) || initial_health.pending_migration_count() != 0 {
if !initial_health.is_ready() || initial_health.migration_version() != std::option::Option::Some(2) || initial_health.pending_migration_count() != 0 {
return std::result::Result::Err(LiveFailure::new("initial_health"));
}
let wrong_network_result = open_backend_result(uri, "testnet", true, true).await;
@@ -211,7 +215,7 @@ async fn run_raw_transaction_scenario(admin: &mut tokio_postgres::Client, uri: &
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let reopened_health = reopened.health().await;
if !reopened_health.is_ready() || reopened_health.migration_version() != std::option::Option::Some(1) {
if !reopened_health.is_ready() || reopened_health.migration_version() != std::option::Option::Some(2) {
return std::result::Result::Err(LiveFailure::new("reopen_health"));
}
let reference_result = raw_reference(10);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
// version: 9
// version: 14
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -75,6 +75,31 @@ fn pre_003_retention_compaction_error_code_matches_store_contract_value() {
return;
}
#[test]
fn pre_004_raw_account_read_bridge_uses_only_backend_independent_models() {
let _state = ksp_store_postgres_lib::PostgresBackend::get_raw_account_state;
let _observation = ksp_store_postgres_lib::PostgresBackend::get_raw_account_observation;
return;
}
#[test]
fn pre_005_raw_account_acquisition_bridge_uses_only_backend_independent_models_and_outcomes() {
let _acquisition = ksp_store_postgres_lib::PostgresBackend::persist_raw_account_acquisition;
return;
}
#[test]
fn pre_006_raw_account_observation_write_bridge_uses_only_backend_independent_models_and_outcomes() {
let _observation = ksp_store_postgres_lib::PostgresBackend::record_raw_account_observation;
return;
}
#[test]
fn pre_007_raw_account_list_bridge_uses_backend_independent_query_page_and_reference_models() {
let _list = ksp_store_postgres_lib::PostgresBackend::list_raw_account_states;
return;
}
#[test]
fn pre_004_raw_read_bridge_uses_only_backend_independent_models() {
let _get = ksp_store_postgres_lib::PostgresBackend::get_raw_transaction;
@@ -103,21 +128,25 @@ fn pre_007_raw_retention_write_bridge_uses_backend_independent_transition_and_ou
return;
}
fn assert_raw_transaction_capabilities<T>()
fn assert_raw_capabilities<T>()
where
T: ksp_store_api::RawTransactionRead
+ ksp_store_api::RawTransactionWrite
T: ksp_store_api::RawAccountObservationRead
+ ksp_store_api::RawAccountObservationWrite
+ ksp_store_api::RawAccountStateRead
+ ksp_store_api::RawAccountStateWrite
+ ksp_store_api::RawTransactionObservationRead
+ ksp_store_api::RawTransactionObservationWrite
+ ksp_store_api::RawTransactionRead
+ ksp_store_api::RawTransactionRetentionRead
+ ksp_store_api::RawTransactionRetentionWrite,
+ ksp_store_api::RawTransactionRetentionWrite
+ ksp_store_api::RawTransactionWrite,
{
let _marker = std::marker::PhantomData::<T>;
return;
}
#[test]
fn pre_008_postgres_backend_implements_all_six_raw_transaction_capabilities() {
assert_raw_transaction_capabilities::<ksp_store_postgres_lib::PostgresBackend>();
fn pre_008_postgres_backend_implements_exact_raw_capability_set_10_of_10() {
assert_raw_capabilities::<ksp_store_postgres_lib::PostgresBackend>();
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
// version: 5
// version: 8
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
@@ -10,6 +10,7 @@ fn embedded(version: i64, name: &'static str, resources: &'static [crate::Schema
checksum: super::MigrationChecksum::Resources,
hook: super::MigrationHook::None,
name,
previous_checksums: &[],
resources,
version,
};
@@ -17,7 +18,7 @@ fn embedded(version: i64, name: &'static str, resources: &'static [crate::Schema
#[test]
fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned_v001() {
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 2);
assert!(super::EMBEDDED_MIGRATIONS.len() >= 2);
let v000 = &super::EMBEDDED_MIGRATIONS[0];
assert_eq!(v000.version, 0);
assert_eq!(v000.name, "bootstrap");
@@ -32,7 +33,54 @@ fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned
assert_eq!(v001.resources.len(), 40);
assert_eq!(super::migration_checksum(v001), "31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51");
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
assert_eq!(crate::current_migration_version(), 1);
return;
}
#[test]
fn pre_003_v002_registry_is_complete_and_keeps_v000_v001_checksums_stable() {
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 3);
let v000 = &super::EMBEDDED_MIGRATIONS[0];
let v001 = &super::EMBEDDED_MIGRATIONS[1];
let v002 = &super::EMBEDDED_MIGRATIONS[2];
assert_eq!(super::migration_checksum(v000), "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
assert_eq!(super::migration_checksum(v001), "31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51");
assert_eq!(v002.version, 2);
assert_eq!(v002.name, "raw_account_state");
assert_eq!(v002.hook, super::MigrationHook::None);
assert_eq!(v002.previous_checksums, &[super::V002_PROVISIONAL_CHECKSUM_PRE_002]);
assert_eq!(v002.resources.len(), 32);
assert_eq!(super::migration_checksum(v002), "ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e");
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
assert_eq!(crate::current_migration_version(), 2);
let full = [
applied(0, v000.name, super::migration_checksum(v000).as_str()),
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, super::migration_checksum(v002).as_str()),
];
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(3));
return;
}
#[test]
fn pre_003_v002_provisional_checksum_requires_schema_autoupdate_and_rejects_unknown_history() {
let v000 = &super::EMBEDDED_MIGRATIONS[0];
let v001 = &super::EMBEDDED_MIGRATIONS[1];
let v002 = &super::EMBEDDED_MIGRATIONS[2];
let provisional = [
applied(0, v000.name, super::migration_checksum(v000).as_str()),
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, super::V002_PROVISIONAL_CHECKSUM_PRE_002),
];
let strict = super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(strict.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
assert_eq!(super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, true).ok(), std::option::Option::Some(3));
let unknown = [
applied(0, v000.name, super::migration_checksum(v000).as_str()),
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, "unknown-v002-checksum"),
];
let unknown_result = super::validate_history(&unknown, super::EMBEDDED_MIGRATIONS, true);
assert_eq!(unknown_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
return;
}
@@ -83,10 +131,10 @@ fn pre_003_fix_001_ordered_registry_accepts_v000_prefix_and_full_v001_history()
let v001 = super::EMBEDDED_MIGRATIONS[1];
let v000_checksum = super::migration_checksum(&v000);
let prefix = [applied(0, v000.name, v000_checksum.as_str())];
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(1));
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(1));
let v001_checksum = super::migration_checksum(&v001);
let full = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str())];
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(2));
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(2));
return;
}
@@ -115,7 +163,7 @@ fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
let missing: [super::AppliedMigration; 0] = [];
let missing_v000 = [applied(1, v001.name, v001_checksum.as_str())];
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..], &missing_v000[..]] {
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS);
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
}
return;
@@ -125,10 +173,17 @@ fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
fn pre_003_fix_001_newer_history_is_rejected_without_down_migration() {
let v000 = super::EMBEDDED_MIGRATIONS[0];
let v001 = super::EMBEDDED_MIGRATIONS[1];
let v002 = super::EMBEDDED_MIGRATIONS[2];
let v000_checksum = super::migration_checksum(&v000);
let v001_checksum = super::migration_checksum(&v001);
let history = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str()), applied(2, "future", "future-checksum")];
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS);
let v002_checksum = super::migration_checksum(&v002);
let history = [
applied(0, v000.name, v000_checksum.as_str()),
applied(1, v001.name, v001_checksum.as_str()),
applied(2, v002.name, v002_checksum.as_str()),
applied(3, "future", "future-checksum"),
];
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
return;
}

View File

@@ -0,0 +1,525 @@
// file: crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
// version: 5
fn network() -> ksp_store_api::RawNetworkId {
return match ksp_store_api::RawNetworkId::new("devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
};
}
fn page_query(
network_name: &str,
pubkey: std::option::Option<ksp_store_api::Pubkey>,
start: std::option::Option<u64>,
end: std::option::Option<u64>,
direction: ksp_store_api::RawSortDirection,
) -> ksp_store_api::RawAccountStateQuery {
let network = match ksp_store_api::RawNetworkId::new(network_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid test network rejected: {error:?}"),
};
let slots = match ksp_store_api::RawSlotRange::new(start, end) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid slot range rejected: {error:?}"),
};
let limit = match ksp_store_api::RawPageLimit::new(2) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid page limit rejected: {error:?}"),
};
return ksp_store_api::RawAccountStateQuery::new(network, pubkey, slots, direction, ksp_store_api::RawPageRequest::first(limit));
}
fn state_row() -> super::RawAccountStateDbRow {
return super::RawAccountStateDbRow {
data: vec![1, 2, 3, 4],
executable: true,
lamports_text: u64::MAX.to_string(),
owner: vec![2; 32],
pubkey: vec![1; 32],
rent_epoch_text: u64::MAX.to_string(),
slot_text: u64::MAX.to_string(),
state_hash: vec![3; 32],
};
}
fn observation_row() -> super::RawAccountObservationDbRow {
return super::RawAccountObservationDbRow {
account_pubkey: vec![1; 32],
account_slot_text: u64::MAX.to_string(),
account_state_hash: vec![3; 32],
acquisition_method: "account_subscribe".to_owned(),
capture_session_id: std::option::Option::Some("session_1".to_owned()),
commitment: std::option::Option::Some("confirmed".to_owned()),
endpoint_id: std::option::Option::Some("endpoint_1".to_owned()),
filter_id: std::option::Option::Some("filter_1".to_owned()),
is_startup: std::option::Option::Some(true),
observation_key: vec![4; 32],
observed_at_unix_millis: std::option::Option::Some(1_700_000_000_000),
origin: "live".to_owned(),
protocol: "yellowstone_grpc".to_owned(),
provider: "publicnode".to_owned(),
received_at_unix_millis: 1_700_000_000_001,
source_payload_hash: std::option::Option::Some(vec![5; 32]),
source_payload_size_bytes: std::option::Option::Some(67_108_864),
transaction_signature: std::option::Option::Some(vec![6; 64]),
write_version_text: std::option::Option::Some(u64::MAX.to_string()),
};
}
#[test]
fn pre_004_account_state_row_round_trips_complete_u64_domain_and_bytes() {
let network = network();
let decoded = super::decode_raw_account_state_row(&network, state_row());
let state = match decoded {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account state row rejected: {error:?}"),
};
assert_eq!(state.reference().network().as_str(), "devnet");
assert_eq!(state.reference().pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
assert_eq!(state.reference().slot(), u64::MAX);
assert_eq!(state.reference().state_hash(), ksp_store_api::RawContentHash::new([3_u8; 32]));
assert_eq!(state.lamports(), u64::MAX);
assert_eq!(state.owner(), &ksp_store_api::Pubkey::new_from_array([2_u8; 32]));
assert!(state.executable());
assert_eq!(state.rent_epoch(), u64::MAX);
assert_eq!(state.data(), &[1, 2, 3, 4]);
return;
}
#[test]
fn pre_004_account_state_accepts_empty_data_and_rejects_oversized_data() {
let network = network();
let mut empty = state_row();
empty.data = std::vec::Vec::new();
let empty = super::decode_raw_account_state_row(&network, empty);
assert!(matches!(empty, std::result::Result::Ok(_)));
let mut oversized = state_row();
oversized.data = vec![0_u8; ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES + 1];
let oversized = super::decode_raw_account_state_row(&network, oversized);
assert_eq!(oversized.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
return;
}
#[test]
fn pre_004_account_state_rejects_hostile_width_and_decimal_rows_without_echo() {
let network = network();
let mut bad_pubkey = state_row();
bad_pubkey.pubkey = vec![0xA5; 31];
let error = super::decode_raw_account_state_row(&network, bad_pubkey).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid));
let mut bad_slot = state_row();
bad_slot.slot_text = "18446744073709551616-HOSTILE".to_owned();
let error = match super::decode_raw_account_state_row(&network, bad_slot) {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => panic!("hostile slot unexpectedly decoded"),
};
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::DataInvalid);
assert!(!std::format!("{error:?}").contains("HOSTILE"));
let mut bad_lamports = state_row();
bad_lamports.lamports_text = "-1".to_owned();
assert_eq!(
super::decode_raw_account_state_row(&network, bad_lamports).err().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
);
return;
}
#[test]
fn pre_004_account_observation_round_trips_complete_provenance_and_yellowstone_metadata() {
let network = network();
let decoded = super::decode_raw_account_observation_row(&network, observation_row());
let observation = match decoded {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account observation row rejected: {error:?}"),
};
assert_eq!(observation.observation_key(), ksp_store_api::RawObservationKey::new([4_u8; 32]));
assert_eq!(observation.account().network().as_str(), "devnet");
assert_eq!(observation.account().pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
assert_eq!(observation.account().slot(), u64::MAX);
assert_eq!(observation.account().state_hash(), ksp_store_api::RawContentHash::new([3_u8; 32]));
assert_eq!(observation.provenance().provider().as_str(), "publicnode");
assert_eq!(observation.provenance().protocol().as_str(), "yellowstone_grpc");
assert_eq!(observation.provenance().acquisition_method().as_str(), "account_subscribe");
assert_eq!(observation.provenance().capture_session_id().map(|value| return value.as_str()), std::option::Option::Some("session_1"));
assert_eq!(observation.provenance().commitment().map(|value| return value.as_str()), std::option::Option::Some("confirmed"));
assert_eq!(observation.provenance().endpoint_id().map(|value| return value.as_str()), std::option::Option::Some("endpoint_1"));
assert_eq!(observation.provenance().filter_id().map(|value| return value.as_str()), std::option::Option::Some("filter_1"));
assert_eq!(observation.provenance().received_at().unix_millis(), 1_700_000_000_001);
assert_eq!(observation.provenance().observed_at().map(|value| return value.unix_millis()), std::option::Option::Some(1_700_000_000_000));
assert_eq!(observation.provenance().source_payload_hash(), std::option::Option::Some(ksp_store_api::RawContentHash::new([5_u8; 32])));
assert_eq!(observation.provenance().source_payload_size_bytes(), std::option::Option::Some(ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES));
assert_eq!(observation.is_startup(), std::option::Option::Some(true));
assert_eq!(observation.transaction_signature(), std::option::Option::Some(ksp_store_api::RawTransactionSignature::new([6_u8; 64])));
assert_eq!(observation.write_version(), std::option::Option::Some(u64::MAX));
return;
}
#[test]
fn pre_004_account_observation_keeps_optional_metadata_absent() {
let network = network();
let mut row = observation_row();
row.capture_session_id = std::option::Option::None;
row.commitment = std::option::Option::None;
row.endpoint_id = std::option::Option::None;
row.filter_id = std::option::Option::None;
row.observed_at_unix_millis = std::option::Option::None;
row.source_payload_hash = std::option::Option::None;
row.source_payload_size_bytes = std::option::Option::None;
row.is_startup = std::option::Option::None;
row.transaction_signature = std::option::Option::None;
row.write_version_text = std::option::Option::None;
let decoded = super::decode_raw_account_observation_row(&network, row);
let observation = match decoded {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid sparse account observation rejected: {error:?}"),
};
assert!(observation.provenance().capture_session_id().is_none());
assert!(observation.provenance().commitment().is_none());
assert!(observation.provenance().endpoint_id().is_none());
assert!(observation.provenance().filter_id().is_none());
assert!(observation.provenance().observed_at().is_none());
assert!(observation.provenance().source_payload_hash().is_none());
assert!(observation.provenance().source_payload_size_bytes().is_none());
assert!(observation.is_startup().is_none());
assert!(observation.transaction_signature().is_none());
assert!(observation.write_version().is_none());
return;
}
#[test]
fn pre_004_account_observation_rejects_hostile_rows_without_echoing_values() {
let network = network();
let mut bad_signature = observation_row();
bad_signature.transaction_signature = std::option::Option::Some(vec![8; 63]);
assert_eq!(
super::decode_raw_account_observation_row(&network, bad_signature).err().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
);
let mut bad_write_version = observation_row();
bad_write_version.write_version_text = std::option::Option::Some("18446744073709551616-HOSTILE".to_owned());
let error = match super::decode_raw_account_observation_row(&network, bad_write_version) {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => panic!("hostile write version unexpectedly decoded"),
};
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::DataInvalid);
assert!(!std::format!("{error:?}").contains("HOSTILE"));
let mut bad_origin = observation_row();
bad_origin.origin = "hostile-origin".to_owned();
assert_eq!(
super::decode_raw_account_observation_row(&network, bad_origin).err().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
);
let mut bad_time = observation_row();
bad_time.observed_at_unix_millis = std::option::Option::Some(bad_time.received_at_unix_millis + 1);
assert_eq!(
super::decode_raw_account_observation_row(&network, bad_time).err().map(|value| return value.kind()),
std::option::Option::Some(crate::PostgresBackendErrorKind::DataInvalid)
);
return;
}
#[test]
fn pre_004_account_wrong_network_guard_is_pre_io_and_static() {
let backend = network();
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
};
let reference = ksp_store_api::RawAccountStateReference::new(
foreign,
ksp_store_api::Pubkey::new_from_array([1_u8; 32]),
1,
ksp_store_api::RawContentHash::new([2_u8; 32]),
);
let error = super::ensure_network(&backend, &reference, "raw_account_state_network").err();
let error = match error {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("wrong-network account reference unexpectedly accepted"),
};
assert_eq!(error.kind(), crate::PostgresBackendErrorKind::WrongNetwork);
assert_eq!(error.phase(), "raw_account_state_network");
return;
}
#[test]
fn pre_005_account_acquisition_input_guard_requires_network_and_exact_reference() {
let backend = network();
let valid_state = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
};
let valid_observation = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
};
assert!(super::ensure_acquisition_inputs(&backend, &valid_state, &valid_observation).is_ok());
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
};
let foreign_state = match super::decode_raw_account_state_row(&foreign, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign account state rejected: {error:?}"),
};
let foreign_observation = match super::decode_raw_account_observation_row(&foreign, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign account observation rejected: {error:?}"),
};
let error = super::ensure_acquisition_inputs(&backend, &foreign_state, &foreign_observation).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
let mut mismatched_row = observation_row();
mismatched_row.account_state_hash = vec![9; 32];
let mismatched_observation = match super::decode_raw_account_observation_row(&backend, mismatched_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid mismatched observation model rejected: {error:?}"),
};
let error = super::ensure_acquisition_inputs(&backend, &valid_state, &mismatched_observation).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::Conflict));
return;
}
#[test]
fn pre_005_account_state_idempotence_compares_complete_content_not_state_hash_only() {
let backend = network();
let first = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account state rejected: {error:?}"),
};
let identical = match super::decode_raw_account_state_row(&backend, state_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid identical account state rejected: {error:?}"),
};
assert!(super::raw_account_states_equal(&first, &identical));
let mut divergent_row = state_row();
divergent_row.lamports_text = (u64::MAX - 1).to_string();
let divergent = match super::decode_raw_account_state_row(&backend, divergent_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid divergent account state rejected: {error:?}"),
};
assert_eq!(first.reference(), divergent.reference());
assert!(!super::raw_account_states_equal(&first, &divergent));
return;
}
#[test]
fn pre_005_account_observation_idempotence_compares_optional_metadata_exactly() {
let backend = network();
let first = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account observation rejected: {error:?}"),
};
let identical = match super::decode_raw_account_observation_row(&backend, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid identical account observation rejected: {error:?}"),
};
assert_eq!(first, identical);
let mut divergent_row = observation_row();
divergent_row.write_version_text = std::option::Option::Some((u64::MAX - 1).to_string());
let divergent = match super::decode_raw_account_observation_row(&backend, divergent_row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid divergent account observation rejected: {error:?}"),
};
assert_eq!(first.observation_key(), divergent.observation_key());
assert_eq!(first.account(), divergent.account());
assert_ne!(first, divergent);
return;
}
#[test]
fn pre_005_account_origin_encoding_is_exact_and_static() {
let cases = [
(ksp_store_api::RawAcquisitionOrigin::Backfill, "backfill"),
(ksp_store_api::RawAcquisitionOrigin::Import, "import"),
(ksp_store_api::RawAcquisitionOrigin::Live, "live"),
(ksp_store_api::RawAcquisitionOrigin::Repair, "repair"),
(ksp_store_api::RawAcquisitionOrigin::Replay, "replay"),
];
for (origin, expected) in cases {
let encoded = match super::encode_origin(origin) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account origin rejected: {error:?}"),
};
assert_eq!(encoded, expected);
}
return;
}
#[test]
fn pre_006_additional_observation_input_guard_rejects_wrong_network_before_io() {
let backend = network();
let foreign = match ksp_store_api::RawNetworkId::new("mainnet-beta") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign network rejected: {error:?}"),
};
let observation = match super::decode_raw_account_observation_row(&foreign, observation_row()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid foreign account observation rejected: {error:?}"),
};
let error = super::ensure_observation_write_input(&backend, &observation).err();
assert_eq!(error.map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::WrongNetwork));
return;
}
#[test]
fn pre_006_additional_observation_race_and_cancellation_contract_is_transaction_bounded() {
let source = include_str!("../src/raw_account.rs");
let start = match source.find("pub(crate) async fn record_raw_account_observation") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("record_raw_account_observation function missing"),
};
let tail = &source[start..];
let end = match tail.find("fn raw_account_observation_db_row") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("record_raw_account_observation boundary missing"),
};
let function = &tail[..end];
let begin = match function.find("client.transaction().await") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("additional observation transaction begin missing"),
};
let key_share = match function.find("LOCK_ACCOUNT_REFERENCE_SQL") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("additional observation reference lock missing"),
};
let persist = match function.find("persist_account_observation_row") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("additional observation insert/compare missing"),
};
let commit = match function.find("sql_transaction.commit().await") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("additional observation commit missing"),
};
assert!(begin < key_share);
assert!(key_share < persist);
assert!(persist < commit);
assert!(!function.contains("tokio::spawn"));
assert!(!function.contains("std::mem::forget"));
assert!(!function.contains("ON CONFLICT DO UPDATE"));
return;
}
#[test]
fn pre_007_account_cursor_v1_round_trip_is_fixed_109_bytes_and_exact() {
let pubkey = ksp_store_api::Pubkey::new_from_array([7_u8; 32]);
let query = page_query(
"devnet",
std::option::Option::Some(pubkey),
std::option::Option::Some(10),
std::option::Option::Some(20),
ksp_store_api::RawSortDirection::Ascending,
);
let state_hash = ksp_store_api::RawContentHash::new([9_u8; 32]);
let cursor = match crate::encode_raw_account_cursor(&query, 15, &pubkey, &state_hash) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
};
assert_eq!(cursor.as_bytes().len(), 109);
assert_eq!(cursor.as_bytes().get(0..4), std::option::Option::Some(b"KSPA".as_ref()));
assert_eq!(cursor.as_bytes().get(4).copied(), std::option::Option::Some(1));
let decoded = match crate::decode_raw_account_cursor(&query, &cursor) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account cursor decoding rejected: {error:?}"),
};
assert_eq!(decoded.last_slot, 15);
assert_eq!(decoded.last_pubkey, [7_u8; 32]);
assert_eq!(decoded.last_state_hash, [9_u8; 32]);
assert_eq!(std::format!("{cursor:?}"), "RawPageCursor { len: 109 }");
return;
}
#[test]
fn pre_007_account_cursor_rejects_replay_across_query_context_and_transaction_family() {
let pubkey = ksp_store_api::Pubkey::new_from_array([4_u8; 32]);
let query = page_query(
"devnet",
std::option::Option::Some(pubkey),
std::option::Option::Some(10),
std::option::Option::Some(20),
ksp_store_api::RawSortDirection::Ascending,
);
let state_hash = ksp_store_api::RawContentHash::new([5_u8; 32]);
let cursor = match crate::encode_raw_account_cursor(&query, 15, &pubkey, &state_hash) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
};
let other_network = page_query("mainnet-beta", Some(pubkey), Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending);
let other_pubkey =
page_query("devnet", Some(ksp_store_api::Pubkey::new_from_array([6_u8; 32])), Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending);
let other_direction = page_query("devnet", Some(pubkey), Some(10), Some(20), ksp_store_api::RawSortDirection::Descending);
let other_range = page_query("devnet", Some(pubkey), Some(11), Some(20), ksp_store_api::RawSortDirection::Ascending);
for candidate in [&other_network, &other_pubkey, &other_direction, &other_range] {
let error = crate::decode_raw_account_cursor(candidate, &cursor).err();
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
}
let transaction_query = match ksp_store_api::RawSlotRange::new(Some(10), Some(20)) {
std::result::Result::Ok(slots) => {
let limit = match ksp_store_api::RawPageLimit::new(2) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid transaction page limit rejected: {error:?}"),
};
ksp_store_api::RawTransactionQuery::new(network(), slots, ksp_store_api::RawSortDirection::Ascending, ksp_store_api::RawPageRequest::first(limit))
},
std::result::Result::Err(error) => panic!("valid transaction slot range rejected: {error:?}"),
};
let signature = ksp_store_api::RawTransactionSignature::new([8_u8; 64]);
let transaction_cursor = match crate::encode_raw_transaction_cursor(&transaction_query, 15, &signature) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid transaction cursor encoding rejected: {error:?}"),
};
let error = crate::decode_raw_account_cursor(&query, &transaction_cursor).err();
assert_eq!(error.map(|value| return value.phase()), Some("raw_account_cursor_magic"));
return;
}
#[test]
fn pre_007_account_cursor_hostile_size_magic_version_and_digest_are_rejected() {
let query = page_query("devnet", None, None, None, ksp_store_api::RawSortDirection::Ascending);
for size in [1_usize, 108, 109, 110, 4_096] {
let raw = match ksp_store_api::RawPageCursor::try_new(vec![0_u8; size].into_boxed_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("bounded hostile cursor rejected by API before backend test: {error:?}"),
};
assert!(crate::decode_raw_account_cursor(&query, &raw).is_err());
}
let pubkey = ksp_store_api::Pubkey::new_from_array([3_u8; 32]);
let state_hash = ksp_store_api::RawContentHash::new([2_u8; 32]);
let valid = match crate::encode_raw_account_cursor(&query, 42, &pubkey, &state_hash) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account cursor encoding rejected: {error:?}"),
};
for index in [0_usize, 4, 108] {
let mut bytes = valid.as_bytes().to_vec();
bytes[index] ^= 0xff;
let hostile = match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("fixed-size hostile cursor rejected by API before backend test: {error:?}"),
};
let error = crate::decode_raw_account_cursor(&query, &hostile).err();
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
}
return;
}
#[test]
fn pre_007_account_page_limit_exposes_only_the_real_postgres_limit_plus_one_boundary() {
let maximum = crate::raw_account_physical_page_limit(9_223_372_036_854_775_806);
assert_eq!(maximum, Ok((9_223_372_036_854_775_806_usize, i64::MAX)));
let rejected = crate::raw_account_physical_page_limit(9_223_372_036_854_775_807).err();
assert_eq!(rejected.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::PageLimitUnsupported));
return;
}
#[test]
fn pre_007_account_list_row_decodes_full_reference_without_integer_narrowing() {
let row = super::RawAccountListDbRow { pubkey: vec![1_u8; 32], slot_text: u64::MAX.to_string(), state_hash: vec![2_u8; 32] };
let decoded = match super::decode_raw_account_list_row(&network(), row) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("valid account list row rejected: {error:?}"),
};
assert_eq!(decoded.0, u64::MAX);
assert_eq!(decoded.1.pubkey(), &ksp_store_api::Pubkey::new_from_array([1_u8; 32]));
assert_eq!(decoded.1.slot(), u64::MAX);
assert_eq!(decoded.1.state_hash(), ksp_store_api::RawContentHash::new([2_u8; 32]));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs
// version: 4
// version: 7
fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn {
return super::ActualColumn {
@@ -48,6 +48,100 @@ fn pre_003_fix_001_v001_resources_are_split_and_idempotent_by_object_family() {
return;
}
#[test]
fn pre_003_v002_resources_are_complete_bounded_and_have_one_unfiltered_navigation_index() {
assert_eq!(crate::V002_RESOURCES.len(), 32);
let ids = crate::V002_RESOURCES.iter().map(|resource| return resource.id).collect::<std::vec::Vec<_>>();
assert_eq!(ids.iter().filter(|id| return id.starts_with("tables/")).count(), 2);
assert_eq!(ids.iter().filter(|id| return id.starts_with("constraints/")).count(), 29);
assert_eq!(ids.iter().filter(|id| return id.starts_with("indexes/")).count(), 1);
assert!(ids[..2].iter().all(|id| return id.starts_with("tables/")));
assert!(ids[2..31].iter().all(|id| return id.starts_with("constraints/")));
assert_eq!(ids[31], "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
let mut unique = std::collections::BTreeSet::<&str>::new();
for id in &ids {
assert!(unique.insert(id), "duplicate V002 resource: {id}");
}
let sql = crate::V002_RESOURCES.iter().map(|resource| return resource.sql).collect::<std::vec::Vec<_>>().concat();
let normalized_sql = sql.split_ascii_whitespace().collect::<std::vec::Vec<_>>().join(" ");
for required in [
"CREATE TABLE IF NOT EXISTS ksp_raw_account_states",
"CREATE TABLE IF NOT EXISTS ksp_raw_account_observations",
"slot NUMERIC(20, 0) NOT NULL",
"lamports NUMERIC(20, 0) NOT NULL",
"rent_epoch NUMERIC(20, 0) NOT NULL",
"write_version NUMERIC(20, 0) NULL",
"PRIMARY KEY (pubkey, slot, state_hash)",
"PRIMARY KEY (observation_key)",
"FOREIGN KEY (account_pubkey, account_slot, account_state_hash) REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) ON DELETE RESTRICT",
"octet_length(pubkey) = 32",
"slot >= 0 AND slot <= 18446744073709551615",
"octet_length(state_hash) = 32",
"lamports >= 0 AND lamports <= 18446744073709551615",
"octet_length(owner) = 32",
"rent_epoch >= 0 AND rent_epoch <= 18446744073709551615",
"octet_length(data) <= 16777216",
"octet_length(observation_key) = 32",
"octet_length(account_pubkey) = 32",
"account_slot >= 0 AND account_slot <= 18446744073709551615",
"octet_length(account_state_hash) = 32",
"octet_length(provider) >= 1 AND octet_length(provider) <= 128",
"octet_length(protocol) >= 1 AND octet_length(protocol) <= 128",
"octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128",
"origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay'",
"received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999",
"observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis",
"source_payload_size_bytes IS NULL OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864",
"transaction_signature IS NULL OR octet_length(transaction_signature) = 64",
"write_version IS NULL OR write_version >= 0 AND write_version <= 18446744073709551615",
"CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash ON ksp_raw_account_states (slot, pubkey, state_hash)",
] {
assert!(normalized_sql.contains(required), "V002 final physical contract is missing: {required}");
}
for forbidden in [
"ON DELETE CASCADE",
"BIGSERIAL",
"slot BIGINT",
"lamports BIGINT",
"rent_epoch BIGINT",
"write_version BIGINT",
"data TEXT",
"CREATE UNIQUE INDEX",
] {
assert!(!normalized_sql.contains(forbidden), "V002 contains forbidden physical policy/index shape: {forbidden}");
}
let index_resource = &crate::V002_RESOURCES[31];
let contract = match index_resource.object {
super::SchemaObjectContract::Index(value) => value,
super::SchemaObjectContract::Constraint(_) | super::SchemaObjectContract::Table(_) => panic!("V002 final resource must be the navigation index"),
};
assert_eq!(contract.key_fragment, "slot,pubkey,state_hash");
assert_eq!(contract.predicate_fragment, std::option::Option::None);
assert!(!contract.unique);
assert!(!index_resource.sql.contains("WHERE"));
return;
}
#[test]
fn pre_003_v002_external_compatibility_recognizes_only_owned_constraints() {
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "ck_ksp_raw_account_states_slot"));
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_observations", "fk_ksp_raw_account_observations_state",));
assert!(!super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "external_check"));
let slot_resource = crate::V002_RESOURCES.iter().find(|resource| return resource.id == "constraints/005_ck_ksp_raw_account_states_slot.sql");
let slot_resource = match slot_resource {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("V002 slot constraint must remain embedded"),
};
let expected = super::expected_constraint_definition(slot_resource.sql, "ck_ksp_raw_account_states_slot");
let expected = match expected {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("V002 slot constraint definition must be extractable"),
};
assert!(super::matches_expected_constraint_definition(crate::V002_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
assert!(!super::matches_expected_constraint_definition(crate::V001_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
return;
}
#[test]
fn pre_003_fix_001_external_extra_columns_are_accepted_only_when_they_cannot_block_ksp_inserts() {
let nullable = actual_column("external_nullable", "text", true);

View File

@@ -0,0 +1,139 @@
<!-- file: deltas/0.3.4/pre.001-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.001-fix.001` — forecast souple éditable
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.001
workspace.package.version = 0.3.4-pre.1
```
Le gate opérateur de `pre.001` du 30 août 2026 est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés Store/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
## 2. Motif du correctif
Le sizing recalibré de `docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md` était présenté sous forme de tableau. Cette forme décrit correctement le forecast mais ne permet pas d'insérer proprement l'historique des correctifs sous la prerelease concernée.
Le projet possède déjà une convention documentaire adaptée : chaque prerelease du forecast est une sous-section `### pre.NNN` et chaque correctif éventuel est une sous-sous-section `#### pre.NNN-fix.MMM` placée directement sous sa tranche.
## 3. Correction
La section `Sizing recalibré` devient un forecast souple éditable :
```text
## Sizing recalibré — prereleases souples
### pre.NNN — objet
#### pre.NNN-fix.MMM — correctif éventuel
```
Chaque tranche expose désormais :
- un statut modifiable ;
- son budget cible ;
- sa responsabilité technique principale ;
- un emplacement naturel pour les fixes successifs.
Le découpage `pre.001` à `pre.013` puis `rel.001`, les budgets et les responsabilités techniques restent inchangés. Le correctif ajoute seulement la hiérarchie documentaire nécessaire à l'historique futur.
`pre.001-fix.001` est lui-même enregistré sous `pre.001` dans ce forecast.
## 4. Version Cargo inchangée
Ce correctif est strictement documentaire. Conformément à `VER-ID-008` :
```text
workspace.package.version = 0.3.4-pre.1
```
`Cargo.toml` n'est pas modifié.
## 5. Fichiers ajoutés
```text
deltas/0.3.4/pre.001-fix.001.md
```
## 6. Fichiers modifiés
```text
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
```
Le header documentaire du plan passe de `version: 1` à `version: 2` parce que ce fichier est réellement modifié.
## 7. Fichiers non modifiés
En particulier :
```text
Cargo.toml
src/**
tests/**
config/**
migrations/**
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
deltas/0.3.4/pre.001.md
```
Le delta historique `pre.001.md` reste immuable ; le correctif est tracé uniquement dans le nouveau delta `pre.001-fix.001.md` et dans le forecast du plan.
## 8. Invariants techniques
Aucun changement n'est apporté au design V002 figé par `pre.001` :
```text
2 tables seulement
PK state = (pubkey, slot, state_hash)
BYTEA pour les bytes/fixed-width
NUMERIC(20,0) pour les u64 non bornés à i64
atomicité state + observation
idempotence par comparaison exacte
pagination keyset (slot, pubkey, state_hash)
cursor account V1 KSPA distinct de KSPT
10 capabilities RAW finales = 6 transaction + 4 account
```
## 9. Gate opérateur hérité
Le gate exécuté sur `0.3.4-pre.001` avant ce correctif documentaire est PASS :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (240 table(s), 134 file(s))
cargo check --workspace: PASS
cargo clippy --workspace --all-targets: PASS
cargo test -p ksp-store-api: PASS
cargo test -p ksp-store-lib: PASS
cargo test -p ksp-store-postgres-lib: PASS
cargo test -p ksp-config-lib: PASS
cargo check -p ksp-store-lib --no-default-features: PASS
```
Comme le fix ne touche ni code, ni build, ni runtime, ni config, ni migration, aucun nouveau live PostgreSQL n'est requis. L'audit Markdown doit simplement rester propre après la restructuration du plan.
## 10. Validation post-fix
Les audits statiques applicables ont été rejoués après la restructuration :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (239 table(s), 135 file(s))
```
La diminution d'une table correspond exactement au remplacement du tableau de sizing par des sous-sections ; le fichier delta supplémentaire porte le nombre de fichiers Markdown audités à 135.
Contrôles de scope :
```text
Cargo.toml : byte-identique à pre.001
validation 021 : byte-identique à pre.001
deltas/0.3.4/pre.001.md : byte-identique à pre.001
```

206
deltas/0.3.4/pre.001.md Normal file
View File

@@ -0,0 +1,206 @@
<!-- file: deltas/0.3.4/pre.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.001` — audit et physical design `RawAccountState` PostgreSQL
## 1. Objet
Ouverture de :
```text
0.3.4 — Store/PostgreSQL RawAccountState + complétude/conformance RAW
```
Cette tranche reste volontairement un gate de lecture/audit/design/sizing. Aucun SQL V002, aucune table account, aucun repository account et aucune impl capability account ne sont ajoutés.
## 2. Base
Base canonique :
```text
v0.3.3
workspace.package.version = 0.3.3
```
Le gate opérateur fourni avec la reprise est propre sur audits Rust/Markdown, checks, Clippy et suites ciblées/workspace de la release stable ; les live tests restent opt-in/ignored.
## 3. Version
Version workspace après overlay :
```text
0.3.4-pre.1
```
Label de livraison :
```text
0.3.4-pre.001
```
## 4. Fichiers ajoutés
```text
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
deltas/0.3.4/pre.001.md
```
## 5. Fichier modifié
```text
Cargo.toml
```
Modifications :
```text
workspace.package.version : 0.3.3 -> 0.3.4-pre.1
file header version : 363 -> 364
```
## 6. Fichiers supprimés
Aucun.
## 7. Audit effectué
Ont été relus avant design : règles globales/KSP/Rust/dépendances/docs/versioning ; architectures Store/data/execution/workers ; plans/validations `0.3.1`, `0.3.2`, `0.3.3` ; source/tests Store API/façade/backend ; Config `std.store` ; migrations V000/V001 ; archive kbot3 ciblée account.
Inventaire confirmé :
```text
10 capabilities RAW contractuelles
6 RawTransaction implémentées physiquement dans PostgresBackend + Store
4 RawAccount encore non implémentées et explicitement fermées par canaries 0.3.3
```
Checksums recalculés :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
```
## 8. Décisions majeures
### 8.1 V002
V002 sera additive, sous `migrations/v002_raw_account_state/`, avec seulement :
```text
ksp_raw_account_states
ksp_raw_account_observations
```
V000/V001 restent byte-immutables.
### 8.2 Identité state
```text
network API + (pubkey, slot, state_hash)
```
Le réseau est le binding DB existant. PK physique `(pubkey, slot, state_hash)`. Pubkey/owner/hash en BYTEA fixed-width ; slot/lamports/rent_epoch en NUMERIC(20,0). Data account en BYTEA complet, vide autorisé, maximum 16 MiB.
### 8.3 Observation
L'observation référence le state par FK composite et conserve provenance + `is_startup` + `transaction_signature` + `write_version`. Ces metadata restent observation-only. Aucune FK transaction n'est ajoutée.
### 8.4 Atomicité/idempotence
Pattern retenu : transaction PostgreSQL, `INSERT ... ON CONFLICT DO NOTHING RETURNING`, puis `SELECT ... FOR UPDATE` et comparaison exacte sur collision. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*`, aucun overwrite silencieux.
`record_raw_account_observation` vérifie le state préexistant et ne le crée jamais implicitement.
### 8.5 Pagination
Ordre total :
```text
(slot, pubkey, state_hash)
```
Cursor V1 account fixe 109 bytes, magic `KSPA`, domaine `KSP/raw-account-state-cursor/v1`, digest lié au réseau, filtre pubkey, range, direction et dernière clé. Le cursor transaction `KSPT` est donc non rejouable.
Index métier additionnel unique :
```text
(slot, pubkey, state_hash)
```
Aucun index owner/provider/time/status.
## 9. Audit kbot3
Classification :
- **REPRENDRE** : invariants unicité/FK et principe d'index account+slot ; NUMERIC pour valeurs monétaires ;
- **REDESSINER** : identité durable, BYTEA fixed-width, bytes complets, u64 exact, observation séparée, atomicité/conflict/cursor ;
- **REPORTER** : normalisation CORE, processing ledger, batch 1000, indexes owner/provider/time/status ;
- **REJETER** : sqlx/monolithe, BIGSERIAL comme identité publique, base64 physique, BIGINT narrowing, status/error RAW, maintenance destructive comme comportement normal.
Aucun code historique n'est copié.
## 10. Threat model
Le plan couvre mauvaise DB/réseau, fixed-width hostile, overflow u64, 16 MiB, observation orpheline, rollback partiel, même pubkey+slot multi-state, conflits exacts, collisions observation, races, cursor hostile/cross-query/cross-family, ordre pagination, metadata Yellowstone, fuite SQL/server text, cancellation et migration mismatch.
## 11. Sizing recalibré
Le forecast est étendu à `pre.013` afin de séparer V002 registry/tables de ses contraintes/schema compatibility, puis acquisition write de la concurrence/observation supplémentaire. Le gate technique final reste suivi d'une lane documentaire et d'une lane publication distinctes.
## 12. Hors scope conservé
```text
ksp-store-api redesign
account retention/archive/purge
STRUCTURAL/CORE/DECODED/DOMAIN persistence
Interface events
workers/jobs/backfills
batch/priorité/backlog policy
nouveaux indexes sans query
nouveaux backends/apps
```
## 13. Questions ouvertes
Aucune question de contrat bloquante après `pre.001`. Les numéros exacts de ressources SQL et le découpage de helpers privés sont laissés à `pre.002/pre.003` sans pouvoir modifier les invariants figés.
## 14. Validations exécutées
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
Markdown table audit: clean (240 table(s), 134 file(s))
assertions structurelles locales
workspace.package.version = 0.3.4-pre.1
Cargo.toml header version = 364
plan 025 / validation 021 / delta pre.001 présents
aucun répertoire migrations/v002_raw_account_state
aucun module backend raw_account créé
```
## 15. Validations non exécutées dans le conteneur d'assemblage
L'exécutable `cargo` n'est pas disponible dans cet environnement. L'appel `cargo fmt --all` s'arrête donc immédiatement avec `cargo: command not found`; aucune commande Cargo post-overlay n'est déclarée PASS.
À exécuter côté opérateur :
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Le gate opérateur de la base `v0.3.3` fourni avec la reprise était intégralement vert avant cet overlay. Cet historique ne remplace pas le gate post-overlay ci-dessus.

View File

@@ -0,0 +1,168 @@
<!-- file: deltas/0.3.4/pre.002-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.002-fix.001` — correction des canaris V002
## 1. Base
Base opérateur obligatoire :
```text
0.3.4-pre.002
workspace.package.version = 0.3.4-pre.2
```
Le gate opérateur du 2026-08-30 est propre sur les audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, Config et `ksp-store-lib --no-default-features`. Il échoue uniquement dans :
```text
cargo test -p ksp-store-postgres-lib
```
avec 41 tests unitaires backend passants et deux canaris en échec.
## 2. Version
Ce correctif modifie deux sources Rust de tests. Conformément à `VER-ID-007` et `VER-ID-010` :
```text
workspace.package.version = 0.3.4-pre.2.fix.1
```
Aucune version de migration n'est ajoutée ou changée.
## 3. Défaut 1 — canari `SchemaNewer` devenu obsolète
Le test historique :
```text
pre_003_fix_001_newer_history_is_rejected_without_down_migration
```
construisait V000, V001 puis une migration inconnue de version `2`. Ce scénario était valide quand V001 était la dernière migration embedded. Depuis `pre.002`, V002 occupe légitimement la version `2`; le même historique représente donc une divergence V002 et doit être classé `MigrationMismatch`, ce qui explique l'échec observé :
```text
left: Some(MigrationMismatch)
right: Some(SchemaNewer)
```
Le canari est recalibré sans modifier `validate_history` : il construit le préfixe valide V000/V001/V002 avec leurs checksums réels, puis ajoute une migration inconnue de version `3`. Le scénario teste à nouveau exactement un historique plus récent que le registry KSP et conserve l'attente `SchemaNewer`.
## 4. Défaut 2 — assertion FK sensible au formatage SQL
Le canari :
```text
pre_002_v002_resources_are_exactly_two_tables_plus_base_pk_fk_without_indexes_or_domain_checks
```
cherchait la FK complète sous forme d'une chaîne contenant un espace entre la clause `FOREIGN KEY (...)` et `REFERENCES`. La ressource SQL V002 contient un retour à la ligne à cet endroit, sans différence sémantique PostgreSQL.
La ressource SQL n'est **pas** réécrite. Le test normalise uniquement les espaces ASCII du SQL embedded avant de vérifier ses fragments obligatoires/interdits. Il continue d'exiger :
- exactement 5 resources V002 ;
- les 2 tables ;
- les 2 PK ;
- la FK composite avec `ON DELETE RESTRICT` ;
- `NUMERIC(20,0)` pour les champs concernés ;
- absence d'index, de `CHECK` de `pre.003`, de cascade, de `BIGSERIAL`, de `slot BIGINT` et de `data TEXT`.
## 5. Invariants préservés
Aucune ressource SQL n'est modifiée :
```text
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/**
```
Le checksum V002 intermédiaire reste donc :
```text
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
```
Restent également inchangés :
```text
V000 / V001
registry V002 et ses 5 resources
src/migration.rs
src/schema.rs
ksp-store-api
ksp-store-lib
Config
aucune capability RawAccount* backend/façade
aucun repository raw_account
aucun index/check de pre.003
```
## 6. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/unit_tests/migration.rs
crates/ksp-store-postgres-lib/unit_tests/schema.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
Fichier ajouté :
```text
deltas/0.3.4/pre.002-fix.001.md
```
Aucun fichier supprimé.
## 7. Validations déjà fournies par le gate opérateur `pre.002`
PASS avant le correctif :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Backend avant correctif :
```text
cargo test -p ksp-store-postgres-lib
41 passed; 2 failed
```
## 8. Gate opérateur du correctif
À rejouer après extraction :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Les commandes Cargo post-correctif ne sont pas exécutables dans l'environnement d'assemblage courant faute d'exécutable `cargo`.
## 9. Décisions
- corriger les canaris, pas la production ni V002 ;
- préserver strictement le checksum de migration ;
- conserver `SchemaNewer` pour un historique réellement au-delà de la dernière migration embedded ;
- rendre l'assertion FK insensible uniquement au whitespace SQL, sans affaiblir le contrat de contenu ;
- ne pas avancer `pre.003`.
## 10. Questions ouvertes
Aucune pour ce correctif. Le passage à `pre.003` dépend du gate opérateur vert de `pre.002-fix.001`.

232
deltas/0.3.4/pre.002.md Normal file
View File

@@ -0,0 +1,232 @@
<!-- file: deltas/0.3.4/pre.002.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.002` — registry V002 et tables RAW account minimales
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.001-fix.001
workspace.package.version = 0.3.4-pre.1
```
Le gate opérateur fourni pour `pre.001` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests ciblés Store/Config et `cargo check -p ksp-store-lib --no-default-features` passent. `pre.001-fix.001` est ensuite un correctif strictement documentaire ; ses audits statiques sont propres et il ne modifie pas Cargo/runtime.
## 2. Objectif
Matérialiser uniquement la première fondation physique V002 décidée en `pre.001` :
```text
migration logique V002 = raw_account_state
2 tables
2 primary keys
1 foreign key observation -> state
```
Cette tranche n'ouvre aucun repository account, aucun dispatch Store, aucun index métier et aucune contrainte de domaine/fixed-width complète.
## 3. Version Cargo
Cette prerelease modifie code et migration SQL. Conformément à `VER-ID-009` :
```text
workspace.package.version : 0.3.4-pre.1 -> 0.3.4-pre.2
Cargo.toml header : 364 -> 365
```
## 4. Registry V002
`EMBEDDED_MIGRATIONS` contient désormais exactement trois migrations logiques ordonnées :
```text
V000 bootstrap
V001 raw_transaction
V002 raw_account_state
```
V002 utilise `MigrationChecksum::Resources`, `MigrationHook::None` et `V002_RESOURCES`. `current_migration_version()` passe de `1` à `2`.
Les checksums acquis restent inchangés :
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
```
Le checksum calculé sur les cinq resources V002 de cette tranche est :
```text
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
```
Il est volontairement **provisoire de prerelease** : `pre.003` doit encore ajouter les contraintes de domaine et l'index prévus, traiter explicitement la transition depuis cet état intermédiaire puis figer le checksum V002 final avant toute preuve live account persistante.
## 5. Ressources physiques ajoutées
```text
migrations/v002_raw_account_state/
tables/
001_ksp_raw_account_states.sql
002_ksp_raw_account_observations.sql
constraints/
001_pk_ksp_raw_account_states.sql
002_pk_ksp_raw_account_observations.sql
003_fk_ksp_raw_account_observations_state.sql
```
### 5.1 State
Colonnes matérialisées exactement selon le design `pre.001` :
```text
pubkey BYTEA NOT NULL
slot NUMERIC(20,0) NOT NULL
state_hash BYTEA NOT NULL
lamports NUMERIC(20,0) NOT NULL
owner BYTEA NOT NULL
executable BOOLEAN NOT NULL
rent_epoch NUMERIC(20,0) NOT NULL
data BYTEA NOT NULL
```
PK :
```text
(pubkey, slot, state_hash)
```
### 5.2 Observation
Les champs de provenance existants sont conservés avec les optionalités account décidées, dont :
```text
is_startup BOOLEAN NULL
transaction_signature BYTEA NULL
write_version NUMERIC(20,0) NULL
```
PK : `(observation_key)`.
FK :
```text
(account_pubkey, account_slot, account_state_hash)
-> ksp_raw_account_states(pubkey, slot, state_hash)
ON DELETE RESTRICT
```
Aucune FK vers `ksp_raw_transactions` n'est ajoutée.
## 6. Scope explicitement non avancé
```text
aucun CHECK fixed-width/u64/data/provenance account
aucun index (slot,pubkey,state_hash)
aucun index owner/provider/time/status
aucun src/raw_account.rs
aucun repository/read/write account
aucune impl RawAccount* sur PostgresBackend
aucune impl RawAccount* sur Store
aucune modification ksp-store-api
aucune retention/archive/purge account
```
Les contraintes et l'index sont réservés à `pre.003` ; le mapping/read commence seulement à `pre.004`.
## 7. Tests/canaris ajustés
Les tests historiques qui interdisaient l'existence même de V002 sont recalibrés sans ouvrir les capabilities account :
- registry V000/V001/V002 exact et `current_migration_version = 2` ;
- inventaire V002 exact `2 tables + 3 contraintes PK/FK` ;
- absence de CHECK/index dans V002 `pre.002` ;
- absence de module repository account et d'impl `RawAccount*` runtime ;
- live tests transaction/foundation existants attendent désormais migration version `2` s'ils sont exécutés.
Les canaris qui interdisent les quatre implémentations `RawAccount*` restent actifs.
## 8. Fichiers ajoutés
```text
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/001_pk_ksp_raw_account_states.sql
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/002_pk_ksp_raw_account_observations.sql
crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql
deltas/0.3.4/pre.002.md
```
## 9. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/migration.rs
crates/ksp-store-postgres-lib/src/schema.rs
crates/ksp-store-postgres-lib/unit_tests/migration.rs
crates/ksp-store-postgres-lib/unit_tests/schema.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/postgres_foundation_live.rs
crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 10. Fichiers supprimés
Aucun.
## 11. Validations exécutées dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
Markdown table audit: clean (239 table(s), 136 file(s))
assertions structurelles locales
DIFF_SCOPE=PASS
HEADER_VERSIONS=PASS
V000_V001_IMMUTABLE=PASS
V002_MINIMAL_SCOPE=PASS
V002_CHECKSUM=30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
```
Le checksum V002 intermédiaire a été recalculé indépendamment à partir du domaine `ksp-migration-resources-v1\0`, des IDs ordonnés et des bytes exacts des cinq resources.
## 12. Validations non exécutées dans l'environnement d'assemblage
L'exécutable `cargo` n'est pas disponible dans cet environnement. Après application du delta, rejouer côté opérateur :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Aucun live PostgreSQL account n'est revendiqué dans `pre.002`.
## 13. Décisions prises
- V002 reste additive et ne modifie aucun byte V000/V001.
- Les PK/FK sont des resources séparées et inspectables par le moteur existant.
- La table state porte directement les bytes D1 RAW ; aucune représentation base64 physique n'est introduite.
- `transaction_signature` reste metadata d'observation sans FK transaction.
- La tranche ne crée pas de deuxième vérité `space`, de surrogate ID, d'archive account ni de processing ledger.
## 14. Questions ouvertes
Aucune question de contrat nouvelle. `pre.003` doit finaliser la surface physique V002 déjà décidée : contraintes de domaine/fixed-width, index de navigation, compatibilité schema et checksum final, y compris la transition depuis l'état intermédiaire de cette prerelease.

View File

@@ -0,0 +1,111 @@
<!-- file: deltas/0.3.4/pre.003-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.003-fix.001` — conformité `RUST-FMT-104` du bloc migration
## 1. Base requise
```text
0.3.4-pre.003
workspace.package.version = 0.3.4-pre.3
```
Le gate opérateur du 2026-08-30 confirme :
```text
Markdown audit PASS, 239 tables / 138 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS, 45 tests
cargo test -p ksp-config-lib PASS, 128 tests
cargo check -p ksp-store-lib --no-default-features PASS
Rust rule audit FAIL, 1 x RUST-FMT-104
```
## 2. Objectif
Corriger l'unique violation de conformité restante de `pre.003` sans modifier le comportement de migration ni le schéma V002.
## 3. Correction
Dans `crates/ksp-store-postgres-lib/src/migration.rs`, le bloc homogène de constantes est remis dans l'ordre alphabétique attendu par `RUST-FMT-104` :
```text
HISTORY_INSERT_SQL
HISTORY_LOAD_SQL
HISTORY_UPDATE_CHECKSUM_SQL
```
`HISTORY_LOAD_SQL` est seulement déplacée avant `HISTORY_UPDATE_CHECKSUM_SQL`. Aucune valeur de constante, chaîne SQL ou logique de code n'est modifiée.
## 4. Version
Le correctif touche un fichier Rust ; `VER-ID-007/010` impose donc :
```text
workspace.package.version = 0.3.4-pre.3.fix.1
label = 0.3.4-pre.003-fix.001
```
## 5. Fichiers ajoutés
```text
deltas/0.3.4/pre.003-fix.001.md
```
## 6. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/migration.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 7. Fichiers supprimés
Aucun.
## 8. Invariants conservés
- V002 reste à 32 resources : 2 tables, 29 contraintes, 1 index ;
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
- checksum provisoire `pre.002` inchangé et bridge conditionnel inchangé ;
- V000/V001 inchangées ;
- aucune ressource SQL modifiée ;
- aucun repository `raw_account`, dispatch Store ou implémentation `RawAccount*` ajouté ;
- aucun changement fonctionnel, de migration, de runtime ou de configuration hors identifiant technique Cargo du fix.
## 9. Validations exécutées dans l'environnement d'assemblage
```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.4
```
## 10. Validations non exécutées dans l'environnement d'assemblage
`cargo` n'est pas disponible dans cet environnement. Le gate opérateur complet du fix reste à rejouer :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 11. Décisions
Le défaut appartient strictement au couloir `pre.003` : il est donc corrigé par `pre.003-fix.001` et ne justifie pas l'ouverture de `pre.004`.
## 12. Questions ouvertes
Aucune.

187
deltas/0.3.4/pre.003.md Normal file
View File

@@ -0,0 +1,187 @@
<!-- file: deltas/0.3.4/pre.003.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.003` — contraintes, index et compatibilité V002
## 1. Base
Base appliquée :
```text
0.3.4-pre.002-fix.001
workspace.package.version = 0.3.4-pre.2.fix.1
```
Le gate opérateur fourni le 2026-08-30 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `cargo check -p ksp-store-lib --no-default-features` passent. Le backend PostgreSQL exécute notamment 43 tests unitaires sans échec après `pre.002-fix.001`.
## 2. Version
Cette tranche contient des changements Rust/SQL fonctionnels :
```text
workspace.package.version = 0.3.4-pre.3
label = 0.3.4-pre.003
```
## 3. Scope exécuté
`pre.003` ferme uniquement le schéma physique V002 et sa compatibilité de migration. Aucun repository account ni dispatch façade n'est ouvert.
Inventaire V002 final :
```text
2 tables
29 contraintes = 2 PK + 1 FK + 26 CHECK
1 index non unique
32 resources au total
```
Les 26 `CHECK` ajoutés matérialisent les invariants déjà possédés par `ksp-store-api` :
- pubkeys, state hashes, observation keys et source hashes aux largeurs fixes attendues ;
- transaction signature optionnelle à 64 bytes ;
- `slot`, `lamports`, `rent_epoch`, `account_slot` et `write_version` dans le domaine `u64` exact, avec stockage `NUMERIC(20,0)` lorsque nécessaire ;
- `data` account bornée à 16 MiB, vide autorisé ;
- provenance logique obligatoire/optionnelle bornée à 1..128 octets avec l'alphabet sûr KSP ;
- origin limité à `backfill`, `import`, `live`, `repair`, `replay` ;
- timestamps bornés à `MAX_RAW_UNIX_MILLIS` et ordre `observed_at <= received_at` ;
- source payload size optionnelle bornée à 64 MiB.
`is_startup` reste un `BOOLEAN NULL` sans contrainte artificielle supplémentaire.
## 4. Index de navigation
Un seul index métier est ajouté :
```text
ix_ksp_raw_account_states_slot_pubkey_state_hash
(slot, pubkey, state_hash)
```
Il est non unique et sans prédicat. Aucun index owner/provider/method/time/status n'est introduit sans query publique qui le justifie.
## 5. Checksum V002 final et transition de prerelease
Checksum V002 intermédiaire écrit par `pre.002` :
```text
30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
```
Checksum V002 final calculé sur les 32 resources ordonnées :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
V000 et V001 restent byte-inchangées et conservent leurs checksums :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
```
Le moteur accepte le checksum V002 intermédiaire **uniquement** avec `schema_update=update_if_needed`. Dans ce cas, sous la transaction de migration existante :
1. l'historique reconnaît uniquement le checksum provisoire explicitement connu ;
2. les resources V002 manquantes sont matérialisées et réinspectées ;
3. la compatibilité externe V002 est vérifiée ;
4. l'entrée V002 de `ksp_store_schema_migrations` est remplacée par le checksum final avec un `UPDATE` conditionné par version, nom et ancien checksum ;
5. le commit publie ensemble schéma et historique final.
Avec `schema_update=disabled`, le checksum provisoire reste un `MigrationMismatch`. Tout checksum V002 inconnu reste rejeté même avec update activé.
## 6. Schema compatibility V002
La vérification externe générique couvre désormais V001 et V002 avec leurs inventaires propres. Pour les deux tables account, elle conserve les mêmes principes que V001 :
- colonnes attendues et types physiques compatibles ;
- contraintes KSP attendues reconnues par nom/définition ;
- extension externe bloquante rejetée ;
- unique index externe non adossé à une contrainte rejeté ;
- triggers/rules externes rejetés.
Le probe d'adoption inclut désormais les deux tables V002 afin qu'un schéma KSP account existant sans metadata de migration ne soit pas considéré comme vide.
Le contrat d'index interne supporte explicitement les deux formes possédées : V001 avec prédicat `retention_state <> 'purged'`, V002 sans prédicat.
## 7. Canaries et tests modifiés
Les canaries backend prouvent désormais :
- 32 resources V002, ordonnées par tables/constraints/index ;
- inventaire exact 2/29/1 ;
- checksum final exact et checksums V000/V001 stables ;
- acceptation conditionnelle du checksum provisoire et rejet des historiques arbitraires ;
- bornes physiques majeures et absence de narrowing `u64` ;
- index unique de navigation non filtré/non unique ;
- compatibilité externe V002 ;
- absence persistante de `src/raw_account.rs`, d'implémentation `RawAccount*` et de dispatch runtime account.
## 8. Scope négatif préservé
Cette tranche n'ajoute pas :
```text
ksp-store-api change
raw_account repository module
RawAccount* implementation on PostgresBackend
RawAccount* implementation on Store
Store account dispatch
account retention/archive/purge
owner/provider/time/status indexes
worker/job/backfill policy
Transport -> Store coupling
new backend
```
## 9. Documentation
`docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md` conserve le forecast souple sous forme de sous-paragraphes `### pre.NNN` / `#### pre.NNN-fix.MMM`. `pre.002-fix.001` y passe au statut gate opérateur complet PASS et `pre.003` au statut réalisé / gate opérateur à rejouer.
`docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md` enregistre le gate vert de la base, l'inventaire V002 final, les checksums et la stratégie de transition de prerelease.
## 10. Validation exécutée dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
=> General Rust rule audit: clean
=> Rust export completeness audit: 0 candidate(s)
=> KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
=> Markdown table audit: clean (239 table(s), 138 file(s))
```
Le calcul indépendant de checksum sur les bytes des resources V002 donne :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
`cargo`, `clippy` et les tests Rust ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo de `pre.003` n'est donc revendiqué ici.
## 11. Gate opérateur demandé
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Les preuves PostgreSQL live account restent hors de cette tranche et sont prévues plus tard dans le forecast.
## 12. Fichiers
Le delta contient uniquement `Cargo.toml`, les fichiers Rust/tests/schema réellement modifiés, les nouvelles resources V002, les deux documents de suivi modifiés et ce fichier `pre.003.md`. Aucun snapshot complet, aucune archive historique et aucun artefact de build ne sont inclus.
## 13. Verdict
`0.3.4-pre.003` : **READY FOR OPERATOR GATE**.

View File

@@ -0,0 +1,119 @@
<!-- file: deltas/0.3.4/pre.004-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.004-fix.001` — conformité Clippy du mapping PostgreSQL account
## 1. Base requise
```text
0.3.4-pre.4
```
Le gate opérateur du 2026-08-30 confirme que `pre.004` compile et que les suites ciblées passent, mais `cargo clippy --workspace --all-targets` échoue sur le nouveau mapping `RawAccountState`.
## 2. Anomalies corrigées
Dans `src/raw_account.rs`, les deux helpers d'extraction physique utilisent 27 formes :
```text
row.try_get(...).map_err(...)?
```
Le profil workspace nie `clippy::question-mark-used` et `clippy::implicit-return`. Chaque occurrence génère donc deux erreurs Clippy, soit 54 diagnostics.
Le même gate signale un warning `unused_variables` pour `crate_root` dans `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
## 3. Correctif
Les 27 extractions sont dépliées en `match` typés :
```text
Ok(value) -> value
Err(_) -> return Err(DataInvalid)
```
La logique et les phases d'erreur restent strictement identiques. Aucune macro, nouveau helper partagé ou nouvelle dépendance n'est introduit.
La variable `crate_root` réellement inutilisée est supprimée du seul canari concerné. Le canari `pre_004_raw_account_read_sql_and_mapping_remain_backend_private_and_read_only` conserve sa propre lecture de `src/lib.rs`, qui reste utilisée.
## 4. Version
Deux fichiers Rust sont modifiés. Conformément à `VER-ID-007/010`, le workspace passe à :
```text
0.3.4-pre.4.fix.1
```
## 5. Scope inchangé
Le fix ne modifie pas :
```text
GET_ACCOUNT_STATE_SQL
GET_ACCOUNT_OBSERVATION_SQL
signatures get_raw_account_state/get_raw_account_observation
modèles ksp-store-api
write SQL account
pagination/cursor KSPA
impl RawAccount*
Store dispatch account
migrations V000/V001/V002
```
Le checksum V002 final reste :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 6. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 7. Fichier ajouté
```text
deltas/0.3.4/pre.004-fix.001.md
```
## 8. Validations exécutées dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
PASS
grep '?' crates/ksp-store-postgres-lib/src/raw_account.rs
aucune occurrence
```
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Cargo post-fix n'est revendiqué PASS ici.
## 9. Gate opérateur à rejouer
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 10. Verdict
`pre.004-fix.001` reste strictement dans la responsabilité de `pre.004` : il corrige uniquement la conformité Clippy et l'hygiène du canari du mapping/read slice déjà introduit, sans ajouter de fonctionnalité.

298
deltas/0.3.4/pre.004.md Normal file
View File

@@ -0,0 +1,298 @@
<!-- file: deltas/0.3.4/pre.004.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.004` — mapping PostgreSQL privé et lectures `RawAccountState`
## 1. Base requise
```text
0.3.4-pre.3.fix.1
```
Le gate opérateur fourni le 2026-08-30 pour `pre.003-fix.001` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 139 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 45 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
V002 finale et son checksum sont donc considérés acquis avant l'ouverture du mapping account.
## 2. Objectif
Implémenter la tranche de lecture de la vertical slice PostgreSQL `RawAccountState` sans ouvrir les writes, la pagination ni les implémentations de traits :
```text
mapping SQL privé state
mapping SQL privé observation
get_raw_account_state
get_raw_account_observation
hostile-row guards
```
Les méthodes du backend retournent uniquement des modèles `ksp-store-api`. Aucun `tokio_postgres::Row`, SQL, bind, SQLSTATE ou type physique ne traverse le bridge public.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.4
```
Aucune crate ne redéfinit localement la version héritée.
## 4. Module physique privé
Nouveau module :
```text
crates/ksp-store-postgres-lib/src/raw_account.rs
```
Il possède les deux SELECT et les codecs physiques de cette tranche. `runtime.rs` ne contient aucun SQL métier et délègue les lectures au module privé.
La tranche est strictement read-only :
```text
INSERT INTO absent
UPDATE absent
DELETE FROM absent
ON CONFLICT absent
FOR UPDATE absent
list/cursor account absent
```
Les écritures state+observation restent `pre.005`, l'observation supplémentaire `pre.006`, la pagination/cursor `pre.007` et les quatre `impl RawAccount*` `pre.008`.
## 5. `get_raw_account_state`
Le SELECT adresse exactement la clé physique :
```text
(pubkey, slot, state_hash)
```
Le mapping reconstruit :
```text
network <- backend mono-réseau
pubkey <- BYTEA exactement 32 bytes
slot <- NUMERIC(20,0)::text -> u64
state_hash <- BYTEA exactement 32 bytes
lamports <- NUMERIC(20,0)::text -> u64
owner <- BYTEA exactement 32 bytes
executable <- BOOLEAN
rent_epoch <- NUMERIC(20,0)::text -> u64
data <- BYTEA exact -> RawAccountState::try_new
```
Le chemin décimal couvre tout le domaine `u64`, y compris les valeurs supérieures à `i64::MAX` et `u64::MAX` lui-même. Aucun narrowing via `BIGINT` n'est introduit.
`RawAccountState::try_new` revalide la borne backend-agnostic des bytes account : data vide est valide ; une ligne hostile dépassant 16 MiB devient `DataInvalid`.
Après décodage, la référence reconstruite doit rester exactement égale à la référence demandée. Une cardinalité autre que 0/1 est également `DataInvalid`.
## 6. Garde réseau pré-I/O
`get_raw_account_state` vérifie avant `pool.get()` :
```text
reference.network == backend.network -> continuer
sinon -> WrongNetwork
```
Le mauvais réseau ne consomme donc aucune connexion PostgreSQL et n'expose aucune valeur fournie.
## 7. `get_raw_account_observation`
`RawObservationKey` ne porte pas de réseau. Le réseau de la `RawAccountStateReference` reconstruite provient donc exclusivement du backend mono-réseau déjà lié par `ksp_store_identity`.
Le mapping couvre :
```text
observation_key
account.pubkey
account.slot
account.state_hash
provider
protocol
acquisition_method
origin
received_at
capture_session_id optionnel
commitment optionnel
endpoint_id optionnel
filter_id optionnel
observed_at optionnel
source_payload_hash optionnel
source_payload_size_bytes optionnel
is_startup optionnel
transaction_signature optionnelle
write_version optionnel
```
Spécificités physiques :
```text
account.slot / write_version -> NUMERIC(20,0)::text -> u64
transaction_signature -> NULL ou exactement 64 bytes
hash/key/pubkey -> exactement 32 bytes
received/observed -> BIGINT -> u64 -> RawTimestamp
source payload size -> BIGINT -> u64 + borne API 64 MiB
provenance codes -> constructeurs API fallibles
origin -> cinq variantes API uniquement
```
Les metadata Yellowstone restent strictement observation-only. Une colonne NULL reste `None`; aucune valeur `false`, zéro, signature ou write version n'est inventée.
## 8. Hostile-row guards
Les tests unitaires couvrent notamment :
```text
state slot/lamports/rent_epoch = u64::MAX
write_version = u64::MAX
account data vide
account data > 16 MiB -> DataInvalid
pubkey de 31 bytes -> DataInvalid
signature transaction de 63 bytes -> DataInvalid
décimal négatif ou > u64::MAX -> DataInvalid
origin hostile -> DataInvalid
observed_at > received_at -> DataInvalid
metadata optionnelles absentes -> None
wrong network state reference -> WrongNetwork pré-I/O
```
Les erreurs ne retiennent que `PostgresBackendErrorKind` + phase `&'static str`; les chaînes hostiles ne sont jamais interpolées dans l'erreur.
## 9. Surface backend
`PostgresBackend` expose désormais le bridge étroit :
```text
get_raw_account_state
get_raw_account_observation
```
Ces méthodes ne constituent pas encore les implémentations de `RawAccountStateRead` et `RawAccountObservationRead` : le premier trait exige aussi `list_raw_account_states`, réservé à `pre.007`. Les quatre traits account seront ouverts ensemble en `pre.008` afin de passer directement de 6/10 à 10/10 capabilities physiques.
## 10. Canaries transformés
Les canaris historiques qui interdisaient tout module account sont ajustés sans perdre leur rôle :
- `dependency_boundary.rs` exige un module `raw_account` privé, read-only, sans pagination ni trait impl ;
- `hardening_completeness.rs` inclut le nouveau module dans l'inventaire exact et dans les scans anti-secret/anti-reverse-edge ;
- les interdictions des quatre `impl RawAccount* for PostgresBackend` restent actives ;
- `public_api.rs` prouve que les deux méthodes du bridge n'utilisent que les modèles backend-agnostic.
Les canaris V000/health business-free restent inchangés.
## 11. Migrations
Aucune ressource sous `migrations/` n'est modifiée.
Checksums conservés :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
Le bridge du checksum provisoire `pre.002` reste inchangé.
## 12. Fichiers ajoutés
```text
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
deltas/0.3.4/pre.004.md
```
## 13. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 14. Fichiers supprimés
```text
aucun
```
## 15. Validations exécutées
Dans l'environnement d'assemblage :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
PASS après alignement RustRover des tableaux touchés
```
Le contrôle différentiel doit également confirmer qu'aucune ressource V000/V001/V002 n'a changé.
## 16. Validations non exécutées dans l'environnement d'assemblage
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans cet environnement. Ne sont donc pas revendiqués PASS ici :
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 17. Décisions prises
- Réutiliser la frontière éprouvée de `0.3.3-pre.004` : SQL/mapping privé, méthodes backend étroites, traits plus tard.
- Garder les conversions `NUMERIC(20,0)` via `::text -> u64` afin de ne pas ajouter de dépendance décimale ni de narrowing.
- Revalider les invariants API au read même si V002 possède déjà des CHECK SQL ; la DB peut être externe, héritée ou hostile.
- Ne pas factoriser prématurément les codecs transaction/account dans un nouveau module commun : les familles ont des formes différentes et `pre.004` doit rester bornée.
- Ne pas ouvrir `RawAccountStateRead` avant que `list_raw_account_states` existe ; l'inventaire de capabilities reste volontairement 6/10 jusqu'à `pre.008`.
## 18. Questions ouvertes
Aucune question bloquante nouvelle. Les détails des writes, races et cursor restent ceux figés par le plan `pre.001` et appartiennent respectivement à `pre.005`, `pre.006` et `pre.007`.
## 19. Gate opérateur attendu
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Aucun test live PostgreSQL account n'est demandé dans cette tranche ; la preuve live complète reste `pre.009`.

308
deltas/0.3.4/pre.005.md Normal file
View File

@@ -0,0 +1,308 @@
<!-- file: deltas/0.3.4/pre.005.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.005` — acquisition atomique et idempotence `RawAccountState`
## 1. Base requise
```text
0.3.4-pre.4.fix.1
```
Le gate opérateur fourni le 2026-08-30 pour `pre.004-fix.001` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 141 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 52 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
Le mapping/read account de `pre.004` est donc considéré acquis avant l'ouverture des writes d'acquisition.
## 2. Objectif
Implémenter uniquement l'écriture atomique du couple :
```text
RawAccountState
+
RawAccountObservation
```
avec les invariants figés par `pre.001` :
```text
guards réseau/référence avant I/O
state insert idempotent
collision state -> FOR UPDATE + comparaison exacte
observation insert idempotente
collision observation -> FOR UPDATE + comparaison exacte
commit unique
aucun overwrite silencieux
```
L'observation supplémentaire sans retransmettre les bytes account reste réservée à `pre.006`.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.5
```
Aucune crate ne redéfinit localement la version héritée.
## 4. Préconditions avant I/O
`persist_raw_account_acquisition` valide avant `pool.get()` :
```text
state.reference.network == backend.network
observation.account.network == backend.network
observation.account == state.reference
```
Un mauvais réseau devient `WrongNetwork`. Une référence observation/state divergente devient `Conflict`. Aucune connexion PostgreSQL n'est consommée avant ces gardes.
## 5. Insertion canonique du state
Le state utilise :
```text
INSERT INTO ksp_raw_account_states (...)
ON CONFLICT (pubkey, slot, state_hash) DO NOTHING
RETURNING pubkey
```
Les domaines `u64` restent encodés sans narrowing via chaînes décimales et cast PostgreSQL `TEXT::NUMERIC` :
```text
slot
lamports
rent_epoch
```
Les pubkeys/hash restent en bytes fixes et `data` est persistée sans base64 ni copie structurale.
Si l'INSERT gagne :
```text
RawEntityWriteOutcome::Inserted
```
Si l'identité existe déjà, le backend charge la ligne canonique sous :
```text
FOR UPDATE
```
et compare exactement :
```text
reference
lamports
owner
executable
rent_epoch
data
```
Le `state_hash` ne suffit donc jamais à déclarer l'idempotence. Contenu identique -> `AlreadyPresent`; contenu divergent -> `Conflict`.
## 6. Insertion idempotente de l'observation
L'observation est encodée avec toute la provenance et toutes les metadata optionnelles :
```text
observation_key
account pubkey/slot/state_hash
provider
protocol
acquisition_method
origin
received_at
capture_session_id
commitment
endpoint_id
filter_id
observed_at
source_payload_hash
source_payload_size_bytes
is_startup
transaction_signature
write_version
```
Le SQL utilise :
```text
ON CONFLICT (observation_key) DO NOTHING
```
En cas de collision, la ligne existante est relue `FOR UPDATE`, décodée avec le codec hostile-row de `pre.004`, puis comparée au `RawAccountObservation` entrant complet. Identique -> `AlreadyPresent`; divergent -> `Conflict`.
Aucun champ optionnel n'est ignoré dans l'idempotence.
## 7. Atomicité
State et observation vivent dans la même transaction PostgreSQL :
```text
BEGIN
state insert/compare
observation insert/compare
COMMIT
```
Si un nouveau state a été inséré mais que l'observation collide avec un contenu divergent, le chemin retourne une erreur avant commit. Le drop de la transaction annule alors l'insertion du state : aucune moitié d'acquisition durable n'est admise.
Aucun pré-check `has_*`, aucun `ON CONFLICT DO UPDATE`, aucun `UPDATE` et aucun `DELETE` account n'est introduit.
## 8. Surface backend
`PostgresBackend` expose le bridge étroit :
```text
persist_raw_account_acquisition(
RawAccountState,
RawAccountObservation,
) -> RawAcquisitionWriteOutcome
```
Le bridge reste backend-agnostic et n'expose aucun type PostgreSQL.
Cette méthode n'implémente pas encore `RawAccountStateWrite`. Les quatre traits account restent volontairement absents jusqu'à `pre.008` afin d'ouvrir la conformance physique 10/10 en une seule tranche.
## 9. Tests et canaries
Les tests unitaires ajoutés couvrent :
```text
guard réseau state
référence observation/state exacte
state identique -> comparaison vraie
même référence/hash + lamports divergent -> comparaison fausse
observation identique -> égalité exacte
même clé/référence + write_version divergent -> non-identique
encodage exact des cinq RawAcquisitionOrigin
```
Le canari `dependency_boundary` exige maintenant :
```text
INSERT_ACCOUNT_STATE_SQL
INSERT_ACCOUNT_OBSERVATION_SQL
LOCK_ACCOUNT_STATE_SQL
LOCK_ACCOUNT_OBSERVATION_SQL
ON CONFLICT ... DO NOTHING
FOR UPDATE
persist_raw_account_acquisition
```
et continue d'interdire :
```text
UPDATE / DELETE account
ON CONFLICT DO UPDATE
record_raw_account_observation
list_raw_account_states
OFFSET
les quatre impl RawAccount*
```
`public_api.rs` vérifie le nouveau bridge backend-agnostic.
## 10. Scope explicitement reporté
```text
record_raw_account_observation pre.006
races/cancellation ciblées pre.006
list_raw_account_states pre.007
cursor KSPA pre.007
4 impl RawAccount* PostgresBackend pre.008
4 dispatch Store pre.008
live PostgreSQL account pre.009
```
## 11. Migrations
Aucune ressource sous `migrations/` n'est modifiée.
Checksums conservés :
```text
V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 12. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 13. Fichier ajouté
```text
deltas/0.3.4/pre.005.md
```
## 14. Fichiers supprimés
```text
aucun
```
## 15. Validations exécutées dans l'environnement d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
PASS
```
Les contrôles différentiels doivent également confirmer l'identité byte-à-byte de V000/V001/V002 et l'absence de surface `pre.006+`.
## 16. Validations non exécutées dans l'environnement d'assemblage
`cargo`, `rustc` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo post-`pre.005` n'est revendiqué ici.
Gate opérateur à rejouer :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 17. Verdict
`pre.005` ouvre uniquement l'acquisition atomique `RawAccountState + RawAccountObservation`. L'observation supplémentaire, les races/cancellations dédiées, la pagination et les capabilities account restent fermées conformément au sizing souple de `0.3.4`.

148
deltas/0.3.4/pre.006.md Normal file
View File

@@ -0,0 +1,148 @@
<!-- file: deltas/0.3.4/pre.006.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.006` — observation account supplémentaire + contrat race/cancellation
## 1. Base requise
```text
0.3.4-pre.5
```
Le gate opérateur fourni le 2026-08-30 pour `pre.005` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 142 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 56 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
L'acquisition atomique/idempotente de `pre.005` est donc considérée acquise.
## 2. Objectif
Implémenter uniquement l'observation supplémentaire d'un `RawAccountState` déjà durable :
```text
network guard avant I/O
state reference obligatoire
FOR KEY SHARE
observation idempotente existante
aucun state implicite
aucun overwrite
transaction unique
```
La pagination account et les quatre implémentations de capabilities restent hors tranche.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.6
```
## 4. Observation supplémentaire
`record_raw_account_observation` valide d'abord :
```text
observation.account.network == backend.network
```
avant `pool.get()`.
La transaction exécute ensuite :
```text
SELECT 1
FROM ksp_raw_account_states
WHERE pubkey = $1
AND slot = $2::TEXT::NUMERIC
AND state_hash = $3
FOR KEY SHARE
```
Une référence absente retourne `ReferenceNotFound` avec phase statique `raw_account_observation_reference`. Aucun state n'est créé ou modifié.
## 5. Idempotence / conflit
Après le lock de référence, la tranche réutilise `persist_account_observation_row` de `pre.005` :
```text
INSERT ... ON CONFLICT (observation_key) DO NOTHING
collision -> SELECT ... FOR UPDATE
identique -> AlreadyPresent
divergent -> Conflict
```
La comparaison porte sur le `RawAccountObservation` complet : référence, provenance et metadata optionnelles incluses.
## 6. Concurrence et cancellation unitaires
Les canaris de `pre.006` figent l'ordre :
```text
client.transaction()
FOR KEY SHARE référence
persist_account_observation_row
sql_transaction.commit()
```
Le chemin ne détache aucune tâche (`tokio::spawn` absent), ne fuit pas le handle (`std::mem::forget` absent) et ne commit pas avant l'insertion/comparaison observation. Si la future est annulée avant le commit, le `Transaction` local est droppé et la transaction PostgreSQL non committée est rollback.
La preuve concurrente réelle avec tâches simultanées et `JoinHandle::abort()` reste explicitement réservée au live `pre.009`.
## 7. Bridge backend
`PostgresBackend` expose :
```text
record_raw_account_observation(
RawAccountObservation,
) -> RawObservationWriteOutcome
```
Le bridge reste construit uniquement avec des types `ksp-store-api`. `RawAccountObservationWrite` n'est pas encore implémenté ; l'ouverture des quatre traits account reste groupée en `pre.008`.
## 8. Scope négatif
Restent absents :
```text
list_raw_account_states
cursor KSPA
OFFSET
UPDATE / DELETE account
ON CONFLICT DO UPDATE
impl RawAccountStateRead for PostgresBackend
impl RawAccountStateWrite for PostgresBackend
impl RawAccountObservationRead for PostgresBackend
impl RawAccountObservationWrite for PostgresBackend
Store dispatch account
```
Toutes les migrations V000/V001/V002 restent byte-inchangées. Le checksum V002 final reste :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 9. Validations d'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
migrations V000/V001/V002 byte-identiques à pre.005 PASS
scope pre.007+ absent PASS
```
Le gate Cargo/Clippy/tests de `pre.006` doit être rejoué par l'opérateur.

View File

@@ -0,0 +1,70 @@
<!-- file: deltas/0.3.4/pre.007-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.007-fix.001`
## Base
```text
0.3.4-pre.007
```
## Objectif
Corriger exclusivement le gate opérateur de `pre.007` : cinq warnings Clippy `clone_on_copy` dans les tests cursor account et deux canaris historiques `pre.005/pre.006` qui interdisaient encore `list_raw_account_states` alors que cette surface est devenue légitime en `pre.007`.
## Modifications
- `workspace.package.version` passe à `0.3.4-pre.7.fix.1` ;
- suppression des cinq `Pubkey::clone()` inutiles dans `unit_tests/raw_account.rs` ;
- transformation des canaris historiques `pre.005/pre.006` pour conserver les interdictions destructives et les interdictions de capabilities `pre.008`, sans interdire la pagination account désormais possédée ;
- mise à jour du plan et de la validation pour tracer le gate et le correctif.
## Invariants préservés
```text
aucun changement src/raw_account.rs
aucun changement src/raw_account/cursor.rs
aucun changement src/runtime.rs
aucun changement SQL runtime
aucune migration V000/V001/V002 modifiée
aucune impl RawAccount* ouverte
aucun dispatch Store account ajouté
```
Le checksum V002 reste :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## Fichier ajouté
```text
deltas/0.3.4/pre.007-fix.001.md
```
## Gate opérateur à rejouer
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```

248
deltas/0.3.4/pre.007.md Normal file
View File

@@ -0,0 +1,248 @@
<!-- file: deltas/0.3.4/pre.007.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.007` — pagination account keyset + cursor `KSPA`
## 1. Base requise
```text
0.3.4-pre.6
```
Le gate opérateur fourni le 2026-08-30 pour `pre.006` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 143 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 58 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
L'observation supplémentaire account et son contrat transactionnel/cancellation sont donc considérés acquis.
## 2. Objectif
Implémenter uniquement la navigation `RawAccountStateQuery` :
```text
ordre total (slot, pubkey, state_hash)
ASC / DESC complètement inversés
filtre pubkey optionnel
bornes de slot inclusives
keyset pagination uniquement
cursor account KSPA V1
LIMIT + 1 sans policy KSP
```
Les quatre capabilities account et leur dispatch `Store` restent hors tranche jusqu'à `pre.008`.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.7
```
## 4. Requêtes physiques
`raw_account.rs` ajoute quatre SELECT privés :
```text
sans pubkey : ASC / DESC
avec pubkey : ASC / DESC
```
L'ordre total est :
```text
ASC (slot ASC, pubkey ASC, state_hash ASC)
DESC (slot DESC, pubkey DESC, state_hash DESC)
```
Les continuations utilisent exclusivement :
```text
(slot, pubkey, state_hash) > (last_slot, last_pubkey, last_state_hash)
(slot, pubkey, state_hash) < (last_slot, last_pubkey, last_state_hash)
```
Sans filtre, l'index V002 `(slot, pubkey, state_hash)` porte la navigation. Avec filtre pubkey, la PK `(pubkey, slot, state_hash)` porte le préfixe account et le même ordre déterministe.
## 5. Cursor account V1
Nouveau module privé :
```text
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
```
Format fixe :
```text
0..4 magic KSPA
4 version 1
5..13 last_slot u64 big-endian
13..45 last_pubkey 32 bytes
45..77 last_state_hash 32 bytes
77..109 SHA-256 binding digest
```
Domaine :
```text
KSP/raw-account-state-cursor/v1
```
Le digest lie :
```text
network
pubkey filter presence/value
direction
slot.start_inclusive
slot.end_inclusive
last_slot
last_pubkey
last_state_hash
```
Un cursor transaction `KSPT` ne peut donc pas être rejoué comme cursor account et échoue dès le magic.
## 6. Limite physique de page
Comme la verticale transaction, le backend applique `LIMIT + 1` uniquement pour détecter une page suivante. Aucun maximum fonctionnel KSP n'est ajouté.
La limite physique exacte reste :
```text
9_223_372_036_854_775_806
```
car `requested + 1` doit être représentable par un `i64` PostgreSQL.
## 7. Mapping de liste
Chaque row de liste revalide :
```text
pubkey exact 32 bytes
slot NUMERIC(20,0)::text -> u64 exact
state_hash exact 32 bytes
```
Le résultat public du bridge est exclusivement :
```text
RawPage<RawAccountStateReference>
```
Aucun row PostgreSQL, SQL ou cursor décodé ne traverse la frontière backend.
## 8. Bridge backend
`PostgresBackend` expose :
```text
list_raw_account_states(
&RawAccountStateQuery,
) -> RawPage<RawAccountStateReference>
```
Ce bridge reste une méthode étroite. `RawAccountStateRead`, `RawAccountStateWrite`, `RawAccountObservationRead` et `RawAccountObservationWrite` ne sont toujours pas implémentés sur `PostgresBackend`.
## 9. Scope négatif
Restent absents :
```text
OFFSET
batch max 500/1000
priority / worker policy
clamp/min artificiel
UPDATE / DELETE account
ON CONFLICT DO UPDATE
impl RawAccountStateRead for PostgresBackend
impl RawAccountStateWrite for PostgresBackend
impl RawAccountObservationRead for PostgresBackend
impl RawAccountObservationWrite for PostgresBackend
Store dispatch account
```
Toutes les migrations V000/V001/V002 restent byte-inchangées. Le checksum V002 final reste :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 10. Fichiers ajoutés
```text
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
deltas/0.3.4/pre.007.md
```
## 11. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/raw_account.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 12. Fichiers supprimés
```text
aucun
```
## 13. Validations exécutées à l'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
migrations V000/V001/V002 byte-identiques à pre.006 PASS
scope pre.008 absent PASS
```
Le gate Cargo/Clippy/tests de `pre.007` doit être rejoué par l'opérateur.
## 14. Validations non exécutées
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
`cargo` n'est pas disponible dans l'environnement d'assemblage.
## 15. Décisions prises
- le cursor account reste distinct de `KSPT` par magic et domaine SHA-256 ;
- le filtre pubkey fait partie du binding du cursor ;
- les deux chemins SQL avec/sans pubkey sont séparés pour conserver une surface physique compatible avec les indexes possédés ;
- aucune policy d'exécution n'est ajoutée à la pagination Store ;
- les quatre capabilities account restent groupées pour `pre.008`.
## 16. Questions ouvertes
Aucune question bloquante pour `pre.008`.

View File

@@ -0,0 +1,72 @@
<!-- file: deltas/0.3.4/pre.008-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.008-fix.001`
## Base
```text
0.3.4-pre.008
```
## Objectif
Corriger exclusivement le canari `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` qui empêche le gate `pre.008` de compiler : une déclaration `runtime` a été retirée du test d'ownership qui la consomme encore et laissée dans le test V002 où elle n'est plus utilisée depuis l'ouverture légitime des capabilities account.
## Modifications
- `workspace.package.version` passe à `0.3.4-pre.8.fix.1` ;
- restauration de `let runtime = include_str!("../src/runtime.rs");` dans `pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private` ;
- suppression de la déclaration désormais inutilisée dans `pre_003_v002_schema_is_complete_and_keeps_capability_implementation_out_of_schema_layers` ;
- aucune assertion métier, capability ou règle d'ownership n'est modifiée ;
- plan et validation réconciliés avec l'échec ciblé et le fix.
## Invariants préservés
```text
PostgresBackend Raw* impls = 10
Store Raw* impls = 10
ksp-store-api inchangée
ksp-store-lib production inchangée
ksp-store-postgres-lib production inchangée
raw_account.rs inchangé
raw_account/cursor.rs inchangé
runtime.rs inchangé
migrations V000/V001/V002 byte-inchangées
```
Checksum V002 inchangé :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## Fichier ajouté
```text
deltas/0.3.4/pre.008-fix.001.md
```
## Gate opérateur à rejouer
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```

219
deltas/0.3.4/pre.008.md Normal file
View File

@@ -0,0 +1,219 @@
<!-- file: deltas/0.3.4/pre.008.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.008` — capabilities account + conformance RAW 10/10
## 1. Base requise
```text
0.3.4-pre.007-fix.001
workspace.package.version = 0.3.4-pre.7.fix.1
```
Le gate opérateur fourni le 2026-08-30 pour `pre.007-fix.001` est entièrement vert :
```text
cargo fmt --all PASS
audit Rust général / exports / workspace PASS
audit Markdown PASS — 239 tables / 145 files
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-store-api PASS
cargo test -p ksp-store-lib PASS
cargo test -p ksp-store-postgres-lib PASS — 63 unit tests + canaris, live ignored
cargo test -p ksp-config-lib PASS — 128 unit tests + ownership/public API
cargo check -p ksp-store-lib --no-default-features PASS
```
La verticale account possède donc déjà ses primitives physiques read/write/pagination validées avant ouverture des traits.
## 2. Objectif
Ouvrir uniquement les quatre capabilities account existantes :
```text
RawAccountStateRead
RawAccountStateWrite
RawAccountObservationRead
RawAccountObservationWrite
```
sur :
```text
PostgresBackend
Store
```
et transformer les canaris pour fermer l'inventaire RAW à :
```text
6 RawTransaction* + 4 RawAccount* = 10/10
```
Aucun nouveau SQL, modèle, migration, type public ou comportement physique n'est ajouté.
## 3. Version
Le workspace passe à :
```text
0.3.4-pre.8
```
## 4. `PostgresBackend` — quatre traits account
`src/runtime.rs` implémente exactement une fois :
```text
RawAccountStateRead
RawAccountStateWrite
RawAccountObservationRead
RawAccountObservationWrite
```
Chaque méthode adapte exclusivement le bridge étroit déjà présent :
```text
get_raw_account_state
list_raw_account_states
persist_raw_account_acquisition
get_raw_account_observation
record_raw_account_observation
```
Les erreurs physiques passent par `map_capability_error`, comme les six traits transaction. Aucun SQL ou type PostgreSQL n'entre dans l'API de capability.
## 5. `Store` — dispatch backend-neutral
`Store` implémente les mêmes quatre traits et délègue uniquement au backend compilé.
Guards réseau avant dispatch :
```text
get state reference.network
list state query.network
persist state state.reference.network + observation.account.network
record obs observation.account.network
get obs aucun input réseau — RawObservationKey uniquement
```
Le backend conserve en plus le guard d'égalité :
```text
observation.account == state.reference
```
avant `pool.get()` sur l'acquisition atomique.
Avec `postgres` désactivé, les dix traits restent implémentés sur `Store` et leur chemin runtime retourne `backend_not_compiled`, sans dépendance physique compilée.
## 6. Canaris transformés
Les canaris historiques anti-account ne sont pas supprimés. Ils deviennent des canaris de placement et de complétude :
```text
PostgresBackend : exactement 10 impl Raw*
Store : exactement 10 impl Raw*
chaque impl account apparaît exactement une fois
aucun impl account dans raw_account.rs
aucun impl métier dans migration.rs/schema.rs
aucun SQL/type PostgreSQL dans ksp-store-lib
validate_operation_network occurrences Store = 14
```
Les canaris `pre.005`, `pre.006` et `pre.007` conservent leurs interdictions destructives et de policy (`UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET`, clamp/batch policy), sans exiger que les traits account restent fermés après leur tranche d'ouverture légitime.
## 7. Invariants préservés
```text
ksp-store-api inchangée
raw_account.rs inchangé
raw_account/cursor.rs inchangé
SQL runtime account inchangé
V000/V001/V002 byte-inchangées
aucun account retention/archive/purge
aucun worker/job/backfill policy
aucun Transport -> Store
aucun nouveau backend
```
Checksum V002 inchangé :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 8. Fichiers ajoutés
```text
deltas/0.3.4/pre.008.md
```
## 9. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-lib/src/lib.rs
crates/ksp-store-lib/src/store.rs
crates/ksp-store-lib/tests/dependency_boundary.rs
crates/ksp-store-lib/tests/hardening_completeness.rs
crates/ksp-store-lib/tests/public_api.rs
crates/ksp-store-postgres-lib/src/lib.rs
crates/ksp-store-postgres-lib/src/runtime.rs
crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/public_api.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 10. Fichiers supprimés
```text
aucun
```
## 11. Validations exécutées à l'assemblage
```text
python3 scripts/audit_rust_workspace_rules.py PASS
python3 scripts/audit_markdown_tables.py ... deltas/0.3.4 PASS
inventaire PostgresBackend Raw* = 10 PASS
inventaire Store Raw* = 10 PASS
validate_operation_network occurrences Store = 14 PASS
migrations V000/V001/V002 byte-identiques à pre.007-fix.001 PASS
raw_account.rs / raw_account/cursor.rs inchangés PASS
ksp-store-api inchangée PASS
```
`cargo`, `rustfmt`, Clippy et les tests Rust ne sont pas disponibles dans l'environnement d'assemblage ; le gate opérateur complet doit être rejoué.
## 12. Gate opérateur à rejouer
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 13. Décisions prises
- `pre.008` n'ajoute aucune primitive physique : elle ouvre uniquement les traits déjà prévus par `ksp-store-api`.
- `RawAccountObservationRead` n'ajoute pas de faux guard réseau, car `RawObservationKey` ne porte pas de réseau.
- La conformance 10/10 est vérifiée par inventaire exact, pas par simple présence partielle.
- Les canaris historiques sont transformés vers les invariants encore pertinents au lieu d'être supprimés.
- La preuve réelle PostgreSQL account reste réservée à `pre.009`.
## 14. Questions ouvertes
```text
aucune question bloquante pour pre.008
```

216
deltas/0.3.4/pre.009.md Normal file
View File

@@ -0,0 +1,216 @@
<!-- file: deltas/0.3.4/pre.009.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.009` — preuve PostgreSQL live `RawAccountState`
## 1. Base et gate d'entrée
Base opérateur obligatoire :
```text
0.3.4-pre.008-fix.001
```
Le gate opérateur fourni le 2026-08-30 est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `ksp-store-lib --no-default-features` passent. L'inventaire RAW 10/10 de `pre.008` est donc acquis.
## 2. Version
```text
workspace.package.version = 0.3.4-pre.9
```
## 3. Nouveau test PostgreSQL réel account
La tranche ajoute :
```text
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
```
Le test est `#[ignore]`, lit une URI PostgreSQL dédiée uniquement sur `stdin`, ne l'affiche jamais et refuse de démarrer lorsqu'une table KSP V000/V001/V002 existe déjà. Il ne nettoie que le schéma qu'il a prouvé absent avant son propre bootstrap.
PostgreSQL < 15 est refusé comme pour les preuves live existantes.
## 4. Bootstrap, réseau, migration V002 et schema update
La preuve couvre :
- bootstrap V000 + V001 + V002 sur base vide ;
- health Ready avec migration 2 ;
- refus d'une réouverture sous un autre `RawNetworkId` ;
- réouverture idempotente ;
- suppression contrôlée de `ix_ksp_raw_account_states_slot_pubkey_state_hash` ;
- `schema_autoupdate=false` qui bloque le drift ;
- `schema_autoupdate=true` qui recrée et revalide l'index account manquant.
Aucune ressource de migration n'est modifiée et aucun historique n'est réécrit.
## 5. Domaine account et round-trip
Le live matérialise les extrêmes figés par `pre.001` :
```text
slot = u64::MAX
lamports = u64::MAX
rent_epoch = u64::MAX
write_version = u64::MAX
data vide = admise
data 16 MiB exact = admise et relue complète
```
La lecture réouverte doit restituer exactement la référence, le contenu commun et les metadata optionnelles sans narrowing.
## 6. Atomicité, idempotence, conflits et observations
La preuve couvre :
- acquisition state + observation `Inserted/Inserted` ;
- replay exact `AlreadyPresent/AlreadyPresent` ;
- même référence complète avec contenu divergent -> `Conflict` ;
- même `(pubkey, slot)` avec deux `state_hash` distincts -> deux states durables ;
- collision d'`observation_key` pendant une nouvelle acquisition -> `Conflict` et rollback du state nouvellement tenté ;
- observation supplémentaire -> `Inserted`, puis replay exact -> `AlreadyPresent` ;
- même observation key divergente -> `Conflict` ;
- référence state absente -> `ReferenceNotFound` ;
- round-trip exact de la provenance commune et des metadata Yellowstone `is_startup`, `transaction_signature`, `write_version`.
## 7. Pagination et cursor `KSPA`
Le test matérialise cinq states sur la plage `200..=202` et prouve :
```text
ASC (slot, pubkey, state_hash)
DESC (slot, pubkey, state_hash) intégralement inversé
page size = 2
filtre pubkey exact
```
Il rejoue ensuite le cursor sous direction différente, corrompt son digest, puis fournit un token de famille transaction `KSPT`. Les trois cas doivent être `QueryInvalid`.
Aucun `OFFSET`, plafond fonctionnel de page, batch-size ou policy worker n'est introduit.
## 8. Concurrence réelle
Deux acquisitions identiques sont lancées en vraies tâches concurrentes : le résultat attendu est exactement un `Inserted` et un `AlreadyPresent`.
Deux acquisitions divergentes partageant la même référence `(pubkey, slot, state_hash)` sont ensuite lancées en concurrence : exactement une gagne et l'autre doit produire `Conflict`.
## 9. Cancellation réelle
Le test :
1. crée une observation account durable ;
2. verrouille cette `observation_key` via une transaction PostgreSQL administrateur `FOR UPDATE` ;
3. démarre une nouvelle acquisition qui insère son state dans une transaction non committée puis bloque sur la collision observation ;
4. vérifie que l'opération reste bloquée au-delà du délai canari ;
5. l'annule via `JoinHandle::abort()` ;
6. libère le verrou administrateur ;
7. vérifie physiquement que le state de l'acquisition annulée n'est pas durable.
Cette preuve exerce le rollback par cancellation réelle figé en `pre.006`.
## 10. Coexistence `RawTransaction`
Dans le même schéma V002, le live persiste une acquisition account et une acquisition transaction utilisant volontairement la même `RawObservationKey` dans leurs tables familiales distinctes. Les deux familles doivent rester lisibles indépendamment, et la transaction est aussi vérifiée physiquement avant le reopen final.
Le test historique `postgres_raw_transaction_live.rs` est réconcilié uniquement sur son inventaire d'isolation : son probe et son cleanup prennent désormais aussi en compte :
```text
ksp_raw_account_states
ksp_raw_account_observations
```
Aucun scénario métier RawTransaction n'est modifié.
## 11. Isolation et cleanup
Les deux lives considèrent désormais l'inventaire complet de tables Store V002 :
```text
ksp_store_schema_migrations
ksp_store_identity
ksp_raw_transactions
ksp_raw_transaction_observations
ksp_raw_transaction_archive_payloads
ksp_raw_account_states
ksp_raw_account_observations
```
Ils refusent un schéma préexistant et ne suppriment que le schéma qu'ils ont eux-mêmes bootstrapé après preuve d'absence initiale.
## 12. Migrations
Aucune ressource V000/V001/V002 n'est modifiée.
Checksums attendus :
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 13. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 14. Fichiers ajoutés
```text
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
deltas/0.3.4/pre.009.md
```
Aucune suppression de fichier.
## 15. Validation exécutée dans l'environnement de génération
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean
```
Cargo, rustfmt et PostgreSQL réel ne sont pas disponibles dans l'environnement de génération. Le gate opérateur standard et le live restent donc à exécuter localement.
## 16. Gate opérateur standard
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 17. Gate PostgreSQL live opt-in
Sur une base PostgreSQL dédiée et vide de toute table KSP gérée :
```bash
printf '%s\n' '<URI_POSTGRES_DEDIEE>' | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
```
Le live `RawTransaction` peut ensuite être rejoué séparément sur une base de nouveau vide :
```bash
printf '%s\n' '<URI_POSTGRES_DEDIEE>' | cargo test -p ksp-store-postgres-lib --test postgres_raw_transaction_live -- --ignored --nocapture --test-threads=1
```
## 18. Suite si les gates sont verts
```text
0.3.4-pre.010 — hardening/completeness cross-family
```

49
deltas/0.3.4/pre.010.md Normal file
View File

@@ -0,0 +1,49 @@
<!-- file: deltas/0.3.4/pre.010.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.010` — hardening et complétude cross-family
## Base
Base directe : `0.3.4-pre.009`. Le gate opérateur standard de `pre.009` est PASS et le live `postgres_raw_account_live` est PASS sur PostgreSQL 17 avec URI dédiée lue depuis stdin.
## Scope
Cette tranche est strictement limitée aux canaris de hardening et de complétude. Aucun runtime, SQL, migration, modèle API, capability ou export public n'est ajouté.
Modifications fonctionnelles de tests :
- `ksp-store-postgres-lib/tests/hardening_completeness.rs` fige la surface SQL account non destructive/keyset-only, la séparation `KSPA`/`KSPT`, l'absence de FK cross-family et l'inventaire V002 exact de 32 resources ;
- `ksp-store-lib/tests/hardening_completeness.rs` compare directement les ensembles de capabilities RAW de `Store` et `PostgresBackend`, qui doivent être exactement identiques à dix traits, sans retention/delete/compaction account.
## Invariants préservés
```text
ksp-store-api inchangée
ksp-store-lib/src inchangé
ksp-store-postgres-lib/src inchangé
tests live inchangés
migrations V000/V001/V002 byte-inchangées
V002 checksum ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
RAW capabilities PostgresBackend 10
RAW capabilities Store 10
```
## Contrôles d'assemblage
Les audits Rust et Markdown doivent passer avant packaging. L'environnement d'assemblage ne possède pas Cargo ; le gate opérateur complet reste donc requis après application du delta.
## Gate opérateur à rejouer
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```

143
deltas/0.3.4/pre.011.md Normal file
View File

@@ -0,0 +1,143 @@
<!-- file: deltas/0.3.4/pre.011.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.011` — gate technique final RawAccountState
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.10
```
Le gate opérateur de `pre.010` du 30 août 2026 est entièrement vert : audits Rust/Markdown, workspace check, Clippy, suites ciblées Store API/façade/PostgreSQL/Config et compilation façade sans feature PostgreSQL passent.
La preuve live PostgreSQL 17 complète de référence reste celle de `pre.009`.
## 2. Objectif
Ouvrir exclusivement le gate technique final de `0.3.4` sans nouveau développement fonctionnel.
Cette tranche ne modifie aucun :
```text
src/**
test fonctionnel
migration SQL
Config runtime
manifest de crate
dépendance runtime
README/USAGE
CHANGELOG/ROADMAP
prompt suivant
```
## 3. Version
```text
workspace.package.version = 0.3.4-pre.11
```
Aucune autre propriété Cargo n'est modifiée.
## 4. Gate technique final attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
cargo test --workspace
cargo tree -p ksp-store-lib --edges normal
cargo tree -p ksp-store-lib -e features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree --duplicates
```
`cargo clean` peut être exécuté avant ce gate pour une reconstruction froide ; il n'est pas un invariant fonctionnel de la release.
## 5. Replay PostgreSQL réel
Le live `RawAccountState` est rejoué comme preuve finale ciblée :
```bash
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
unset KSP_PG_TEST_URI
```
Ce live couvre déjà bootstrap/reopen V002, réparation de schéma, bornes u64/data, acquisition/idempotence/conflits, pagination/cursors, concurrence/cancellation et coexistence réelle avec `RawTransaction`. Les lives foundation et RawTransaction séparés ne sont donc pas requis par `pre.011`.
Le contrat de sécurité reste inchangé : URI via stdin, aucun echo de l'URI, refus d'un schéma KSP préexistant et cleanup uniquement de la surface créée après preuve d'absence initiale.
## 6. Graphes et packaging
Les graphes Cargo figent les frontières façade/backend/features et l'absence de dépendance PostgreSQL lorsque la feature correspondante n'est pas activée.
Aucun build Tauri final n'est imposé : `0.3.4` ne modifie ni resources applicatives, ni packaging Tauri, ni surface Config applicative.
## 7. Fichiers ajoutés
```text
deltas/0.3.4/pre.011.md
```
## 8. Fichiers modifiés
```text
Cargo.toml
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 9. Fichiers supprimés
Aucun.
## 10. Migrations
Aucune ressource SQL n'est modifiée.
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 11. 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.4
contrôle statique version/payload
contrôle byte-for-byte de migrations/
contrôle byte-for-byte de ksp-store-api et des arbres src Store/PostgreSQL
```
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement de génération ; aucun gate Cargo n'est déclaré PASS ici.
## 12. Questions ouvertes
Aucune question de design. Tout défaut découvert par ce gate ouvre `pre.011-fix.NNN` avant la réconciliation documentaire.
## 13. Suite
Après gate technique, graphes et live entièrement verts :
```text
0.3.4-pre.012 — réconciliation documentaire finale
0.3.4-pre.013 — préparation publication + prompt 0.3.5
0.3.4-rel.001 — publication stable
```

185
deltas/0.3.4/pre.012.md Normal file
View File

@@ -0,0 +1,185 @@
<!-- file: deltas/0.3.4/pre.012.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.012` — réconciliation documentaire finale Store/PostgreSQL RAW
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.011
```
Le couloir technique est fermé. Le replay de `pre.011` confirme les tests façade sans backend, le workspace complet, les graphes Cargo et le live `RawAccountState` sur PostgreSQL 17.
## 2. Objectif
Réconcilier exclusivement les documents durables avec la surface Store/PostgreSQL effectivement validée.
Cette tranche ne rouvre aucun comportement Rust, test fonctionnel, Config, schema, migration, dépendance ou feature et ne prépare pas encore la publication.
## 3. Version
Conformément à `VER-ID-009`, cette prerelease non-fix synchronise :
```text
workspace.package.version = 0.3.4-pre.12
```
Aucune autre propriété Cargo n'est modifiée.
## 4. Résultat technique hérité
```text
Store/Postgres RAW capabilities 10/10
RawTransaction capabilities 6
RawAccount capabilities 4
ksp-store-lib --no-default-features PASS
cargo test --workspace PASS
Cargo graphs façade/backend/features/duplicates exécutés
postgres_raw_account_live PASS — PostgreSQL 17
V002 resources 32
V002 checksum ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
Les doublons Cargo du workspace sont transverses et ne révèlent aucune duplication propre au Store nécessitant une correction.
## 5. Réconciliation `ksp-store-lib`
Le README et le USAGE décrivent désormais durablement :
```text
exactement 10 capabilities RAW
6 RawTransaction* + 4 RawAccount*
lecture RawAccountState par référence
pagination keyset (slot,pubkey,state_hash), filtre pubkey optionnel
acquisition account atomique et idempotence exacte
lecture/ajout d'observation account
cursors opaques family-local
aucune rétention/archive/purge/delete/compaction account
aucune policy batch/priorité/scheduling dans Store
```
Les anciennes affirmations indiquant que les capabilities `RawAccount*` n'étaient pas dispatchées sont supprimées.
## 6. Réconciliation `ksp-store-postgres-lib`
Le README et le USAGE décrivent désormais :
```text
V001 = schéma RawTransaction
V002 = schéma RawAccountState
lectures state/observation account
acquisition state+observation atomique
observation supplémentaire reference-guarded
pagination keyset account et cursor KSPA
aucun ON CONFLICT DO UPDATE
aucune FK account transaction_signature -> RawTransaction
aucune rétention destructive account
```
La crate reste un backend physique derrière `ksp-store-lib`, jamais une seconde façade applicative.
## 7. Documents de release
Le plan `025` enregistre le PASS technique de `pre.011`, synchronise la version de travail sur `pre.012` et marque la réconciliation documentaire comme matérialisée.
La validation `021` ferme le verdict `pre.011` et ouvre le verdict documentaire `pre.012`.
`README.md` racine et les architectures Store ont été relus ; aucune affirmation devenue fausse n'y justifie une modification artificielle. `ksp-store-api` n'acquiert pas un README/USAGE uniquement pour cette tranche.
## 8. Hors scope préservé
```text
CHANGELOG.md
ROADMAP.md
prompt 0.3.5
src/**
tests/**
config/**
migrations/**
account retention/archive/purge/delete/compaction
workers/jobs/backfills
batch/priorité/backlog policy
nouveau backend
Store Desk
```
La préparation minimale `CHANGELOG`/`ROADMAP`/prompt reste réservée à `pre.013`.
## 9. Fichiers ajoutés
```text
deltas/0.3.4/pre.012.md
```
## 10. Fichiers modifiés
```text
Cargo.toml
crates/ksp-store-lib/README.md
crates/ksp-store-lib/USAGE.md
crates/ksp-store-postgres-lib/README.md
crates/ksp-store-postgres-lib/USAGE.md
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
## 11. Fichiers supprimés
Aucun.
## 12. Migrations
Aucune migration n'est modifiée.
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 13. Validations de génération
Les validations statiques applicables ont été rejouées après matérialisation de l'overlay :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (240 table(s), 151 file(s))
protected src/tests/config/migrations byte-identical à pre.011: PASS
```
Les exécutables Cargo/Rust ne sont pas disponibles dans l'environnement de génération ; aucun résultat Cargo post-overlay n'est revendiqué ici.
## 14. Gate opérateur après application
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
Aucun nouveau live PostgreSQL, `cargo test --workspace` ou graphe Cargo n'est requis par cette tranche documentaire ; ces preuves sont déjà fermées par `pre.011`.
## 15. Questions ouvertes
Aucune pour le périmètre `0.3.4`.
## 16. Suite
Après gate documentaire vert :
```text
0.3.4-pre.013 — préparation publication minimale + prompt 0.3.5
0.3.4-rel.001 — stabilisation/tag v0.3.4
```

283
deltas/0.3.4/pre.013.md Normal file
View File

@@ -0,0 +1,283 @@
<!-- file: deltas/0.3.4/pre.013.md -->
<!-- version: 1 -->
# Delta `0.3.4-pre.013` — préparation de publication et prompt `0.3.5`
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.012
workspace.package.version = 0.3.4-pre.12
```
Le gate opérateur de `pre.012`, exécuté le **31 août 2026**, est entièrement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
Le gate Markdown confirme :
```text
240 tables / 151 fichiers
```
`pre.012` n'a rouvert ni code fonctionnel ni migration ; la préparation de publication peut donc commencer sans correctif intermédiaire.
## 2. Objectif
Dernière prerelease avant `rel.001`, strictement limitée à la préparation de publication minimale définie par `VER-LIFECYCLE-003` :
```text
workspace.package.version -> 0.3.4-pre.13
finalisation publication de CHANGELOG.md
clôture de 0.3.4 et précision de 0.3.5 dans ROADMAP.md
création de prompts/024-V0_3_5_START_PROMPT.md
delta pre.013
```
Aucun README, USAGE, plan, validation, architecture, règle, source Rust, test, Config, schema ou migration n'est rouvert.
## 3. Version
Conformément à `VER-ID-009` :
```text
workspace.package.version = 0.3.4-pre.13
```
Le header du `Cargo.toml` racine est incrémenté uniquement parce que ce fichier est réellement modifié.
## 4. `CHANGELOG.md`
Une entrée stable `0.3.4` est ajoutée en tête et synthétise :
```text
RAW capabilities Store/PostgreSQL 10/10
4 RawAccount* ajoutées aux 6 RawTransaction*
V002 additive RawAccountState/RawAccountObservation
32 ressources V002
checksum V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
V000/V001 immuables
u64 exact NUMERIC(20,0)
bytes account complets <= 16 MiB
acquisition state+observation atomique
idempotence/conflit exacts
observation supplémentaire reference-guarded
metadata Yellowstone observation-only
pagination keyset (slot,pubkey,state_hash)
cursor KSPA V1 109 bytes anti-replay
aucune rétention destructive account
live PostgreSQL 17 vert
workspace + no-default-features + graphes verts
```
Le changelog référence également le prompt `0.3.5` et précise que l'archive kbot3 n'y est pas requise.
## 5. `ROADMAP.md`
L'entrée `0.3.4` passe à `[X]` et décrit la surface stable effectivement validée.
L'entrée `0.3.5` est précisée sans préjuger du résultat de son `pre.001` :
```text
ksp-interface-lib uniquement
matrice HTTP/WS/Yellowstone/Helius
logs / slot-root-slotsUpdates / transaction status / vote à auditer
seuls les faits event-only réellement convergents et utiles sont matérialisés
aucune duplication RawTransaction/RawAccountState
aucun event bus
aucune dépendance Interface -> Transport/Store
```
Les étapes `0.3.6` et `0.3.7` restent inchangées.
## 6. Prompt `0.3.5`
Le nouveau prompt ouvre :
```text
0.3.5 — ksp-interface-lib : modèles passifs/event-only réellement partagés
```
Base autoritaire :
```text
v0.3.4
```
Le `pre.001` est réservé à :
```text
lecture règles/architecture
inventaire Interface actuel
inventaire Store RAW 10/10
inventaire des surfaces HTTP/WS/gRPC/provider event-like
identification des consumers
matrice producer -> nature de fait
matrice de convergence sémantique
ownership Interface/Transport/Store/composition
anti-duplication RawTransaction/RawAccountState
API sketch des seuls types admis
bornes / Debug / Clone / non_exhaustive
graphe cible
threat model
sizing
plan 026 + validation 022
```
Le prompt interdit une grande enum `Event`, l'Option-soup, les DTOs provider promus tels quels, un event bus, une nouvelle persistence et tout codec/logging runtime non justifié.
## 7. Archive historique
`0.3.5` **n'exige pas** :
```text
khadhroony-bot3_v0.5.3-pre.005-fix010.zip
```
La raison est explicite dans le prompt : l'autorité de cette tranche est la sémantique actuelle déjà implémentée dans KSP, complétée par les sources officielles actuelles HTTP/WS/Yellowstone lorsque nécessaire. Un ancien modèle d'events ne doit pas piloter le contrat public par inertie.
## 8. Prévision souple `0.3.5`
Le prompt réserve :
```text
pre.001 audit consumers/sémantiques/ownership + sizing
pre.002 première famille passive retenue
pre.003 famille supplémentaire seulement si justifiée
pre.004 API publique + consumer externe + anti-duplication
pre.005 hardening/completeness Interface
pre.006 gate technique final
pre.007 réconciliation documentaire
pre.008 préparation publication
rel.001 publication stable
```
Le nombre réel de familles et de prereleases reste recalibrable par `pre.001` ; aucun quota minimal d'events n'est imposé.
## 9. Fichiers ajoutés
```text
prompts/024-V0_3_5_START_PROMPT.md
deltas/0.3.4/pre.013.md
```
## 10. Fichiers modifiés
```text
Cargo.toml
CHANGELOG.md
ROADMAP.md
```
## 11. Fichiers supprimés
```text
aucun
```
## 12. Surfaces explicitement non rouvertes
```text
README.md
RULES.md
crates/**
docs/**
config/**
migrations/**
prompts/001..023
```
Hormis le nouveau delta, aucun fichier sous `docs/` ou `crates/` n'est modifié.
## 13. Migrations
Aucune migration n'est modifiée.
Les checksums stables restent :
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 14. Validations exécutées pendant la génération
```text
python3 scripts/audit_rust_workspace_rules.py
-> General Rust rule audit: clean
-> Rust export completeness audit: 0 candidate(s)
-> KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
-> Markdown table audit: clean (240 table(s), 153 file(s))
scope exact pre.012 -> pre.013
-> 2 fichiers ajoutés
-> 3 fichiers modifiés
-> aucun fichier supprimé
-> DIFF_SCOPE=PASS
crates/** inchangé
-> PASS
docs/** inchangé
-> PASS
config/** inchangé
-> PASS
migrations/** byte-identiques
-> PASS
```
Les cinq fichiers de la lane minimale sont donc les seuls fichiers ajoutés/modifiés.
## 15. Validations non exécutées pendant la génération
La toolchain Cargo/Rust opérateur n'est pas supposée disponible dans l'environnement de génération. Aucun `cargo check`, Clippy ou test Cargo post-overlay n'est revendiqué sans exécution réelle.
Aucun live PostgreSQL supplémentaire n'est requis par cette tranche : `pre.013` ne modifie ni code, ni tests, ni migrations.
## 16. Gate opérateur demandé
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 17. Décisions prises
```text
0.3.4 est techniquement/documentairement fermé avant publication
0.3.5 reste limité à ksp-interface-lib et à ses docs/tests propres
les events persistants/replayables restent Store API
les DTOs protocol/provider restent Transport
aucun event bus n'est ouvert
aucune archive kbot3 n'est requise pour 0.3.5
les candidats event-only restent soumis à une matrice de convergence en pre.001
```
## 18. Questions ouvertes
Aucune pour la publication `0.3.4`.
Les questions de sélection exacte des events appartiennent explicitement à `0.3.5-pre.001`.
## 19. Suite
Si le gate `pre.013` est propre :
```text
0.3.4-rel.001
```
La livraison `rel.001` reste mécanique et minimale : version Cargo stable + delta uniquement. Elle ne corrige aucun défaut documentaire ou fonctionnel.

140
deltas/0.3.4/rel.001.md Normal file
View File

@@ -0,0 +1,140 @@
<!-- file: deltas/0.3.4/rel.001.md -->
<!-- version: 1 -->
# Delta `0.3.4-rel.001` — stabilisation et publication `v0.3.4`
## 1. Base requise
Base directe attendue :
```text
0.3.4-pre.013
workspace.package.version = 0.3.4-pre.13
```
Le gate opérateur de `pre.013`, exécuté le **31 août 2026**, est entièrement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Store/API/PostgreSQL/Config et `cargo check -p ksp-store-lib --no-default-features` passent.
Le gate Markdown confirme :
```text
240 tables / 153 fichiers
```
Aucun correctif intermédiaire n'est requis.
## 2. Objectif
Stabiliser mécaniquement la release `0.3.4` après fermeture technique et documentaire complète de la prerelease.
Cette tranche ne corrige ni code, ni documentation fonctionnelle, ni tests, ni Config, ni migration.
## 3. Version
La seule modification existante est la stabilisation de la version workspace :
```text
workspace.package.version = 0.3.4
```
Le header du `Cargo.toml` racine est incrémenté parce que ce fichier est réellement modifié.
## 4. Fichiers ajoutés
```text
deltas/0.3.4/rel.001.md
```
## 5. Fichiers modifiés
```text
Cargo.toml
```
## 6. Fichiers supprimés
```text
aucun
```
## 7. Surfaces explicitement inchangées
```text
CHANGELOG.md
ROADMAP.md
README.md
RULES.md
prompts/024-V0_3_5_START_PROMPT.md
crates/**
docs/**
config/**
migrations/**
tests/**
```
La surface préparée en `pre.013` reste donc byte-identique, hors mécanique Cargo et ajout du présent delta.
## 8. Migrations
Aucune migration n'est modifiée.
Les checksums stables restent :
```text
V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V002 ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 9. Invariants de publication
```text
RAW PostgreSQL/Store = 10/10 capabilities
RawTransaction inchangé
RawAccountState/Observation validé
V002 additive et figée
ksp-store-lib --no-default-features vert
aucune rétention destructive account
aucune nouvelle dépendance ou surface publique
prompt 0.3.5 déjà préparé en pre.013
```
## 10. Validation de la tranche
`rel.001` est strictement mécanique et ne demande aucun nouveau live PostgreSQL.
Le gate opérateur de `pre.013` constitue la preuve technique finale de la release ; après application du présent delta, le contrôle attendu porte uniquement sur l'absence de dérive de publication.
Commande recommandée :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
```
## 11. Publication
Après gate propre sur cet overlay :
```text
release stable = 0.3.4
tag attendu = v0.3.4
```
Aucun ancien tag stable ne doit être conservé si le workflow du dépôt impose uniquement le dernier tag stable.
## 12. Suite
La session suivante ouvre :
```text
0.3.5 — ksp-interface-lib : modèles passifs/event-only réellement partagés
prompts/024-V0_3_5_START_PROMPT.md
```

View File

@@ -0,0 +1,775 @@
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
<!-- version: 18 -->
# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
## 1. Statut de la release
`0.3.4-pre.001` a figé le design. `0.3.4-pre.002` a matérialisé la fondation physique V002 minimale puis `pre.002-fix.001` a corrigé deux canaris sans toucher au SQL. `0.3.4-pre.003` complète V002 avec les contraintes de domaine, l'index de navigation, la compatibilité de schéma et le checksum final ; `pre.003-fix.001` corrige uniquement l'ordre alphabétique du bloc `const` de `migration.rs` et son gate opérateur complet est PASS. `0.3.4-pre.004` ouvre le mapping PostgreSQL privé et les deux lectures `get` account ; `pre.004-fix.001` corrige uniquement leur conformité au profil Clippy KSP et un canari inutilisé. `pre.005` ajoute l'acquisition atomique, `pre.006` l'observation supplémentaire, `pre.007` la pagination/cursor account et `pre.007-fix.001` réconcilie ses canaris ; le gate opérateur complet du fix est PASS. `pre.008` ouvre les quatre capabilities account sur `PostgresBackend` puis leur dispatch dans `Store`, pour porter l'inventaire RAW à 10/10 sans nouveau SQL ; `pre.008-fix.001` corrige son unique canari mal réconcilié et son gate opérateur complet est PASS. `pre.009` ajoute la preuve PostgreSQL live account et réconcilie l'isolation du live RawTransaction avec le schéma V002 complet ; son gate standard et son live account sur PostgreSQL 17 sont PASS. `pre.010` ferme le hardening et la complétude cross-family sans modifier le runtime ou les migrations ; son gate opérateur complet est PASS. `pre.011` ouvre exclusivement le gate technique final, le replay live ciblé et les graphes Cargo avant toute réconciliation documentaire ; ses tests `--no-default-features`, le workspace complet, les graphes Cargo et le replay live account sur PostgreSQL 17 sont PASS. `pre.012` ouvre exclusivement la réconciliation documentaire finale sans rouvrir le code fonctionnel.
Base canonique auditée :
```text
v0.3.3
workspace.package.version = 0.3.3
```
Version de travail de cette prerelease :
```text
workspace.package.version = 0.3.4-pre.12
label = 0.3.4-pre.012
```
Décision de scope : `ksp-store-api` reste inchangée. L'inventaire backend/façade est désormais 10/10 et le couloir technique est fermé ; il ne reste que la réconciliation documentaire courante, puis la préparation de publication et la stabilisation.
## 2. Invariants hérités et frontières
La trajectoire reste :
```text
RAW -> STRUCTURAL -> DECODED -> DOMAIN
```
`0.3.4` ferme seulement la complétude physique **RAW Store/PostgreSQL**. Store persiste et sert ; il ne choisit ni batch-size, ni priorité, ni backlog, ni retry job, ni stratégie de backfill.
Frontières inchangées :
```text
ksp-store-lib -> ksp-store-api
ksp-store-lib[postgres] -> ksp-store-postgres-lib
ksp-store-postgres-lib -> ksp-store-api
ksp-store-postgres-lib -X-> ksp-store-lib
Config -> Store autorisé
Store/backend -X-> Config
Transport -X-> Store
consumer ordinaire -> ksp-store-lib
consumer ordinaire -X-> ksp-store-postgres-lib
```
Une instance `Store` reste liée à un `RawNetworkId` et une base physique. Aucun multiplexage réseau/database n'est ajouté.
## 3. Inventaire de capabilities : 6/10 -> 10/10
| Capability | Impl physique stable | État base | Action 0.3.4 |
|--------------------------------|-------------------------|---------------|--------------|
| RawTransactionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionObservationRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionObservationWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionRetentionRead | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawTransactionRetentionWrite | PostgresBackend + Store | stable 0.3.3 | préserver |
| RawAccountStateRead | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountStateWrite | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountObservationRead | aucune impl physique | pending 0.3.4 | implémenter |
| RawAccountObservationWrite | aucune impl physique | pending 0.3.4 | implémenter |
Canaries qui ferment encore volontairement la surface account dans `v0.3.3` :
- `crates/ksp-store-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for Store` ;
- `crates/ksp-store-lib/tests/hardening_completeness.rs` fige six impl transaction et interdit les quatre account ;
- `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for PostgresBackend` ;
- `crates/ksp-store-postgres-lib/tests/hardening_completeness.rs` interdit encore `impl ksp_store_api::RawAccount*` et toute matérialisation `ksp_raw_account` dans la slice transaction.
Ces canaries sont **transformées**, pas simplement supprimées, en `pre.008` : elles prouvent désormais exactement six capabilities transaction + quatre account. Les canaries qui imposent que V000 et `health.rs` restent business-free demeurent valides et ne doivent pas être assouplies.
### 3.1 Matrice détaillée des quatre capabilities account
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
## 4. Immutabilité V000/V001
Recalcul indépendant sur les bytes de l'archive stable :
```text
V000 legacy SQL SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
V001 logical resources = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
V001 resource count = 40
```
L'algorithme V001 est celui du moteur stable : domaine `ksp-migration-resources-v1\0`, puis pour chaque ressource ordonnée `id + NUL + SQL bytes + NUL` avant SHA-256.
Règles V002 :
```text
V000 : immuable
V001 : immuable
V002 : additive uniquement
nom logique proposé : raw_account_state
répertoire proposé : migrations/v002_raw_account_state/
resources proposées : tables/ + constraints/ + indexes/
hook migration supplémentaire : aucun
```
Le binding réseau singleton V001 est réutilisé ; V002 ne duplique pas `network` dans chaque ligne métier.
## 5. Audit field-by-field du contrat account
### 5.1 `RawAccountStateReference` + `RawAccountState`
Matrice complète modèle -> physique :
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|------------|----------------|-------------|--------------------|-----------------------|----------------------------------|------------------------|---------------------------------|
| network | RawNetworkId | 1 | ksp_store_identity | exact backend network | binding runtime -> reference | aucune colonne row | aucun si guard exact |
| pubkey | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
| slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes | fixed bytes -> RawContentHash | hash bytes -> BYTEA | aucun ; pas de recalcul backend |
| lamports | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| owner | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
| executable | bool | 1 | BOOLEAN | NOT NULL | bool direct | bool direct | aucun |
| rent_epoch | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
| data | Box<[u8]> | 1 | BYTEA | 0..=16 MiB | bytes + borne -> RawAccountState | bytes exacts -> BYTEA | base64/parse interdits |
Vue synthétique de projection :
| Champ API | Type API | Projection V002 | Invariant |
|------------|---------------------------|------------------------------------------|-----------------------------------------------------------------|
| network | RawNetworkId | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
| pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
| slot | u64 | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
| state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
| lamports | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| owner | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` |
| executable | bool | BOOLEAN NOT NULL | mapping direct |
| rent_epoch | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| data | Box<[u8]> | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
L'identité physique canonique retenue est :
```text
PRIMARY KEY (pubkey, slot, state_hash)
```
Le réseau fait partie de la référence API mais est porté physiquement par `ksp_store_identity` pour toute la base. Cette clé permet explicitement plusieurs états du même pubkey au même slot lorsque `state_hash` diffère.
`state_hash` est un digest **producer-owned**. Le backend ne le recalcule pas et ne suppose pas qu'il s'agit du hash de `data` seulement. L'idempotence vérifie donc le contenu complet malgré l'égalité du hash.
### 5.2 `RawAccountObservation`
Matrice complète modèle -> physique :
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|---------------------------|---------------------------------|-------------|--------------------|------------------------|----------------------------|------------------------|---------------------------|
| observation_key | RawObservationKey | 1 | BYTEA | exact 32 bytes | fixed bytes -> key | key bytes -> BYTEA | aucun |
| account.pubkey | Pubkey | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
| account.slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX + FK | decimal exact -> reference | decimal NUMERIC | narrowing interdit |
| account.state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
| provider | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| protocol | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| acquisition_method | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
| origin | RawAcquisitionOrigin | 1 | TEXT | 5 valeurs stables | static decode | static encode | unknown DB -> DataInvalid |
| received_at | RawTimestamp | 1 | BIGINT | 0..253402300799999 | checked i64 -> timestamp | u64 bounded -> i64 | borne API < i64::MAX |
| capture_session_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| commitment | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| endpoint_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| filter_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
| observed_at | Option<RawTimestamp> | 0..1 | BIGINT NULL | borne + <= received_at | NULL/checked timestamp | Option -> NULL/i64 | borne API < i64::MAX |
| source_payload_hash | Option<RawContentHash> | 0..1 | BYTEA NULL | NULL ou 32 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucun |
| source_payload_size_bytes | Option<u64> | 0..1 | BIGINT NULL | 0..=64 MiB | NULL/checked u64 | Option -> NULL/i64 | borne API < i64::MAX |
| is_startup | Option<bool> | 0..1 | BOOLEAN NULL | nullable | NULL/bool | Option -> NULL/bool | ne pas inventer false |
| transaction_signature | Option<RawTransactionSignature> | 0..1 | BYTEA NULL | NULL ou 64 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucune FK transaction |
| write_version | Option<u64> | 0..1 | NUMERIC(20,0) NULL | NULL ou 0..u64::MAX | NULL/decimal exact | Option -> NULL/NUMERIC | narrowing interdit |
Vue synthétique de projection :
| Champ API | Type API | Projection V002 | Invariant |
|---------------------------|---------------------------------|------------------------|--------------------------------------------------|
| observation_key | RawObservationKey / 32 bytes | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
| account.pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | FK composite vers state |
| account.slot | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
| account.state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | FK composite |
| provider | RawProvenanceCode | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
| protocol | RawProvenanceCode | TEXT NOT NULL | idem V001 |
| acquisition_method | RawProvenanceCode | TEXT NOT NULL | idem V001 |
| origin | RawAcquisitionOrigin | TEXT NOT NULL | backfill/import/live/repair/replay |
| received_at | RawTimestamp | BIGINT NOT NULL | 0..253402300799999 |
| capture_session_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| commitment | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| endpoint_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| filter_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
| observed_at | Option<RawTimestamp> | BIGINT NULL | borne + `observed_at <= received_at` |
| source_payload_hash | Option<RawContentHash> | BYTEA NULL | NULL ou 32 bytes |
| source_payload_size_bytes | Option<u64> | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
| is_startup | Option<bool> | BOOLEAN NULL | Yellowstone metadata observation-only |
| transaction_signature | Option<RawTransactionSignature> | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
| write_version | Option<u64> | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
La relation observation -> state est :
```text
FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)
```
Aucun `ON DELETE CASCADE` n'est nécessaire : l'API account ne possède ni delete ni retention. Aucune FK n'est créée vers `ksp_raw_transactions` pour `transaction_signature`; cette signature est une metadata source optionnelle et la transaction correspondante n'est pas garantie présente dans le Store.
`observation_key` est unique **dans la famille account**. Le contrat API expose des reads family-specific et ne définit aucune unicité globale transaction+account ; une registry globale ajouterait un couplage physique non requis.
### 5.3 `RawAccountStateQuery`
| Entrée query | Cardinalité | Projection physique |
|-----------------------|------------------------|------------------------------------------------------------------|
| network | obligatoire | guard DB mono-réseau + digest cursor |
| pubkey | optionnel | filtre égalité ; aucune query owner |
| slots.start_inclusive | optionnel | `slot >=` |
| slots.end_inclusive | optionnel | `slot <=` |
| direction | Ascending / Descending | ordre total entièrement inversé |
| page.limit | u64 > 0 | limite physique PostgreSQL seulement ; `LIMIT + 1` |
| page.cursor | opaque <= 4 KiB | V1 account fixe, authentification de contexte par digest SHA-256 |
Aucune query API par owner/provider/time/status n'existe. Aucun index pour ces dimensions ne sera introduit dans V002.
## 6. Audit kbot3 ciblé
L'archive historique contient notamment `k_sol_obs_account_observations`, `k_sol_core_account_states`, `AccountStateStore` et le pipeline de normalisation `account_state_normalization`. Cette surface mélangeait acquisition, normalisation CORE et processing policy ; elle n'est pas transposable telle quelle.
| Élément historique | Classement | Décision KSP |
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
| observation duplique létat canonique | REDESSINER | observation référence un state complet séparé |
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
Aucun code kbot3 ne sera copié. Les optional Yellowstone actuelles (`write_version`, `transaction_signature`, `is_startup`) n'existaient pas comme contrat durable équivalent dans cette persistence historique et restent donc définies uniquement par `ksp-store-api` actuel.
Points héritage supplémentaires explicitement classés :
- **REPRENDRE** : l'ancien repository utilisait déjà une transaction pour rendre atomiques state CORE + mutation observation + processing ledger ; seul le principe transactionnel est repris, jamais le couplage CORE/ledger.
- **REPRENDRE** : l'unicité `observation_key` et la validation additive des contraintes/indexes sont de bons invariants physiques.
- **REDESSINER** : `write_version`, `transaction_signature` et `is_startup` n'ont pas d'équivalent persistant account fiable dans cette surface historique ; V002 suit exclusivement les optionalités du contrat KSP actuel.
- **REDESSINER** : l'idempotence historique `source_observation_id` + `ON CONFLICT DO UPDATE` est remplacée par clé producer-owned + comparaison exacte + conflit stable.
- **REJETER** : les erreurs historiques interpolent l'erreur SQL dans `ks_core::Error::db(...)`; KSP conserve uniquement kind/phase statiques et sûres.
## 7. Design physique V002 figé par `pre.001`
### 7.1 Tables minimales
```text
ksp_raw_account_states
ksp_raw_account_observations
```
Aucune table archive/tombstone account n'est ajoutée : `ksp-store-api` ne définit aucune capability de retention account.
### 7.2 Colonnes `ksp_raw_account_states`
```text
pubkey BYTEA NOT NULL
slot NUMERIC(20,0) NOT NULL
state_hash BYTEA NOT NULL
lamports NUMERIC(20,0) NOT NULL
owner BYTEA NOT NULL
executable BOOLEAN NOT NULL
rent_epoch NUMERIC(20,0) NOT NULL
data BYTEA NOT NULL
```
Contraintes minimales : fixed width 32 pour `pubkey`, `owner`, `state_hash`; domaine u64 pour `slot`, `lamports`, `rent_epoch`; `octet_length(data) <= 16777216`; aucune borne basse sur data.
### 7.3 Colonnes `ksp_raw_account_observations`
```text
observation_key BYTEA NOT NULL
account_pubkey BYTEA NOT NULL
account_slot NUMERIC(20,0) NOT NULL
account_state_hash BYTEA NOT NULL
provider TEXT NOT NULL
protocol TEXT NOT NULL
acquisition_method TEXT NOT NULL
origin TEXT NOT NULL
received_at_unix_millis BIGINT NOT NULL
capture_session_id TEXT NULL
commitment TEXT NULL
endpoint_id TEXT NULL
filter_id TEXT NULL
observed_at_unix_millis BIGINT NULL
source_payload_hash BYTEA NULL
source_payload_size_bytes BIGINT NULL
is_startup BOOLEAN NULL
transaction_signature BYTEA NULL
write_version NUMERIC(20,0) NULL
```
Les contraintes provenance doivent être sémantiquement identiques à V001 : code 1..128 octets/alphabet sûr, origin fermé, timestamps bornés/ordonnés, source hash 32 bytes et source payload <= 64 MiB.
### 7.4 Indexes justifiés
Indexes provenant des PK :
```text
PK ksp_raw_account_states(pubkey, slot, state_hash)
PK ksp_raw_account_observations(observation_key)
```
Un seul index métier additionnel est justifié par la query non filtrée :
```text
ix_ksp_raw_account_states_slot_pubkey_state_hash(slot, pubkey, state_hash)
```
Le PK couvre déjà efficacement la navigation lorsque `pubkey = ?` est imposé. Aucun index owner/provider/protocol/method/time/hash isolé n'est admis dans cette release.
## 8. Ordre total et pagination keyset
Ordre canonique :
```text
Ascending : (slot ASC, pubkey ASC, state_hash ASC)
Descending : (slot DESC, pubkey DESC, state_hash DESC)
```
Le backend PostgreSQL compare `BYTEA` selon son ordre btree déterministe. Aucun `OFFSET` n'est utilisé.
Cursor account V1 proposé :
```text
bytes 0..4 magic = "KSPA"
byte 4 version = 1
bytes 5..13 last_slot, u64 big-endian
bytes 13..45 last_pubkey, 32 bytes
bytes 45..77 last_state_hash, 32 bytes
bytes 77..109 SHA-256 binding digest
longueur fixe = 109 bytes
```
Domaine de digest :
```text
KSP/raw-account-state-cursor/v1
```
Le digest lie au minimum : network, présence/valeur du filtre pubkey, direction, start/end slot, last slot, last pubkey, last state_hash. Le magic/domain distinct de `KSPT`/`KSP/raw-transaction-cursor/v1` rend le replay cross-family impossible même si les deux cursors ont 109 octets.
Le plafond physique PostgreSQL reste le précédent réel nécessaire à `LIMIT + 1` :
```text
9_223_372_036_854_775_806
```
Ce plafond n'est pas une policy métier KSP.
## 9. Algorithmes d'écriture
### 9.1 Acquisition state + observation atomiques
Préconditions avant I/O métier :
```text
state.reference.network == backend.network
observation.account.network == backend.network
observation.account == state.reference
```
Algorithme :
```text
BEGIN
INSERT state ... ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING ...
if inserted:
entity_outcome = Inserted
else:
SELECT canonical state BY PK FOR UPDATE
decode + compare exact lamports/owner/executable/rent_epoch/data
identical -> AlreadyPresent
divergent -> ERROR_CODE_RAW_CONFLICT
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
if inserted:
observation_outcome = Inserted
else:
SELECT observation BY key FOR UPDATE
decode + compare exact reference + provenance + all optional metadata
identical -> AlreadyPresent
divergent -> ERROR_CODE_RAW_CONFLICT
COMMIT
```
Une divergence d'observation après insertion d'un nouveau state fait rollback de ce state. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*` préalable, aucun overwrite silencieux.
### 9.2 Observation supplémentaire
```text
validate network before I/O
BEGIN
SELECT state BY composite reference FOR KEY SHARE
absent -> ReferenceNotFound
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
collision -> SELECT FOR UPDATE + exact compare
COMMIT
```
Cette opération ne crée jamais implicitement le state et ne retransmet pas ses bytes.
## 10. Matrice de concurrence
| Scénario | Résultat permis | Invariant |
|------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------|
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | attente unique + comparaison exacte ; aucun doublon |
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | aucun overwrite ; résultat gagnant dépend de lordonnancement |
| même pubkey+slot, state_hash différents | les deux states sont représentables | PK inclut state_hash |
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | transaction perdante rollback son state nouvellement créé |
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | state préexistant inchangé |
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | aucun update silencieux |
| annulation après insertion state avant observation | aucune moitié durable | transaction PostgreSQL rollback/drop |
Le backend ne promet pas quel concurrent divergent gagne ; il promet qu'un seul contenu incompatible devient durable et que l'autre reçoit le conflit stable.
## 11. Taxonomie d'erreurs
Aucun nouveau `PostgresBackendErrorKind` n'est requis par le design actuel.
| Cas | Taxonomie stable | Règle |
|---------------------------------------------------|------------------------------------------------------|---------------------------------|
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | guard avant SQL métier |
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | pas de state implicite |
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | message/context statiques |
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | aucun bytes hostile dans erreur |
| page > limite physique PostgreSQL | PageLimitUnsupported | limite physique, pas policy KSP |
| row DB incompatible/malformée | DataInvalid | aucune valeur hostile recopiée |
| échec SELECT | ReadFailed | pas de server text exposé |
| échec INSERT/transaction | WriteFailed | pas de server text exposé |
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | réutiliser taxonomy stable |
Les erreurs PostgreSQL externes ne doivent jamais être conservées comme source publique ni recopiées dans message/context.
## 12. Threat model `RawAccountState`
| Menace | Risque | Mitigation | Preuve prévue |
|---------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|----------------------------|
| URI pointe vers DB dun autre réseau | corruption cross-network | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
| pubkey/hash/signature DB de mauvaise largeur | construction de modèle invalide | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
| slot/lamports/rent_epoch/write_version > i64::MAX | narrowing/perte | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
| data account > 16 MiB | allocation/persistence hostile | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
| data account vide | rejet incorrect | aucune borne basse SQL ; vide explicitement valide | unit |
| observation sans state | orphelin | FK composite + record pré-vérifie state | unit + live |
| échec observation après state insert | demi-acquisition durable | une transaction PostgreSQL commune | live cancellation/rollback |
| même ref, contenu divergent | overwrite silencieux | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
| même pubkey/slot, hashes distincts | collision didentité | PK `(pubkey,slot,state_hash)` | unit + live |
| collision observation key | fusion de sources distinctes | comparaison exacte de tous les champs ; conflict sinon | unit + live |
| cursor transaction injecté dans account | replay cross-family | magic/domain account distincts | unit cursor |
| cursor account rejoué sur autre query | pagination incohérente | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
| ordre même slot ambigu | duplication/skip entre pages | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
| metadata Yellowstone promue au state | couplage provider | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
| transaction_signature impose transaction row | couplage cross-family non garanti | aucune FK vers `ksp_raw_transactions` | schema canary |
| SQL/server text fuit | secret/PII leak | projection PostgresBackendError statique | hardening |
| V000/V001 modifiées | history incompatible | checksums figés et canaries exactes | migration tests |
| index non justifié | write amplification/bloat | un seul index métier V002 pour query unfiltered | schema completeness |
## 13. Compatibilité schema/migration
V002 devra suivre le moteur resource-owned de V001 : ressources ordonnées, checksums logiques, vérification catalogique et mutation selon les policies foundation existantes.
Principes :
```text
schema_creation = disabled : aucune création implicite
schema_creation = create_if_missing : bootstrap seulement selon contrat existant
schema_update = disabled : historique incomplet -> mismatch sûr
schema_update = update_if_needed : V002 appliquée additivement
history > latest embedded : SchemaNewer
history V000/V001 divergente : MigrationMismatch
extra columns externes : acceptables seulement si elles ne peuvent bloquer les INSERT KSP
colonnes/constraints/index requis incompatibles : mismatch avant ready
```
Les numéros de fichiers SQL exacts sont une responsabilité de `pre.002/pre.003`; leur sémantique est figée ici, pas leurs noms séquentiels.
## 14. Mapping/read privé
Le module backend proposé est `raw_account` privé, analogue structurellement à `raw_transaction` mais sans retention/archive.
Il devra contenir :
```text
mapping row -> RawAccountState
mapping row -> RawAccountObservation
mapping u64 <-> NUMERIC text sans narrowing
get state par composite reference
get observation par observation_key
list references par keyset
cursor account privé
write acquisition/observation privé
```
Aucun row/statement/client PostgreSQL ne traverse `ksp-store-postgres-lib` publiquement ni `ksp-store-lib`.
## 15. Stratégie de preuve PostgreSQL réelle
La preuve live restera `#[ignore]`, opt-in, avec une URI dédiée lue depuis stdin et sans environnement direct. Elle devra refuser de détruire des objets KSP préexistants non créés par le test.
Scénarios minimum :
```text
bootstrap V000+V001+V002 et health ready
réouverture idempotente du schéma
state bytes vide et 16 MiB
u64 au-delà de i64::MAX et u64::MAX pour colonnes NUMERIC
acquisition identique puis divergente
same pubkey + same slot + distinct state_hash
observation supplémentaire / reference not found
collision observation
pagination filtered/unfiltered ASC/DESC multi-page
cursor hostile / replay query / replay transaction
concurrence identique/divergente
cancellation entre state et observation -> rollback
coexistence avec au moins un canari RawTransaction stable dans la même DB V002
cleanup vérifié uniquement pour les objets créés par le test
```
Aucun appel Solana réseau n'est nécessaire à cette preuve.
## 16. Conformance cross-family finale
À `pre.008+`, les canaries devront compter exactement :
```text
PostgresBackend : 10 impl RAW
Store : 10 impl RAW
```
La façade doit continuer de n'exposer que les modèles/capabilities backend-agnostic. Le test `--no-default-features` de `ksp-store-lib` doit rester vert : un build sans backend PostgreSQL compile et rejette proprement un backend non compilé au runtime.
La conformance finale doit aussi démontrer que l'ajout account ne modifie pas les six comportements transaction : lecture/write/observation/list/retention/force-rehydrate restent couverts par leurs suites existantes.
## 17. Sizing recalibré — prereleases souples
Chaque tranche technique vise **environ 15 à 20 minutes de travail effectif** lorsque le sujet s'y prête. Cette durée est une cible de granularité et non une durée maximale : compilation, diagnostic, live PostgreSQL ou difficulté réelle peuvent légitimement prolonger une tranche.
Le forecast reste volontairement souple. Une tranche peut être scindée, fusionnée ou réordonnée par delta si la réalité technique l'exige, à condition de préserver les responsabilités de clôture. Le nombre de prereleases prévu n'impose ni une session de conversation par tranche, ni une clôture artificielle sur le numéro initialement prévu.
Chaque prerelease possède un statut directement modifiable. Lorsqu'un correctif d'une tranche est nécessaire, il est ajouté sous la prerelease concernée avec un titre `#### pre.NNN-fix.MMM`, ce qui conserve l'historique de progression et permet d'ajouter des fixes sans transformer le forecast en tableau.
### `pre.001` — Audit, kbot3, threat model et design V002
**Statut : réalisé ; gate opérateur complet PASS.**
Budget cible : **15-20 min**. Audit de la base `v0.3.3`, étude ciblée kbot3, threat model, design physique V002, sizing et création du plan/de la validation. Aucun SQL V002, aucune table account et aucun repository account ne sont avancés dans cette tranche.
#### `pre.001-fix.001` — Forecast souple éditable et hiérarchie des fixes
**Statut : réalisé ; correctif documentaire uniquement.**
Remplacement du sizing tabulaire par des sous-sections éditables avec statut explicite. Les futurs correctifs peuvent être insérés sous leur prerelease avec un titre `####`, sans modifier le découpage technique, le scope fonctionnel ni les responsabilités de clôture de `0.3.4`. Conformément à `VER-ID-008`, ce correctif documentaire ne modifie pas `workspace.package.version`, qui reste `0.3.4-pre.1`.
### `pre.002` — Registry V002 et tables minimales
**Statut : réalisé ; corrigé par `pre.002-fix.001`, gate opérateur complet PASS.**
Budget cible : **15-20 min**. V002 `raw_account_state` est enregistrée avec exactement cinq resources : deux tables, les PK `(pubkey, slot, state_hash)` / `(observation_key)` et la FK composite observation -> state. Aucun repository account ni dispatch façade n'est ajouté.
Le checksum resources calculable sur cet état intermédiaire est `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55`. Il est **provisoire de prerelease** : `pre.003` ajoute encore les contraintes de domaine et l'index prévus, puis fige le checksum V002 final avant toute preuve live account. V000/V001 restent strictement inchangées.
#### `pre.002-fix.001` — correction des canaris V002
**Statut : réalisé ; gate opérateur complet PASS.**
Le gate opérateur de `pre.002` compile le workspace et passe Clippy, Store API, Store façade, Config et `--no-default-features`, mais révèle deux canaris backend obsolètes. Le premier construisait encore un historique « futur » en version 2 alors que V002 occupe désormais cette version ; il est recalibré avec V002 valide suivie d'une version 3 inconnue. Le second comparait la FK V002 à une chaîne SQL sensible aux retours à la ligne ; il normalise désormais uniquement les espaces avant de vérifier le contrat physique.
Aucune ressource SQL, migration, table, contrainte, index, surface runtime ou capability n'est modifiée. Le checksum V002 intermédiaire reste donc exactement `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55`.
### `pre.003` — Contraintes, index et schema compatibility V002
**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur complet PASS.**
Budget cible : **15-20 min**. V002 est complétée à **32 resources** : deux tables, 29 contraintes (PK/FK incluses) et un index non unique `(slot, pubkey, state_hash)` sans prédicat. Les contraintes matérialisent les bornes backend-agnostic existantes : fixed-width 32/64 bytes, domaines `u64` via `NUMERIC(20,0)`, data `<= 16 MiB`, codes de provenance 1..128 octets/alphabet sûr, timestamps bornés, ordre `observed_at <= received_at` et source payload `<= 64 MiB`.
Le checksum V002 final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. La transition depuis le checksum provisoire `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55` est acceptée **uniquement** sous `schema_update=update_if_needed` : le moteur répare/valide d'abord les resources V002 manquantes, vérifie la compatibilité externe puis remplace atomiquement le checksum d'historique dans la même transaction. `schema_update=disabled` conserve un mismatch sûr et tout checksum inconnu reste rejeté.
Le probe d'adoption reconnaît désormais aussi les deux tables V002. Aucun `src/raw_account.rs`, aucune implémentation `RawAccount*` et aucun dispatch Store ne sont ouverts dans cette tranche.
Gate de base `pre.002-fix.001` fourni le 2026-08-30 : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `ksp-store-lib --no-default-features` sont tous PASS.
#### `pre.003-fix.001` — ordre alphabétique du bloc `const` migration
**Statut : réalisé ; gate opérateur complet PASS.**
Le gate opérateur de `pre.003` confirme `cargo check --workspace`, Clippy all-targets, `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib` (45 tests), `ksp-config-lib` et `ksp-store-lib --no-default-features`, mais l'audit Rust détecte une unique violation `RUST-FMT-104` dans `src/migration.rs`. Le correctif déplace seulement `HISTORY_LOAD_SQL` avant `HISTORY_UPDATE_CHECKSUM_SQL` afin de restaurer l'ordre alphabétique du bloc homogène de constantes.
Aucune valeur de constante, requête SQL, ressource V002, checksum, logique de migration, compatibilité de schéma, repository ou capability n'est modifiée. La version Cargo devient `0.3.4-pre.3.fix.1` conformément à `VER-ID-007/010`, car un fichier Rust est modifié.
### `pre.004` — Mapping privé et lectures `get`
**Statut : réalisé ; gate opérateur initial en échec ciblé sur un canari.**
Budget cible : **15-20 min**. Le module privé `src/raw_account.rs` possède désormais les deux SELECT de lecture et les codecs physiques de `RawAccountState` / `RawAccountObservation`. `PostgresBackend` expose uniquement `get_raw_account_state` et `get_raw_account_observation` avec des modèles `ksp-store-api` ; aucun `tokio_postgres::Row`, SQL ou type physique ne traverse le bridge.
Le mapping revalide les invariants même si PostgreSQL devait contenir une ligne hostile : `pubkey`/owner/hash/key en largeur exacte, signature optionnelle à 64 bytes, `slot`/`lamports`/`rent_epoch`/`write_version` via `NUMERIC(20,0)::text -> u64`, timestamps et source payload size bornés, provenance reconstruite via les constructeurs API et ordre `observed_at <= received_at`. Les bytes account sont admis via `RawAccountState::try_new`, donc data vide reste valide et `> 16 MiB` devient `DataInvalid`.
Le `RawAccountStateReference` reçu par `get` est vérifié contre le réseau mono-backend **avant `pool.get()`**. `RawObservationKey` ne porte pas de réseau ; le réseau de la référence account reconstruite provient exclusivement du backend déjà lié par `ksp_store_identity`.
La tranche reste strictement read-only : aucun `INSERT`, `UPDATE`, `DELETE`, `ON CONFLICT`, `FOR UPDATE`, cursor ou `list_raw_account_states`. Les quatre `impl RawAccount* for PostgresBackend` restent interdits jusqu'à `pre.008`; `pre.005` et `pre.006` possèdent encore les écritures.
#### `pre.004-fix.001` — conformité Clippy du mapping `Row`
**Statut : réalisé ; gate opérateur complet PASS.**
Le gate opérateur de `pre.004` confirme `cargo check --workspace`, les suites ciblées `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib` (52 tests), `ksp-config-lib` et `ksp-store-lib --no-default-features`, tandis que `cargo clippy --workspace --all-targets` rejette les 27 extractions `Row::try_get(...).map_err(...)?` du nouveau module. Chaque occurrence produit conjointement `clippy::question-mark-used` et `clippy::implicit-return`, conformément aux interdictions `RUST-ERR-002/003`. Le même gate signale aussi un `crate_root` inutilisé dans le canari `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
Le correctif déplie les extractions physiques en `match` typés avec retour `DataInvalid` explicite et supprime uniquement la variable de test inutilisée. Aucun SELECT, phase d'erreur, type physique, invariant de mapping, signature de bridge, test fonctionnel, migration ou checksum n'est modifié. La version Cargo devient `0.3.4-pre.4.fix.1` conformément à `VER-ID-007/010`, car deux fichiers Rust sont modifiés. Le gate opérateur complet du correctif est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
### `pre.005` — Acquisition atomique et idempotence
**Statut : réalisé ; gate opérateur complet PASS.**
Budget cible : **15-20 min**. L'acquisition account est désormais une transaction PostgreSQL unique : guard réseau et identité exacte avant I/O, `INSERT ... ON CONFLICT DO NOTHING` du state, verrouillage `FOR UPDATE` et comparaison exacte de `lamports/owner/executable/rent_epoch/data` en cas de collision, puis insertion idempotente de l'observation avec comparaison exacte de la référence, de toute la provenance et des metadata optionnelles. Une divergence retourne `Conflict` sans overwrite.
Si le state vient d'être inséré mais que l'observation collide avec un contenu divergent, le retour d'erreur abandonne la transaction et ne laisse donc aucun state orphelin durable. Le bridge étroit `PostgresBackend::persist_raw_account_acquisition` est ouvert sans implémenter `RawAccountStateWrite`; les quatre traits account restent fermés jusqu'à `pre.008`.
À la clôture de `pre.005`, `record_raw_account_observation`, `list_raw_account_states`, le cursor `KSPA`, tout `UPDATE`/`DELETE` account et tout `ON CONFLICT DO UPDATE` restent absents. Le gate opérateur complet du 2026-08-30 est vert : audits Rust/Markdown, workspace check, Clippy all-targets, suites Store API/façade/PostgreSQL/Config et `--no-default-features` passent.
### `pre.006` — Observation supplémentaire et concurrence unitaire
**Statut : réalisé ; gate opérateur complet à rejouer.**
Budget cible : **15-20 min**. `PostgresBackend::record_raw_account_observation` ouvre le bridge étroit d'observation supplémentaire sans implémenter encore `RawAccountObservationWrite`. Le guard `observation.account.network == backend.network` est évalué avant `pool.get()`. La transaction verrouille ensuite la référence `(pubkey, slot, state_hash)` existante avec `SELECT 1 ... FOR KEY SHARE`; l'absence retourne `ReferenceNotFound` et aucun state n'est créé implicitement.
L'insertion réutilise exactement `persist_account_observation_row` de `pre.005` : `ON CONFLICT (observation_key) DO NOTHING`, puis `SELECT ... FOR UPDATE` et comparaison du `RawAccountObservation` complet sur collision. Une observation identique retourne `AlreadyPresent`; une collision divergente retourne `Conflict`, sans overwrite.
Les canaris unitaires figent l'ordre transactionnel `BEGIN -> FOR KEY SHARE -> insert/compare observation -> COMMIT`. Aucun `spawn`, `mem::forget` ou commit anticipé n'est admis dans ce chemin : si la future est annulée avant le commit, le `Transaction` local est droppé et PostgreSQL rollback la mutation non committée. Cette tranche prouve donc la structure de cancellation/race ; la preuve avec vraies tâches concurrentes et `JoinHandle::abort()` reste volontairement au live `pre.009`.
`list_raw_account_states`, le cursor `KSPA`, `OFFSET`, tout `UPDATE`/`DELETE` account, `ON CONFLICT DO UPDATE` et les quatre `impl RawAccount* for PostgresBackend` restent absents.
### `pre.007` — Liste account et cursor keyset V1
**Statut : réalisé ; gate opérateur complet PASS via `pre.007-fix.001`.**
Budget cible : **15-20 min**. `RawAccountStateQuery` est matérialisée dans le backend PostgreSQL avec navigation keyset sur l'ordre total `(slot, pubkey, state_hash)`, entièrement inversé en DESC. Les requêtes sans filtre utilisent l'index V002 `(slot, pubkey, state_hash)` ; les requêtes filtrées par pubkey conservent le même ordre total et sont compatibles avec la PK `(pubkey, slot, state_hash)`. Aucun `OFFSET` ni plafond fonctionnel KSP n'est ajouté.
Le cursor V1 account est fixe à **109 bytes** : magic `KSPA`, version `1`, `last_slot` u64 big-endian, `last_pubkey` 32 bytes, `last_state_hash` 32 bytes et digest SHA-256. Le domaine `KSP/raw-account-state-cursor/v1` lie le réseau, la présence/valeur du filtre pubkey, les bornes de slot, la direction et la dernière clé complète ; les replays cross-query et cross-family `KSPT` sont rejetés. La seule limite de page est la limite physique PostgreSQL nécessaire à `LIMIT + 1`, soit `9_223_372_036_854_775_806`.
`PostgresBackend::list_raw_account_states` expose uniquement `RawAccountStateQuery -> RawPage<RawAccountStateReference>`. Les quatre implémentations `RawAccount*` et le dispatch `Store` restent réservés à `pre.008`.
#### `pre.007-fix.001` — Réconciliation Clippy/canaris pagination
**Statut : réalisé ; gate opérateur complet PASS.**
Fix strictement borné aux tests/canaris : suppression de cinq `clone()` sur `Pubkey: Copy` dans les canaris cursor, et transformation des canaris hérités de `pre.005/pre.006` pour qu'ils continuent d'interdire `UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET` et les quatre implémentations `RawAccount*`, sans interdire `list_raw_account_states` désormais possédée par `pre.007`. Aucun code de pagination, SQL métier ou migration n'est modifié.
### `pre.008` — Implémentations backend, dispatch Store et conformance 10/10
**Statut : réalisé ; gate opérateur complet à rejouer.**
Budget cible : **15-20 min**. Les quatre capabilities account sont implémentées directement sur `PostgresBackend` en adaptant exclusivement les bridges déjà validés en `pre.004` à `pre.007`, puis sur `Store` avec le même mapping d'erreur et les mêmes guards réseau que la verticale transaction. Aucun nouveau SQL, migration, DTO ou type public n'est ajouté.
L'inventaire devient exactement :
```text
6 RawTransaction* + 4 RawAccount* = 10/10
PostgresBackend = 10 capabilities RAW
Store = 10 capabilities RAW
```
`RawAccountStateRead` délègue `get` et `list`; `RawAccountStateWrite` délègue l'acquisition atomique state+observation; `RawAccountObservationRead` délègue la lecture par observation key; `RawAccountObservationWrite` délègue l'observation supplémentaire avec référence state existante. La façade vérifie le réseau sur reference/query/state/observation avant dispatch lorsque l'input porte un réseau ; `RawAccountObservationRead` reste sans guard d'input réseau puisque sa clé n'en contient pas.
Les canaris historiques sont transformés pour vérifier l'emplacement des implémentations dans `runtime.rs`, l'absence d'implémentation directe dans `raw_account.rs`, l'inventaire exact 10/10 et l'absence de types/SQL PostgreSQL dans `ksp-store-lib`. `--no-default-features` doit conserver les dix traits sur `Store` tout en retournant `backend_not_compiled` au runtime faute de backend compilé.
#### `pre.008-fix.001` — Réconciliation du canari `dependency_boundary`
**Statut : réalisé ; gate opérateur complet PASS.**
Fix strictement borné à `tests/dependency_boundary.rs` : la déclaration `runtime = include_str!("../src/runtime.rs")` est restaurée dans le canari d'ownership `pre.005` qui l'utilise encore pour interdire l'environnement et le SQL physique dans `runtime.rs`, puis retirée du canari V002 `pre.003``pre.008` ne l'utilise plus. Aucune assertion de fond, capability, façade, runtime métier, SQL ou migration n'est modifiée.
### `pre.009` — Preuve PostgreSQL live account
**Statut : réalisé ; gate opérateur standard PASS et live account PASS sur PostgreSQL 17.**
Budget cible : **15-20 min**. La preuve réelle est matérialisée dans `tests/postgres_raw_account_live.rs`, ignored/opt-in et alimentée exclusivement par une URI dédiée lue depuis stdin sans écho. Elle refuse tout schéma KSP déjà présent, exige PostgreSQL >= 15, ouvre/ferme/re-ouvre le backend V002, vérifie la réparation de l'index account selon `schema_autoupdate`, puis nettoie uniquement le schéma qu'elle a prouvé absent avant démarrage.
La matrice live couvre : domaine `u64::MAX`, data vide et borne exacte 16 MiB, acquisition atomique, idempotence exacte, conflit de contenu sous même référence, deux `state_hash` distincts au même `(pubkey, slot)`, collision de clé observation avec rollback du state nouvellement inséré, observation supplémentaire et `ReferenceNotFound`, round-trip des metadata Yellowstone, pagination ASC/DESC filtrée/non filtrée, cursor `KSPA` hostile/cross-query et rejet cross-family `KSPT`, concurrence identique/divergente, cancellation réelle via `JoinHandle::abort()` pendant un conflit d'observation verrouillé, puis reopen.
La coexistence cross-family est prouvée dans le même schéma : une acquisition account et une acquisition `RawTransaction` peuvent utiliser la même `RawObservationKey` dans leurs tables familiales distinctes, restent lisibles indépendamment et survivent au reopen. Le live `RawTransaction` existant est également réconcilié pour considérer les deux tables V002 account dans son probe et son cleanup d'isolation ; aucune logique métier RawTransaction n'est modifiée.
### `pre.010` — Hardening et complétude cross-family
**Statut : réalisé ; gate opérateur complet PASS.**
Budget cible : **15-20 min**. La tranche n'ajoute aucun runtime, SQL, migration, capability ou type public. Elle ferme les canaris de sécurité, ownership et complétude cross-family autour de la surface effectivement validée.
Les canaris backend figent désormais explicitement : surface SQL account non destructive et keyset-only ; cursor `KSPA` distinct du cursor transaction `KSPT` ; V002 exactement 32 resources (2 tables, 2 PK, 1 FK state-local, 26 CHECK, 1 index non filtré) ; absence totale de table/FK/registry transaction dans V002 ; `transaction_signature` account reste une metadata fixed-width sans FK vers `ksp_raw_transactions`.
Les canaris façade comparent directement les noms des dix traits RAW implémentés par `Store` et `PostgresBackend`, imposent l'égalité exacte des deux ensembles et interdisent toute apparition implicite de retention/delete/compaction account. Les inventaires publics, dépendances, guards réseau, erreurs sûres, absence d'environnement/SQL physique dans la façade et placement des implémentations backend restent couverts par les canaris hérités.
`ksp-store-api`, tous les fichiers `src/`, les tests live et les migrations V000/V001/V002 restent byte-inchangés par cette tranche.
### `pre.011` — Gate technique final
**Statut : réalisé ; gate technique final PASS.**
Budget cible : **15-20 min**. La tranche n'ajoute aucun code, test fonctionnel, SQL, migration, capability, dépendance runtime, README/USAGE, CHANGELOG/ROADMAP ou prompt suivant. Elle synchronise uniquement la version de prerelease, le plan, la validation et son delta afin de rejouer le gate technique final sur la surface candidate déjà durcie.
Le replay de clôture confirme `cargo test -p ksp-store-lib --no-default-features`, `cargo test --workspace`, les graphes Cargo façade/backend/features/duplicates et `postgres_raw_account_live` avec URI dédiée via stdin. Le live account passe sur PostgreSQL 17 et traverse bootstrap/reopen V002, concurrence/cancellation et coexistence réelle avec `RawTransaction`; les lives foundation et RawTransaction séparés ne sont pas requis par cette tranche.
### `pre.012` — Réconciliation documentaire finale
**Statut : matérialisé ; gate documentaire opérateur à exécuter.**
Budget cible : **10-15 min**. Réconcilier les documents durables requis avec la surface effectivement validée, sans rouvrir le code fonctionnel ni préparer la publication. Les README/USAGE de `ksp-store-lib` et `ksp-store-postgres-lib` décrivent désormais la surface RAW 10/10 et les opérations `RawAccountState` ; le plan et la validation enregistrent le PASS technique de `pre.011`. `CHANGELOG.md`, `ROADMAP.md` et le prompt `0.3.5` restent réservés à `pre.013`.
### `pre.013` — Préparation de publication minimale
**Statut : planifié.**
Budget cible : **10-15 min**. Dernière prerelease : prompt `0.3.5`, `CHANGELOG.md` et `ROADMAP.md` uniquement hors mécanique Cargo/delta, conformément au couloir de publication.
### `rel.001` — Stabilisation et tag `v0.3.4`
**Statut : planifié.**
Budget cible : **5-10 min**. Stabilisation/tag après gate opérateur final, sans absorption de nouveau scope fonctionnel ou documentaire.
## 18. Hors scope explicite
```text
modification ksp-store-api
retention/archivage/purge account
STRUCTURAL/CORE materialization
Interface events
workers/jobs/backfill executors
batch-size/priorité/backlog policy
Transport -> Store direct
index owner/provider/time/status sans query
compression account
nouveau backend autre que PostgreSQL
application Store Desk
```
## 19. Questions ouvertes après `pre.001`
Aucune question **bloquante** de contrat ne subsiste.
Les détails suivants restent volontairement d'implémentation : numérotation exacte des ressources V002, découpage précis des helpers de mapping et noms internes des statements. Ils ne doivent pas modifier les invariants de ce plan.
## 20. Gate d'acceptation `pre.001`
`pre.001` est acceptable si :
```text
base v0.3.3 vérifiée
V000/V001 checksums recalculés et figés
inventaire 6/10 -> 10/10 explicite
field mapping account complet
kbot3 classifié REPRENDRE/REDESSINER/REPORTER/REJETER
V002 figée sans SQL écrit
ordre/cursor figés
algorithmes atomicité/idempotence/concurrence figés
threat model complet
forecast recalibré
aucune modification fonctionnelle hors version prerelease
audits statiques applicables verts ; gate Cargo opérateur rejoué
```

View File

@@ -0,0 +1,766 @@
<!-- file: docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md -->
<!-- version: 17 -->
# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW
## 1. Portée
Cette matrice est ouverte par `0.3.4-pre.001`. `0.3.4-pre.002` matérialise la fondation V002 minimale et `pre.002-fix.001` corrige deux canaris sans modifier le SQL ; son gate opérateur complet est PASS. `0.3.4-pre.003` complète les contraintes de domaine, l'index de navigation, la compatibilité externe et le checksum V002 final ; `pre.003-fix.001` corrige uniquement l'ordre alphabétique du bloc `const` et son gate opérateur complet est PASS. `pre.004` à `pre.007` matérialisent successivement mapping/read, writes, observation supplémentaire et pagination account ; `pre.007-fix.001` clôt son gate opérateur en PASS. `pre.008` ouvre les quatre capabilities account dans `PostgresBackend` et `Store`, portant l'inventaire RAW statique à 10/10 ; `pre.008-fix.001` corrige son unique canari mal réconcilié et son gate opérateur complet est PASS. `pre.009` matérialise la preuve PostgreSQL live account, la concurrence/cancellation réelle et la coexistence RawTransaction sans modifier les migrations ; son gate standard et son live account sur PostgreSQL 17 sont PASS. `pre.010` ferme les canaris cross-family sans modifier le runtime et son gate opérateur complet est PASS. `pre.011` ouvre le gate technique final, le replay live ciblé et les graphes Cargo sans nouveau développement.
Base :
```text
v0.3.3
```
Cible release :
```text
PostgresBackend = 10/10 capabilities RAW
Store = 10/10 capabilities RAW
```
## 2. Gate base `pre.001`
| Preuve | Attendu | État pre.001 |
|------------------------------------|------------------|-------------------------------------------------|
| workspace.package.version base | 0.3.3 | PASS audit |
| V000 checksum | d29068b8…46a1450 | PASS recalcul indépendant |
| V001 checksum | 31488cda…d72cc51 | PASS recalcul indépendant / 40 resources |
| capabilities transaction physiques | 6 | PASS inventaire source |
| capabilities account physiques | 0 | PASS inventaire source ; volontairement pending |
| canaries anti-account 0.3.3 | présentes | PASS inventaire tests |
| ksp-store-api change nécessaire | non | PASS audit |
| SQL V002 créé en pre.001 | non | PASS scope |
## 3. Matrice capability finale
| Capability | Base v0.3.3 | Cible 0.3.4 | Preuve finale |
|--------------------------------|-------------|-------------|-------------------------------|
| RawTransactionRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionWrite | PASS | PASS | régression suite 0.3.3 |
| RawTransactionObservationRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionObservationWrite | PASS | PASS | régression suite 0.3.3 |
| RawTransactionRetentionRead | PASS | PASS | régression suite 0.3.3 |
| RawTransactionRetentionWrite | PASS | PASS | régression suite 0.3.3 |
| RawAccountStateRead | PENDING | PASS | tests backend + façade + live |
| RawAccountStateWrite | PENDING | PASS | tests backend + façade + live |
| RawAccountObservationRead | PENDING | PASS | tests backend + façade + live |
| RawAccountObservationWrite | PENDING | PASS | tests backend + façade + live |
### 3.1 Contrat opérationnel détaillé des quatre capabilities account
| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live |
|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------|
| RawAccountStateRead | reference / query | Option<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
## 4. Matrice `RawAccountState`
| Champ | Représentation décidée | Validation requise |
|------------|------------------------------------------|-----------------------------------------------------------------|
| network | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
| pubkey | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
| slot | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
| state_hash | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
| lamports | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| owner | BYTEA NOT NULL | `octet_length = 32` |
| executable | BOOLEAN NOT NULL | mapping direct |
| rent_epoch | NUMERIC(20,0) NOT NULL | domaine u64 exact |
| data | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
Points structurants :
```text
identity = network API + (pubkey, slot, state_hash)
network SQL = singleton ksp_store_identity, pas colonne row
PK state = (pubkey, slot, state_hash)
state_hash n'est pas une preuve suffisante d'idempotence : comparaison contenu exacte obligatoire
empty data = valide
```
## 5. Matrice observation
| Champ | Représentation décidée | Validation requise |
|---------------------------|------------------------|--------------------------------------------------|
| observation_key | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
| account.pubkey | BYTEA NOT NULL | FK composite vers state |
| account.slot | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
| account.state_hash | BYTEA NOT NULL | FK composite |
| provider | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
| protocol | TEXT NOT NULL | idem V001 |
| acquisition_method | TEXT NOT NULL | idem V001 |
| origin | TEXT NOT NULL | backfill/import/live/repair/replay |
| received_at | BIGINT NOT NULL | 0..253402300799999 |
| capture_session_id | TEXT NULL | mêmes checks V001 |
| commitment | TEXT NULL | mêmes checks V001 |
| endpoint_id | TEXT NULL | mêmes checks V001 |
| filter_id | TEXT NULL | mêmes checks V001 |
| observed_at | BIGINT NULL | borne + `observed_at <= received_at` |
| source_payload_hash | BYTEA NULL | NULL ou 32 bytes |
| source_payload_size_bytes | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
| is_startup | BOOLEAN NULL | Yellowstone metadata observation-only |
| transaction_signature | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
| write_version | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
Invariants additionnels :
```text
FK observation -> state composite
record_raw_account_observation n'insère jamais le state
transaction_signature observation-only, sans FK transaction
write_version/is_startup observation-only
observation_key family-local unique
```
## 6. Query/pagination
| Élément | Décision | Canari requis |
|-----------------|------------------------------------------------|--------------------------------|
| ordre ASC | slot, pubkey, state_hash ASC | multi-page sans duplicate/skip |
| ordre DESC | slot, pubkey, state_hash DESC | symétrique ASC |
| filtre pubkey | égalité optionnelle | cursor lié au filtre |
| slot range | inclusive | cursor lié aux deux bornes |
| cursor magic | KSPA | rejette KSPT transaction |
| cursor version | 1 | rejette version hostile |
| cursor longueur | 109 bytes | rejette taille hostile |
| cursor domain | KSP/raw-account-state-cursor/v1 | digest family-bound |
| cursor digest | network+pubkey filter+range+direction+last key | anti-replay query |
| pagination | keyset seulement | aucun OFFSET |
| limit | limite physique PostgreSQL uniquement | pas de batch policy |
## 7. Migration/schema V002
| Objet | Décision / état physique | État |
|----------------------------------|--------------------------------------------------------------------------|---------------------|
| V000 | bytes/checksum inchangés | PASS |
| V001 | 40 resources + checksum inchangés | PASS |
| V002 logical name | `raw_account_state` | PASS pre.002 |
| table state | `ksp_raw_account_states` | PASS pre.002 |
| table observation | `ksp_raw_account_observations` | PASS pre.002 |
| PK state | `(pubkey,slot,state_hash)` | PASS pre.002 |
| PK observation | `(observation_key)` | PASS pre.002 |
| FK observation -> state | `(account_pubkey,account_slot,account_state_hash)` -> state composite PK | PASS pre.002 |
| contraintes domaine/fixed-width | 26 CHECK + PK/FK, bornes API exactes | PASS pre.003 |
| index métier | `(slot,pubkey,state_hash)` non unique / non filtré | PASS pre.003 |
| index owner/provider/time/status | aucun prévu | PASS negative scope |
| archive/tombstone account | aucune | PASS negative scope |
| V002 checksum intermédiaire | `30ac8749…26311f55` sur les 5 resources pre.002 | PROVISIONAL |
| V002 checksum final | `ff21605e…bdb550e` sur 32 resources | PASS pre.003 |
## 8. Idempotence et concurrence
| Scénario | Résultat attendu | État |
|------------------------------------------------------------|-------------------------------------------------------|-------------------|
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | PENDING impl/live |
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | PENDING impl/live |
| même pubkey+slot, state_hash différents | les deux states sont représentables | PENDING impl/live |
| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | PENDING impl/live |
| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | PENDING impl/live |
| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | PENDING impl/live |
| annulation après insertion state avant observation | aucune moitié durable | PENDING impl/live |
Le test de contenu divergent doit comparer les fields complets et ne jamais traiter l'égalité de `state_hash` comme preuve suffisante.
## 9. Threat model
| Menace | Mitigation figée | Preuve |
|---------------------------------------------------|-----------------------------------------------------------------------|----------------------------|
| URI pointe vers DB dun autre réseau | binding `ksp_store_identity` + guard réseau avant opération | unit + live |
| pubkey/hash/signature DB de mauvaise largeur | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows |
| slot/lamports/rent_epoch/write_version > i64::MAX | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 |
| data account > 16 MiB | admission API + CHECK `octet_length <= 16777216` | unit + live boundary |
| data account vide | aucune borne basse SQL ; vide explicitement valide | unit |
| observation sans state | FK composite + record pré-vérifie state | unit + live |
| échec observation après state insert | une transaction PostgreSQL commune | live cancellation/rollback |
| même ref, contenu divergent | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent |
| même pubkey/slot, hashes distincts | PK `(pubkey,slot,state_hash)` | unit + live |
| collision observation key | comparaison exacte de tous les champs ; conflict sinon | unit + live |
| cursor transaction injecté dans account | magic/domain account distincts | unit cursor |
| cursor account rejoué sur autre query | digest lie network/pubkey/range/direction/last key | unit cursor matrix |
| ordre même slot ambigu | ordre total `(slot,pubkey,state_hash)` | unit + live pagination |
| metadata Yellowstone promue au state | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary |
| transaction_signature impose transaction row | aucune FK vers `ksp_raw_transactions` | schema canary |
| SQL/server text fuit | projection PostgresBackendError statique | hardening |
| V000/V001 modifiées | checksums figés et canaries exactes | migration tests |
| index non justifié | un seul index métier V002 pour query unfiltered | schema completeness |
## 10. Audit historique kbot3
| Élément | Classement | Disposition |
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
| observation duplique létat canonique | REDESSINER | observation référence un state complet séparé |
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
Le pipeline historique `k_sol_obs_account_observations -> k_sol_core_account_states` reste une source de comparaison, pas une architecture cible. La normalisation versionnée et ses caps de batch sont hors scope `0.3.4`.
## 11. Canaries `0.3.3` à faire évoluer
À l'ouverture effective de la surface account :
```text
ksp-store-lib/tests/dependency_boundary.rs
remplacer interdiction RawAccount par exigence des 4 impl + absence types SQL
ksp-store-lib/tests/hardening_completeness.rs
remplacer inventaire 6 + RawAccount fermé par inventaire exact 10
ksp-store-postgres-lib/tests/dependency_boundary.rs
ajouter module raw_account privé et exiger les 4 impl dans runtime bridge
conserver V000 et health business-free
ksp-store-postgres-lib/tests/hardening_completeness.rs
remplacer interdiction RawAccount globale par inventaire exact 10
remplacer l'absence V002/account par checks de surface V002 minimale
```
Les tests historiques nommés `pre_XXX` peuvent conserver leurs noms si le contrat qu'ils vérifient reste valide ; seul leur contenu doit être élargi lorsqu'il fige intentionnellement le scope `0.3.3`.
## 12. Taxonomie d'erreur à prouver
| Cas | Code/projection attendue | État |
|---------------------------------------------------|------------------------------------------------------|--------------|
| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | PENDING impl |
| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | PENDING impl |
| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | PENDING impl |
| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | PENDING impl |
| page > limite physique PostgreSQL | PageLimitUnsupported | PENDING impl |
| row DB incompatible/malformée | DataInvalid | PENDING impl |
| échec SELECT | ReadFailed | PENDING impl |
| échec INSERT/transaction | WriteFailed | PENDING impl |
| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | PENDING impl |
Aucune nouvelle variante backend n'est justifiée au gate `pre.001`.
## 13. Preuve live finale
Le test live account devra être opt-in/ignored, URI stdin, sans environnement ni fuite de secret. Il doit couvrir V002 et rejouer un canari transaction dans la même base afin de prouver la coexistence cross-family.
| Scénario live | Obligation |
|-----------------------------------------|-------------|
| schema update V001 -> V002 | obligatoire |
| reopen idempotent | obligatoire |
| u64 > i64::MAX / u64::MAX | obligatoire |
| data empty / 16 MiB | obligatoire |
| identical/divergent acquisition | obligatoire |
| same pubkey+slot / distinct hash | obligatoire |
| additional observation + missing ref | obligatoire |
| collision observation | obligatoire |
| keyset filtered/unfiltered ASC/DESC | obligatoire |
| cursor hostile/cross-query/cross-family | obligatoire |
| concurrency | obligatoire |
| cancellation rollback | obligatoire |
| RawTransaction regression canary | obligatoire |
| cleanup isolation | obligatoire |
## 14. Sizing
| Tranche | Objet | État |
|---------|-------------------------------------------------------------------|---------|
| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | DONE |
| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PASS |
| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PASS |
| pre.004 | mapping privé state/observation + get reads + hostile rows | PASS |
| pre.005 | acquisition atomique state+observation + idempotence/conflict | PASS |
| pre.006 | observation supplémentaire + races/cancellation unitaires | PASS |
| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PASS |
| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PASS |
| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | CURRENT |
| pre.010 | hardening/completeness cross-family + canaries ownership | PLANNED |
| pre.011 | gate technique final + replay live ciblé + graphes | PLANNED |
| pre.012 | réconciliation docs finale sans rouvrir le code | PLANNED |
| pre.013 | publication prep minimale + prompt 0.3.5 | PLANNED |
| rel.001 | stabilisation/tag après gate opérateur | PLANNED |
## 15. Negative scope final
| Surface | Doit rester absent en 0.3.4 |
|--------------------------------------------|-----------------------------|
| ksp-store-api redesign | oui |
| account retention/archive/purge | oui |
| CORE/STRUCTURAL persistence | oui |
| workers/jobs/backfills | oui |
| Store batch/priorité policy | oui |
| owner/provider/time/status queries/indexes | oui |
| Transport -> Store coupling | oui |
| PostgreSQL types via ksp-store-lib | oui |
| nouveau backend | oui |
## 16. Gate opérateur prévu pour clôture technique
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
cargo test --workspace
cargo tree -p ksp-store-lib --edges normal
cargo tree -p ksp-store-lib -e features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree --duplicates
```
Les preuves live restent séparées et explicitement `--ignored --nocapture`.
## 17. Verdict `pre.002`
Fondation V002 minimale : **PASS statique**. Le registry logique V002 expose exactement cinq resources et aucune surface repository/runtime account.
Gate opérateur du 2026-08-30 : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, `ksp-store-api`, `ksp-store-lib`, `ksp-config-lib` et `ksp-store-lib --no-default-features` passent. `cargo test -p ksp-store-postgres-lib` exécute 43 tests unitaires et échoue uniquement sur deux canaris obsolètes :
- `pre_003_fix_001_newer_history_is_rejected_without_down_migration` attendait `SchemaNewer` avec une fausse version future `2`, désormais occupée par V002 ;
- `pre_002_v002_resources_are_exactly_two_tables_plus_base_pk_fk_without_indexes_or_domain_checks` comparait la FK à une chaîne sensible au saut de ligne avant `REFERENCES`.
Ces deux défauts appartiennent strictement au couloir `pre.002` et sont corrigés par `pre.002-fix.001`. Ils ne remettent en cause ni les cinq resources V002 ni leur checksum.
Contraintes complètes/index/schema compatibility/checksum final : **PASS `pre.003`**. Mapping/lectures account : **PENDING `pre.004`** à ce verdict historique ; writes/pagination/capabilities restent `pre.005+`.
## 18. Correctif `pre.002-fix.001`
Le correctif applique deux recalibrages de tests seulement :
1. le canari d'historique construit désormais le préfixe valide V000/V001/V002 puis ajoute une version `3` inconnue, ce qui teste réellement le cas « schéma plus récent » ;
2. le canari de fondation V002 normalise les espaces ASCII du SQL embedded avant ses assertions sémantiques, sans relâcher l'inventaire exact des cinq resources ni les interdictions `CREATE INDEX`, `CHECK`, cascade, `BIGSERIAL`, `BIGINT` ou `TEXT` pour les données account.
Invariants du correctif :
- aucune ressource sous `migrations/v002_raw_account_state/` modifiée ;
- checksum V002 intermédiaire inchangé : `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55` ;
- V000/V001 inchangées ;
- aucun module repository account, dispatch Store ou implémentation `RawAccount*` ouvert ;
- `workspace.package.version = 0.3.4-pre.2.fix.1` conformément à `VER-ID-007/010`, car des sources Rust de tests sont modifiées.
Gate opérateur du correctif du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts.
## 19. Verdict `pre.003`
V002 finale : **PASS fonctionnel opérateur ; fix de conformité Rust requis**.
Inventaire final :
```text
V002 resources = 32
tables = 2
constraints = 29 (2 PK + 1 FK + 26 CHECK)
indexes = 1
final checksum = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
provisional pre.002= 30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55
```
Contraintes matérialisées : exactitude fixed-width des identités/hash/signature, domaines `u64` en `NUMERIC(20,0)`, data account `<= 16 MiB`, provenance logique 1..128 octets/alphabet sûr, origin fermé aux cinq variantes API, timestamps `0..MAX_RAW_UNIX_MILLIS`, `observed_at <= received_at`, hash source 32 bytes et source size `<= 64 MiB`. `is_startup` reste un booléen nullable sans contrainte artificielle.
L'unique index métier V002 est non unique et non filtré :
```text
(slot, pubkey, state_hash)
```
Aucun index owner/provider/method/time/status n'est ajouté.
Compatibilité de prerelease : le checksum provisoire V002 de `pre.002` n'est accepté que lorsque `schema_update=update_if_needed`. Dans ce cas, les resources manquantes sont matérialisées et vérifiées, la compatibilité externe V002 est contrôlée, puis l'entrée d'historique est mise à jour atomiquement vers le checksum final. Avec `schema_update=disabled`, le checksum provisoire est rejeté ; tout checksum non explicitement connu reste `MigrationMismatch`.
Le probe d'adoption couvre V001 + V002. V000/V001 et leurs checksums restent inchangés. Aucune surface repository/runtime account n'est ouverte : `src/raw_account.rs` absent, aucune implémentation `RawAccount*` et aucun dispatch Store.
Gate opérateur de `pre.003` du 2026-08-30 : `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (45 tests), Config et `--no-default-features` sont **PASS**. L'audit Markdown est **PASS**. L'unique défaut est `RUST-FMT-104` dans `src/migration.rs` : le bloc `const` n'est pas intégralement trié alphabétiquement.
## 20. Correctif `pre.003-fix.001`
Le correctif est strictement hygiénique : `HISTORY_LOAD_SQL` est déplacée avant `HISTORY_UPDATE_CHECKSUM_SQL`. Les valeurs, types et usages des constantes restent identiques.
Invariants du fix :
- aucune ressource V002 modifiée ;
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
- checksum provisoire `pre.002` et bridge de migration inchangés ;
- aucune modification de schéma, repository, dispatch ou capability ;
- `workspace.package.version = 0.3.4-pre.3.fix.1` conformément à `VER-ID-007/010`.
Gate opérateur de `pre.003-fix.001` du 2026-08-30 : **PASS complet** — audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (45 tests), Config et `--no-default-features` sont verts.
## 21. Verdict `pre.004`
Mapping PostgreSQL privé account : **PASS final via `pre.004-fix.001`**. Le gate initial `pre.004` avait échoué uniquement sur Clippy ; le gate complet du correctif est désormais vert.
Surface matérialisée :
```text
src/raw_account.rs privé
PostgresBackend::get_raw_account_state public bridge backend
PostgresBackend::get_raw_account_observation public bridge backend
RawAccount* trait impl 0/4 — volontairement fermé
write SQL account absent
pagination/cursor account absent
migrations V000/V001/V002 inchangées
```
Le mapping state couvre `slot`, `lamports` et `rent_epoch` sur tout le domaine `u64` via les projections `::text`, les largeurs exactes 32 bytes de pubkey/owner/state hash, le booléen `executable` et les bytes account complets. `RawAccountState::try_new` fournit la dernière admission backend-agnostic et transforme un payload > 16 MiB en `DataInvalid`; les comptes à data vide restent valides.
Le mapping observation reconstruit l'identité account, toute la provenance commune et les metadata optionnelles `is_startup`, `transaction_signature` et `write_version`. `write_version` conserve tout le domaine `u64` via `NUMERIC(20,0)::text`; une signature présente doit faire exactement 64 bytes. L'absence de metadata optionnelle reste `None`, sans valeur inventée.
Hostile-row guards déterministes couvrent notamment : largeur pubkey/hash/signature invalide, décimal négatif ou > `u64::MAX`, provenance/origin invalide, ordre temporel impossible et data oversized. Les erreurs restent `DataInvalid`/`ReadFailed`/`WrongNetwork` avec phases statiques et sans écho de valeur hostile.
Le garde réseau de `get_raw_account_state` est exécuté avant acquisition d'une connexion. `get_raw_account_observation` reçoit seulement un `RawObservationKey`; la référence reconstruite utilise donc le réseau mono-backend déjà validé à l'ouverture, comme prévu par le contrat.
Les canaris de scope prouvent encore l'absence de :
```text
INSERT / UPDATE / DELETE account
ON CONFLICT / FOR UPDATE account
list_raw_account_states / cursor KSPA
impl RawAccountStateRead/Write for PostgresBackend
impl RawAccountObservationRead/Write for PostgresBackend
Store dispatch account
```
Le gate opérateur de `pre.004` du 2026-08-30 établit : `cargo check --workspace` PASS, suites `ksp-store-api`/`ksp-store-lib`/`ksp-store-postgres-lib`/`ksp-config-lib` PASS, `ksp-store-lib --no-default-features` PASS et audits Rust/Markdown PASS ; `cargo clippy --workspace --all-targets` FAIL uniquement sur les 27 usages de `?` du nouveau mapping, doublés par 27 retours implicites de closures, avec un warning séparé de variable `crate_root` inutilisée dans `dependency_boundary.rs`.
### 21.1. Correctif `pre.004-fix.001`
Le correctif remplace exclusivement les `map_err(...)?` de `raw_account_observation_db_row` et `raw_account_state_db_row` par des `match` typés avec retours explicites `DataInvalid`, conformément à `RUST-ERR-002/003`. Il retire également la déclaration `crate_root` inutilisée du canari `pre_003_v002_schema_is_complete_without_account_trait_or_write_dispatch`.
Invariants du fix :
- aucun SQL ou bind account modifié ;
- aucun invariant de mapping ou phase d'erreur modifié ;
- aucune migration V000/V001/V002 modifiée ;
- checksum V002 final inchangé : `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e` ;
- aucun write, cursor, trait account ou dispatch Store ouvert ;
- `workspace.package.version = 0.3.4-pre.4.fix.1`.
Validations exécutées dans l'environnement d'assemblage du fix : audit Rust général/export/workspace **PASS**, audit Markdown **PASS**, absence de `?` dans `src/raw_account.rs` confirmée. Gate opérateur du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (52 tests), Config et `--no-default-features` sont verts.
## 22. Verdict `pre.005`
Acquisition atomique account : **PASS complet opérateur**.
Surface ajoutée :
```text
PostgresBackend::persist_raw_account_acquisition
INSERT state ON CONFLICT DO NOTHING
SELECT state FOR UPDATE sur collision
comparaison exacte du state canonique complet
INSERT observation ON CONFLICT DO NOTHING
SELECT observation FOR UPDATE sur collision
comparaison exacte référence + provenance + metadata optionnelles
COMMIT unique state + observation
```
Les préconditions `state.reference.network == backend.network`, `observation.account.network == backend.network` et `observation.account == state.reference` sont évaluées avant `pool.get()`. Une divergence de référence ou de contenu est classée `Conflict`; un échec physique d'écriture reste `WriteFailed`, sans texte PostgreSQL conservé.
L'idempotence state ne fait aucune confiance exclusive à `state_hash` : un même `(pubkey, slot, state_hash)` n'est `AlreadyPresent` que si `lamports`, `owner`, `executable`, `rent_epoch` et `data` sont identiques. L'idempotence observation compare le modèle `RawAccountObservation` complet, y compris `is_startup`, `transaction_signature`, `write_version` et toute la provenance.
La transaction commune garantit qu'une collision divergente sur l'observation après insertion d'un nouveau state ne peut pas laisser ce state durable isolément. Aucun `has_*`, `ON CONFLICT DO UPDATE`, `UPDATE` ou `DELETE` account n'est introduit.
Canaries `pre.005` : guard réseau/référence, comparaison state complète, comparaison observation complète, encodage origin fermé, SQL `DO NOTHING + FOR UPDATE`, bridge backend-agnostic et absence des quatre implémentations `RawAccount*`.
Le gate opérateur du 2026-08-30 est **PASS complet** : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, Store API, Store façade, backend PostgreSQL (56 tests unitaires), Config et `ksp-store-lib --no-default-features` sont verts.
Reste après `pre.005` : observation supplémentaire/concurrence ciblée (`pre.006`), pagination/cursor (`pre.007`) et implémentations/dispatch 10/10 (`pre.008`). Les migrations V000/V001/V002 restent inchangées et le checksum V002 final demeure `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
## 23. Verdict `pre.006`
Observation supplémentaire account : **PASS complet opérateur**.
Surface ajoutée :
```text
PostgresBackend::record_raw_account_observation
network guard avant pool.get()
SELECT state reference FOR KEY SHARE
state absent -> ReferenceNotFound
INSERT observation ON CONFLICT DO NOTHING
collision -> SELECT observation FOR UPDATE + comparaison exacte
COMMIT unique
```
Le state référencé doit être déjà durable : `record_raw_account_observation` ne crée jamais de `RawAccountState` et ne retransmet pas ses bytes. Le lock `FOR KEY SHARE` protège la référence pendant l'insertion observation/FK sans prendre un lock de contenu plus fort que nécessaire.
L'idempotence et le conflit réutilisent le chemin `persist_account_observation_row` déjà validé en `pre.005`. Une même clé avec observation identique produit `AlreadyPresent`; une même clé avec contenu/provenance/metadata divergents produit `Conflict`. Le réseau de l'observation est rejeté avant toute acquisition de connexion lorsqu'il diffère du backend.
La preuve de race/cancellation de cette tranche est volontairement unitaire/structurelle : les canaris imposent l'ordre `transaction -> FOR KEY SHARE -> persist/compare -> commit`, l'absence de commit anticipé et l'absence de tâche détachée ou de fuite volontaire du handle transactionnel. Une cancellation avant commit droppe donc le `Transaction` local et laisse le rollback à PostgreSQL. Les scénarios de vraies tâches concurrentes et `JoinHandle::abort()` restent au test live `pre.009`.
Restent fermés : `list_raw_account_states`, cursor `KSPA`, `OFFSET`, `UPDATE`/`DELETE`, `ON CONFLICT DO UPDATE`, les quatre implémentations `RawAccount*` sur `PostgresBackend` et tout dispatch account dans `Store`. Les migrations V000/V001/V002 sont inchangées ; le checksum V002 final reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
## 24. Verdict `pre.007`
Pagination `RawAccountStateQuery` et cursor `KSPA` : **PASS complet opérateur via `pre.007-fix.001`**. Le gate initial avait isolé cinq warnings Clippy `clone_on_copy` et deux canaris historiques stales ; le fix les corrige sans toucher au code de pagination.
Ordre physique figé :
```text
ASC (slot ASC, pubkey ASC, state_hash ASC)
DESC (slot DESC, pubkey DESC, state_hash DESC)
```
Deux familles de SELECT sont utilisées pour chaque direction : sans filtre pubkey, l'index V002 `(slot, pubkey, state_hash)` porte la navigation ; avec filtre pubkey, la PK `(pubkey, slot, state_hash)` porte le préfixe account puis le même ordre total. Les predicates de continuation utilisent exclusivement les comparaisons tuple `>` / `<`. Aucun `OFFSET`, batch maximum, priorité, clamp ou policy worker n'est introduit.
Le cursor account V1 est fixe à 109 bytes :
```text
0..4 magic KSPA
4 version 1
5..13 last_slot u64 big-endian
13..45 last_pubkey 32 bytes
45..77 last_state_hash 32 bytes
77..109 SHA-256 binding digest
```
Le digest utilise le domaine `KSP/raw-account-state-cursor/v1` et lie le réseau, le filtre pubkey optionnel, la direction, les bornes de slot et la dernière clé complète. Les canaris couvrent round-trip, taille/magic/version/digest hostiles, replay réseau/pubkey/direction/plage et replay cross-family depuis `KSPT`.
La page conserve la sémantique Store existante : `LIMIT + 1` détecte la continuation, le backend n'impose aucun maximum fonctionnel KSP et expose seulement la borne PostgreSQL `9_223_372_036_854_775_806` nécessaire pour convertir `requested + 1` en `i64`. Les rows de liste revalident pubkey/hash à 32 bytes et `slot` via decimal exact `u64`, sans narrowing.
Surface runtime ajoutée :
```text
PostgresBackend::list_raw_account_states(
&RawAccountStateQuery,
) -> RawPage<RawAccountStateReference>
```
Les quatre `impl RawAccount* for PostgresBackend` et tout dispatch account dans `ksp-store-lib::Store` restent absents jusqu'à `pre.008`. Les migrations V000/V001/V002 restent byte-inchangées et le checksum V002 demeure `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`.
## 25. Verdict `pre.007-fix.001`
Le fix ne modifie ni `src/raw_account.rs`, ni `src/raw_account/cursor.rs`, ni le SQL runtime, ni aucune migration. Les cinq `Pubkey::clone()` inutiles des tests cursor sont supprimés. Les canaris `pre.005/pre.006` sont transformés pour accepter `list_raw_account_states`, devenu scope légitime en `pre.007`, tout en continuant d'interdire les surfaces destructives (`UPDATE`, `DELETE`, `ON CONFLICT DO UPDATE`, `OFFSET`) et les quatre implémentations `RawAccount*` réservées à `pre.008`.
Le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. Le gate opérateur complet du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (63 tests unitaires), Config et `--no-default-features` sont verts.
## 26. Verdict `pre.008`
Conformance RAW backend/façade : **10/10 matérialisée ; gate initial en échec ciblé sur le canari `dependency_boundary`**.
Inventaire exact attendu et désormais canarisé :
```text
PostgresBackend
RawTransactionRead
RawTransactionWrite
RawTransactionObservationRead
RawTransactionObservationWrite
RawTransactionRetentionRead
RawTransactionRetentionWrite
RawAccountStateRead
RawAccountStateWrite
RawAccountObservationRead
RawAccountObservationWrite
Store
même inventaire exact de 10 capabilities
```
Les quatre implémentations backend ne créent aucune nouvelle primitive physique : elles adaptent les méthodes étroites `get_raw_account_state`, `list_raw_account_states`, `persist_raw_account_acquisition`, `get_raw_account_observation` et `record_raw_account_observation` déjà validées. Les quatre implémentations de `Store` délèguent uniquement au backend compilé, appliquent `validate_operation_network` aux inputs réseau-scopés et réutilisent `map_postgres_error`.
La lecture `RawAccountObservationRead` reste la seule capability account sans guard réseau pré-dispatch parce que `RawObservationKey` ne porte aucun réseau ; le backend mono-network reconstruit la référence avec son binding déjà validé. L'acquisition state+observation vérifie séparément les deux réseaux dans la façade puis conserve le guard d'égalité de référence backend avant `pool.get()`.
Canaris transformés :
```text
inventaire exact PostgresBackend = 10
inventaire exact Store = 10
impl account uniquement dans runtime bridge backend
aucun impl account direct dans raw_account.rs
aucun SQL/type PostgreSQL dans ksp-store-lib
validate_operation_network occurrences Store = 14
feature postgres optionnelle inchangée
no-default-features doit continuer à compiler
```
Aucune migration V000/V001/V002 n'est modifiée ; le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. `ksp-store-api` reste inchangée. La preuve réelle PostgreSQL de coexistence, concurrence et cancellation account reste en `pre.009`.
## 27. Verdict `pre.008-fix.001`
Le gate opérateur de `pre.008` confirme que les audits Rust/Markdown, le workspace check, `ksp-store-api`, `ksp-store-lib`, `ksp-config-lib` et `--no-default-features` passent. L'unique blocage est la compilation de `ksp-store-postgres-lib/tests/dependency_boundary.rs` : `runtime` est référencé dans le canari d'ownership `pre.005` sans y être déclaré, tandis qu'une déclaration devenue inutilisée subsiste dans le canari V002 `pre.003`.
Le fix déplace uniquement :
```text
let runtime = include_str!("../src/runtime.rs");
```
vers le canari qui le consomme effectivement. Les assertions de placement 10/10 restent inchangées : les implémentations account demeurent uniques dans `runtime.rs`, absentes de `raw_account.rs`, et les couches migration/schema restent sans capability métier. Aucun fichier de production, SQL runtime ou migration V000/V001/V002 n'est modifié.
Le checksum V002 reste `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. Le gate opérateur complet du 2026-08-30 sur `0.3.4-pre.8.fix.1` est **PASS** : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL (63 tests unitaires + 13 dependency + 7 hardening + 13 public), Config et `--no-default-features` sont verts.
## 28. Verdict `pre.009`
Preuve PostgreSQL live `RawAccountState` : **PASS complet opérateur**.
Nouveau test :
```text
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
#[ignore = "opt-in real PostgreSQL RawAccountState proof; reads one dedicated URI from stdin"]
```
Invariants d'exploitation :
```text
URI dédiée lue depuis stdin uniquement
aucune lecture d'environnement
aucun affichage de l'URI
refus si une table KSP managée existe déjà
PostgreSQL >= 15
cleanup uniquement après preuve d'absence initiale
reopen health migration_version = 2
```
Matrice matérialisée :
| Scénario | Preuve `pre.009` |
|--------------------------------------------|--------------------------------------|
| bootstrap/reopen V002 | oui |
| schema_autoupdate index account | oui |
| slot/lamports/rent_epoch/write_version u64 | oui, `u64::MAX` |
| data account vide | oui |
| data account 16 MiB exact | oui |
| acquisition insert/idempotence | oui |
| même référence / contenu divergent | `Conflict` |
| même pubkey+slot / hashes distincts | oui |
| collision observation + rollback state | oui |
| observation supplémentaire identique | `AlreadyPresent` |
| observation supplémentaire divergente | `Conflict` |
| référence state absente | `ReferenceNotFound` |
| metadata Yellowstone | round-trip exact |
| pagination ASC/DESC | oui |
| filtre pubkey | oui |
| cursor cross-query / digest hostile | `QueryInvalid` |
| cursor cross-family `KSPT` | `QueryInvalid` |
| concurrence identique | Inserted + AlreadyPresent |
| concurrence divergente | Inserted + Conflict |
| cancellation réelle | `JoinHandle::abort()` + state absent |
| coexistence RawTransaction | oui |
| cleanup isolation | oui |
Le test `postgres_raw_transaction_live.rs` est ajusté uniquement sur son inventaire de schéma managé : il probe et supprime désormais aussi `ksp_raw_account_states` et `ksp_raw_account_observations`, nécessaires depuis V002. Aucun scénario métier RawTransaction n'est modifié.
Le gate standard du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts. Le live account est ensuite PASS sur PostgreSQL 17 en 2,72 s, avec URI dédiée lue depuis stdin sans écho.
Les migrations V000/V001/V002 restent byte-inchangées ; le checksum V002 reste :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
## 29. Verdict `pre.010`
Hardening et complétude cross-family : **PASS opérateur complet**.
Aucun fichier de production n'est modifié. Les nouveaux canaris backend vérifient que le SQL account reste limité aux lectures, inserts idempotents, locks et pagination keyset ; ils interdisent `UPDATE`, `DELETE`, `TRUNCATE`, `OFFSET`, `ON CONFLICT DO UPDATE` et toute policy worker. Le cursor account reste `KSPA`, domaine `KSP/raw-account-state-cursor/v1`, 109 bytes, distinct de `KSPT`.
Le registre V002 est figé à exactement 32 resources : 2 tables, 2 PK, 1 FK observation->state, 26 CHECK et 1 index `(slot,pubkey,state_hash)` non filtré. Les canaris imposent l'absence de `ksp_raw_transactions` et `ksp_raw_transaction_observations` dans V002. Le CHECK de `transaction_signature` impose seulement 64 bytes lorsqu'elle est présente et aucune FK transaction.
La façade compare directement son inventaire de traits RAW avec celui de `PostgresBackend` : les deux ensembles doivent être strictement identiques et contenir exactement dix capabilities. Aucune capability account de retention, delete ou compaction n'est admise.
Les fichiers `ksp-store-api`, `ksp-store-lib/src`, `ksp-store-postgres-lib/src`, les tests live et les migrations V000/V001/V002 restent inchangés. Le checksum V002 demeure :
```text
ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e
```
Le gate opérateur du 2026-08-30 est PASS : audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts. Les canaris supplémentaires de `pre.010` passent : inventaire RAW exact 10/10 des deux côtés, V002 exact à 32 resources, SQL account non destructif/keyset-only et séparation cross-family maintenue.
## 30. Verdict `pre.011`
Gate technique final : **PASS**.
Cette tranche ne modifie aucun code de production, test fonctionnel, SQL, migration, Config runtime, manifest de crate, dépendance runtime, README/USAGE, CHANGELOG/ROADMAP ou prompt suivant. Elle synchronise uniquement la version workspace et documente le gate final.
Matrice opérateur attendue :
```text
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-store-api
cargo test -p ksp-store-lib
cargo test -p ksp-store-lib --no-default-features
cargo test -p ksp-store-postgres-lib
cargo test -p ksp-config-lib
cargo check -p ksp-store-lib --no-default-features
cargo test --workspace
cargo tree -p ksp-store-lib --edges normal
cargo tree -p ksp-store-lib -e features
cargo tree -p ksp-store-postgres-lib --edges normal
cargo tree --duplicates
```
Replay PostgreSQL réel ciblé :
```bash
read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo
printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_account_live -- --ignored --nocapture --test-threads=1
unset KSP_PG_TEST_URI
```
Le live account est la preuve finale ciblée : il couvre bootstrap/reopen V002, bornes u64/data, idempotence/conflits, pagination/cursors, concurrence/cancellation et coexistence `RawTransaction`. Les lives foundation et RawTransaction séparés ne sont pas requis par `pre.011`.
Le replay opérateur de clôture confirme :
```text
cargo test -p ksp-store-lib --no-default-features PASS
cargo test --workspace PASS
cargo tree façade/backend/features/duplicates exécutés
postgres_raw_account_live PASS — PostgreSQL 17, 3.00 s
```
Les graphes ne révèlent pas de reverse edge Store ni de dépendance PostgreSQL lorsque la feature façade correspondante est désactivée. Les doublons Cargo observés appartiennent au workspace transverse et ne matérialisent aucune duplication propre à la verticale Store nécessitant un correctif. Le live final lit l'URI dédiée depuis stdin sans l'écho.
Le couloir technique est donc fermé ; aucun `pre.011-fix.NNN` n'est requis.
## 31. Verdict `pre.012`
Réconciliation documentaire finale : **matérialisée ; gate documentaire opérateur à exécuter**.
Documents durables réconciliés :
```text
crates/ksp-store-lib/README.md
crates/ksp-store-lib/USAGE.md
crates/ksp-store-postgres-lib/README.md
crates/ksp-store-postgres-lib/USAGE.md
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md
```
La documentation décrit maintenant la surface réellement validée : dix capabilities RAW exactes, quatre capabilities `RawAccount*`, acquisition account atomique/idempotente, observation supplémentaire reference-guarded, pagination keyset `(slot,pubkey,state_hash)` avec cursor `KSPA`, séparation stricte avec `RawTransaction` et absence de rétention/destruction account.
Relus et volontairement inchangés :
```text
README.md racine
docs/architecture/003-COMPONENT_CONTRACTS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/architecture/005-DEPENDENCY_GRAPH.md
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
ksp-config-lib README/USAGE
ksp-store-api — aucun README/USAGE à créer artificiellement
```
`CHANGELOG.md`, `ROADMAP.md` et le prompt de démarrage `0.3.5` restent hors de cette tranche et appartiennent à `pre.013`. Aucun `src/**`, test, Config, schema, migration ou dépendance n'est modifié.

File diff suppressed because it is too large Load Diff