From d95b1095e52ad161ee7f8a537eb5970b9a0e3e07 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 30 Aug 2026 19:21:15 +0200 Subject: [PATCH] v0.3.4-pre.001 --- Cargo.toml | 4 +- deltas/0.3.4/pre.001.md | 206 ++++++ ...-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md | 610 ++++++++++++++++++ .../021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md | 327 ++++++++++ 4 files changed, 1145 insertions(+), 2 deletions(-) create mode 100644 deltas/0.3.4/pre.001.md create mode 100644 docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md create mode 100644 docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md diff --git a/Cargo.toml b/Cargo.toml index 2453692..f5c270f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 363 +# version: 364 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.3" +version = "0.3.4-pre.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/deltas/0.3.4/pre.001.md b/deltas/0.3.4/pre.001.md new file mode 100644 index 0000000..e98280d --- /dev/null +++ b/deltas/0.3.4/pre.001.md @@ -0,0 +1,206 @@ + + + +# 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. diff --git a/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md new file mode 100644 index 0000000..cbceb6b --- /dev/null +++ b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md @@ -0,0 +1,610 @@ + + + +# Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW + +## 1. Statut du gate `pre.001` + +`0.3.4-pre.001` est un gate de conception. Il ne crée **aucun** SQL V002, aucune table account et aucun repository account. Il fixe la forme physique et le séquencement nécessaires avant implémentation. + +Base canonique auditée : + +```text +v0.3.3 +workspace.package.version = 0.3.3 +``` + +Version de travail de cette prerelease : + +```text +workspace.package.version = 0.3.4-pre.1 +label = 0.3.4-pre.001 +``` + +Décision de scope : `ksp-store-api` reste inchangée. L'audit n'a révélé aucun gap backend-agnostic bloquant ; la difficulté restante est exclusivement l'implémentation physique PostgreSQL et son dispatch par la façade. + +## 2. Invariants hérités et frontières + +La trajectoire reste : + +```text +RAW -> STRUCTURAL -> DECODED -> DOMAIN +``` + +`0.3.4` ferme seulement la complétude physique **RAW Store/PostgreSQL**. Store persiste et sert ; il ne choisit ni batch-size, ni priorité, ni backlog, ni retry job, ni stratégie de backfill. + +Frontières inchangées : + +```text +ksp-store-lib -> ksp-store-api +ksp-store-lib[postgres] -> ksp-store-postgres-lib +ksp-store-postgres-lib -> ksp-store-api +ksp-store-postgres-lib -X-> ksp-store-lib +Config -> Store autorisé +Store/backend -X-> Config +Transport -X-> Store +consumer ordinaire -> ksp-store-lib +consumer ordinaire -X-> ksp-store-postgres-lib +``` + +Une instance `Store` reste liée à un `RawNetworkId` et une base physique. Aucun multiplexage réseau/database n'est ajouté. + +## 3. Inventaire de capabilities : 6/10 -> 10/10 + +| Capability | Impl physique stable | État base | Action 0.3.4 | +|--------------------------------|-------------------------|---------------|--------------| +| RawTransactionRead | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawTransactionWrite | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawTransactionObservationRead | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawTransactionObservationWrite | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawTransactionRetentionRead | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawTransactionRetentionWrite | PostgresBackend + Store | stable 0.3.3 | préserver | +| RawAccountStateRead | aucune impl physique | pending 0.3.4 | implémenter | +| RawAccountStateWrite | aucune impl physique | pending 0.3.4 | implémenter | +| RawAccountObservationRead | aucune impl physique | pending 0.3.4 | implémenter | +| RawAccountObservationWrite | aucune impl physique | pending 0.3.4 | implémenter | + +Canaries qui ferment encore volontairement la surface account dans `v0.3.3` : + +- `crates/ksp-store-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for Store` ; +- `crates/ksp-store-lib/tests/hardening_completeness.rs` fige six impl transaction et interdit les quatre account ; +- `crates/ksp-store-postgres-lib/tests/dependency_boundary.rs` interdit les quatre `impl RawAccount* for PostgresBackend` ; +- `crates/ksp-store-postgres-lib/tests/hardening_completeness.rs` interdit encore `impl ksp_store_api::RawAccount*` et toute matérialisation `ksp_raw_account` dans la slice transaction. + +Ces canaries devront être **transformées**, pas simplement supprimées : à `pre.008`, elles devront prouver exactement six capabilities transaction + quatre account. Les canaries qui imposent que V000 et `health.rs` restent business-free demeurent valides et ne doivent pas être assouplies. + +### 3.1 Matrice détaillée des quatre capabilities account + +| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live | +|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------| +| RawAccountStateRead | reference / query | Option / Page | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix | +| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation | +| RawAccountObservationRead | observation_key | Option | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata | +| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence | + +## 4. Immutabilité V000/V001 + +Recalcul indépendant sur les bytes de l'archive stable : + +```text +V000 legacy SQL SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +V001 logical resources = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 +V001 resource count = 40 +``` + +L'algorithme V001 est celui du moteur stable : domaine `ksp-migration-resources-v1\0`, puis pour chaque ressource ordonnée `id + NUL + SQL bytes + NUL` avant SHA-256. + +Règles V002 : + +```text +V000 : immuable +V001 : immuable +V002 : additive uniquement +nom logique proposé : raw_account_state +répertoire proposé : migrations/v002_raw_account_state/ +resources proposées : tables/ + constraints/ + indexes/ +hook migration supplémentaire : aucun +``` + +Le binding réseau singleton V001 est réutilisé ; V002 ne duplique pas `network` dans chaque ligne métier. + +## 5. Audit field-by-field du contrat account + +### 5.1 `RawAccountStateReference` + `RawAccountState` + +Matrice complète modèle -> physique : + +| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte | +|------------|----------------|-------------|--------------------|-----------------------|----------------------------------|------------------------|---------------------------------| +| network | RawNetworkId | 1 | ksp_store_identity | exact backend network | binding runtime -> reference | aucune colonne row | aucun si guard exact | +| pubkey | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun | +| slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit | +| state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes | fixed bytes -> RawContentHash | hash bytes -> BYTEA | aucun ; pas de recalcul backend | +| lamports | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit | +| owner | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun | +| executable | bool | 1 | BOOLEAN | NOT NULL | bool direct | bool direct | aucun | +| rent_epoch | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit | +| data | Box<[u8]> | 1 | BYTEA | 0..=16 MiB | bytes + borne -> RawAccountState | bytes exacts -> BYTEA | base64/parse interdits | + +Vue synthétique de projection : + +| Champ API | Type API | Projection V002 | Invariant | +|------------|---------------------------|------------------------------------------|-----------------------------------------------------------------| +| network | RawNetworkId | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O | +| pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK | +| slot | u64 | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing | +| state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store | +| lamports | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact | +| owner | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` | +| executable | bool | BOOLEAN NOT NULL | mapping direct | +| rent_epoch | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact | +| data | Box<[u8]> | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 | + +L'identité physique canonique retenue est : + +```text +PRIMARY KEY (pubkey, slot, state_hash) +``` + +Le réseau fait partie de la référence API mais est porté physiquement par `ksp_store_identity` pour toute la base. Cette clé permet explicitement plusieurs états du même pubkey au même slot lorsque `state_hash` diffère. + +`state_hash` est un digest **producer-owned**. Le backend ne le recalcule pas et ne suppose pas qu'il s'agit du hash de `data` seulement. L'idempotence vérifie donc le contenu complet malgré l'égalité du hash. + +### 5.2 `RawAccountObservation` + +Matrice complète modèle -> physique : + +| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte | +|---------------------------|---------------------------------|-------------|--------------------|------------------------|----------------------------|------------------------|---------------------------| +| observation_key | RawObservationKey | 1 | BYTEA | exact 32 bytes | fixed bytes -> key | key bytes -> BYTEA | aucun | +| account.pubkey | Pubkey | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun | +| account.slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX + FK | decimal exact -> reference | decimal NUMERIC | narrowing interdit | +| account.state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun | +| provider | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun | +| protocol | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun | +| acquisition_method | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun | +| origin | RawAcquisitionOrigin | 1 | TEXT | 5 valeurs stables | static decode | static encode | unknown DB -> DataInvalid | +| received_at | RawTimestamp | 1 | BIGINT | 0..253402300799999 | checked i64 -> timestamp | u64 bounded -> i64 | borne API < i64::MAX | +| capture_session_id | Option | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun | +| commitment | Option | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun | +| endpoint_id | Option | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun | +| filter_id | Option | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun | +| observed_at | Option | 0..1 | BIGINT NULL | borne + <= received_at | NULL/checked timestamp | Option -> NULL/i64 | borne API < i64::MAX | +| source_payload_hash | Option | 0..1 | BYTEA NULL | NULL ou 32 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucun | +| source_payload_size_bytes | Option | 0..1 | BIGINT NULL | 0..=64 MiB | NULL/checked u64 | Option -> NULL/i64 | borne API < i64::MAX | +| is_startup | Option | 0..1 | BOOLEAN NULL | nullable | NULL/bool | Option -> NULL/bool | ne pas inventer false | +| transaction_signature | Option | 0..1 | BYTEA NULL | NULL ou 64 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucune FK transaction | +| write_version | Option | 0..1 | NUMERIC(20,0) NULL | NULL ou 0..u64::MAX | NULL/decimal exact | Option -> NULL/NUMERIC | narrowing interdit | + +Vue synthétique de projection : + +| Champ API | Type API | Projection V002 | Invariant | +|---------------------------|---------------------------------|------------------------|--------------------------------------------------| +| observation_key | RawObservationKey / 32 bytes | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte | +| account.pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | FK composite vers state | +| account.slot | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite | +| account.state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | FK composite | +| provider | RawProvenanceCode | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 | +| protocol | RawProvenanceCode | TEXT NOT NULL | idem V001 | +| acquisition_method | RawProvenanceCode | TEXT NOT NULL | idem V001 | +| origin | RawAcquisitionOrigin | TEXT NOT NULL | backfill/import/live/repair/replay | +| received_at | RawTimestamp | BIGINT NOT NULL | 0..253402300799999 | +| capture_session_id | Option | TEXT NULL | mêmes checks V001 | +| commitment | Option | TEXT NULL | mêmes checks V001 | +| endpoint_id | Option | TEXT NULL | mêmes checks V001 | +| filter_id | Option | TEXT NULL | mêmes checks V001 | +| observed_at | Option | BIGINT NULL | borne + `observed_at <= received_at` | +| source_payload_hash | Option | BYTEA NULL | NULL ou 32 bytes | +| source_payload_size_bytes | Option | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT | +| is_startup | Option | BOOLEAN NULL | Yellowstone metadata observation-only | +| transaction_signature | Option | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction | +| write_version | Option | NUMERIC(20,0) NULL | NULL ou domaine u64 exact | + +La relation observation -> state est : + +```text +FOREIGN KEY (account_pubkey, account_slot, account_state_hash) + REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) +``` + +Aucun `ON DELETE CASCADE` n'est nécessaire : l'API account ne possède ni delete ni retention. Aucune FK n'est créée vers `ksp_raw_transactions` pour `transaction_signature`; cette signature est une metadata source optionnelle et la transaction correspondante n'est pas garantie présente dans le Store. + +`observation_key` est unique **dans la famille account**. Le contrat API expose des reads family-specific et ne définit aucune unicité globale transaction+account ; une registry globale ajouterait un couplage physique non requis. + +### 5.3 `RawAccountStateQuery` + +| Entrée query | Cardinalité | Projection physique | +|-----------------------|------------------------|------------------------------------------------------------------| +| network | obligatoire | guard DB mono-réseau + digest cursor | +| pubkey | optionnel | filtre égalité ; aucune query owner | +| slots.start_inclusive | optionnel | `slot >=` | +| slots.end_inclusive | optionnel | `slot <=` | +| direction | Ascending / Descending | ordre total entièrement inversé | +| page.limit | u64 > 0 | limite physique PostgreSQL seulement ; `LIMIT + 1` | +| page.cursor | opaque <= 4 KiB | V1 account fixe, authentification de contexte par digest SHA-256 | + +Aucune query API par owner/provider/time/status n'existe. Aucun index pour ces dimensions ne sera introduit dans V002. + +## 6. Audit kbot3 ciblé + +L'archive historique contient notamment `k_sol_obs_account_observations`, `k_sol_core_account_states`, `AccountStateStore` et le pipeline de normalisation `account_state_normalization`. Cette surface mélangeait acquisition, normalisation CORE et processing policy ; elle n'est pas transposable telle quelle. + +| Élément historique | Classement | Décision KSP | +|----------------------------------------------------------|------------|-----------------------------------------------------------------------| +| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles | +| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées | +| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet | +| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` | +| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width | +| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth | +| observation duplique l’état canonique | REDESSINER | observation référence un state complet séparé | +| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing | +| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 | +| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store | +| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie | +| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier | +| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib | +| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account | + +Aucun code kbot3 ne sera copié. Les optional Yellowstone actuelles (`write_version`, `transaction_signature`, `is_startup`) n'existaient pas comme contrat durable équivalent dans cette persistence historique et restent donc définies uniquement par `ksp-store-api` actuel. + +Points héritage supplémentaires explicitement classés : + +- **REPRENDRE** : l'ancien repository utilisait déjà une transaction pour rendre atomiques state CORE + mutation observation + processing ledger ; seul le principe transactionnel est repris, jamais le couplage CORE/ledger. +- **REPRENDRE** : l'unicité `observation_key` et la validation additive des contraintes/indexes sont de bons invariants physiques. +- **REDESSINER** : `write_version`, `transaction_signature` et `is_startup` n'ont pas d'équivalent persistant account fiable dans cette surface historique ; V002 suit exclusivement les optionalités du contrat KSP actuel. +- **REDESSINER** : l'idempotence historique `source_observation_id` + `ON CONFLICT DO UPDATE` est remplacée par clé producer-owned + comparaison exacte + conflit stable. +- **REJETER** : les erreurs historiques interpolent l'erreur SQL dans `ks_core::Error::db(...)`; KSP conserve uniquement kind/phase statiques et sûres. + +## 7. Design physique V002 figé par `pre.001` + +### 7.1 Tables minimales + +```text +ksp_raw_account_states +ksp_raw_account_observations +``` + +Aucune table archive/tombstone account n'est ajoutée : `ksp-store-api` ne définit aucune capability de retention account. + +### 7.2 Colonnes `ksp_raw_account_states` + +```text +pubkey BYTEA NOT NULL +slot NUMERIC(20,0) NOT NULL +state_hash BYTEA NOT NULL +lamports NUMERIC(20,0) NOT NULL +owner BYTEA NOT NULL +executable BOOLEAN NOT NULL +rent_epoch NUMERIC(20,0) NOT NULL +data BYTEA NOT NULL +``` + +Contraintes minimales : fixed width 32 pour `pubkey`, `owner`, `state_hash`; domaine u64 pour `slot`, `lamports`, `rent_epoch`; `octet_length(data) <= 16777216`; aucune borne basse sur data. + +### 7.3 Colonnes `ksp_raw_account_observations` + +```text +observation_key BYTEA NOT NULL +account_pubkey BYTEA NOT NULL +account_slot NUMERIC(20,0) NOT NULL +account_state_hash BYTEA NOT NULL +provider TEXT NOT NULL +protocol TEXT NOT NULL +acquisition_method TEXT NOT NULL +origin TEXT NOT NULL +received_at_unix_millis BIGINT NOT NULL +capture_session_id TEXT NULL +commitment TEXT NULL +endpoint_id TEXT NULL +filter_id TEXT NULL +observed_at_unix_millis BIGINT NULL +source_payload_hash BYTEA NULL +source_payload_size_bytes BIGINT NULL +is_startup BOOLEAN NULL +transaction_signature BYTEA NULL +write_version NUMERIC(20,0) NULL +``` + +Les contraintes provenance doivent être sémantiquement identiques à V001 : code 1..128 octets/alphabet sûr, origin fermé, timestamps bornés/ordonnés, source hash 32 bytes et source payload <= 64 MiB. + +### 7.4 Indexes justifiés + +Indexes provenant des PK : + +```text +PK ksp_raw_account_states(pubkey, slot, state_hash) +PK ksp_raw_account_observations(observation_key) +``` + +Un seul index métier additionnel est justifié par la query non filtrée : + +```text +ix_ksp_raw_account_states_slot_pubkey_state_hash(slot, pubkey, state_hash) +``` + +Le PK couvre déjà efficacement la navigation lorsque `pubkey = ?` est imposé. Aucun index owner/provider/protocol/method/time/hash isolé n'est admis dans cette release. + +## 8. Ordre total et pagination keyset + +Ordre canonique : + +```text +Ascending : (slot ASC, pubkey ASC, state_hash ASC) +Descending : (slot DESC, pubkey DESC, state_hash DESC) +``` + +Le backend PostgreSQL compare `BYTEA` selon son ordre btree déterministe. Aucun `OFFSET` n'est utilisé. + +Cursor account V1 proposé : + +```text +bytes 0..4 magic = "KSPA" +byte 4 version = 1 +bytes 5..13 last_slot, u64 big-endian +bytes 13..45 last_pubkey, 32 bytes +bytes 45..77 last_state_hash, 32 bytes +bytes 77..109 SHA-256 binding digest +longueur fixe = 109 bytes +``` + +Domaine de digest : + +```text +KSP/raw-account-state-cursor/v1 +``` + +Le digest lie au minimum : network, présence/valeur du filtre pubkey, direction, start/end slot, last slot, last pubkey, last state_hash. Le magic/domain distinct de `KSPT`/`KSP/raw-transaction-cursor/v1` rend le replay cross-family impossible même si les deux cursors ont 109 octets. + +Le plafond physique PostgreSQL reste le précédent réel nécessaire à `LIMIT + 1` : + +```text +9_223_372_036_854_775_806 +``` + +Ce plafond n'est pas une policy métier KSP. + +## 9. Algorithmes d'écriture + +### 9.1 Acquisition state + observation atomiques + +Préconditions avant I/O métier : + +```text +state.reference.network == backend.network +observation.account.network == backend.network +observation.account == state.reference +``` + +Algorithme : + +```text +BEGIN +INSERT state ... ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING ... +if inserted: + entity_outcome = Inserted +else: + SELECT canonical state BY PK FOR UPDATE + decode + compare exact lamports/owner/executable/rent_epoch/data + identical -> AlreadyPresent + divergent -> ERROR_CODE_RAW_CONFLICT + +INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ... +if inserted: + observation_outcome = Inserted +else: + SELECT observation BY key FOR UPDATE + decode + compare exact reference + provenance + all optional metadata + identical -> AlreadyPresent + divergent -> ERROR_CODE_RAW_CONFLICT +COMMIT +``` + +Une divergence d'observation après insertion d'un nouveau state fait rollback de ce state. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*` préalable, aucun overwrite silencieux. + +### 9.2 Observation supplémentaire + +```text +validate network before I/O +BEGIN +SELECT state BY composite reference FOR KEY SHARE +absent -> ReferenceNotFound +INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ... +collision -> SELECT FOR UPDATE + exact compare +COMMIT +``` + +Cette opération ne crée jamais implicitement le state et ne retransmet pas ses bytes. + +## 10. Matrice de concurrence + +| Scénario | Résultat permis | Invariant | +|------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------| +| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | attente unique + comparaison exacte ; aucun doublon | +| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | aucun overwrite ; résultat gagnant dépend de l’ordonnancement | +| même pubkey+slot, state_hash différents | les deux states sont représentables | PK inclut state_hash | +| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | transaction perdante rollback son state nouvellement créé | +| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | state préexistant inchangé | +| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | aucun update silencieux | +| annulation après insertion state avant observation | aucune moitié durable | transaction PostgreSQL rollback/drop | + +Le backend ne promet pas quel concurrent divergent gagne ; il promet qu'un seul contenu incompatible devient durable et que l'autre reçoit le conflit stable. + +## 11. Taxonomie d'erreurs + +Aucun nouveau `PostgresBackendErrorKind` n'est requis par le design actuel. + +| Cas | Taxonomie stable | Règle | +|---------------------------------------------------|------------------------------------------------------|---------------------------------| +| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | guard avant SQL métier | +| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | pas de state implicite | +| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | message/context statiques | +| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | aucun bytes hostile dans erreur | +| page > limite physique PostgreSQL | PageLimitUnsupported | limite physique, pas policy KSP | +| row DB incompatible/malformée | DataInvalid | aucune valeur hostile recopiée | +| échec SELECT | ReadFailed | pas de server text exposé | +| échec INSERT/transaction | WriteFailed | pas de server text exposé | +| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | réutiliser taxonomy stable | + +Les erreurs PostgreSQL externes ne doivent jamais être conservées comme source publique ni recopiées dans message/context. + +## 12. Threat model `RawAccountState` + +| Menace | Risque | Mitigation | Preuve prévue | +|---------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|----------------------------| +| URI pointe vers DB d’un autre réseau | corruption cross-network | binding `ksp_store_identity` + guard réseau avant opération | unit + live | +| pubkey/hash/signature DB de mauvaise largeur | construction de modèle invalide | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows | +| slot/lamports/rent_epoch/write_version > i64::MAX | narrowing/perte | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 | +| data account > 16 MiB | allocation/persistence hostile | admission API + CHECK `octet_length <= 16777216` | unit + live boundary | +| data account vide | rejet incorrect | aucune borne basse SQL ; vide explicitement valide | unit | +| observation sans state | orphelin | FK composite + record pré-vérifie state | unit + live | +| échec observation après state insert | demi-acquisition durable | une transaction PostgreSQL commune | live cancellation/rollback | +| même ref, contenu divergent | overwrite silencieux | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent | +| même pubkey/slot, hashes distincts | collision d’identité | PK `(pubkey,slot,state_hash)` | unit + live | +| collision observation key | fusion de sources distinctes | comparaison exacte de tous les champs ; conflict sinon | unit + live | +| cursor transaction injecté dans account | replay cross-family | magic/domain account distincts | unit cursor | +| cursor account rejoué sur autre query | pagination incohérente | digest lie network/pubkey/range/direction/last key | unit cursor matrix | +| ordre même slot ambigu | duplication/skip entre pages | ordre total `(slot,pubkey,state_hash)` | unit + live pagination | +| metadata Yellowstone promue au state | couplage provider | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary | +| transaction_signature impose transaction row | couplage cross-family non garanti | aucune FK vers `ksp_raw_transactions` | schema canary | +| SQL/server text fuit | secret/PII leak | projection PostgresBackendError statique | hardening | +| V000/V001 modifiées | history incompatible | checksums figés et canaries exactes | migration tests | +| index non justifié | write amplification/bloat | un seul index métier V002 pour query unfiltered | schema completeness | + +## 13. Compatibilité schema/migration + +V002 devra suivre le moteur resource-owned de V001 : ressources ordonnées, checksums logiques, vérification catalogique et mutation selon les policies foundation existantes. + +Principes : + +```text +schema_creation = disabled : aucune création implicite +schema_creation = create_if_missing : bootstrap seulement selon contrat existant +schema_update = disabled : historique incomplet -> mismatch sûr +schema_update = update_if_needed : V002 appliquée additivement +history > latest embedded : SchemaNewer +history V000/V001 divergente : MigrationMismatch +extra columns externes : acceptables seulement si elles ne peuvent bloquer les INSERT KSP +colonnes/constraints/index requis incompatibles : mismatch avant ready +``` + +Les numéros de fichiers SQL exacts sont une responsabilité de `pre.002/pre.003`; leur sémantique est figée ici, pas leurs noms séquentiels. + +## 14. Mapping/read privé + +Le module backend proposé est `raw_account` privé, analogue structurellement à `raw_transaction` mais sans retention/archive. + +Il devra contenir : + +```text +mapping row -> RawAccountState +mapping row -> RawAccountObservation +mapping u64 <-> NUMERIC text sans narrowing +get state par composite reference +get observation par observation_key +list references par keyset +cursor account privé +write acquisition/observation privé +``` + +Aucun row/statement/client PostgreSQL ne traverse `ksp-store-postgres-lib` publiquement ni `ksp-store-lib`. + +## 15. Stratégie de preuve PostgreSQL réelle + +La preuve live restera `#[ignore]`, opt-in, avec une URI dédiée lue depuis stdin et sans environnement direct. Elle devra refuser de détruire des objets KSP préexistants non créés par le test. + +Scénarios minimum : + +```text +bootstrap V000+V001+V002 et health ready +réouverture idempotente du schéma +state bytes vide et 16 MiB +u64 au-delà de i64::MAX et u64::MAX pour colonnes NUMERIC +acquisition identique puis divergente +same pubkey + same slot + distinct state_hash +observation supplémentaire / reference not found +collision observation +pagination filtered/unfiltered ASC/DESC multi-page +cursor hostile / replay query / replay transaction +concurrence identique/divergente +cancellation entre state et observation -> rollback +coexistence avec au moins un canari RawTransaction stable dans la même DB V002 +cleanup vérifié uniquement pour les objets créés par le test +``` + +Aucun appel Solana réseau n'est nécessaire à cette preuve. + +## 16. Conformance cross-family finale + +À `pre.008+`, les canaries devront compter exactement : + +```text +PostgresBackend : 10 impl RAW +Store : 10 impl RAW +``` + +La façade doit continuer de n'exposer que les modèles/capabilities backend-agnostic. Le test `--no-default-features` de `ksp-store-lib` doit rester vert : un build sans backend PostgreSQL compile et rejette proprement un backend non compilé au runtime. + +La conformance finale doit aussi démontrer que l'ajout account ne modifie pas les six comportements transaction : lecture/write/observation/list/retention/force-rehydrate restent couverts par leurs suites existantes. + +## 17. Sizing recalibré + +Le forecast initial du prompt concentrait migration et schema compatibility dans une seule tranche, ainsi que write+concurrence. Ces lots sont séparés pour rester dans la cible opérateur `~15-20 min/pre`. + +| Tranche | Objet | Budget | Note | +|---------|-------------------------------------------------------------------|-----------|----------------------------------------| +| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | 15-20 min | cette tranche | +| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | 15-20 min | split du forecast initial | +| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | 15-20 min | split du forecast initial | +| pre.004 | mapping privé state/observation + get reads + hostile rows | 15-20 min | pas de write | +| pre.005 | acquisition atomique state+observation + idempotence/conflict | 15-20 min | write principal | +| pre.006 | observation supplémentaire + races/cancellation unitaires | 15-20 min | concurrence isolée | +| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | 15-20 min | navigation uniquement | +| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | 15-20 min | frontières cross-crate | +| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | 15-20 min | ignored/opt-in | +| pre.010 | hardening/completeness cross-family + canaries ownership | 15-20 min | sécurité/completude | +| pre.011 | gate technique final + replay live ciblé + graphes | 15-20 min | avant docs finale | +| pre.012 | réconciliation docs finale sans rouvrir le code | 10-15 min | README/USAGE/plan/validation si requis | +| pre.013 | publication prep minimale + prompt 0.3.5 | 10-15 min | lane publication | +| rel.001 | stabilisation/tag après gate opérateur | 5-10 min | aucun scope nouveau | + +## 18. Hors scope explicite + +```text +modification ksp-store-api +retention/archivage/purge account +STRUCTURAL/CORE materialization +Interface events +workers/jobs/backfill executors +batch-size/priorité/backlog policy +Transport -> Store direct +index owner/provider/time/status sans query +compression account +nouveau backend autre que PostgreSQL +application Store Desk +``` + +## 19. Questions ouvertes après `pre.001` + +Aucune question **bloquante** de contrat ne subsiste. + +Les détails suivants restent volontairement d'implémentation : numérotation exacte des ressources V002, découpage précis des helpers de mapping et noms internes des statements. Ils ne doivent pas modifier les invariants de ce plan. + +## 20. Gate d'acceptation `pre.001` + +`pre.001` est acceptable si : + +```text +base v0.3.3 vérifiée +V000/V001 checksums recalculés et figés +inventaire 6/10 -> 10/10 explicite +field mapping account complet +kbot3 classifié REPRENDRE/REDESSINER/REPORTER/REJETER +V002 figée sans SQL écrit +ordre/cursor figés +algorithmes atomicité/idempotence/concurrence figés +threat model complet +forecast recalibré +aucune modification fonctionnelle hors version prerelease +audits statiques applicables verts ; gate Cargo opérateur rejoué +``` diff --git a/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md new file mode 100644 index 0000000..a4135cd --- /dev/null +++ b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md @@ -0,0 +1,327 @@ + + + +# Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW + +## 1. Portée + +Cette matrice est ouverte par `0.3.4-pre.001`. La tranche courante valide **le design et le sizing uniquement** ; elle n'affirme aucune présence de V002 ou d'implémentation account. + +Base : + +```text +v0.3.3 +``` + +Cible release : + +```text +PostgresBackend = 10/10 capabilities RAW +Store = 10/10 capabilities RAW +``` + +## 2. Gate base `pre.001` + +| Preuve | Attendu | État pre.001 | +|------------------------------------|------------------|-------------------------------------------------| +| workspace.package.version base | 0.3.3 | PASS audit | +| V000 checksum | d29068b8…46a1450 | PASS recalcul indépendant | +| V001 checksum | 31488cda…d72cc51 | PASS recalcul indépendant / 40 resources | +| capabilities transaction physiques | 6 | PASS inventaire source | +| capabilities account physiques | 0 | PASS inventaire source ; volontairement pending | +| canaries anti-account 0.3.3 | présentes | PASS inventaire tests | +| ksp-store-api change nécessaire | non | PASS audit | +| SQL V002 créé en pre.001 | non | PASS scope | + +## 3. Matrice capability finale + +| Capability | Base v0.3.3 | Cible 0.3.4 | Preuve finale | +|--------------------------------|-------------|-------------|-------------------------------| +| RawTransactionRead | PASS | PASS | régression suite 0.3.3 | +| RawTransactionWrite | PASS | PASS | régression suite 0.3.3 | +| RawTransactionObservationRead | PASS | PASS | régression suite 0.3.3 | +| RawTransactionObservationWrite | PASS | PASS | régression suite 0.3.3 | +| RawTransactionRetentionRead | PASS | PASS | régression suite 0.3.3 | +| RawTransactionRetentionWrite | PASS | PASS | régression suite 0.3.3 | +| RawAccountStateRead | PENDING | PASS | tests backend + façade + live | +| RawAccountStateWrite | PENDING | PASS | tests backend + façade + live | +| RawAccountObservationRead | PENDING | PASS | tests backend + façade + live | +| RawAccountObservationWrite | PENDING | PASS | tests backend + façade + live | + +### 3.1 Contrat opérationnel détaillé des quatre capabilities account + +| Capability | Input | Output | Guard réseau | SQL/transaction | Idempotence | Conflit | Absence/référence | Pagination | Error mapping | Preuve live | +|----------------------------|---------------------|---------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------|----------------------------------------|-----------------------------------|---------------------|---------------------------------------------------------------------------|------------------------------------------------| +| RawAccountStateRead | reference / query | Option / Page | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix | +| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation | +| RawAccountObservationRead | observation_key | Option | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata | +| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence | + +## 4. Matrice `RawAccountState` + +| Champ | Représentation décidée | Validation requise | +|------------|------------------------------------------|-----------------------------------------------------------------| +| network | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O | +| pubkey | BYTEA NOT NULL | `octet_length = 32` ; composant PK | +| slot | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing | +| state_hash | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store | +| lamports | NUMERIC(20,0) NOT NULL | domaine u64 exact | +| owner | BYTEA NOT NULL | `octet_length = 32` | +| executable | BOOLEAN NOT NULL | mapping direct | +| rent_epoch | NUMERIC(20,0) NOT NULL | domaine u64 exact | +| data | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 | + +Points structurants : + +```text +identity = network API + (pubkey, slot, state_hash) +network SQL = singleton ksp_store_identity, pas colonne row +PK state = (pubkey, slot, state_hash) +state_hash n'est pas une preuve suffisante d'idempotence : comparaison contenu exacte obligatoire +empty data = valide +``` + +## 5. Matrice observation + +| Champ | Représentation décidée | Validation requise | +|---------------------------|------------------------|--------------------------------------------------| +| observation_key | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte | +| account.pubkey | BYTEA NOT NULL | FK composite vers state | +| account.slot | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite | +| account.state_hash | BYTEA NOT NULL | FK composite | +| provider | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 | +| protocol | TEXT NOT NULL | idem V001 | +| acquisition_method | TEXT NOT NULL | idem V001 | +| origin | TEXT NOT NULL | backfill/import/live/repair/replay | +| received_at | BIGINT NOT NULL | 0..253402300799999 | +| capture_session_id | TEXT NULL | mêmes checks V001 | +| commitment | TEXT NULL | mêmes checks V001 | +| endpoint_id | TEXT NULL | mêmes checks V001 | +| filter_id | TEXT NULL | mêmes checks V001 | +| observed_at | BIGINT NULL | borne + `observed_at <= received_at` | +| source_payload_hash | BYTEA NULL | NULL ou 32 bytes | +| source_payload_size_bytes | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT | +| is_startup | BOOLEAN NULL | Yellowstone metadata observation-only | +| transaction_signature | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction | +| write_version | NUMERIC(20,0) NULL | NULL ou domaine u64 exact | + +Invariants additionnels : + +```text +FK observation -> state composite +record_raw_account_observation n'insère jamais le state +transaction_signature observation-only, sans FK transaction +write_version/is_startup observation-only +observation_key family-local unique +``` + +## 6. Query/pagination + +| Élément | Décision | Canari requis | +|-----------------|------------------------------------------------|--------------------------------| +| ordre ASC | slot, pubkey, state_hash ASC | multi-page sans duplicate/skip | +| ordre DESC | slot, pubkey, state_hash DESC | symétrique ASC | +| filtre pubkey | égalité optionnelle | cursor lié au filtre | +| slot range | inclusive | cursor lié aux deux bornes | +| cursor magic | KSPA | rejette KSPT transaction | +| cursor version | 1 | rejette version hostile | +| cursor longueur | 109 bytes | rejette taille hostile | +| cursor domain | KSP/raw-account-state-cursor/v1 | digest family-bound | +| cursor digest | network+pubkey filter+range+direction+last key | anti-replay query | +| pagination | keyset seulement | aucun OFFSET | +| limit | limite physique PostgreSQL uniquement | pas de batch policy | + +## 7. Migration/schema V002 + +| Objet | Décision pre.001 | État | +|----------------------------------|---------------------------------------|-------------------------| +| V000 | immuable | PASS design | +| V001 | immuable | PASS design | +| V002 logical name | raw_account_state | PASS design | +| table state | ksp_raw_account_states | PASS design | +| table observation | ksp_raw_account_observations | PASS design | +| archive/tombstone account | aucune | PASS negative scope | +| index métier | (slot,pubkey,state_hash) | PASS design | +| index owner/provider/time/status | aucun | PASS negative scope | +| SQL/resource files | pas encore créés | PENDING pre.002/pre.003 | +| V002 checksum | inconnu tant que resources non créées | PENDING pre.003 | + +## 8. Idempotence et concurrence + +| Scénario | Résultat attendu | État | +|------------------------------------------------------------|-------------------------------------------------------|-------------------| +| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | PENDING impl/live | +| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | PENDING impl/live | +| même pubkey+slot, state_hash différents | les deux states sont représentables | PENDING impl/live | +| observation key identique, observations divergentes | une observation gagne ; autre `RAW_CONFLICT` | PENDING impl/live | +| record observation identique concurrent | 1 Inserted ; 1 AlreadyPresent | PENDING impl/live | +| record observation divergente concurrent | 1 Inserted ; 1 `RAW_CONFLICT` | PENDING impl/live | +| annulation après insertion state avant observation | aucune moitié durable | PENDING impl/live | + +Le test de contenu divergent doit comparer les fields complets et ne jamais traiter l'égalité de `state_hash` comme preuve suffisante. + +## 9. Threat model + +| Menace | Mitigation figée | Preuve | +|---------------------------------------------------|-----------------------------------------------------------------------|----------------------------| +| URI pointe vers DB d’un autre réseau | binding `ksp_store_identity` + guard réseau avant opération | unit + live | +| pubkey/hash/signature DB de mauvaise largeur | CHECK SQL + mapping `DataInvalid` statique | unit hostile rows | +| slot/lamports/rent_epoch/write_version > i64::MAX | NUMERIC(20,0) + conversion décimale fallible | unit limites u64 | +| data account > 16 MiB | admission API + CHECK `octet_length <= 16777216` | unit + live boundary | +| data account vide | aucune borne basse SQL ; vide explicitement valide | unit | +| observation sans state | FK composite + record pré-vérifie state | unit + live | +| échec observation après state insert | une transaction PostgreSQL commune | live cancellation/rollback | +| même ref, contenu divergent | ON CONFLICT DO NOTHING + SELECT FOR UPDATE + comparaison exacte | unit + live concurrent | +| même pubkey/slot, hashes distincts | PK `(pubkey,slot,state_hash)` | unit + live | +| collision observation key | comparaison exacte de tous les champs ; conflict sinon | unit + live | +| cursor transaction injecté dans account | magic/domain account distincts | unit cursor | +| cursor account rejoué sur autre query | digest lie network/pubkey/range/direction/last key | unit cursor matrix | +| ordre même slot ambigu | ordre total `(slot,pubkey,state_hash)` | unit + live pagination | +| metadata Yellowstone promue au state | is_startup/transaction_signature/write_version uniquement observation | schema + mapping canary | +| transaction_signature impose transaction row | aucune FK vers `ksp_raw_transactions` | schema canary | +| SQL/server text fuit | projection PostgresBackendError statique | hardening | +| V000/V001 modifiées | checksums figés et canaries exactes | migration tests | +| index non justifié | un seul index métier V002 pour query unfiltered | schema completeness | + +## 10. Audit historique kbot3 + +| Élément | Classement | Disposition | +|----------------------------------------------------------|------------|-----------------------------------------------------------------------| +| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles | +| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées | +| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet | +| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` | +| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width | +| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth | +| observation duplique l’état canonique | REDESSINER | observation référence un state complet séparé | +| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing | +| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 | +| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store | +| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie | +| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier | +| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib | +| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account | + +Le pipeline historique `k_sol_obs_account_observations -> k_sol_core_account_states` reste une source de comparaison, pas une architecture cible. La normalisation versionnée et ses caps de batch sont hors scope `0.3.4`. + +## 11. Canaries `0.3.3` à faire évoluer + +À l'ouverture effective de la surface account : + +```text +ksp-store-lib/tests/dependency_boundary.rs + remplacer interdiction RawAccount par exigence des 4 impl + absence types SQL + +ksp-store-lib/tests/hardening_completeness.rs + remplacer inventaire 6 + RawAccount fermé par inventaire exact 10 + +ksp-store-postgres-lib/tests/dependency_boundary.rs + ajouter module raw_account privé et exiger les 4 impl dans runtime bridge + conserver V000 et health business-free + +ksp-store-postgres-lib/tests/hardening_completeness.rs + remplacer interdiction RawAccount globale par inventaire exact 10 + remplacer l'absence V002/account par checks de surface V002 minimale +``` + +Les tests historiques nommés `pre_XXX` peuvent conserver leurs noms si le contrat qu'ils vérifient reste valide ; seul leur contenu doit être élargi lorsqu'il fige intentionnellement le scope `0.3.3`. + +## 12. Taxonomie d'erreur à prouver + +| Cas | Code/projection attendue | État | +|---------------------------------------------------|------------------------------------------------------|--------------| +| mauvais réseau opération | WrongNetwork -> `store.wrong_network` | PENDING impl | +| référence absente pour observation supplémentaire | ReferenceNotFound -> `store.raw_reference_not_found` | PENDING impl | +| contenu divergent / collision observation | Conflict -> `store_api.raw_conflict` | PENDING impl | +| cursor/query invalide | QueryInvalid -> `store_api.raw_query_invalid` | PENDING impl | +| page > limite physique PostgreSQL | PageLimitUnsupported | PENDING impl | +| row DB incompatible/malformée | DataInvalid | PENDING impl | +| échec SELECT | ReadFailed | PENDING impl | +| échec INSERT/transaction | WriteFailed | PENDING impl | +| V002 absente/incompatible/divergente | MigrationMismatch / MigrationFailed / SchemaNewer | PENDING impl | + +Aucune nouvelle variante backend n'est justifiée au gate `pre.001`. + +## 13. Preuve live finale + +Le test live account devra être opt-in/ignored, URI stdin, sans environnement ni fuite de secret. Il doit couvrir V002 et rejouer un canari transaction dans la même base afin de prouver la coexistence cross-family. + +| Scénario live | Obligation | +|-----------------------------------------|-------------| +| schema update V001 -> V002 | obligatoire | +| reopen idempotent | obligatoire | +| u64 > i64::MAX / u64::MAX | obligatoire | +| data empty / 16 MiB | obligatoire | +| identical/divergent acquisition | obligatoire | +| same pubkey+slot / distinct hash | obligatoire | +| additional observation + missing ref | obligatoire | +| collision observation | obligatoire | +| keyset filtered/unfiltered ASC/DESC | obligatoire | +| cursor hostile/cross-query/cross-family | obligatoire | +| concurrency | obligatoire | +| cancellation rollback | obligatoire | +| RawTransaction regression canary | obligatoire | +| cleanup isolation | obligatoire | + +## 14. Sizing + +| Tranche | Objet | État | +|---------|-------------------------------------------------------------------|---------| +| pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | CURRENT | +| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PLANNED | +| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PLANNED | +| pre.004 | mapping privé state/observation + get reads + hostile rows | PLANNED | +| pre.005 | acquisition atomique state+observation + idempotence/conflict | PLANNED | +| pre.006 | observation supplémentaire + races/cancellation unitaires | PLANNED | +| pre.007 | list RawAccountStateQuery + keyset cursor V1 account | PLANNED | +| pre.008 | 4 impl backend + 4 dispatch Store + conformance 10/10 | PLANNED | +| pre.009 | preuve PostgreSQL live account + coexistence RawTransaction | PLANNED | +| pre.010 | hardening/completeness cross-family + canaries ownership | PLANNED | +| pre.011 | gate technique final + replay live ciblé + graphes | PLANNED | +| pre.012 | réconciliation docs finale sans rouvrir le code | PLANNED | +| pre.013 | publication prep minimale + prompt 0.3.5 | PLANNED | +| rel.001 | stabilisation/tag après gate opérateur | PLANNED | + +## 15. Negative scope final + +| Surface | Doit rester absent en 0.3.4 | +|--------------------------------------------|-----------------------------| +| ksp-store-api redesign | oui | +| account retention/archive/purge | oui | +| CORE/STRUCTURAL persistence | oui | +| workers/jobs/backfills | oui | +| Store batch/priorité policy | oui | +| owner/provider/time/status queries/indexes | oui | +| Transport -> Store coupling | oui | +| PostgreSQL types via ksp-store-lib | oui | +| nouveau backend | oui | + +## 16. Gate opérateur prévu pour clôture technique + +```text +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +cargo test --workspace +cargo tree -p ksp-store-lib --edges normal +cargo tree -p ksp-store-lib -e features +cargo tree -p ksp-store-postgres-lib --edges normal +cargo tree --duplicates +``` + +Les preuves live restent séparées et explicitement `--ignored --nocapture`. + +## 17. Verdict `pre.001` + +Design : **PASS**. Les audits Rust/Markdown de l'overlay sont propres. + +Compilation/Clippy/tests post-overlay : **NON EXÉCUTÉS dans le conteneur d'assemblage**, faute d'exécutable `cargo`; ils restent à rejouer par l'opérateur. + +Implémentation V002/account : **PENDING**, volontairement non ouverte dans cette tranche.