v0.3.4-pre.001
This commit is contained in:
610
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
Normal file
610
docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md
Normal file
@@ -0,0 +1,610 @@
|
||||
<!-- file: docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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<State> / Page<Reference> | reference/query network == backend | SELECT ; list keyset | n/a | n/a | get absent -> None | oui, cursor account | WrongNetwork, QueryInvalid, PageLimitUnsupported, DataInvalid, ReadFailed | get exact + list matrix |
|
||||
| RawAccountStateWrite | state + observation | RawAcquisitionWriteOutcome | state + observation networks + exact same reference | transaction state+observation | exact existing -> AlreadyPresent | same full ref divergent -> RawConflict | n/a | non | WrongNetwork, Conflict, DataInvalid, WriteFailed | atomicité + identical/divergent + cancellation |
|
||||
| RawAccountObservationRead | observation_key | Option<Observation> | aucun input réseau ; backend fournit son network à la référence reconstruite | SELECT by PK | n/a | n/a | absent -> None | non | DataInvalid, ReadFailed | round-trip optional metadata |
|
||||
| RawAccountObservationWrite | observation | RawObservationWriteOutcome | observation.account.network == backend | transaction ; state FOR KEY SHARE + observation insert | exact existing -> AlreadyPresent | same key divergent -> RawConflict | state absent -> ReferenceNotFound | non | WrongNetwork, ReferenceNotFound, Conflict, DataInvalid, WriteFailed | missing ref + collision + concurrence |
|
||||
|
||||
## 4. Immutabilité V000/V001
|
||||
|
||||
Recalcul indépendant sur les bytes de l'archive stable :
|
||||
|
||||
```text
|
||||
V000 legacy SQL SHA-256 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450
|
||||
V001 logical resources = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51
|
||||
V001 resource count = 40
|
||||
```
|
||||
|
||||
L'algorithme V001 est celui du moteur stable : domaine `ksp-migration-resources-v1\0`, puis pour chaque ressource ordonnée `id + NUL + SQL bytes + NUL` avant SHA-256.
|
||||
|
||||
Règles V002 :
|
||||
|
||||
```text
|
||||
V000 : immuable
|
||||
V001 : immuable
|
||||
V002 : additive uniquement
|
||||
nom logique proposé : raw_account_state
|
||||
répertoire proposé : migrations/v002_raw_account_state/
|
||||
resources proposées : tables/ + constraints/ + indexes/
|
||||
hook migration supplémentaire : aucun
|
||||
```
|
||||
|
||||
Le binding réseau singleton V001 est réutilisé ; V002 ne duplique pas `network` dans chaque ligne métier.
|
||||
|
||||
## 5. Audit field-by-field du contrat account
|
||||
|
||||
### 5.1 `RawAccountStateReference` + `RawAccountState`
|
||||
|
||||
Matrice complète modèle -> physique :
|
||||
|
||||
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|
||||
|------------|----------------|-------------|--------------------|-----------------------|----------------------------------|------------------------|---------------------------------|
|
||||
| network | RawNetworkId | 1 | ksp_store_identity | exact backend network | binding runtime -> reference | aucune colonne row | aucun si guard exact |
|
||||
| pubkey | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
|
||||
| slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes | fixed bytes -> RawContentHash | hash bytes -> BYTEA | aucun ; pas de recalcul backend |
|
||||
| lamports | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| owner | Pubkey | 1 | BYTEA | exact 32 bytes | fixed bytes -> Pubkey | Pubkey bytes -> BYTEA | aucun |
|
||||
| executable | bool | 1 | BOOLEAN | NOT NULL | bool direct | bool direct | aucun |
|
||||
| rent_epoch | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX | decimal exact -> u64 | u64 -> decimal NUMERIC | narrowing interdit |
|
||||
| data | Box<[u8]> | 1 | BYTEA | 0..=16 MiB | bytes + borne -> RawAccountState | bytes exacts -> BYTEA | base64/parse interdits |
|
||||
|
||||
Vue synthétique de projection :
|
||||
|
||||
| Champ API | Type API | Projection V002 | Invariant |
|
||||
|------------|---------------------------|------------------------------------------|-----------------------------------------------------------------|
|
||||
| network | RawNetworkId | binding DB existant `ksp_store_identity` | pas de colonne par ligne ; guard exact avant I/O |
|
||||
| pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK |
|
||||
| slot | u64 | NUMERIC(20,0) NOT NULL | 0..18446744073709551615 ; aucun narrowing |
|
||||
| state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | `octet_length = 32` ; composant PK ; jamais recalculé par Store |
|
||||
| lamports | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| owner | Pubkey / 32 bytes | BYTEA NOT NULL | `octet_length = 32` |
|
||||
| executable | bool | BOOLEAN NOT NULL | mapping direct |
|
||||
| rent_epoch | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact |
|
||||
| data | Box<[u8]> | BYTEA NOT NULL | 0..=16 MiB ; vide autorisé ; aucun base64 |
|
||||
|
||||
L'identité physique canonique retenue est :
|
||||
|
||||
```text
|
||||
PRIMARY KEY (pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Le réseau fait partie de la référence API mais est porté physiquement par `ksp_store_identity` pour toute la base. Cette clé permet explicitement plusieurs états du même pubkey au même slot lorsque `state_hash` diffère.
|
||||
|
||||
`state_hash` est un digest **producer-owned**. Le backend ne le recalcule pas et ne suppose pas qu'il s'agit du hash de `data` seulement. L'idempotence vérifie donc le contenu complet malgré l'égalité du hash.
|
||||
|
||||
### 5.2 `RawAccountObservation`
|
||||
|
||||
Matrice complète modèle -> physique :
|
||||
|
||||
| Champ API | Rust | Cardinalité | PostgreSQL | Borne/constraint | Mapping read | Mapping write | Risque perte |
|
||||
|---------------------------|---------------------------------|-------------|--------------------|------------------------|----------------------------|------------------------|---------------------------|
|
||||
| observation_key | RawObservationKey | 1 | BYTEA | exact 32 bytes | fixed bytes -> key | key bytes -> BYTEA | aucun |
|
||||
| account.pubkey | Pubkey | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
|
||||
| account.slot | u64 | 1 | NUMERIC(20,0) | 0..u64::MAX + FK | decimal exact -> reference | decimal NUMERIC | narrowing interdit |
|
||||
| account.state_hash | RawContentHash | 1 | BYTEA | exact 32 bytes + FK | fixed bytes -> reference | bytes -> FK columns | aucun |
|
||||
| provider | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| protocol | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| acquisition_method | RawProvenanceCode | 1 | TEXT | 1..128 + safe alphabet | validate common code | code text | aucun |
|
||||
| origin | RawAcquisitionOrigin | 1 | TEXT | 5 valeurs stables | static decode | static encode | unknown DB -> DataInvalid |
|
||||
| received_at | RawTimestamp | 1 | BIGINT | 0..253402300799999 | checked i64 -> timestamp | u64 bounded -> i64 | borne API < i64::MAX |
|
||||
| capture_session_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| commitment | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| endpoint_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| filter_id | Option<RawProvenanceCode> | 0..1 | TEXT NULL | NULL ou code sûr | NULL/validate | Option -> NULL/text | aucun |
|
||||
| observed_at | Option<RawTimestamp> | 0..1 | BIGINT NULL | borne + <= received_at | NULL/checked timestamp | Option -> NULL/i64 | borne API < i64::MAX |
|
||||
| source_payload_hash | Option<RawContentHash> | 0..1 | BYTEA NULL | NULL ou 32 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucun |
|
||||
| source_payload_size_bytes | Option<u64> | 0..1 | BIGINT NULL | 0..=64 MiB | NULL/checked u64 | Option -> NULL/i64 | borne API < i64::MAX |
|
||||
| is_startup | Option<bool> | 0..1 | BOOLEAN NULL | nullable | NULL/bool | Option -> NULL/bool | ne pas inventer false |
|
||||
| transaction_signature | Option<RawTransactionSignature> | 0..1 | BYTEA NULL | NULL ou 64 bytes | NULL/fixed bytes | Option -> NULL/BYTEA | aucune FK transaction |
|
||||
| write_version | Option<u64> | 0..1 | NUMERIC(20,0) NULL | NULL ou 0..u64::MAX | NULL/decimal exact | Option -> NULL/NUMERIC | narrowing interdit |
|
||||
|
||||
Vue synthétique de projection :
|
||||
|
||||
| Champ API | Type API | Projection V002 | Invariant |
|
||||
|---------------------------|---------------------------------|------------------------|--------------------------------------------------|
|
||||
| observation_key | RawObservationKey / 32 bytes | BYTEA NOT NULL | PK family-local ; collision = comparaison exacte |
|
||||
| account.pubkey | Pubkey / 32 bytes | BYTEA NOT NULL | FK composite vers state |
|
||||
| account.slot | u64 | NUMERIC(20,0) NOT NULL | domaine u64 exact ; FK composite |
|
||||
| account.state_hash | RawContentHash / 32 bytes | BYTEA NOT NULL | FK composite |
|
||||
| provider | RawProvenanceCode | TEXT NOT NULL | 1..128 octets + alphabet logique stable V001 |
|
||||
| protocol | RawProvenanceCode | TEXT NOT NULL | idem V001 |
|
||||
| acquisition_method | RawProvenanceCode | TEXT NOT NULL | idem V001 |
|
||||
| origin | RawAcquisitionOrigin | TEXT NOT NULL | backfill/import/live/repair/replay |
|
||||
| received_at | RawTimestamp | BIGINT NOT NULL | 0..253402300799999 |
|
||||
| capture_session_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| commitment | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| endpoint_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| filter_id | Option<RawProvenanceCode> | TEXT NULL | mêmes checks V001 |
|
||||
| observed_at | Option<RawTimestamp> | BIGINT NULL | borne + `observed_at <= received_at` |
|
||||
| source_payload_hash | Option<RawContentHash> | BYTEA NULL | NULL ou 32 bytes |
|
||||
| source_payload_size_bytes | Option<u64> | BIGINT NULL | 0..=64 MiB ; borne API tient dans BIGINT |
|
||||
| is_startup | Option<bool> | BOOLEAN NULL | Yellowstone metadata observation-only |
|
||||
| transaction_signature | Option<RawTransactionSignature> | BYTEA NULL | NULL ou 64 bytes ; aucune FK transaction |
|
||||
| write_version | Option<u64> | NUMERIC(20,0) NULL | NULL ou domaine u64 exact |
|
||||
|
||||
La relation observation -> state est :
|
||||
|
||||
```text
|
||||
FOREIGN KEY (account_pubkey, account_slot, account_state_hash)
|
||||
REFERENCES ksp_raw_account_states(pubkey, slot, state_hash)
|
||||
```
|
||||
|
||||
Aucun `ON DELETE CASCADE` n'est nécessaire : l'API account ne possède ni delete ni retention. Aucune FK n'est créée vers `ksp_raw_transactions` pour `transaction_signature`; cette signature est une metadata source optionnelle et la transaction correspondante n'est pas garantie présente dans le Store.
|
||||
|
||||
`observation_key` est unique **dans la famille account**. Le contrat API expose des reads family-specific et ne définit aucune unicité globale transaction+account ; une registry globale ajouterait un couplage physique non requis.
|
||||
|
||||
### 5.3 `RawAccountStateQuery`
|
||||
|
||||
| Entrée query | Cardinalité | Projection physique |
|
||||
|-----------------------|------------------------|------------------------------------------------------------------|
|
||||
| network | obligatoire | guard DB mono-réseau + digest cursor |
|
||||
| pubkey | optionnel | filtre égalité ; aucune query owner |
|
||||
| slots.start_inclusive | optionnel | `slot >=` |
|
||||
| slots.end_inclusive | optionnel | `slot <=` |
|
||||
| direction | Ascending / Descending | ordre total entièrement inversé |
|
||||
| page.limit | u64 > 0 | limite physique PostgreSQL seulement ; `LIMIT + 1` |
|
||||
| page.cursor | opaque <= 4 KiB | V1 account fixe, authentification de contexte par digest SHA-256 |
|
||||
|
||||
Aucune query API par owner/provider/time/status n'existe. Aucun index pour ces dimensions ne sera introduit dans V002.
|
||||
|
||||
## 6. Audit kbot3 ciblé
|
||||
|
||||
L'archive historique contient notamment `k_sol_obs_account_observations`, `k_sol_core_account_states`, `AccountStateStore` et le pipeline de normalisation `account_state_normalization`. Cette surface mélangeait acquisition, normalisation CORE et processing policy ; elle n'est pas transposable telle quelle.
|
||||
|
||||
| Élément historique | Classement | Décision KSP |
|
||||
|----------------------------------------------------------|------------|-----------------------------------------------------------------------|
|
||||
| unicité observation_key et FK comme invariants physiques | REPRENDRE | garder le principe, avec clés KSP actuelles |
|
||||
| index account+slot pour navigation déterministe | REPRENDRE | réutiliser le principe seulement pour les queries réellement exposées |
|
||||
| NUMERIC(20,0) pour lamports/rent_epoch | REPRENDRE | étendre à slot/write_version pour le domaine u64 complet |
|
||||
| BIGSERIAL/id technique comme identité | REDESSINER | identité durable = `(pubkey, slot, state_hash)` |
|
||||
| pubkey/owner/hash en TEXT/base58 | REDESSINER | BYTEA fixed-width |
|
||||
| account data en base64 + `space` indépendant | REDESSINER | BYTEA complet ; longueur dérivée ; aucun double truth |
|
||||
| observation duplique l’état canonique | REDESSINER | observation référence un state complet séparé |
|
||||
| slot BIGINT/i64 | REDESSINER | NUMERIC(20,0) sans narrowing |
|
||||
| normalisation N1 -> CORE + processing ledger/version | REPORTER | future CORE/jobs/workers ; hors Store RAW 0.3.4 |
|
||||
| batch maximum 1000 / policy de sélection | REPORTER | policy worker/job ; pas pagination Store |
|
||||
| indexes owner/provider/method/time/status | REPORTER | aucune query publique actuelle ne les justifie |
|
||||
| status/error_message persistés dans observation RAW | REJETER | observation réussie uniquement ; erreurs sûres hors ligne métier |
|
||||
| sqlx et Store monolithique | REJETER | backend tokio-postgres séparé derrière ksp-store-lib |
|
||||
| maintenance CASCADE/TRUNCATE comme surface normale | REJETER | aucune destructive capability account |
|
||||
|
||||
Aucun code kbot3 ne sera copié. Les optional Yellowstone actuelles (`write_version`, `transaction_signature`, `is_startup`) n'existaient pas comme contrat durable équivalent dans cette persistence historique et restent donc définies uniquement par `ksp-store-api` actuel.
|
||||
|
||||
Points héritage supplémentaires explicitement classés :
|
||||
|
||||
- **REPRENDRE** : l'ancien repository utilisait déjà une transaction pour rendre atomiques state CORE + mutation observation + processing ledger ; seul le principe transactionnel est repris, jamais le couplage CORE/ledger.
|
||||
- **REPRENDRE** : l'unicité `observation_key` et la validation additive des contraintes/indexes sont de bons invariants physiques.
|
||||
- **REDESSINER** : `write_version`, `transaction_signature` et `is_startup` n'ont pas d'équivalent persistant account fiable dans cette surface historique ; V002 suit exclusivement les optionalités du contrat KSP actuel.
|
||||
- **REDESSINER** : l'idempotence historique `source_observation_id` + `ON CONFLICT DO UPDATE` est remplacée par clé producer-owned + comparaison exacte + conflit stable.
|
||||
- **REJETER** : les erreurs historiques interpolent l'erreur SQL dans `ks_core::Error::db(...)`; KSP conserve uniquement kind/phase statiques et sûres.
|
||||
|
||||
## 7. Design physique V002 figé par `pre.001`
|
||||
|
||||
### 7.1 Tables minimales
|
||||
|
||||
```text
|
||||
ksp_raw_account_states
|
||||
ksp_raw_account_observations
|
||||
```
|
||||
|
||||
Aucune table archive/tombstone account n'est ajoutée : `ksp-store-api` ne définit aucune capability de retention account.
|
||||
|
||||
### 7.2 Colonnes `ksp_raw_account_states`
|
||||
|
||||
```text
|
||||
pubkey BYTEA NOT NULL
|
||||
slot NUMERIC(20,0) NOT NULL
|
||||
state_hash BYTEA NOT NULL
|
||||
lamports NUMERIC(20,0) NOT NULL
|
||||
owner BYTEA NOT NULL
|
||||
executable BOOLEAN NOT NULL
|
||||
rent_epoch NUMERIC(20,0) NOT NULL
|
||||
data BYTEA NOT NULL
|
||||
```
|
||||
|
||||
Contraintes minimales : fixed width 32 pour `pubkey`, `owner`, `state_hash`; domaine u64 pour `slot`, `lamports`, `rent_epoch`; `octet_length(data) <= 16777216`; aucune borne basse sur data.
|
||||
|
||||
### 7.3 Colonnes `ksp_raw_account_observations`
|
||||
|
||||
```text
|
||||
observation_key BYTEA NOT NULL
|
||||
account_pubkey BYTEA NOT NULL
|
||||
account_slot NUMERIC(20,0) NOT NULL
|
||||
account_state_hash BYTEA NOT NULL
|
||||
provider TEXT NOT NULL
|
||||
protocol TEXT NOT NULL
|
||||
acquisition_method TEXT NOT NULL
|
||||
origin TEXT NOT NULL
|
||||
received_at_unix_millis BIGINT NOT NULL
|
||||
capture_session_id TEXT NULL
|
||||
commitment TEXT NULL
|
||||
endpoint_id TEXT NULL
|
||||
filter_id TEXT NULL
|
||||
observed_at_unix_millis BIGINT NULL
|
||||
source_payload_hash BYTEA NULL
|
||||
source_payload_size_bytes BIGINT NULL
|
||||
is_startup BOOLEAN NULL
|
||||
transaction_signature BYTEA NULL
|
||||
write_version NUMERIC(20,0) NULL
|
||||
```
|
||||
|
||||
Les contraintes provenance doivent être sémantiquement identiques à V001 : code 1..128 octets/alphabet sûr, origin fermé, timestamps bornés/ordonnés, source hash 32 bytes et source payload <= 64 MiB.
|
||||
|
||||
### 7.4 Indexes justifiés
|
||||
|
||||
Indexes provenant des PK :
|
||||
|
||||
```text
|
||||
PK ksp_raw_account_states(pubkey, slot, state_hash)
|
||||
PK ksp_raw_account_observations(observation_key)
|
||||
```
|
||||
|
||||
Un seul index métier additionnel est justifié par la query non filtrée :
|
||||
|
||||
```text
|
||||
ix_ksp_raw_account_states_slot_pubkey_state_hash(slot, pubkey, state_hash)
|
||||
```
|
||||
|
||||
Le PK couvre déjà efficacement la navigation lorsque `pubkey = ?` est imposé. Aucun index owner/provider/protocol/method/time/hash isolé n'est admis dans cette release.
|
||||
|
||||
## 8. Ordre total et pagination keyset
|
||||
|
||||
Ordre canonique :
|
||||
|
||||
```text
|
||||
Ascending : (slot ASC, pubkey ASC, state_hash ASC)
|
||||
Descending : (slot DESC, pubkey DESC, state_hash DESC)
|
||||
```
|
||||
|
||||
Le backend PostgreSQL compare `BYTEA` selon son ordre btree déterministe. Aucun `OFFSET` n'est utilisé.
|
||||
|
||||
Cursor account V1 proposé :
|
||||
|
||||
```text
|
||||
bytes 0..4 magic = "KSPA"
|
||||
byte 4 version = 1
|
||||
bytes 5..13 last_slot, u64 big-endian
|
||||
bytes 13..45 last_pubkey, 32 bytes
|
||||
bytes 45..77 last_state_hash, 32 bytes
|
||||
bytes 77..109 SHA-256 binding digest
|
||||
longueur fixe = 109 bytes
|
||||
```
|
||||
|
||||
Domaine de digest :
|
||||
|
||||
```text
|
||||
KSP/raw-account-state-cursor/v1
|
||||
```
|
||||
|
||||
Le digest lie au minimum : network, présence/valeur du filtre pubkey, direction, start/end slot, last slot, last pubkey, last state_hash. Le magic/domain distinct de `KSPT`/`KSP/raw-transaction-cursor/v1` rend le replay cross-family impossible même si les deux cursors ont 109 octets.
|
||||
|
||||
Le plafond physique PostgreSQL reste le précédent réel nécessaire à `LIMIT + 1` :
|
||||
|
||||
```text
|
||||
9_223_372_036_854_775_806
|
||||
```
|
||||
|
||||
Ce plafond n'est pas une policy métier KSP.
|
||||
|
||||
## 9. Algorithmes d'écriture
|
||||
|
||||
### 9.1 Acquisition state + observation atomiques
|
||||
|
||||
Préconditions avant I/O métier :
|
||||
|
||||
```text
|
||||
state.reference.network == backend.network
|
||||
observation.account.network == backend.network
|
||||
observation.account == state.reference
|
||||
```
|
||||
|
||||
Algorithme :
|
||||
|
||||
```text
|
||||
BEGIN
|
||||
INSERT state ... ON CONFLICT (pubkey, slot, state_hash) DO NOTHING RETURNING ...
|
||||
if inserted:
|
||||
entity_outcome = Inserted
|
||||
else:
|
||||
SELECT canonical state BY PK FOR UPDATE
|
||||
decode + compare exact lamports/owner/executable/rent_epoch/data
|
||||
identical -> AlreadyPresent
|
||||
divergent -> ERROR_CODE_RAW_CONFLICT
|
||||
|
||||
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
|
||||
if inserted:
|
||||
observation_outcome = Inserted
|
||||
else:
|
||||
SELECT observation BY key FOR UPDATE
|
||||
decode + compare exact reference + provenance + all optional metadata
|
||||
identical -> AlreadyPresent
|
||||
divergent -> ERROR_CODE_RAW_CONFLICT
|
||||
COMMIT
|
||||
```
|
||||
|
||||
Une divergence d'observation après insertion d'un nouveau state fait rollback de ce state. Aucun `ON CONFLICT DO UPDATE`, aucun `has_*` préalable, aucun overwrite silencieux.
|
||||
|
||||
### 9.2 Observation supplémentaire
|
||||
|
||||
```text
|
||||
validate network before I/O
|
||||
BEGIN
|
||||
SELECT state BY composite reference FOR KEY SHARE
|
||||
absent -> ReferenceNotFound
|
||||
INSERT observation ... ON CONFLICT (observation_key) DO NOTHING RETURNING ...
|
||||
collision -> SELECT FOR UPDATE + exact compare
|
||||
COMMIT
|
||||
```
|
||||
|
||||
Cette opération ne crée jamais implicitement le state et ne retransmet pas ses bytes.
|
||||
|
||||
## 10. Matrice de concurrence
|
||||
|
||||
| Scénario | Résultat permis | Invariant |
|
||||
|------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------|
|
||||
| 2 acquisitions identiques, même ref + même observation key | 1 Inserted/Inserted ; 1 AlreadyPresent/AlreadyPresent | attente unique + comparaison exacte ; aucun doublon |
|
||||
| 2 acquisitions même ref, contenu divergent | un gagnant durable ; autre `RAW_CONFLICT` | aucun overwrite ; résultat gagnant dépend de 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é
|
||||
```
|
||||
Reference in New Issue
Block a user