33 Commits

Author SHA1 Message Date
3de0488cab v0.3.5-rel.001 2026-08-31 14:28:12 +02:00
2460162243 v0.3.5-pre.008-fix.001 2026-08-31 14:27:15 +02:00
1b9104d64b v0.3.5-pre.008 2026-08-31 14:01:23 +02:00
80be36bfdd v0.3.5-pre.007 2026-08-31 13:07:56 +02:00
e8d2382ac3 v0.3.5-pre.006 2026-08-31 11:43:40 +02:00
749e53edc2 v0.3.5-pre.005-fix.001 2026-08-31 11:23:40 +02:00
7377a6f6cd v0.3.5-pre.005 2026-08-31 11:17:09 +02:00
45601ce386 v0.3.5-pre.004 2026-08-31 11:14:44 +02:00
7d490eb5d8 v0.3.5-pre.003 2026-08-31 11:01:45 +02:00
ca018095bf v0.3.5-pre.002-fix.001 2026-08-31 10:44:51 +02:00
0dd722ffca v0.3.5-pre.002 2026-08-31 10:36:10 +02:00
75e8030b07 v0.3.5-pre.001-fix.001 2026-08-31 10:21:47 +02:00
c24e4c48f8 v0.3.5-pre.001 2026-08-31 09:45:27 +02:00
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
118 changed files with 16995 additions and 310 deletions

View File

