Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3de0488cab | |||
| 2460162243 | |||
| 1b9104d64b | |||
| 80be36bfdd | |||
| e8d2382ac3 | |||
| 749e53edc2 | |||
| 7377a6f6cd | |||
| 45601ce386 | |||
| 7d490eb5d8 | |||
| ca018095bf | |||
| 0dd722ffca | |||
| 75e8030b07 | |||
| c24e4c48f8 | |||
| c7cfb9557d | |||
| c27428d6c0 | |||
| dfa01b1381 | |||
| 5fc7f65b2c | |||
| bbf804b7fc | |||
| 13687ec2fe | |||
| b756da5149 | |||
| 04234926ce | |||
| bfc69f5631 | |||
| 902b5fba99 | |||
| fccb7d876c | |||
| 50d4142797 | |||
| eb6dbc31e8 | |||
| fb8b585aa2 | |||
| dad18c16d5 | |||
| a704a5722e | |||
| dfa3723a7f | |||
| 6bd593f467 | |||
| 03ad0ba063 | |||
| d95b1095e5 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
23
ROADMAP.md
23
ROADMAP.md
@@ -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 d’acquisition, 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 d’exploitation 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 d’admission 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 lorsqu’un 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 d’un wake-up « donnée persistée disponible » reste distinct et appartient à `ksp-store-api` conformément à `KSP-NOTIFY-*`, mais ne sera matérialisé qu’avec 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 d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un 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 d’un 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 l’intersection réellement partagée, sans persistence Store par défaut et sans promettre l’ordre/complétude du flux.
|
||||
- [ ] **TODO** — vote realtime : reste hors Interface tant qu’aucun 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 d’abord ê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 n’est identifiée.
|
||||
- [ ] **TODO** — processing ledger : reprendre l’idée kbot2/kbot3 `stage + processor identity/version + input identity/hash + terminal status`, sans faire d’un `processed: bool` la preuve durable unique ; prévoir force replay/version upgrades lorsque les processors seront ouverts.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
53
crates/ksp-interface-lib/src/slot_lifecycle.rs
Normal file
53
crates/ksp-interface-lib/src/slot_lifecycle.rs
Normal 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;
|
||||
85
crates/ksp-interface-lib/src/transaction_execution.rs
Normal file
85
crates/ksp-interface-lib/src/transaction_execution.rs
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
100
crates/ksp-interface-lib/tests/security_hardening.rs
Normal file
100
crates/ksp-interface-lib/tests/security_hardening.rs
Normal 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;
|
||||
}
|
||||
29
crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
Normal file
29
crates/ksp-interface-lib/tests/slot_lifecycle_public_api.rs
Normal 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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
45
crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
Normal file
45
crates/ksp-interface-lib/unit_tests/slot_lifecycle.rs
Normal 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"));
|
||||
}
|
||||
28
crates/ksp-interface-lib/unit_tests/transaction_execution.rs
Normal file
28
crates/ksp-interface-lib/unit_tests/transaction_execution.rs
Normal 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 }");
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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$;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
962
crates/ksp-store-postgres-lib/src/raw_account.rs
Normal file
962
crates/ksp-store-postgres-lib/src/raw_account.rs
Normal 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;
|
||||
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal file
230
crates/ksp-store-postgres-lib/src/raw_account/cursor.rs
Normal 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(());
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
1216
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
Normal file
1216
crates/ksp-store-postgres-lib/tests/postgres_raw_account_live.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
525
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
Normal file
525
crates/ksp-store-postgres-lib/unit_tests/raw_account.rs
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
139
deltas/0.3.4/pre.001-fix.001.md
Normal file
139
deltas/0.3.4/pre.001-fix.001.md
Normal 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
206
deltas/0.3.4/pre.001.md
Normal 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.
|
||||
168
deltas/0.3.4/pre.002-fix.001.md
Normal file
168
deltas/0.3.4/pre.002-fix.001.md
Normal 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
232
deltas/0.3.4/pre.002.md
Normal 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.
|
||||
111
deltas/0.3.4/pre.003-fix.001.md
Normal file
111
deltas/0.3.4/pre.003-fix.001.md
Normal 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
187
deltas/0.3.4/pre.003.md
Normal 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**.
|
||||
119
deltas/0.3.4/pre.004-fix.001.md
Normal file
119
deltas/0.3.4/pre.004-fix.001.md
Normal 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
298
deltas/0.3.4/pre.004.md
Normal 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
308
deltas/0.3.4/pre.005.md
Normal 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
148
deltas/0.3.4/pre.006.md
Normal 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.
|
||||
70
deltas/0.3.4/pre.007-fix.001.md
Normal file
70
deltas/0.3.4/pre.007-fix.001.md
Normal 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
248
deltas/0.3.4/pre.007.md
Normal 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`.
|
||||
72
deltas/0.3.4/pre.008-fix.001.md
Normal file
72
deltas/0.3.4/pre.008-fix.001.md
Normal 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
219
deltas/0.3.4/pre.008.md
Normal 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
216
deltas/0.3.4/pre.009.md
Normal 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
49
deltas/0.3.4/pre.010.md
Normal 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
143
deltas/0.3.4/pre.011.md
Normal 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
185
deltas/0.3.4/pre.012.md
Normal 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
283
deltas/0.3.4/pre.013.md
Normal 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
140
deltas/0.3.4/rel.001.md
Normal 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
|
||||
```
|
||||
191
deltas/0.3.5/pre.001-fix.001.md
Normal file
191
deltas/0.3.5/pre.001-fix.001.md
Normal 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
119
deltas/0.3.5/pre.001.md
Normal 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
|
||||
```
|
||||
67
deltas/0.3.5/pre.002-fix.001.md
Normal file
67
deltas/0.3.5/pre.002-fix.001.md
Normal 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
180
deltas/0.3.5/pre.002.md
Normal 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
193
deltas/0.3.5/pre.003.md
Normal 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
153
deltas/0.3.5/pre.004.md
Normal 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.
|
||||
78
deltas/0.3.5/pre.005-fix.001.md
Normal file
78
deltas/0.3.5/pre.005-fix.001.md
Normal 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
138
deltas/0.3.5/pre.005.md
Normal 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
Reference in New Issue
Block a user