@@ -1,8 +1,32 @@
<!-- file: CHANGELOG.md -->
<!-- version: 22 -->
<!-- version: 25 -->
# Changelog KSP
## 0.3.5 — Interface acquisition events partagés — 2026-08-31
`0.3.5` étend `ksp-interface-lib` avec deux familles passives d'acquisition réellement partagées, sans transformer Interface en runtime, en event bus ou en seconde couche RAW. `SlotLifecycleEvent` expose un `slot` et un `SlotLifecycleStage` non exhaustif limité à `Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed` et `Rooted`. La normalisation conserve la sémantique commune : les notifications Solana `optimisticConfirmation` et Yellowstone `Confirmed` convergent vers `OptimisticallyConfirmed`, tandis que Solana `root` et Yellowstone `Finalized` convergent vers `Rooted`; les différences d'ordre, de complétude, de replay et de transport restent la responsabilité du producteur/Transport.
La seconde famille matérialise le fait minimal d'exécution transactionnelle partagé par plusieurs sources. `TransactionSignature` possède exactement 64 octets et un `Debug` redacted, `TransactionExecutionOutcome` distingue seulement `Succeeded` et `Failed`, et `TransactionExecutionEvent` transporte uniquement `slot + signature + outcome`. Les logs, erreurs provider détaillées, commitments, indexes, timestamps, filtres et payloads complets restent Transport-owned. Les snapshots HTTP `getSignatureStatuses`, les transitions one-shot `signatureSubscribe`, un éventuel `TransactionLogEvent`, les votes et les entrées Yellowstone ne sont pas fusionnés artificiellement dans ce contrat.
La frontière d'ownership reste stricte : les DTOs wire/provider demeurent dans `ksp-onchain-transport-lib`, les événements passifs provider-neutral sont Interface-owned, et les modèles persistants/replayables `RawTransaction` / `RawAccountState` restent `ksp-store-api`. `ksp-interface-lib` conserve exactement `ksp-core-lib` comme seule dépendance normale, sans feature propre, dev/build dependency, serde, codec, logging ou runtime. Les canaris publics, external consumer, inventaires exacts et hardening vérifient également qu'aucun second RAW, metadata source ou payload hostile n'entre dans la surface Interface.
Les gates de clôture passent audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, tests ciblés Interface/Program API, `cargo test --workspace` et graphes Cargo. `cargo tree -p ksp-interface-lib --edges normal` confirme le chemin `ksp-interface-lib -> ksp-core-lib -> solana-pubkey -> solana-address`; le graphe features ne montre aucune feature propre Interface et les doublons éventuels restent ceux du workspace global. La documentation durable a été réconciliée et `TransactionLogEvent` est conservé comme idée différée soumise à un nouveau gate consumer/bornes.
`prompts/025-V0_3_6_START_PROMPT.md` ouvre `0.3.6` sur le développement parallèle de `ksp-job-api` et `ksp-job-backfill-lib`. La release doit reprendre fonctionnellement le backfill historique kbot3 sans en copier le code : audit obligatoire de l'archive historique, `RawTransaction` par adresse via `getSignaturesForAddress` + `getTransaction`, directions/anchors et déduplication, hydratation/persistence via les façades KSP, frontier/checkpoint/reprise, cancellation/concurrency bornées, idempotence et distinctions missing/conflit. `ksp-job-api` doit en parallèle stabiliser lifecycle/progress/outcome et un contrat de notifications/listeners borné pour qu'une couche supérieure puisse visualiser l'état sans parser les logs. Le ROADMAP enchaîne ensuite l'app de backfill/inspection `0.3.7`, `ksp-worker-api` `0.3.8`, `ksp-worker-live-transactions-retriever-lib` `0.3.9` puis une application de monitoring/visualisation Jobs + Workers `0.3.10`, avec reprise du même pattern d'observabilité côté Worker sans fusionner les sémantiques Job/Worker/Store.
## 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: 392
[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.5"
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: 100 -->
# Roadmap KSP
@@ -96,19 +96,22 @@ 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`.
- [ ] `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.
- [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.
- [X] `0.3.5``ksp-interface-lib` étendu avec deux familles passives réellement partagées : `SlotLifecycleEvent` (`Processed`, `FirstShredReceived`, `Completed`, `CreatedBank`, `Dead`, `OptimisticallyConfirmed`, `Rooted`) et `TransactionExecutionEvent` (`slot + TransactionSignature[64] + Succeeded/Failed`). Interface reste Core-only, provider-neutral, sans serde/codec/runtime/event bus et sans duplication de `RawTransaction`/`RawAccountState`; les DTOs riches restent Transport-owned et les candidats non convergents restent différés.
- [ ] `0.3.6` — Introduire en parallèle `ksp-job-api` et `ksp-job-backfill-lib` : reprendre fonctionnellement le backfill historique kbot3 sur les abstractions KSP actuelles (`ksp-onchain-transport-lib` + `ksp-store-lib`), fermer une première verticale `RawTransaction` historique par adresse avec directions/anchors, déduplication, hydratation, idempotence, frontier/checkpoint/reprise, cancellation et concurrency bornées, et stabiliser un lifecycle/progress/outcome avec notifications/listeners sûrs pour les couches supérieures.
- [ ] `0.3.7` — Introduire une application spécialisée de backfill/inspection RAW consommant `ksp-job-api` : lancement/annulation contrôlés, état et progression live, compteurs, checkpoint/frontière, outcome terminal et inspection RAW sans parser les logs ni connaître les providers/backends physiques.
- [ ] `0.3.8` — Introduire `ksp-worker-api` comme API générique de lifecycle/health/progression pour services continus, en reprenant le pattern de notifications/listeners stabilisé par Job tout en gardant les sémantiques Worker distinctes des jobs terminables et des wake-ups Store post-commit.
- [ ] `0.3.9` — Introduire `ksp-worker-live-transactions-retriever-lib` pour l'acquisition continue `RawTransaction` via les surfaces live de `ksp-onchain-transport-lib`, persistence par `ksp-store-lib`, reprise/backpressure/idempotence et notifications `ksp-worker-api`, sans decode Program ni dépendance backend/provider directe.
- [ ] `0.3.10` — Introduire une application de monitoring/visualisation Jobs + Workers : vue graphique des lifecycles, health/progression, rates/backpressure/retries sûrs, checkpoints et outcomes ; l'application consomme les APIs publiques Job/Worker et ne devient ni scheduler caché ni source de vérité du backlog.
### 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`.
- [ ] **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.
- [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**statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif dexécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsquun consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusquà démonstration dun consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusquà STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
- [X] slot/root/slotsUpdates `0.3.5` stabilise `SlotLifecycleEvent` pour lintersection réellement partagée, sans persistence Store par défaut et sans promettre lordre/complétude du flux.
- [ ] **TODO** — vote realtime : reste hors Interface tant quaucun consumer transversal et aucune sémantique provider-neutral suffisamment précise ne justifient un contrat partagé.
- [ ] **IDEA**`RawBlock` : ne rouvrir que si une information block-level non reconstructible devient nécessaire ; `getBlock` doit dabord être traité comme source de `RawTransaction`, pas comme invitation à recopier le ledger en blocs.
- [ ] **REJET ACTUEL** — Yellowstone `Entry` : trop bas niveau et aucune destination replay/decomposition/event métier justifiant un modèle KSP nest identifiée.
- [ ] **TODO** — processing ledger : reprendre lidée kbot2/kbot3 `stage + processor identity/version + input identity/hash + terminal status`, sans faire dun `processed: bool` la preuve durable unique ; prévoir force replay/version upgrades lorsque les processors seront ouverts.

View File

@@ -1,9 +1,11 @@
<!-- file: crates/ksp-interface-lib/README.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# ksp-interface-lib
`ksp-interface-lib` est la façade wire officielle KSP destinée aux contrats passifs partagés par les implémentations Program Solana officielles ou externes. La crate expose uniquement des structures de représentation/admission ; elle ne possède ni transport, ni exécution, ni persistence, ni comportement métier Program.
`ksp-interface-lib` possède les contrats passifs KSP qui doivent être partagés entre plusieurs composants sans imposer leur runtime d'origine. Sa surface couvre actuellement deux familles distinctes : les contrats wire Program génériques et un petit ensemble de faits d'acquisition provider-neutral dont la sémantique commune a été démontrée.
La crate reste une façade de représentation. Elle ne possède ni transport réseau, ni event bus, ni worker/job, ni persistence, ni exécution, ni comportement métier Program.
## Ownership
@@ -15,9 +17,9 @@ Error / ErrorCode / Result
Program IDs fondamentaux
```
`Pubkey` est réexporté depuis le crate-root Interface afin qu'un consumer wire n'introduise aucun wrapper d'identité parallèle. Les Program IDs restent possédés et répertoriés par Core.
`Pubkey` est réexporté depuis le crate-root Interface afin qu'un consumer n'introduise aucun wrapper d'identité parallèle. Les Program IDs restent possédés et répertoriés par Core.
La dependency direction candidate `0.2.13` reste strictement :
Le graphe normal reste strictement :
```text
ksp-interface-lib
@@ -25,9 +27,11 @@ ksp-interface-lib
└── solana-pubkey
```
## Surface publique `0.2.13`
La crate ne possède aucune feature Cargo, aucune `dev-dependency` et aucune `build-dependency` runtime propre.
La façade crate-root expose exactement :
## Surface publique
La façade crate-root expose :
```text
Pubkey
@@ -36,10 +40,17 @@ MAX_PROGRAM_INSTRUCTION_ACCOUNTS
ProgramInstruction
MAX_PROGRAM_INSTRUCTION_DATA_LEN
ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED
SlotLifecycleStage
SlotLifecycleEvent
TransactionSignature
TransactionExecutionOutcome
TransactionExecutionEvent
```
Aucun module interne n'est public.
## Contrats Program passifs
### `ProgramAccountMeta`
`ProgramAccountMeta` représente un compte ordonné d'une instruction avec :
@@ -68,16 +79,14 @@ data: Vec<u8>
Les cas vides sont valides et une `program_id` inconnue du registry KSP reste admissible.
## Bornes d'admission
Interface applique deux limites locales :
### Bornes d'admission
| Limite | Valeur |
|------------------------------------|----------|
| `MAX_PROGRAM_INSTRUCTION_ACCOUNTS` | `255` |
| `MAX_PROGRAM_INSTRUCTION_DATA_LEN` | `10_240` |
Ces valeurs sont des **bornes d'admission Interface**. Elles ne constituent pas une garantie qu'une instruction donnée tient dans toutes les contraintes d'une transaction Solana top-level. La limite CPI de comptes uniques n'est notamment pas transformée en règle artificielle sur la liste d'account metas.
Ces valeurs sont des **bornes d'admission Interface**. Elles ne constituent pas une garantie qu'une instruction donnée respecte à elle seule toutes les contraintes d'une transaction Solana complète.
Un dépassement utilise le code commun :
@@ -89,21 +98,74 @@ Le contexte d'erreur est limité aux métadonnées sûres `field`, `actual_len`
Le `Debug` de `ProgramInstruction` est volontairement borné : il affiche `program_id`, `account_count` et `data_len`, jamais les accounts complets ni les octets du payload.
## Faits passifs d'acquisition
Les événements Interface sont des **projections provider-neutral supplémentaires**. Ils ne remplacent jamais les DTOs riches de `ksp-onchain-transport-lib` et ne deviennent jamais la source de vérité durable d'un backlog.
La conversion depuis un DTO HTTP/WS/gRPC/provider appartient à la composition ou au consumer qui connaît les deux contrats. `ksp-interface-lib` ne dépend donc pas de Transport.
### `SlotLifecycleEvent`
`SlotLifecycleEvent` conserve exactement :
```text
slot: u64
stage: SlotLifecycleStage
```
Les stages actuellement représentés sont :
```text
Processed
FirstShredReceived
Completed
CreatedBank
Dead
OptimisticallyConfirmed
Rooted
```
`SlotLifecycleStage` est `#[non_exhaustive]` afin qu'un consumer externe traite explicitement l'évolution future de l'enum.
Parent, timestamp, diagnostics de slot mort, source/provider, filter/session metadata et autres détails Transport ne sont pas copiés dans ce contrat minimal.
### `TransactionExecutionEvent`
`TransactionExecutionEvent` conserve exactement :
```text
slot: u64
signature: TransactionSignature
outcome: TransactionExecutionOutcome
```
`TransactionSignature` contient exactement les 64 bytes canoniques déjà décodés d'une signature Solana. Son `Debug` ne rend pas les bytes.
`TransactionExecutionOutcome` distingue uniquement :
```text
Succeeded
Failed
```
L'enum est `#[non_exhaustive]`. Le contrat ne contient aucun log, payload RAW, erreur provider, commitment, provenance, network id, source metadata ni détail d'exécution arbitraire. Une source ambiguë ou insuffisante doit rester dans son owner Transport au lieu de forcer un événement Interface.
## Codecs et runtime
La foundation `0.2.13` n'ajoute aucun codec par réflexe :
La crate n'ajoute aucun codec ou runtime par réflexe :
```text
serde / serde_json absents
borsh absent
wincode absent
bincode absent
solana-instruction absent
borsh absent tant qu'aucun wire réel ne le requiert
wincode absent tant qu'aucun wire réel ne le requiert
bincode interdit pour les codecs wire KSP
transport/runtime réseau absent
logging runtime absent
```
Des codecs/layouts/discriminants spécifiques pourront être ajoutés ultérieurement uniquement lorsqu'un vertical Program réel en démontre le besoin et que leur ownership wire appartient bien à Interface.
Des codecs/layouts/discriminants spécifiques peuvent être ajoutés uniquement lorsqu'un protocole réel en démontre le besoin et que leur ownership wire appartient bien à Interface.
La crate ne produit aucun événement runtime. Elle ne dépend donc pas de `ksp-logging-lib` et ne possède ni `constants.rs` ni `TRACING_TARGET`. Si un futur comportement Interface exige réellement du logging, le runtime devra passer par la façade Logging KSP plutôt que par une dépendance directe à Tracing.
Les événements passifs ne constituent pas un comportement runtime. La crate ne dépend donc pas de `ksp-logging-lib` et ne possède ni `constants.rs` ni `TRACING_TARGET`.
## Frontières
@@ -112,20 +174,20 @@ La crate ne produit aucun événement runtime. Elle ne dépend donc pas de `ksp-
```text
RPC / WebSocket / gRPC
provider DTOs Transport
wallet / signature
sessions / reconnect / backpressure
Config / environnement
persistence / Store
persistence / Store / cursor / retention
notifications post-commit Store
worker / job / scheduler / event bus
Program decoding / recognition / proofs
execution policy / signers
transaction replay / CPI path / runtime logs
lifecycle réseau
RawTransaction / RawAccountState
```
La foundation Program API est reportée à `0.2.14`. Les wires génériques d'acquisition/CORE restent reportés à `0.3.2+`.
Les données persistantes/replayables restent dans `ksp-store-api`. Les notifications post-commit de données persistées restent un contrat Store API lorsqu'un publisher/consumer réel les justifie. Les faits réseau riches restent Transport-owned.
## Références
- [Usage public](USAGE.md)
- [Plan `0.2.13`](../../docs/plans/020-V0_2_13_INTERFACE_PLAN.md)
- [Validation `0.2.13`](../../docs/validation/016-V0_2_13_INTERFACE.md)
- [Architecture Wire + Program](../../docs/architecture/006-WIRE_AND_PROGRAM.md)
- [Architecture Acquisition/Workers/Jobs](../../docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md)

View File

@@ -1,9 +1,9 @@
<!-- file: crates/ksp-interface-lib/USAGE.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Usage de ksp-interface-lib
Cette page décrit la façade publique matérialisée par `0.2.13`. Les modules internes ne font pas partie du contrat consommable : utiliser uniquement les exports du crate-root.
Cette page décrit l'utilisation durable de la façade publique. Les modules internes ne font pas partie du contrat consommable : utiliser uniquement les exports du crate-root.
## Construire des account metas
@@ -52,9 +52,9 @@ match result {
L'ordre et les doublons des accounts sont conservés. Les octets `data` restent opaques : `ProgramInstruction` ne les sérialise, désérialise ni interprète.
Les `Vec` fournis à `try_new` sont consommés par la structure après validation des bornes ; aucun clone ou reformatage interne n'est requis par le contrat actuel.
Les `Vec` fournis à `try_new` sont consommés par la structure après validation des bornes ; aucun clone ou reformatage interne n'est requis par le contrat.
## Bornes
## Respecter les bornes Program
Les limites publiques sont :
@@ -63,9 +63,9 @@ assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS, 255);
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN, 10_240);
```
`255` account metas et `10_240` bytes de data sont admis. `256` account metas ou `10_241` bytes sont refusés par `try_new` avant création d'un `ProgramInstruction` valide.
`255` account metas et `10_240` bytes de data sont admis. `256` account metas ou `10_241` bytes sont refusés par `ProgramInstruction::try_new`.
Ces limites sont des bornes locales Interface et ne promettent pas qu'une instruction admise respecte à elle seule toutes les contraintes de taille/account-set d'une transaction Solana complète.
Ces limites sont locales à Interface et ne promettent pas qu'une instruction admise respecte à elle seule toutes les contraintes d'une transaction Solana complète.
## Observer une erreur de limite
@@ -92,31 +92,108 @@ maximum_len
Le contenu du payload et les account metas arbitraires ne sont pas projetés dans le diagnostic.
## Debug borné
## Construire un événement de lifecycle de slot
Le `Debug` de `ProgramInstruction` contient uniquement :
Un producer/composer qui a déjà établi la correspondance sémantique avec son DTO Transport peut construire le fait passif partagé :
```text
program_id
account_count
data_len
```rust
let event = ksp_interface_lib::SlotLifecycleEvent::new(
42,
ksp_interface_lib::SlotLifecycleStage::Processed,
);
assert_eq!(event.slot(), 42);
assert_eq!(event.stage(), ksp_interface_lib::SlotLifecycleStage::Processed);
```
Il ne faut donc pas attendre de ce rendu une sérialisation wire ou un dump du payload.
Les stages actuellement disponibles sont :
```text
Processed
FirstShredReceived
Completed
CreatedBank
Dead
OptimisticallyConfirmed
Rooted
```
Un consumer externe doit traiter `SlotLifecycleStage` comme une enum évolutive `#[non_exhaustive]` et prévoir un fallback dans ses `match`.
Le type ne contient volontairement ni timestamp, ni parent, ni diagnostic, ni source/provider, ni identifiant de subscription.
## Construire un événement d'exécution de transaction
Une signature doit d'abord être disponible sous sa forme canonique de 64 bytes :
```rust
let signature = ksp_interface_lib::TransactionSignature::new([7_u8; 64]);
let event = ksp_interface_lib::TransactionExecutionEvent::new(
123,
signature,
ksp_interface_lib::TransactionExecutionOutcome::Succeeded,
);
assert_eq!(event.slot(), 123);
assert_eq!(event.signature().as_bytes(), &[7_u8; 64]);
assert_eq!(
event.outcome(),
ksp_interface_lib::TransactionExecutionOutcome::Succeeded,
);
```
`TransactionExecutionOutcome` distingue uniquement `Succeeded` et `Failed` et reste `#[non_exhaustive]`.
Le `Debug` de `TransactionSignature` et de `TransactionExecutionEvent` n'affiche pas les bytes de signature.
## Convertir depuis Transport
Ne pas ajouter `ksp-onchain-transport-lib` comme dépendance de `ksp-interface-lib` pour fournir des `From<TransportDto>`.
La conversion appartient au composant qui connaît les deux côtés :
```text
Transport DTO riche
|
| conversion explicite dans composition/consumer
v
Interface event passif minimal
```
Ne construire un événement Interface que si la source fournit suffisamment d'information pour le fait commun exact. Si l'état est ambigu, conserver le DTO dans son owner Transport ou déclencher une hydratation adaptée ; ne pas inventer de valeur par défaut.
## Ne pas utiliser les événements comme stockage durable
`SlotLifecycleEvent` et `TransactionExecutionEvent` sont des faits passifs, pas des modèles RAW replayables.
Ils ne remplacent pas :
```text
RawTransaction
RawTransactionObservation
RawAccountState
RawAccountObservation
Store backlog / cursor / retention
```
Les consumers qui ont besoin de reprise après crash ou de replay doivent s'appuyer sur `ksp-store-lib`/`ksp-store-api` selon leur responsabilité, pas sur un event Interface en mémoire.
## Dépendances à ne pas ajouter côté consumer
Un consumer de la façade Interface n'a pas besoin d'ajouter un SDK Program Solana uniquement pour reconstruire `ProgramInstruction`. La crate utilise le `Pubkey` canonique partagé avec Core et conserve son propre contrat passif.
Un consumer de la façade Interface n'a pas besoin d'ajouter un SDK Program Solana uniquement pour reconstruire `ProgramInstruction`, ni un runtime Transport pour manipuler les événements passifs déjà normalisés.
La foundation ne fournit volontairement pas :
La crate ne fournit volontairement pas :
```text
serde générique
Borsh / Wincode générique
solana-instruction interop automatique
transport réseau
converters provider automatiques
Program decoder/preparer
signing/execution
persistence/event bus
```
Ces surfaces doivent être introduites dans leur owner respectif lorsqu'un cas réel le justifie, pas comme dépendances implicites d'un consumer Interface.
Ces surfaces doivent être introduites dans leur owner respectif lorsqu'un cas réel le justifie.

View File

@@ -1,20 +1,22 @@
// file: crates/ksp-interface-lib/src/lib.rs
// version: 3
// version: 5
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Passive wire contracts shared by KSP Program implementations.
//! Passive contracts shared across KSP component boundaries.
//!
//! The foundation reuses the canonical Solana [`Pubkey`] owned by
//! `ksp-core-lib` and exposes only bounded, passive Program-facing structures.
//! Runtime, transport, persistence and Program behavior remain outside this
//! crate.
//! The crate reuses canonical Solana primitives owned by `ksp-core-lib` and
//! exposes only bounded, passive Program-facing and provider-neutral
//! acquisition structures. Runtime, transport, persistence and Program
//! behavior remain outside this crate.
mod error;
mod program_account_meta;
mod program_instruction;
mod slot_lifecycle;
mod transaction_execution;
/// Error code used when an Interface-owned Program instruction admission limit is exceeded.
pub use self::error::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED;
@@ -26,5 +28,15 @@ pub use self::program_account_meta::ProgramAccountMeta;
pub use self::program_instruction::MAX_PROGRAM_INSTRUCTION_DATA_LEN;
/// Passive, bounded Program instruction wire contract.
pub use self::program_instruction::ProgramInstruction;
/// Passive provider-neutral occurrence of one slot lifecycle stage.
pub use self::slot_lifecycle::SlotLifecycleEvent;
/// Provider-neutral stage in the lifecycle of an observed Solana slot.
pub use self::slot_lifecycle::SlotLifecycleStage;
/// Passive provider-neutral observation of one transaction execution result.
pub use self::transaction_execution::TransactionExecutionEvent;
/// Provider-neutral outcome of one observed transaction execution.
pub use self::transaction_execution::TransactionExecutionOutcome;
/// Canonical 64-byte Solana transaction signature used by passive Interface events.
pub use self::transaction_execution::TransactionSignature;
/// Canonical Solana account address primitive owned by `ksp-core-lib`.
pub use ksp_core_lib::Pubkey;

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-interface-lib/src/slot_lifecycle.rs
// version: 1
/// Provider-neutral stage in the lifecycle of an observed Solana slot.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SlotLifecycleStage {
/// The slot has been processed.
Processed,
/// The first shred for the slot has been received.
FirstShredReceived,
/// Slot ingestion has completed.
Completed,
/// A bank has been created for the slot.
CreatedBank,
/// The slot has been marked dead.
Dead,
/// The slot has reached optimistic confirmation.
OptimisticallyConfirmed,
/// The slot has become rooted.
Rooted,
}
/// Passive provider-neutral occurrence of one slot lifecycle stage.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SlotLifecycleEvent {
slot: u64,
stage: SlotLifecycleStage,
}
impl SlotLifecycleEvent {
/// Creates a lifecycle event for `slot` and `stage`.
#[must_use]
pub const fn new(slot: u64, stage: SlotLifecycleStage) -> Self {
return Self { slot, stage };
}
/// Returns the observed slot exactly as supplied.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the provider-neutral lifecycle stage.
#[must_use]
pub const fn stage(&self) -> SlotLifecycleStage {
return self.stage;
}
}
#[cfg(test)]
#[path = "../unit_tests/slot_lifecycle.rs"]
mod tests;

View File

@@ -0,0 +1,85 @@
// file: crates/ksp-interface-lib/src/transaction_execution.rs
// version: 1
/// Canonical 64-byte Solana transaction signature used by passive Interface events.
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TransactionSignature([u8; 64]);
impl TransactionSignature {
/// Creates one transaction signature from already-decoded canonical bytes.
#[must_use]
pub const fn new(bytes: [u8; 64]) -> Self {
return Self(bytes);
}
/// Returns the exact canonical signature bytes.
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 64] {
return &self.0;
}
}
impl std::fmt::Debug for TransactionSignature {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("TransactionSignature(..)");
}
}
/// Provider-neutral outcome of one observed transaction execution.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TransactionExecutionOutcome {
/// The observed transaction execution completed without a transaction error.
Succeeded,
/// The observed transaction execution completed with a transaction error.
Failed,
}
/// Passive provider-neutral observation of one transaction execution result.
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct TransactionExecutionEvent {
slot: u64,
signature: TransactionSignature,
outcome: TransactionExecutionOutcome,
}
impl TransactionExecutionEvent {
/// Creates one transaction execution event from its common provider-neutral fields.
#[must_use]
pub const fn new(slot: u64, signature: TransactionSignature, outcome: TransactionExecutionOutcome) -> Self {
return Self { slot, signature, outcome };
}
/// Returns the containing slot exactly as observed by the producer.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the canonical transaction signature.
#[must_use]
pub const fn signature(&self) -> TransactionSignature {
return self.signature;
}
/// Returns the normalized execution outcome.
#[must_use]
pub const fn outcome(&self) -> TransactionExecutionOutcome {
return self.outcome;
}
}
impl std::fmt::Debug for TransactionExecutionEvent {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("TransactionExecutionEvent")
.field("slot", &self.slot)
.field("signature", &"<omitted>")
.field("outcome", &self.outcome)
.finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/transaction_execution.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-interface-lib/tests/dependency_boundary.rs
// version: 5
// version: 8
//! Dependency and passive-surface canaries for the Interface foundation.
@@ -91,6 +91,8 @@ fn pre_005_all_production_sources_preserve_the_dependency_firewall() {
include_str!("../src/lib.rs"),
include_str!("../src/program_account_meta.rs"),
include_str!("../src/program_instruction.rs"),
include_str!("../src/slot_lifecycle.rs"),
include_str!("../src/transaction_execution.rs"),
];
for source in production_sources {
for forbidden in [
@@ -129,3 +131,52 @@ fn pre_005_instruction_source_has_no_narrowing_cast_or_hidden_codec_entry_point(
}
return;
}
#[test]
fn v0_3_5_pre_004_acquisition_sources_remain_provider_neutral_and_runtime_free() {
let acquisition_sources = [include_str!("../src/slot_lifecycle.rs"), include_str!("../src/transaction_execution.rs")];
for source in acquisition_sources {
for forbidden in [
"borsh::",
"bincode::",
"ksp_config_lib::",
"ksp_logging_lib::",
"ksp_onchain_transport_lib::",
"ksp_store_api::",
"ksp_store_lib::",
"reqwest::",
"serde::",
"serde_json::",
"solana_instruction::",
"tauri::",
"tokio::",
"tonic::",
concat!("tracing", "::"),
"wincode::",
] {
assert!(!source.contains(forbidden), "forbidden acquisition dependency path detected: {forbidden}");
}
}
return;
}
#[test]
fn v0_3_5_pre_005_manifest_keeps_exact_core_only_graph_without_features_or_hidden_dependency_sections() {
let manifest = include_str!("../Cargo.toml");
assert!(!manifest.contains("[features]"));
assert!(!manifest.contains("[dev-dependencies]"));
assert!(!manifest.contains("[build-dependencies]"));
assert_eq!(manifest.matches("[dependencies]").count(), 1);
let dependencies_tail = manifest.split("[dependencies]").nth(1);
assert!(dependencies_tail.is_some(), "Interface dependencies section must exist");
let dependencies_tail = match dependencies_tail {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dependencies = match dependencies_tail.split("[lints]").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(manifest_dependency_names(dependencies), std::vec!["ksp-core-lib"]);
return;
}

View File

@@ -1,12 +1,22 @@
// file: crates/ksp-interface-lib/tests/external_consumer.rs
// version: 1
// version: 2
//! Downstream-style consumer canary for the public Interface facade.
//! Downstream-style consumer canaries for the public Interface facade.
fn consume_instruction(instruction: ksp_interface_lib::ProgramInstruction) -> (ksp_interface_lib::Pubkey, usize, usize) {
return (*instruction.program_id(), instruction.accounts().len(), instruction.data().len());
}
fn consume_slot_lifecycle(event: ksp_interface_lib::SlotLifecycleEvent) -> (u64, ksp_interface_lib::SlotLifecycleStage) {
return (event.slot(), event.stage());
}
fn consume_transaction_execution(
event: ksp_interface_lib::TransactionExecutionEvent,
) -> (u64, ksp_interface_lib::TransactionSignature, ksp_interface_lib::TransactionExecutionOutcome) {
return (event.slot(), event.signature(), event.outcome());
}
#[test]
fn pre_005_external_consumer_uses_only_the_crate_root_facade() {
let program_id = ksp_interface_lib::Pubkey::new_from_array([0xA1_u8; 32]);
@@ -25,3 +35,13 @@ fn pre_005_external_consumer_uses_only_the_crate_root_facade() {
assert_eq!(ksp_interface_lib::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED.domain(), "interface");
return;
}
#[test]
fn v0_3_5_pre_004_external_consumer_uses_only_crate_root_for_both_acquisition_families() {
let lifecycle = ksp_interface_lib::SlotLifecycleEvent::new(u64::MAX, ksp_interface_lib::SlotLifecycleStage::OptimisticallyConfirmed);
assert_eq!(consume_slot_lifecycle(lifecycle), (u64::MAX, ksp_interface_lib::SlotLifecycleStage::OptimisticallyConfirmed));
let signature = ksp_interface_lib::TransactionSignature::new([0xA3_u8; 64]);
let execution = ksp_interface_lib::TransactionExecutionEvent::new(u64::MAX - 1, signature, ksp_interface_lib::TransactionExecutionOutcome::Failed);
assert_eq!(consume_transaction_execution(execution), (u64::MAX - 1, signature, ksp_interface_lib::TransactionExecutionOutcome::Failed));
return;
}

View File

@@ -1,75 +1,75 @@
// file: crates/ksp-interface-lib/tests/release_completeness.rs
// version: 1
// version: 5
//! Release-level completeness canaries for the `0.2.13` Interface foundation.
//! Release-completeness canaries for the public `ksp-interface-lib` surface.
#[test]
fn pre_005_exact_crate_root_export_inventory_is_stable() {
let crate_root = include_str!("../src/lib.rs");
let mut actual = std::vec::Vec::new();
for line in crate_root.lines() {
let trimmed = line.trim();
if trimmed.starts_with("pub use ") {
actual.push(trimmed);
}
}
actual.sort_unstable();
let mut expected = std::vec![
"pub use self::error::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED;",
"pub use self::program_account_meta::MAX_PROGRAM_INSTRUCTION_ACCOUNTS;",
"pub use self::program_account_meta::ProgramAccountMeta;",
"pub use self::program_instruction::MAX_PROGRAM_INSTRUCTION_DATA_LEN;",
"pub use self::program_instruction::ProgramInstruction;",
"pub use ksp_core_lib::Pubkey;",
];
expected.sort_unstable();
assert_eq!(actual, expected);
assert!(!crate_root.contains("pub mod "));
return;
fn crate_root_source() -> std::result::Result<std::string::String, std::io::Error> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/lib.rs");
return std::fs::read_to_string(path);
}
#[test]
fn pre_005_production_module_inventory_contains_no_second_wire_domain() -> std::io::Result<()> {
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let entries = match std::fs::read_dir(source_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut names = std::vec::Vec::new();
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let file_type = match entry.file_type() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !file_type.is_file() {
continue;
}
let name = match entry.file_name().into_string() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
if name.ends_with(".rs") {
names.push(name);
}
}
names.sort_unstable();
assert_eq!(names, std::vec!["error.rs", "lib.rs", "program_account_meta.rs", "program_instruction.rs"]);
return std::result::Result::Ok(());
}
#[test]
fn pre_005_foundation_has_one_error_code_and_two_bounded_passive_types() {
fn v0_3_5_pre_004_foundation_keeps_exactly_two_passive_acquisition_families() {
assert_eq!(ksp_interface_lib::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED.domain(), "interface");
assert_eq!(ksp_interface_lib::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED.code(), "program_instruction_limit_exceeded");
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS, 255);
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN, 10_240);
let program_id = ksp_interface_lib::Pubkey::new_from_array([0xB1_u8; 32]);
let account = ksp_interface_lib::ProgramAccountMeta::readonly(ksp_interface_lib::Pubkey::new_from_array([0xB2_u8; 32]), false);
let instruction = ksp_interface_lib::ProgramInstruction::try_new(program_id, std::vec![account], std::vec![0xB3_u8]);
assert!(instruction.is_ok());
let lifecycle = ksp_interface_lib::SlotLifecycleEvent::new(u64::MAX, ksp_interface_lib::SlotLifecycleStage::Rooted);
assert_eq!(lifecycle.slot(), u64::MAX);
assert_eq!(lifecycle.stage(), ksp_interface_lib::SlotLifecycleStage::Rooted);
let signature = ksp_interface_lib::TransactionSignature::new([0xB4_u8; 64]);
let execution = ksp_interface_lib::TransactionExecutionEvent::new(u64::MAX, signature, ksp_interface_lib::TransactionExecutionOutcome::Succeeded);
assert_eq!(execution.slot(), u64::MAX);
assert_eq!(execution.signature(), signature);
assert_eq!(execution.outcome(), ksp_interface_lib::TransactionExecutionOutcome::Succeeded);
return;
}
#[test]
fn v0_3_5_pre_004_exact_crate_root_export_inventory_is_stable() -> std::result::Result<(), std::io::Error> {
let source = match crate_root_source() {
Ok(source) => source,
Err(error) => return Err(error),
};
let public_use_count = source.lines().filter(|line| return line.starts_with("pub use ")).count();
assert_eq!(public_use_count, 11);
assert!(source.contains("pub use self::error::ERROR_CODE_PROGRAM_INSTRUCTION_LIMIT_EXCEEDED;"));
assert!(source.contains("pub use self::program_account_meta::MAX_PROGRAM_INSTRUCTION_ACCOUNTS;"));
assert!(source.contains("pub use self::program_account_meta::ProgramAccountMeta;"));
assert!(source.contains("pub use self::program_instruction::MAX_PROGRAM_INSTRUCTION_DATA_LEN;"));
assert!(source.contains("pub use self::program_instruction::ProgramInstruction;"));
assert!(source.contains("pub use self::slot_lifecycle::SlotLifecycleEvent;"));
assert!(source.contains("pub use self::slot_lifecycle::SlotLifecycleStage;"));
assert!(source.contains("pub use self::transaction_execution::TransactionExecutionEvent;"));
assert!(source.contains("pub use self::transaction_execution::TransactionExecutionOutcome;"));
assert!(source.contains("pub use self::transaction_execution::TransactionSignature;"));
assert!(source.contains("pub use ksp_core_lib::Pubkey;"));
return Ok(());
}
#[test]
fn v0_3_5_pre_004_production_module_inventory_is_exact() -> std::result::Result<(), std::io::Error> {
let source = match crate_root_source() {
Ok(source) => source,
Err(error) => return Err(error),
};
let modules = source
.lines()
.filter_map(|line| return line.strip_prefix("mod ").and_then(|module| return module.strip_suffix(';')))
.collect::<std::vec::Vec<_>>();
assert_eq!(modules, std::vec!["error", "program_account_meta", "program_instruction", "slot_lifecycle", "transaction_execution"]);
assert!(!source.contains("serde"));
assert!(!source.contains("tracing"));
assert!(!source.contains("ksp_onchain_transport"));
assert!(!source.contains("ksp_store"));
return Ok(());
}
#[test]
fn v0_3_5_pre_004_evolutive_acquisition_enums_remain_non_exhaustive() {
let lifecycle_source = include_str!("../src/slot_lifecycle.rs");
let execution_source = include_str!("../src/transaction_execution.rs");
assert!(lifecycle_source.contains("#[non_exhaustive]\npub enum SlotLifecycleStage"));
assert!(execution_source.contains("#[non_exhaustive]\npub enum TransactionExecutionOutcome"));
return;
}

View File

@@ -0,0 +1,100 @@
// file: crates/ksp-interface-lib/tests/security_hardening.rs
// version: 1
//! Adversarial and ownership-hardening canaries for passive Interface acquisition events.
const HOSTILE_MARKER: &str = "INTERFACE-ACQUISITION-SECRET-CANARY";
#[test]
fn v0_3_5_pre_005_signature_and_event_debug_never_render_signature_bytes() {
let mut bytes = [0x41_u8; 64];
bytes[..HOSTILE_MARKER.len()].copy_from_slice(HOSTILE_MARKER.as_bytes());
let signature = ksp_interface_lib::TransactionSignature::new(bytes);
assert_eq!(signature.as_bytes(), &bytes);
let signature_debug = std::format!("{signature:?}");
assert_eq!(signature_debug, "TransactionSignature(..)");
assert!(!signature_debug.contains(HOSTILE_MARKER));
let event = ksp_interface_lib::TransactionExecutionEvent::new(u64::MAX, signature, ksp_interface_lib::TransactionExecutionOutcome::Failed);
let event_debug = std::format!("{event:?}");
assert!(event_debug.contains("TransactionExecutionEvent"));
assert!(event_debug.contains("18446744073709551615"));
assert!(event_debug.contains("Failed"));
assert!(!event_debug.contains(HOSTILE_MARKER));
return;
}
#[test]
fn v0_3_5_pre_005_acquisition_structs_keep_exact_minimal_fields_without_payload_or_source_metadata() {
let lifecycle_source = include_str!("../src/slot_lifecycle.rs");
let execution_source = include_str!("../src/transaction_execution.rs");
assert!(lifecycle_source.contains("pub struct SlotLifecycleEvent {\n slot: u64,\n stage: SlotLifecycleStage,\n}"));
assert!(execution_source.contains("pub struct TransactionSignature([u8; 64]);"));
assert!(execution_source.contains(
"pub struct TransactionExecutionEvent {\n slot: u64,\n signature: TransactionSignature,\n outcome: TransactionExecutionOutcome,\n}"
));
for source in [lifecycle_source, execution_source] {
for forbidden in [
"std::vec::Vec<",
"std::string::String",
"Box<",
"serde_json::Value",
"timestamp:",
"provider_id:",
"endpoint:",
"filter:",
"network:",
"payload:",
] {
assert!(!source.contains(forbidden), "forbidden acquisition field/surface detected: {forbidden}");
}
}
return;
}
#[test]
fn v0_3_5_pre_005_interface_exports_no_second_raw_or_deferred_event_family() {
let crate_root = include_str!("../src/lib.rs");
for forbidden in [
"RawTransaction",
"RawAccount",
"RawPayload",
"RawPageCursor",
"RawRetention",
"RawObservation",
"TransactionLogEvent",
"TransactionCommitmentEvent",
"LedgerEntryEvent",
"VoteEvent",
"BlockEvent",
] {
assert!(!crate_root.contains(forbidden), "forbidden Interface export detected: {forbidden}");
}
return;
}
#[test]
fn v0_3_5_pre_005_program_instruction_foundation_remains_bounded_and_unchanged() {
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS, 255);
assert_eq!(ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN, 10_240);
let program_id = ksp_interface_lib::Pubkey::new_from_array([0xD1_u8; 32]);
let account = ksp_interface_lib::ProgramAccountMeta::readonly(ksp_interface_lib::Pubkey::new_from_array([0xD2_u8; 32]), false);
let admitted = ksp_interface_lib::ProgramInstruction::try_new(
program_id,
std::vec![account; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS],
std::vec![0x5A_u8; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN],
);
assert!(admitted.is_ok());
let admitted = match admitted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(admitted.accounts().len(), ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS);
assert_eq!(admitted.data().len(), ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN);
let too_many_accounts =
ksp_interface_lib::ProgramInstruction::try_new(program_id, std::vec![account; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_ACCOUNTS + 1], std::vec![]);
assert!(too_many_accounts.is_err());
let oversized_data =
ksp_interface_lib::ProgramInstruction::try_new(program_id, std::vec![], std::vec![0x5A_u8; ksp_interface_lib::MAX_PROGRAM_INSTRUCTION_DATA_LEN + 1]);
assert!(oversized_data.is_err());
return;
}

View File

@@ -0,0 +1,29 @@
// file: crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
// version: 2
//! Public API canaries for the shared slot lifecycle event contract.
#[test]
fn public_v0_3_5_pre_002_slot_lifecycle_contract_is_available_from_crate_root() {
let event = ksp_interface_lib::SlotLifecycleEvent::new(u64::MAX, ksp_interface_lib::SlotLifecycleStage::Rooted);
assert_eq!(event.slot(), u64::MAX);
assert_eq!(event.stage(), ksp_interface_lib::SlotLifecycleStage::Rooted);
}
#[test]
fn public_v0_3_5_pre_002_slot_lifecycle_stage_remains_downstream_evolvable() {
fn stage_label(stage: ksp_interface_lib::SlotLifecycleStage) -> &'static str {
return match stage {
ksp_interface_lib::SlotLifecycleStage::Processed => "processed",
ksp_interface_lib::SlotLifecycleStage::FirstShredReceived => "first_shred_received",
ksp_interface_lib::SlotLifecycleStage::Completed => "completed",
ksp_interface_lib::SlotLifecycleStage::CreatedBank => "created_bank",
ksp_interface_lib::SlotLifecycleStage::Dead => "dead",
ksp_interface_lib::SlotLifecycleStage::OptimisticallyConfirmed => "optimistically_confirmed",
ksp_interface_lib::SlotLifecycleStage::Rooted => "rooted",
_ => "future",
};
}
assert_eq!(stage_label(ksp_interface_lib::SlotLifecycleStage::Processed), "processed");
assert_eq!(stage_label(ksp_interface_lib::SlotLifecycleStage::Rooted), "rooted");
}

View File

@@ -0,0 +1,28 @@
// file: crates/ksp-interface-lib/tests/transaction_execution_public_api.rs
// version: 1
//! Public API canaries for the provider-neutral transaction execution contract.
#[test]
fn public_v0_3_5_pre_003_transaction_execution_contract_is_available_from_crate_root() {
let signature = ksp_interface_lib::TransactionSignature::new([0xC7_u8; 64]);
let event = ksp_interface_lib::TransactionExecutionEvent::new(u64::MAX, signature, ksp_interface_lib::TransactionExecutionOutcome::Succeeded);
assert_eq!(signature.as_bytes(), &[0xC7_u8; 64]);
assert_eq!(event.slot(), u64::MAX);
assert_eq!(event.signature(), signature);
assert_eq!(event.outcome(), ksp_interface_lib::TransactionExecutionOutcome::Succeeded);
}
fn outcome_label(outcome: ksp_interface_lib::TransactionExecutionOutcome) -> &'static str {
return match outcome {
ksp_interface_lib::TransactionExecutionOutcome::Succeeded => "succeeded",
ksp_interface_lib::TransactionExecutionOutcome::Failed => "failed",
_ => "future",
};
}
#[test]
fn public_v0_3_5_pre_003_transaction_execution_outcome_remains_downstream_evolvable() {
assert_eq!(outcome_label(ksp_interface_lib::TransactionExecutionOutcome::Succeeded), "succeeded");
assert_eq!(outcome_label(ksp_interface_lib::TransactionExecutionOutcome::Failed), "failed");
}

View File

@@ -0,0 +1,45 @@
// file: crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
// version: 2
#[test]
fn slot_lifecycle_stages_are_distinct_copy_and_complete_for_the_admitted_family() {
fn assert_copy<T: Copy>() {}
assert_copy::<crate::SlotLifecycleStage>();
let stages = [
crate::SlotLifecycleStage::Processed,
crate::SlotLifecycleStage::FirstShredReceived,
crate::SlotLifecycleStage::Completed,
crate::SlotLifecycleStage::CreatedBank,
crate::SlotLifecycleStage::Dead,
crate::SlotLifecycleStage::OptimisticallyConfirmed,
crate::SlotLifecycleStage::Rooted,
];
for (index, stage) in stages.iter().enumerate() {
for other in stages.iter().skip(index + 1) {
assert_ne!(stage, other);
}
}
}
#[test]
fn slot_lifecycle_event_preserves_full_u64_slot_and_stage() {
fn assert_copy<T: Copy>() {}
assert_copy::<crate::SlotLifecycleEvent>();
let event = crate::SlotLifecycleEvent::new(u64::MAX, crate::SlotLifecycleStage::Rooted);
assert_eq!(event.slot(), u64::MAX);
assert_eq!(event.stage(), crate::SlotLifecycleStage::Rooted);
let copied = event;
assert_eq!(event, copied);
}
#[test]
fn slot_lifecycle_debug_is_bounded_and_contains_only_shared_fields() {
let event = crate::SlotLifecycleEvent::new(42, crate::SlotLifecycleStage::OptimisticallyConfirmed);
let debug = std::format!("{event:?}");
assert!(debug.len() <= 128);
assert!(debug.contains("slot: 42"));
assert!(debug.contains("OptimisticallyConfirmed"));
assert!(!debug.contains("provider"));
assert!(!debug.contains("yellowstone"));
assert!(!debug.contains("websocket"));
}

View File

@@ -0,0 +1,28 @@
// file: crates/ksp-interface-lib/unit_tests/transaction_execution.rs
// version: 1
#[test]
fn transaction_signature_preserves_exact_bytes_and_redacts_debug() {
let signature = crate::TransactionSignature::new([0xA5_u8; 64]);
assert_eq!(signature.as_bytes(), &[0xA5_u8; 64]);
assert_eq!(std::format!("{signature:?}"), "TransactionSignature(..)");
}
#[test]
fn transaction_execution_outcomes_are_distinct_and_copy() {
let succeeded = crate::TransactionExecutionOutcome::Succeeded;
let failed = crate::TransactionExecutionOutcome::Failed;
assert_ne!(succeeded, failed);
let copied = succeeded;
assert_eq!(copied, crate::TransactionExecutionOutcome::Succeeded);
}
#[test]
fn transaction_execution_event_preserves_full_slot_signature_and_outcome() {
let signature = crate::TransactionSignature::new([0xB6_u8; 64]);
let event = crate::TransactionExecutionEvent::new(u64::MAX, signature, crate::TransactionExecutionOutcome::Failed);
assert_eq!(event.slot(), u64::MAX);
assert_eq!(event.signature(), signature);
assert_eq!(event.outcome(), crate::TransactionExecutionOutcome::Failed);
assert_eq!(std::format!("{event:?}"), "TransactionExecutionEvent { slot: 18446744073709551615, signature: \"<omitted>\", outcome: Failed }");
}

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,13 +540,15 @@ 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;
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,191 @@
<!-- file: deltas/0.3.5/pre.001-fix.001.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.001-fix.001` — correction du gate Interface acquisition events
## Base requise
```text
0.3.5-pre.001
workspace.package.version = 0.3.5-pre.1
```
Ce fix est strictement documentaire. Conformément au workflow KSP, il ne modifie pas `workspace.package.version` et ne touche aucun fichier Cargo/Rust/runtime/config.
## Motif du fix
Le brainstorming post-gate a identifié deux décisions trop conservatrices dans `pre.001` :
1. `OptimisticConfirmation <-> Yellowstone Confirmed` et `Root <-> Yellowstone Finalized` représentent bien les mêmes faits de lifecycle lorsqu'ils sont normalisés avec des noms KSP qui conservent la sémantique source ;
2. la catégorie générique « transaction status » mélangeait snapshot, commitment et exécution, masquant un candidat event-only transversal possible.
## Corrections de conception
### Slot lifecycle
L'intersection admise devient :
```text
SlotLifecycleStage
+ Processed
+ FirstShredReceived
+ Completed
+ CreatedBank
+ Dead
+ OptimisticallyConfirmed
+ Rooted
```
Mappings corrigés :
```text
Solana slotsUpdates::OptimisticConfirmation
-> SlotLifecycleStage::OptimisticallyConfirmed
<- Yellowstone SlotStatus::Confirmed
Solana rootSubscribe / slotsUpdates::Root
-> SlotLifecycleStage::Rooted
<- Yellowstone SlotStatus::Finalized
```
Les noms `OptimisticallyConfirmed` et `Rooted` évitent de promouvoir les labels Yellowstone `Confirmed`/`Finalized` en terminologie KSP universelle. `Frozen` reste hors intersection.
### Transaction execution
`TransactionExecutionEvent` est rouvert comme **candidat actif de `0.3.5`**, conceptuellement :
```text
TransactionExecutionEvent {
slot,
signature,
outcome = Succeeded | Failed,
}
```
Producers candidats :
```text
Solana logsSubscribe
Yellowstone TransactionStatus
Helius transactionSubscribe
```
Le type n'est pas encore admis. `pre.003` doit d'abord auditer la représentation KSP de signature, les mappings exacts, l'anti-duplication Store et le consumer concret. Aucun type partiel ne doit être créé si ce gate échoue.
### Logs et commitment
Les familles sont désormais explicitement séparées :
```text
TransactionLogEvent
-> idée différée ; bornes/consumer/multi-producer à démontrer
getSignatureStatuses
-> snapshot Transport
signatureSubscribe
-> transition one-shot de commitment Transport
TransactionCommitmentEvent
-> non admis ; ne jamais fusionner avec TransactionExecutionEvent par Option soup
```
### Extensions provider/protocol
La règle d'ownership est clarifiée :
```text
DTO provider/protocol spécifique
-> Transport
fait passif provider-neutral réellement commun
-> Interface après gate sémantique
```
Ainsi `Helius transactionSubscribe` et Yellowstone `Entry` restent Transport-owned comme DTOs. Leur caractère non standard n'interdit pas qu'un futur fait commun qu'ils permettent d'observer soit Interface-owned.
## Traçabilité TODO/IDEAS
Le fix ne crée pas artificiellement une entrée durable séparée pendant que le gate `0.3.5` est encore actif :
```text
TransactionExecutionEvent -> suivi actif dans plan/validation 0.3.5
TransactionLogEvent -> idée différée explicitement tracée dans plan/validation
```
Si `TransactionLogEvent` reste hors scope à la réconciliation documentaire finale, il devra être transféré dans le TODO/IDEAS durable approprié.
## Plan de prereleases corrigé
```text
pre.002 implémentation SlotLifecycleEvent/Stage à 7 stages
pre.003 gate TransactionExecutionEvent + implémentation seulement si gate complet
pre.004 canaris externes + complétude API
pre.005 hardening final
pre.006 gate technique final
pre.007 réconciliation documentaire finale
pre.008 préparation de publication
rel.001 stable 0.3.5
```
## Gate opérateur reçu pour `pre.001`
Avant ce fix, l'opérateur a rejoué :
```text
cargo clean
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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
```
Résultat fourni :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (251 table(s), 137 file(s))
cargo check --workspace: PASS
cargo clippy --workspace --all-targets: PASS
cargo test -p ksp-interface-lib: PASS
```
Les tests Interface détaillés fournis sont tous propres : 10 unitaires, 4 dependency-boundary, 1 external-consumer, 5 public-api, 3 release-completeness, aucun échec de doc-test.
## Fichiers modifiés
```text
docs/plans/026-V0_3_5_INTERFACE_ACQUISITION_EVENTS_PLAN.md
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
```
## Fichiers ajoutés
```text
deltas/0.3.5/pre.001-fix.001.md
```
## Fichiers explicitement inchangés
```text
Cargo.toml
crates/**
README.md
ROADMAP.md
CHANGELOG.md
```
## Gate demandé après application du fix
Le fix ne change que Markdown. Rejouer :
```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.5
```
Aucun rerun Cargo n'est requis par le contenu du fix lui-même.

119
deltas/0.3.5/pre.001.md Normal file
View File

@@ -0,0 +1,119 @@
<!-- file: deltas/0.3.5/pre.001.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.001` — audit et planification Interface acquisition events
## Base requise
```text
v0.3.4
workspace.package.version = 0.3.4
```
Le log opérateur fourni avec l'archive d'entrée montre le gate stable `v0.3.4` propre. Cette tranche part exclusivement de l'archive `khadhroony-solana-project-v0.3.4.zip` fournie par l'opérateur.
## Objectif
Exécuter le gate `pre.001` prescrit par `prompts/024-V0_3_5_START_PROMPT.md` : lecture, inventaire producer/consumer, audit sémantique multi-transport actuel, ownership Interface/Transport/Store, threat model, sizing et planification.
Aucun modèle Rust n'est implémenté dans cette tranche.
## Décisions prises
Une seule famille passe le gate d'admission Interface :
```text
SlotLifecycleEvent
+ Processed
+ FirstShredReceived
+ Completed
+ CreatedBank
+ Dead
```
Cette intersection correspond uniquement aux faits communs démontrés entre Solana WebSocket et Yellowstone. Les metadata riches restent Transport.
Décisions négatives :
```text
Root -X-> Finalized
OptimisticConfirmation -X-> Confirmed
logsSubscribe -> reporté
TransactionStatusObservation -> reporté
vote -> reporté
account/program -> Store/Transport
transaction/block -> Store/Transport
Yellowstone Entry -> Transport
Helius Enhanced transaction -> Transport
notification after commit -> Store API selon KSP-NOTIFY-001..006
```
La release est redimensionnée autour d'une seule famille et réserve explicitement les couloirs hardening, gate technique final, réconciliation documentaire puis publication.
## Fichiers ajoutés
```text
docs/plans/026-V0_3_5_INTERFACE_ACQUISITION_EVENTS_PLAN.md
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
deltas/0.3.5/pre.001.md
```
## Fichiers modifiés
```text
Cargo.toml
```
Modifications Cargo :
```text
header version: 382 -> 383
workspace.package.version: 0.3.4 -> 0.3.5-pre.1
```
## Fichiers supprimés
Aucun.
## 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.5
-> Markdown table audit: clean (251 table(s), 137 file(s))
```
## Validations non exécutées dans l'environnement d'assemblage
`cargo` n'est pas installé dans l'environnement d'assemblage. Les commandes suivantes ne sont donc pas déclarées réussies :
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
```
Elles doivent être rejouées par l'opérateur après application de l'overlay.
## Questions ouvertes
Aucune question bloquante pour `pre.002`.
Les familles logs/status/vote/root restent volontairement reportées et ne doivent être rouvertes que si un nouveau producer/consumer concret apporte une convergence qui n'existe pas dans ce gate.
## Suite prévue
```text
pre.002 implémentation SlotLifecycleEvent/Stage
pre.003 canaris externes + complétude API
pre.004 hardening final
pre.005 gate technique final
pre.006 réconciliation documentaire finale
pre.007 préparation de publication
rel.001 stable 0.3.5
```

View File

@@ -0,0 +1,67 @@
# Delta `0.3.5-pre.002-fix.001`
## Objet
Corriger les défauts de conformité Rust révélés par le gate opérateur de `0.3.5-pre.002`, sans modifier le contrat fonctionnel `SlotLifecycleEvent` / `SlotLifecycleStage`.
## Base
```text
0.3.5-pre.002
workspace.package.version = 0.3.5-pre.2
```
## Diagnostic opérateur
Le code fonctionnel et les tests ciblés passent, mais le gate global n'est pas propre :
- 15 violations `RUST-FMT-101` pour des lignes vides à l'intérieur de fonctions de tests ;
- 2 usages de l'opérateur `?` rejetés par `clippy::question_mark_used` ;
- closures à retour implicite rejetées par `clippy::implicit_return` ;
- documentation crate-level manquante dans les deux nouveaux tests d'intégration.
## Correction
- supprimer les lignes vides internes aux fonctions concernées ;
- remplacer `?` par une propagation explicite via `match` ;
- rendre explicites les `return` des closures concernées ;
- ajouter la documentation crate-level aux tests d'intégration ;
- conserver strictement les sept variantes de `SlotLifecycleStage` et la forme de `SlotLifecycleEvent` ;
- ne pas ajouter de `allow`, de dépendance, de codec, de runtime ou de conversion Transport.
## Version
Le fix modifie des fichiers Rust ; la version workspace est donc incrémentée suivant la convention de fix prerelease :
```text
workspace.package.version = 0.3.5-pre.2.fix.1
```
Le numéro documentaire reste :
```text
0.3.5-pre.002-fix.001
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-interface-lib/tests/release_completeness.rs
crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
deltas/0.3.5/pre.002-fix.001.md
```
## Gate 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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```

180
deltas/0.3.5/pre.002.md Normal file
View File

@@ -0,0 +1,180 @@
<!-- file: deltas/0.3.5/pre.002.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.002` — `SlotLifecycleEvent`
## Base requise
```text
0.3.5-pre.001-fix.001
workspace.package.version = 0.3.5-pre.1
```
Gate d'entrée opérateur reçu après le fix :
```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), 138 file(s))
```
## Objectif
Matérialiser uniquement la famille passive provider-neutral `SlotLifecycleEvent` admise par le gate corrigé de `pre.001`, sans ouvrir la famille candidate `TransactionExecutionEvent` et sans déplacer les DTOs protocol/provider hors de Transport.
## Version
Cette tranche est une nouvelle prerelease non-fix. La version workspace devient :
```text
0.3.5-pre.2
```
Le manifest `crates/ksp-interface-lib/Cargo.toml` reste inchangé et continue d'hériter de la version workspace.
## Implémentation
Nouvelle surface crate-root :
```text
SlotLifecycleStage
SlotLifecycleEvent
```
`SlotLifecycleStage` est `#[non_exhaustive]` et contient exactement :
```text
Processed
FirstShredReceived
Completed
CreatedBank
Dead
OptimisticallyConfirmed
Rooted
```
`SlotLifecycleEvent` reste volontairement minimal :
```text
slot: u64
stage: SlotLifecycleStage
```
avec uniquement :
```text
SlotLifecycleEvent::new(slot, stage)
SlotLifecycleEvent::slot()
SlotLifecycleEvent::stage()
```
Aucune validation faillible n'est ajoutée : tout `u64` est un slot admissible au niveau de ce contrat passif.
## Ownership et dépendances
Préservé :
```text
ksp-interface-lib -> ksp-core-lib
```
Absent :
```text
serde
codec wire
tracing/logging runtime
tokio/channel/event bus
ksp-onchain-transport-lib
ksp-store-api
ksp-store-lib
ksp-store-postgres-lib
```
Aucun converter depuis les DTOs Solana WS, Yellowstone ou Helius n'est ajouté dans Interface. Les conversions appartiennent à la future composition/consumer.
## Canaris
Ajoutés :
```text
crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
```
Le premier verrouille les 7 stages, le domaine `u64`, les traits passifs retenus et un `Debug` borné/provider-neutral. Le second vérifie la consommation depuis crate root et la capacité d'évolution downstream de l'enum non-exhaustive.
Le canari historique `tests/release_completeness.rs` est ajusté afin que son inventaire exact accepte la seule extension prévue de cette tranche : `slot_lifecycle` et ses deux reexports publics.
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-interface-lib/src/lib.rs
crates/ksp-interface-lib/tests/release_completeness.rs
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
```
## Fichiers ajoutés
```text
crates/ksp-interface-lib/src/slot_lifecycle.rs
crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
deltas/0.3.5/pre.002.md
```
## Fichiers explicitement hors scope
```text
crates/ksp-interface-lib/Cargo.toml
crates/ksp-onchain-transport-lib/**
crates/ksp-store-api/**
crates/ksp-store-lib/**
crates/ksp-store-postgres-lib/**
README.md
ROADMAP.md
CHANGELOG.md
```
## Validations exécutées dans l'environnement d'assemblage
Validations statiques de l'overlay :
```text
version workspace = 0.3.5-pre.2
nouvelle source Interface sans serde/tracing/tokio/Transport/Store
crate-root = 4 modules privés / 8 reexports publics attendus
SlotLifecycleStage = 7 variantes prévues
aucun TransactionExecutionEvent introduit
archive = fichiers modifiés/ajoutés uniquement
```
L'environnement d'assemblage ne fournit pas le workspace Cargo complet exécutable ; aucun `cargo check`, Clippy ou test Rust de cette tranche n'est donc déclaré réussi ici.
## 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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```
`ksp-program-api` est inclus car il est le consumer actuel direct de `ksp-interface-lib`, même si `pre.002` ne modifie aucun contrat Program existant.
## Questions ouvertes
Aucune question ne bloque `SlotLifecycleEvent`.
Le seul gate fonctionnel suivant reste `TransactionExecutionEvent` : représentation de signature, convergence exacte Solana/Yellowstone/Helius, anti-duplication Store et consumer concret doivent être prouvés en `pre.003` avant toute implémentation.
## Suite
```text
0.3.5-pre.003 — gate TransactionExecutionEvent ; implémentation uniquement si le gate est complet
```

193
deltas/0.3.5/pre.003.md Normal file
View File

@@ -0,0 +1,193 @@
<!-- file: deltas/0.3.5/pre.003.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.003` — `TransactionExecutionEvent`
## Base requise
```text
0.3.5-pre.2.fix.1
```
Le gate opérateur de cette base confirme les audits Rust, `cargo check --workspace`, Clippy, `cargo test -p ksp-interface-lib` et `cargo test -p ksp-program-api`. Le seul défaut restant est une double ligne vide dans `docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md`; conformément à la décision opérateur, cette correction documentaire est absorbée par le présent delta au lieu de créer un fix dédié.
## Version
Le delta modifie la surface Rust Interface. La version workspace devient :
```text
0.3.5-pre.3
```
## Mission
Fermer le gate dédié à `TransactionExecutionEvent` prévu par le plan corrigé de `0.3.5` : représentation de signature, convergence Solana/Yellowstone/Helius, anti-duplication Store et consumer concret doivent tous être démontrés avant création du type.
## Décision de gate
Le gate passe avec une projection strictement minimale :
```text
TransactionSignature([u8; 64])
TransactionExecutionOutcome {
Succeeded,
Failed,
}
TransactionExecutionEvent {
slot,
signature,
outcome,
}
```
La convergence retenue est :
```text
Solana logsSubscribe
context.slot + signature + err
Yellowstone TransactionStatus
slot + signature + err
Helius transactionSubscribe
slot + signature + résultat d'exécution seulement lorsque la forme/detail mode le rend déterminable
```
`err` n'est jamais copié dans Interface. Solana `err: null`, Yellowstone `err: None` et un Helius `err: null` explicitement disponible donnent `Succeeded`; une erreur explicite donne `Failed`.
Pour Helius, la projection est conservative : un `err` omis, une forme `none`, inconnue ou sans information d'erreur exploitable ne produit pas artificiellement un `TransactionExecutionEvent`.
## Signature
`TransactionSignature` possède exactement 64 octets déjà décodés.
Interface n'ajoute :
```text
aucun base58
aucun serde
aucun codec
aucune crate Solana/Anza supplémentaire
aucune capability Wallet de signature
```
Le `Debug` est redacted et ne rend jamais les bytes.
`ksp-store-api::RawTransactionSignature` reste Store-owned. Aucun alias/reexport/converter cross-domain n'est ajouté ; la future composition convertit explicitement les 64 octets si nécessaire.
## Ownership
Restent Transport-owned :
```text
Solana logs notification DTO
Yellowstone TransactionStatus DTO
Helius transactionSubscribe DTOs
commitment
subscription/filter metadata
transaction index
logs
memo
block time
confirmation status
provider timestamps
opaque transaction errors
```
Interface possède uniquement le fait passif commun.
## Consumer
Le futur worker RAW live peut utiliser `TransactionExecutionEvent` comme signal provider-neutral d'exécution observée et décider ensuite d'hydrater/persister un `RawTransaction` par sa propre composition/policy.
Le premier backfill historique `0.3.6` reste indépendant de cette API.
## Surface Rust
Ajouts crate-root :
```text
TransactionSignature
TransactionExecutionOutcome
TransactionExecutionEvent
```
`TransactionExecutionOutcome` est `#[non_exhaustive]`.
Les champs des structs restent privés et la surface ne possède ni runtime ni comportement réseau.
## Tests/canaris
Ajoutés :
```text
unit_tests/transaction_execution.rs
tests/transaction_execution_public_api.rs
```
Le canari `tests/release_completeness.rs` évolue pour figer :
```text
5 modules privés de production
11 reexports crate-root
transaction_execution comme seule nouvelle famille pre.003
absence de serde/tracing/Transport/Store dans la racine Interface
```
## Documentation
Le plan et la validation passent `TransactionExecutionEvent` de candidat actif à `ADMIS pre.003`.
`TransactionLogEvent` reste une idée différée distincte. `TransactionCommitmentEvent` reste non admis. Yellowstone `Entry` et les DTOs Helius restent Transport-owned même lorsqu'ils pourront contribuer à de futurs faits Interface réellement transversaux.
La double ligne vide signalée par le gate opérateur précédent est supprimée dans la validation.
## Fichiers
Modifiés :
```text
Cargo.toml
crates/ksp-interface-lib/src/lib.rs
crates/ksp-interface-lib/tests/release_completeness.rs
docs/plans/026-V0_3_5_INTERFACE_ACQUISITION_EVENTS_PLAN.md
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
```
Ajoutés :
```text
crates/ksp-interface-lib/src/transaction_execution.rs
crates/ksp-interface-lib/unit_tests/transaction_execution.rs
crates/ksp-interface-lib/tests/transaction_execution_public_api.rs
deltas/0.3.5/pre.003.md
```
Explicitement inchangés :
```text
crates/ksp-interface-lib/Cargo.toml
crates/ksp-onchain-transport-lib/**
crates/ksp-store-api/**
crates/ksp-store-lib/**
crates/ksp-store-postgres-lib/**
README.md
ROADMAP.md
CHANGELOG.md
```
## Gate 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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```
Les gates Cargo ne sont pas déclarés réussis par l'environnement d'assemblage.

153
deltas/0.3.5/pre.004.md Normal file
View File

@@ -0,0 +1,153 @@
<!-- file: deltas/0.3.5/pre.004.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.004` — canaris externes et complétude API
## Base requise
```text
0.3.5-pre.3
```
Le gate opérateur de `pre.003` est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests `ksp-interface-lib` et `ksp-program-api` passent.
## Version
La tranche modifie des sources Rust de test/canari. La version workspace devient :
```text
0.3.5-pre.4
```
## Mission
Verrouiller la surface des deux familles passives admises sans ajouter de comportement ni de type de production :
```text
SlotLifecycleEvent / SlotLifecycleStage
TransactionExecutionEvent / TransactionExecutionOutcome / TransactionSignature
```
## Consumer externe
`tests/external_consumer.rs` consomme maintenant les deux familles acquisition uniquement depuis le crate-root public.
Le canari prouve notamment qu'un consumer downstream peut récupérer :
```text
slot + stage
slot + signature + outcome
```
sans accès aux modules privés, à Transport ou à Store.
Le canari Program historique reste conservé.
## Inventaire exact
`tests/release_completeness.rs` fige :
```text
5 modules privés de production
11 reexports crate-root
2 familles passives d'acquisition exactement
```
Aucun nouveau module ni export de production n'est ajouté par `pre.004`.
## Evolutivité
Les deux enums publics évolutifs restent explicitement `#[non_exhaustive]` :
```text
SlotLifecycleStage
TransactionExecutionOutcome
```
Les canaris publics downstream de `pre.002` et `pre.003` restent inchangés et continuent d'utiliser un fallback `_`.
## Firewalls négatifs
Le scan de production inclut désormais les deux modules acquisition.
Un canari spécifique interdit dans `slot_lifecycle.rs` et `transaction_execution.rs` les paths de :
```text
Config
Logging
Transport
Store
reqwest
serde
Solana instruction
Tauri
Tokio
Tonic
tracing
borsh/bincode/wincode
```
Le manifeste `ksp-interface-lib` reste Core-only et inchangé.
## Hors scope
Toujours interdits/reportés :
```text
converters Transport -> Interface
provider DTOs
Store RAW
serde/codecs
runtime/channels
logging
TransactionLogEvent
TransactionCommitmentEvent
```
## Fichiers
Modifiés :
```text
Cargo.toml
crates/ksp-interface-lib/tests/dependency_boundary.rs
crates/ksp-interface-lib/tests/external_consumer.rs
crates/ksp-interface-lib/tests/release_completeness.rs
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
```
Ajouté :
```text
deltas/0.3.5/pre.004.md
```
Explicitement inchangés :
```text
crates/ksp-interface-lib/Cargo.toml
crates/ksp-interface-lib/src/**
crates/ksp-interface-lib/unit_tests/**
crates/ksp-onchain-transport-lib/**
crates/ksp-store-api/**
crates/ksp-store-lib/**
crates/ksp-store-postgres-lib/**
crates/ksp-program-api/**
README.md
ROADMAP.md
CHANGELOG.md
```
## Gate 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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```
Les gates Cargo restent à exécuter par l'opérateur.

View File

@@ -0,0 +1,78 @@
# Delta `0.3.5-pre.005-fix.001`
## Objet
Corriger le faux négatif du canari de manifeste ajouté par `0.3.5-pre.005`, sans modifier le contrat fonctionnel ni le graphe de dépendances de `ksp-interface-lib`.
## Base
```text
0.3.5-pre.005
workspace.package.version = 0.3.5-pre.5
```
## Diagnostic opérateur
Le gate du `2026-08-31` est propre pour les audits Rust/Markdown et `cargo check --workspace`, mais le nouveau test :
```text
v0_3_5_pre_005_manifest_keeps_exact_core_only_graph_without_features_or_hidden_dependency_sections
```
échoue avec :
```text
left: 2
right: 1
```
La cause est le comptage textuel `manifest.matches("ksp-core-lib")` appliqué à la déclaration :
```text
ksp-core-lib = { path = "../ksp-core-lib" }
```
Cette ligne contient légitimement deux occurrences de la chaîne tout en déclarant une seule dépendance. Le manifeste lui-même est correct.
## Correction
- conserver les interdictions de sections `[features]`, `[dev-dependencies]` et `[build-dependencies]` ;
- conserver l'exigence d'une seule section `[dependencies]` ;
- extraire cette section puis réutiliser `manifest_dependency_names(...)` ;
- vérifier l'inventaire exact des clés de dépendance : `ksp-core-lib` uniquement ;
- ne modifier aucun manifeste de crate, source de production, type public ou dependency graph.
## Version
Le fix modifie une source Rust de test ; la version workspace devient :
```text
workspace.package.version = 0.3.5-pre.5.fix.1
```
Le numéro documentaire reste :
```text
0.3.5-pre.005-fix.001
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-interface-lib/tests/dependency_boundary.rs
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
deltas/0.3.5/pre.005-fix.001.md
```
## Gate 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.5
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```

138
deltas/0.3.5/pre.005.md Normal file
View File

@@ -0,0 +1,138 @@
<!-- file: deltas/0.3.5/pre.005.md -->
<!-- version: 1 -->
# Delta `0.3.5-pre.005` — hardening et complétude Interface
## Base requise
```text
0.3.5-pre.4
```
Le gate opérateur de `pre.004` est entièrement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, tests `ksp-interface-lib` et `ksp-program-api` passent.
## Version
La tranche ajoute/modifie des sources Rust de test/canari. La version workspace devient :
```text
0.3.5-pre.5
```
## Mission
Fermer le hardening fonctionnel de `0.3.5` sans ajouter de troisième famille Interface ni modifier les contrats de production admis :
```text
SlotLifecycleEvent / SlotLifecycleStage
TransactionExecutionEvent / TransactionExecutionOutcome / TransactionSignature
```
## Hardening adversarial
Le nouveau `tests/security_hardening.rs` vérifie :
- conservation byte-exact d'une signature hostile de 64 octets ;
- redaction de la signature dans son `Debug` et dans celui de `TransactionExecutionEvent` ;
- conservation de `u64::MAX` sans narrowing ;
- structure minimale exacte des deux events ;
- absence de collection/payload/timestamp/provider/endpoint/filter/network dans les contrats acquisition ;
- absence de second RAW ou de famille reportée exportée silencieusement ;
- non-régression des bornes `ProgramInstruction` historiques.
## Graphe et dépendances
`tests/dependency_boundary.rs` ajoute un canari final imposant :
```text
une seule section [dependencies]
exactement une référence ksp-core-lib
aucune [features]
aucune [dev-dependencies]
aucune [build-dependencies]
```
Le manifeste `ksp-interface-lib` reste inchangé et Core-only.
## Candidats reportés
Aucun candidat reporté n'est promu par hardening :
```text
TransactionLogEvent -> idée différée
TransactionCommitmentEvent -> reporté
VoteEvent -> reporté
LedgerEntryEvent -> reporté
Account/Program RAW event -> rejet Interface
Transaction complète -> rejet Interface
Block event -> rejet Interface
Persisted-data available -> Store API owner
```
`TransactionLogEvent` devra être transféré vers TODO/IDEAS durable pendant `pre.007` s'il reste hors scope.
## Fichiers
Modifiés :
```text
Cargo.toml
crates/ksp-interface-lib/tests/dependency_boundary.rs
docs/validation/022-V0_3_5_INTERFACE_ACQUISITION_EVENTS.md
```
Ajoutés :
```text
crates/ksp-interface-lib/tests/security_hardening.rs
deltas/0.3.5/pre.005.md
```
Explicitement inchangés :
```text
crates/ksp-interface-lib/Cargo.toml
crates/ksp-interface-lib/src/**
crates/ksp-interface-lib/unit_tests/**
crates/ksp-onchain-transport-lib/**
crates/ksp-store-api/**
crates/ksp-store-lib/**
crates/ksp-store-postgres-lib/**
crates/ksp-program-api/**
README.md
ROADMAP.md
CHANGELOG.md
```
## 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.5
```
## Validations non exécutées dans l'environnement d'assemblage
`cargo` n'est pas installé dans le sandbox d'assemblage. Restent donc à exécuter côté opérateur :
```text
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-interface-lib
cargo test -p ksp-program-api
```
## Décisions
- deux familles acquisition exactement restent admises ;
- aucun contrat production n'est modifié ;
- aucun nouveau bound métier n'est inventé ;
- la signature reste une primitive passive fixe de 64 octets sans codec ;
- aucune metadata source/provider n'entre dans Interface ;
- aucun second RAW n'est créé ;
- Program instruction foundation reste inchangée.
## Questions ouvertes
Aucune question fonctionnelle bloquante pour `pre.006`. La seule idée différée explicitement à conserver durablement est `TransactionLogEvent`.

Some files were not shown because too many files have changed in this diff Show More