Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c84f3df8 | |||
| 36c683df9e | |||
| 510adcb49b | |||
| ab87fd23cd | |||
| 1a352aa8d0 | |||
| 919b4ed46d | |||
| f7c57f21c5 | |||
| d4f4237723 | |||
| ba574f3052 | |||
| 0683b70abe | |||
| eaf6fae94c | |||
| e19a202565 | |||
| 8309d829cc | |||
| aabe8dedb2 | |||
| d0da907c00 | |||
| 6b434750c5 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,8 +1,22 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 29 -->
|
<!-- version: 30 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
|
## 0.3.10 — Lower-layer RAW Transaction commune + qualification cross-source — 2026-09-08
|
||||||
|
|
||||||
|
`0.3.10` introduit `ksp-raw-transaction-lib` comme lower-layer source-neutral commune aux producteurs `RawTransaction`. La canonicalisation RAW v1 auparavant locale à `ksp-job-backfill-lib` est extraite sans modifier les golden bytes/hash existants ; le Backfill consomme désormais cette common crate sans créer de dépendance vers un Worker. La bibliothèque construit les modèles `RawTransaction`/`RawTransactionObservation` de `ksp-store-api`, conserve l'identité canonique `(network, signature)`, borne et redacted les entrées hostiles, et reste volontairement sans Transport, Config, runtime async, Job, Worker ou backend Store physique.
|
||||||
|
|
||||||
|
La common matérialise également un wire Solana source-neutral Legacy/V0/V1 avec sérialisation Base64 et extraction de signature depuis le matériau transactionnel complet. La représentation V1 couvre header, compiled instructions, configuration V1 et signatures terminales ; les gardes structurelles rejettent les états non supportés plutôt que d'inventer une compatibilité. `ksp-onchain-transport-lib` ajoute `get_block_observed`, qui conserve l'identité sûre de la route réellement gagnante tout en réutilisant la validation `getBlock` existante.
|
||||||
|
|
||||||
|
Les preuves cross-source ferment explicitement ce qui est qualifié et ce qui exige encore hydration. Un `blockSubscribe` standard `full/base64` peut produire le même RAW v1 qu'un `getBlock` HTTP sur le sous-ensemble déterministe prouvé. Helius `transactionSubscribe` reste un signal nécessitant hydration HTTP lorsque `blockTime`/version manquent. Yellowstone qualifie le wire Transaction V1 et la conservation du `block_time` dans Block, mais `Transaction` nécessite hydration pour le temps de bloc et le chemin Block complet reste conservateur tant que l'identité byte-for-byte entre meta protobuf et meta JSON HTTP n'est pas prouvée. Aucun RAW-direct Yellowstone complet ni feed EARLY n'est revendiqué sans preuve.
|
||||||
|
|
||||||
|
Le gate technique final `pre.007` passe les audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features avec `-D warnings`, `cargo test --workspace --all-targets --all-features`, les suites ciblées common/Transport/Backfill et les graphes Cargo demandés ; les smokes live opt-in restent ignorés conformément à leur contrat. La réconciliation `pre.008` aligne ensuite la terminologie durable des couches Store sur `D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN`, réserve `N1–N4` aux niveaux architecturaux et complète `README.md`/`USAGE.md` de `ksp-raw-transaction-lib`. Son gate opérateur repasse format, audits, `cargo check --workspace` et Clippy strict sur `0.3.10-pre.8`.
|
||||||
|
|
||||||
|
Le runtime concret a été volontairement retiré de `0.3.10` après recalibrage du sizing. La trajectoire devient `0.3.11` fondation runtime/persistence déterministe, `0.3.12` Yellowstone + hydration/continuité, `0.3.13` WS standard/Helius/HTTP live + convergence multi-source, `0.3.14` gap repair/hardening/smokes, `0.3.15` Desk d'ingestion et `0.3.16` extension Backfill multi-source. Le Job historique paramétré et le Worker continu restent des producteurs indépendants du même Store.
|
||||||
|
|
||||||
|
`prompts/030-V0_3_11_START_PROMPT.md` ouvre `0.3.11` uniquement depuis le tag stable `v0.3.10`. Son `pre.001` doit auditer la base réelle, le graphe de dépendances, le runtime/supervision, les bornes de channels, la persistence/déduplication déterministe, les snapshots et le dimensionnement avant toute implémentation lourde ; aucune source live complexe n'est requise pour fermer cette release.
|
||||||
|
|
||||||
## 0.3.9 — Worker API générique + audit exhaustif d'acquisition RawTransaction — 2026-09-05
|
## 0.3.9 — Worker API générique + audit exhaustif d'acquisition RawTransaction — 2026-09-05
|
||||||
|
|
||||||
`0.3.9` introduit `ksp-worker-api` comme façade générique, courte et runtime-neutral pour services continus. La surface stable reste Core-only et expose `WorkerId`, `WorkerKindCode`, `WorkerState`, `WorkerHealth`, `WorkerActivity`, `WorkerLifecycle`, `WorkerStopToken`, `WorkerSnapshotSequence`, `WorkerSnapshot`, `WorkerSnapshotFuture` et `WorkerSnapshotSource`. Les identités sont bornées et redacted, le lifecycle possède des transitions explicites avec terminaux immuables, le stop token est partagé/idempotent, la séquence ne wrappe pas et le snapshot source latest-value reste object-safe, `Send + Sync` et implémentable depuis l'extérieur. L'API ne possède aucun runtime `start/stop` universel, aucune sémantique Job/checkpoint, aucun domaine Solana et aucune dépendance Transport/Store/Config/Tauri.
|
`0.3.9` introduit `ksp-worker-api` comme façade générique, courte et runtime-neutral pour services continus. La surface stable reste Core-only et expose `WorkerId`, `WorkerKindCode`, `WorkerState`, `WorkerHealth`, `WorkerActivity`, `WorkerLifecycle`, `WorkerStopToken`, `WorkerSnapshotSequence`, `WorkerSnapshot`, `WorkerSnapshotFuture` et `WorkerSnapshotSource`. Les identités sont bornées et redacted, le lifecycle possède des transitions explicites avec terminaux immuables, le stop token est partagé/idempotent, la séquence ne wrappe pas et le snapshot source latest-value reste object-safe, `Send + Sync` et implémentable depuis l'extérieur. L'API ne possède aucun runtime `start/stop` universel, aucune sémantique Job/checkpoint, aucun domaine Solana et aucune dépendance Transport/Store/Config/Tauri.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 489
|
# version: 505
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-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", "crates/ksp-worker-api"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.9"
|
version = "0.3.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
34
ROADMAP.md
34
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 104 -->
|
<!-- version: 108 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
|
|
||||||
### Cadrage
|
### Cadrage
|
||||||
|
|
||||||
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/STRUCTURAL/DECODED/DOMAIN stabilisés.
|
||||||
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
||||||
|
|
||||||
### Releases fonctionnelles décidées/pressenties
|
### Releases fonctionnelles décidées/pressenties
|
||||||
@@ -87,13 +87,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
- **RAW** et **STRUCTURAL** ne nécessitent aucun décodage Program.
|
||||||
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en N1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
- La progression n'est pas une chaîne obligatoire pour chaque famille : une donnée event-only peut s'arrêter en D1, et un état de compte pourra aller directement vers un futur decoder si aucune décomposition STRUCTURAL utile n'existe.
|
||||||
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
- À la fin des couches horizontales RAW/STRUCTURAL réellement persistées, ajouter les jobs/workers/apps nécessaires avant d'ouvrir la couche suivante.
|
||||||
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
- À partir de **DECODED**, avancer verticalement groupe par groupe ; **DOMAIN** désigne les projections métier et la matérialisation est le processus qui les produit.
|
||||||
|
|
||||||
## 0.3.x — RAW / acquisition persistée
|
## 0.3.x — RAW / acquisition persistée
|
||||||
|
|
||||||
- [X] `0.3.1` — `ksp-store-api` stable : modèles N1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
- [X] `0.3.1` — `ksp-store-api` stable : modèles D1 RAW backend-agnostic `RawTransaction` et `RawAccountState` avec observations, provenance, payload/hash/timestamps bornés, 10 capabilities object-safe, queries cursorisées sans plafond métier arbitraire, outcomes idempotence/conflit et lifecycle logique rétention/tombstone/force-rehydrate ; aucun backend physique, Config, runtime Store, notification dédiée ni surface STRUCTURAL/DECODED/DOMAIN.
|
||||||
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
- [X] `0.3.2` — `ksp-store-lib` + `ksp-store-postgres-lib` stables comme fondation runtime/backend PostgreSQL : feature `postgres` par défaut, `Store` lié à un unique `RawNetworkId`, Config `std.store` avec targets/bases `devnet`/`mainnet`/`testnet`, pool Deadpool borné, `tokio-postgres`, TLS Rustls `Disabled`/`VerifyFull`, moteur de migrations privé `V000` + SHA-256/advisory lock, health/readiness portable et close borné. Gate complet + PostgreSQL réel major 17 verts ; aucune table/capability `RawTransaction`/`RawAccountState` métier n'est encore ajoutée.
|
||||||
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
- [X] `0.3.3` — Vertical slice PostgreSQL `RawTransaction` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : six capabilities transaction/observation/rétention, V001 physique liée à un réseau, acquisition canonical+observation atomique, idempotence/conflit, get/list keyset cursorisé, archive/purge/tombstone/ForceRehydrate, hardening des erreurs et du schéma, concurrence et rollback validés sur PostgreSQL 17.
|
||||||
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
- [X] `0.3.4` — Vertical slice PostgreSQL `RawAccountState` complète sur `ksp-store-lib` + `ksp-store-postgres-lib` : quatre capabilities account ajoutées aux six transaction pour une conformance RAW 10/10, V002 additive de 32 ressources au-dessus de V000/V001 immuables, state+observation atomiques, idempotence/conflit exacts, metadata Yellowstone observation-only, get/list keyset `(slot,pubkey,state_hash)` avec cursor KSPA anti-replay, hardening cross-family et live validé sur PostgreSQL 17 sans rétention destructive account.
|
||||||
@@ -102,21 +102,25 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
- [X] `0.3.7` — `ksp-app-backfill-desk` livrée comme Desk Tauri KSP spécialisée : composition Config Mainnet/Devnet/Testnet, quatre scopes HTTP, validation/start single-run, monitoring latest-value `ksp-backfill-status`, Cancel ciblé/idempotent, Resume in-session par checkpoint Rust-only réémis pour un nouveau JobId, autocomplete libre depuis `ksp-core-lib`, hardening IPC/dependency boundaries et build Linux `.deb`/`.rpm`/`.AppImage`. Aucun SQL/backend/provider physique ni checkpoint durable n’est exposé.
|
||||||
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
- [X] `0.3.8` — `ksp-app-store-desk` V1 RAW livrée sur le gabarit KSP courant comme application Tauri read-only backend-neutral. La Desk compose Config + Logging + `ksp-store-lib`, expose health/runtime sûrs, DataTables `serverSide` pour `RawTransaction`, `RawAccountState` et leurs observations, détails bornés, provenance sûre et états de rétention/tombstone, sans SQL/backend physique/Transport dans l'application. La nouvelle inspection random-access `offset + limit + counts exacts` reste distincte de la pagination machine cursor/keyset conservée pour workers/backfills/replays. Le gate final et les builds Linux `.deb`/`.rpm` sont verts.
|
||||||
- [X] `0.3.9` — `ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
|
- [X] `0.3.9` — `ksp-worker-api` stable comme API générique et volontairement courte pour services continus : identité/kind bornés, lifecycle explicite, health/activity sûrs, stop token partagé, séquence/snapshot latest-value et `WorkerSnapshotSource` object-safe, avec dépendance Core-only et sans runtime/start-stop universel, Solana, Transport, Store, Config ou Tauri. Après freeze Worker API, l'audit `RawTransaction` a été consolidé dans une synthèse Store-centrique exhaustive séparant possibilités/support/preuve et classant HTTP, WS standard/provider, Yellowstone gRPC, blocs/slots, discovery + hydration, replay de continuité, archives et sources EARLY. Le Job Backfill historique paramétré et le Worker Ingest live start/stop sont deux producteurs indépendants du même Store ; `mainnet` est désormais l'identité KSP canonique et `mainnet-beta` un alias legacy/externe. Le handoff retient `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW v1 pour `0.3.10`, sans edge Job ↔ Worker.
|
||||||
- [ ] `0.3.10` — Introduire `ksp-raw-transaction-lib` puis `ksp-worker-raw-transaction-ingest-lib` **multi-source dès V1**. La lower-layer commune extrait de `ksp-job-backfill-lib` la canonicalisation RAW v1 sans changer ses golden bytes/hash ni ses campagnes historiques. Le worker continu est démarré/arrêté sans paramètres métier de campagne, ouvre les sources/rôles activés par la composition Config, acquiert depuis son démarrage, persiste `RawTransaction + RawTransactionObservation`, publie ses snapshots/notifications indépendamment de leurs lecteurs et répare uniquement ses propres gaps de continuité live. HTTP, WS, Yellowstone gRPC et extensions provider sont des capabilities orthogonales : HTTP peut servir au live/hydration et gRPC/WS ne deviennent jamais synonymes de Worker. La release porte seulement les adaptations Transport/Config/common réellement justifiées par l'audit (`get_block_observed`, projections full source-neutral, provenance sûre, replay `from_slot`, profils/capabilities nécessaires), avec support architectural conservé même lorsque certains smokes provider restent bloqués par tier. Aucun nouveau backfill multi-stratégie, decode Program, backend physique direct ni dépendance Worker ↔ Job.
|
- [X] `0.3.10` — Lower-layer RAW Transaction commune stabilisée : `ksp-raw-transaction-lib` possède la canonicalisation RAW v1 source-neutral partagée, le Backfill est migré sans changement des golden bytes/hash, Transport expose `get_block_observed`, le wire Solana Legacy/V0/V1 est matérialisé et les canaris HTTP/WS standard/Helius/Yellowstone qualifient précisément les voies directes ou nécessitant hydration. La release se ferme sans runtime Worker concret ; celui-ci commence en `0.3.11` selon le redécoupage `0.3.11`–`0.3.14`.
|
||||||
- [ ] `0.3.11` — Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP spécialisée qui choisit et supervise **une ou plusieurs sources/méthodes** réellement admises par `0.3.10` : lifecycle, health, rates, backpressure, reconnect/recovery, gap state et compteurs sûrs. La Desk ne réimplémente ni discovery, hydration, déduplication, reprise ni persistence ; l'inspection détaillée des RAW persistés reste la responsabilité de `ksp-app-store-desk`.
|
- [ ] `0.3.11` — Introduire la **fondation runtime** de `ksp-worker-raw-transaction-ingest-lib` sans source live complexe : crate/dependency firewall, settings source-neutral, handle/start-stop, lifecycle, snapshots, supervisor, bounded channels et pipeline de persistence/déduplication déterministe. Aucun protocole live n'est encore requis pour fermer cette release.
|
||||||
- [ ] `0.3.12` — Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres (par exemple block/slot, replay provider lorsqu'il existe), sans supposer qu'une source live WS constitue un historique universel.
|
- [ ] `0.3.12` — Ajouter au Worker la voie **Yellowstone + hydration HTTP** : transactions/blocks/status, projection vers la common RAW, provenance sûre, `from_slot`, replay info, frontier de run et continuité propre au run, avec fixtures déterministes et smokes accessibles sans prétendre à un RAW-direct lorsque les métadonnées restent incomplètes.
|
||||||
|
- [ ] `0.3.13` — Ajouter les voies **WS standard / Helius / HTTP live** et leur convergence multi-source : `logsSubscribe + getTransaction`, `blockSubscribe`, `transactionSubscribe + hydration`, polling blocs HTTP, observations multiples, content conflict explicite, coalescence et backpressure sans second actor Transport.
|
||||||
|
- [ ] `0.3.14` — Fermer le Worker par le **gap repair/hardening multi-source** : replay natif, source redondante, scan HTTP blocs, hydration de réparation, unresolved gaps visibles, politiques degraded/unhealthy/faulted, shutdown pendant repair, smokes provider accessibles et adapter EARLY seulement si réellement prouvé.
|
||||||
|
- [ ] `0.3.15` — Introduire `ksp-app-raw-transaction-ingest-desk`, Desk Tauri KSP spécialisée qui choisit et supervise une ou plusieurs sources/méthodes réellement admises par le Worker finalisé : lifecycle, health, rates, backpressure, reconnect/recovery, gap state et compteurs sûrs. La Desk ne réimplémente ni discovery, hydration, déduplication, reprise ni persistence.
|
||||||
|
- [ ] `0.3.16` — Étendre `ksp-job-backfill-lib` et `ksp-app-backfill-desk` au **backfill multi-source/multi-stratégie** à partir de la même matrice d'acquisition auditée en `0.3.9`. Conserver la stratégie actuelle `getSignaturesForAddress + getTransaction` comme première voie HTTP valide, puis ajouter seulement les voies historiques/catch-up/gap-repair réellement pertinentes et sûres, sans supposer qu'une source live WS constitue un historique universel.
|
||||||
|
|
||||||
### TODO/IDEAS — applications spécialisées et control plane
|
### TODO/IDEAS — applications spécialisées et control plane
|
||||||
|
|
||||||
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
- [ ] **TODO** — faire évoluer `ksp-app-store-desk` avec chaque nouvelle couche réellement persistée : RAW d'abord, puis STRUCTURAL, DECODED, journal de processing/materialization et projections DOMAIN selon les contrats effectivement disponibles. Les vues avancées restent backend-agnostiques et passent uniquement par `ksp-store-lib`.
|
||||||
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'un niveau N3/D3 de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
- [ ] **TODO** — différer une future `ksp-app-control-desk` jusqu'à ce que KSP dispose au minimum d'une couche D3 DECODED de processing/materialization exploitable et de plusieurs decoders réels. Cette application sera une surface **end-user simplifiée** : démarrer/arrêter les flux autorisés, lancer une recherche courante, voir l'état global et les erreurs importantes. Elle ne recopiera pas le diagnostic détaillé, les réglages avancés, les tables complètes ni toutes les fonctions des `ksp-app-*-desk` spécialisées, qui resteront les outils d'administration, développement et investigation approfondie.
|
||||||
|
|
||||||
### TODO/IDEAS — taxonomie N1, processing et rétention
|
### TODO/IDEAS — taxonomie D1, processing et rétention
|
||||||
|
|
||||||
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille N1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
- [ ] **TODO** — maintenir la matrice d’admission HTTP/WS/gRPC/provider lors de toute nouvelle famille D1 : plusieurs sources ne convergent vers un même struct que si elles satisfont la même sémantique sans perte.
|
||||||
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs indépendants `0.3.10` Worker live / `0.3.12` Backfill historique.
|
- [X] **TODO `0.3.9`** — matrice RAW Transaction unique produite et consolidée dans `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`, avec séparation possibilités/support/preuve, capabilities orthogonales aux producteurs, provenance/déduplication/continuité et handoffs désormais scindés `0.3.11`–`0.3.14` Worker live / `0.3.16` Backfill historique.
|
||||||
- [ ] **TODO Helius `0.3.10`** — ajouter uniquement les profils/capabilities Helius réellement nécessaires au Worker live après revalidation des surfaces/tier courants ; réutiliser exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config et la surface `transactionSubscribe` déjà Transport-owned, sans SDK provider ni second client parallèle.
|
- [ ] **TODO Helius `0.3.13`** — ajouter uniquement les profils/capabilities Helius réellement nécessaires au Worker live après revalidation des surfaces/tier courants ; réutiliser exclusivement `KSP_SECRET_HELIUS_API_KEY` via Config et la surface `transactionSubscribe` déjà Transport-owned, sans SDK provider ni second client parallèle.
|
||||||
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données N1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
|
- [X] **TODO réseau** — identité KSP canonique fixée à `mainnet` dans Config/Store/Transport/tests ; `mainnet-beta` reste uniquement un alias legacy/externe lorsque la frontière provider l'exige. Les données D1 RAW antérieures restent non autoritaires pendant cette phase et aucune compatibilité de base de test n'impose l'ancien libellé.
|
||||||
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
- [X] `RawAccountState` + observation — contrat commun stabilisé en `0.3.1` avec bytes complets + slot, provenance séparée et enrichissements source-specific optionnels ; la persistence PostgreSQL physique est complétée en `0.3.4` avec les quatre capabilities account et la conformance RAW 10/10.
|
||||||
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
- [ ] **TODO** — statut/commitment transactionnel restant : `0.3.5` couvre uniquement le fait passif d’exécution `slot + signature + outcome`; réauditer séparément `signatureSubscribe` et `getSignatureStatuses` lorsqu’un consumer de commitment/snapshot réel apparaît, sans fusionner snapshot, transition et execution update dans un modèle Option-soup.
|
||||||
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
- [ ] **IDEA** — logs realtime enrichis : `logsSubscribe` alimente déjà la projection minimale `TransactionExecutionEvent`, mais un éventuel `TransactionLogEvent` portant les lignes de log reste différé dans `docs/IDEAS.md` jusqu’à démonstration d’un consumer et de bornes explicites. `logMessages` reste dans `RawTransaction` jusqu’à STRUCTURAL ; le wake-up post-commit reste distinct et Store API-owned conformément à `KSP-NOTIFY-*`.
|
||||||
@@ -129,13 +133,13 @@ RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
|||||||
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
- [X] rétention physique `RawTransaction` PostgreSQL — `0.3.3` matérialise `Full -> Archived -> Purged`, tombstone et ForceRehydrate atomiques ; `Compacted` reste explicitement unsupported tant qu’aucune représentation compactée réelle n’existe.
|
||||||
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
- [ ] **TODO** — policy de rétention/compaction : définir les critères d’éligibilité fondés sur les preuves de processing et la maintenance worker/job ; Store applique une transition demandée mais ne décide pas seul qu’un RAW peut être archivé/purgé, et la compaction physique ne sera ajoutée qu’avec un besoin réel.
|
||||||
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
- [X] frontière `ksp-interface-lib` / `ksp-store-api` — ownership documenté et canaris de non-duplication stabilisés en `0.3.1`; les events passifs non persistés restent Interface, les modèles persistants/replayables restent Store API.
|
||||||
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de N2/N3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
- [ ] **IDEA** — réauditer la structure de processing/decode/materialization historique kbot2/kbot3 lors de l’ouverture de D2/D3 ; conserver l’isolation instruction/CPI et les statuts terminal/versionnés, sans reprendre automatiquement le schéma SQL historique.
|
||||||
|
|
||||||
## Série STRUCTURAL suivante
|
## Série STRUCTURAL suivante
|
||||||
|
|
||||||
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
- [ ] Définir la persistence STRUCTURAL canonique Solana générique pour les familles réellement décomposables.
|
||||||
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
- [ ] Implémenter en priorité `RawTransaction -> STRUCTURAL` sans decoder Program : transaction/message, comptes/références, instructions top-level, CPI/inner instructions, logs/meta/balances/return data et relations structurelles.
|
||||||
- [ ] Vérifier avant extension si d'autres familles N1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
- [ ] Vérifier avant extension si d'autres familles D1 possèdent une vraie décomposition STRUCTURAL utile ; ne pas créer de niveau vide par convention.
|
||||||
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
- [ ] Ajouter replay/backfill RAW -> STRUCTURAL avec processing versionné.
|
||||||
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
- [ ] Ajouter le worker/service STRUCTURAL utile sans le coupler à un worker RAW concret.
|
||||||
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
- [ ] Faire évoluer `ksp-app-store-desk` avec des vues/requêtes STRUCTURAL lorsque cette couche est réellement persistée ; ne pas créer une seconde application de browsing des mêmes données.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
# file: crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
# version: 4
|
# version: 7
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-job-backfill-lib"
|
name = "ksp-job-backfill-lib"
|
||||||
@@ -13,6 +13,7 @@ ksp-core-lib = { path = "../ksp-core-lib" }
|
|||||||
ksp-job-api = { path = "../ksp-job-api" }
|
ksp-job-api = { path = "../ksp-job-api" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||||
|
ksp-raw-transaction-lib = { path = "../ksp-raw-transaction-lib" }
|
||||||
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
|
ksp-store-lib = { path = "../ksp-store-lib", default-features = false }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
@@ -20,6 +21,9 @@ tokio = { workspace = true, features = ["macros", "sync"] }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
|
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- file: crates/ksp-job-backfill-lib/README.md -->
|
<!-- file: crates/ksp-job-backfill-lib/README.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 2 -->
|
||||||
|
|
||||||
# ksp-job-backfill-lib
|
# ksp-job-backfill-lib
|
||||||
|
|
||||||
`ksp-job-backfill-lib` implémente le premier job historique concret de KSP : un backfill borné de transactions Solana vers la couche RAW durable.
|
`ksp-job-backfill-lib` implémente le premier job historique concret de KSP : un backfill borné de transactions Solana vers la couche RAW durable.
|
||||||
|
|
||||||
La crate compose les contrats Job, Transport observé et Store backend-neutral sans posséder leurs politiques internes. Elle couvre l'admission, la découverte historique, l'hydratation `getTransaction`, la conversion RAW v1, la persistance atomique, la concurrence bornée, la frontier contiguë, le checkpoint caller-owned, l'annulation coopérative et les snapshots latest-value.
|
La crate compose les contrats Job, Transport observé, common RAW et Store backend-neutral sans posséder leurs politiques internes. Elle couvre l'admission, la découverte historique, l'hydratation `getTransaction`, l'adaptation vers `ksp-raw-transaction-lib`, la persistance atomique, la concurrence bornée, la frontier contiguë, le checkpoint caller-owned, l'annulation coopérative et les snapshots latest-value.
|
||||||
|
|
||||||
## Identité
|
## Identité
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ ksp-core-lib
|
|||||||
ksp-job-api
|
ksp-job-api
|
||||||
ksp-logging-lib
|
ksp-logging-lib
|
||||||
ksp-onchain-transport-lib
|
ksp-onchain-transport-lib
|
||||||
|
ksp-raw-transaction-lib
|
||||||
ksp-store-lib (default-features = false)
|
ksp-store-lib (default-features = false)
|
||||||
futures-util
|
futures-util
|
||||||
serde_json
|
serde_json
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/src/conversion.rs
|
// file: crates/ksp-job-backfill-lib/src/conversion.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
use sha2::Digest; // rust-rules: trait-import
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
/// KSP-owned source-independent RAW transaction format identifier produced by this Backfill vertical.
|
|
||||||
pub const RAW_TRANSACTION_FORMAT_ID: &str = "ksp.solana.raw_transaction";
|
|
||||||
/// Initial KSP-owned RAW transaction format version produced by this Backfill vertical.
|
|
||||||
pub const RAW_TRANSACTION_FORMAT_VERSION: u32 = 1;
|
|
||||||
|
|
||||||
const RAW_TRANSACTION_METHOD_CODE: &str = "getTransaction";
|
const RAW_TRANSACTION_METHOD_CODE: &str = "getTransaction";
|
||||||
const RAW_TRANSACTION_OBSERVATION_CONTRACT_VERSION: u32 = 1;
|
const RAW_TRANSACTION_OBSERVATION_CONTRACT_VERSION: u32 = 1;
|
||||||
const RAW_TRANSACTION_PROTOCOL_CODE: &str = "solana.http.json_rpc";
|
const RAW_TRANSACTION_PROTOCOL_CODE: &str = "solana.http.json_rpc";
|
||||||
|
|
||||||
/// Complete in-memory RAW transaction acquisition ready for the later Store persistence tranche.
|
/// Complete in-memory RAW transaction acquisition ready for Store persistence.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BackfillRawAcquisition {
|
pub struct BackfillRawAcquisition {
|
||||||
inner: Box<BackfillRawAcquisitionInner>,
|
inner: Box<BackfillRawAcquisitionInner>,
|
||||||
@@ -20,35 +15,34 @@ pub struct BackfillRawAcquisition {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct BackfillRawAcquisitionInner {
|
struct BackfillRawAcquisitionInner {
|
||||||
transaction: ksp_store_lib::RawTransaction,
|
acquisition: ksp_raw_transaction_lib::RawTransactionAcquisition,
|
||||||
observation: ksp_store_lib::RawTransactionObservation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::BackfillRawAcquisition {
|
impl crate::BackfillRawAcquisition {
|
||||||
/// Returns the canonical RAW transaction produced from the typed Transport response.
|
/// Returns the canonical RAW transaction produced through the source-neutral common layer.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn transaction(&self) -> &ksp_store_lib::RawTransaction {
|
pub fn transaction(&self) -> &ksp_store_lib::RawTransaction {
|
||||||
return &self.inner.transaction;
|
return self.inner.acquisition.transaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the acquisition observation whose provenance records the actual successful endpoint.
|
/// Returns the Backfill-owned acquisition observation linked by the common assembly layer.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn observation(&self) -> &ksp_store_lib::RawTransactionObservation {
|
pub fn observation(&self) -> &ksp_store_lib::RawTransactionObservation {
|
||||||
return &self.inner.observation;
|
return self.inner.acquisition.observation();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consumes the in-memory acquisition into the canonical transaction and its observation.
|
/// Consumes the in-memory acquisition into the canonical transaction and its Backfill-owned observation.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_parts(self) -> (ksp_store_lib::RawTransaction, ksp_store_lib::RawTransactionObservation) {
|
pub fn into_parts(self) -> (ksp_store_lib::RawTransaction, ksp_store_lib::RawTransactionObservation) {
|
||||||
let inner = *self.inner;
|
let inner = *self.inner;
|
||||||
return (inner.transaction, inner.observation);
|
return inner.acquisition.into_parts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of hydrating one deterministic Backfill candidate through observed `getTransaction`.
|
/// Result of hydrating one deterministic Backfill candidate through observed `getTransaction`.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum BackfillHydrationOutcome {
|
pub enum BackfillHydrationOutcome {
|
||||||
/// The RPC returned one complete transaction and conversion produced canonical RAW plus provenance.
|
/// The RPC returned one complete transaction and common conversion produced canonical RAW plus Backfill provenance.
|
||||||
Available(crate::BackfillRawAcquisition),
|
Available(crate::BackfillRawAcquisition),
|
||||||
/// The RPC returned JSON `null`; only the canonical transaction identity exists and no provenance is fabricated.
|
/// The RPC returned JSON `null`; only the canonical transaction identity exists and no provenance is fabricated.
|
||||||
Missing(ksp_store_lib::RawTransactionReference),
|
Missing(ksp_store_lib::RawTransactionReference),
|
||||||
@@ -71,11 +65,12 @@ impl crate::BackfillHydrationOutcome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hydrates one candidate with the typed observed Transport path and converts it to canonical RAW v1.
|
/// Hydrates one candidate with the typed observed Transport path and converts it through the common RAW v1 layer.
|
||||||
///
|
///
|
||||||
/// The caller supplies the local receipt timestamp because wall-clock ownership remains outside this
|
/// The caller supplies the local receipt timestamp because wall-clock ownership remains outside this
|
||||||
/// pure conversion tranche. Transport retains endpoint selection and retry. This function never
|
/// pure conversion path. Transport retains endpoint selection and retry. Backfill retains campaign
|
||||||
/// persists to Store; persistence begins in `pre.007`.
|
/// provenance and observation-key ownership; the common crate owns signature parsing, RAW v1
|
||||||
|
/// canonicalization, content hashing and transaction/observation assembly.
|
||||||
pub async fn hydrate_backfill_candidate(
|
pub async fn hydrate_backfill_candidate(
|
||||||
transport: &ksp_onchain_transport_lib::HttpTransportPool,
|
transport: &ksp_onchain_transport_lib::HttpTransportPool,
|
||||||
request: &crate::BackfillRequest,
|
request: &crate::BackfillRequest,
|
||||||
@@ -105,12 +100,12 @@ pub async fn hydrate_backfill_candidate(
|
|||||||
std::option::Option::None => return std::result::Result::Ok(crate::BackfillHydrationOutcome::Missing(reference)),
|
std::option::Option::None => return std::result::Result::Ok(crate::BackfillHydrationOutcome::Missing(reference)),
|
||||||
};
|
};
|
||||||
let fields = CanonicalTransactionFields {
|
let fields = CanonicalTransactionFields {
|
||||||
slot: transaction.slot(),
|
|
||||||
block_time: transaction.block_time(),
|
block_time: transaction.block_time(),
|
||||||
transaction: transaction.transaction(),
|
|
||||||
meta: transaction.meta(),
|
meta: transaction.meta(),
|
||||||
version: transaction.version(),
|
slot: transaction.slot(),
|
||||||
|
transaction: transaction.transaction(),
|
||||||
transaction_index: transaction.transaction_index(),
|
transaction_index: transaction.transaction_index(),
|
||||||
|
version: transaction.version(),
|
||||||
};
|
};
|
||||||
let acquisition = convert_available_fields(request, reference, fields, provider.as_str(), endpoint.as_str(), received_at);
|
let acquisition = convert_available_fields(request, reference, fields, provider.as_str(), endpoint.as_str(), received_at);
|
||||||
return match acquisition {
|
return match acquisition {
|
||||||
@@ -119,49 +114,22 @@ pub async fn hydrate_backfill_candidate(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one validated Base58 signature to exactly 64 canonical bytes without a Solana SDK dependency.
|
/// Decodes one validated Base58 signature through the source-neutral common parser while preserving Backfill error semantics.
|
||||||
pub(crate) fn decode_backfill_signature(signature: &crate::BackfillSignature) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionSignature> {
|
pub(crate) fn decode_backfill_signature(signature: &crate::BackfillSignature) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionSignature> {
|
||||||
let text = signature.as_str().as_bytes();
|
let parsed = ksp_raw_transaction_lib::parse_raw_transaction_signature(signature.as_str());
|
||||||
let mut decoded = [0_u8; 64];
|
return match parsed {
|
||||||
let mut leading_zeroes = 0_usize;
|
std::result::Result::Ok(signature) => std::result::Result::Ok(signature),
|
||||||
for byte in text {
|
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("signature")),
|
||||||
if *byte != b'1' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
leading_zeroes += 1;
|
|
||||||
}
|
|
||||||
for byte in text {
|
|
||||||
let digit = match base58_digit(*byte) {
|
|
||||||
std::option::Option::Some(digit) => digit,
|
|
||||||
std::option::Option::None => return std::result::Result::Err(conversion_error("signature")),
|
|
||||||
};
|
};
|
||||||
let mut carry = u32::from(digit);
|
|
||||||
for output in decoded.iter_mut().rev() {
|
|
||||||
let value = (u32::from(*output) * 58) + carry;
|
|
||||||
*output = (value & 0xff) as u8;
|
|
||||||
carry = value >> 8;
|
|
||||||
}
|
|
||||||
if carry != 0 {
|
|
||||||
return std::result::Result::Err(conversion_error("signature"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let significant_len = match decoded.iter().position(|byte| return *byte != 0) {
|
|
||||||
std::option::Option::Some(index) => decoded.len() - index,
|
|
||||||
std::option::Option::None => 0,
|
|
||||||
};
|
|
||||||
if leading_zeroes + significant_len != decoded.len() {
|
|
||||||
return std::result::Result::Err(conversion_error("signature"));
|
|
||||||
}
|
|
||||||
return std::result::Result::Ok(ksp_store_lib::RawTransactionSignature::new(decoded));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CanonicalTransactionFields<'a> {
|
struct CanonicalTransactionFields<'a> {
|
||||||
slot: u64,
|
|
||||||
block_time: std::option::Option<i64>,
|
block_time: std::option::Option<i64>,
|
||||||
transaction: &'a ksp_onchain_transport_lib::SolanaEncodedTransaction,
|
|
||||||
meta: &'a ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>,
|
meta: &'a ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>,
|
||||||
version: &'a ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
slot: u64,
|
||||||
|
transaction: &'a ksp_onchain_transport_lib::SolanaEncodedTransaction,
|
||||||
transaction_index: &'a ksp_onchain_transport_lib::SolanaWireField<u32>,
|
transaction_index: &'a ksp_onchain_transport_lib::SolanaWireField<u32>,
|
||||||
|
version: &'a ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonical_reference(request: &crate::BackfillRequest, candidate: &crate::BackfillCandidate) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
fn canonical_reference(request: &crate::BackfillRequest, candidate: &crate::BackfillCandidate) -> ksp_core_lib::Result<ksp_store_lib::RawTransactionReference> {
|
||||||
@@ -184,30 +152,9 @@ fn convert_available_fields(
|
|||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
received_at: ksp_store_lib::RawTimestamp,
|
received_at: ksp_store_lib::RawTimestamp,
|
||||||
) -> ksp_core_lib::Result<crate::BackfillRawAcquisition> {
|
) -> ksp_core_lib::Result<crate::BackfillRawAcquisition> {
|
||||||
let block_time = convert_block_time(fields.block_time);
|
let transaction = canonical_transaction(&reference, fields);
|
||||||
let block_time = match block_time {
|
let transaction = match transaction {
|
||||||
std::result::Result::Ok(block_time) => block_time,
|
std::result::Result::Ok(transaction) => transaction,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
};
|
|
||||||
let bytes = canonical_payload_bytes(&fields);
|
|
||||||
let bytes = match bytes {
|
|
||||||
std::result::Result::Ok(bytes) => bytes,
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
};
|
|
||||||
let hash: [u8; 32] = sha2::Sha256::digest(bytes.as_slice()).into();
|
|
||||||
let format_id = ksp_store_lib::RawFormatId::new(crate::RAW_TRANSACTION_FORMAT_ID);
|
|
||||||
let format_id = match format_id {
|
|
||||||
std::result::Result::Ok(format_id) => format_id,
|
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(conversion_error("payload.format_id")),
|
|
||||||
};
|
|
||||||
let payload = ksp_store_lib::RawPayload::try_new(
|
|
||||||
format_id,
|
|
||||||
crate::RAW_TRANSACTION_FORMAT_VERSION,
|
|
||||||
bytes.into_boxed_slice(),
|
|
||||||
ksp_store_lib::RawContentHash::new(hash),
|
|
||||||
);
|
|
||||||
let payload = match payload {
|
|
||||||
std::result::Result::Ok(payload) => payload,
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let provenance = build_provenance(request, provider, endpoint, received_at);
|
let provenance = build_provenance(request, provider, endpoint, received_at);
|
||||||
@@ -215,182 +162,60 @@ fn convert_available_fields(
|
|||||||
std::result::Result::Ok(provenance) => provenance,
|
std::result::Result::Ok(provenance) => provenance,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let observation_key = observation_key(request, &reference, provider, endpoint);
|
let observation_key = observation_key(request, transaction.reference(), provider, endpoint);
|
||||||
let transaction = ksp_store_lib::RawTransaction::new(reference.clone(), fields.slot, block_time, payload);
|
let acquisition = ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(transaction, observation_key, provenance);
|
||||||
let observation = ksp_store_lib::RawTransactionObservation::new(observation_key, reference, provenance);
|
return std::result::Result::Ok(crate::BackfillRawAcquisition { inner: Box::new(BackfillRawAcquisitionInner { acquisition }) });
|
||||||
return std::result::Result::Ok(crate::BackfillRawAcquisition { inner: Box::new(BackfillRawAcquisitionInner { transaction, observation }) });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_block_time(value: std::option::Option<i64>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::RawTimestamp>> {
|
fn canonical_transaction(
|
||||||
let seconds = match value {
|
reference: &ksp_store_lib::RawTransactionReference,
|
||||||
std::option::Option::Some(seconds) => seconds,
|
fields: CanonicalTransactionFields<'_>,
|
||||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
) -> ksp_core_lib::Result<ksp_store_lib::RawTransaction> {
|
||||||
};
|
let transaction_data = match fields.transaction {
|
||||||
let seconds = match u64::try_from(seconds) {
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding }
|
||||||
std::result::Result::Ok(seconds) => seconds,
|
if *encoding == ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 =>
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(conversion_error("block_time")),
|
{
|
||||||
};
|
data.clone()
|
||||||
let millis = match seconds.checked_mul(1_000) {
|
|
||||||
std::option::Option::Some(millis) => millis,
|
|
||||||
std::option::Option::None => return std::result::Result::Err(conversion_error("block_time")),
|
|
||||||
};
|
|
||||||
let timestamp = ksp_store_lib::RawTimestamp::from_unix_millis(millis);
|
|
||||||
return match timestamp {
|
|
||||||
std::result::Result::Ok(timestamp) => std::result::Result::Ok(std::option::Option::Some(timestamp)),
|
|
||||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("block_time")),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn canonical_payload_bytes(fields: &CanonicalTransactionFields<'_>) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
|
||||||
let (transaction_data, transaction_encoding) = match fields.transaction {
|
|
||||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding } => {
|
|
||||||
if *encoding != ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 {
|
|
||||||
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
|
||||||
}
|
|
||||||
(data.as_str(), "base64")
|
|
||||||
},
|
},
|
||||||
ksp_onchain_transport_lib::SolanaEncodedTransaction::LegacyBinary(_) | ksp_onchain_transport_lib::SolanaEncodedTransaction::Json(_) => {
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { .. }
|
||||||
|
| ksp_onchain_transport_lib::SolanaEncodedTransaction::LegacyBinary(_)
|
||||||
|
| ksp_onchain_transport_lib::SolanaEncodedTransaction::Json(_) => {
|
||||||
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
return std::result::Result::Err(conversion_error("transaction.encoding"));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let mut bytes = std::vec::Vec::new();
|
let meta = map_wire_field(fields.meta, |value| return value.clone());
|
||||||
bytes.extend_from_slice(b"{\"transaction\":[");
|
let version = map_wire_field(fields.version, |value| {
|
||||||
if let std::result::Result::Err(error) = append_json_string(&mut bytes, transaction_data) {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
bytes.push(b',');
|
|
||||||
if let std::result::Result::Err(error) = append_json_string(&mut bytes, transaction_encoding) {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
bytes.push(b']');
|
|
||||||
if let std::result::Result::Err(error) = append_wire_value(&mut bytes, "meta", fields.meta, |output, value| return append_canonical_json(output, value)) {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
let version_result = append_wire_value(&mut bytes, "version", fields.version, |output, value| {
|
|
||||||
return match value {
|
return match value {
|
||||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy => append_json_string(output, "legacy"),
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy => ksp_raw_transaction_lib::RawTransactionVersion::Legacy,
|
||||||
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number) => {
|
ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number) => ksp_raw_transaction_lib::RawTransactionVersion::Number(*number),
|
||||||
output.extend_from_slice(number.to_string().as_bytes());
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if let std::result::Result::Err(error) = version_result {
|
let transaction_index = map_wire_field(fields.transaction_index, |value| return *value);
|
||||||
return std::result::Result::Err(error);
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||||
}
|
reference.network().clone(),
|
||||||
let transaction_index_result = append_wire_value(&mut bytes, "transactionIndex", fields.transaction_index, |output, value| {
|
reference.signature(),
|
||||||
output.extend_from_slice(value.to_string().as_bytes());
|
fields.slot,
|
||||||
return std::result::Result::Ok(());
|
fields.block_time,
|
||||||
});
|
transaction_data,
|
||||||
if let std::result::Result::Err(error) = transaction_index_result {
|
meta,
|
||||||
return std::result::Result::Err(error);
|
version,
|
||||||
}
|
transaction_index,
|
||||||
bytes.push(b'}');
|
);
|
||||||
return std::result::Result::Ok(bytes);
|
let canonical = ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
|
||||||
|
return match canonical {
|
||||||
|
std::result::Result::Ok(transaction) => std::result::Result::Ok(transaction),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("common.canonicalization")),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_wire_value<T, F>(
|
fn map_wire_field<T, U, F>(field: &ksp_onchain_transport_lib::SolanaWireField<T>, mut map_value: F) -> ksp_raw_transaction_lib::RawTransactionWireField<U>
|
||||||
output: &mut std::vec::Vec<u8>,
|
|
||||||
key: &str,
|
|
||||||
field: &ksp_onchain_transport_lib::SolanaWireField<T>,
|
|
||||||
mut append_value: F,
|
|
||||||
) -> ksp_core_lib::Result<()>
|
|
||||||
where
|
where
|
||||||
F: FnMut(&mut std::vec::Vec<u8>, &T) -> ksp_core_lib::Result<()>,
|
F: FnMut(&T) -> U,
|
||||||
{
|
{
|
||||||
return match field {
|
return match field {
|
||||||
ksp_onchain_transport_lib::SolanaWireField::Omitted => std::result::Result::Ok(()),
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
ksp_onchain_transport_lib::SolanaWireField::Null => {
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
output.push(b',');
|
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(map_value(value)),
|
||||||
let key_result = append_json_string(output, key);
|
|
||||||
if let std::result::Result::Err(error) = key_result {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
output.extend_from_slice(b":null");
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => {
|
|
||||||
output.push(b',');
|
|
||||||
let key_result = append_json_string(output, key);
|
|
||||||
if let std::result::Result::Err(error) = key_result {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
output.push(b':');
|
|
||||||
append_value(output, value)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_canonical_json(output: &mut std::vec::Vec<u8>, value: &serde_json::Value) -> ksp_core_lib::Result<()> {
|
|
||||||
return match value {
|
|
||||||
serde_json::Value::Null => {
|
|
||||||
output.extend_from_slice(b"null");
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
serde_json::Value::Bool(value) => {
|
|
||||||
if *value {
|
|
||||||
output.extend_from_slice(b"true");
|
|
||||||
} else {
|
|
||||||
output.extend_from_slice(b"false");
|
|
||||||
}
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
serde_json::Value::Number(value) => {
|
|
||||||
output.extend_from_slice(value.to_string().as_bytes());
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
serde_json::Value::String(value) => append_json_string(output, value.as_str()),
|
|
||||||
serde_json::Value::Array(values) => {
|
|
||||||
output.push(b'[');
|
|
||||||
for (index, item) in values.iter().enumerate() {
|
|
||||||
if index != 0 {
|
|
||||||
output.push(b',');
|
|
||||||
}
|
|
||||||
let item_result = append_canonical_json(output, item);
|
|
||||||
if let std::result::Result::Err(error) = item_result {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.push(b']');
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
serde_json::Value::Object(values) => {
|
|
||||||
output.push(b'{');
|
|
||||||
let mut keys = values.keys().collect::<std::vec::Vec<_>>();
|
|
||||||
keys.sort_unstable();
|
|
||||||
for (index, key) in keys.iter().enumerate() {
|
|
||||||
if index != 0 {
|
|
||||||
output.push(b',');
|
|
||||||
}
|
|
||||||
let key_result = append_json_string(output, key.as_str());
|
|
||||||
if let std::result::Result::Err(error) = key_result {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
output.push(b':');
|
|
||||||
let item = values.get(key.as_str());
|
|
||||||
let item = match item {
|
|
||||||
std::option::Option::Some(item) => item,
|
|
||||||
std::option::Option::None => return std::result::Result::Err(conversion_error("payload.meta")),
|
|
||||||
};
|
|
||||||
let item_result = append_canonical_json(output, item);
|
|
||||||
if let std::result::Result::Err(error) = item_result {
|
|
||||||
return std::result::Result::Err(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.push(b'}');
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_json_string(output: &mut std::vec::Vec<u8>, value: &str) -> ksp_core_lib::Result<()> {
|
|
||||||
let encoded = serde_json::to_vec(value);
|
|
||||||
return match encoded {
|
|
||||||
std::result::Result::Ok(encoded) => {
|
|
||||||
output.extend_from_slice(encoded.as_slice());
|
|
||||||
std::result::Result::Ok(())
|
|
||||||
},
|
|
||||||
std::result::Result::Err(_) => std::result::Result::Err(conversion_error("payload.json")),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,18 +288,6 @@ fn hash_bytes(hasher: &mut sha2::Sha256, value: &[u8]) {
|
|||||||
hasher.update(value);
|
hasher.update(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base58_digit(byte: u8) -> std::option::Option<u8> {
|
|
||||||
return match byte {
|
|
||||||
b'1'..=b'9' => std::option::Option::Some(byte - b'1'),
|
|
||||||
b'A'..=b'H' => std::option::Option::Some((byte - b'A') + 9),
|
|
||||||
b'J'..=b'N' => std::option::Option::Some((byte - b'J') + 17),
|
|
||||||
b'P'..=b'Z' => std::option::Option::Some((byte - b'P') + 22),
|
|
||||||
b'a'..=b'k' => std::option::Option::Some((byte - b'a') + 33),
|
|
||||||
b'm'..=b'z' => std::option::Option::Some((byte - b'm') + 44),
|
|
||||||
_ => std::option::Option::None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn conversion_error(field: &'static str) -> ksp_core_lib::Error {
|
fn conversion_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID, "invalid deterministic Backfill RAW conversion")
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID, "invalid deterministic Backfill RAW conversion")
|
||||||
.with_context("field", field);
|
.with_context("field", field);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/src/lib.rs
|
// file: crates/ksp-job-backfill-lib/src/lib.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -30,10 +30,6 @@ pub use self::checkpoint::BackfillCheckpoint;
|
|||||||
pub use self::conversion::BackfillHydrationOutcome;
|
pub use self::conversion::BackfillHydrationOutcome;
|
||||||
/// Complete in-memory RAW transaction acquisition ready for later Store persistence.
|
/// Complete in-memory RAW transaction acquisition ready for later Store persistence.
|
||||||
pub use self::conversion::BackfillRawAcquisition;
|
pub use self::conversion::BackfillRawAcquisition;
|
||||||
/// KSP-owned source-independent RAW transaction format identifier produced by this Backfill vertical.
|
|
||||||
pub use self::conversion::RAW_TRANSACTION_FORMAT_ID;
|
|
||||||
/// Initial KSP-owned RAW transaction format version produced by this Backfill vertical.
|
|
||||||
pub use self::conversion::RAW_TRANSACTION_FORMAT_VERSION;
|
|
||||||
/// Hydrates one candidate through observed Transport and converts a non-null response to canonical RAW v1.
|
/// Hydrates one candidate through observed Transport and converts a non-null response to canonical RAW v1.
|
||||||
pub use self::conversion::hydrate_backfill_candidate;
|
pub use self::conversion::hydrate_backfill_candidate;
|
||||||
/// One deterministic transaction candidate produced by bounded discovery.
|
/// One deterministic transaction candidate produced by bounded discovery.
|
||||||
@@ -112,6 +108,10 @@ pub use self::runtime::BackfillJobRuntime;
|
|||||||
pub use self::runtime::BackfillJobSnapshot;
|
pub use self::runtime::BackfillJobSnapshot;
|
||||||
/// Cloneable runtime-neutral-facing latest-value source for concrete Backfill snapshots.
|
/// Cloneable runtime-neutral-facing latest-value source for concrete Backfill snapshots.
|
||||||
pub use self::runtime::BackfillSnapshotSource;
|
pub use self::runtime::BackfillSnapshotSource;
|
||||||
|
/// KSP-owned source-independent RAW transaction format identifier re-exported from the common RAW layer.
|
||||||
|
pub use ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_ID;
|
||||||
|
/// Frozen RAW transaction format version re-exported from the common RAW layer.
|
||||||
|
pub use ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_VERSION;
|
||||||
|
|
||||||
/// Internal contiguous completion frontier used by bounded execution.
|
/// Internal contiguous completion frontier used by bounded execution.
|
||||||
pub(crate) use self::checkpoint::CompletionFrontier;
|
pub(crate) use self::checkpoint::CompletionFrontier;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/src/request.rs
|
// file: crates/ksp-job-backfill-lib/src/request.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
use sha2::Digest; // rust-rules: trait-import
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ pub const MAX_BACKFILL_PAGES: usize = 10_000;
|
|||||||
/// Maximum page size admitted for one `getSignaturesForAddress` request.
|
/// Maximum page size admitted for one `getSignaturesForAddress` request.
|
||||||
pub const MAX_BACKFILL_PAGE_SIZE: usize = 1_000;
|
pub const MAX_BACKFILL_PAGE_SIZE: usize = 1_000;
|
||||||
/// Maximum Base58 text length possible for one canonical 64-byte Solana signature.
|
/// Maximum Base58 text length possible for one canonical 64-byte Solana signature.
|
||||||
pub const MAX_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 88;
|
pub const MAX_BACKFILL_SIGNATURE_TEXT_BYTES: usize = ksp_raw_transaction_lib::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||||
/// Minimum Base58 text length possible for one canonical 64-byte Solana signature.
|
/// Minimum Base58 text length possible for one canonical 64-byte Solana signature.
|
||||||
pub const MIN_BACKFILL_SIGNATURE_TEXT_BYTES: usize = 64;
|
pub const MIN_BACKFILL_SIGNATURE_TEXT_BYTES: usize = ksp_raw_transaction_lib::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||||
|
|
||||||
/// Commitment levels intentionally admitted by the historical Backfill vertical.
|
/// Commitment levels intentionally admitted by the historical Backfill vertical.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
// version: 6
|
// version: 9
|
||||||
|
|
||||||
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
//! Dependency firewall canaries through the concrete cancellation and latest-value runtime tranche.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_009_manifest_uses_only_planned_ksp_edges_and_private_tokio_runtime() {
|
fn pre_003_manifest_adds_common_raw_edge_without_crossing_store_or_runtime_boundaries() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
let manifest = include_str!("../Cargo.toml");
|
||||||
|
let production_manifest = match manifest.split_once("[dev-dependencies]") {
|
||||||
|
std::option::Option::Some((production, _)) => production,
|
||||||
|
std::option::Option::None => manifest,
|
||||||
|
};
|
||||||
for required in [
|
for required in [
|
||||||
"futures-util = { workspace = true, features = [\"std\"] }",
|
"futures-util = { workspace = true, features = [\"std\"] }",
|
||||||
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
"ksp-core-lib = { path = \"../ksp-core-lib\" }",
|
||||||
"ksp-job-api = { path = \"../ksp-job-api\" }",
|
"ksp-job-api = { path = \"../ksp-job-api\" }",
|
||||||
"ksp-logging-lib = { path = \"../ksp-logging-lib\" }",
|
"ksp-logging-lib = { path = \"../ksp-logging-lib\" }",
|
||||||
"ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }",
|
"ksp-onchain-transport-lib = { path = \"../ksp-onchain-transport-lib\" }",
|
||||||
|
"ksp-raw-transaction-lib = { path = \"../ksp-raw-transaction-lib\" }",
|
||||||
"ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }",
|
"ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }",
|
||||||
"serde_json.workspace = true",
|
"serde_json.workspace = true",
|
||||||
"sha2.workspace = true",
|
"sha2.workspace = true",
|
||||||
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
"tokio = { workspace = true, features = [\"macros\", \"sync\"] }",
|
||||||
] {
|
] {
|
||||||
assert!(manifest.contains(required), "required Backfill dependency missing: {required}");
|
assert!(production_manifest.contains(required), "required Backfill dependency missing: {required}");
|
||||||
}
|
}
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
"ksp-config-lib",
|
"ksp-config-lib",
|
||||||
@@ -32,13 +37,56 @@ fn pre_009_manifest_uses_only_planned_ksp_edges_and_private_tokio_runtime() {
|
|||||||
"serde = {",
|
"serde = {",
|
||||||
"tonic",
|
"tonic",
|
||||||
] {
|
] {
|
||||||
assert!(!manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
assert!(!production_manifest.contains(forbidden), "forbidden Backfill dependency present: {forbidden}");
|
||||||
}
|
}
|
||||||
let root = include_str!("../src/lib.rs");
|
let root = include_str!("../src/lib.rs");
|
||||||
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
assert!(!root.contains("tokio::"), "Tokio implementation types must not leak through the public crate root");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_websocket_parity_dependency_stays_test_only() {
|
||||||
|
let manifest = include_str!("../Cargo.toml");
|
||||||
|
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||||
|
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||||
|
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||||
|
_ => {
|
||||||
|
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert!(!dependencies.contains("tokio-tungstenite"), "WebSocket parity client must not become a production Backfill dependency");
|
||||||
|
assert!(
|
||||||
|
dev_dependencies.contains("tokio-tungstenite = { workspace = true, features = [\"handshake\"] }"),
|
||||||
|
"WebSocket parity client must remain an explicit test-only dependency",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_yellowstone_parity_dependencies_stay_test_only() {
|
||||||
|
let manifest = include_str!("../Cargo.toml");
|
||||||
|
let parts = manifest.splitn(2, "[dev-dependencies]").collect::<std::vec::Vec<_>>();
|
||||||
|
let (dependencies, dev_dependencies) = match parts.as_slice() {
|
||||||
|
[dependencies, dev_dependencies] => (*dependencies, *dev_dependencies),
|
||||||
|
_ => {
|
||||||
|
assert_eq!(parts.len(), 2, "Backfill manifest must retain exactly one dev-dependencies section");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert!(!dependencies.contains("tonic"), "Yellowstone parity server must not become a production Backfill dependency");
|
||||||
|
assert!(!dependencies.contains("yellowstone-grpc-proto"), "Yellowstone protobuf must not become a production Backfill dependency");
|
||||||
|
assert!(
|
||||||
|
dev_dependencies.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"),
|
||||||
|
"Yellowstone parity server must remain an explicit test-only dependency",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dev_dependencies.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"),
|
||||||
|
"Yellowstone protobuf must remain an explicit test-only dependency",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separate() {
|
||||||
let neutral_sources = [
|
let neutral_sources = [
|
||||||
@@ -56,7 +104,12 @@ fn pre_009_production_sources_keep_transport_store_and_scheduler_ownership_separ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let conversion = include_str!("../src/conversion.rs");
|
let conversion = include_str!("../src/conversion.rs");
|
||||||
assert!(conversion.contains("serde_json::"));
|
assert!(conversion.contains("ksp_raw_transaction_lib::canonicalize_raw_transaction"));
|
||||||
|
assert!(conversion.contains("ksp_raw_transaction_lib::assemble_raw_transaction_acquisition"));
|
||||||
|
assert!(conversion.contains("ksp_raw_transaction_lib::parse_raw_transaction_signature"));
|
||||||
|
assert!(!conversion.contains("fn append_canonical_json"));
|
||||||
|
assert!(!conversion.contains("fn base58_digit"));
|
||||||
|
assert!(!conversion.contains("RawPayload::try_new"));
|
||||||
assert!(conversion.contains("get_transaction_observed"));
|
assert!(conversion.contains("get_transaction_observed"));
|
||||||
assert!(conversion.contains("SolanaTransactionEncoding::Base64"));
|
assert!(conversion.contains("SolanaTransactionEncoding::Base64"));
|
||||||
assert!(conversion.contains("std::option::Option::Some(0)"));
|
assert!(conversion.contains("std::option::Option::Some(0)"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
// version: 4
|
// version: 7
|
||||||
|
|
||||||
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
||||||
|
|
||||||
@@ -268,13 +268,14 @@ fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
|
|||||||
"ksp-job-api",
|
"ksp-job-api",
|
||||||
"ksp-logging-lib",
|
"ksp-logging-lib",
|
||||||
"ksp-onchain-transport-lib",
|
"ksp-onchain-transport-lib",
|
||||||
|
"ksp-raw-transaction-lib",
|
||||||
"ksp-store-lib",
|
"ksp-store-lib",
|
||||||
"serde_json.workspace",
|
"serde_json.workspace",
|
||||||
"sha2.workspace",
|
"sha2.workspace",
|
||||||
"tokio",
|
"tokio",
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio"]));
|
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite", "tonic", "yellowstone-grpc-proto"]));
|
||||||
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
||||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
||||||
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
||||||
@@ -328,6 +329,7 @@ fn pre_010_lower_layers_have_no_dependency_return_to_job() {
|
|||||||
include_str!("../../ksp-core-lib/Cargo.toml"),
|
include_str!("../../ksp-core-lib/Cargo.toml"),
|
||||||
include_str!("../../ksp-logging-lib/Cargo.toml"),
|
include_str!("../../ksp-logging-lib/Cargo.toml"),
|
||||||
include_str!("../../ksp-onchain-transport-lib/Cargo.toml"),
|
include_str!("../../ksp-onchain-transport-lib/Cargo.toml"),
|
||||||
|
include_str!("../../ksp-raw-transaction-lib/Cargo.toml"),
|
||||||
include_str!("../../ksp-store-api/Cargo.toml"),
|
include_str!("../../ksp-store-api/Cargo.toml"),
|
||||||
include_str!("../../ksp-store-lib/Cargo.toml"),
|
include_str!("../../ksp-store-lib/Cargo.toml"),
|
||||||
include_str!("../../ksp-store-postgres-lib/Cargo.toml"),
|
include_str!("../../ksp-store-postgres-lib/Cargo.toml"),
|
||||||
|
|||||||
158
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal file
158
crates/ksp-job-backfill-lib/tests/http_block_material.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// file: crates/ksp-job-backfill-lib/tests/http_block_material.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Cross-layer canary for the future Worker-owned HTTP block adapter without changing production ownership.
|
||||||
|
|
||||||
|
fn pool_for_url(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||||
|
let endpoint_url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse(url) {
|
||||||
|
Ok(endpoint_url) => endpoint_url,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||||
|
10,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
"http-block-fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new("fixture-provider"),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||||
|
endpoint_url,
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
std::vec![role],
|
||||||
|
);
|
||||||
|
return ksp_onchain_transport_lib::HttpTransportPool::new(ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||||
|
std::vec![endpoint],
|
||||||
|
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_once(body: &'static str) -> std::io::Result<(std::string::String, std::thread::JoinHandle<std::io::Result<()>>)> {
|
||||||
|
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let address = match listener.local_addr() {
|
||||||
|
Ok(address) => address,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = match listener.accept() {
|
||||||
|
Ok(accepted) => accepted,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let mut bytes = std::vec::Vec::new();
|
||||||
|
let mut buffer = [0_u8; 1024];
|
||||||
|
loop {
|
||||||
|
let count = match std::io::Read::read(&mut stream, &mut buffer) {
|
||||||
|
Ok(count) => count,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&buffer[..count]);
|
||||||
|
if bytes.windows(4).any(|window| return window == b"\r\n\r\n") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||||
|
if let Err(error) = std::io::Write::write_all(&mut stream, response.as_bytes()) {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
});
|
||||||
|
return Ok((format!("http://{address}"), handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_meta(field: &ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>) -> ksp_raw_transaction_lib::RawTransactionWireField<serde_json::Value> {
|
||||||
|
return match field {
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_version(
|
||||||
|
field: &ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||||
|
) -> ksp_raw_transaction_lib::RawTransactionWireField<ksp_raw_transaction_lib::RawTransactionVersion> {
|
||||||
|
return match field {
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy) => {
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy)
|
||||||
|
},
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number)) => {
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Number(*number))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn pre_004_http_observed_block_projects_each_base64_transaction_to_common_material_without_new_production_edge() {
|
||||||
|
const BODY: &str = concat!(
|
||||||
|
"{\"jsonrpc\":\"2.0\",\"result\":{\"previousBlockhash\":\"previous\",\"blockhash\":\"block\",\"parentSlot\":430000122,",
|
||||||
|
"\"rewards\":[],\"numRewardPartitions\":0,\"blockTime\":1787072400,\"blockHeight\":410000000,",
|
||||||
|
"\"transactions\":[{\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAA\",\"base64\"],\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\"},",
|
||||||
|
"{\"transaction\":[\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEA\",",
|
||||||
|
"\"base64\"],\"meta\":{\"err\":null,\"fee\":6000},\"version\":0}]},\"id\":1}",
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_once(BODY).expect("fixture server must start");
|
||||||
|
let pool = pool_for_url(url.as_str()).expect("fixture pool must build");
|
||||||
|
let config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
);
|
||||||
|
let observed = pool
|
||||||
|
.get_block_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("observed block fixture must succeed");
|
||||||
|
assert_eq!(observed.endpoint_name(), "http-block-fixture");
|
||||||
|
assert_eq!(observed.provider().as_str(), "fixture-provider");
|
||||||
|
let block = observed.value().as_ref().expect("fixture block must be present");
|
||||||
|
let transactions = block.transactions().value().expect("fixture transactions must be present");
|
||||||
|
assert_eq!(transactions.len(), 2);
|
||||||
|
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("fixture network must be valid");
|
||||||
|
let expected_signatures = [[0_u8; 64], [1_u8; 64]];
|
||||||
|
for (index, transaction) in transactions.iter().enumerate() {
|
||||||
|
let (data, meta, version) = match transaction.transaction() {
|
||||||
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary {
|
||||||
|
data,
|
||||||
|
encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64,
|
||||||
|
} => (data.as_str(), map_meta(transaction.meta()), map_version(transaction.version())),
|
||||||
|
_ => panic!("fixture must remain full Base64 transaction material"),
|
||||||
|
};
|
||||||
|
let transaction_index = u32::try_from(index).expect("fixture transaction index must fit u32");
|
||||||
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||||
|
network.clone(),
|
||||||
|
430_000_123,
|
||||||
|
block.block_time(),
|
||||||
|
data,
|
||||||
|
meta,
|
||||||
|
version,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||||
|
)
|
||||||
|
.expect("HTTP block material must map to common RAW material");
|
||||||
|
let raw = ksp_raw_transaction_lib::canonicalize_raw_transaction(material).expect("mapped HTTP block material must canonicalize");
|
||||||
|
assert_eq!(raw.reference().signature().as_bytes(), &expected_signatures[index]);
|
||||||
|
assert_eq!(raw.slot(), 430_000_123);
|
||||||
|
assert_eq!(raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
|
||||||
|
assert!(raw.payload().bytes().ends_with(format!("\"transactionIndex\":{transaction_index}}}").as_bytes()));
|
||||||
|
}
|
||||||
|
handle.join().expect("fixture server must join").expect("fixture server must complete successfully");
|
||||||
|
return;
|
||||||
|
}
|
||||||
503
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
Normal file
503
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
// file: crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Cross-layer parity canaries qualifying standard `blockSubscribe` and Helius `transactionSubscribe` for RAW v1 ingestion.
|
||||||
|
|
||||||
|
use futures_util::SinkExt; // rust-rules: trait-import
|
||||||
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const FIXTURE_BLOCK_TIME_MILLIS: u64 = 1_787_072_400_000;
|
||||||
|
const FIXTURE_SLOT: u64 = 430_000_123;
|
||||||
|
const ZERO_SIGNATURE_TEXT: &str = "1111111111111111111111111111111111111111111111111111111111111111";
|
||||||
|
const ZERO_TRANSACTION_BASE64: &str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
|
||||||
|
type FixtureWebSocket = tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>;
|
||||||
|
|
||||||
|
fn http_pool_for_url(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTransportPool> {
|
||||||
|
let endpoint_url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse(url) {
|
||||||
|
Ok(endpoint_url) => endpoint_url,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||||
|
10,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleLimits::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
"ws-parity-http-fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new("fixture-http-provider"),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||||
|
endpoint_url,
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
std::vec![role],
|
||||||
|
);
|
||||||
|
return ksp_onchain_transport_lib::HttpTransportPool::new(ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||||
|
std::vec![endpoint],
|
||||||
|
ksp_onchain_transport_lib::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_http_once(body: &'static str) -> std::io::Result<(std::string::String, std::thread::JoinHandle<std::io::Result<()>>)> {
|
||||||
|
let listener = match std::net::TcpListener::bind("127.0.0.1:0") {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let address = match listener.local_addr() {
|
||||||
|
Ok(address) => address,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = match listener.accept() {
|
||||||
|
Ok(accepted) => accepted,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let mut bytes = std::vec::Vec::new();
|
||||||
|
let mut buffer = [0_u8; 1024];
|
||||||
|
loop {
|
||||||
|
let count = match std::io::Read::read(&mut stream, &mut buffer) {
|
||||||
|
Ok(count) => count,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&buffer[..count]);
|
||||||
|
if bytes.windows(4).any(|window| return window == b"\r\n\r\n") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body);
|
||||||
|
if let Err(error) = std::io::Write::write_all(&mut stream, response.as_bytes()) {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
});
|
||||||
|
return Ok((format!("http://{address}"), handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws_endpoint(url: &str, protocol: ksp_onchain_transport_lib::WsProtocolKind) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsEndpointSettings> {
|
||||||
|
let parsed = match ksp_onchain_transport_lib::WsEndpointUrl::parse(url) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
return Ok(ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||||
|
"ws-raw-parity-fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::WsProviderName::new("fixture-ws-provider"),
|
||||||
|
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
|
||||||
|
protocol,
|
||||||
|
parsed,
|
||||||
|
ksp_onchain_transport_lib::WsSessionSettings::default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_ws_request(websocket: &mut FixtureWebSocket) -> std::result::Result<serde_json::Value, std::string::String> {
|
||||||
|
let message = match websocket.next().await {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(message)) => message,
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) => return Err("fixture WebSocket request decode failed".to_owned()),
|
||||||
|
std::option::Option::None => return Err("fixture WebSocket request was absent".to_owned()),
|
||||||
|
};
|
||||||
|
let text = match message.to_text() {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(_) => return Err("fixture WebSocket request was not text".to_owned()),
|
||||||
|
};
|
||||||
|
return serde_json::from_str(text).map_err(|_| return "fixture WebSocket request JSON was invalid".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_ws_json(websocket: &mut FixtureWebSocket, value: serde_json::Value) -> std::result::Result<(), std::string::String> {
|
||||||
|
return websocket
|
||||||
|
.send(tokio_tungstenite::tungstenite::Message::Text(value.to_string().into()))
|
||||||
|
.await
|
||||||
|
.map_err(|_| return "fixture WebSocket response send failed".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_ws_result(
|
||||||
|
websocket: &mut FixtureWebSocket,
|
||||||
|
request: &serde_json::Value,
|
||||||
|
result: serde_json::Value,
|
||||||
|
) -> std::result::Result<(), std::string::String> {
|
||||||
|
let id = match request.get("id").and_then(serde_json::Value::as_u64) {
|
||||||
|
std::option::Option::Some(id) => id,
|
||||||
|
std::option::Option::None => return Err("fixture request id was absent".to_owned()),
|
||||||
|
};
|
||||||
|
return send_ws_json(websocket, serde_json::json!({"jsonrpc":"2.0","id":id,"result":result})).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_ws_close(websocket: &mut FixtureWebSocket) {
|
||||||
|
loop {
|
||||||
|
match websocket.next().await {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_meta(field: &ksp_onchain_transport_lib::SolanaWireField<serde_json::Value>) -> ksp_raw_transaction_lib::RawTransactionWireField<serde_json::Value> {
|
||||||
|
return match field {
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_version(
|
||||||
|
field: &ksp_onchain_transport_lib::SolanaWireField<ksp_onchain_transport_lib::SolanaTransactionVersion>,
|
||||||
|
) -> ksp_raw_transaction_lib::RawTransactionWireField<ksp_raw_transaction_lib::RawTransactionVersion> {
|
||||||
|
return match field {
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Legacy) => {
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy)
|
||||||
|
},
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(ksp_onchain_transport_lib::SolanaTransactionVersion::Number(number)) => {
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Number(*number))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_from_block_transaction(
|
||||||
|
network: ksp_store_lib::RawNetworkId,
|
||||||
|
slot: u64,
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
transaction: &ksp_onchain_transport_lib::SolanaBlockTransaction,
|
||||||
|
transaction_index: u32,
|
||||||
|
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||||
|
let data = match transaction.transaction() {
|
||||||
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 } => {
|
||||||
|
data.as_str()
|
||||||
|
},
|
||||||
|
_ => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||||
|
network,
|
||||||
|
slot,
|
||||||
|
block_time,
|
||||||
|
data,
|
||||||
|
map_meta(transaction.meta()),
|
||||||
|
map_version(transaction.version()),
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||||
|
) {
|
||||||
|
Ok(material) => material,
|
||||||
|
Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_from_confirmed_transaction(
|
||||||
|
network: ksp_store_lib::RawNetworkId,
|
||||||
|
transaction: &ksp_onchain_transport_lib::SolanaConfirmedTransaction,
|
||||||
|
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||||
|
let data = match transaction.transaction() {
|
||||||
|
ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary { data, encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64 } => {
|
||||||
|
data.as_str()
|
||||||
|
},
|
||||||
|
_ => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||||
|
network,
|
||||||
|
transaction.slot(),
|
||||||
|
transaction.block_time(),
|
||||||
|
data,
|
||||||
|
map_meta(transaction.meta()),
|
||||||
|
map_version(transaction.version()),
|
||||||
|
match transaction.transaction_index() {
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Omitted => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(*value),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(material) => material,
|
||||||
|
Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_from_helius_full(
|
||||||
|
network: ksp_store_lib::RawNetworkId,
|
||||||
|
notification: &ksp_onchain_transport_lib::HeliusFullTransactionNotification,
|
||||||
|
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||||
|
let object = match notification.transaction().as_object() {
|
||||||
|
std::option::Option::Some(object) => object,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let encoded = match object.get("transaction") {
|
||||||
|
std::option::Option::Some(value) => match value.as_array() {
|
||||||
|
std::option::Option::Some(encoded) => encoded,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
},
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
if encoded.len() != 2 {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
let encoding = match encoded.get(1) {
|
||||||
|
std::option::Option::Some(value) => match value.as_str() {
|
||||||
|
std::option::Option::Some(encoding) => encoding,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
},
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
if encoding != "base64" {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
let data = match encoded.first() {
|
||||||
|
std::option::Option::Some(value) => match value.as_str() {
|
||||||
|
std::option::Option::Some(data) => data,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
},
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let meta = match object.get("meta") {
|
||||||
|
std::option::Option::None => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
std::option::Option::Some(serde_json::Value::Null) => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
std::option::Option::Some(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||||
|
};
|
||||||
|
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature(notification.signature()) {
|
||||||
|
Ok(signature) => signature,
|
||||||
|
Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let transaction_index = match u32::try_from(notification.transaction_index()) {
|
||||||
|
Ok(transaction_index) => transaction_index,
|
||||||
|
Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
notification.slot(),
|
||||||
|
std::option::Option::None,
|
||||||
|
data,
|
||||||
|
meta,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||||
|
);
|
||||||
|
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn pre_005_standard_block_subscribe_full_base64_matches_http_get_block_raw_v1_for_legacy_transaction() {
|
||||||
|
const HTTP_BODY: &str = concat!(
|
||||||
|
"{\"jsonrpc\":\"2.0\",\"result\":{\"previousBlockhash\":\"previous\",\"blockhash\":\"block\",\"parentSlot\":430000122,",
|
||||||
|
"\"rewards\":[],\"numRewardPartitions\":0,\"blockTime\":1787072400,\"blockHeight\":410000000,",
|
||||||
|
"\"transactions\":[{\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"AAAAAA\",\"base64\"],\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\"}]},\"id\":1}",
|
||||||
|
);
|
||||||
|
let (http_url, http_server) = serve_http_once(HTTP_BODY).expect("HTTP parity fixture must start");
|
||||||
|
let http_pool = http_pool_for_url(http_url.as_str()).expect("HTTP parity pool must build");
|
||||||
|
let http_config = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
);
|
||||||
|
let http = http_pool
|
||||||
|
.get_block_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), FIXTURE_SLOT, std::option::Option::Some(&http_config))
|
||||||
|
.await
|
||||||
|
.expect("HTTP block parity fixture must decode");
|
||||||
|
let http_block = http.value().as_ref().expect("HTTP parity block must be present").clone();
|
||||||
|
http_server.join().expect("HTTP parity fixture must join").expect("HTTP parity fixture must complete");
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("WS parity listener must bind");
|
||||||
|
let address = listener.local_addr().expect("WS parity listener address must resolve");
|
||||||
|
let ws_block = serde_json::from_str::<serde_json::Value>(HTTP_BODY).expect("HTTP parity JSON must parse")["result"].clone();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = match listener.accept().await {
|
||||||
|
Ok(accepted) => accepted,
|
||||||
|
Err(_) => return Err("WS parity server accept failed".to_owned()),
|
||||||
|
};
|
||||||
|
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||||
|
Ok(websocket) => websocket,
|
||||||
|
Err(_) => return Err("WS parity handshake failed".to_owned()),
|
||||||
|
};
|
||||||
|
let subscribe = match read_ws_request(&mut websocket).await {
|
||||||
|
Ok(subscribe) => subscribe,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockSubscribe")) {
|
||||||
|
return Err("WS parity server received unexpected subscribe method".to_owned());
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(501)).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_json(
|
||||||
|
&mut websocket,
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"blockNotification",
|
||||||
|
"params":{"subscription":501,"result":{"context":{"slot":FIXTURE_SLOT},"value":{"slot":FIXTURE_SLOT,"block":ws_block,"err":null}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||||
|
Ok(unsubscribe) => unsubscribe,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockUnsubscribe")) {
|
||||||
|
return Err("WS parity server received unexpected unsubscribe method".to_owned());
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
wait_for_ws_close(&mut websocket).await;
|
||||||
|
return Ok::<(), std::string::String>(());
|
||||||
|
});
|
||||||
|
let endpoint = ws_endpoint(format!("ws://{address}").as_str(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard)
|
||||||
|
.expect("standard WS parity endpoint must build");
|
||||||
|
let session = ksp_onchain_transport_lib::SolanaStandardWsSession::connect(endpoint).await.expect("standard WS parity session must connect");
|
||||||
|
let ws_config = ksp_onchain_transport_lib::SolanaBlockSubscribeConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
);
|
||||||
|
let mut subscription = session
|
||||||
|
.block_subscribe(&ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All, std::option::Option::Some(&ws_config))
|
||||||
|
.await
|
||||||
|
.expect("standard blockSubscribe parity subscription must register");
|
||||||
|
let notification = subscription
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("standard blockSubscribe parity notification must arrive")
|
||||||
|
.expect("standard blockSubscribe parity notification must decode");
|
||||||
|
let ws_block = notification.value().block().expect("standard blockSubscribe parity block must be present");
|
||||||
|
assert_eq!(ws_block, &http_block);
|
||||||
|
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("parity fixture network must be valid");
|
||||||
|
let http_transaction =
|
||||||
|
http_block.transactions().value().expect("HTTP parity transactions must be present").first().expect("HTTP parity transaction must exist");
|
||||||
|
let ws_transaction = ws_block.transactions().value().expect("WS parity transactions must be present").first().expect("WS parity transaction must exist");
|
||||||
|
let http_raw = raw_from_block_transaction(network.clone(), FIXTURE_SLOT, http_block.block_time(), http_transaction, 0)
|
||||||
|
.expect("HTTP block transaction must canonicalize");
|
||||||
|
let ws_raw = raw_from_block_transaction(network, notification.value().slot(), ws_block.block_time(), ws_transaction, 0)
|
||||||
|
.expect("WS block transaction must canonicalize");
|
||||||
|
assert_eq!(ws_raw.reference(), http_raw.reference());
|
||||||
|
assert_eq!(ws_raw.slot(), http_raw.slot());
|
||||||
|
assert_eq!(ws_raw.block_time(), http_raw.block_time());
|
||||||
|
assert_eq!(ws_raw.payload().bytes(), http_raw.payload().bytes());
|
||||||
|
assert_eq!(ws_raw.payload().content_hash(), http_raw.payload().content_hash());
|
||||||
|
assert!(subscription.unsubscribe().await.expect("standard blockSubscribe parity unsubscribe must complete"));
|
||||||
|
session.close().await.expect("standard WS parity session must close");
|
||||||
|
server.await.expect("standard WS parity server task must join").expect("standard WS parity server must complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn pre_005_helius_full_base64_without_block_time_and_version_requires_http_hydration_for_raw_v1() {
|
||||||
|
const HTTP_BODY: &str = concat!(
|
||||||
|
"{\"jsonrpc\":\"2.0\",\"result\":{\"slot\":430000123,\"blockTime\":1787072400,",
|
||||||
|
"\"transaction\":[\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"base64\"],",
|
||||||
|
"\"meta\":{\"err\":null,\"fee\":5000},\"version\":\"legacy\",\"transactionIndex\":0},\"id\":1}",
|
||||||
|
);
|
||||||
|
let (http_url, http_server) = serve_http_once(HTTP_BODY).expect("Helius hydration HTTP fixture must start");
|
||||||
|
let http_pool = http_pool_for_url(http_url.as_str()).expect("Helius hydration HTTP pool must build");
|
||||||
|
let http_config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let http = http_pool
|
||||||
|
.get_transaction_observed(&ksp_onchain_transport_lib::HttpRoleName::new("default"), ZERO_SIGNATURE_TEXT, std::option::Option::Some(&http_config))
|
||||||
|
.await
|
||||||
|
.expect("Helius hydration HTTP fixture must decode");
|
||||||
|
let http_transaction = http.value().as_ref().expect("Helius hydration HTTP transaction must be present");
|
||||||
|
let network = ksp_store_lib::RawNetworkId::new("devnet").expect("Helius hydration fixture network must be valid");
|
||||||
|
let http_raw = raw_from_confirmed_transaction(network.clone(), http_transaction).expect("hydrated HTTP transaction must canonicalize");
|
||||||
|
http_server.join().expect("Helius hydration HTTP fixture must join").expect("Helius hydration HTTP fixture must complete");
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("Helius parity listener must bind");
|
||||||
|
let address = listener.local_addr().expect("Helius parity listener address must resolve");
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = match listener.accept().await {
|
||||||
|
Ok(accepted) => accepted,
|
||||||
|
Err(_) => return Err("Helius parity server accept failed".to_owned()),
|
||||||
|
};
|
||||||
|
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||||
|
Ok(websocket) => websocket,
|
||||||
|
Err(_) => return Err("Helius parity handshake failed".to_owned()),
|
||||||
|
};
|
||||||
|
let subscribe = match read_ws_request(&mut websocket).await {
|
||||||
|
Ok(subscribe) => subscribe,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionSubscribe")) {
|
||||||
|
return Err("Helius parity server received unexpected subscribe method".to_owned());
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(601)).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_json(
|
||||||
|
&mut websocket,
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"transactionNotification",
|
||||||
|
"params":{
|
||||||
|
"subscription":601,
|
||||||
|
"result":{
|
||||||
|
"transaction":{"transaction":[ZERO_TRANSACTION_BASE64,"base64"],"meta":{"err":null,"fee":5000}},
|
||||||
|
"signature":ZERO_SIGNATURE_TEXT,
|
||||||
|
"slot":FIXTURE_SLOT,
|
||||||
|
"transactionIndex":0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||||
|
Ok(unsubscribe) => unsubscribe,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionUnsubscribe")) {
|
||||||
|
return Err("Helius parity server received unexpected unsubscribe method".to_owned());
|
||||||
|
}
|
||||||
|
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
wait_for_ws_close(&mut websocket).await;
|
||||||
|
return Ok::<(), std::string::String>(());
|
||||||
|
});
|
||||||
|
let endpoint = ws_endpoint(format!("ws://{address}").as_str(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream)
|
||||||
|
.expect("Helius parity endpoint must build");
|
||||||
|
let session = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(endpoint).await.expect("Helius parity session must connect");
|
||||||
|
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(
|
||||||
|
ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::default(),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::Base64),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
let mut subscription = session.transaction_subscribe(&request).await.expect("Helius parity transaction subscription must register");
|
||||||
|
let notification = subscription.recv().await.expect("Helius parity notification must arrive").expect("Helius parity notification must decode");
|
||||||
|
assert!(matches!(¬ification, ksp_onchain_transport_lib::HeliusTransactionNotification::Full(_)));
|
||||||
|
let full = match notification {
|
||||||
|
ksp_onchain_transport_lib::HeliusTransactionNotification::Full(full) => full,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let helius_raw = raw_from_helius_full(network, &full).expect("best-effort Helius full projection must canonicalize for negative parity proof");
|
||||||
|
assert_eq!(helius_raw.reference(), http_raw.reference());
|
||||||
|
assert_eq!(helius_raw.slot(), http_raw.slot());
|
||||||
|
assert!(helius_raw.block_time().is_none());
|
||||||
|
assert_eq!(http_raw.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(FIXTURE_BLOCK_TIME_MILLIS),);
|
||||||
|
assert_ne!(helius_raw.block_time(), http_raw.block_time());
|
||||||
|
assert_ne!(helius_raw.payload().bytes(), http_raw.payload().bytes());
|
||||||
|
assert_ne!(helius_raw.payload().content_hash(), http_raw.payload().content_hash());
|
||||||
|
assert!(subscription.unsubscribe().await.expect("Helius parity transaction unsubscribe must complete"));
|
||||||
|
session.close().await.expect("Helius parity session must close");
|
||||||
|
server.await.expect("Helius parity server task must join").expect("Helius parity server must complete");
|
||||||
|
}
|
||||||
471
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
471
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
// file: crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Deterministic local Yellowstone gRPC parity canary for source-neutral RAW transaction v1 projection.
|
||||||
|
|
||||||
|
struct FixtureStream<T> {
|
||||||
|
receiver: tokio::sync::mpsc::Receiver<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FixtureStream<T> {
|
||||||
|
fn new(receiver: tokio::sync::mpsc::Receiver<T>) -> Self {
|
||||||
|
return Self { receiver };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> futures_util::Stream for FixtureStream<T> {
|
||||||
|
type Item = T;
|
||||||
|
|
||||||
|
fn poll_next(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<std::option::Option<Self::Item>> {
|
||||||
|
return self.get_mut().receiver.poll_recv(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FixtureGeyser;
|
||||||
|
|
||||||
|
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
|
||||||
|
type SubscribeStream = FixtureStream<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
|
||||||
|
let mut inbound = request.into_inner();
|
||||||
|
let (sender, receiver) = tokio::sync::mpsc::channel(4);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let initial = inbound.message().await;
|
||||||
|
if !matches!(initial, std::result::Result::Ok(std::option::Option::Some(_))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if sender.send(std::result::Result::Ok(transaction_update())).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = sender.send(std::result::Result::Ok(block_update())).await;
|
||||||
|
});
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(FixtureStream::new(receiver)));
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeDeshredStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe_deshred(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeGossipStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateGossip, tonic::Status>>;
|
||||||
|
|
||||||
|
async fn subscribe_gossip(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeGossipRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeGossipStream>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("gossip is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn subscribe_replay_info(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ping(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_latest_blockhash(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_block_height(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_slot(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_blockhash_valid(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_version(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside the pre.006 fixture"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FixtureServer {
|
||||||
|
endpoint_url: std::string::String,
|
||||||
|
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixtureServer {
|
||||||
|
async fn start() -> std::result::Result<Self, &'static str> {
|
||||||
|
let bind_address = match "127.0.0.1:0".parse::<std::net::SocketAddr>() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture bind address must parse"),
|
||||||
|
};
|
||||||
|
let incoming = match tonic::transport::server::TcpIncoming::bind(bind_address) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must bind"),
|
||||||
|
};
|
||||||
|
let local_address = match incoming.local_addr() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err("fixture gRPC listener must expose local address"),
|
||||||
|
};
|
||||||
|
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser);
|
||||||
|
let result = tonic::transport::Server::builder()
|
||||||
|
.serve_with_incoming_shutdown(service, incoming, async move {
|
||||||
|
let _ = shutdown_receiver.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
});
|
||||||
|
return std::result::Result::Ok(Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop(mut self) {
|
||||||
|
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
|
||||||
|
let _ = shutdown.send(());
|
||||||
|
}
|
||||||
|
let result = self.task.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_info() -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
signature: vec![9_u8; 64],
|
||||||
|
is_vote: false,
|
||||||
|
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
|
||||||
|
signatures: vec![vec![9_u8; 64]],
|
||||||
|
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
|
||||||
|
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
|
||||||
|
num_required_signatures: 1,
|
||||||
|
num_readonly_signed_accounts: 0,
|
||||||
|
num_readonly_unsigned_accounts: 1,
|
||||||
|
}),
|
||||||
|
account_keys: vec![vec![1_u8; 32], vec![2_u8; 32]],
|
||||||
|
recent_blockhash: vec![3_u8; 32],
|
||||||
|
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction {
|
||||||
|
program_id_index: 1,
|
||||||
|
accounts: vec![0_u8],
|
||||||
|
data: vec![4_u8, 5, 6],
|
||||||
|
}],
|
||||||
|
versioned: true,
|
||||||
|
address_table_lookups: vec![],
|
||||||
|
config: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionConfig {
|
||||||
|
priority_fee: std::option::Option::Some(7),
|
||||||
|
compute_unit_limit: std::option::Option::Some(8),
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option::Some(9),
|
||||||
|
heap_size: std::option::Option::Some(32_768),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
|
||||||
|
fee: 5_000,
|
||||||
|
pre_balances: vec![100, 200],
|
||||||
|
post_balances: vec![90, 210],
|
||||||
|
compute_units_consumed: std::option::Option::Some(123),
|
||||||
|
cost_units: std::option::Option::Some(456),
|
||||||
|
..std::default::Default::default()
|
||||||
|
}),
|
||||||
|
index: 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transaction_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["transactions-pre-006".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction { transaction: std::option::Option::Some(transaction_info()), slot: 42 },
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["blocks-pre-006".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
|
||||||
|
slot: 42,
|
||||||
|
blockhash: ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string(),
|
||||||
|
rewards: std::option::Option::None,
|
||||||
|
block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_787_104_000 }),
|
||||||
|
block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 41 }),
|
||||||
|
transactions: vec![transaction_info()],
|
||||||
|
parent_slot: 41,
|
||||||
|
parent_blockhash: ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string(),
|
||||||
|
executed_transaction_count: 1,
|
||||||
|
updated_account_count: 0,
|
||||||
|
accounts: vec![],
|
||||||
|
entries_count: 0,
|
||||||
|
entries: vec![],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_settings(url: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||||
|
let endpoint_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(url) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"pre-006-fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
endpoint_url,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_wire(
|
||||||
|
transaction: &ksp_onchain_transport_lib::YellowstoneStoredTransaction,
|
||||||
|
) -> ksp_core_lib::Result<ksp_raw_transaction_lib::RawSolanaTransactionWire> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let header = message.header();
|
||||||
|
let required = match u8::try_from(header.num_required_signatures()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_required_signatures")),
|
||||||
|
};
|
||||||
|
let readonly_signed = match u8::try_from(header.num_readonly_signed_accounts()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_signed_accounts")),
|
||||||
|
};
|
||||||
|
let readonly_unsigned = match u8::try_from(header.num_readonly_unsigned_accounts()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("num_readonly_unsigned_accounts")),
|
||||||
|
};
|
||||||
|
let version = if message.config().is_some() {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1
|
||||||
|
} else if message.versioned() {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V0
|
||||||
|
} else {
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::Legacy
|
||||||
|
};
|
||||||
|
let signatures = transaction.signatures().iter().map(|value| return *value.as_bytes()).collect();
|
||||||
|
let account_keys = message.account_keys().iter().map(solana_pubkey_bytes).collect();
|
||||||
|
let mut instructions = std::vec::Vec::with_capacity(message.instructions().len());
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
let program_id_index = match u8::try_from(instruction.program_id_index()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(projection_error("program_id_index")),
|
||||||
|
};
|
||||||
|
instructions.push(ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(
|
||||||
|
program_id_index,
|
||||||
|
instruction.accounts().to_vec(),
|
||||||
|
instruction.data().to_vec(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let address_table_lookups = message
|
||||||
|
.address_table_lookups()
|
||||||
|
.iter()
|
||||||
|
.map(|lookup| {
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaAddressTableLookup::new(
|
||||||
|
lookup.account_key().to_bytes(),
|
||||||
|
lookup.writable_indexes().to_vec(),
|
||||||
|
lookup.readonly_indexes().to_vec(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let config = message.config().map(|value| {
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
|
||||||
|
value.priority_fee(),
|
||||||
|
value.compute_unit_limit(),
|
||||||
|
value.loaded_accounts_data_size_limit(),
|
||||||
|
value.heap_size(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let raw_message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
version,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(required, readonly_signed, readonly_unsigned),
|
||||||
|
account_keys,
|
||||||
|
*message.recent_blockhash().as_bytes(),
|
||||||
|
instructions,
|
||||||
|
address_table_lookups,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(ksp_raw_transaction_lib::RawSolanaTransactionWire::new(signatures, raw_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solana_pubkey_bytes(value: &ksp_core_lib::Pubkey) -> [u8; 32] {
|
||||||
|
return value.to_bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projection_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "yellowstone_raw_projection"), "Yellowstone fixture field does not fit common wire")
|
||||||
|
.with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_code_text(error: &ksp_core_lib::Error) -> std::string::String {
|
||||||
|
return format!("{}.{}", error.code().domain(), error.code().code());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_v1_wire() -> ksp_raw_transaction_lib::RawSolanaTransactionWire {
|
||||||
|
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[1_u8; 32], [2_u8; 32]],
|
||||||
|
[3_u8; 32],
|
||||||
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(1, vec![0_u8], vec![4_u8, 5, 6])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::new(
|
||||||
|
std::option::Option::Some(7),
|
||||||
|
std::option::Option::Some(8),
|
||||||
|
std::option::Option::Some(9),
|
||||||
|
std::option::Option::Some(32_768),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[9_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail_test(message: std::string::String) {
|
||||||
|
assert!(message.is_empty(), "{message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pre_006_yellowstone_transaction_and_block_preserve_exact_v1_wire_and_block_time() {
|
||||||
|
let server = match FixtureServer::start().await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
assert!(error.is_empty(), "fixture server start failed: {error}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let settings = match fixture_settings(server.endpoint_url.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture settings failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&settings).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture channel connect failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
let mut session = match channel.open_standard_subscribe(request).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("fixture subscribe failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let expected = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&expected_v1_wire()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("expected V1 wire serialization failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction = match session.next_update().await {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
std::result::Result::Ok(std::option::Option::None) => {
|
||||||
|
fail_test("transaction update must be present".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction update decode failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction = match transaction {
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Transaction(value) => value,
|
||||||
|
_ => {
|
||||||
|
fail_test("first Yellowstone fixture update must be a transaction".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(transaction.slot(), 42);
|
||||||
|
assert_eq!(transaction.transaction().index(), 3);
|
||||||
|
assert_eq!(transaction.transaction().meta().fee(), 5_000);
|
||||||
|
let transaction_wire = match project_wire(transaction.transaction().transaction()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction projection failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let transaction_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&transaction_wire) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("transaction V1 wire serialization failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(transaction_bytes, expected);
|
||||||
|
let block = match session.next_update().await {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
std::result::Result::Ok(std::option::Option::None) => {
|
||||||
|
fail_test("block update must be present".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block update decode failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let block = match block {
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) => value,
|
||||||
|
_ => {
|
||||||
|
fail_test("second Yellowstone fixture update must be a block".to_owned());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(block.slot(), 42);
|
||||||
|
assert_eq!(block.block_time(), std::option::Option::Some(1_787_104_000));
|
||||||
|
assert_eq!(block.transactions().len(), 1);
|
||||||
|
assert_eq!(block.transactions()[0].index(), 3);
|
||||||
|
let block_wire = match project_wire(block.transactions()[0].transaction()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block transaction projection failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let block_bytes = match ksp_raw_transaction_lib::serialize_solana_transaction_wire(&block_wire) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
fail_test(format!("block V1 wire serialization failed: {}", error_code_text(&error)));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(block_bytes, expected);
|
||||||
|
assert_eq!(block_bytes, transaction_bytes);
|
||||||
|
assert!(transaction.transaction().meta().error().is_none());
|
||||||
|
assert_eq!(block.transactions()[0].meta().fee(), transaction.transaction().meta().fee());
|
||||||
|
assert!(session.close().await.is_ok());
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/unit_tests/conversion.rs
|
// file: crates/ksp-job-backfill-lib/unit_tests/conversion.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
fn signature_text() -> std::option::Option<crate::BackfillSignature> {
|
fn signature_text() -> std::option::Option<crate::BackfillSignature> {
|
||||||
return match crate::BackfillSignature::new("1".repeat(64)) {
|
return match crate::BackfillSignature::new("1".repeat(64)) {
|
||||||
@@ -46,6 +46,14 @@ fn candidate(network: &str, signature: crate::BackfillSignature) -> std::option:
|
|||||||
return std::option::Option::Some(crate::BackfillCandidate::new(crate::BackfillCandidateIdentity::new(network, signature), std::option::Option::Some(42)));
|
return std::option::Option::Some(crate::BackfillCandidate::new(crate::BackfillCandidateIdentity::new(network, signature), std::option::Option::Some(42)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw_reference(network: &str) -> std::option::Option<ksp_store_lib::RawTransactionReference> {
|
||||||
|
let network = match ksp_store_lib::RawNetworkId::new(network) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return std::option::Option::Some(ksp_store_lib::RawTransactionReference::new(network, ksp_store_lib::RawTransactionSignature::new([0_u8; 64])));
|
||||||
|
}
|
||||||
|
|
||||||
fn received_at() -> std::option::Option<ksp_store_lib::RawTimestamp> {
|
fn received_at() -> std::option::Option<ksp_store_lib::RawTimestamp> {
|
||||||
return match ksp_store_lib::RawTimestamp::from_unix_millis(1_700_000_001_000) {
|
return match ksp_store_lib::RawTimestamp::from_unix_millis(1_700_000_001_000) {
|
||||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||||
@@ -169,21 +177,26 @@ fn pre_006_wire_omission_and_null_produce_distinct_canonical_bytes() {
|
|||||||
let version_null = ksp_onchain_transport_lib::SolanaWireField::Null;
|
let version_null = ksp_onchain_transport_lib::SolanaWireField::Null;
|
||||||
let transaction_index_omitted = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
let transaction_index_omitted = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
let transaction_index_null = ksp_onchain_transport_lib::SolanaWireField::Null;
|
let transaction_index_null = ksp_onchain_transport_lib::SolanaWireField::Null;
|
||||||
let omitted = super::canonical_payload_bytes(&fields(&transaction, &meta_omitted, &version_omitted, &transaction_index_omitted, std::option::Option::None));
|
let reference = match raw_reference("devnet") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let omitted =
|
||||||
|
super::canonical_transaction(&reference, fields(&transaction, &meta_omitted, &version_omitted, &transaction_index_omitted, std::option::Option::None));
|
||||||
assert!(omitted.is_ok());
|
assert!(omitted.is_ok());
|
||||||
let omitted = match omitted {
|
let omitted = match omitted {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return,
|
std::result::Result::Err(_) => return,
|
||||||
};
|
};
|
||||||
assert_eq!(omitted, b"{\"transaction\":[\"AQID\",\"base64\"]}");
|
assert_eq!(omitted.payload().bytes(), b"{\"transaction\":[\"AQID\",\"base64\"]}");
|
||||||
let nulls = super::canonical_payload_bytes(&fields(&transaction, &meta_null, &version_null, &transaction_index_null, std::option::Option::None));
|
let nulls = super::canonical_transaction(&reference, fields(&transaction, &meta_null, &version_null, &transaction_index_null, std::option::Option::None));
|
||||||
assert!(nulls.is_ok());
|
assert!(nulls.is_ok());
|
||||||
let nulls = match nulls {
|
let nulls = match nulls {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(_) => return,
|
std::result::Result::Err(_) => return,
|
||||||
};
|
};
|
||||||
assert_eq!(nulls, b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":null,\"version\":null,\"transactionIndex\":null}");
|
assert_eq!(nulls.payload().bytes(), b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":null,\"version\":null,\"transactionIndex\":null}");
|
||||||
assert_ne!(omitted, nulls);
|
assert_ne!(omitted.payload().bytes(), nulls.payload().bytes());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +211,12 @@ fn pre_006_non_base64_transaction_shapes_are_rejected() {
|
|||||||
let meta = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
let meta = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
let version = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
let version = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
let transaction_index = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
let transaction_index = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
|
let reference = match raw_reference("devnet") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
for transaction in [&base58, &legacy, &json] {
|
for transaction in [&base58, &legacy, &json] {
|
||||||
let result = super::canonical_payload_bytes(&fields(transaction, &meta, &version, &transaction_index, std::option::Option::None));
|
let result = super::canonical_transaction(&reference, fields(transaction, &meta, &version, &transaction_index, std::option::Option::None));
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
if let std::result::Result::Err(error) = result {
|
if let std::result::Result::Err(error) = result {
|
||||||
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID);
|
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID);
|
||||||
@@ -210,12 +227,26 @@ fn pre_006_non_base64_transaction_shapes_are_rejected() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_006_negative_and_unrepresentable_block_times_are_terminal_conversion_errors() {
|
fn pre_006_negative_and_unrepresentable_block_times_are_terminal_conversion_errors() {
|
||||||
let negative = super::convert_block_time(std::option::Option::Some(-1));
|
let reference = match raw_reference("devnet") {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let transaction = ksp_onchain_transport_lib::SolanaEncodedTransaction::Binary {
|
||||||
|
data: "AQID".to_owned(),
|
||||||
|
encoding: ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64,
|
||||||
|
};
|
||||||
|
let meta = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
|
let version = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
|
let transaction_index = ksp_onchain_transport_lib::SolanaWireField::Omitted;
|
||||||
|
let negative = super::canonical_transaction(&reference, fields(&transaction, &meta, &version, &transaction_index, std::option::Option::Some(-1)));
|
||||||
assert!(negative.is_err());
|
assert!(negative.is_err());
|
||||||
let oversized = super::convert_block_time(std::option::Option::Some(i64::MAX));
|
let oversized = super::canonical_transaction(&reference, fields(&transaction, &meta, &version, &transaction_index, std::option::Option::Some(i64::MAX)));
|
||||||
assert!(oversized.is_err());
|
assert!(oversized.is_err());
|
||||||
let absent = super::convert_block_time(std::option::Option::None);
|
let absent = super::canonical_transaction(&reference, fields(&transaction, &meta, &version, &transaction_index, std::option::Option::None));
|
||||||
assert!(matches!(absent, std::result::Result::Ok(std::option::Option::None)));
|
assert!(absent.is_ok());
|
||||||
|
if let std::result::Result::Ok(absent) = absent {
|
||||||
|
assert!(absent.block_time().is_none());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +273,12 @@ fn pre_006_observation_key_is_deterministic_and_endpoint_specific() {
|
|||||||
let other_endpoint = super::observation_key(&request, &reference, "provider", "endpoint-b");
|
let other_endpoint = super::observation_key(&request, &reference, "provider", "endpoint-b");
|
||||||
let other_provider = super::observation_key(&request, &reference, "provider-2", "endpoint-a");
|
let other_provider = super::observation_key(&request, &reference, "provider-2", "endpoint-a");
|
||||||
assert_eq!(first, same);
|
assert_eq!(first, same);
|
||||||
|
assert_eq!(
|
||||||
|
first.as_bytes(),
|
||||||
|
&[
|
||||||
|
184, 85, 15, 15, 33, 101, 243, 112, 222, 145, 139, 212, 251, 14, 199, 130, 57, 26, 253, 221, 184, 140, 87, 240, 16, 116, 62, 0, 11, 19, 103, 2
|
||||||
|
]
|
||||||
|
);
|
||||||
assert_ne!(first, other_endpoint);
|
assert_ne!(first, other_endpoint);
|
||||||
assert_ne!(first, other_provider);
|
assert_ne!(first, other_provider);
|
||||||
return;
|
return;
|
||||||
@@ -294,6 +331,15 @@ fn pre_006_missing_outcome_contains_only_network_scoped_reference() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_003_backfill_uses_common_raw_v1_contract_without_changing_frozen_identity() {
|
||||||
|
assert_eq!(crate::RAW_TRANSACTION_FORMAT_ID, ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_ID);
|
||||||
|
assert_eq!(crate::RAW_TRANSACTION_FORMAT_VERSION, ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_VERSION);
|
||||||
|
assert_eq!(crate::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, ksp_raw_transaction_lib::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES);
|
||||||
|
assert_eq!(crate::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, ksp_raw_transaction_lib::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_006_fix_001_raw_acquisition_uses_one_private_indirection() {
|
fn pre_006_fix_001_raw_acquisition_uses_one_private_indirection() {
|
||||||
assert_eq!(std::mem::size_of::<crate::BackfillRawAcquisition>(), std::mem::size_of::<usize>(),);
|
assert_eq!(std::mem::size_of::<crate::BackfillRawAcquisition>(), std::mem::size_of::<usize>(),);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||||
<!-- version: 24 -->
|
<!-- version: 25 -->
|
||||||
|
|
||||||
# Utilisation de `ksp-onchain-transport-lib`
|
# Utilisation de `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
@@ -449,7 +449,9 @@ let inflation_rate = pool.get_inflation_rate(&role).await;
|
|||||||
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
|
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
|
||||||
```
|
```
|
||||||
|
|
||||||
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
|
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Pour les consumers qui doivent conserver la provenance exacte d’un pool multi-endpoint, `get_block_observed(...)` exécute la même forme moderne et retourne la valeur typée avec le nom sûr de l’endpoint et le provider réellement gagnants après sélection/retry/reroute. Cette projection n’expose ni URL, ni headers, ni body HTTP brut. Une réponse RPC `null` reste `None` dans la valeur observée.
|
||||||
|
|
||||||
|
Lorsque `transactionDetails = full` et `encoding = base64`, chaque `SolanaBlockTransaction` conserve le transaction wire Base64, `meta` et `version`. Le consumer peut alors projeter ces DTOs vers sa couche métier sans faire dépendre Transport d’une canonicalisation RAW particulière. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
|
||||||
|
|
||||||
## 6. Exécution JSON-RPC standard générique
|
## 6. Exécution JSON-RPC standard générique
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[],"numRewardPartitions":0,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
|
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
|
||||||
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
|
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
|
||||||
@@ -591,25 +591,42 @@ impl crate::HttpTransportPool {
|
|||||||
slot: u64,
|
slot: u64,
|
||||||
config: std::option::Option<&crate::SolanaGetBlockConfig>,
|
config: std::option::Option<&crate::SolanaGetBlockConfig>,
|
||||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||||
if let std::option::Option::Some(config) = config
|
let params = get_block_params(slot, config);
|
||||||
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
let params = match params {
|
||||||
{
|
std::result::Result::Ok(params) => params,
|
||||||
return std::result::Result::Err(
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
ksp_core_lib::Error::new(
|
};
|
||||||
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
|
||||||
"getBlock commitment must be confirmed or finalized when explicitly provided",
|
|
||||||
)
|
|
||||||
.with_context("rpc_method", "getBlock")
|
|
||||||
.with_context("commitment", "processed"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut params = std::vec![serde_json::json!(slot)];
|
|
||||||
if let std::option::Option::Some(config) = config {
|
|
||||||
params.push((*config).to_json_value());
|
|
||||||
}
|
|
||||||
return self.execute_get_block(role, params).await;
|
return self.execute_get_block(role, params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the current object-form `getBlock` request and reports the safe identity of the endpoint that produced the successful response.
|
||||||
|
///
|
||||||
|
/// Routing, admission, timeout and retry behavior are identical to [`Self::get_block`]. The returned observation never contains an endpoint URL,
|
||||||
|
/// HTTP headers or a raw HTTP body.
|
||||||
|
pub async fn get_block_observed(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
slot: u64,
|
||||||
|
config: std::option::Option<&crate::SolanaGetBlockConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::HttpObservedValue<std::option::Option<crate::SolanaConfirmedBlock>>> {
|
||||||
|
let params = get_block_params(slot, config);
|
||||||
|
let params = match params {
|
||||||
|
std::result::Result::Ok(params) => params,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let observed = self.execute_blocks_rpc_observed("getBlock", role, params).await;
|
||||||
|
let observed = match observed {
|
||||||
|
std::result::Result::Ok(observed) => observed,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let (value, endpoint_name, provider) = observed.into_parts();
|
||||||
|
let block = decode_get_block(value);
|
||||||
|
return match block {
|
||||||
|
std::result::Result::Ok(block) => std::result::Result::Ok(crate::HttpObservedValue::new(block, endpoint_name, provider)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility.
|
/// Executes the deprecated bare-encoding `getBlock` request form retained by Solana RPC for backwards compatibility.
|
||||||
#[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")]
|
#[deprecated(note = "use HttpTransportPool::get_block with SolanaGetBlockConfig; the bare encoding request form is deprecated")]
|
||||||
pub async fn get_block_legacy(
|
pub async fn get_block_legacy(
|
||||||
@@ -635,16 +652,8 @@ impl crate::HttpTransportPool {
|
|||||||
params: std::vec::Vec<serde_json::Value>,
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||||
let value = self.execute_blocks_rpc("getBlock", role, params).await;
|
let value = self.execute_blocks_rpc("getBlock", role, params).await;
|
||||||
let value = match value {
|
return match value {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => decode_get_block(value),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
};
|
|
||||||
if value.is_null() {
|
|
||||||
return std::result::Result::Ok(std::option::Option::None);
|
|
||||||
}
|
|
||||||
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
|
|
||||||
return match block {
|
|
||||||
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
|
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -829,6 +838,48 @@ impl crate::HttpTransportPool {
|
|||||||
};
|
};
|
||||||
return self.execute_standard_rpc(role, method, params).await;
|
return self.execute_standard_rpc(role, method, params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_blocks_rpc_observed(
|
||||||
|
&self,
|
||||||
|
method_name: &'static str,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
|
) -> ksp_core_lib::Result<crate::HttpObservedValue<serde_json::Value>> {
|
||||||
|
let method = blocks_descriptor(method_name);
|
||||||
|
let method = match method {
|
||||||
|
std::result::Result::Ok(method) => method,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return self.execute_standard_rpc_observed(role, method, params).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_get_block(value: serde_json::Value) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedBlock>> {
|
||||||
|
if value.is_null() {
|
||||||
|
return std::result::Result::Ok(std::option::Option::None);
|
||||||
|
}
|
||||||
|
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value);
|
||||||
|
return match block {
|
||||||
|
std::result::Result::Ok(block) => std::result::Result::Ok(std::option::Option::Some(block)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_block_params(slot: u64, config: std::option::Option<&crate::SolanaGetBlockConfig>) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getBlock commitment must be confirmed or finalized when explicitly provided")
|
||||||
|
.with_context("rpc_method", "getBlock")
|
||||||
|
.with_context("commitment", "processed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut params = std::vec![serde_json::json!(slot)];
|
||||||
|
if let std::option::Option::Some(config) = config {
|
||||||
|
params.push((*config).to_json_value());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 49
|
// version: 50
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -1021,3 +1021,10 @@ fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_
|
|||||||
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
||||||
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_3_10_pre_004_observed_get_block_surface_is_available_from_crate_root() {
|
||||||
|
let method = ksp_onchain_transport_lib::HttpTransportPool::get_block_observed;
|
||||||
|
let _ = method;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
|
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
|
||||||
@@ -188,6 +188,35 @@ fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHand
|
|||||||
return (format!("http://{address}"), handle);
|
return (format!("http://{address}"), handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_status_and_count(status_line: &'static str) -> (std::string::String, std::thread::JoinHandle<(usize, std::string::String)>) {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||||
|
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().expect("fixture server must accept first request");
|
||||||
|
let first_request = read_request(&mut stream);
|
||||||
|
let response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||||
|
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||||
|
let mut count = 1_usize;
|
||||||
|
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(120);
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
match listener.accept() {
|
||||||
|
std::result::Result::Ok((mut retry_stream, _)) => {
|
||||||
|
let _ = read_request(&mut retry_stream);
|
||||||
|
std::io::Write::write_all(&mut retry_stream, response.as_bytes()).expect("fixture retry response must write");
|
||||||
|
count = count.saturating_add(1);
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => panic!("fixture listener failed while counting retries: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (count, first_request);
|
||||||
|
});
|
||||||
|
return (format!("http://{address}"), handle);
|
||||||
|
}
|
||||||
|
|
||||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||||
let mut bytes = std::vec::Vec::new();
|
let mut bytes = std::vec::Vec::new();
|
||||||
let mut buffer = [0_u8; 1024];
|
let mut buffer = [0_u8; 1024];
|
||||||
@@ -784,3 +813,92 @@ async fn typed_get_block_rejects_processed_commitment_before_io() {
|
|||||||
assert_eq!(error.context()[1].key(), "commitment");
|
assert_eq!(error.context()[1].key(), "commitment");
|
||||||
assert_eq!(error.context()[1].value(), "processed");
|
assert_eq!(error.context()[1].value(), "processed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn pre_004_get_block_observed_reports_actual_winner_after_retry_reroute() {
|
||||||
|
let (first_url, first_handle) = serve_status_and_count("429 Too Many Requests");
|
||||||
|
let (winner_url, winner_handle) = serve_once(include_str!("../fixtures/http/get_block.observed_material.success.json"));
|
||||||
|
let urls = [(first_url.as_str(), "first-endpoint", "first-provider"), (winner_url.as_str(), "winner-endpoint", "winner-provider")];
|
||||||
|
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
|
||||||
|
for (url, endpoint_name, provider) in urls {
|
||||||
|
let role = crate::HttpEndpointRoleSettings::new(
|
||||||
|
crate::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![crate::HttpRequestKind::wildcard()],
|
||||||
|
10,
|
||||||
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
endpoints.push(crate::HttpEndpointSettings::new(
|
||||||
|
endpoint_name,
|
||||||
|
true,
|
||||||
|
crate::HttpProviderName::new(provider),
|
||||||
|
crate::HttpClusterName::new("local"),
|
||||||
|
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
std::vec![role],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let pool = crate::HttpTransportPool::new(crate::HttpTransportSettings::new(
|
||||||
|
endpoints,
|
||||||
|
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||||
|
))
|
||||||
|
.expect("observed fixture pool must build");
|
||||||
|
let config = crate::SolanaGetBlockConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
);
|
||||||
|
let observed = pool
|
||||||
|
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("retry-safe observed getBlock must succeed on the second endpoint");
|
||||||
|
assert_eq!(observed.endpoint_name(), "winner-endpoint");
|
||||||
|
assert_eq!(observed.provider().as_str(), "winner-provider");
|
||||||
|
let block = observed.value().as_ref().expect("winning response must contain a block");
|
||||||
|
let transactions = block.transactions().value().expect("winning block must contain transactions");
|
||||||
|
assert_eq!(transactions.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
transactions[0].transaction(),
|
||||||
|
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
|
||||||
|
));
|
||||||
|
let rendered = format!("{observed:?}");
|
||||||
|
assert!(rendered.contains("winner-endpoint"));
|
||||||
|
assert!(rendered.contains("winner-provider"));
|
||||||
|
assert!(rendered.contains("<available>"));
|
||||||
|
assert!(!rendered.contains("AQAAAAAAAA"));
|
||||||
|
let (first_count, first_request) = first_handle.join().expect("first fixture server must join");
|
||||||
|
assert_eq!(first_count, 1);
|
||||||
|
assert_eq!(request_body(first_request.as_str())["method"], serde_json::json!("getBlock"));
|
||||||
|
let winner_request = winner_handle.join().expect("winner fixture server must join");
|
||||||
|
assert_eq!(request_body(winner_request.as_str())["method"], serde_json::json!("getBlock"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn pre_004_get_block_observed_preserves_null_and_reuses_get_block_validation() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block.null.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let observed = pool
|
||||||
|
.get_block_observed(&crate::HttpRoleName::new("default"), 430_000_123, std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("observed null getBlock must succeed");
|
||||||
|
assert!(observed.value().is_none());
|
||||||
|
assert_eq!(observed.endpoint_name(), "fixture");
|
||||||
|
assert_eq!(observed.provider().as_str(), "fixture");
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
let processed = crate::SolanaGetBlockConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
);
|
||||||
|
let result = pool.get_block_observed(&crate::HttpRoleName::new("default"), 1, std::option::Option::Some(&processed)).await;
|
||||||
|
let error = result.expect_err("processed observed getBlock commitment must reject before I/O");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|||||||
18
crates/ksp-raw-transaction-lib/Cargo.toml
Normal file
18
crates/ksp-raw-transaction-lib/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# file: crates/ksp-raw-transaction-lib/Cargo.toml
|
||||||
|
# version: 2
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-raw-transaction-lib"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64.workspace = true
|
||||||
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
|
ksp-store-api = { path = "../ksp-store-api" }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
97
crates/ksp-raw-transaction-lib/README.md
Normal file
97
crates/ksp-raw-transaction-lib/README.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!-- file: crates/ksp-raw-transaction-lib/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# ksp-raw-transaction-lib
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib` fournit la lower-layer KSP source-neutral commune aux producteurs de `RawTransaction`.
|
||||||
|
|
||||||
|
La crate transforme un matériau transactionnel complet déjà acquis en représentation RAW canonique KSP, fournit les primitives de parsing de signature et de sérialisation wire Solana nécessaires aux voies qualifiées, puis permet d'associer la transaction canonique à une observation/provenance possédée par le producer.
|
||||||
|
|
||||||
|
Elle ne possède aucun Transport, Config, Job, Worker, runtime async, Store runtime ou backend physique.
|
||||||
|
|
||||||
|
## Responsabilités
|
||||||
|
|
||||||
|
La façade crate-root expose quatre familles de contrats.
|
||||||
|
|
||||||
|
### Canonicalisation RAW Transaction
|
||||||
|
|
||||||
|
- `RawTransactionMaterial` ;
|
||||||
|
- `RawTransactionWireField<T>` pour préserver distinctement omission, `null` explicite et valeur ;
|
||||||
|
- `RawTransactionVersion` ;
|
||||||
|
- `canonicalize_raw_transaction` ;
|
||||||
|
- `RAW_TRANSACTION_FORMAT_ID` et `RAW_TRANSACTION_FORMAT_VERSION`.
|
||||||
|
|
||||||
|
La canonicalisation produit un `ksp_store_api::RawTransaction` avec payload KSP déterministe et SHA-256 de contenu. Le producer reste responsable de fournir un matériau source-neutral complet ; la crate n'effectue aucune I/O réseau.
|
||||||
|
|
||||||
|
### Signature Solana
|
||||||
|
|
||||||
|
- `parse_raw_transaction_signature` valide et décode une signature Base58 bornée vers exactement 64 bytes ;
|
||||||
|
- `extract_raw_transaction_signature_from_binary_base64` extrait la première signature canonique d'un wire transactionnel Base64 complet ;
|
||||||
|
- les bornes textuelles publiques permettent l'admission avant décodage.
|
||||||
|
|
||||||
|
Les diagnostics ne recopient pas la signature hostile.
|
||||||
|
|
||||||
|
### Wire transactionnel source-neutral
|
||||||
|
|
||||||
|
La surface `RawSolana*` représente et sérialise les messages transactionnels Solana Legacy, V0 et V1 nécessaires à la common RAW :
|
||||||
|
|
||||||
|
- `RawSolanaMessageVersion` ;
|
||||||
|
- `RawSolanaMessageHeader` ;
|
||||||
|
- `RawSolanaCompiledInstruction` ;
|
||||||
|
- `RawSolanaAddressTableLookup` ;
|
||||||
|
- `RawSolanaTransactionConfig` ;
|
||||||
|
- `RawSolanaTransactionMessage` ;
|
||||||
|
- `RawSolanaTransactionWire` ;
|
||||||
|
- `serialize_solana_transaction_wire` et `serialize_solana_transaction_wire_base64`.
|
||||||
|
|
||||||
|
Cette surface est un contrat de wire commun, pas un decoder Program et pas une façade Transport.
|
||||||
|
|
||||||
|
### Acquisition canonique + observation
|
||||||
|
|
||||||
|
`assemble_raw_transaction_acquisition` associe une transaction canonique à :
|
||||||
|
|
||||||
|
- une `RawObservationKey` déterministe possédée par le producer ;
|
||||||
|
- une `RawAcquisitionProvenance` sûre possédée par le producer.
|
||||||
|
|
||||||
|
Le résultat `RawTransactionAcquisition` contient exactement une entité canonique et une observation liée. Il ne persiste rien lui-même.
|
||||||
|
|
||||||
|
## Frontières
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport / fixture / import / producer
|
||||||
|
|
|
||||||
|
v
|
||||||
|
matériau source-neutral
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ksp-raw-transaction-lib
|
||||||
|
| |
|
||||||
|
| +-> exact Solana wire helpers
|
||||||
|
v
|
||||||
|
RawTransaction + RawTransactionObservation
|
||||||
|
|
|
||||||
|
v
|
||||||
|
producer runtime -> ksp-store-lib / capability Store appropriée
|
||||||
|
```
|
||||||
|
|
||||||
|
Interdictions structurelles :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-onchain-transport-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-config-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-job-api / ksp-job-backfill-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-worker-api / worker concret
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-store-lib / backend physique
|
||||||
|
```
|
||||||
|
|
||||||
|
Le graphe normal reste limité à `ksp-core-lib`, `ksp-store-api`, `base64`, `serde_json` et `sha2`.
|
||||||
|
|
||||||
|
## Relation avec STRUCTURAL
|
||||||
|
|
||||||
|
Cette crate appartient à **RAW**. Elle ne réalise pas la décomposition Store D2 `RAW -> STRUCTURAL`. La future couche STRUCTURAL consommera le RAW persistant et le décomposera en unités Solana génériques plus fines avant tout décodage Program.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [`USAGE.md`](USAGE.md) — utilisation durable de la façade publique ;
|
||||||
|
- [`../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md`](../../docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md) — frontières RAW / STRUCTURAL / DECODED / DOMAIN ;
|
||||||
|
- [`../../docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`](../../docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md) — qualification des sources `RawTransaction`.
|
||||||
153
crates/ksp-raw-transaction-lib/USAGE.md
Normal file
153
crates/ksp-raw-transaction-lib/USAGE.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<!-- file: crates/ksp-raw-transaction-lib/USAGE.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Utilisation de ksp-raw-transaction-lib
|
||||||
|
|
||||||
|
Cette page décrit la façade publique durable de `ksp-raw-transaction-lib`. Les consumers utilisent uniquement les exports du crate-root.
|
||||||
|
|
||||||
|
## Parser une signature transactionnelle
|
||||||
|
|
||||||
|
Lorsqu'une source fournit une signature Base58 textuelle, la convertir avec le parser commun plutôt que de dupliquer le décodage :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn parse_signature(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
return ksp_raw_transaction_lib::parse_raw_transaction_signature(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résultat contient exactement 64 bytes canoniques. Les entrées vides, hors bornes, Base58 invalides ou de longueur décodée incorrecte sont rejetées sans recopier la valeur hostile dans l'erreur.
|
||||||
|
|
||||||
|
Lorsqu'un wire Base64 complet contient déjà son tableau de signatures, utiliser :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn embedded_signature(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
return ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Construire et sérialiser un wire Solana source-neutral
|
||||||
|
|
||||||
|
Le producer peut projeter son DTO Transport/protobuf vers les types `RawSolana*`, puis utiliser le sérialiseur commun. Exemple V1 minimal :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn serialize_v1() -> ksp_core_lib::Result<std::string::String> {
|
||||||
|
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[1_u8; 32]],
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(0, vec![0], vec![3])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[4_u8; 64]], message);
|
||||||
|
return ksp_raw_transaction_lib::serialize_solana_transaction_wire_base64(&wire);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour Legacy ou V0, choisir `RawSolanaMessageVersion::Legacy` ou `V0` et fournir uniquement les éléments admis par cette version. Le sérialiseur valide les invariants structurels avant de produire les bytes.
|
||||||
|
|
||||||
|
`RawSolanaAddressTableLookup` représente les lookups V0. `RawSolanaTransactionConfig` représente la configuration inline V1 ; le cas explicite où toutes ses options valent `None` reste sémantiquement présent.
|
||||||
|
|
||||||
|
## Construire le matériau RAW canonique
|
||||||
|
|
||||||
|
Une fois le wire transactionnel Base64 et les métadonnées source-neutral disponibles, construire `RawTransactionMaterial`. Les états wire doivent conserver la différence entre champ absent, `null` explicite et valeur :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn material(
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
signature: ksp_store_api::RawTransactionSignature,
|
||||||
|
transaction_base64: std::string::String,
|
||||||
|
) -> ksp_raw_transaction_lib::RawTransactionMaterial {
|
||||||
|
return ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
42,
|
||||||
|
std::option::Option::None,
|
||||||
|
transaction_base64,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy),
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour une source où la signature doit être extraite du wire lui-même, `RawTransactionMaterial::binary_base64_with_embedded_signature` évite un second parser producer-owned.
|
||||||
|
|
||||||
|
## Canonicaliser vers RawTransaction
|
||||||
|
|
||||||
|
La canonicalisation est déterministe pour un même matériau complet :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn canonicalize(
|
||||||
|
material: ksp_raw_transaction_lib::RawTransactionMaterial,
|
||||||
|
) -> ksp_core_lib::Result<ksp_store_api::RawTransaction> {
|
||||||
|
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La crate :
|
||||||
|
|
||||||
|
- canonicalise les sous-arbres JSON significatifs ;
|
||||||
|
- conserve les états omission / `null` / valeur ;
|
||||||
|
- construit le payload au format KSP RAW Transaction ;
|
||||||
|
- calcule son SHA-256 ;
|
||||||
|
- applique les bornes communes avant construction du modèle Store.
|
||||||
|
|
||||||
|
Elle ne persiste pas la transaction et ne choisit aucune politique de retry, endpoint ou source.
|
||||||
|
|
||||||
|
## Associer l'observation du producer
|
||||||
|
|
||||||
|
Le producer construit sa provenance avec les types Store API, puis assemble l'entité et l'observation :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn acquisition(
|
||||||
|
transaction: ksp_store_api::RawTransaction,
|
||||||
|
) -> ksp_core_lib::Result<ksp_raw_transaction_lib::RawTransactionAcquisition> {
|
||||||
|
let provider = match ksp_store_api::RawProvenanceCode::new("example") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let protocol = match ksp_store_api::RawProvenanceCode::new("solana.http") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let method = match ksp_store_api::RawProvenanceCode::new("get_transaction") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_700_000_000_000) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let provenance = ksp_store_api::RawAcquisitionProvenance::new(
|
||||||
|
provider,
|
||||||
|
protocol,
|
||||||
|
method,
|
||||||
|
ksp_store_api::RawAcquisitionOrigin::Live,
|
||||||
|
received_at,
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(
|
||||||
|
transaction,
|
||||||
|
ksp_store_api::RawObservationKey::new([7_u8; 32]),
|
||||||
|
provenance,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La clé d'observation doit être déterministe selon la politique du producer. Plusieurs sources peuvent produire des observations différentes pour la même identité canonique `(network, signature)`.
|
||||||
|
|
||||||
|
## Persister depuis un Job ou un Worker
|
||||||
|
|
||||||
|
La common crate ne dépend pas du Store runtime. Le lifecycle host concret récupère les deux parties puis appelle la façade Store appropriée :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (transaction, observation) = acquisition.into_parts();
|
||||||
|
// Le Job/Worker concret transmet ensuite transaction + observation à ksp-store-lib.
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne pas introduire `ksp-store-lib`, Transport, Config, Job ou Worker dans `ksp-raw-transaction-lib` pour simplifier cet appel. La direction de dépendance reste du producer vers la common RAW, puis du producer vers le Store runtime.
|
||||||
|
|
||||||
|
## Frontière avec STRUCTURAL
|
||||||
|
|
||||||
|
Ne pas utiliser cette crate pour décoder des Programs ou pour produire directement des unités STRUCTURAL. Son résultat reste D1 RAW. La décomposition `RAW -> STRUCTURAL` appartient à une couche ultérieure et réutilise le RAW durable comme input.
|
||||||
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal file
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/acquisition.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||||
|
pub struct RawTransactionAcquisition {
|
||||||
|
observation: ksp_store_api::RawTransactionObservation,
|
||||||
|
transaction: ksp_store_api::RawTransaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawTransactionAcquisition {
|
||||||
|
/// Returns the producer-owned acquisition observation linked to the canonical transaction.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn observation(&self) -> &ksp_store_api::RawTransactionObservation {
|
||||||
|
return &self.observation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the canonical source-independent RAW transaction.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn transaction(&self) -> &ksp_store_api::RawTransaction {
|
||||||
|
return &self.transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this acquisition into its canonical transaction and observation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_parts(self) -> (ksp_store_api::RawTransaction, ksp_store_api::RawTransactionObservation) {
|
||||||
|
return (self.transaction, self.observation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawTransactionAcquisition {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.write_str("RawTransactionAcquisition(..)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
|
||||||
|
#[must_use]
|
||||||
|
pub fn assemble_raw_transaction_acquisition(
|
||||||
|
transaction: ksp_store_api::RawTransaction,
|
||||||
|
observation_key: ksp_store_api::RawObservationKey,
|
||||||
|
provenance: ksp_store_api::RawAcquisitionProvenance,
|
||||||
|
) -> crate::RawTransactionAcquisition {
|
||||||
|
let observation = ksp_store_api::RawTransactionObservation::new(observation_key, transaction.reference().clone(), provenance);
|
||||||
|
return crate::RawTransactionAcquisition { observation, transaction };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/acquisition.rs"]
|
||||||
|
mod tests;
|
||||||
325
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal file
325
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/canonical.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// KSP-owned source-independent RAW transaction format identifier.
|
||||||
|
pub const RAW_TRANSACTION_FORMAT_ID: &str = "ksp.solana.raw_transaction";
|
||||||
|
/// Frozen KSP-owned RAW transaction format version.
|
||||||
|
pub const RAW_TRANSACTION_FORMAT_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum RawTransactionWireField<T> {
|
||||||
|
/// The source field was omitted entirely.
|
||||||
|
Omitted,
|
||||||
|
/// The source field was explicitly present as JSON/protocol null.
|
||||||
|
Null,
|
||||||
|
/// The source field carried one concrete value.
|
||||||
|
Value(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Debug for crate::RawTransactionWireField<T> {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return match self {
|
||||||
|
Self::Omitted => formatter.write_str("Omitted"),
|
||||||
|
Self::Null => formatter.write_str("Null"),
|
||||||
|
Self::Value(_) => formatter.write_str("Value(..)"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical Solana transaction version admitted by RAW transaction format v1.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RawTransactionVersion {
|
||||||
|
/// Legacy unversioned transaction representation.
|
||||||
|
Legacy,
|
||||||
|
/// Explicit versioned transaction number admitted by the current Solana wire contract.
|
||||||
|
Number(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete source-neutral material required to construct one canonical RAW transaction.
|
||||||
|
pub struct RawTransactionMaterial {
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
meta: crate::RawTransactionWireField<serde_json::Value>,
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
signature: ksp_store_api::RawTransactionSignature,
|
||||||
|
slot: u64,
|
||||||
|
transaction_data: std::string::String,
|
||||||
|
transaction_index: crate::RawTransactionWireField<u32>,
|
||||||
|
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawTransactionMaterial {
|
||||||
|
/// Creates complete source-neutral material for the frozen RAW v1 binary Base64 body path.
|
||||||
|
#[must_use]
|
||||||
|
pub fn binary_base64(
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
signature: ksp_store_api::RawTransactionSignature,
|
||||||
|
slot: u64,
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
transaction_data: impl std::convert::Into<std::string::String>,
|
||||||
|
meta: crate::RawTransactionWireField<serde_json::Value>,
|
||||||
|
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
|
||||||
|
transaction_index: crate::RawTransactionWireField<u32>,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
block_time,
|
||||||
|
meta,
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
slot,
|
||||||
|
transaction_data: transaction_data.into(),
|
||||||
|
transaction_index,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates complete source-neutral RAW v1 material from a Base64 transaction wire that embeds its own canonical signature array.
|
||||||
|
pub fn binary_base64_with_embedded_signature(
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
slot: u64,
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
transaction_data: impl std::convert::Into<std::string::String>,
|
||||||
|
meta: crate::RawTransactionWireField<serde_json::Value>,
|
||||||
|
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
|
||||||
|
transaction_index: crate::RawTransactionWireField<u32>,
|
||||||
|
) -> ksp_core_lib::Result<Self> {
|
||||||
|
let transaction_data = transaction_data.into();
|
||||||
|
let signature = crate::extract_raw_transaction_signature_from_binary_base64(transaction_data.as_str());
|
||||||
|
return match signature {
|
||||||
|
std::result::Result::Ok(signature) => {
|
||||||
|
std::result::Result::Ok(Self { block_time, meta, network, signature, slot, transaction_data, transaction_index, version })
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawTransactionMaterial {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawTransactionMaterial")
|
||||||
|
.field("network", &self.network)
|
||||||
|
.field("slot", &self.slot)
|
||||||
|
.field("block_time", &self.block_time)
|
||||||
|
.field("transaction_data_len", &self.transaction_data.len())
|
||||||
|
.field("meta", &wire_field_state(&self.meta))
|
||||||
|
.field("version", &wire_field_state(&self.version))
|
||||||
|
.field("transaction_index", &wire_field_state(&self.transaction_index))
|
||||||
|
.finish_non_exhaustive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
|
||||||
|
pub fn canonicalize_raw_transaction(material: crate::RawTransactionMaterial) -> ksp_core_lib::Result<ksp_store_api::RawTransaction> {
|
||||||
|
let block_time = convert_block_time(material.block_time);
|
||||||
|
let block_time = match block_time {
|
||||||
|
std::result::Result::Ok(block_time) => block_time,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let bytes = canonical_payload_bytes(&material);
|
||||||
|
let bytes = match bytes {
|
||||||
|
std::result::Result::Ok(bytes) => bytes,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if bytes.is_empty() || bytes.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
|
||||||
|
return std::result::Result::Err(crate::canonicalization_error("payload.bytes"));
|
||||||
|
}
|
||||||
|
let hash: [u8; 32] = sha2::Sha256::digest(bytes.as_slice()).into();
|
||||||
|
let format_id = ksp_store_api::RawFormatId::new(crate::RAW_TRANSACTION_FORMAT_ID);
|
||||||
|
let format_id = match format_id {
|
||||||
|
std::result::Result::Ok(format_id) => format_id,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload.format_id")),
|
||||||
|
};
|
||||||
|
let payload = ksp_store_api::RawPayload::try_new(
|
||||||
|
format_id,
|
||||||
|
crate::RAW_TRANSACTION_FORMAT_VERSION,
|
||||||
|
bytes.into_boxed_slice(),
|
||||||
|
ksp_store_api::RawContentHash::new(hash),
|
||||||
|
);
|
||||||
|
let payload = match payload {
|
||||||
|
std::result::Result::Ok(payload) => payload,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload")),
|
||||||
|
};
|
||||||
|
let reference = ksp_store_api::RawTransactionReference::new(material.network, material.signature);
|
||||||
|
return std::result::Result::Ok(ksp_store_api::RawTransaction::new(reference, material.slot, block_time, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_canonical_json(output: &mut std::vec::Vec<u8>, value: &serde_json::Value) -> ksp_core_lib::Result<()> {
|
||||||
|
return match value {
|
||||||
|
serde_json::Value::Null => {
|
||||||
|
output.extend_from_slice(b"null");
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
serde_json::Value::Bool(value) => {
|
||||||
|
if *value {
|
||||||
|
output.extend_from_slice(b"true");
|
||||||
|
} else {
|
||||||
|
output.extend_from_slice(b"false");
|
||||||
|
}
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
serde_json::Value::Number(value) => {
|
||||||
|
output.extend_from_slice(value.to_string().as_bytes());
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
serde_json::Value::String(value) => append_json_string(output, value.as_str()),
|
||||||
|
serde_json::Value::Array(values) => {
|
||||||
|
output.push(b'[');
|
||||||
|
for (index, item) in values.iter().enumerate() {
|
||||||
|
if index != 0 {
|
||||||
|
output.push(b',');
|
||||||
|
}
|
||||||
|
let item_result = append_canonical_json(output, item);
|
||||||
|
if let std::result::Result::Err(error) = item_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push(b']');
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
serde_json::Value::Object(values) => {
|
||||||
|
output.push(b'{');
|
||||||
|
let mut keys = values.keys().collect::<std::vec::Vec<_>>();
|
||||||
|
keys.sort_unstable();
|
||||||
|
for (index, key) in keys.iter().enumerate() {
|
||||||
|
if index != 0 {
|
||||||
|
output.push(b',');
|
||||||
|
}
|
||||||
|
let key_result = append_json_string(output, key.as_str());
|
||||||
|
if let std::result::Result::Err(error) = key_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.push(b':');
|
||||||
|
let item = values.get(key.as_str());
|
||||||
|
let item = match item {
|
||||||
|
std::option::Option::Some(item) => item,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::canonicalization_error("payload.meta")),
|
||||||
|
};
|
||||||
|
let item_result = append_canonical_json(output, item);
|
||||||
|
if let std::result::Result::Err(error) = item_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.push(b'}');
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_json_string(output: &mut std::vec::Vec<u8>, value: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
let encoded = serde_json::to_vec(value);
|
||||||
|
return match encoded {
|
||||||
|
std::result::Result::Ok(encoded) => {
|
||||||
|
output.extend_from_slice(encoded.as_slice());
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(crate::canonicalization_error("payload.json")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_wire_value<T, F>(
|
||||||
|
output: &mut std::vec::Vec<u8>,
|
||||||
|
key: &str,
|
||||||
|
field: &crate::RawTransactionWireField<T>,
|
||||||
|
mut append_value: F,
|
||||||
|
) -> ksp_core_lib::Result<()>
|
||||||
|
where
|
||||||
|
F: FnMut(&mut std::vec::Vec<u8>, &T) -> ksp_core_lib::Result<()>,
|
||||||
|
{
|
||||||
|
return match field {
|
||||||
|
crate::RawTransactionWireField::Omitted => std::result::Result::Ok(()),
|
||||||
|
crate::RawTransactionWireField::Null => {
|
||||||
|
output.push(b',');
|
||||||
|
let key_result = append_json_string(output, key);
|
||||||
|
if let std::result::Result::Err(error) = key_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(b":null");
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
crate::RawTransactionWireField::Value(value) => {
|
||||||
|
output.push(b',');
|
||||||
|
let key_result = append_json_string(output, key);
|
||||||
|
if let std::result::Result::Err(error) = key_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.push(b':');
|
||||||
|
append_value(output, value)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonical_payload_bytes(material: &crate::RawTransactionMaterial) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||||
|
let mut bytes = std::vec::Vec::new();
|
||||||
|
bytes.extend_from_slice(b"{\"transaction\":[");
|
||||||
|
let transaction_result = append_json_string(&mut bytes, material.transaction_data.as_str());
|
||||||
|
if let std::result::Result::Err(error) = transaction_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
bytes.push(b',');
|
||||||
|
let encoding_result = append_json_string(&mut bytes, "base64");
|
||||||
|
if let std::result::Result::Err(error) = encoding_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
bytes.push(b']');
|
||||||
|
let meta_result = append_wire_value(&mut bytes, "meta", &material.meta, |output, value| return append_canonical_json(output, value));
|
||||||
|
if let std::result::Result::Err(error) = meta_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let version_result = append_wire_value(&mut bytes, "version", &material.version, |output, value| {
|
||||||
|
return match value {
|
||||||
|
crate::RawTransactionVersion::Legacy => append_json_string(output, "legacy"),
|
||||||
|
crate::RawTransactionVersion::Number(number) => {
|
||||||
|
output.extend_from_slice(number.to_string().as_bytes());
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if let std::result::Result::Err(error) = version_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let transaction_index_result = append_wire_value(&mut bytes, "transactionIndex", &material.transaction_index, |output, value| {
|
||||||
|
output.extend_from_slice(value.to_string().as_bytes());
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
});
|
||||||
|
if let std::result::Result::Err(error) = transaction_index_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
bytes.push(b'}');
|
||||||
|
return std::result::Result::Ok(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_block_time(value: std::option::Option<i64>) -> ksp_core_lib::Result<std::option::Option<ksp_store_api::RawTimestamp>> {
|
||||||
|
let seconds = match value {
|
||||||
|
std::option::Option::Some(seconds) => seconds,
|
||||||
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||||
|
};
|
||||||
|
let seconds = match u64::try_from(seconds) {
|
||||||
|
std::result::Result::Ok(seconds) => seconds,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(crate::material_error("block_time")),
|
||||||
|
};
|
||||||
|
let millis = match seconds.checked_mul(1_000) {
|
||||||
|
std::option::Option::Some(millis) => millis,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::material_error("block_time")),
|
||||||
|
};
|
||||||
|
let timestamp = ksp_store_api::RawTimestamp::from_unix_millis(millis);
|
||||||
|
return match timestamp {
|
||||||
|
std::result::Result::Ok(timestamp) => std::result::Result::Ok(std::option::Option::Some(timestamp)),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(crate::material_error("block_time")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wire_field_state<T>(field: &crate::RawTransactionWireField<T>) -> &'static str {
|
||||||
|
return match field {
|
||||||
|
crate::RawTransactionWireField::Omitted => "omitted",
|
||||||
|
crate::RawTransactionWireField::Null => "null",
|
||||||
|
crate::RawTransactionWireField::Value(_) => "value",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/canonical.rs"]
|
||||||
|
mod tests;
|
||||||
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal file
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/error.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
|
||||||
|
pub const ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID: ksp_core_lib::ErrorCode =
|
||||||
|
ksp_core_lib::ErrorCode::new("raw_transaction", "canonicalization_invalid");
|
||||||
|
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||||
|
pub const ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "material_invalid");
|
||||||
|
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
|
||||||
|
pub const ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "signature_invalid");
|
||||||
|
|
||||||
|
/// Creates a safe canonicalization error without copying source payload material.
|
||||||
|
pub(crate) fn canonicalization_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID, "invalid canonical RAW transaction representation")
|
||||||
|
.with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a safe source-material error without copying source payload material.
|
||||||
|
pub(crate) fn material_error(field: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID, "invalid source-neutral RAW transaction material")
|
||||||
|
.with_context("field", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a safe signature error without copying source signature or transaction material.
|
||||||
|
pub(crate) fn signature_error() -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID, "invalid canonical Solana transaction signature");
|
||||||
|
}
|
||||||
75
crates/ksp-raw-transaction-lib/src/lib.rs
Normal file
75
crates/ksp-raw-transaction-lib/src/lib.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
//! Source-neutral canonicalization of complete Solana transactions into KSP RAW transaction v1.
|
||||||
|
//!
|
||||||
|
//! This lower layer owns the frozen RAW transaction format, textual signature parsing, canonical
|
||||||
|
//! JSON construction, content hashing and assembly of one canonical transaction with one
|
||||||
|
//! producer-owned observation. It owns no Transport, Config, Job, Worker, async runtime or Store
|
||||||
|
//! backend behavior.
|
||||||
|
|
||||||
|
mod acquisition;
|
||||||
|
mod canonical;
|
||||||
|
mod error;
|
||||||
|
mod signature;
|
||||||
|
mod wire;
|
||||||
|
|
||||||
|
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||||
|
pub use self::acquisition::RawTransactionAcquisition;
|
||||||
|
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
|
||||||
|
pub use self::acquisition::assemble_raw_transaction_acquisition;
|
||||||
|
/// KSP-owned source-independent RAW transaction format identifier.
|
||||||
|
pub use self::canonical::RAW_TRANSACTION_FORMAT_ID;
|
||||||
|
/// Frozen KSP-owned RAW transaction format version.
|
||||||
|
pub use self::canonical::RAW_TRANSACTION_FORMAT_VERSION;
|
||||||
|
/// Complete source-neutral material required to construct one canonical RAW transaction.
|
||||||
|
pub use self::canonical::RawTransactionMaterial;
|
||||||
|
/// Canonical Solana transaction version admitted by RAW transaction format v1.
|
||||||
|
pub use self::canonical::RawTransactionVersion;
|
||||||
|
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
|
||||||
|
pub use self::canonical::RawTransactionWireField;
|
||||||
|
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
|
||||||
|
pub use self::canonical::canonicalize_raw_transaction;
|
||||||
|
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
|
||||||
|
pub use self::error::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID;
|
||||||
|
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||||
|
pub use self::error::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID;
|
||||||
|
/// Error code used when Solana transaction signature material cannot resolve to exactly 64 canonical bytes.
|
||||||
|
pub use self::error::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID;
|
||||||
|
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||||
|
pub use self::signature::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||||
|
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||||
|
pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||||
|
/// Extracts the first canonical 64-byte Solana signature from one complete Base64 transaction wire.
|
||||||
|
pub use self::signature::extract_raw_transaction_signature_from_binary_base64;
|
||||||
|
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||||
|
pub use self::signature::parse_raw_transaction_signature;
|
||||||
|
/// One source-neutral v0 address-table lookup.
|
||||||
|
pub use self::wire::RawSolanaAddressTableLookup;
|
||||||
|
/// One source-neutral compiled Solana instruction.
|
||||||
|
pub use self::wire::RawSolanaCompiledInstruction;
|
||||||
|
/// Source-neutral Solana message header.
|
||||||
|
pub use self::wire::RawSolanaMessageHeader;
|
||||||
|
/// Source-neutral Solana transaction message version.
|
||||||
|
pub use self::wire::RawSolanaMessageVersion;
|
||||||
|
/// Optional source-neutral Transaction V1 inline configuration.
|
||||||
|
pub use self::wire::RawSolanaTransactionConfig;
|
||||||
|
/// Complete source-neutral Solana transaction message.
|
||||||
|
pub use self::wire::RawSolanaTransactionMessage;
|
||||||
|
/// Complete source-neutral Solana transaction wire material.
|
||||||
|
pub use self::wire::RawSolanaTransactionWire;
|
||||||
|
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||||
|
pub use self::wire::serialize_solana_transaction_wire;
|
||||||
|
/// Serializes one source-neutral Solana transaction to canonical padded standard Base64.
|
||||||
|
pub use self::wire::serialize_solana_transaction_wire_base64;
|
||||||
|
|
||||||
|
/// Creates a safe canonicalization error without copying source payload material.
|
||||||
|
pub(crate) use self::error::canonicalization_error;
|
||||||
|
/// Creates a safe source-material error without copying source payload material.
|
||||||
|
pub(crate) use self::error::material_error;
|
||||||
|
/// Creates a safe signature error without copying textual signature material.
|
||||||
|
pub(crate) use self::error::signature_error;
|
||||||
290
crates/ksp-raw-transaction-lib/src/signature.rs
Normal file
290
crates/ksp-raw-transaction-lib/src/signature.rs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||||
|
pub const MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 88;
|
||||||
|
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||||
|
pub const MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 64;
|
||||||
|
|
||||||
|
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||||
|
pub fn parse_raw_transaction_signature(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let text = value.as_bytes();
|
||||||
|
if text.len() < crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES || text.len() > crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
crate::signature_error()
|
||||||
|
.with_context("actual_len", text.len().to_string())
|
||||||
|
.with_context("minimum_len", crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string())
|
||||||
|
.with_context("maximum_len", crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut decoded = [0_u8; 64];
|
||||||
|
let mut leading_zeroes = 0_usize;
|
||||||
|
for byte in text {
|
||||||
|
if *byte != b'1' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
leading_zeroes += 1;
|
||||||
|
}
|
||||||
|
for byte in text {
|
||||||
|
let digit = match base58_digit(*byte) {
|
||||||
|
std::option::Option::Some(digit) => digit,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut carry = u32::from(digit);
|
||||||
|
for output in decoded.iter_mut().rev() {
|
||||||
|
let expanded = (u32::from(*output) * 58) + carry;
|
||||||
|
*output = (expanded & 0xff) as u8;
|
||||||
|
carry = expanded >> 8;
|
||||||
|
}
|
||||||
|
if carry != 0 {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let significant_len = match decoded.iter().position(|byte| return *byte != 0) {
|
||||||
|
std::option::Option::Some(index) => decoded.len() - index,
|
||||||
|
std::option::Option::None => 0,
|
||||||
|
};
|
||||||
|
if leading_zeroes + significant_len != decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(decoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the first canonical 64-byte Solana signature from one complete Base64-encoded transaction wire.
|
||||||
|
///
|
||||||
|
/// The compact signature-count prefix must use its canonical short-vector representation, contain at least one signature,
|
||||||
|
/// and the decoded transaction must retain message bytes after the declared signature array.
|
||||||
|
pub fn extract_raw_transaction_signature_from_binary_base64(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
if value.is_empty() || value.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let decoded = base64::engine::general_purpose::STANDARD.decode(value);
|
||||||
|
let decoded = match decoded {
|
||||||
|
std::result::Result::Ok(decoded) => decoded,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if base64::engine::general_purpose::STANDARD.encode(decoded.as_slice()) != value {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
if decoded.first() == std::option::Option::Some(&0x81) {
|
||||||
|
return extract_v1_signature(decoded.as_slice());
|
||||||
|
}
|
||||||
|
return extract_legacy_or_v0_signature(decoded.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_legacy_or_v0_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let prefix = decode_signature_count(decoded);
|
||||||
|
let (signature_count, prefix_len) = match prefix {
|
||||||
|
std::result::Result::Ok(prefix) => prefix,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if signature_count == 0 {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let signatures_len = match signature_count.checked_mul(64) {
|
||||||
|
std::option::Option::Some(length) => length,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let message_offset = match prefix_len.checked_add(signatures_len) {
|
||||||
|
std::option::Option::Some(offset) => offset,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if message_offset >= decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
return copy_signature(decoded, prefix_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_v1_signature(decoded: &[u8]) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
if decoded.len() > 4_096 || decoded.len() < 42 {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let required_signatures = match decoded.get(1) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let readonly_signed = match decoded.get(2) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let readonly_unsigned = match decoded.get(3) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if required_signatures == 0 || required_signatures > 12 || readonly_signed >= required_signatures {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let mask_bytes = match decoded.get(4..8) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mask = u32::from_le_bytes([mask_bytes[0], mask_bytes[1], mask_bytes[2], mask_bytes[3]]);
|
||||||
|
if mask & !0x1f != 0 || matches!(mask & 0b11, 1 | 2) {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let instruction_count = match decoded.get(40) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let address_count = match decoded.get(41) {
|
||||||
|
std::option::Option::Some(value) => usize::from(*value),
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if instruction_count > 64 || address_count > 64 || address_count < required_signatures + readonly_unsigned {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let addresses_len = match address_count.checked_mul(32) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut offset = match 42_usize.checked_add(addresses_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let config_len = config_value_length(mask);
|
||||||
|
offset = match offset.checked_add(config_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let headers_len = match instruction_count.checked_mul(4) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let headers_end = match offset.checked_add(headers_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if headers_end > decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
let mut payload_len = 0_usize;
|
||||||
|
for index in 0..instruction_count {
|
||||||
|
let header_offset = offset + (index * 4);
|
||||||
|
let program_index = usize::from(decoded[header_offset]);
|
||||||
|
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||||
|
let data_len = u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]);
|
||||||
|
if program_index >= address_count {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
payload_len = match payload_len.checked_add(account_index_count).and_then(|value| return value.checked_add(usize::from(data_len))) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let payload_end = match headers_end.checked_add(payload_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let payload = match decoded.get(headers_end..payload_end) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut payload_offset = 0_usize;
|
||||||
|
for index in 0..instruction_count {
|
||||||
|
let header_offset = offset + (index * 4);
|
||||||
|
let account_index_count = usize::from(decoded[header_offset + 1]);
|
||||||
|
let data_len = usize::from(u16::from_le_bytes([decoded[header_offset + 2], decoded[header_offset + 3]]));
|
||||||
|
let account_end = match payload_offset.checked_add(account_index_count) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let account_indexes = match payload.get(payload_offset..account_end) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if account_indexes.iter().any(|value| return usize::from(*value) >= address_count) {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
payload_offset = match account_end.checked_add(data_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let signatures_len = match required_signatures.checked_mul(64) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let expected_end = match payload_end.checked_add(signatures_len) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
if expected_end != decoded.len() {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
return copy_signature(decoded, payload_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_value_length(mask: u32) -> usize {
|
||||||
|
let mut length = 0_usize;
|
||||||
|
if mask & 0b11 == 0b11 {
|
||||||
|
length += 8;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 2) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 3) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
if mask & (1_u32 << 4) != 0 {
|
||||||
|
length += 4;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_signature(decoded: &[u8], offset: usize) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let first_end = match offset.checked_add(64) {
|
||||||
|
std::option::Option::Some(end) => end,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let first = match decoded.get(offset..first_end) {
|
||||||
|
std::option::Option::Some(first) => first,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let mut signature = [0_u8; 64];
|
||||||
|
signature.copy_from_slice(first);
|
||||||
|
return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_signature_count(bytes: &[u8]) -> ksp_core_lib::Result<(usize, usize)> {
|
||||||
|
let mut value = 0_usize;
|
||||||
|
for index in 0..3_usize {
|
||||||
|
let byte = match bytes.get(index) {
|
||||||
|
std::option::Option::Some(byte) => *byte,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||||
|
};
|
||||||
|
let payload = usize::from(byte & 0x7f);
|
||||||
|
let shift = index * 7;
|
||||||
|
value |= payload << shift;
|
||||||
|
if byte & 0x80 == 0 {
|
||||||
|
if index != 0 {
|
||||||
|
let minimum = 1_usize << shift;
|
||||||
|
if value < minimum {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value > usize::from(u16::MAX) {
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok((value, index + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(crate::signature_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base58_digit(byte: u8) -> std::option::Option<u8> {
|
||||||
|
return match byte {
|
||||||
|
b'1'..=b'9' => std::option::Option::Some(byte - b'1'),
|
||||||
|
b'A'..=b'H' => std::option::Option::Some((byte - b'A') + 9),
|
||||||
|
b'J'..=b'N' => std::option::Option::Some((byte - b'J') + 17),
|
||||||
|
b'P'..=b'Z' => std::option::Option::Some((byte - b'P') + 22),
|
||||||
|
b'a'..=b'k' => std::option::Option::Some((byte - b'a') + 33),
|
||||||
|
b'm'..=b'z' => std::option::Option::Some((byte - b'm') + 44),
|
||||||
|
_ => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/signature.rs"]
|
||||||
|
mod tests;
|
||||||
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
545
crates/ksp-raw-transaction-lib/src/wire.rs
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/src/wire.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const MAX_LEGACY_SHORT_VECTOR_VALUE: usize = u16::MAX as usize;
|
||||||
|
const MAX_V1_ADDRESS_COUNT: usize = 64;
|
||||||
|
const MAX_V1_INSTRUCTION_COUNT: usize = 64;
|
||||||
|
const MAX_V1_SIGNATURE_COUNT: usize = 12;
|
||||||
|
const MAX_V1_TRANSACTION_BYTES: usize = 4_096;
|
||||||
|
|
||||||
|
/// Source-neutral Solana transaction message version used by the exact wire serializer.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RawSolanaMessageVersion {
|
||||||
|
/// Legacy Solana message without a version prefix.
|
||||||
|
Legacy,
|
||||||
|
/// Versioned transaction message v0, prefixed with `0x80`.
|
||||||
|
V0,
|
||||||
|
/// Transaction V1 / SIMD-0385 message, prefixed with `0x81`.
|
||||||
|
V1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exact three-byte Solana message header.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaMessageHeader {
|
||||||
|
num_required_signatures: u8,
|
||||||
|
num_readonly_signed_accounts: u8,
|
||||||
|
num_readonly_unsigned_accounts: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaMessageHeader {
|
||||||
|
/// Creates an exact Solana message header.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(num_required_signatures: u8, num_readonly_signed_accounts: u8, num_readonly_unsigned_accounts: u8) -> Self {
|
||||||
|
return Self { num_required_signatures, num_readonly_signed_accounts, num_readonly_unsigned_accounts };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the required signature count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_required_signatures(self) -> u8 {
|
||||||
|
return self.num_required_signatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the readonly signed-account count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_readonly_signed_accounts(self) -> u8 {
|
||||||
|
return self.num_readonly_signed_accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the readonly unsigned-account count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn num_readonly_unsigned_accounts(self) -> u8 {
|
||||||
|
return self.num_readonly_unsigned_accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source-neutral compiled Solana instruction.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaCompiledInstruction {
|
||||||
|
program_id_index: u8,
|
||||||
|
accounts: std::vec::Vec<u8>,
|
||||||
|
data: std::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaCompiledInstruction {
|
||||||
|
/// Creates one compiled instruction while preserving exact ordered account indexes and data bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(program_id_index: u8, accounts: std::vec::Vec<u8>, data: std::vec::Vec<u8>) -> Self {
|
||||||
|
return Self { program_id_index, accounts, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the program account index.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn program_id_index(&self) -> u8 {
|
||||||
|
return self.program_id_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered account indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accounts(&self) -> &[u8] {
|
||||||
|
return self.accounts.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns exact instruction data bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn data(&self) -> &[u8] {
|
||||||
|
return self.data.as_slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaCompiledInstruction {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaCompiledInstruction")
|
||||||
|
.field("program_id_index", &self.program_id_index)
|
||||||
|
.field("account_index_count", &self.accounts.len())
|
||||||
|
.field("data_length", &self.data.len())
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source-neutral v0 address-table lookup.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaAddressTableLookup {
|
||||||
|
account_key: [u8; 32],
|
||||||
|
writable_indexes: std::vec::Vec<u8>,
|
||||||
|
readonly_indexes: std::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaAddressTableLookup {
|
||||||
|
/// Creates one exact v0 address-table lookup.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(account_key: [u8; 32], writable_indexes: std::vec::Vec<u8>, readonly_indexes: std::vec::Vec<u8>) -> Self {
|
||||||
|
return Self { account_key, writable_indexes, readonly_indexes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the lookup-table account key bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn account_key(&self) -> &[u8; 32] {
|
||||||
|
return &self.account_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered writable lookup indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn writable_indexes(&self) -> &[u8] {
|
||||||
|
return self.writable_indexes.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered readonly lookup indexes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn readonly_indexes(&self) -> &[u8] {
|
||||||
|
return self.readonly_indexes.as_slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaAddressTableLookup {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaAddressTableLookup")
|
||||||
|
.field("writable_index_count", &self.writable_indexes.len())
|
||||||
|
.field("readonly_index_count", &self.readonly_indexes.len())
|
||||||
|
.finish_non_exhaustive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional inline budget configuration carried by Solana Transaction V1.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionConfig {
|
||||||
|
priority_fee: std::option::Option<u64>,
|
||||||
|
compute_unit_limit: std::option::Option<u32>,
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||||
|
heap_size: std::option::Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionConfig {
|
||||||
|
/// Creates an explicit Transaction V1 configuration, including the meaningful all-`None` case.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
priority_fee: std::option::Option<u64>,
|
||||||
|
compute_unit_limit: std::option::Option<u32>,
|
||||||
|
loaded_accounts_data_size_limit: std::option::Option<u32>,
|
||||||
|
heap_size: std::option::Option<u32>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { priority_fee, compute_unit_limit, loaded_accounts_data_size_limit, heap_size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional priority fee.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn priority_fee(self) -> std::option::Option<u64> {
|
||||||
|
return self.priority_fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional compute-unit limit.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn compute_unit_limit(self) -> std::option::Option<u32> {
|
||||||
|
return self.compute_unit_limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional loaded-account-data-size limit.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn loaded_accounts_data_size_limit(self) -> std::option::Option<u32> {
|
||||||
|
return self.loaded_accounts_data_size_limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional heap-size override.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn heap_size(self) -> std::option::Option<u32> {
|
||||||
|
return self.heap_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete source-neutral Solana transaction message required for exact wire serialization.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionMessage {
|
||||||
|
version: crate::RawSolanaMessageVersion,
|
||||||
|
header: crate::RawSolanaMessageHeader,
|
||||||
|
account_keys: std::vec::Vec<[u8; 32]>,
|
||||||
|
recent_blockhash: [u8; 32],
|
||||||
|
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||||
|
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||||
|
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionMessage {
|
||||||
|
/// Creates one exact source-neutral message.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
version: crate::RawSolanaMessageVersion,
|
||||||
|
header: crate::RawSolanaMessageHeader,
|
||||||
|
account_keys: std::vec::Vec<[u8; 32]>,
|
||||||
|
recent_blockhash: [u8; 32],
|
||||||
|
instructions: std::vec::Vec<crate::RawSolanaCompiledInstruction>,
|
||||||
|
address_table_lookups: std::vec::Vec<crate::RawSolanaAddressTableLookup>,
|
||||||
|
config: std::option::Option<crate::RawSolanaTransactionConfig>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { version, header, account_keys, recent_blockhash, instructions, address_table_lookups, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the message version.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn version(&self) -> crate::RawSolanaMessageVersion {
|
||||||
|
return self.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the message header.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn header(&self) -> crate::RawSolanaMessageHeader {
|
||||||
|
return self.header;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered static account key bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_keys(&self) -> &[[u8; 32]] {
|
||||||
|
return self.account_keys.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the recent blockhash bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn recent_blockhash(&self) -> &[u8; 32] {
|
||||||
|
return &self.recent_blockhash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered compiled instructions.
|
||||||
|
#[must_use]
|
||||||
|
pub fn instructions(&self) -> &[crate::RawSolanaCompiledInstruction] {
|
||||||
|
return self.instructions.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered v0 address-table lookups.
|
||||||
|
#[must_use]
|
||||||
|
pub fn address_table_lookups(&self) -> &[crate::RawSolanaAddressTableLookup] {
|
||||||
|
return self.address_table_lookups.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns optional V1 inline configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn config(&self) -> std::option::Option<crate::RawSolanaTransactionConfig> {
|
||||||
|
return self.config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaTransactionMessage {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("RawSolanaTransactionMessage")
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("header", &self.header)
|
||||||
|
.field("account_key_count", &self.account_keys.len())
|
||||||
|
.field("instruction_count", &self.instructions.len())
|
||||||
|
.field("address_table_lookup_count", &self.address_table_lookups.len())
|
||||||
|
.field("has_config", &self.config.is_some())
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete source-neutral Solana transaction wire material.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct RawSolanaTransactionWire {
|
||||||
|
signatures: std::vec::Vec<[u8; 64]>,
|
||||||
|
message: crate::RawSolanaTransactionMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::RawSolanaTransactionWire {
|
||||||
|
/// Creates one complete transaction wire.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(signatures: std::vec::Vec<[u8; 64]>, message: crate::RawSolanaTransactionMessage) -> Self {
|
||||||
|
return Self { signatures, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ordered signature bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signatures(&self) -> &[[u8; 64]] {
|
||||||
|
return self.signatures.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the exact source-neutral message.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn message(&self) -> &crate::RawSolanaTransactionMessage {
|
||||||
|
return &self.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::RawSolanaTransactionWire {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("RawSolanaTransactionWire").field("signature_count", &self.signatures.len()).field("message", &self.message).finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes one source-neutral Solana transaction to exact canonical wire bytes.
|
||||||
|
pub fn serialize_solana_transaction_wire(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||||
|
if let std::result::Result::Err(error) = validate_transaction(transaction) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let mut output = std::vec::Vec::new();
|
||||||
|
match transaction.message().version() {
|
||||||
|
crate::RawSolanaMessageVersion::Legacy | crate::RawSolanaMessageVersion::V0 => {
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(transaction.signatures().len(), &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for signature in transaction.signatures() {
|
||||||
|
output.extend_from_slice(signature);
|
||||||
|
}
|
||||||
|
if transaction.message().version() == crate::RawSolanaMessageVersion::V0 {
|
||||||
|
output.push(0x80);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = encode_legacy_or_v0_message(transaction.message(), &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V1 => {
|
||||||
|
if let std::result::Result::Err(error) = encode_v1_message(transaction, &mut output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if output.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||||
|
}
|
||||||
|
if transaction.message().version() == crate::RawSolanaMessageVersion::V1 && output.len() > MAX_V1_TRANSACTION_BYTES {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire.bytes").with_context("actual_len", output.len().to_string()));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes one source-neutral Solana transaction and returns canonical padded standard Base64.
|
||||||
|
pub fn serialize_solana_transaction_wire_base64(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<std::string::String> {
|
||||||
|
let bytes = match crate::serialize_solana_transaction_wire(transaction) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(base64::engine::general_purpose::STANDARD.encode(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_transaction(transaction: &crate::RawSolanaTransactionWire) -> ksp_core_lib::Result<()> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let required_signatures = usize::from(message.header().num_required_signatures());
|
||||||
|
if required_signatures == 0 || transaction.signatures().len() != required_signatures {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if usize::from(message.header().num_readonly_signed_accounts()) >= required_signatures {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if message.account_keys().len() < required_signatures + usize::from(message.header().num_readonly_unsigned_accounts()) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
let loaded_count = message.address_table_lookups().iter().fold(0_usize, |count, lookup| {
|
||||||
|
return count.saturating_add(lookup.writable_indexes().len()).saturating_add(lookup.readonly_indexes().len());
|
||||||
|
});
|
||||||
|
let account_count = message.account_keys().len().saturating_add(loaded_count);
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
if usize::from(instruction.program_id_index()) >= account_count {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
for account_index in instruction.accounts() {
|
||||||
|
if usize::from(*account_index) >= account_count {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match message.version() {
|
||||||
|
crate::RawSolanaMessageVersion::Legacy => {
|
||||||
|
if !message.address_table_lookups().is_empty() || message.config().is_some() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V0 => {
|
||||||
|
if message.config().is_some() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::RawSolanaMessageVersion::V1 => {
|
||||||
|
if message.config().is_none() || !message.address_table_lookups().is_empty() {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
if transaction.signatures().len() > MAX_V1_SIGNATURE_COUNT
|
||||||
|
|| message.account_keys().len() > MAX_V1_ADDRESS_COUNT
|
||||||
|
|| message.instructions().len() > MAX_V1_INSTRUCTION_COUNT
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
for (index, account) in message.account_keys().iter().enumerate() {
|
||||||
|
if message.account_keys()[..index].contains(account) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
if instruction.accounts().len() > usize::from(u8::MAX) || instruction.data().len() > usize::from(u16::MAX) {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(heap_size) = message.config().and_then(crate::RawSolanaTransactionConfig::heap_size)
|
||||||
|
&& (!(32_768..=262_144).contains(&heap_size) || heap_size % 1_024 != 0)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_legacy_or_v0_message(message: &crate::RawSolanaTransactionMessage, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
encode_header(message.header(), output);
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.account_keys().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for account_key in message.account_keys() {
|
||||||
|
output.extend_from_slice(account_key);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(message.recent_blockhash());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.instructions().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.push(instruction.program_id_index());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(instruction.accounts().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(instruction.accounts());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(instruction.data().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(instruction.data());
|
||||||
|
}
|
||||||
|
if message.version() == crate::RawSolanaMessageVersion::V0 {
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(message.address_table_lookups().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
for lookup in message.address_table_lookups() {
|
||||||
|
output.extend_from_slice(lookup.account_key());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(lookup.writable_indexes().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(lookup.writable_indexes());
|
||||||
|
if let std::result::Result::Err(error) = encode_short_vec(lookup.readonly_indexes().len(), output) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
output.extend_from_slice(lookup.readonly_indexes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_v1_message(transaction: &crate::RawSolanaTransactionWire, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
let message = transaction.message();
|
||||||
|
let config = match message.config() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::material_error("wire")),
|
||||||
|
};
|
||||||
|
output.push(0x81);
|
||||||
|
encode_header(message.header(), output);
|
||||||
|
let mut mask = 0_u32;
|
||||||
|
if config.priority_fee().is_some() {
|
||||||
|
mask |= 0b11;
|
||||||
|
}
|
||||||
|
if config.compute_unit_limit().is_some() {
|
||||||
|
mask |= 1_u32 << 2;
|
||||||
|
}
|
||||||
|
if config.loaded_accounts_data_size_limit().is_some() {
|
||||||
|
mask |= 1_u32 << 3;
|
||||||
|
}
|
||||||
|
if config.heap_size().is_some() {
|
||||||
|
mask |= 1_u32 << 4;
|
||||||
|
}
|
||||||
|
output.extend_from_slice(&mask.to_le_bytes());
|
||||||
|
output.extend_from_slice(message.recent_blockhash());
|
||||||
|
output.push(message.instructions().len() as u8);
|
||||||
|
output.push(message.account_keys().len() as u8);
|
||||||
|
for account_key in message.account_keys() {
|
||||||
|
output.extend_from_slice(account_key);
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.priority_fee() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.compute_unit_limit() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.loaded_accounts_data_size_limit() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = config.heap_size() {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.push(instruction.program_id_index());
|
||||||
|
output.push(instruction.accounts().len() as u8);
|
||||||
|
output.extend_from_slice(&(instruction.data().len() as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
for instruction in message.instructions() {
|
||||||
|
output.extend_from_slice(instruction.accounts());
|
||||||
|
output.extend_from_slice(instruction.data());
|
||||||
|
}
|
||||||
|
for signature in transaction.signatures() {
|
||||||
|
output.extend_from_slice(signature);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_header(header: crate::RawSolanaMessageHeader, output: &mut std::vec::Vec<u8>) {
|
||||||
|
output.push(header.num_required_signatures());
|
||||||
|
output.push(header.num_readonly_signed_accounts());
|
||||||
|
output.push(header.num_readonly_unsigned_accounts());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_short_vec(value: usize, output: &mut std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||||
|
if value > MAX_LEGACY_SHORT_VECTOR_VALUE {
|
||||||
|
return std::result::Result::Err(crate::material_error("wire"));
|
||||||
|
}
|
||||||
|
let mut remaining = value;
|
||||||
|
loop {
|
||||||
|
let mut byte = (remaining & 0x7f) as u8;
|
||||||
|
remaining >>= 7;
|
||||||
|
if remaining != 0 {
|
||||||
|
byte |= 0x80;
|
||||||
|
}
|
||||||
|
output.push(byte);
|
||||||
|
if remaining == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/wire.rs"]
|
||||||
|
mod tests;
|
||||||
55
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
Normal file
55
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Dependency-boundary canaries for the common RAW Transaction foundation.
|
||||||
|
|
||||||
|
fn manifest() -> std::string::String {
|
||||||
|
return std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn production_sources() -> std::string::String {
|
||||||
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||||
|
let mut combined = std::string::String::new();
|
||||||
|
for name in ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"] {
|
||||||
|
let source = std::fs::read_to_string(root.join(name)).unwrap_or_default();
|
||||||
|
combined.push_str(source.as_str());
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_manifest_adds_only_base64_to_the_exact_common_dependency_surface() {
|
||||||
|
let manifest = manifest();
|
||||||
|
assert!(manifest.contains("base64.workspace = true"));
|
||||||
|
assert!(manifest.contains("ksp-core-lib = { path = \"../ksp-core-lib\" }"));
|
||||||
|
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
||||||
|
assert!(manifest.contains("serde_json = { workspace = true }"));
|
||||||
|
assert!(manifest.contains("sha2 = { workspace = true }"));
|
||||||
|
for forbidden in
|
||||||
|
["ksp-store-lib", "ksp-onchain-transport-lib", "ksp-config-lib", "ksp-job-api", "ksp-job-backfill-lib", "ksp-worker-api", "tokio", "futures"]
|
||||||
|
{
|
||||||
|
assert!(!manifest.contains(forbidden));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_production_sources_keep_transport_runtime_job_worker_and_backend_out() {
|
||||||
|
let sources = production_sources();
|
||||||
|
for forbidden in [
|
||||||
|
"ksp_store_lib",
|
||||||
|
"ksp_onchain_transport_lib",
|
||||||
|
"ksp_config_lib",
|
||||||
|
"ksp_job_api",
|
||||||
|
"ksp_job_backfill_lib",
|
||||||
|
"ksp_worker_api",
|
||||||
|
"ksp_store_postgres_lib",
|
||||||
|
"tokio::",
|
||||||
|
"futures::",
|
||||||
|
"reqwest::",
|
||||||
|
"tonic::",
|
||||||
|
] {
|
||||||
|
assert!(!sources.contains(forbidden));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
122
crates/ksp-raw-transaction-lib/tests/public_api.rs
Normal file
122
crates/ksp-raw-transaction-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
//! Integration canaries for the public common RAW Transaction crate-root surface.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_common_raw_contract_is_consumable_from_crate_root() {
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_ID, "ksp.solana.raw_transaction");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::RAW_TRANSACTION_FORMAT_VERSION, 1);
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES, 64);
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES, 88);
|
||||||
|
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature("1".repeat(64).as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
5,
|
||||||
|
std::option::Option::None,
|
||||||
|
"AQID",
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(ksp_raw_transaction_lib::RawTransactionVersion::Legacy),
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
);
|
||||||
|
let transaction = ksp_raw_transaction_lib::canonicalize_raw_transaction(material);
|
||||||
|
assert!(transaction.is_ok());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_acquisition_assembly_accepts_store_api_producer_owned_metadata() {
|
||||||
|
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature("1".repeat(64).as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
5,
|
||||||
|
std::option::Option::None,
|
||||||
|
"AQID",
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
);
|
||||||
|
let transaction = match ksp_raw_transaction_lib::canonicalize_raw_transaction(material) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let provider = match ksp_store_api::RawProvenanceCode::new("fixture") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let protocol = match ksp_store_api::RawProvenanceCode::new("fixture.protocol") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let method = match ksp_store_api::RawProvenanceCode::new("fixture.method") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let provenance = ksp_store_api::RawAcquisitionProvenance::new(provider, protocol, method, ksp_store_api::RawAcquisitionOrigin::Import, received_at);
|
||||||
|
let acquisition = ksp_raw_transaction_lib::assemble_raw_transaction_acquisition(transaction, ksp_store_api::RawObservationKey::new([9_u8; 32]), provenance);
|
||||||
|
assert_eq!(acquisition.transaction().reference(), acquisition.observation().transaction());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_embedded_signature_and_block_material_contract_are_available_from_crate_root() {
|
||||||
|
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
let signature = ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||||
|
assert!(signature.is_ok());
|
||||||
|
let network = ksp_store_api::RawNetworkId::new("devnet");
|
||||||
|
assert!(network.is_ok());
|
||||||
|
if let std::result::Result::Ok(network) = network {
|
||||||
|
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||||
|
network,
|
||||||
|
1,
|
||||||
|
std::option::Option::None,
|
||||||
|
encoded,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||||
|
ksp_raw_transaction_lib::RawTransactionWireField::Value(0),
|
||||||
|
);
|
||||||
|
assert!(material.is_ok());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_source_neutral_wire_contract_is_available_from_crate_root() {
|
||||||
|
let message = ksp_raw_transaction_lib::RawSolanaTransactionMessage::new(
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageVersion::V1,
|
||||||
|
ksp_raw_transaction_lib::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[1_u8; 32]],
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![ksp_raw_transaction_lib::RawSolanaCompiledInstruction::new(0, vec![0], vec![3])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(ksp_raw_transaction_lib::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = ksp_raw_transaction_lib::RawSolanaTransactionWire::new(vec![[4_u8; 64]], message);
|
||||||
|
let encoded = ksp_raw_transaction_lib::serialize_solana_transaction_wire_base64(&wire);
|
||||||
|
assert!(encoded.is_ok());
|
||||||
|
if let std::result::Result::Ok(encoded) = encoded {
|
||||||
|
assert!(encoded.starts_with("gQ"));
|
||||||
|
assert!(ksp_raw_transaction_lib::extract_raw_transaction_signature_from_binary_base64(encoded.as_str()).is_ok());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
41
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
Normal file
41
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Release-completeness canaries for the common RAW Transaction foundation.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_production_module_inventory_is_exact() {
|
||||||
|
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||||
|
let entries = match std::fs::read_dir(source_root) {
|
||||||
|
std::result::Result::Ok(entries) => entries,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut names = std::vec::Vec::new();
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("rs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let std::option::Option::Some(name) = path.file_name().and_then(std::ffi::OsStr::to_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
names.push(name.to_owned());
|
||||||
|
}
|
||||||
|
names.sort();
|
||||||
|
let expected = ["acquisition.rs", "canonical.rs", "error.rs", "lib.rs", "signature.rs", "wire.rs"];
|
||||||
|
assert_eq!(names.len(), expected.len());
|
||||||
|
for (actual, expected_name) in names.iter().zip(expected.iter()) {
|
||||||
|
assert_eq!(actual.as_str(), *expected_name);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_common_surface_contains_no_worker_job_or_transport_dependency_or_public_module() {
|
||||||
|
let lib = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/lib.rs")).unwrap_or_default();
|
||||||
|
assert!(!lib.contains("pub mod "));
|
||||||
|
for forbidden in ["ksp_worker", "ksp_job", "ksp_onchain_transport", "ksp_config", "ksp_store_lib", "ksp_store_postgres"] {
|
||||||
|
assert!(!lib.contains(forbidden));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
28
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
Normal file
28
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/tests/security_hardening.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Adversarial canaries for the common RAW Transaction foundation.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_signature_errors_never_echo_hostile_input() {
|
||||||
|
let hostile = format!("{}0", "A".repeat(63));
|
||||||
|
let result = ksp_raw_transaction_lib::parse_raw_transaction_signature(hostile.as_str());
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
|
||||||
|
assert!(!std::format!("{error:?}").contains(hostile.as_str()));
|
||||||
|
assert!(!error.to_string().contains(hostile.as_str()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_public_error_codes_are_stable_and_domain_scoped() {
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID.domain(), "raw_transaction");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID.code(), "canonicalization_invalid");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID.domain(), "raw_transaction");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID.code(), "material_invalid");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID.domain(), "raw_transaction");
|
||||||
|
assert_eq!(ksp_raw_transaction_lib::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID.code(), "signature_invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
73
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
Normal file
73
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn canonical_transaction() -> std::option::Option<ksp_store_api::RawTransaction> {
|
||||||
|
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let signature = match crate::parse_raw_transaction_signature("1".repeat(64).as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let material = crate::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
42,
|
||||||
|
std::option::Option::None,
|
||||||
|
"AQID",
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
);
|
||||||
|
return match crate::canonicalize_raw_transaction(material) {
|
||||||
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||||
|
std::result::Result::Err(_) => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provenance() -> std::option::Option<ksp_store_api::RawAcquisitionProvenance> {
|
||||||
|
let provider = match ksp_store_api::RawProvenanceCode::new("fixture") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let protocol = match ksp_store_api::RawProvenanceCode::new("fixture.protocol") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let method = match ksp_store_api::RawProvenanceCode::new("fixture.method") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let received_at = match ksp_store_api::RawTimestamp::from_unix_millis(1_700_000_000_000) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return std::option::Option::Some(ksp_store_api::RawAcquisitionProvenance::new(
|
||||||
|
provider,
|
||||||
|
protocol,
|
||||||
|
method,
|
||||||
|
ksp_store_api::RawAcquisitionOrigin::Import,
|
||||||
|
received_at,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_acquisition_preserves_producer_key_and_exact_transaction_reference() {
|
||||||
|
let transaction = match canonical_transaction() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let provenance = match provenance() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let key = ksp_store_api::RawObservationKey::new([7_u8; 32]);
|
||||||
|
let acquisition = crate::assemble_raw_transaction_acquisition(transaction, key, provenance);
|
||||||
|
assert_eq!(acquisition.observation().observation_key(), key);
|
||||||
|
assert_eq!(acquisition.observation().transaction(), acquisition.transaction().reference());
|
||||||
|
assert_eq!(std::format!("{acquisition:?}"), "RawTransactionAcquisition(..)");
|
||||||
|
let (transaction, observation) = acquisition.into_parts();
|
||||||
|
assert_eq!(observation.transaction(), transaction.reference());
|
||||||
|
return;
|
||||||
|
}
|
||||||
247
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
Normal file
247
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
fn network() -> std::option::Option<ksp_store_api::RawNetworkId> {
|
||||||
|
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||||
|
std::result::Result::Err(_) => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature() -> std::option::Option<ksp_store_api::RawTransactionSignature> {
|
||||||
|
let result = crate::parse_raw_transaction_signature("1".repeat(64).as_str());
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||||
|
std::result::Result::Err(_) => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn material(
|
||||||
|
block_time: std::option::Option<i64>,
|
||||||
|
transaction_data: std::string::String,
|
||||||
|
meta: crate::RawTransactionWireField<serde_json::Value>,
|
||||||
|
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
|
||||||
|
transaction_index: crate::RawTransactionWireField<u32>,
|
||||||
|
) -> std::option::Option<crate::RawTransactionMaterial> {
|
||||||
|
let network = match network() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
let signature = match signature() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::option::Option::None,
|
||||||
|
};
|
||||||
|
return std::option::Option::Some(crate::RawTransactionMaterial::binary_base64(
|
||||||
|
network,
|
||||||
|
signature,
|
||||||
|
123,
|
||||||
|
block_time,
|
||||||
|
transaction_data,
|
||||||
|
meta,
|
||||||
|
version,
|
||||||
|
transaction_index,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_canonical_raw_v1_golden_bytes_and_hash_are_exact() {
|
||||||
|
let material = match material(
|
||||||
|
std::option::Option::Some(1_700_000_000),
|
||||||
|
"AQID".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Value(serde_json::json!({"z": 1, "a": {"y": true, "x": null}})),
|
||||||
|
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
|
||||||
|
crate::RawTransactionWireField::Value(7_u32),
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let transaction = crate::canonicalize_raw_transaction(material);
|
||||||
|
assert!(transaction.is_ok());
|
||||||
|
let transaction = match transaction {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let expected = b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":{\"a\":{\"x\":null,\"y\":true},\"z\":1},\"version\":\"legacy\",\"transactionIndex\":7}";
|
||||||
|
assert_eq!(transaction.payload().bytes(), expected);
|
||||||
|
assert_eq!(transaction.payload().byte_len(), 112);
|
||||||
|
assert_eq!(transaction.payload().format_id().as_str(), crate::RAW_TRANSACTION_FORMAT_ID);
|
||||||
|
assert_eq!(transaction.payload().format_version(), crate::RAW_TRANSACTION_FORMAT_VERSION);
|
||||||
|
assert_eq!(
|
||||||
|
transaction.payload().content_hash().as_bytes(),
|
||||||
|
&[
|
||||||
|
34, 7, 146, 210, 177, 93, 38, 47, 218, 36, 44, 178, 32, 119, 78, 233, 221, 239, 254, 191, 4, 220, 250, 218, 188, 248, 239, 118, 169, 177, 167, 195
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(transaction.slot(), 123);
|
||||||
|
assert_eq!(transaction.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_700_000_000_000));
|
||||||
|
assert_eq!(transaction.reference().network().as_str(), "devnet");
|
||||||
|
assert_eq!(transaction.reference().signature().as_bytes(), &[0_u8; 64]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_wire_omitted_null_and_value_states_remain_distinct() {
|
||||||
|
let omitted = match material(
|
||||||
|
std::option::Option::None,
|
||||||
|
"AQID".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let omitted = super::canonical_payload_bytes(&omitted);
|
||||||
|
assert!(omitted.is_ok());
|
||||||
|
if let std::result::Result::Ok(omitted) = omitted {
|
||||||
|
assert_eq!(omitted, b"{\"transaction\":[\"AQID\",\"base64\"]}");
|
||||||
|
}
|
||||||
|
let nulls = match material(
|
||||||
|
std::option::Option::None,
|
||||||
|
"AQID".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Null,
|
||||||
|
crate::RawTransactionWireField::Null,
|
||||||
|
crate::RawTransactionWireField::Null,
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let nulls = super::canonical_payload_bytes(&nulls);
|
||||||
|
assert!(nulls.is_ok());
|
||||||
|
if let std::result::Result::Ok(nulls) = nulls {
|
||||||
|
assert_eq!(nulls, b"{\"transaction\":[\"AQID\",\"base64\"],\"meta\":null,\"version\":null,\"transactionIndex\":null}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_canonical_json_sorts_objects_preserves_arrays_and_escapes_strings() {
|
||||||
|
let meta = serde_json::json!({
|
||||||
|
"z": [3, 2, 1],
|
||||||
|
"quote": "a\"b\\c\n",
|
||||||
|
"a": {"n": -12.5, "b": false, "a": true}
|
||||||
|
});
|
||||||
|
let material = match material(
|
||||||
|
std::option::Option::None,
|
||||||
|
"A\"B".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Value(meta),
|
||||||
|
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Number(0)),
|
||||||
|
crate::RawTransactionWireField::Value(u32::MAX),
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let bytes = super::canonical_payload_bytes(&material);
|
||||||
|
assert!(bytes.is_ok());
|
||||||
|
if let std::result::Result::Ok(bytes) = bytes {
|
||||||
|
let expected = b"{\"transaction\":[\"A\\\"B\",\"base64\"],\"meta\":{\"a\":{\"a\":true,\"b\":false,\"n\":-12.5},\"quote\":\"a\\\"b\\\\c\\n\",\"z\":[3,2,1]},\"version\":0,\"transactionIndex\":4294967295}";
|
||||||
|
assert_eq!(bytes, expected);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_block_time_rejects_negative_overflow_and_store_maximum_overrun() {
|
||||||
|
for block_time in [-1_i64, i64::MAX, 253_402_300_800_i64] {
|
||||||
|
let material = match material(
|
||||||
|
std::option::Option::Some(block_time),
|
||||||
|
"AQID".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let result = crate::canonicalize_raw_transaction(material);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let maximum = match material(
|
||||||
|
std::option::Option::Some(253_402_300_799_i64),
|
||||||
|
"AQID".to_owned(),
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let maximum = crate::canonicalize_raw_transaction(maximum);
|
||||||
|
assert!(maximum.is_ok());
|
||||||
|
if let std::result::Result::Ok(maximum) = maximum {
|
||||||
|
assert_eq!(maximum.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(253_402_300_799_000));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_payload_bound_is_enforced_by_common_before_store_construction() {
|
||||||
|
let material = match material(
|
||||||
|
std::option::Option::None,
|
||||||
|
"A".repeat(ksp_store_api::MAX_RAW_PAYLOAD_BYTES),
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
crate::RawTransactionWireField::Omitted,
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let result = crate::canonicalize_raw_transaction(material);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_material_and_wire_debug_do_not_render_transaction_or_meta_material() {
|
||||||
|
let secret_transaction = "SECRET_TRANSACTION_BODY";
|
||||||
|
let secret_meta = "SECRET_META_VALUE";
|
||||||
|
let material = match material(
|
||||||
|
std::option::Option::None,
|
||||||
|
secret_transaction.to_owned(),
|
||||||
|
crate::RawTransactionWireField::Value(serde_json::json!({"secret": secret_meta})),
|
||||||
|
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
|
||||||
|
crate::RawTransactionWireField::Value(1_u32),
|
||||||
|
) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let debug = std::format!("{material:?}");
|
||||||
|
assert!(!debug.contains(secret_transaction));
|
||||||
|
assert!(!debug.contains(secret_meta));
|
||||||
|
assert_eq!(std::format!("{:?}", crate::RawTransactionWireField::Value(secret_meta)), "Value(..)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_embedded_signature_material_preserves_block_fields_and_canonicalizes() {
|
||||||
|
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(network) => network,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let material = crate::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||||
|
network,
|
||||||
|
430_000_123,
|
||||||
|
std::option::Option::Some(1_787_072_400),
|
||||||
|
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
crate::RawTransactionWireField::Value(serde_json::json!({"err":null,"fee":5000})),
|
||||||
|
crate::RawTransactionWireField::Value(crate::RawTransactionVersion::Legacy),
|
||||||
|
crate::RawTransactionWireField::Value(0_u32),
|
||||||
|
);
|
||||||
|
assert!(material.is_ok());
|
||||||
|
let transaction = match material.and_then(crate::canonicalize_raw_transaction) {
|
||||||
|
std::result::Result::Ok(transaction) => transaction,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(transaction.reference().signature().as_bytes(), &[0_u8; 64]);
|
||||||
|
assert_eq!(transaction.slot(), 430_000_123);
|
||||||
|
assert_eq!(transaction.block_time().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_072_400_000));
|
||||||
|
assert!(transaction.payload().bytes().starts_with(b"{\"transaction\":[\"AQAAAA"));
|
||||||
|
assert!(transaction.payload().bytes().ends_with(b"\"version\":\"legacy\",\"transactionIndex\":0}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
81
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
Normal file
81
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_signature_parser_accepts_exact_sixty_four_zero_bytes() {
|
||||||
|
let text = "1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES);
|
||||||
|
let signature = crate::parse_raw_transaction_signature(text.as_str());
|
||||||
|
assert!(signature.is_ok());
|
||||||
|
if let std::result::Result::Ok(signature) = signature {
|
||||||
|
assert_eq!(signature.as_bytes(), &[0_u8; 64]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_002_signature_parser_rejects_text_bounds_invalid_base58_and_noncanonical_lengths() {
|
||||||
|
for text in [
|
||||||
|
"1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES - 1),
|
||||||
|
"1".repeat(crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES + 1),
|
||||||
|
format!("{}0", "1".repeat(crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES - 1)),
|
||||||
|
"1".repeat(65),
|
||||||
|
"z".repeat(crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES),
|
||||||
|
] {
|
||||||
|
let result = crate::parse_raw_transaction_signature(text.as_str());
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
|
||||||
|
let debug = std::format!("{error:?}");
|
||||||
|
assert!(!debug.contains(text.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_embedded_signature_extracts_first_signature_from_canonical_transaction_base64() {
|
||||||
|
let encoded = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
let signature = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||||
|
assert!(signature.is_ok());
|
||||||
|
if let std::result::Result::Ok(signature) = signature {
|
||||||
|
assert_eq!(signature.as_bytes(), &[0_u8; 64]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_004_embedded_signature_rejects_invalid_base64_noncanonical_short_vec_and_truncation() {
|
||||||
|
for encoded in [
|
||||||
|
"not-base64",
|
||||||
|
"gQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
||||||
|
] {
|
||||||
|
let result = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let std::result::Result::Err(error) = result {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID);
|
||||||
|
assert!(!error.to_string().contains(encoded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_embedded_signature_extracts_terminal_v1_signature_and_rejects_trailing_bytes() {
|
||||||
|
let encoded = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||||
|
let signature = crate::extract_raw_transaction_signature_from_binary_base64(encoded);
|
||||||
|
assert!(signature.is_ok());
|
||||||
|
if let std::result::Result::Ok(signature) = signature {
|
||||||
|
assert_eq!(signature.as_bytes(), &[0xaa_u8; 64]);
|
||||||
|
}
|
||||||
|
let decoded = base64::engine::general_purpose::STANDARD.decode(encoded);
|
||||||
|
assert!(decoded.is_ok());
|
||||||
|
if let std::result::Result::Ok(mut decoded) = decoded {
|
||||||
|
decoded.push(0);
|
||||||
|
let trailing = base64::engine::general_purpose::STANDARD.encode(decoded);
|
||||||
|
assert!(crate::extract_raw_transaction_signature_from_binary_base64(trailing.as_str()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
122
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// file: crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
const LEGACY_GOLDEN_BASE64: &str = "AREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREBAAECISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExAQEBAAKquw==";
|
||||||
|
const V0_GOLDEN_BASE64: &str = "ARISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhKAAQAAASMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIBAAEBAcwBJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQBAAA=";
|
||||||
|
const V1_EMPTY_CONFIG_GOLDEN_BASE64: &str = "gQEAAAAAAABSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgABQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0Orq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur";
|
||||||
|
const V1_GOLDEN_BASE64: &str = "gQEAAR8AAABRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUQECQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQgcAAAAAAAAACAAAAAkAAAAAgAAAAQEDAAABAgOqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
|
||||||
|
|
||||||
|
fn legacy_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::Legacy,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[0x21_u8; 32], [0x22_u8; 32]],
|
||||||
|
[0x31_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![0xaa, 0xbb])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0x11_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v0_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V0,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[0x23_u8; 32]],
|
||||||
|
[0x32_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(0, vec![1], vec![0xcc])],
|
||||||
|
vec![crate::RawSolanaAddressTableLookup::new([0x24_u8; 32], vec![0], vec![])],
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0x12_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 1),
|
||||||
|
vec![[0x41_u8; 32], [0x42_u8; 32]],
|
||||||
|
[0x51_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(1, vec![0], vec![1, 2, 3])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::new(
|
||||||
|
std::option::Option::Some(7),
|
||||||
|
std::option::Option::Some(8),
|
||||||
|
std::option::Option::Some(9),
|
||||||
|
std::option::Option::Some(32_768),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0xaa_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1_empty_config_wire() -> crate::RawSolanaTransactionWire {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[0x43_u8; 32]],
|
||||||
|
[0x52_u8; 32],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
return crate::RawSolanaTransactionWire::new(vec![[0xab_u8; 64]], message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_legacy_v0_and_v1_wire_goldens_are_exact() {
|
||||||
|
let legacy = crate::serialize_solana_transaction_wire_base64(&legacy_wire());
|
||||||
|
let v0 = crate::serialize_solana_transaction_wire_base64(&v0_wire());
|
||||||
|
let v1 = crate::serialize_solana_transaction_wire_base64(&v1_wire());
|
||||||
|
let v1_empty = crate::serialize_solana_transaction_wire_base64(&v1_empty_config_wire());
|
||||||
|
assert!(matches!(legacy.as_deref(), std::result::Result::Ok(value) if value == LEGACY_GOLDEN_BASE64));
|
||||||
|
assert!(matches!(v0.as_deref(), std::result::Result::Ok(value) if value == V0_GOLDEN_BASE64));
|
||||||
|
assert!(matches!(v1.as_deref(), std::result::Result::Ok(value) if value == V1_GOLDEN_BASE64));
|
||||||
|
assert!(matches!(v1_empty.as_deref(), std::result::Result::Ok(value) if value == V1_EMPTY_CONFIG_GOLDEN_BASE64));
|
||||||
|
let v1_bytes = crate::serialize_solana_transaction_wire(&v1_wire());
|
||||||
|
assert!(v1_bytes.is_ok());
|
||||||
|
if let std::result::Result::Ok(v1_bytes) = v1_bytes {
|
||||||
|
assert_eq!(v1_bytes.len(), 198);
|
||||||
|
assert_eq!(v1_bytes.first(), std::option::Option::Some(&0x81));
|
||||||
|
assert_eq!(v1_bytes.get(v1_bytes.len() - 64..), std::option::Option::Some([0xaa_u8; 64].as_slice()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_v1_structural_guards_reject_alt_duplicates_and_missing_config() {
|
||||||
|
let mut missing_config = v1_wire();
|
||||||
|
missing_config.message.config = std::option::Option::None;
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&missing_config).is_err());
|
||||||
|
let mut with_lookup = v1_wire();
|
||||||
|
with_lookup.message.address_table_lookups.push(crate::RawSolanaAddressTableLookup::new([9_u8; 32], vec![], vec![]));
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&with_lookup).is_err());
|
||||||
|
let mut duplicate = v1_wire();
|
||||||
|
duplicate.message.account_keys[1] = duplicate.message.account_keys[0];
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&duplicate).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_006_v1_program_index_zero_is_valid_and_bounds_remain_enforced() {
|
||||||
|
let message = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
vec![[1_u8; 32]],
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![crate::RawSolanaCompiledInstruction::new(0, vec![0], vec![])],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], message);
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&wire).is_ok());
|
||||||
|
let too_many_addresses = crate::RawSolanaTransactionMessage::new(
|
||||||
|
crate::RawSolanaMessageVersion::V1,
|
||||||
|
crate::RawSolanaMessageHeader::new(1, 0, 0),
|
||||||
|
(0_u8..65_u8).map(|value| return [value; 32]).collect(),
|
||||||
|
[2_u8; 32],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
std::option::Option::Some(crate::RawSolanaTransactionConfig::default()),
|
||||||
|
);
|
||||||
|
let wire = crate::RawSolanaTransactionWire::new(vec![[3_u8; 64]], too_many_addresses);
|
||||||
|
assert!(crate::serialize_solana_transaction_wire(&wire).is_err());
|
||||||
|
}
|
||||||
263
deltas/0.3.10/pre.001.md
Normal file
263
deltas/0.3.10/pre.001.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta 0.3.10-pre.001 — audit/sizing RAW Transaction + Worker ingest
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
archive : khadhroony-solana-project-v0.3.9.zip
|
||||||
|
SHA-256 : 41ef2589a11410aabfb329d387f0008e116006da60ada0802e5f72bc30d3466f
|
||||||
|
ZIP bytes : 7859118
|
||||||
|
ZIP entries : 1855
|
||||||
|
unzip -t : PASS
|
||||||
|
workspace members : 19
|
||||||
|
workspace.package.version base : 0.3.9
|
||||||
|
```
|
||||||
|
|
||||||
|
Le ZIP a été contrôlé sans path traversal/entrée absolue, symlink, `.git/`, `target/`, `node_modules/`, `Cargo.lock`, `.env` ou fichier de clé privée détecté.
|
||||||
|
|
||||||
|
## Type de livraison
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-general-0.3.10-pre.001.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Le type `general` est obligatoire parce que `Cargo.toml` racine est synchronisé vers `0.3.10-pre.1` par `VER-ID-009`. L’archive d’échange ne doit contenir que les fichiers ajoutés/modifiés et le delta, conformément à `VER-ARCHIVE-004`.
|
||||||
|
|
||||||
|
## Baseline exécutée avant modification
|
||||||
|
|
||||||
|
```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
|
||||||
|
Markdown table audit: clean (332 table(s), 750 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo` n’est pas installé dans l’environnement d’assemblage. Aucun gate Cargo local n’est déclaré PASS. Le journal opérateur stable `0.3.9` reste une preuve externe distincte.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Exécuter le gate d’ouverture `0.3.10-pre.001` demandé par le prompt `029` : vérifier intégralement la base `v0.3.9`, auditer l’extraction RAW commune et les capabilities live, réévaluer `TR-B..TR-F`, fixer les frontières Common/Worker/Transport/Config/Store, puis dimensionner les tranches suivantes sans commencer l’implémentation lourde.
|
||||||
|
|
||||||
|
## Mission exécutée
|
||||||
|
|
||||||
|
`pre.001` reste une tranche de planification, conformément au prompt `029` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
lecture règles/architecture obligatoires
|
||||||
|
inventaire workspace/manifests/features
|
||||||
|
extraction RAW commune auditée
|
||||||
|
capability matrix Worker implementation-ready
|
||||||
|
TR-B..TR-F réévalués
|
||||||
|
fraîcheur Solana/Yellowstone/providers revalidée
|
||||||
|
runtime/source supervision décidés
|
||||||
|
continuité/gap repair décidés
|
||||||
|
snapshot concret décidé
|
||||||
|
Config/secrets boundary décidée
|
||||||
|
threat model et preuves définis
|
||||||
|
dependency graph cible fixé
|
||||||
|
prereleases redimensionnées
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune implémentation lourde des deux nouvelles crates n’est commencée.
|
||||||
|
|
||||||
|
## Décisions principales
|
||||||
|
|
||||||
|
### Common RAW
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate : ksp-raw-transaction-lib
|
||||||
|
RAW format : ksp.solana.raw_transaction v1 inchangé
|
||||||
|
common deps : ksp-core-lib + ksp-store-lib(default-features=false) + serde_json + sha2
|
||||||
|
aucun Transport/Config/Job/Worker/runtime/backend
|
||||||
|
RawObservationKey reste producer-owned
|
||||||
|
Backfill conserve son observation key/job_id/scope_fingerprint/origin
|
||||||
|
```
|
||||||
|
|
||||||
|
Golden confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
byte_len = 112
|
||||||
|
sha256 = 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
|
||||||
|
```
|
||||||
|
|
||||||
|
Toute source full doit prouver une parité byte-for-byte avec RAW v1 avant persistence directe ; sinon elle reste discovery + hydration.
|
||||||
|
|
||||||
|
### Worker concret
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate : ksp-worker-raw-transaction-ingest-lib
|
||||||
|
caller-owned Tokio runtime
|
||||||
|
Worker-owned source/supervisor tasks
|
||||||
|
bounded channels uniquement
|
||||||
|
multi-source simultané
|
||||||
|
Store idempotence comme vérité de convergence
|
||||||
|
observations distinctes conservées
|
||||||
|
content conflict terminal
|
||||||
|
repair borné au run frontier
|
||||||
|
reconnect != replay
|
||||||
|
aucun paramètre historique métier au start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transport gaps
|
||||||
|
|
||||||
|
```text
|
||||||
|
TR-B confirmé : get_block_observed
|
||||||
|
TR-C confirmé/scindé : common material + adapters Worker + Transport change only if DTO lacks fidelity
|
||||||
|
TR-D confirmé : provenance durable full-material + source runtime snapshot
|
||||||
|
TR-E confirmé : reuse from_slot/replay info, aucun second Yellowstone client
|
||||||
|
TR-F réservé : EARLY seulement si protocole/accès/rôle RAW prouvés
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun edge Worker -> Config
|
||||||
|
std.transport V3 actuel réutilisé
|
||||||
|
pas de nouveau document métier Worker imposé en 0.3.10
|
||||||
|
adaptation Config seulement si un manque Transport concret l’exige
|
||||||
|
secrets restent Config-owned et redacted
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fraîcheur externe
|
||||||
|
|
||||||
|
Vérifié le 5 septembre 2026 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto latest = 12.7.0 (2026-08-29)
|
||||||
|
workspace stable = ^12.7 -> aucun bump initial
|
||||||
|
Yellowstone block from_slot replay a reçu un fix upstream en juillet 2026 -> smoke block replay obligatoire
|
||||||
|
Helius transactionSubscribe courant ; Helius blockSubscribe explicitement non supporté
|
||||||
|
Helius LaserStream replay 24 h documenté
|
||||||
|
PublicNode Yellowstone Mainnet/Testnet visible ; replay depth non prouvée
|
||||||
|
OrbitFlare gRPC Devnet disponible Free/Developer
|
||||||
|
QuickNode gRPC Scale+ ; port 443/x-token ; port 10000 sunset 2026-10-01
|
||||||
|
Alchemy replay docs contradictoires 6000 vs 432000 slots -> aucune constante KSP
|
||||||
|
Jito ShredStream shutdown annoncé pour 2026-09-05 -> SUNSET, non retenu TR-F
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan recalibré
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.002 common RAW foundation
|
||||||
|
pre.003 Backfill migration
|
||||||
|
pre.004 observed block + HTTP block material
|
||||||
|
pre.005 WS/Helius full parity
|
||||||
|
pre.006 Yellowstone parity
|
||||||
|
pre.007 Worker runtime foundation
|
||||||
|
pre.008 Yellowstone live/replay
|
||||||
|
pre.009 WS/HTTP live
|
||||||
|
pre.010 multi-source persistence/hardening
|
||||||
|
pre.011 gap repair
|
||||||
|
pre.012 Config/profiles + smokes accessibles
|
||||||
|
pre.013 EARLY si prouvé, sinon consolidation
|
||||||
|
pre.014 gate technique/live final
|
||||||
|
pre.015 réconciliation documentaire
|
||||||
|
pre.016 préparation publication
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
deltas/0.3.10/pre.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
`Cargo.toml` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
file version 489 -> 490
|
||||||
|
workspace.package.version 0.3.9 -> 0.3.10-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce bump est requis par `VER-ID-009` pour toute prerelease non-fix, même lorsque la tranche est documentaire.
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation après modification
|
||||||
|
|
||||||
|
Audits statiques réellement exécutés après matérialisation de `pre.001` :
|
||||||
|
|
||||||
|
```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
|
||||||
|
Markdown table audit: clean (339 table(s), 753 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier passage Markdown post-modification a détecté 32 erreurs d’alignement uniquement dans les nouvelles tables. Elles ont été corrigées selon la règle KSP, puis le gate complet ci-dessus a été relancé jusqu’à PASS.
|
||||||
|
|
||||||
|
La comparaison byte-à-byte contre le contenu du ZIP stable, après suppression du cache Python d’audit, donne exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ajoutés :
|
||||||
|
deltas/0.3.10/pre.001.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
modifié :
|
||||||
|
Cargo.toml
|
||||||
|
supprimé :
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
Les commandes Cargo/Tauri ne sont pas exécutables dans l’environnement d’assemblage courant (`cargo` absent). Elles restent donc explicitement `NON EXÉCUTÉES LOCALEMENT` pour ce delta ; aucune preuve opérateur antérieure n’est transformée en PASS local.
|
||||||
|
|
||||||
|
## Validations non exécutées localement
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
cargo tree --duplicates
|
||||||
|
builds Tauri
|
||||||
|
smokes live opt-in
|
||||||
|
```
|
||||||
|
|
||||||
|
Motif : toolchain Cargo absent du sandbox d’assemblage et aucune credential/provider live n’est injectée. Les preuves stables fournies par l’opérateur sont conservées comme contexte externe distinct ; elles ne remplacent pas les gates futurs de `0.3.10`.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question de boundary ne bloque `pre.002`. Restent volontairement à résoudre par preuve pendant les tranches prévues :
|
||||||
|
|
||||||
|
```text
|
||||||
|
parité byte-for-byte RAW v1 des sources WS/Helius/Yellowstone full
|
||||||
|
profondeur replay réellement observable par provider au moment du smoke
|
||||||
|
nécessité éventuelle d’un profil/rôle Transport supplémentaire pour un smoke accessible
|
||||||
|
admission d’une source EARLY seulement si protocole, accès et rôle RAW sont effectivement prouvés
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces points ne doivent pas être anticipés par des constantes, des claims lossless ou une RAW v2.
|
||||||
|
|
||||||
|
## Non inclus
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucune nouvelle crate créée
|
||||||
|
aucun Rust modifié
|
||||||
|
aucun Transport/Config/Store code modifié
|
||||||
|
aucun kbot3 ouvert ou copié
|
||||||
|
aucun Worker live
|
||||||
|
aucun RAW v2
|
||||||
|
aucun CHANGELOG/ROADMAP modifié
|
||||||
|
aucun prompt 0.3.11 créé
|
||||||
|
aucun Cargo gate déclaré localement
|
||||||
|
```
|
||||||
138
deltas/0.3.10/pre.002-fix.001.md
Normal file
138
deltas/0.3.10/pre.002-fix.001.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.002-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.002-fix.001` — correction des canaris d’intégration RAW et du gate Clippy
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.3.10-pre.002
|
||||||
|
workspace.package.version = 0.3.10-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le delta historique `deltas/0.3.10/pre.002.md` reste inchangé. Le présent fix trace séparément les défauts révélés par la validation opérateur du 2026-09-06.
|
||||||
|
|
||||||
|
## Résultat opérateur de la base
|
||||||
|
|
||||||
|
La validation communiquée confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all : exécuté
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (339 table(s), 754 file(s))
|
||||||
|
cargo check --workspace : PASS
|
||||||
|
cargo test -p ksp-raw-transaction-lib : PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests de `ksp-raw-transaction-lib` passent fonctionnellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
9 tests unitaires : PASS
|
||||||
|
8 tests d’intégration : PASS
|
||||||
|
0 échec
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes communiqués confirment la frontière prévue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-core-lib
|
||||||
|
ksp-store-api
|
||||||
|
serde_json
|
||||||
|
sha2
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun runtime, Transport, Config, Job, Worker ou backend Store n’apparaît dans le graphe normal de la common crate.
|
||||||
|
|
||||||
|
## Motif du fix
|
||||||
|
|
||||||
|
Le gate strict :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
échoue exclusivement sur les nouveaux tests d’intégration de `ksp-raw-transaction-lib`.
|
||||||
|
|
||||||
|
### Rustdoc de crate manquante
|
||||||
|
|
||||||
|
Quatre fichiers `tests/*.rs` n’avaient pas de documentation de crate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Avec `missing_docs` promu en erreur par `-D warnings`, chaque binaire de test d’intégration doit documenter sa crate.
|
||||||
|
|
||||||
|
Correction : ajout d’une rustdoc `//! ...` conforme aux autres tests d’intégration KSP. Aucun `#[allow(missing_docs)]` n’est ajouté.
|
||||||
|
|
||||||
|
### `clippy::collapsible_if`
|
||||||
|
|
||||||
|
Le canari d’inventaire des modules production utilisait deux conditions imbriquées pour filtrer les fichiers `.rs` puis extraire leur nom.
|
||||||
|
|
||||||
|
Correction : filtrage anticipé avec `continue`, puis `let ... else`, sans `#[allow(clippy::collapsible_if)]` et sans changement de l’inventaire attendu.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Ce fix modifie du Rust de test. Le signal technique Cargo est donc synchronisé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.2 -> 0.3.10-pre.2.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `491` à `492`.
|
||||||
|
|
||||||
|
## Invariants volontairement inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
RAW_TRANSACTION_FORMAT_ID = ksp.solana.raw_transaction
|
||||||
|
RAW_TRANSACTION_FORMAT_VERSION = 1
|
||||||
|
golden byte_len = 112
|
||||||
|
golden sha256 = 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
|
||||||
|
RawObservationKey = producer-owned
|
||||||
|
dépendances common = core + store-api + serde_json + sha2
|
||||||
|
aucune migration Backfill
|
||||||
|
aucun Worker
|
||||||
|
aucune nouvelle feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.002-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validation à rejouer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-raw-transaction-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.003` reste bloquée tant que ce gate n’est pas intégralement vert.
|
||||||
261
deltas/0.3.10/pre.002.md
Normal file
261
deltas/0.3.10/pre.002.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.002` — fondation RAW Transaction commune
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.001
|
||||||
|
workspace.package.version = 0.3.10-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur communiqué pour `pre.001` passe `cargo fmt --all`, les audits Rust/Markdown, `cargo check --workspace` et Clippy workspace `--all-targets --all-features -- -D warnings`. Le message ne fournit pas de `cargo test` supplémentaire ; aucun test non communiqué n’est déclaré exécuté.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Créer uniquement `ksp-raw-transaction-lib` comme lower-layer commune de canonicalisation RAW Transaction v1 : matériau source-neutral, parser de signature, canonical JSON, SHA-256 et assemblage transaction/observation. Aucun Worker et aucune migration du Backfill dans cette tranche.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
Cette tranche est une prerelease non-fix :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `Cargo.toml` racine passe du header `490` au header `491` et ajoute `crates/ksp-raw-transaction-lib` aux membres du workspace.
|
||||||
|
|
||||||
|
## Correction normative du plan `pre.001`
|
||||||
|
|
||||||
|
Le sizing `pre.001` avait retenu `ksp-store-lib(default-features=false)` pour la common crate. La relecture au moment de matérialiser la frontière révèle une règle plus spécifique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DEP-PIPE-006
|
||||||
|
Le pipeline raw ingestion peut dépendre des modèles homogènes de
|
||||||
|
ksp-onchain-transport-lib et de ksp-store-api, mais pas de ksp-store-lib.
|
||||||
|
```
|
||||||
|
|
||||||
|
La dépendance est donc corrigée vers `ksp-store-api`. Cette correction ne change aucun invariant RAW ; elle retire au contraire la façade/runtime Store de la lower-layer réutilisable.
|
||||||
|
|
||||||
|
## Dépendances exactes
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> ksp-store-api
|
||||||
|
-> serde_json
|
||||||
|
-> sha2
|
||||||
|
```
|
||||||
|
|
||||||
|
Absents de la crate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-store-lib
|
||||||
|
ksp-store-postgres-lib
|
||||||
|
ksp-onchain-transport-lib
|
||||||
|
ksp-config-lib
|
||||||
|
ksp-job-api
|
||||||
|
ksp-job-backfill-lib
|
||||||
|
ksp-worker-api
|
||||||
|
Tokio / futures
|
||||||
|
provider SDK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Surface publique
|
||||||
|
|
||||||
|
```text
|
||||||
|
RAW_TRANSACTION_FORMAT_ID = ksp.solana.raw_transaction
|
||||||
|
RAW_TRANSACTION_FORMAT_VERSION = 1
|
||||||
|
MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 64
|
||||||
|
MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES = 88
|
||||||
|
|
||||||
|
RawTransactionMaterial
|
||||||
|
RawTransactionWireField<T>
|
||||||
|
RawTransactionVersion
|
||||||
|
RawTransactionAcquisition
|
||||||
|
|
||||||
|
parse_raw_transaction_signature(...)
|
||||||
|
canonicalize_raw_transaction(...)
|
||||||
|
assemble_raw_transaction_acquisition(...)
|
||||||
|
|
||||||
|
ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID
|
||||||
|
ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID
|
||||||
|
ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID
|
||||||
|
```
|
||||||
|
|
||||||
|
Tous les modules restent privés et les éléments consommables sont réexportés au crate-root.
|
||||||
|
|
||||||
|
## Contrat matérialisé
|
||||||
|
|
||||||
|
### Signature
|
||||||
|
|
||||||
|
Le parser reçoit un texte Base58 borné de `64..=88` octets et n’accepte que les représentations décodant exactement vers `64` octets. Aucun Solana SDK ou codec externe n’est ajouté.
|
||||||
|
|
||||||
|
Les erreurs ne recopient jamais la signature hostile ; seules les longueurs numériques sûres peuvent apparaître en contexte.
|
||||||
|
|
||||||
|
### Matériau source-neutral
|
||||||
|
|
||||||
|
`RawTransactionMaterial::binary_base64(...)` reçoit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
network
|
||||||
|
signature canonique 64 bytes
|
||||||
|
slot u64
|
||||||
|
block_time Option<i64>
|
||||||
|
transaction data binaire Base64 déjà obtenu de la source
|
||||||
|
meta omitted/null/value
|
||||||
|
version omitted/null/value (legacy ou u8)
|
||||||
|
transactionIndex omitted/null/value u32
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun type Transport/provider n’entre dans la common crate. La représentation interne peut être étendue ultérieurement pour Yellowstone uniquement lorsqu’une parité RAW v1 sera démontrée.
|
||||||
|
|
||||||
|
### Canonicalisation RAW v1
|
||||||
|
|
||||||
|
La sortie conserve exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ordre top-level : transaction -> meta -> version -> transactionIndex
|
||||||
|
transaction = [data, "base64"]
|
||||||
|
tri récursif des clés d’objets JSON
|
||||||
|
ordre des arrays conservé
|
||||||
|
omitted != null != value
|
||||||
|
block_time hors payload, converti en RawTimestamp
|
||||||
|
SHA-256 sur les bytes canoniques exacts
|
||||||
|
format id/version gelés
|
||||||
|
```
|
||||||
|
|
||||||
|
Golden verrouillé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
bytes = {"transaction":["AQID","base64"],"meta":{"a":{"x":null,"y":true},"z":1},"version":"legacy","transactionIndex":7}
|
||||||
|
byte_len = 112
|
||||||
|
sha256 = 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observation producer-owned
|
||||||
|
|
||||||
|
`assemble_raw_transaction_acquisition(...)` reçoit la `RawObservationKey` déjà choisie par le producteur et construit l’observation sur la référence exacte de la transaction. Aucun domaine de clé Backfill ou Worker n’est introduit dans common.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
Tests unitaires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
signature exacte 64 bytes
|
||||||
|
signature bornes / caractère Base58 invalide / overflow / longueur décodée non canonique
|
||||||
|
golden bytes + hash RAW v1
|
||||||
|
omitted/null/value
|
||||||
|
golden JSON récursif / arrays / escaping / nombres
|
||||||
|
block_time négatif / overflow / maximum
|
||||||
|
borne payload
|
||||||
|
Debug material/wire redacted
|
||||||
|
assemblage acquisition + observation key producer-owned
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests d’intégration :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dependency boundary exacte
|
||||||
|
absence runtime/Transport/Config/Job/Worker/backend
|
||||||
|
consommation crate-root externe
|
||||||
|
assemblage avec modèles Store API
|
||||||
|
inventaire modules production
|
||||||
|
absence de contrat Worker/Backfill/Transport dans la façade
|
||||||
|
error codes stables
|
||||||
|
signature hostile non recopiée
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-raw-transaction-lib/Cargo.toml
|
||||||
|
crates/ksp-raw-transaction-lib/src/acquisition.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/canonical.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/error.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/acquisition.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/security_hardening.rs
|
||||||
|
deltas/0.3.10/pre.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Documentation volontairement différée
|
||||||
|
|
||||||
|
`README.md` et `USAGE.md` de la nouvelle crate ne sont pas finalisés dans cette tranche de fondation. La réconciliation version-neutral reste réservée au couloir documentaire `pre.015`, après parité cross-source, migration Backfill et freeze fonctionnelle.
|
||||||
|
|
||||||
|
## Validations exécutées dans l’environnement d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
Markdown table audit: clean (339 table(s), 754 file(s))
|
||||||
|
|
||||||
|
contrôle TOML : 20 membres workspace, nouvelle crate présente
|
||||||
|
contrôle manifest common : dépendances exactes core/store-api/serde_json/sha2, aucune feature/dev/build dependency
|
||||||
|
```
|
||||||
|
|
||||||
|
L’environnement d’assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo de `pre.002` n’est donc déclaré PASS localement.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-raw-transaction-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations non exécutées localement
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-raw-transaction-lib -e features
|
||||||
|
smokes live : non pertinents pour cette tranche purement déterministe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
RAW v1 reste unique et inchangé
|
||||||
|
ksp-store-api remplace ksp-store-lib dans common pour conformité DEP-PIPE-006
|
||||||
|
RawObservationKey reste producer-owned
|
||||||
|
aucun type Transport dans common
|
||||||
|
aucun async/runtime dans common
|
||||||
|
aucune migration Backfill avant pre.003
|
||||||
|
aucun Worker avant pre.007 selon le plan recalibré
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque `pre.003` après un gate opérateur vert. Les projections structurées WS/Helius/Yellowstone restent volontairement différées jusqu’aux tranches de parité prévues ; elles ne doivent pas modifier RAW v1 par anticipation.
|
||||||
236
deltas/0.3.10/pre.003.md
Normal file
236
deltas/0.3.10/pre.003.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.003` — migration du Backfill vers la common RAW Transaction
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.002-fix.001
|
||||||
|
workspace.package.version = 0.3.10-pre.2.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur communiqué le 2026-09-06 ferme intégralement la base : audits statiques propres, `cargo check --workspace` PASS, Clippy workspace `--all-targets --all-features -- -D warnings` PASS, `cargo test -p ksp-raw-transaction-lib` PASS avec 9 tests unitaires et 8 tests d’intégration, arbres normal/features conformes.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Migrer uniquement `ksp-job-backfill-lib` vers `ksp-raw-transaction-lib` sans modifier le comportement de campagne : common devient propriétaire du parser Base58, de RAW v1, du hash payload et de l’assemblage transaction/observation ; Backfill conserve Transport, provenance, observation key, Job scope et Store persistence.
|
||||||
|
|
||||||
|
Aucun Worker, aucune nouvelle source Transport et aucune extension du format RAW ne sont introduits.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `492` à `493`. Le nombre de membres workspace reste `20`.
|
||||||
|
|
||||||
|
## Migration de dépendance
|
||||||
|
|
||||||
|
`ksp-job-backfill-lib` ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib = { path = "../ksp-raw-transaction-lib" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Le reste de son graphe runtime est conservé. `serde_json` reste nécessaire au type Transport `SolanaWireField<serde_json::Value>`. `sha2` reste nécessaire aux fingerprints et à la `RawObservationKey` Backfill ; il n’est plus utilisé pour le hash du payload RAW.
|
||||||
|
|
||||||
|
## Chemin RAW après migration
|
||||||
|
|
||||||
|
```text
|
||||||
|
BackfillSignature
|
||||||
|
-> parse_raw_transaction_signature
|
||||||
|
-> RawTransactionSignature
|
||||||
|
|
||||||
|
getTransaction observed
|
||||||
|
-> adapter SolanaEncodedTransaction/SolanaWireField
|
||||||
|
-> RawTransactionMaterial::binary_base64
|
||||||
|
-> canonicalize_raw_transaction
|
||||||
|
-> RawTransaction
|
||||||
|
|
||||||
|
Backfill provenance + Backfill RawObservationKey
|
||||||
|
-> assemble_raw_transaction_acquisition
|
||||||
|
-> BackfillRawAcquisition
|
||||||
|
-> Store persistence existante
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs du parser/canonicalizer common sont remappées au boundary Backfill vers :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Job ne change donc pas de domaine d’erreur externe par effet de la migration.
|
||||||
|
|
||||||
|
## Code privé supprimé du Backfill
|
||||||
|
|
||||||
|
La migration retire de `conversion.rs` les implémentations locales de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base58_digit(...)
|
||||||
|
algorithme de décodage Base58 64-byte
|
||||||
|
convert_block_time(...)
|
||||||
|
canonical_payload_bytes(...)
|
||||||
|
append_wire_value(...)
|
||||||
|
append_canonical_json(...)
|
||||||
|
append_json_string(...)
|
||||||
|
RawPayload::try_new + SHA-256 du payload
|
||||||
|
construction directe RawTransactionObservation
|
||||||
|
```
|
||||||
|
|
||||||
|
`CanonicalTransactionFields` reste un adapter Transport privé ; il ne définit aucun format RAW.
|
||||||
|
|
||||||
|
## Propriété Backfill conservée
|
||||||
|
|
||||||
|
Le Backfill continue de posséder :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getTransaction observed / rôle HTTP
|
||||||
|
provider gagnant
|
||||||
|
endpoint gagnant
|
||||||
|
commitment
|
||||||
|
job_id
|
||||||
|
scope_fingerprint
|
||||||
|
RawAcquisitionOrigin::Backfill
|
||||||
|
capture_session
|
||||||
|
observation contract version = 1
|
||||||
|
algorithme RawObservationKey Backfill
|
||||||
|
persistence Store
|
||||||
|
sémantique Missing
|
||||||
|
```
|
||||||
|
|
||||||
|
La common crate ne reçoit donc aucun vocabulaire de campagne.
|
||||||
|
|
||||||
|
## Parité déterministe
|
||||||
|
|
||||||
|
Le golden historique reste inchangé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
format_id = ksp.solana.raw_transaction
|
||||||
|
format_version = 1
|
||||||
|
bytes = {"transaction":["AQID","base64"],"meta":{"a":{"x":null,"y":true},"z":1},"version":"legacy","transactionIndex":7}
|
||||||
|
byte_len = 112
|
||||||
|
sha256 = 220792d2b15d262fda242cb220774ee9ddeffebf04dcfadabcf8ef76a9b1a7c3
|
||||||
|
```
|
||||||
|
|
||||||
|
La provenance historique reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = publicnode
|
||||||
|
protocol = solana.http.json_rpc
|
||||||
|
method = getTransaction
|
||||||
|
endpoint = devnet-primary
|
||||||
|
commitment = finalized
|
||||||
|
capture_session = backfill:pre006
|
||||||
|
origin = Backfill
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari d’observation key est désormais exact :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = provider
|
||||||
|
endpoint = endpoint-a
|
||||||
|
key = b8550f0f2165f370de918bd4fb0ec782391afdddb88c57f010743e000b136702
|
||||||
|
```
|
||||||
|
|
||||||
|
## Canaris mis à jour
|
||||||
|
|
||||||
|
```text
|
||||||
|
BackfillSignature délègue au parser common
|
||||||
|
bornes Backfill == bornes common
|
||||||
|
format id/version Backfill == common
|
||||||
|
wire omitted/null reste distinct via common
|
||||||
|
non-Base64 reste rejeté avec error code Backfill
|
||||||
|
block_time invalide reste terminal avec error code Backfill
|
||||||
|
golden bytes/hash/provenance inchangés
|
||||||
|
observation key exacte inchangée
|
||||||
|
manifest Backfill exige l’edge common
|
||||||
|
conversion.rs exige parser + canonicalizer + assembler common
|
||||||
|
conversion.rs interdit canonical JSON/Base58/RawPayload privés
|
||||||
|
lower-layer common interdit toute dépendance retour vers Job
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.003.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/README.md
|
||||||
|
crates/ksp-job-backfill-lib/src/conversion.rs
|
||||||
|
crates/ksp-job-backfill-lib/src/lib.rs
|
||||||
|
crates/ksp-job-backfill-lib/src/request.rs
|
||||||
|
crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
|
crates/ksp-job-backfill-lib/unit_tests/conversion.rs
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations exécutées dans l’environnement d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
Markdown table audit: clean (339 table(s), 756 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
L’environnement d’assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations non exécutées localement
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
smokes live : non pertinents pour cette tranche de migration déterministe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
common possède parser + canonical RAW v1 + payload hash + assembly
|
||||||
|
Backfill conserve observation key/provenance/campagne
|
||||||
|
les erreurs common sont remappées vers le code Backfill historique
|
||||||
|
les constantes publiques RAW du Backfill sont des reexports common
|
||||||
|
les bornes publiques de signature Backfill suivent les constantes common
|
||||||
|
aucun changement du Store persistence path
|
||||||
|
aucun changement de source/acquisition réseau
|
||||||
|
aucun Worker avant la tranche planifiée
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque le gate de `pre.003`. `pre.004` reste strictement dédiée à `get_block_observed` et au matériau HTTP block après validation opérateur complète de cette migration.
|
||||||
165
deltas/0.3.10/pre.004-fix.001.md
Normal file
165
deltas/0.3.10/pre.004-fix.001.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.004-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.004-fix.001` — correction du canari HTTP block et du gate Clippy
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.3.10-pre.004
|
||||||
|
workspace.package.version = 0.3.10-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Le delta historique `deltas/0.3.10/pre.004.md` reste inchangé. Le présent fix trace séparément le défaut révélé par la validation opérateur du 2026-09-06.
|
||||||
|
|
||||||
|
## Résultat opérateur de la base
|
||||||
|
|
||||||
|
La validation communiquée confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all : exécuté
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (339 table(s), 757 file(s))
|
||||||
|
cargo check --workspace : PASS
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings : FAIL
|
||||||
|
cargo test -p ksp-raw-transaction-lib : PASS
|
||||||
|
cargo test -p ksp-onchain-transport-lib : PASS
|
||||||
|
cargo test -p ksp-job-backfill-lib --test http_block_material : PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests fonctionnels communiqués confirment notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib : 12 unit + 9 intégration, 0 échec
|
||||||
|
ksp-onchain-transport-lib : 387 unit + 51 public API + 43 release completeness + 4 doctests, 0 échec
|
||||||
|
http_block_material : 1 test, PASS
|
||||||
|
smokes live Transport : ignorés comme prévu
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres normaux communiqués conservent les frontières prévues : la common RAW dépend de `base64`, `ksp-core-lib`, `ksp-store-api`, `serde_json`, `sha2`, et `ksp-onchain-transport-lib` ne dépend pas de `ksp-raw-transaction-lib`.
|
||||||
|
|
||||||
|
## Motif du fix
|
||||||
|
|
||||||
|
Le gate strict :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
échoue exclusivement sur sept `clippy::expect_used` dans les deux helpers privés du nouveau test d’intégration :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pool_for_url(...)
|
||||||
|
serve_once(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Les usages concernés sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
parsing HttpEndpointUrl
|
||||||
|
construction HttpTransportPool
|
||||||
|
bind du listener local
|
||||||
|
lecture de local_addr
|
||||||
|
accept de la connexion fixture
|
||||||
|
read de la requête fixture
|
||||||
|
write de la réponse fixture
|
||||||
|
```
|
||||||
|
|
||||||
|
La configuration commune autorise `expect` dans les fonctions de test, mais ces helpers ne portent pas eux-mêmes l’attribut `#[test]`/`#[tokio::test]` et sont donc correctement rejetés par Clippy.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
Aucun lint n’est désactivé.
|
||||||
|
|
||||||
|
`pool_for_url(...)` retourne désormais explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp_core_lib::Result<HttpTransportPool>
|
||||||
|
```
|
||||||
|
|
||||||
|
et propage les deux erreurs possibles par `match` explicite.
|
||||||
|
|
||||||
|
`serve_once(...)` retourne désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
std::io::Result<(String, JoinHandle<std::io::Result<()>>)>
|
||||||
|
```
|
||||||
|
|
||||||
|
Le bind, `local_addr`, `accept`, `read` et `write_all` retournent leurs erreurs par contrôle de flux explicite. Les assertions/`expect()` de fixture restent uniquement dans le corps `#[tokio::test]`, conformément à `allow-expect-in-tests = true`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Ce fix modifie du Rust de test. Le signal technique Cargo est donc synchronisé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.4 -> 0.3.10-pre.4.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `494` à `495`.
|
||||||
|
|
||||||
|
## Invariants volontairement inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
get_block_observed : inchangé
|
||||||
|
winner provider/endpoint : inchangé
|
||||||
|
RAW_TRANSACTION_FORMAT_ID = ksp.solana.raw_transaction
|
||||||
|
RAW_TRANSACTION_FORMAT_VERSION = 1
|
||||||
|
extraction Base64/short_vec : inchangée
|
||||||
|
projection 2 transactions / index 0-1 : inchangée
|
||||||
|
TR-C2 : adapter productif toujours Worker-owned
|
||||||
|
aucun changement de code production
|
||||||
|
aucune dépendance ou feature ajoutée
|
||||||
|
aucun nouveau Worker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/http_block_material.rs
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.004-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations exécutées dans l’environnement d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
Markdown table audit: clean (339 table(s), 758 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
L’environnement d’assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`; aucun gate Cargo du fix n’est donc déclaré PASS localement.
|
||||||
|
|
||||||
|
## Validation à rejouer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib --test http_block_material
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --edges normal
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.005` reste bloquée tant que ce gate n’est pas intégralement vert.
|
||||||
279
deltas/0.3.10/pre.004.md
Normal file
279
deltas/0.3.10/pre.004.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.004` — HTTP observed block + matériau RAW block
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.003
|
||||||
|
workspace.package.version = 0.3.10-pre.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur communiqué le 2026-09-06 ferme intégralement `pre.003` : audits statiques propres, `cargo check --workspace` PASS, Clippy workspace `--all-targets --all-features -- -D warnings` PASS, tests de `ksp-raw-transaction-lib` PASS, tests de `ksp-job-backfill-lib` PASS avec 51 tests unitaires et 20 tests d’intégration, arbres normal/features conformes.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Fermer `TR-B` et la preuve HTTP block de `TR-C` sans ouvrir le Worker :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getBlock pool HTTP
|
||||||
|
-> valeur observée + provider/endpoint winner
|
||||||
|
-> SolanaConfirmedBlock
|
||||||
|
-> N SolanaBlockTransaction full Base64
|
||||||
|
-> matériau RAW source-neutral
|
||||||
|
```
|
||||||
|
|
||||||
|
La tranche ne crée aucun adapter productif Transport -> common. Conformément à `TR-C2`, cet adapter reste producer-owned et sera matérialisé dans le futur `ksp-worker-raw-transaction-ingest-lib`.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `493` à `494`. Le nombre de membres workspace reste `20`.
|
||||||
|
|
||||||
|
## `get_block_observed`
|
||||||
|
|
||||||
|
`ksp-onchain-transport-lib` ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HttpTransportPool::get_block_observed(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le wrapper :
|
||||||
|
|
||||||
|
```text
|
||||||
|
réutilise exactement la validation/config moderne de get_block
|
||||||
|
utilise le routing/admission/timeout/retry standard du pool
|
||||||
|
retourne HttpObservedValue<Option<SolanaConfirmedBlock>>
|
||||||
|
conserve endpoint_name + provider du winner réel après retry/reroute
|
||||||
|
préserve result = null comme None
|
||||||
|
n’expose ni URL, ni headers, ni raw HTTP body
|
||||||
|
réutilise le décodage typé SolanaConfirmedBlock
|
||||||
|
```
|
||||||
|
|
||||||
|
`get_block(...)` et `get_block_observed(...)` partagent désormais le même helper privé de paramètres et la même validation du commitment `processed`.
|
||||||
|
|
||||||
|
## Provenance winner déterministe
|
||||||
|
|
||||||
|
Le canari Transport configure deux endpoints :
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint 1 -> HTTP 429
|
||||||
|
endpoint 2 -> getBlock success
|
||||||
|
```
|
||||||
|
|
||||||
|
La valeur observée doit reporter exclusivement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint_name = winner-endpoint
|
||||||
|
provider = winner-provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Debug conserve uniquement la projection sûre et ne rend pas le transaction wire Base64.
|
||||||
|
|
||||||
|
## Matériau Base64 source-neutral
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib` ajoute `base64` à son graphe common et expose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
extract_raw_transaction_signature_from_binary_base64(...)
|
||||||
|
RawTransactionMaterial::binary_base64_with_embedded_signature(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
L’extracteur lit directement le wire transaction Solana Base64 sans dépendance à une crate transaction runtime :
|
||||||
|
|
||||||
|
```text
|
||||||
|
borne la représentation textuelle avant décodage
|
||||||
|
exige Base64 STANDARD canonique
|
||||||
|
lit un short_vec de signature count borné et canonique
|
||||||
|
refuse count = 0
|
||||||
|
refuse short_vec tronqué/non canonique
|
||||||
|
refuse tableau de signatures tronqué
|
||||||
|
exige au moins un byte de message après les signatures déclarées
|
||||||
|
retourne uniquement la première signature exacte de 64 octets
|
||||||
|
ne copie jamais le transaction wire dans erreur/Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Le constructeur common réutilise cette signature puis conserve les dimensions RAW v1 existantes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
network
|
||||||
|
slot
|
||||||
|
block_time
|
||||||
|
transaction Base64
|
||||||
|
meta omitted/null/value
|
||||||
|
version omitted/null/legacy/number
|
||||||
|
transaction_index
|
||||||
|
```
|
||||||
|
|
||||||
|
Le format canonique reste strictement `ksp.solana.raw_transaction` v1.
|
||||||
|
|
||||||
|
## Preuve cross-layer sans nouveau couplage de production
|
||||||
|
|
||||||
|
Le nouveau canari test-only :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-backfill-lib/tests/http_block_material.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
utilise une crate qui dépend déjà de Transport et de common pour prouver la composition future sans créer d’edge architectural supplémentaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
get_block_observed
|
||||||
|
-> SolanaConfirmedBlock
|
||||||
|
-> 2 SolanaBlockTransaction distinctes
|
||||||
|
-> full Base64 + meta + version + transactionIndex 0/1
|
||||||
|
-> RawTransactionMaterial::binary_base64_with_embedded_signature
|
||||||
|
-> canonicalize_raw_transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux transaction wires minimales font `66` octets chacune et utilisent des signatures distinctes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction 0 -> signature [0; 64]
|
||||||
|
transaction 1 -> signature [1; 64]
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari vérifie que ces signatures et les index 0/1 restent associés à la bonne transaction, avec le slot et le block time communs du bloc.
|
||||||
|
|
||||||
|
Cette preuve reste strictement test-only. Aucun symbole `ksp_raw_transaction_lib` n’est ajouté aux sources de production de `ksp-onchain-transport-lib`.
|
||||||
|
|
||||||
|
## Dépendances common
|
||||||
|
|
||||||
|
Le graphe runtime direct de `ksp-raw-transaction-lib` devient exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
base64
|
||||||
|
ksp-core-lib
|
||||||
|
ksp-store-api
|
||||||
|
serde_json
|
||||||
|
sha2
|
||||||
|
```
|
||||||
|
|
||||||
|
Restent interdits :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-store-lib
|
||||||
|
ksp-onchain-transport-lib
|
||||||
|
ksp-config-lib
|
||||||
|
ksp-job-api
|
||||||
|
ksp-job-backfill-lib
|
||||||
|
ksp-worker-api
|
||||||
|
tokio
|
||||||
|
futures
|
||||||
|
```
|
||||||
|
|
||||||
|
La règle `DEP-PIPE-006` reste donc respectée.
|
||||||
|
|
||||||
|
## Documentation durable
|
||||||
|
|
||||||
|
`crates/ksp-onchain-transport-lib/USAGE.md` documente la différence entre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
get_block(...)
|
||||||
|
get_block_observed(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
sans transformer le guide en journal de prerelease.
|
||||||
|
|
||||||
|
`docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` ferme `TR-B`, conserve `TR-C2` producer-owned et corrige le graphe common vers `ksp-store-api`.
|
||||||
|
|
||||||
|
Le plan `031` et la validation `027` enregistrent également la fermeture opérateur de `pre.003` et le périmètre exact de `pre.004`.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-backfill-lib/tests/http_block_material.rs
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_block.observed_material.success.json
|
||||||
|
deltas/0.3.10/pre.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
||||||
|
crates/ksp-raw-transaction-lib/Cargo.toml
|
||||||
|
crates/ksp-raw-transaction-lib/src/canonical.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/error.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/canonical.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations exécutées dans l’environnement d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
Markdown table audit: clean (339 table(s), 757 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
L’environnement d’assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib --test http_block_material
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --edges normal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations non exécutées localement
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib --test http_block_material
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --edges normal
|
||||||
|
smokes live : non requis pour cette tranche déterministe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
TR-B est fermé par get_block_observed
|
||||||
|
get_block et get_block_observed partagent validation + décodage
|
||||||
|
la common extrait la signature d’un transaction wire Base64 complet
|
||||||
|
aucune dépendance Solana transaction runtime n’est ajoutée à la common
|
||||||
|
aucun adapter productif Transport -> common dans Transport
|
||||||
|
TR-C2 reste Worker-owned
|
||||||
|
la preuve block -> common est cross-layer et test-only
|
||||||
|
RAW v1 reste inchangé
|
||||||
|
aucun Worker live n’est ouvert dans pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque le gate de `pre.004`. `pre.005` reste dédiée à la parité WS `blockSubscribe` + Helius `transactionSubscribe`, avec golden parity ou fallback hydration explicitement qualifié selon la représentation réellement disponible.
|
||||||
92
deltas/0.3.10/pre.005-fix.001.md
Normal file
92
deltas/0.3.10/pre.005-fix.001.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.005-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.005-fix.001` — correction de typage du canari WS RAW parity
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.005
|
||||||
|
```
|
||||||
|
|
||||||
|
## Motif du fix
|
||||||
|
|
||||||
|
Le premier rejeu opérateur de `pre.005` valide les audits statiques, `cargo check --workspace`, la common RAW, la suite Transport complète et les arbres Cargo, mais Clippy strict et la suite Backfill s’arrêtent sur une unique erreur de compilation `E0308` dans `tests/ws_raw_parity.rs`.
|
||||||
|
|
||||||
|
Le canari comparait :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RawTransaction::block_time().unix_millis() -> u64
|
||||||
|
FIXTURE_BLOCK_TIME * 1_000 -> i64
|
||||||
|
```
|
||||||
|
|
||||||
|
L’assertion produisait donc `Option<u64> != Option<i64>` au niveau du type.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
La constante de fixture devient directement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
FIXTURE_BLOCK_TIME_MILLIS: u64 = 1_787_072_400_000
|
||||||
|
```
|
||||||
|
|
||||||
|
L’assertion compare désormais `Option<u64>` à `Option<u64>` sans cast, conversion runtime, `unwrap`, `expect` ou modification du comportement testé.
|
||||||
|
|
||||||
|
## Invariants inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockSubscribe standard confirmed/full/base64 legacy-v0 -> golden RAW-direct positif
|
||||||
|
Helius transactionSubscribe full/base64 -> signal + hydration HTTP
|
||||||
|
RAW v1 bytes/hash contracts
|
||||||
|
TR-C2 Worker-owned
|
||||||
|
tokio-tungstenite strictement dev-only
|
||||||
|
aucun code de production modifié
|
||||||
|
aucune dépendance ni feature modifiée
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.5.fix.1
|
||||||
|
Cargo.toml header = 497
|
||||||
|
workspace members = 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.005-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audits d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (339 table(s), 760 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gate opérateur à rejouer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres Cargo n’ont pas besoin d’être rejoués pour ce fix : ni `Cargo.toml` de crate, ni dépendance, ni feature ne change.
|
||||||
101
deltas/0.3.10/pre.005-fix.002.md
Normal file
101
deltas/0.3.10/pre.005-fix.002.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.005-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.005-fix.002` — conformité Clippy du canari WS + inventaire dev exact
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.005-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Motif du fix
|
||||||
|
|
||||||
|
Le second rejeu opérateur confirme `cargo check --workspace`, la common RAW et la suite Transport complète, mais révèle deux défauts test-only :
|
||||||
|
|
||||||
|
```text
|
||||||
|
25 clippy::question_mark_used dans crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||||
|
1 canari hardening encore calibré sur dev-dependencies = {tokio}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le manifest `pre.005` contient légitimement `tokio-tungstenite` en dev-only pour les serveurs WebSocket déterministes ; `tests/dependency_boundary.rs` verrouille déjà qu’il ne migre pas vers les dépendances de production.
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
`ws_raw_parity.rs` remplace tous les `?` par du contrôle de flux explicite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Option -> match + retour None
|
||||||
|
Result -> match + retour Err/None
|
||||||
|
appel async Result -> if let Err(error) + retour Err(error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `#[allow(...)]`, aucun changement de golden et aucune modification du scénario positif `blockSubscribe` ou négatif Helius ne sont introduits.
|
||||||
|
|
||||||
|
`hardening.rs` attend désormais exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dev-dependencies = {tokio, tokio-tungstenite}
|
||||||
|
```
|
||||||
|
|
||||||
|
L’inventaire des dépendances normales reste inchangé.
|
||||||
|
|
||||||
|
## Invariants inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockSubscribe standard confirmed/full/base64 legacy-v0 -> golden RAW-direct positif
|
||||||
|
Helius transactionSubscribe full/base64 -> signal + hydration HTTP
|
||||||
|
RAW v1 bytes/hash contracts
|
||||||
|
TR-C2 Worker-owned
|
||||||
|
tokio-tungstenite strictement dev-only
|
||||||
|
aucun code de production modifié
|
||||||
|
aucune dépendance ni feature modifiée par ce fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.5.fix.2
|
||||||
|
Cargo.toml header = 498
|
||||||
|
workspace members = 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
|
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.005-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audits d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (339 table(s), 761 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gate opérateur à rejouer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres Cargo n’ont pas besoin d’être rejoués pour ce fix : aucun manifest de crate, aucune dépendance et aucune feature ne change.
|
||||||
232
deltas/0.3.10/pre.005.md
Normal file
232
deltas/0.3.10/pre.005.md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.005` — qualification RAW WS standard et Helius
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.3.10-pre.004-fix.001
|
||||||
|
workspace.package.version = 0.3.10-pre.4.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le rejeu opérateur communiqué le 2026-09-06 ferme intégralement le fix : audits statiques propres, `cargo check --workspace` PASS, Clippy strict PASS, tests common PASS, 387 tests Transport plus suites publiques/release/doctests PASS, et suite complète Backfill PASS. Les arbres Cargo n'ont pas été rejoués sur le fix car aucune dépendance ni feature n'y avait changé.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Qualifier les deux surfaces WebSocket déjà présentes sans ouvrir le Worker :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Solana standard blockSubscribe full/base64
|
||||||
|
-> prouver ou refuser la production RAW directe
|
||||||
|
|
||||||
|
Helius transactionSubscribe full/base64
|
||||||
|
-> prouver ou refuser la production RAW directe
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun adapter productif n'est placé dans Transport ou Backfill. `TR-C2` reste Worker-owned.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `495` à `496`. Le workspace reste à `20` membres.
|
||||||
|
|
||||||
|
## Qualification positive — `blockSubscribe`
|
||||||
|
|
||||||
|
Le canari cross-layer `crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs` monte un serveur HTTP local et un serveur WebSocket standard local sur une fixture commune.
|
||||||
|
|
||||||
|
Configuration qualifiée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commitment = confirmed
|
||||||
|
encoding = base64
|
||||||
|
transactionDetails = full
|
||||||
|
maxSupportedTransactionVersion = 0
|
||||||
|
showRewards = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test compare :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP getBlock observed
|
||||||
|
vs
|
||||||
|
WS blockSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
puis exige :
|
||||||
|
|
||||||
|
```text
|
||||||
|
même SolanaConfirmedBlock
|
||||||
|
même RawTransactionReference
|
||||||
|
même slot
|
||||||
|
même block_time
|
||||||
|
mêmes bytes RAW v1
|
||||||
|
même content_hash
|
||||||
|
```
|
||||||
|
|
||||||
|
La qualification directe est volontairement bornée au sous-ensemble legacy/v0 prouvé. Transaction V1 reste au gate `pre.006`.
|
||||||
|
|
||||||
|
## Qualification négative — Helius `transactionSubscribe`
|
||||||
|
|
||||||
|
Le même canari ouvre une session Helius locale et une hydration HTTP de référence pour la même transaction.
|
||||||
|
|
||||||
|
L'enveloppe full/base64 qualifiée conserve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction wire
|
||||||
|
meta
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
transactionIndex
|
||||||
|
```
|
||||||
|
|
||||||
|
mais ne porte pas, dans le contrat typé actuel :
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockTime
|
||||||
|
version
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test construit donc la meilleure projection RAW possible sans inventer ces champs et prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
même identité que HTTP
|
||||||
|
même slot que HTTP
|
||||||
|
block_time différent
|
||||||
|
bytes RAW v1 différents
|
||||||
|
content_hash différent
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision normative :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius transactionSubscribe full/base64
|
||||||
|
-> signal live riche
|
||||||
|
-> HTTP hydration obligatoire
|
||||||
|
-> persistence RAW depuis le matériau HTTP complet
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun RAW v2 automatique et aucune normalisation ad hoc ne sont introduits.
|
||||||
|
|
||||||
|
## Dependency firewall
|
||||||
|
|
||||||
|
Pour les serveurs WebSocket locaux du canari, le manifest Backfill ajoute uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-tungstenite = { workspace = true, features = ["handshake"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/dependency_boundary.rs` vérifie explicitement que `tokio-tungstenite` n'apparaît pas dans `[dependencies]` et reste dans `[dev-dependencies]`.
|
||||||
|
|
||||||
|
Il n'existe toujours aucun edge productif :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-onchain-transport-lib -> ksp-raw-transaction-lib
|
||||||
|
ksp-raw-transaction-lib -> ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
`docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md` distingue désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockSubscribe full/base64 legacy-v0 -> direct après parité prouvée
|
||||||
|
Helius transactionSubscribe full/base64 -> signal + hydration HTTP
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plan `031` et la validation `027` enregistrent la même qualification, la fermeture opérateur de `pre.004-fix.001` et le maintien de Transaction V1 dans `pre.006`.
|
||||||
|
|
||||||
|
## Fraîcheur externe revalidée
|
||||||
|
|
||||||
|
Les surfaces changeantes ont été revalidées le 2026-09-06 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Solana blockSubscribe : méthode unstable, confirmed/finalized, base64, full, maxSupportedTransactionVersion disponible
|
||||||
|
Helius transactionSubscribe : full transaction streaming disponible ; enveloppe KSP qualifiée sans blockTime/version
|
||||||
|
```
|
||||||
|
|
||||||
|
Sources documentaires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://solana.com/docs/rpc/websocket/blocksubscribe
|
||||||
|
https://solana.com/docs/rpc/json-structures
|
||||||
|
https://www.helius.dev/docs/rpc/websocket/transaction-subscribe
|
||||||
|
https://www.helius.dev/blog/introducing-next-generation-enhanced-websockets
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces faits d'accès/protocole restent documentaires et ne deviennent pas des constantes provider dans le Worker.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||||
|
deltas/0.3.10/pre.005.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations exécutées dans l'environnement d'assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
Markdown table audit: clean (339 table(s), 759 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt` ; aucun gate Cargo de `pre.005` n'est déclaré PASS localement.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres sont requis cette fois-ci parce que `pre.005` ajoute une dev-dependency et doit prouver qu'elle ne contamine pas le graphe productif.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockSubscribe full/base64 legacy-v0 est qualifié RAW-direct
|
||||||
|
Helius transactionSubscribe full/base64 n'est pas qualifié RAW-direct
|
||||||
|
Helius doit hydrater par HTTP avant persistence RAW
|
||||||
|
aucun champ manquant n'est inventé
|
||||||
|
aucun RAW v2 automatique
|
||||||
|
TR-C2 reste Worker-owned
|
||||||
|
aucun code de production ajouté dans cette tranche
|
||||||
|
Transaction V1 reste à pre.006
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque le gate déterministe de `pre.005`. `pre.006` reste dédiée aux adapters Yellowstone transactions/blocks et au gate Transaction V1 avant toute persistance directe de ces matériaux.
|
||||||
166
deltas/0.3.10/pre.006-fix.001.md
Normal file
166
deltas/0.3.10/pre.006-fix.001.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.006-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.006-fix.001` — corrections Rust du gate `pre.006` + recalibrage du sizing
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.006
|
||||||
|
```
|
||||||
|
|
||||||
|
L'archive opérateur `khadhroony-solana-project- v0.3.10-pre.006.zip` est la base autoritaire de ce correctif.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Corriger strictement les défauts Rust révélés par le premier gate opérateur de `pre.006` sans modifier le contrat RAW V1, les goldens, la décision Yellowstone ni les dépendances de production, puis corriger le plan devenu surdimensionné avant toute implémentation lourde du Worker.
|
||||||
|
|
||||||
|
Le gate opérateur a révélé trois familles de défauts dans la responsabilité `pre.006` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
clippy::collapsible_if dans la validation V1 du heap size
|
||||||
|
assert_eq! sur Result<&str, &Error> exigeant à tort PartialEq sur Error
|
||||||
|
canari Yellowstone utilisant Error::new avec un &str et formatant ErrorCode via Display inexistant
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
Le correctif modifie du Rust et synchronise donc la version technique Cargo conformément à `VER-ID-007` et `VER-ID-010` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.6.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Corrections Rust
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib/src/wire.rs` applique la forme Clippy proposée pour la garde `heap_size` V1 sans modifier les bornes ni le résultat fonctionnel.
|
||||||
|
|
||||||
|
`unit_tests/wire.rs` conserve exactement les quatre golden Base64 mais compare les succès par pattern matching, sans imposer `PartialEq` au type d'erreur KSP.
|
||||||
|
|
||||||
|
`yellowstone_raw_parity.rs` construit désormais l'erreur de projection test-only avec un `ErrorCode` structuré et formate les codes d'erreur explicitement via `domain()` + `code()`. Aucun code d'erreur public de production n'est ajouté.
|
||||||
|
|
||||||
|
## Recalibrage du sizing
|
||||||
|
|
||||||
|
Le plan initial associait dans `0.3.10` deux moitiés devenues nettement séparables :
|
||||||
|
|
||||||
|
```text
|
||||||
|
common RAW + migration/parités = déjà matérialisées en pre.002 à pre.006
|
||||||
|
runtime Worker + sources + repair = pas encore commencé
|
||||||
|
```
|
||||||
|
|
||||||
|
Les anciennes tranches `pre.007` à `pre.011` cumulaient plusieurs responsabilités chacune et ne respectaient plus le budget normatif d'environ 15 à 20 minutes. La clôture de toute la mission initiale dans une seule session devenait également incertaine.
|
||||||
|
|
||||||
|
Le redécoupage durable retenu est donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10 = common RAW + observed block + qualification HTTP/WS/Helius/Yellowstone
|
||||||
|
0.3.11 = Worker foundation/runtime + persistence déterministe
|
||||||
|
0.3.12 = Yellowstone + hydration HTTP + continuity
|
||||||
|
0.3.13 = WS/Helius/HTTP live + convergence multi-source
|
||||||
|
0.3.14 = gap repair/hardening + smokes provider accessibles
|
||||||
|
0.3.15 = Desk d'ingestion
|
||||||
|
0.3.16 = Backfill multi-source/multi-stratégie
|
||||||
|
```
|
||||||
|
|
||||||
|
La queue de fermeture de `0.3.10` devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.007 = gate technique final, budget 15-20 min
|
||||||
|
pre.008 = réconciliation documentaire, budget 10-15 min
|
||||||
|
pre.009 = préparation de publication, budget 5-10 min
|
||||||
|
rel.001 = publication stable mécanique
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque nouvelle release Worker recommencera par sa propre `pre.001` de sizing et devra scinder toute prerelease estimée au-delà du budget.
|
||||||
|
|
||||||
|
## Vérification de l'archive
|
||||||
|
|
||||||
|
Exécuté sur l'archive autoritaire avant modification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
unzip -t : PASS
|
||||||
|
racine ZIP unique : PASS
|
||||||
|
chemins absolus/traversal : 0
|
||||||
|
symlinks : 0
|
||||||
|
workspace.package.version base : 0.3.10-pre.6
|
||||||
|
prompt 029 présent : PASS
|
||||||
|
règles obligatoires présentes : PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations exécutées
|
||||||
|
|
||||||
|
Après les corrections Rust et le recalibrage documentaire :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats :
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit : clean
|
||||||
|
Rust export completeness audit : 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit : clean
|
||||||
|
Markdown table audit : clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Les contrôles structurels ont également vérifié que le delta ne contient aucun chemin d'archive dangereux et que les références durables principales du nouveau découpage sont cohérentes.
|
||||||
|
|
||||||
|
## Validations non exécutées dans l'environnement d'assemblage
|
||||||
|
|
||||||
|
L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Les commandes suivantes ne sont donc **pas** déclarées PASS ici :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Elles constituent le rejeu opérateur requis pour fermer le fix.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.006-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/wire.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||||
|
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||||
|
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||||
|
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
|
||||||
|
docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
- Les goldens Legacy/V0/V1 et le contrat RAW V1 de `pre.006` ne changent pas.
|
||||||
|
- Le canari Yellowstone reste test-only et n'ajoute aucun nouvel edge productif.
|
||||||
|
- `0.3.10` est réduit à la common RAW et aux qualifications cross-source déjà matérialisées avant ouverture du runtime Worker.
|
||||||
|
- Le Worker concret est réparti sur `0.3.11` à `0.3.14` afin de respecter une release concrète par session et des prereleases bornées.
|
||||||
|
- La Desk et le Backfill multi-source sont décalés respectivement à `0.3.15` et `0.3.16`.
|
||||||
|
- Les anciens prompts/plans de releases déjà ouvertes restent des traces historiques et ne sont pas réécrits rétroactivement uniquement pour refléter ce recalibrage.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question fonctionnelle nouvelle pour ce fix. Le prochain gate est le rejeu Rust opérateur du correctif, puis `pre.007` si ce rejeu est vert.
|
||||||
118
deltas/0.3.10/pre.006-fix.002.md
Normal file
118
deltas/0.3.10/pre.006-fix.002.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.006-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.006-fix.002` — rustdoc Yellowstone + réconciliation architecture/prompt handoff
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.006-fix.001 appliquée
|
||||||
|
workspace.package.version = 0.3.10-pre.6.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur du 7 septembre 2026 confirme que les erreurs Rust corrigées par `fix.001` ont disparu. Le seul blocage restant sous Clippy strict est le lint crate-level `missing_docs` de `crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs`.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
Le correctif modifie un test Rust compilé. Conformément à `VER-ID-007` / `VER-ID-010` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.6.fix.2
|
||||||
|
commit attendu = v0.3.10-pre.006-fix.002
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune version npm/Tauri n'est modifiée : aucune application Desk n'est touchée.
|
||||||
|
|
||||||
|
## 3. Correction Rust
|
||||||
|
|
||||||
|
`tests/yellowstone_raw_parity.rs` reçoit la rustdoc crate-level manquante :
|
||||||
|
|
||||||
|
```text
|
||||||
|
//! Deterministic local Yellowstone gRPC parity canary for source-neutral RAW transaction v1 projection.
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `#[allow(missing_docs)]` n'est ajouté. Le serveur fixture, la projection Yellowstone, les goldens, les assertions et les dépendances restent inchangés.
|
||||||
|
|
||||||
|
## 4. Inventaire des composants
|
||||||
|
|
||||||
|
`docs/architecture/004-COMPONENT_INVENTORY.md` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
+ ksp-raw-transaction-lib comme common RAW Transaction implémentée en 0.3.10
|
||||||
|
CORE processor -> STRUCTURAL job
|
||||||
|
CORE worker -> STRUCTURAL worker
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Job STRUCTURAL représente le traitement borné/rejouable RAW -> CORE ; le Worker STRUCTURAL représente le service continu backlog RAW -> CORE. Les références durables correspondantes dans `009-ACQUISITION_WORKERS_AND_JOBS.md`, `010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md` et la séquence fonctionnelle active sont synchronisées. Les noms/packagings concrets restent à fixer lors de l'ouverture de la couche CORE.
|
||||||
|
|
||||||
|
## 5. Graphe de dépendances
|
||||||
|
|
||||||
|
`docs/architecture/005-DEPENDENCY_GRAPH.md` matérialise la lower layer réelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib
|
||||||
|
-> ksp-store-api
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> base64 / serde_json / sha2
|
||||||
|
|
||||||
|
-X-> Transport
|
||||||
|
-X-> ksp-store-lib
|
||||||
|
-X-> Job / Worker
|
||||||
|
-X-> Config / runtime async / backend Store
|
||||||
|
```
|
||||||
|
|
||||||
|
Le graphe Backfill ajoute son edge réel vers `ksp-raw-transaction-lib`, et le graphe du futur Worker réserve la même common crate avant la persistence par `ksp-store-lib`.
|
||||||
|
|
||||||
|
## 6. Note de génération des prompts `0.3.12` à `0.3.15`
|
||||||
|
|
||||||
|
Le plan `031` n'introduit pas quatre prompts prématurés. Il fixe un handoff séquentiel :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.11 ferme -> génère le prompt 0.3.12
|
||||||
|
0.3.12 ferme -> génère le prompt 0.3.13
|
||||||
|
0.3.13 ferme -> génère le prompt 0.3.14
|
||||||
|
0.3.14 ferme -> génère le prompt 0.3.15
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque prompt doit partir de la base stable réellement obtenue, refaire son audit/sizing `pre.001`, rester sous le budget 15–20 min par tranche et rescinder sa release si la clôture dans une session devient incertaine. La note transfère les missions Yellowstone, WS/Helius/HTTP, gap repair/hardening puis Desk sans les figer comme vérité future.
|
||||||
|
|
||||||
|
Le plan corrige aussi l'ancienne référence résiduelle de la Desk `0.3.11` vers sa cible recalibrée `0.3.15`.
|
||||||
|
|
||||||
|
## 7. Validation opérateur requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune commande ne doit être déclarée PASS avant exécution réelle. Si ce gate est vert, `pre.006-fix.002` ferme le défaut technique restant de `pre.006` et `pre.007` peut rester le gate technique final recalibré de `0.3.10`.
|
||||||
|
|
||||||
|
## 8. Périmètre du delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||||
|
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||||
|
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
|
||||||
|
docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
deltas/0.3.10/pre.006-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun code de production, manifest de crate, dépendance, feature, golden RAW, Config, Store schema, frontend ou application n'est modifié.
|
||||||
206
deltas/0.3.10/pre.006.md
Normal file
206
deltas/0.3.10/pre.006.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.006` — Transaction V1 + parité Yellowstone transaction/block
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.3.10-pre.005-fix.002
|
||||||
|
workspace.package.version = 0.3.10-pre.5.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur communiqué le 2026-09-06 ferme cette baseline : audits statiques propres, `cargo check --workspace` PASS, Clippy strict PASS, common RAW PASS, 387 tests Transport avec suites publiques/release/doctests PASS, et suite Backfill complète PASS, y compris hardening et les deux canaris WS/Helius.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Fermer avant le Worker concret :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction V1 / SIMD-0385 dans la common source-neutral
|
||||||
|
Yellowstone Transaction -> transaction wire exact
|
||||||
|
Yellowstone Block -> même transaction wire + block_time
|
||||||
|
qualification RAW-direct vs hydration
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun adapter productif `Transport DTO -> common` n'est ajouté. `TR-C2` reste Worker-owned et `pre.007` reste le premier point où la crate Worker concrète peut apparaître.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.6
|
||||||
|
Cargo.toml header = 499
|
||||||
|
workspace members = 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common wire Legacy/V0/V1
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib` ajoute un modèle source-neutral :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RawSolanaMessageVersion
|
||||||
|
RawSolanaMessageHeader
|
||||||
|
RawSolanaCompiledInstruction
|
||||||
|
RawSolanaAddressTableLookup
|
||||||
|
RawSolanaTransactionConfig
|
||||||
|
RawSolanaTransactionMessage
|
||||||
|
RawSolanaTransactionWire
|
||||||
|
```
|
||||||
|
|
||||||
|
et deux serializers :
|
||||||
|
|
||||||
|
```text
|
||||||
|
serialize_solana_transaction_wire
|
||||||
|
serialize_solana_transaction_wire_base64
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy et V0 conservent le layout signatures-first existant. V1 suit SIMD-0385 : `0x81`, header, config mask, lifetime, compteurs, adresses, valeurs config, headers/payloads d'instructions et signatures terminales.
|
||||||
|
|
||||||
|
Les gardes V1 verrouillent notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction <= 4096 bytes
|
||||||
|
1..=12 signatures
|
||||||
|
<= 64 adresses
|
||||||
|
<= 64 instructions
|
||||||
|
num_readonly_signed < num_required_signatures
|
||||||
|
indexes programme/comptes bornés
|
||||||
|
aucune Address Lookup Table
|
||||||
|
aucune adresse dupliquée
|
||||||
|
config présente, y compris vide
|
||||||
|
aucun trailing byte après les signatures
|
||||||
|
```
|
||||||
|
|
||||||
|
Les goldens couvrent Legacy, V0, V1 configuration complète et V1 configuration vide.
|
||||||
|
|
||||||
|
## Extraction de signature V1
|
||||||
|
|
||||||
|
`extract_raw_transaction_signature_from_binary_base64` distingue désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
premier octet == 0x81 -> parser V1 et signatures terminales
|
||||||
|
sinon -> parser short-vector signatures-first Legacy/V0
|
||||||
|
```
|
||||||
|
|
||||||
|
Le parser V1 parcourt structurellement mask/config, adresses, headers, payloads et signatures et exige la fin exacte du wire.
|
||||||
|
|
||||||
|
## Parité Yellowstone
|
||||||
|
|
||||||
|
`crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs` monte un serveur Geyser local déterministe, ouvre le vrai `YellowstoneGrpcChannel::open_standard_subscribe` puis reçoit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SubscribeUpdateTransaction V1
|
||||||
|
SubscribeUpdateBlock V1 portant la même transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
La projection vers `RawSolanaTransactionWire` est strictement test-only. Le canari exige le même wire V1 exact pour les deux updates et vérifie slot, transaction index et `block_time` côté Block.
|
||||||
|
|
||||||
|
## Qualification RAW
|
||||||
|
|
||||||
|
Le gate ferme volontairement une qualification partielle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone transaction wire = direct/fiable
|
||||||
|
Yellowstone Transaction RAW = hydration HTTP obligatoire : block_time absent
|
||||||
|
Yellowstone Block RAW complet= hydration HTTP obligatoire : meta JSON byte-identical non prouvée
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun champ manquant n'est inventé et aucune projection protobuf -> JSON ad hoc n'est introduite pour forcer un hash RAW identique.
|
||||||
|
|
||||||
|
## Dependency firewall
|
||||||
|
|
||||||
|
Le Backfill ajoute uniquement en dev :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`dependency_boundary.rs` verrouille leur absence des dépendances production et `hardening.rs` recalibre le set dev exact. Les dépendances normales restent inchangées.
|
||||||
|
|
||||||
|
Il n'existe toujours aucun edge productif :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-onchain-transport-lib -> ksp-raw-transaction-lib
|
||||||
|
ksp-raw-transaction-lib -> ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fraîcheur externe revalidée
|
||||||
|
|
||||||
|
Revalidation le 2026-09-07 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SIMD-0385 : Transaction V1, status Review, version byte 129, signatures terminales, sans ALT
|
||||||
|
Solana Transaction V1 examples : capacité Yellowstone avec Message.config ; activation cluster encore feature-gated/pre-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Sources :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md
|
||||||
|
https://github.com/solana-foundation/transaction-v1-examples
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP implémente la capacité de lecture/canonicalisation V1 sans déclarer son activation sur Mainnet ou un autre cluster.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-job-backfill-lib/tests/yellowstone_raw_parity.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/wire.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/wire.rs
|
||||||
|
deltas/0.3.10/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/lib.rs
|
||||||
|
crates/ksp-raw-transaction-lib/src/signature.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/dependency_boundary.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/public_api.rs
|
||||||
|
crates/ksp-raw-transaction-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/signature.rs
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Les arbres Cargo sont requis car `pre.006` ajoute deux dev-dependencies test-only.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
common sérialise Legacy/V0/V1 sans dépendre de Transport
|
||||||
|
V1 est reconnu par 0x81 et possède des signatures terminales
|
||||||
|
Yellowstone Transaction/Block conservent un transaction wire V1 exact sur canari
|
||||||
|
Yellowstone RAW complet reste hydraté par HTTP tant que TR-C4 meta/block_time complet n'est pas prouvé
|
||||||
|
aucun RAW v2 automatique
|
||||||
|
aucun Worker concret avant pre.007
|
||||||
|
TR-C2 reste Worker-owned
|
||||||
|
```
|
||||||
121
deltas/0.3.10/pre.007.md
Normal file
121
deltas/0.3.10/pre.007.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.007.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.007` — gate technique final RAW commune / cross-source
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.006-fix.002 appliquée
|
||||||
|
workspace.package.version = 0.3.10-pre.6.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le rejeu opérateur communiqué le 8 septembre 2026 ferme entièrement `pre.006-fix.002` : audits Rust/Markdown, `cargo check --workspace`, Clippy workspace `--all-targets --all-features -- -D warnings`, puis les suites `ksp-raw-transaction-lib`, `ksp-onchain-transport-lib` et `ksp-job-backfill-lib` sont PASS.
|
||||||
|
|
||||||
|
Les smokes réseau Transport opt-in restent `ignored` par politique ; aucun succès live n'est revendiqué à partir de ces tests non exécutés.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Ouvrir exclusivement le gate technique final de `0.3.10` sans nouveau développement fonctionnel et sans effectuer la réconciliation documentaire réservée à `pre.008`.
|
||||||
|
|
||||||
|
Cette tranche fige l'état à tester sous l'identifiant :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate vert de `pre.006-fix.002` est une précondition. Il ne remplace jamais la preuve finale sur cette version exacte.
|
||||||
|
|
||||||
|
## 3. Version
|
||||||
|
|
||||||
|
Cette livraison est une prerelease non-fix. Conformément à la politique de version KSP :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.7
|
||||||
|
commit attendu = v0.3.10-pre.007
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune version npm/Tauri n'est modifiée.
|
||||||
|
|
||||||
|
## 4. Périmètre
|
||||||
|
|
||||||
|
Aucun changement de code métier, API, golden RAW, dépendance, feature, Config, Store, Transport, Backfill ou frontend.
|
||||||
|
|
||||||
|
Le seul ajustement de validation corrige une référence devenue obsolète après le redécoupage de `pre.006` : `ksp-worker-raw-transaction-ingest-lib` n'existe pas dans `0.3.10` et appartient désormais à `0.3.11`. Le gate final utilise donc les graphes réellement pertinents de la common RAW et du Backfill.
|
||||||
|
|
||||||
|
## 5. Gate déterministe final
|
||||||
|
|
||||||
|
À exécuter séquentiellement sur `0.3.10-pre.7` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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
|
||||||
|
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges dev
|
||||||
|
cargo tree -p ksp-job-backfill-lib -e features
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate est séquentiel : tout échec bloque `pre.007`, même si des commandes suivantes réussissent. Un défaut découvert ouvre `pre.007-fix.NNN` avant toute poursuite vers `pre.008`.
|
||||||
|
|
||||||
|
## 6. Non-claims live
|
||||||
|
|
||||||
|
Cette tranche n'ajoute aucun nouveau smoke réseau. Les smokes déjà présents restent opt-in selon leurs politiques respectives. En particulier, un test `ignored` n'est pas déclaré PASS live.
|
||||||
|
|
||||||
|
La qualification déterministe acquise reste inchangée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP / WS standard / Helius / Yellowstone : preuves fixture/parité existantes
|
||||||
|
Yellowstone RAW complet : hydration HTTP conservée selon qualification actuelle
|
||||||
|
Helius transactionSubscribe RAW complet : hydration HTTP conservée selon qualification actuelle
|
||||||
|
Worker concret : hors 0.3.10, transféré à 0.3.11+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.007.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Validation de l'environnement d'assemblage
|
||||||
|
|
||||||
|
À enregistrer avant livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
contrôle statique version/scope/header
|
||||||
|
contrôle archive delta et reconstruction overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Cargo de `pre.007` n'est déclaré PASS ici.
|
||||||
|
|
||||||
|
## 10. Suite
|
||||||
|
|
||||||
|
Après retour opérateur entièrement vert sur `0.3.10-pre.7` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.008 = réconciliation documentaire finale 0.3.10
|
||||||
|
pre.009 = préparation publication + prompt 0.3.11
|
||||||
|
rel.001 = publication stable mécanique
|
||||||
|
```
|
||||||
109
deltas/0.3.10/pre.008-fix.001.md
Normal file
109
deltas/0.3.10/pre.008-fix.001.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.008-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.008-fix.001` — fermeture de la nomenclature D1–D4
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.008
|
||||||
|
workspace.package.version = 0.3.10-pre.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.008` est vert : format check, audits Rust/Markdown, `cargo check --workspace` et Clippy workspace/all-targets/all-features avec `-D warnings` passent sur cette baseline.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Corriger les dernières survivances actives de l'ancienne notation des couches de données découvertes pendant la préparation de `pre.009`, sans les absorber dans le couloir de publication.
|
||||||
|
|
||||||
|
`pre.008` a réservé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1–N4 = niveaux architecturaux de composants
|
||||||
|
D1–D4 = couches durables Store/data
|
||||||
|
|
||||||
|
D1 RAW
|
||||||
|
D2 STRUCTURAL
|
||||||
|
D3 DECODED
|
||||||
|
D4 DOMAIN
|
||||||
|
```
|
||||||
|
|
||||||
|
La correction reste donc strictement dans la responsabilité de réconciliation documentaire de `pre.008`.
|
||||||
|
|
||||||
|
## 3. Version Cargo
|
||||||
|
|
||||||
|
Correction strictement documentaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version reste 0.3.10-pre.8
|
||||||
|
Cargo.toml inchangé
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun identifiant `fix` n'est reflété dans Cargo pour ce correctif documentaire.
|
||||||
|
|
||||||
|
## 4. Corrections
|
||||||
|
|
||||||
|
### `ROADMAP.md`
|
||||||
|
|
||||||
|
Les usages actifs qui désignaient encore les couches de données avec `N1`, `N2` ou `N3` deviennent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 data/event-only -> D1
|
||||||
|
« taxonomie N1 » -> « taxonomie D1 »
|
||||||
|
« nouvelle famille N1 » -> « nouvelle famille D1 »
|
||||||
|
« ouverture N2/N3 » -> « ouverture D2/D3 »
|
||||||
|
« autres familles N1 » -> « autres familles D1 »
|
||||||
|
```
|
||||||
|
|
||||||
|
Les occurrences où `N1–N4` désignent réellement les niveaux architecturaux restent inchangées.
|
||||||
|
|
||||||
|
### `docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md`
|
||||||
|
|
||||||
|
La mention active « migration de données N1 » devient « migration de données D1 ».
|
||||||
|
|
||||||
|
Les anciennes traces historiques de releases, prompts, plans ou validations clôturés ne sont pas réécrites.
|
||||||
|
|
||||||
|
## 5. Hors périmètre
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
CHANGELOG.md
|
||||||
|
prompt 0.3.11
|
||||||
|
code / tests / Config / schemas
|
||||||
|
README / USAGE
|
||||||
|
plans / validations
|
||||||
|
autres corrections de publication
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.009` reste la prochaine tranche et conservera uniquement CHANGELOG, ROADMAP, prompt suivant et fichiers mécaniques.
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
ROADMAP.md
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/pre.008-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Comme le correctif est exclusivement documentaire et ne touche pas Cargo, Rust, build, runtime ou Config, aucun replay du workspace complet n'est requis si ces audits restent verts.
|
||||||
|
|
||||||
|
## 9. Suite
|
||||||
|
|
||||||
|
Après application et audits documentaires verts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.009 — préparation de publication + prompt 0.3.11
|
||||||
|
0.3.10-rel.001 — publication stable mécanique
|
||||||
|
```
|
||||||
201
deltas/0.3.10/pre.008.md
Normal file
201
deltas/0.3.10/pre.008.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.008.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.008` — réconciliation documentaire finale RAW commune / cross-source
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.007 appliquée
|
||||||
|
workspace.package.version = 0.3.10-pre.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Le rejeu opérateur communiqué le 8 septembre 2026 ferme entièrement le gate technique final de `pre.007` : audits Rust/Markdown, `cargo check --workspace`, Clippy workspace strict, `cargo test --workspace --all-targets --all-features`, suites ciblées common/Transport/Backfill et graphes Cargo demandés sont exécutés sans échec.
|
||||||
|
|
||||||
|
Les smokes live opt-in restent `ignored` par politique et ne sont pas revendiqués comme preuves réseau exécutées.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Effectuer exclusivement la réconciliation documentaire finale de `0.3.10` après fermeture technique de `pre.007`, sans nouveau développement fonctionnel.
|
||||||
|
|
||||||
|
Cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
réconcilie la nomenclature durable des couches de données Store ;
|
||||||
|
synchronise la séquence fonctionnelle active avec le redécoupage 0.3.10 -> 0.3.16 ;
|
||||||
|
réconcilie les règles et documents d'architecture actifs ;
|
||||||
|
complète la documentation durable de ksp-raw-transaction-lib ;
|
||||||
|
ne modifie aucun comportement Rust, API, dépendance, feature ou runtime.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Version
|
||||||
|
|
||||||
|
Cette livraison est une prerelease non-fix :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.8
|
||||||
|
commit attendu = v0.3.10-pre.008
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune version npm/Tauri n'est modifiée.
|
||||||
|
|
||||||
|
## 4. Nomenclature canonique des couches de données
|
||||||
|
|
||||||
|
La progression durable du Store devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN
|
||||||
|
```
|
||||||
|
|
||||||
|
Sémantique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
D1 RAW = matériau acquis et conservé avant décomposition métier ;
|
||||||
|
D2 STRUCTURAL = décomposition Solana générique en structures unitaires exploitables ;
|
||||||
|
D3 DECODED = décodage générique des structures selon les contrats de programmes ;
|
||||||
|
D4 DOMAIN = matérialisation métier/spécialisée issue des données décodées.
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ancien nom de couche D2 `CORE` est abandonné. Il créait une ambiguïté avec `ksp-core-lib` et le domaine Core fondamental alors que cette couche Store représente une décomposition structurelle du RAW.
|
||||||
|
|
||||||
|
Cette normalisation ne renomme jamais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-core-lib ;
|
||||||
|
le domaine Core architectural fondamental ;
|
||||||
|
les noms propres comme « Solana Core Programs » ;
|
||||||
|
les traces historiques clôturées qui documentent l'ancienne terminologie.
|
||||||
|
```
|
||||||
|
|
||||||
|
Les niveaux architecturaux `N1–N4` de `docs/architecture/002-LAYERS_AND_DEPENDENCIES.md` restent réservés aux niveaux de composants. Les couches de données utilisent désormais `D1–D4` afin d'éviter toute collision de vocabulaire. Les plans Store historiques qui parlaient de N1–N4 data restent interprétables selon la correspondance ci-dessus mais ne sont pas réécrits en masse.
|
||||||
|
|
||||||
|
## 5. Conséquences STRUCTURAL
|
||||||
|
|
||||||
|
Les rôles actifs deviennent explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
STRUCTURAL persistence
|
||||||
|
RAW -> STRUCTURAL replay/backfill
|
||||||
|
STRUCTURAL job
|
||||||
|
STRUCTURAL worker
|
||||||
|
STRUCTURAL inspection/control app lorsque pertinent
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `STRUCTURAL job` est borné et rejouable. Le `STRUCTURAL worker` traite continuellement le backlog RAW. Aucun des deux ne réalise le décodage Program : la frontière `STRUCTURAL -> DECODED` reste en aval.
|
||||||
|
|
||||||
|
## 6. Séquence fonctionnelle active
|
||||||
|
|
||||||
|
`docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` est remis en cohérence avec la trajectoire réellement matérialisée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.1 ksp-store-api RAW
|
||||||
|
0.3.2 Store runtime + backend PostgreSQL foundation
|
||||||
|
0.3.3 persistence RawTransaction
|
||||||
|
0.3.4 persistence RawAccountState
|
||||||
|
0.3.5 Interface acquisition events
|
||||||
|
0.3.6 ksp-job-api + RawTransaction Backfill
|
||||||
|
0.3.7 ksp-app-backfill-desk
|
||||||
|
0.3.8 ksp-app-store-desk RAW
|
||||||
|
0.3.9 ksp-worker-api + audit acquisition RawTransaction
|
||||||
|
0.3.10 common ksp-raw-transaction-lib + preuves cross-source
|
||||||
|
0.3.11-0.3.14 Worker RAW ingest découpé par responsabilité
|
||||||
|
0.3.15 ksp-app-raw-transaction-ingest-desk
|
||||||
|
0.3.16 Backfill multi-source / multi-stratégie
|
||||||
|
```
|
||||||
|
|
||||||
|
Le redécoupage ne change pas les frontières Job/Worker arrêtées précédemment et ne réintroduit aucun Worker concret dans `0.3.10`.
|
||||||
|
|
||||||
|
## 7. Documentation de `ksp-raw-transaction-lib`
|
||||||
|
|
||||||
|
La crate common RAW étant désormais une bibliothèque complétée de `0.3.10`, elle reçoit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-raw-transaction-lib/README.md
|
||||||
|
crates/ksp-raw-transaction-lib/USAGE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `README.md` fixe son rôle, ses dépendances autorisées et ses frontières. Le `USAGE.md` reste volontairement version-neutre et montre uniquement l'utilisation durable de la surface publique de crate root : signatures, wire Solana, canonicalisation RAW et assemblage acquisition/observation.
|
||||||
|
|
||||||
|
La documentation rappelle explicitement que le résultat de cette crate reste D1 RAW et qu'elle n'implémente pas la transformation RAW -> STRUCTURAL.
|
||||||
|
|
||||||
|
## 8. ROADMAP et publication
|
||||||
|
|
||||||
|
`ROADMAP.md` est modifié uniquement pour éliminer l'ancien libellé actif `RAW/CORE/DECODE/SPECIALIZED` au profit de `RAW/STRUCTURAL/DECODED/DOMAIN`.
|
||||||
|
|
||||||
|
Les changements de statut de release, le CHANGELOG et la préparation du prompt `0.3.11` restent réservés à `pre.009`.
|
||||||
|
|
||||||
|
## 9. Traces historiques
|
||||||
|
|
||||||
|
Les prompts, deltas, plans et validations clôturés ne sont pas réécrits en masse. Une ancienne occurrence de `CORE` dans une trace historique peut donc rester légitime lorsqu'elle décrit l'état documentaire de sa livraison.
|
||||||
|
|
||||||
|
Cette tranche corrige les documents durables actifs, pas l'histoire du repository.
|
||||||
|
|
||||||
|
## 10. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
docs/IDEAS.md
|
||||||
|
docs/architecture/002-LAYERS_AND_DEPENDENCIES.md
|
||||||
|
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||||
|
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||||
|
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||||
|
docs/architecture/006-WIRE_AND_PROGRAM.md
|
||||||
|
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||||
|
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
|
||||||
|
docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/rules/RULES_DEPENDENCIES.md
|
||||||
|
docs/rules/RULES_KSP.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-raw-transaction-lib/README.md
|
||||||
|
crates/ksp-raw-transaction-lib/USAGE.md
|
||||||
|
deltas/0.3.10/pre.008.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun fichier Rust source, manifest de crate, Config, frontend ou dépendance n'est modifié.
|
||||||
|
|
||||||
|
## 12. Validation d'assemblage
|
||||||
|
|
||||||
|
À enregistrer avant livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
contrôle statique nomenclature D1-D4 / STRUCTURAL
|
||||||
|
contrôle statique version/scope/headers
|
||||||
|
contrôle exact de l'inventaire delta
|
||||||
|
contrôle archive delta et reconstruction overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Cargo de `pre.008` n'est déclaré PASS localement. Le gate technique complet de `pre.007` reste la baseline opérateur fermée ; `pre.008` ne modifie aucun code ni graphe de dépendances.
|
||||||
|
|
||||||
|
## 13. Rejeu opérateur `pre.008`
|
||||||
|
|
||||||
|
Comme cette tranche est documentaire hors incrément mécanique de version workspace, le rejeu minimal attendu est :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
Un échec ouvre `pre.008-fix.NNN`. Aucun nouveau test fonctionnel ou smoke live n'est requis par cette réconciliation documentaire seule.
|
||||||
|
|
||||||
|
## 14. Suite
|
||||||
|
|
||||||
|
Après retour opérateur vert sur `0.3.10-pre.8` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.009 = préparation publication, CHANGELOG, statuts ROADMAP et prompt 0.3.11
|
||||||
|
rel.001 = publication stable mécanique de 0.3.10
|
||||||
|
```
|
||||||
188
deltas/0.3.10/pre.009.md
Normal file
188
deltas/0.3.10/pre.009.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<!-- file: deltas/0.3.10/pre.009.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-pre.009` — préparation de publication
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.008 appliquée
|
||||||
|
0.3.10-pre.008-fix.001 appliqué
|
||||||
|
workspace.package.version = 0.3.10-pre.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.008` est vert sur format, audits Rust/Markdown, `cargo check --workspace` et Clippy workspace/all-targets/all-features avec `-D warnings`. Le correctif documentaire `pre.008-fix.001` est ensuite validé par les audits Rust/Markdown, tous deux verts.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Préparer exclusivement la publication stable de `0.3.10` après fermeture technique `pre.007` et réconciliation documentaire `pre.008` + `pre.008-fix.001`.
|
||||||
|
|
||||||
|
Conformément à `PROMPT_STRUCTURE.md` et `VER-LIFECYCLE-003`, cette tranche ne modifie fonctionnellement que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompt de démarrage 0.3.11
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
avec les fichiers mécaniques obligatoires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
deltas/0.3.10/pre.009.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10-pre.9
|
||||||
|
delivery = 0.3.10-pre.009
|
||||||
|
commit attendu = v0.3.10-pre.009
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header du `Cargo.toml` racine passe de `503` à `504`.
|
||||||
|
|
||||||
|
## 4. CHANGELOG
|
||||||
|
|
||||||
|
`CHANGELOG.md` reçoit la synthèse stable de `0.3.10` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib comme common D1 RAW source-neutral
|
||||||
|
migration Backfill sans changement des golden bytes/hash
|
||||||
|
wire Solana Legacy/V0/V1
|
||||||
|
get_block_observed
|
||||||
|
qualification HTTP / WS standard / Helius / Yellowstone
|
||||||
|
non-claims explicites lorsqu'une hydration reste nécessaire
|
||||||
|
gate technique final pre.007
|
||||||
|
nomenclature D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN
|
||||||
|
redécoupage Worker 0.3.11 -> 0.3.14
|
||||||
|
handoff 0.3.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle preuve live n'est inventée par la synthèse.
|
||||||
|
|
||||||
|
## 5. ROADMAP
|
||||||
|
|
||||||
|
`ROADMAP.md` marque `0.3.10` comme réalisé et remplace son texte prospectif par le résultat effectivement livré. Les releases `0.3.11` à `0.3.16` restent prévues et conservent leurs frontières déjà réconciliées.
|
||||||
|
|
||||||
|
Aucune autre réconciliation documentaire n'appartient à cette tranche.
|
||||||
|
|
||||||
|
## 6. Prompt `0.3.11`
|
||||||
|
|
||||||
|
Ajout :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/030-V0_3_11_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt ouvre `0.3.11` uniquement depuis `v0.3.10` et impose un `pre.001` audit/brainstorming/sizing avant codage lourd.
|
||||||
|
|
||||||
|
Mission initiale à réauditer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-worker-raw-transaction-ingest-lib
|
||||||
|
crate/dependency firewall
|
||||||
|
settings source-neutral
|
||||||
|
handle/start-stop/lifecycle
|
||||||
|
supervisor privé
|
||||||
|
channels bornés
|
||||||
|
pipeline déterministe common RAW -> Store
|
||||||
|
persistence/deduplication/content conflict
|
||||||
|
snapshots latest-value sûrs
|
||||||
|
hardening runtime de fondation
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune voie Yellowstone/WS/Helius/HTTP live complexe n'est requise pour fermer `0.3.11`. Le prompt impose de rescinder la release avant implémentation si son sizing réel ne tient pas dans une session.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/030-V0_3_11_START_PROMPT.md
|
||||||
|
deltas/0.3.10/pre.009.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Hors périmètre
|
||||||
|
|
||||||
|
Aucun fichier suivant ne doit être corrigé dans `pre.009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
README / USAGE
|
||||||
|
architecture
|
||||||
|
plans / validations
|
||||||
|
code Rust / TypeScript
|
||||||
|
tests
|
||||||
|
Config / schemas
|
||||||
|
migrations
|
||||||
|
manifest de crate
|
||||||
|
dépendances/features
|
||||||
|
runtime/backend
|
||||||
|
smokes live
|
||||||
|
```
|
||||||
|
|
||||||
|
Toute anomalie appartenant à un couloir antérieur renvoie vers une tranche dédiée avant publication.
|
||||||
|
|
||||||
|
## 11. Validations exécutées à l'assemblage
|
||||||
|
|
||||||
|
Résultats réellement obtenus avant empaquetage :
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (340 table(s), 771 file(s))
|
||||||
|
contrôle du scope: exactement cinq fichiers ajoutés/modifiés
|
||||||
|
unzip -t: PASS
|
||||||
|
reconstruction overlay: 1713 fichiers, 0 différence byte
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo/rustc/rustfmt ne sont pas disponibles dans l'environnement d'assemblage ; aucun gate Rust local n'est déclaré PASS.
|
||||||
|
|
||||||
|
## 12. Validation opérateur attendue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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
|
||||||
|
cargo check --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Comme la tranche ne change aucun code ni dépendance, ce gate de publication reste volontairement minimal. Un échec ouvre `pre.009-fix.NNN` seulement s'il relève du CHANGELOG, ROADMAP, prompt ou de la mécanique de publication ; une anomalie de code/documentation durable antérieure retourne dans son couloir propre.
|
||||||
|
|
||||||
|
## 13. Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10 est fermé fonctionnellement autour de la common RAW et de ses preuves cross-source
|
||||||
|
le Worker concret commence en 0.3.11
|
||||||
|
0.3.11 n'exige aucune source live complexe pour sa clôture
|
||||||
|
pre.001 de 0.3.11 doit re-sizer la release avant codage lourd
|
||||||
|
D1/D2/D3/D4 et STRUCTURAL restent la terminologie durable des couches Store
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question de `0.3.10` ne doit être résolue dans `pre.009`. Les choix exacts de runtime/settings/supervisor/harness du Worker sont transférés au gate `0.3.11-pre.001`.
|
||||||
|
|
||||||
|
## 15. Suite
|
||||||
|
|
||||||
|
Si le gate opérateur de cette tranche est vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
doit rester une publication stable mécanique : version Cargo finale `0.3.10`, delta `rel.001`, commit de release puis tag stable `v0.3.10`, sans rattrapage fonctionnel ou documentaire.
|
||||||
184
deltas/0.3.10/rel.001.md
Normal file
184
deltas/0.3.10/rel.001.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<!-- file: deltas/0.3.10/rel.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.10-rel.001` — publication stable
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Base directe attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.009
|
||||||
|
workspace.package.version = 0.3.10-pre.9
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate minimal opérateur de `pre.009`, exécuté le **8 septembre 2026**, est propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all -- --check: PASS
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (340 table(s), 771 file(s))
|
||||||
|
cargo check --workspace: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les couloirs de fermeture antérieurs sont déjà fermés :
|
||||||
|
|
||||||
|
- `pre.007` : gate technique final sur `0.3.10-pre.7`, avec audits Rust/Markdown, `cargo check`, Clippy workspace/all-targets/all-features `-D warnings`, workspace tests complets, suites common/Transport/Backfill et graphes Cargo propres ;
|
||||||
|
- `pre.008` puis `pre.008-fix.001` : réconciliation documentaire finale, terminologie Store `D1 RAW -> D2 STRUCTURAL -> D3 DECODED -> D4 DOMAIN`, réservation de `N1–N4` aux niveaux architecturaux et documentation durable de `ksp-raw-transaction-lib` ;
|
||||||
|
- `pre.009` : préparation minimale de publication avec `CHANGELOG.md`, `ROADMAP.md` et `prompts/030-V0_3_11_START_PROMPT.md`.
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Publier mécaniquement la version stable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Conformément à `VER-LIFECYCLE-012`, cette tranche ne corrige ni code, ni test, ni documentation durable, ni architecture, ni configuration, ni dépendance, ni prompt.
|
||||||
|
|
||||||
|
Tout défaut nouveau renvoie vers une prerelease appropriée ; `rel.001` n'est pas une tranche de rattrapage.
|
||||||
|
|
||||||
|
## Modification
|
||||||
|
|
||||||
|
### `Cargo.toml`
|
||||||
|
|
||||||
|
Le header est incrémenté parce que le fichier est réellement modifié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
504 -> 505
|
||||||
|
```
|
||||||
|
|
||||||
|
La version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.10-pre.9 -> 0.3.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune autre ligne du `Cargo.toml` racine n'est modifiée.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.10/rel.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Surfaces explicitement inchangées
|
||||||
|
|
||||||
|
```text
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
README.md
|
||||||
|
RULES.md
|
||||||
|
prompts/030-V0_3_11_START_PROMPT.md
|
||||||
|
crates/**
|
||||||
|
docs/**
|
||||||
|
config/**
|
||||||
|
```
|
||||||
|
|
||||||
|
La surface préparée en `pre.009` reste byte-identique hors mécanique Cargo et ajout du présent delta.
|
||||||
|
|
||||||
|
## Surface stable publiée
|
||||||
|
|
||||||
|
`0.3.10` stabilise `ksp-raw-transaction-lib` comme lower-layer D1 RAW source-neutral commune aux producteurs `RawTransaction` :
|
||||||
|
|
||||||
|
- canonicalisation RAW v1 commune extraite du Backfill sans changement des golden bytes/hash ;
|
||||||
|
- construction de `RawTransaction` et `RawTransactionObservation` via les modèles backend-neutral de `ksp-store-api` ;
|
||||||
|
- identité canonique `(network, signature)` et observations producteur séparées ;
|
||||||
|
- wire Solana source-neutral Legacy/V0/V1, sérialisation Base64 et extraction de signature depuis le matériau transactionnel complet ;
|
||||||
|
- dépendances volontairement limitées, sans Transport, Config, runtime async, Job, Worker, `ksp-store-lib` ni backend Store physique ;
|
||||||
|
- migration de `ksp-job-backfill-lib` vers la common crate sans edge Job vers Worker.
|
||||||
|
|
||||||
|
La release stabilise aussi la qualification cross-source préparatoire au futur Worker :
|
||||||
|
|
||||||
|
- `ksp-onchain-transport-lib::get_block_observed` conserve l'identité sûre de la route HTTP réellement gagnante ;
|
||||||
|
- `blockSubscribe` standard `full/base64` est qualifié comme RAW-direct sur le sous-ensemble déterministe prouvé ;
|
||||||
|
- Helius `transactionSubscribe` reste un signal nécessitant hydration HTTP lorsque les métadonnées indispensables manquent ;
|
||||||
|
- Yellowstone Transaction V1 qualifie le wire source-neutral ; Transaction nécessite hydration HTTP pour le `block_time` ;
|
||||||
|
- Yellowstone Block conserve le `block_time`, mais le RAW complet reste conservateur tant que la meta protobuf n'est pas prouvée byte-identical à la meta JSON HTTP ;
|
||||||
|
- aucun RAW-direct Yellowstone complet ni feed EARLY n'est revendiqué sans preuve.
|
||||||
|
|
||||||
|
La nomenclature durable des couches Store est désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
D1 RAW
|
||||||
|
D2 STRUCTURAL
|
||||||
|
D3 DECODED
|
||||||
|
D4 DOMAIN
|
||||||
|
```
|
||||||
|
|
||||||
|
`N1–N4` restent réservés aux niveaux architecturaux. Le terme `CORE` n'est plus le nom de la couche Store D2 ; `ksp-core-lib` et les usages réellement liés au Core fondamental restent inchangés.
|
||||||
|
|
||||||
|
Le runtime Worker concret est volontairement hors de `0.3.10`. La trajectoire stabilisée devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.11 fondation runtime/persistence déterministe du Worker RawTransaction ingest
|
||||||
|
0.3.12 Yellowstone + hydration HTTP + continuité
|
||||||
|
0.3.13 WS standard / Helius / HTTP live + convergence multi-source
|
||||||
|
0.3.14 gap repair / hardening / smokes provider accessibles
|
||||||
|
0.3.15 ksp-app-raw-transaction-ingest-desk
|
||||||
|
0.3.16 Backfill multi-source / multi-stratégie
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Job Backfill historique paramétré et le Worker Ingest continu restent deux producteurs indépendants du même Store.
|
||||||
|
|
||||||
|
## Suite préparée
|
||||||
|
|
||||||
|
La session suivante part exclusivement de la base stable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.3.10
|
||||||
|
```
|
||||||
|
|
||||||
|
et exécute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/030-V0_3_11_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
pour ouvrir :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.11 — fondation runtime du Worker RawTransaction ingest
|
||||||
|
```
|
||||||
|
|
||||||
|
`0.3.11-pre.001` doit commencer par lecture, audit, brainstorming, sizing et planification avant toute implémentation lourde. Si le périmètre n'est pas clôturable dans une seule session ou si une tranche dépasse le budget prévu, la release doit être rescindée avant le développement lourd.
|
||||||
|
|
||||||
|
## Gate demandé
|
||||||
|
|
||||||
|
La tranche est purement mécanique. Aucune dépendance, feature, source runtime, configuration ou documentation durable n'est modifiée par `rel.001`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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.10
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-job-backfill-lib
|
||||||
|
cargo tree -p ksp-raw-transaction-lib --edges normal
|
||||||
|
cargo tree -p ksp-job-backfill-lib --edges normal
|
||||||
|
```
|
||||||
|
|
||||||
|
Après gate propre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commit : v0.3.10-rel.001
|
||||||
|
tag : v0.3.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag de prerelease ou `rel.001` n'est requis.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/IDEAS.md -->
|
<!-- file: docs/IDEAS.md -->
|
||||||
<!-- version: 27 -->
|
<!-- version: 28 -->
|
||||||
|
|
||||||
# Idées à explorer
|
# Idées à explorer
|
||||||
|
|
||||||
@@ -159,9 +159,9 @@ Conserver comme pistes séparées les offres pré-exécution/shred/deshred (Heli
|
|||||||
|
|
||||||
**Status :** Retenue, granularité révisée
|
**Status :** Retenue, granularité révisée
|
||||||
|
|
||||||
RAW et CORE peuvent disposer de workers dédiés à la fin de leur couche respective.
|
RAW et STRUCTURAL peuvent disposer de workers dédiés à la fin de leur couche respective.
|
||||||
|
|
||||||
À partir de DECODE, ne pas figer à l'avance une chaîne globale `generic-materializer -> domain-projector` pour tout Solana : la granularité des workers/processors doit émerger des vertical slices Program réels et réutiliser les mêmes transformations que les jobs de replay correspondants.
|
À partir de DECODED, ne pas figer à l'avance une chaîne globale `generic-materializer -> domain-projector` pour tout Solana : la granularité des workers/processors doit émerger des vertical slices Program réels et réutiliser les mêmes transformations que les jobs de replay correspondants.
|
||||||
|
|
||||||
### Worker control
|
### Worker control
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ Les futurs processing outcomes versionnés constituent la preuve durable de trai
|
|||||||
|
|
||||||
**Status :** Requalifiée par `0.2.0-pre.003`
|
**Status :** Requalifiée par `0.2.0-pre.003`
|
||||||
|
|
||||||
L'ancienne liste figée `ksp-job-replay-core` / `ksp-job-replay-generic-materialization` / `ksp-job-replay-domain-projection` n'est plus une décision KSP. La frontière `RAW -> CORE` pourra introduire un replay Core lorsque CORE sera ouverte. À partir de DECODE, les jobs de replay doivent émerger avec les groupes/capacités verticaux réels et réutiliser la même logique que le processing live correspondant, sans imposer un materializer/projector global à tout Solana.
|
L'ancienne liste figée `ksp-job-replay-core` / `ksp-job-replay-generic-materialization` / `ksp-job-replay-domain-projection` n'est plus une décision KSP. La frontière `RAW -> STRUCTURAL` pourra introduire un replay STRUCTURAL lorsque STRUCTURAL sera ouverte. À partir de DECODED, les jobs de replay doivent émerger avec les groupes/capacités verticaux réels et réutiliser la même logique que le processing live correspondant, sans imposer un materializer/projector global à tout Solana.
|
||||||
|
|
||||||
### Notification backend de référence
|
### Notification backend de référence
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ Définir le schéma SQL, la durée/renouvellement de lease et la technique Postg
|
|||||||
|
|
||||||
Fixer les noms/types exacts et distinguer Produced, NoOutput, NotApplicable, Unsupported et failure déterministe sans transformer des situations normales en erreurs.
|
Fixer les noms/types exacts et distinguer Produced, NoOutput, NotApplicable, Unsupported et failure déterministe sans transformer des situations normales en erreurs.
|
||||||
|
|
||||||
### Contexte stateful des projections SPECIALIZED
|
### Contexte stateful des projections DOMAIN
|
||||||
|
|
||||||
**Status :** À explorer avec la première projection nécessitant un état existant
|
**Status :** À explorer avec la première projection nécessitant un état existant
|
||||||
|
|
||||||
@@ -426,4 +426,4 @@ Si cette capacité devient utile, l’intégration doit être conçue dans la pi
|
|||||||
|
|
||||||
`0.2.0-pre.002` avait fixé le premier séquencement concret. `0.2.1-pre.001-fix.001` le recalibre désormais sur `0.2.1 -> 0.2.13`, sous réserve du gate de dimensionnement de chaque `pre.001` et avec possibilité d'enchaîner plusieurs releases complètement clôturées dans une même session lorsque le sizing le permet.
|
`0.2.0-pre.002` avait fixé le premier séquencement concret. `0.2.1-pre.001-fix.001` le recalibre désormais sur `0.2.1 -> 0.2.13`, sous réserve du gate de dimensionnement de chaque `pre.001` et avec possibilité d'enchaîner plusieurs releases complètement clôturées dans une même session lorsque le sizing le permet.
|
||||||
|
|
||||||
Les séries après RAW/CORE ne sont volontairement pas numérotées programme par programme à ce stade : la règle est de redécouper chaque vertical slice selon sa taille réelle et de ne jamais ouvrir une release qui ne peut pas être clôturée dans sa session.
|
Les séries après RAW/STRUCTURAL ne sont volontairement pas numérotées programme par programme à ce stade : la règle est de redécouper chaque vertical slice selon sa taille réelle et de ne jamais ouvrir une release qui ne peut pas être clôturée dans sa session.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/002-LAYERS_AND_DEPENDENCIES.md -->
|
<!-- file: docs/architecture/002-LAYERS_AND_DEPENDENCIES.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# Couches et dépendances KSP
|
# Couches et dépendances KSP
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ Les niveaux architecturaux N1–N4 décrivent les familles de composants du proj
|
|||||||
### N3 — Données, jobs, workers et processing
|
### N3 — Données, jobs, workers et processing
|
||||||
|
|
||||||
- `ksp-store-api` / `ksp-store-lib` ;
|
- `ksp-store-api` / `ksp-store-lib` ;
|
||||||
- `ksp-materializer-api` / implementations lorsque DECODE s'ouvre ;
|
- `ksp-materializer-api` / implementations lorsque DECODED s'ouvre ;
|
||||||
- `ksp-job-api`, `ksp-job-backfill-lib` puis les jobs concrets introduits par les couches ;
|
- `ksp-job-api`, `ksp-job-backfill-lib` puis les jobs concrets introduits par les couches ;
|
||||||
- `ksp-worker-api` et workers ;
|
- `ksp-worker-api` et workers ;
|
||||||
- processors/pipelines spécialisés réellement réutilisés.
|
- processors/pipelines spécialisés réellement réutilisés.
|
||||||
@@ -45,32 +45,32 @@ La chaîne de données canonique est :
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
-> D2 CORE
|
-> D2 STRUCTURAL
|
||||||
-> D3 DECODE
|
-> D3 DECODED
|
||||||
-> D4 SPECIALIZED
|
-> D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Aliases fonctionnels :
|
Aliases fonctionnels :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
RAW et CORE sont indépendants du décodage Program.
|
RAW et STRUCTURAL sont indépendants du décodage Program.
|
||||||
|
|
||||||
CORE est une normalisation générique de Solana : structure des blocs, transactions, messages, comptes, instructions/CPI brutes, logs/meta et relations fondamentales.
|
STRUCTURAL est une normalisation générique de Solana : structure des blocs, transactions, messages, comptes, instructions/CPI brutes, logs/meta et relations fondamentales.
|
||||||
|
|
||||||
Le premier decoder Program intervient seulement à `CORE -> DECODE`.
|
Le premier decoder Program intervient seulement à `STRUCTURAL -> DECODED`.
|
||||||
|
|
||||||
## Progression par couche
|
## Progression par couche
|
||||||
|
|
||||||
### RAW et CORE
|
### RAW et STRUCTURAL
|
||||||
|
|
||||||
Ces deux couches sont construites horizontalement.
|
Ces deux couches sont construites horizontalement.
|
||||||
|
|
||||||
À la fin de chaque couche, KSP ajoute les composants d'exploitation nécessaires : persistence, replay/backfill, worker/service et application de contrôle lorsque utiles.
|
À la fin de chaque couche, KSP ajoute les composants d'exploitation nécessaires : persistence, replay/backfill, worker/service et application de contrôle lorsque utiles.
|
||||||
|
|
||||||
### DECODE et SPECIALIZED
|
### DECODED et DOMAIN
|
||||||
|
|
||||||
À partir du décodage, KSP progresse verticalement par groupe fonctionnel :
|
À partir du décodage, KSP progresse verticalement par groupe fonctionnel :
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ Les applications Tauri restent minces :
|
|||||||
- instrumentation frontend ;
|
- instrumentation frontend ;
|
||||||
- aucun déplacement de logique de transport, Wallet, Config, Program, Store ou Materializer dans Tauri.
|
- aucun déplacement de logique de transport, Wallet, Config, Program, Store ou Materializer dans Tauri.
|
||||||
|
|
||||||
Des applications spécialisées sont ajoutées au fur et à mesure pour valider les couches : Config Desk, Wallet Desk, `ksp-app-solprices-desk`, `ksp-app-backfill-desk`, `ksp-app-store-desk`, CORE tooling puis Market Desk. `ksp-app-solprices-desk` reste une HID mince : elle consomme l’inventaire, les observations, les états et les opérations génériques de `ksp-offchain-transport-lib` sans connaître les providers, leurs endpoints, leurs credentials ni leurs limites. `ksp-app-backfill-desk` compose Config, Transport HTTP, Store et `ksp-job-backfill-lib` sans absorber découverte, retry/rate-limit, persistance ou checkpoint ; son frontend ne reçoit que des DTOs sûrs et le checkpoint de reprise reste Rust-only. `ksp-app-store-desk` compose Config, Logging et `ksp-store-lib` pour une inspection RAW read-only : il n'accède ni au backend physique ni au SQL et sépare la pagination random-access de l'interface de la pagination cursor/keyset réservée aux consumers machine.
|
Des applications spécialisées sont ajoutées au fur et à mesure pour valider les couches : Config Desk, Wallet Desk, `ksp-app-solprices-desk`, `ksp-app-backfill-desk`, `ksp-app-store-desk`, STRUCTURAL tooling puis Market Desk. `ksp-app-solprices-desk` reste une HID mince : elle consomme l’inventaire, les observations, les états et les opérations génériques de `ksp-offchain-transport-lib` sans connaître les providers, leurs endpoints, leurs credentials ni leurs limites. `ksp-app-backfill-desk` compose Config, Transport HTTP, Store et `ksp-job-backfill-lib` sans absorber découverte, retry/rate-limit, persistance ou checkpoint ; son frontend ne reçoit que des DTOs sûrs et le checkpoint de reprise reste Rust-only. `ksp-app-store-desk` compose Config, Logging et `ksp-store-lib` pour une inspection RAW read-only : il n'accède ni au backend physique ni au SQL et sépare la pagination random-access de l'interface de la pagination cursor/keyset réservée aux consumers machine.
|
||||||
|
|
||||||
## Workers et jobs
|
## Workers et jobs
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ ksp-execution-policy-api
|
|||||||
|
|
||||||
## Groupes Program prioritaires
|
## Groupes Program prioritaires
|
||||||
|
|
||||||
Après RAW/CORE :
|
Après RAW/STRUCTURAL :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Solana Core Programs
|
Solana Core Programs
|
||||||
@@ -198,4 +198,4 @@ Un satellite nécessaire à un protocole reste dans son groupe : Pump fee avec P
|
|||||||
- split éventuel d'une API Interface séparée uniquement si un vrai besoin apparaît ;
|
- split éventuel d'une API Interface séparée uniquement si un vrai besoin apparaît ;
|
||||||
- contrats Rust exacts de Program/Materializer/Store ;
|
- contrats Rust exacts de Program/Materializer/Store ;
|
||||||
- mécanisme IPC du premier manager de worker autonome ;
|
- mécanisme IPC du premier manager de worker autonome ;
|
||||||
- granularité future des workers DECODE/SPECIALIZED par groupe.
|
- granularité future des workers DECODED/DOMAIN par groupe.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/003-COMPONENT_CONTRACTS.md -->
|
<!-- file: docs/architecture/003-COMPONENT_CONTRACTS.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
|
|
||||||
# Contrats initiaux des composants KSP
|
# Contrats initiaux des composants KSP
|
||||||
|
|
||||||
@@ -137,24 +137,24 @@ La chaîne durable est :
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
-> D2 CORE
|
-> D2 STRUCTURAL
|
||||||
-> D3 DECODE
|
-> D3 DECODED
|
||||||
-> D4 SPECIALIZED
|
-> D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
### RAW
|
### RAW
|
||||||
|
|
||||||
Acquisition replayable + provenance, sans décodage Program.
|
Acquisition replayable + provenance, sans décodage Program.
|
||||||
|
|
||||||
### CORE
|
### STRUCTURAL
|
||||||
|
|
||||||
Normalisation générique Solana, sans décodage Program.
|
Normalisation générique Solana, sans décodage Program.
|
||||||
|
|
||||||
### DECODE
|
### DECODED
|
||||||
|
|
||||||
Interprétation Program/protocole puis matérialisation générique/journal durable.
|
Interprétation Program/protocole puis matérialisation générique/journal durable.
|
||||||
|
|
||||||
### SPECIALIZED
|
### DOMAIN
|
||||||
|
|
||||||
Projections queryables de domaine : token, metadata, pools, trades, OHLC, routes, etc.
|
Projections queryables de domaine : token, metadata, pools, trades, OHLC, routes, etc.
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ La première Store release est RAW-only ; les couches suivantes sont ajoutées q
|
|||||||
|
|
||||||
## Materializer
|
## Materializer
|
||||||
|
|
||||||
`ksp-materializer-api`/`ksp-materializer-lib` sont introduits avec le premier besoin DECODE réel, pas avant.
|
`ksp-materializer-api`/`ksp-materializer-lib` sont introduits avec le premier besoin DECODED réel, pas avant.
|
||||||
|
|
||||||
Program et Materializer restent indépendants du backend Store ; les composants de composition convertissent leurs outputs vers les DTO persistants.
|
Program et Materializer restent indépendants du backend Store ; les composants de composition convertissent leurs outputs vers les DTO persistants.
|
||||||
|
|
||||||
@@ -174,9 +174,9 @@ Program et Materializer restent indépendants du backend Store ; les composants
|
|||||||
|
|
||||||
`ksp-worker-api` est la lifecycle API des services continus.
|
`ksp-worker-api` est la lifecycle API des services continus.
|
||||||
|
|
||||||
RAW et CORE peuvent recevoir leurs workers à la fin de leur couche respective.
|
RAW et STRUCTURAL peuvent recevoir leurs workers à la fin de leur couche respective.
|
||||||
|
|
||||||
Les workers DECODE/SPECIALIZED sont introduits avec les groupes Program réels, afin de ne pas créer une orchestration générique vide avant les processors.
|
Les workers DECODED/DOMAIN sont introduits avec les groupes Program réels, afin de ne pas créer une orchestration générique vide avant les processors.
|
||||||
|
|
||||||
## Jobs
|
## Jobs
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ Les workers DECODE/SPECIALIZED sont introduits avec les groupes Program réels,
|
|||||||
|
|
||||||
Le premier job concret est `ksp-job-backfill-lib`. Il couvre un backfill historique `RawTransaction` : quatre scopes bornés, découverte/hydratation Transport observée, conversion RAW v1, persistance atomique par `ksp-store-lib`, concurrence bornée, frontier/checkpoint contigus caller-owned, annulation coopérative et snapshots complets sûrs. Il ne dépend ni de Config, ni d'un backend Store concret, ni d'un Worker.
|
Le premier job concret est `ksp-job-backfill-lib`. Il couvre un backfill historique `RawTransaction` : quatre scopes bornés, découverte/hydratation Transport observée, conversion RAW v1, persistance atomique par `ksp-store-lib`, concurrence bornée, frontier/checkpoint contigus caller-owned, annulation coopérative et snapshots complets sûrs. Il ne dépend ni de Config, ni d'un backend Store concret, ni d'un Worker.
|
||||||
|
|
||||||
Les jobs de replay suivants pourront suivre les frontières durables ouvertes : RAW -> CORE, CORE -> DECODE, DECODE -> SPECIALIZED. Ils ne sont pas forcés d'adopter le contrat métier du backfill RAW ; seuls les contrats vraiment communs appartiennent à `ksp-job-api`.
|
Les jobs de replay suivants pourront suivre les frontières durables ouvertes : RAW -> STRUCTURAL, STRUCTURAL -> DECODED, DECODED -> DOMAIN. Ils ne sont pas forcés d'adopter le contrat métier du backfill RAW ; seuls les contrats vraiment communs appartiennent à `ksp-job-api`.
|
||||||
|
|
||||||
Aucune `ksp-job-control-lib` n'est créée sans duplication concrète.
|
Aucune `ksp-job-control-lib` n'est créée sans duplication concrète.
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ ksp-app-wallet-desk
|
|||||||
ksp-app-solprices-desk
|
ksp-app-solprices-desk
|
||||||
ksp-app-backfill-desk
|
ksp-app-backfill-desk
|
||||||
ksp-app-store-desk
|
ksp-app-store-desk
|
||||||
CORE tooling
|
STRUCTURAL tooling
|
||||||
ksp-app-market-desk
|
ksp-app-market-desk
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ Une application globale reste future.
|
|||||||
|
|
||||||
## Progression verticale Program
|
## Progression verticale Program
|
||||||
|
|
||||||
À partir de DECODE :
|
À partir de DECODED :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wire -> decode -> materialize -> specialized -> prepare -> policy -> execute -> scenario
|
wire -> decode -> materialize -> specialized -> prepare -> policy -> execute -> scenario
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
|
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
|
||||||
<!-- version: 34 -->
|
<!-- version: 37 -->
|
||||||
|
|
||||||
# Inventaire initial des composants KSP
|
# Inventaire initial des composants KSP
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
|
|||||||
## Inventaire synthétique
|
## Inventaire synthétique
|
||||||
|
|
||||||
| Domaine | Composant | Type | Statut | Première cible actuelle | Mission |
|
| Domaine | Composant | Type | Statut | Première cible actuelle | Mission |
|
||||||
|-------------------------|-----------------------------------------|---------------|--------------|---------------------------------|---------------------------------------------------------------------------------------------|
|
|-------------------------|-----------------------------------------|-------------|--------------|---------------------------------|---------------------------------------------------------------------------------------------|
|
||||||
| Core | `ksp-core-lib` | lib | Stable | `0.1.1` | Error/Result, Program IDs et primitives fondamentales |
|
| Core | `ksp-core-lib` | lib | Stable | `0.1.1` | Error/Result, Program IDs et primitives fondamentales |
|
||||||
| Logging | `ksp-logging-lib` | lib | Stable | `0.1.2` | façade unique tracing KSP |
|
| Logging | `ksp-logging-lib` | lib | Stable | `0.1.2` | façade unique tracing KSP |
|
||||||
| Config | `ksp-config-lib` | lib | Stable | `0.1.3` | documents, profils, env et persistence Config |
|
| Config | `ksp-config-lib` | lib | Stable | `0.1.3` | documents, profils, env et persistence Config |
|
||||||
@@ -41,16 +41,17 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
|
|||||||
| Store runtime | `ksp-store-lib` | lib | Stable | `0.3.2`–`0.3.4` | façade backend-neutral et conformance RAW 10/10 |
|
| Store runtime | `ksp-store-lib` | lib | Stable | `0.3.2`–`0.3.4` | façade backend-neutral et conformance RAW 10/10 |
|
||||||
| Store PostgreSQL | `ksp-store-postgres-lib` | lib | Stable | `0.3.2`–`0.3.4` | backend référence : fondation, RawTransaction et RawAccountState |
|
| Store PostgreSQL | `ksp-store-postgres-lib` | lib | Stable | `0.3.2`–`0.3.4` | backend référence : fondation, RawTransaction et RawAccountState |
|
||||||
| Job lifecycle | `ksp-job-api` | API | Implémenté | `0.3.6` | identité/lifecycle/annulation/notifications latest-value runtime-neutral |
|
| Job lifecycle | `ksp-job-api` | API | Implémenté | `0.3.6` | identité/lifecycle/annulation/notifications latest-value runtime-neutral |
|
||||||
| Backfill | `ksp-job-backfill-lib` | lib | Implémenté | `0.3.6`, extension `0.3.12` | première verticale HTTP puis extension multi-source/historique sans changer l’identité RAW |
|
| Backfill | `ksp-job-backfill-lib` | lib | Implémenté | `0.3.6`, extension `0.3.16` | première verticale HTTP puis extension multi-source/historique sans changer l’identité RAW |
|
||||||
| Backfill Desk | `ksp-app-backfill-desk` | app | Implémenté | `0.3.7`, extension `0.3.12` | contrôle du backfill RAW ; sélection des stratégies/sources ajoutée après le worker live |
|
| Backfill Desk | `ksp-app-backfill-desk` | app | Implémenté | `0.3.7`, extension `0.3.16` | contrôle du backfill RAW ; sélection des stratégies/sources ajoutée après le worker live |
|
||||||
| Store Desk | `ksp-app-store-desk` | app | Implémenté | `0.3.8` | inspection RAW read-only Transaction/Account/Observation via façade Store |
|
| Store Desk | `ksp-app-store-desk` | app | Implémenté | `0.3.8` | inspection RAW read-only Transaction/Account/Observation via façade Store |
|
||||||
|
| RAW transaction common | `ksp-raw-transaction-lib` | lib | Implémenté | `0.3.10` | canonicalisation/wire RAW Transaction v1 source-neutral partagé entre producteurs |
|
||||||
| Worker lifecycle | `ksp-worker-api` | API | Retenu | `0.3.9` | lifecycle/health/progression génériques des services continus |
|
| Worker lifecycle | `ksp-worker-api` | API | Retenu | `0.3.9` | lifecycle/health/progression génériques des services continus |
|
||||||
| RAW transaction worker | `ksp-worker-raw-transaction-ingest-lib` | worker/lib | Retenu | `0.3.10` | ingestion continue `RawTransaction` multi-source, déduplication/provenance/recovery |
|
| RAW transaction worker | `ksp-worker-raw-transaction-ingest-lib` | worker/lib | Retenu | `0.3.11`–`0.3.14` | ingestion continue `RawTransaction` multi-source, déduplication/provenance/recovery |
|
||||||
| RAW ingest Desk | `ksp-app-raw-transaction-ingest-desk` | app | Retenu | `0.3.11` | choix/supervision d’une ou plusieurs sources/méthodes sans réimplémenter le worker |
|
| RAW ingest Desk | `ksp-app-raw-transaction-ingest-desk` | app | Retenu | `0.3.15` | choix/supervision d’une ou plusieurs sources/méthodes sans réimplémenter le worker |
|
||||||
| CORE processor | nom à fixer | processor/lib | Retenu | couche CORE | normalisation Solana générique RAW -> CORE |
|
| STRUCTURAL job | nom à fixer | job/lib | Retenu | couche STRUCTURAL | normalisation Solana générique RAW -> STRUCTURAL bornée/rejouable |
|
||||||
| CORE worker | nom à fixer | worker | Retenu | fin couche CORE | backlog RAW -> CORE continu |
|
| STRUCTURAL worker | nom à fixer | worker/lib | Retenu | fin couche STRUCTURAL | backlog RAW -> STRUCTURAL continu |
|
||||||
| Materializer API | `ksp-materializer-api` | API | Retenu | premier groupe DECODE | contrats extensibles matérialisation |
|
| Materializer API | `ksp-materializer-api` | API | Retenu | premier groupe DECODED | contrats extensibles matérialisation |
|
||||||
| Materializer impl. | `ksp-materializer-lib` | lib | Retenu | premier groupe DECODE | implementations officielles communes |
|
| Materializer impl. | `ksp-materializer-lib` | lib | Retenu | premier groupe DECODED | implementations officielles communes |
|
||||||
| Execution policy | `ksp-execution-policy-api` | API | Retenu | premier vrai besoin execution | décision/safety multi-contexte |
|
| Execution policy | `ksp-execution-policy-api` | API | Retenu | premier vrai besoin execution | décision/safety multi-contexte |
|
||||||
| Execution orchestration | `ksp-execution-lib` | lib | Retenu | premier vrai cycle execution | Program + policy + Wallet + transport |
|
| Execution orchestration | `ksp-execution-lib` | lib | Retenu | premier vrai cycle execution | Program + policy + Wallet + transport |
|
||||||
| Scenarios | `ksp-scenario-<domain>-lib` | lib | Retenu | vertical slices | validation métier/devnet par groupe |
|
| Scenarios | `ksp-scenario-<domain>-lib` | lib | Retenu | vertical slices | validation métier/devnet par groupe |
|
||||||
@@ -106,19 +107,19 @@ Import/export reste extensible ; les formats supplémentaires sont suivis dans `
|
|||||||
## Data plane
|
## Data plane
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
- RAW : acquisition replayable ;
|
- RAW : acquisition replayable ;
|
||||||
- CORE : normalisation blockchain générique sans decoder Program ;
|
- STRUCTURAL : normalisation blockchain générique sans decoder Program ;
|
||||||
- DECODE : interpretation Program + matérialisation générique/journal ;
|
- DECODED : interpretation Program + matérialisation générique/journal ;
|
||||||
- SPECIALIZED : projections queryables de domaine.
|
- DOMAIN : projections queryables de domaine.
|
||||||
|
|
||||||
## Progression des processors
|
## Progression structurelle
|
||||||
|
|
||||||
RAW et CORE sont complétés couche par couche avec jobs/workers/apps utiles.
|
RAW et STRUCTURAL sont complétés couche par couche avec jobs/workers/apps utiles.
|
||||||
|
|
||||||
À partir de DECODE, progression verticale par groupe :
|
À partir de DECODED, progression verticale par groupe :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wire -> decode -> materialize -> specialized -> prepare -> policy -> execute -> scenario
|
wire -> decode -> materialize -> specialized -> prepare -> policy -> execute -> scenario
|
||||||
@@ -153,6 +154,6 @@ Market Desk est progressive : V1 après les DEX prioritaires, puis enrichissemen
|
|||||||
## Questions restantes
|
## Questions restantes
|
||||||
|
|
||||||
- nécessité future d'un pool automatique WS ;
|
- nécessité future d'un pool automatique WS ;
|
||||||
- types exacts `ksp-materializer-api` lors de l'ouverture DECODE ;
|
- types exacts `ksp-materializer-api` lors de l'ouverture DECODED ;
|
||||||
- nom/packaging précis du premier RAW worker et du CORE normalizer ;
|
- nom/packaging précis des futurs STRUCTURAL job et STRUCTURAL worker ;
|
||||||
- granularité des workers DECODE/SPECIALIZED par groupe.
|
- granularité des workers DECODED/DOMAIN par groupe.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
|
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
|
||||||
<!-- version: 23 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# Graphe de dépendances KSP
|
# Graphe de dépendances KSP
|
||||||
|
|
||||||
@@ -244,27 +244,55 @@ Une petite policy spécifique peut vivre dans une crate scenario/orchestrateur.
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
-> D2 CORE
|
-> D2 STRUCTURAL
|
||||||
-> D3 DECODE
|
-> D3 DECODED
|
||||||
-> D4 SPECIALIZED
|
-> D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
### RAW
|
### RAW
|
||||||
|
|
||||||
|
La canonicalisation `RawTransaction` source-neutral est une lower layer explicite, séparée du Transport et de la persistence runtime :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
transport model
|
ksp-raw-transaction-lib
|
||||||
|
-> ksp-store-api
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> base64 / serde_json / sha2
|
||||||
|
```
|
||||||
|
|
||||||
|
Interdictions :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-onchain-transport-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-store-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-job-api / ksp-job-backfill-lib
|
||||||
|
ksp-raw-transaction-lib -X-> ksp-worker-api / concrete workers
|
||||||
|
ksp-raw-transaction-lib -X-> Config / runtime async / backend Store
|
||||||
|
```
|
||||||
|
|
||||||
|
Le flux de composition devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transport model / fixture source-neutral
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
composition/RAW ingestion
|
producer adapter
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
ksp-store-api
|
ksp-raw-transaction-lib
|
||||||
|
|
|
||||||
|
+--> RawTransaction + RawTransactionObservation (types ksp-store-api)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
producer concret
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
ksp-store-lib
|
ksp-store-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
### CORE
|
Le Job Backfill et le Worker RAW peuvent donc partager exactement la canonicalisation et le wire sans que la common crate possède le runtime, le Transport ou le backend.
|
||||||
|
|
||||||
|
### STRUCTURAL
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
@@ -273,23 +301,23 @@ D1 RAW
|
|||||||
Solana generic normalizer
|
Solana generic normalizer
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
```
|
```
|
||||||
|
|
||||||
Le normalizer CORE peut utiliser `ksp-interface-lib` pour des wires Solana génériques.
|
Le normalizer STRUCTURAL peut utiliser `ksp-interface-lib` pour des wires Solana génériques.
|
||||||
|
|
||||||
Interdictions :
|
Interdictions :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -X-> ksp-program-api
|
RAW -> STRUCTURAL -X-> ksp-program-api
|
||||||
RAW -> CORE -X-> ksp-program-lib
|
RAW -> STRUCTURAL -X-> ksp-program-lib
|
||||||
RAW -> CORE -X-> ksp-materializer-api
|
RAW -> STRUCTURAL -X-> ksp-materializer-api
|
||||||
```
|
```
|
||||||
|
|
||||||
### DECODE
|
### DECODED
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
ksp-program-api implementation
|
ksp-program-api implementation
|
||||||
@@ -301,19 +329,19 @@ decoded facts
|
|||||||
ksp-materializer-api implementation
|
ksp-materializer-api implementation
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D3 DECODE / generic journal
|
D3 DECODED / generic journal
|
||||||
```
|
```
|
||||||
|
|
||||||
### SPECIALIZED
|
### DOMAIN
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D3 DECODE
|
D3 DECODED
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
specialized projector/materializer
|
specialized projector/materializer
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D4 SPECIALIZED
|
D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
## Materialization
|
## Materialization
|
||||||
@@ -358,7 +386,7 @@ ksp-store-postgres-lib -X-> ksp-store-lib
|
|||||||
worker/job -X-> ksp-store-postgres-lib
|
worker/job -X-> ksp-store-postgres-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
La première release Store (`0.3.1`) est RAW-only ; les contrats CORE/DECODE/SPECIALIZED sont ajoutés avec leurs couches. Les consumers runtime ordinaires (jobs, workers, apps) utilisent `ksp-store-lib`; ils ne sélectionnent ni n'importent directement `ksp-store-postgres-lib` ou un autre backend.
|
La première release Store (`0.3.1`) est RAW-only ; les contrats STRUCTURAL/DECODED/DOMAIN sont ajoutés avec leurs couches. Les consumers runtime ordinaires (jobs, workers, apps) utilisent `ksp-store-lib`; ils ne sélectionnent ni n'importent directement `ksp-store-postgres-lib` ou un autre backend.
|
||||||
|
|
||||||
## Jobs
|
## Jobs
|
||||||
|
|
||||||
@@ -375,9 +403,10 @@ ksp-job-backfill-lib
|
|||||||
-> ksp-core-lib
|
-> ksp-core-lib
|
||||||
-> ksp-logging-lib
|
-> ksp-logging-lib
|
||||||
-> ksp-onchain-transport-lib
|
-> ksp-onchain-transport-lib
|
||||||
|
-> ksp-raw-transaction-lib # canonicalisation/wire RAW v1 partagés
|
||||||
-> ksp-store-lib # default-features = false ; aucun backend imposé
|
-> ksp-store-lib # default-features = false ; aucun backend imposé
|
||||||
-> futures-util / tokio # runtime privé du job, jamais dans ksp-job-api
|
-> futures-util / tokio # runtime privé du job, jamais dans ksp-job-api
|
||||||
-> serde_json / sha2 # canonicalisation RAW v1 et digest
|
-> serde_json / sha2 # usages résiduels propres au job tant qu'ils existent
|
||||||
```
|
```
|
||||||
|
|
||||||
Il remplit RAW et ne décode aucun programme. Il ne dépend pas de Config : la composition supérieure construit explicitement Transport, Store et `BackfillRequest`. Le réseau appartient au scope/à l'identité durable `(network, signature)` ; rôle, provider, endpoint et protocole restent des choix ou provenances d'acquisition et ne deviennent jamais une clé de transaction.
|
Il remplit RAW et ne décode aucun programme. Il ne dépend pas de Config : la composition supérieure construit explicitement Transport, Store et `BackfillRequest`. Le réseau appartient au scope/à l'identité durable `(network, signature)` ; rôle, provider, endpoint et protocole restent des choix ou provenances d'acquisition et ne deviennent jamais une clé de transaction.
|
||||||
@@ -393,6 +422,7 @@ ksp-worker-api
|
|||||||
ksp-worker-raw-transaction-ingest-lib
|
ksp-worker-raw-transaction-ingest-lib
|
||||||
-> ksp-worker-api
|
-> ksp-worker-api
|
||||||
-> ksp-onchain-transport-lib
|
-> ksp-onchain-transport-lib
|
||||||
|
-> ksp-raw-transaction-lib
|
||||||
-> ksp-store-lib
|
-> ksp-store-lib
|
||||||
-> ksp-logging-lib
|
-> ksp-logging-lib
|
||||||
# Config reste possédé par la composition supérieure ; aucune dépendance backend/provider physique
|
# Config reste possédé par la composition supérieure ; aucune dépendance backend/provider physique
|
||||||
@@ -402,11 +432,11 @@ ksp-worker-control-lib
|
|||||||
-> ksp-core-lib
|
-> ksp-core-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
`ksp-worker-api` est ouvert en `0.3.9` comme contrat générique de services continus et ne connaît ni Solana, ni Transport, ni Store, ni Tauri. Son premier consumer concret est prévu en `0.3.10` avec `ksp-worker-raw-transaction-ingest-lib`.
|
`ksp-worker-api` est ouvert en `0.3.9` comme contrat générique de services continus et ne connaît ni Solana, ni Transport, ni Store, ni Tauri. Son premier consumer concret est prévu en `0.3.11` avec `ksp-worker-raw-transaction-ingest-lib`, puis ses sources live sont complétées jusqu’en `0.3.14`.
|
||||||
|
|
||||||
Le worker RAW Transaction n'est pas défini comme « un worker WebSocket » ou « un worker gRPC ». Il reçoit une ou plusieurs stratégies d'acquisition construites au-dessus des façades KSP réellement disponibles ; celles-ci peuvent être alternatives, complémentaires (discovery + hydration), redondantes entre providers ou spécialisées live/catch-up/gap-repair. La transaction canonique reste identifiée indépendamment de la source et chaque acquisition utile conserve sa propre observation/provenance Store.
|
Le worker RAW Transaction n'est pas défini comme « un worker WebSocket » ou « un worker gRPC ». Il reçoit une ou plusieurs stratégies d'acquisition construites au-dessus des façades KSP réellement disponibles ; celles-ci peuvent être alternatives, complémentaires (discovery + hydration), redondantes entre providers ou spécialisées live/catch-up/gap-repair. La transaction canonique reste identifiée indépendamment de la source et chaque acquisition utile conserve sa propre observation/provenance Store.
|
||||||
|
|
||||||
RAW worker et CORE worker sont introduits à la fin de leur couche respective, lorsque persistence/backlog sont disponibles. Les workers DECODE/SPECIALIZED sont introduits avec les groupes Program concernés plutôt que tous anticipés en bloc.
|
RAW worker puis STRUCTURAL worker sont introduits à la fin de leur couche respective, lorsque persistence/backlog sont disponibles. Le traitement RAW -> STRUCTURAL borné est porté par un STRUCTURAL job distinct du service continu. Les workers DECODED/DOMAIN sont introduits avec les groupes Program concernés plutôt que tous anticipés en bloc.
|
||||||
|
|
||||||
## Apps
|
## Apps
|
||||||
|
|
||||||
@@ -485,7 +515,7 @@ ksp-app-market-desk
|
|||||||
-> KSP domain/query contracts
|
-> KSP domain/query contracts
|
||||||
```
|
```
|
||||||
|
|
||||||
Elle consomme les projections SPECIALIZED normalisées ; elle ne dépend pas directement des bibliothèques protocole externes.
|
Elle consomme les projections DOMAIN normalisées ; elle ne dépend pas directement des bibliothèques protocole externes.
|
||||||
|
|
||||||
## Scenarios
|
## Scenarios
|
||||||
|
|
||||||
@@ -506,7 +536,7 @@ L'app demo correspondante reste un adapter UI mince.
|
|||||||
|
|
||||||
## Progression verticale Program
|
## Progression verticale Program
|
||||||
|
|
||||||
À partir de DECODE :
|
À partir de DECODED :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wire
|
wire
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/006-WIRE_AND_PROGRAM.md -->
|
<!-- file: docs/architecture/006-WIRE_AND_PROGRAM.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Wire, Program API et implémentations Program
|
# Wire, Program API et implémentations Program
|
||||||
|
|
||||||
@@ -442,7 +442,7 @@ Une crate externe provoquant volontairement une génération incompatible de dé
|
|||||||
|
|
||||||
## Progression verticale par groupe
|
## Progression verticale par groupe
|
||||||
|
|
||||||
Après les couches RAW/CORE, les Program implementations ne sont pas développées horizontalement comme une longue liste de decoders isolés. Chaque groupe prioritaire avance successivement :
|
Après les couches RAW/STRUCTURAL, les Program implementations ne sont pas développées horizontalement comme une longue liste de decoders isolés. Chaque groupe prioritaire avance successivement :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wire -> decode -> materialize -> specialized -> execution preparation -> policy -> execution -> scenario
|
wire -> decode -> materialize -> specialized -> execution preparation -> policy -> execution -> scenario
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md -->
|
<!-- file: docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Data, Materialization et Store
|
# Data, Materialization et Store
|
||||||
|
|
||||||
@@ -10,19 +10,21 @@ Ce document définit la chaîne durable KSP, la responsabilité du Store et les
|
|||||||
La nomenclature canonique est désormais :
|
La nomenclature canonique est désormais :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Les aliases D1–D4 restent utilisés pour les niveaux persistés :
|
Les aliases D1–D4 restent utilisés pour les niveaux persistés :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D1 = RAW
|
D1 = RAW
|
||||||
D2 = CORE
|
D2 = STRUCTURAL
|
||||||
D3 = DECODE / matérialisation générique décodée
|
D3 = DECODED / matérialisation générique décodée
|
||||||
D4 = SPECIALIZED
|
D4 = DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Cette clarification remplace l'ancienne interprétation où D1 -> D2 pouvait déjà dépendre de `ksp-program-api`. **RAW et CORE sont indépendants de tout decoder Program.**
|
Le nom de couche historique `CORE` est abandonné pour D2 parce qu'il confondait la fondation commune `ksp-core-lib` avec une opération de décomposition structurelle du Store. `Core` reste inchangé lorsqu'il désigne la crate fondamentale ou un nom propre comme « Solana Core Programs ».
|
||||||
|
|
||||||
|
Cette clarification remplace l'ancienne interprétation où D1 -> D2 pouvait déjà dépendre de `ksp-program-api`. **RAW et STRUCTURAL sont indépendants de tout decoder Program.**
|
||||||
|
|
||||||
## Principes structurants
|
## Principes structurants
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ Cette clarification remplace l'ancienne interprétation où D1 -> D2 pouvait dé
|
|||||||
|
|
||||||
### Mission
|
### Mission
|
||||||
|
|
||||||
RAW conserve l'acquisition suffisamment fidèlement pour reconstruire CORE sans redemander la donnée au provider lorsqu'elle a déjà été capturée.
|
RAW conserve l'acquisition suffisamment fidèlement pour reconstruire STRUCTURAL sans redemander la donnée au provider lorsqu'elle a déjà été capturée.
|
||||||
|
|
||||||
Le transport peut normaliser plusieurs providers vers un modèle KSP homogène, mais D1 doit rester lossless pour les besoins de replay couverts.
|
Le transport peut normaliser plusieurs providers vers un modèle KSP homogène, mais D1 doit rester lossless pour les besoins de replay couverts.
|
||||||
|
|
||||||
@@ -77,15 +79,15 @@ Selon la catégorie, D1 doit pouvoir conserver notamment :
|
|||||||
- identité/hash d'idempotence ;
|
- identité/hash d'idempotence ;
|
||||||
- cursor/page/range/checkpoint lorsque pertinent.
|
- cursor/page/range/checkpoint lorsque pertinent.
|
||||||
|
|
||||||
## D2 — CORE
|
## D2 — STRUCTURAL
|
||||||
|
|
||||||
### Mission
|
### Mission
|
||||||
|
|
||||||
CORE est une **normalisation canonique générique de la blockchain Solana**.
|
STRUCTURAL est une **normalisation canonique générique de la blockchain Solana**.
|
||||||
|
|
||||||
Cette couche doit fonctionner même si `ksp-program-api` et `ksp-program-lib` ne sont pas encore capables de décoder le moindre programme métier.
|
Cette couche doit fonctionner même si `ksp-program-api` et `ksp-program-lib` ne sont pas encore capables de décoder le moindre programme métier.
|
||||||
|
|
||||||
Exemples de faits CORE candidats :
|
Exemples de faits STRUCTURAL candidats :
|
||||||
|
|
||||||
- slots ;
|
- slots ;
|
||||||
- blocks et block metadata ;
|
- blocks et block metadata ;
|
||||||
@@ -102,9 +104,9 @@ Exemples de faits CORE candidats :
|
|||||||
- return data brute ;
|
- return data brute ;
|
||||||
- relations structurelles transaction/message/instruction/account.
|
- relations structurelles transaction/message/instruction/account.
|
||||||
|
|
||||||
Un fait CORE peut contenir un `program_id`, des bytes et des indexes sans savoir que l'instruction représente un `Transfer`, un `Swap` ou une mutation Metadata.
|
Un fait STRUCTURAL peut contenir un `program_id`, des bytes et des indexes sans savoir que l'instruction représente un `Transfer`, un `Swap` ou une mutation Metadata.
|
||||||
|
|
||||||
### Frontière RAW -> CORE
|
### Frontière RAW -> STRUCTURAL
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
@@ -113,39 +115,39 @@ D1 RAW
|
|||||||
normalisation Solana générique
|
normalisation Solana générique
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
```
|
```
|
||||||
|
|
||||||
Interdictions :
|
Interdictions :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -X-> ksp-program-api
|
RAW -> STRUCTURAL -X-> ksp-program-api
|
||||||
RAW -> CORE -X-> ksp-program-lib
|
RAW -> STRUCTURAL -X-> ksp-program-lib
|
||||||
RAW -> CORE -X-> ksp-materializer-api
|
RAW -> STRUCTURAL -X-> ksp-materializer-api
|
||||||
```
|
```
|
||||||
|
|
||||||
Les codecs/wires génériques nécessaires à la structure Solana peuvent provenir de `ksp-interface-lib` lorsqu'ils appartiennent à la façade wire officielle, sans transformer cette étape en décodage Program.
|
Les codecs/wires génériques nécessaires à la structure Solana peuvent provenir de `ksp-interface-lib` lorsqu'ils appartiennent à la façade wire officielle, sans transformer cette étape en décodage Program.
|
||||||
|
|
||||||
### Provenance CORE
|
### Provenance STRUCTURAL
|
||||||
|
|
||||||
D2 doit pouvoir relier chaque résultat à :
|
D2 doit pouvoir relier chaque résultat à :
|
||||||
|
|
||||||
- son input D1 ;
|
- son input D1 ;
|
||||||
- l'identité/version du normalizer CORE ;
|
- l'identité/version du normalizer STRUCTURAL ;
|
||||||
- un hash logique d'input ;
|
- un hash logique d'input ;
|
||||||
- l'instant de processing/persistence ;
|
- l'instant de processing/persistence ;
|
||||||
- son état de processing durable lorsque nécessaire.
|
- son état de processing durable lorsque nécessaire.
|
||||||
|
|
||||||
## D3 — DECODE / matérialisation générique
|
## D3 — DECODED / matérialisation générique
|
||||||
|
|
||||||
### Mission
|
### Mission
|
||||||
|
|
||||||
DECODE commence lorsque KSP interprète un `program_id`, un layout d'instruction, un compte ou un événement selon un contrat Program/protocole.
|
DECODED commence lorsque KSP interprète un `program_id`, un layout d'instruction, un compte ou un événement selon un contrat Program/protocole.
|
||||||
|
|
||||||
La progression logique d'un groupe est :
|
La progression logique d'un groupe est :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CORE
|
STRUCTURAL
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
decoder Program
|
decoder Program
|
||||||
@@ -157,7 +159,7 @@ decoded facts
|
|||||||
materialisation générique / journal durable
|
materialisation générique / journal durable
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D3 DECODE
|
D3 DECODED
|
||||||
```
|
```
|
||||||
|
|
||||||
D3 conserve l'équivalent conceptuel obligatoire du journal générique de matérialisation de bot3 (`k_sol_mat_outputs`), sans imposer son ancien schéma ou son nom physique.
|
D3 conserve l'équivalent conceptuel obligatoire du journal générique de matérialisation de bot3 (`k_sol_mat_outputs`), sans imposer son ancien schéma ou son nom physique.
|
||||||
@@ -165,7 +167,7 @@ D3 conserve l'équivalent conceptuel obligatoire du journal générique de maté
|
|||||||
Le journal doit pouvoir répondre au minimum :
|
Le journal doit pouvoir répondre au minimum :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
quel input CORE ?
|
quel input STRUCTURAL ?
|
||||||
quel program/decoder ?
|
quel program/decoder ?
|
||||||
quelle version ?
|
quelle version ?
|
||||||
quel materializer ?
|
quel materializer ?
|
||||||
@@ -179,10 +181,10 @@ quel état/superseded/failed/replay ?
|
|||||||
|
|
||||||
Les types exacts de decoded facts et du journal sont décidés lorsque les premiers vertical slices Program existent.
|
Les types exacts de decoded facts et du journal sont décidés lorsque les premiers vertical slices Program existent.
|
||||||
|
|
||||||
### Frontière CORE -> DECODE
|
### Frontière STRUCTURAL -> DECODED
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
ksp-program-api implementation
|
ksp-program-api implementation
|
||||||
@@ -201,11 +203,11 @@ Les implémentations officielles pourront provenir de `ksp-program-lib` et `ksp-
|
|||||||
|
|
||||||
Program et Materializer ne dépendent pas du backend Store.
|
Program et Materializer ne dépendent pas du backend Store.
|
||||||
|
|
||||||
## D4 — SPECIALIZED
|
## D4 — DOMAIN
|
||||||
|
|
||||||
### Mission
|
### Mission
|
||||||
|
|
||||||
SPECIALIZED expose des projections queryables utiles aux applications, analyses et futurs modèles ML.
|
DOMAIN expose des projections queryables utiles aux applications, analyses et futurs modèles ML.
|
||||||
|
|
||||||
Exemples :
|
Exemples :
|
||||||
|
|
||||||
@@ -258,15 +260,15 @@ La direction reste :
|
|||||||
|
|
||||||
### OHLC
|
### OHLC
|
||||||
|
|
||||||
Les candles sont des projections SPECIALIZED calculées à partir des trades/price observations persistés.
|
Les candles sont des projections DOMAIN calculées à partir des trades/price observations persistés.
|
||||||
|
|
||||||
Une application marché lit les OHLC matérialisés ; elle ne reparcourt pas toutes les transactions pour reconstruire les candles à chaque affichage.
|
Une application marché lit les OHLC matérialisés ; elle ne reparcourt pas toutes les transactions pour reconstruire les candles à chaque affichage.
|
||||||
|
|
||||||
## Vertical slices Program
|
## Vertical slices Program
|
||||||
|
|
||||||
RAW et CORE sont développés horizontalement.
|
RAW et STRUCTURAL sont développés horizontalement.
|
||||||
|
|
||||||
À partir de DECODE, la progression est verticale par groupe :
|
À partir de DECODED, la progression est verticale par groupe :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
wire
|
wire
|
||||||
@@ -289,7 +291,7 @@ Les composants satellites nécessaires à un protocole appartiennent à son grou
|
|||||||
|
|
||||||
La première implementation `0.3.1` est volontairement **RAW-only** : elle ne crée pas prématurément les contrats physiques D2/D3/D4.
|
La première implementation `0.3.1` est volontairement **RAW-only** : elle ne crée pas prématurément les contrats physiques D2/D3/D4.
|
||||||
|
|
||||||
Les surfaces CORE/DECODE/SPECIALIZED sont ajoutées quand leurs couches sont réellement ouvertes.
|
Les surfaces STRUCTURAL/DECODED/DOMAIN sont ajoutées quand leurs couches sont réellement ouvertes.
|
||||||
|
|
||||||
## `ksp-store-lib` et backends physiques
|
## `ksp-store-lib` et backends physiques
|
||||||
|
|
||||||
@@ -342,7 +344,7 @@ La voie `RawInspectionPageRequest` est backend-neutral mais conçue pour une ins
|
|||||||
|
|
||||||
## `ksp-materializer-api` et `ksp-materializer-lib`
|
## `ksp-materializer-api` et `ksp-materializer-lib`
|
||||||
|
|
||||||
Ils sont introduits seulement lorsque le premier groupe DECODE démontre le contrat réel.
|
Ils sont introduits seulement lorsque le premier groupe DECODED démontre le contrat réel.
|
||||||
|
|
||||||
`ksp-materializer-api` porte les contrats extensibles ; `ksp-materializer-lib` contient les implementations officielles communes.
|
`ksp-materializer-api` porte les contrats extensibles ; `ksp-materializer-lib` contient les implementations officielles communes.
|
||||||
|
|
||||||
@@ -353,9 +355,9 @@ Une projection très locale/spécifique peut rester dans son groupe si la créat
|
|||||||
Les frontières durables restent replayables indépendamment :
|
Les frontières durables restent replayables indépendamment :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE
|
RAW -> STRUCTURAL
|
||||||
CORE -> DECODE
|
STRUCTURAL -> DECODED
|
||||||
DECODE -> SPECIALIZED
|
DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Un replay d'une couche dérivée ne doit pas refaire arbitrairement les couches précédentes.
|
Un replay d'une couche dérivée ne doit pas refaire arbitrairement les couches précédentes.
|
||||||
@@ -393,16 +395,16 @@ Ils ne dupliquent pas le contrat durable.
|
|||||||
La stabilité cible est différente selon la couche :
|
La stabilité cible est différente selon la couche :
|
||||||
|
|
||||||
- RAW : fortement stable après mise en production ;
|
- RAW : fortement stable après mise en production ;
|
||||||
- CORE : fortement stable après validation de la normalisation Solana générique ;
|
- STRUCTURAL : fortement stable après validation de la normalisation Solana générique ;
|
||||||
- DECODE : extensible par nouveaux Program/versions ;
|
- DECODED : extensible par nouveaux Program/versions ;
|
||||||
- SPECIALIZED : plus évolutif selon les besoins de query, trading et analytics.
|
- DOMAIN : plus évolutif selon les besoins de query, trading et analytics.
|
||||||
|
|
||||||
## Questions laissées ouvertes
|
## Questions laissées ouvertes
|
||||||
|
|
||||||
- schémas SQL exacts RAW puis CORE ;
|
- schémas SQL exacts RAW puis STRUCTURAL ;
|
||||||
- représentation persistable exacte d'un decoded output ;
|
- représentation persistable exacte d'un decoded output ;
|
||||||
- contrat exact du journal D3 ;
|
- contrat exact du journal D3 ;
|
||||||
- granularité des projectors SPECIALIZED ;
|
- granularité des projectors DOMAIN ;
|
||||||
- politique de supersession/versioning des outputs ;
|
- politique de supersession/versioning des outputs ;
|
||||||
- fenêtres OHLC initiales ;
|
- fenêtres OHLC initiales ;
|
||||||
- mécanisme de contexte pour les projections stateful.
|
- mécanisme de contexte pour les projections stateful.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md -->
|
<!-- file: docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 17 -->
|
||||||
|
|
||||||
# Acquisition, workers, jobs et pipelines spécialisés
|
# Acquisition, workers, jobs et pipelines spécialisés
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
Ce document définit le lifecycle opérationnel autour des couches :
|
Ce document définit le lifecycle opérationnel autour des couches :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Il conserve la séparation stricte entre :
|
Il conserve la séparation stricte entre :
|
||||||
@@ -20,7 +20,7 @@ Il conserve la séparation stricte entre :
|
|||||||
|
|
||||||
## Règle de progression
|
## Règle de progression
|
||||||
|
|
||||||
RAW et CORE sont les deux premières couches horizontales. Elles ne nécessitent aucun decoder Program.
|
RAW et STRUCTURAL sont les deux premières couches horizontales. Elles ne nécessitent aucun decoder Program.
|
||||||
|
|
||||||
Pour chacune, KSP peut terminer successivement :
|
Pour chacune, KSP peut terminer successivement :
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ persistence
|
|||||||
-> application de contrôle/inspection si utile
|
-> application de contrôle/inspection si utile
|
||||||
```
|
```
|
||||||
|
|
||||||
À partir de DECODE, les processors/jobs/workers/scenarios sont introduits **avec le groupe Program concerné**, en vertical slice, au lieu de créer à l'avance une grande flotte générique de workers de décodage/materialisation sans programme réel.
|
À partir de DECODED, les processors/jobs/workers/scenarios sont introduits **avec le groupe Program concerné**, en vertical slice, au lieu de créer à l'avance une grande flotte générique de workers de décodage/materialisation sans programme réel.
|
||||||
|
|
||||||
## RAW
|
## RAW
|
||||||
|
|
||||||
@@ -86,11 +86,11 @@ Le job :
|
|||||||
- limite les hydrations concurrentes, avance seulement une frontier contiguë durable et retourne un checkpoint opaque caller-owned ;
|
- limite les hydrations concurrentes, avance seulement une frontier contiguë durable et retourne un checkpoint opaque caller-owned ;
|
||||||
- arrête coopérativement les nouvelles admissions lors d'une annulation et draine une persistence Store déjà soumise ;
|
- arrête coopérativement les nouvelles admissions lors d'une annulation et draine une persistence Store déjà soumise ;
|
||||||
- publie des snapshots latest-value sûrs sans payload RAW ni secrets/URLs Transport ;
|
- publie des snapshots latest-value sûrs sans payload RAW ni secrets/URLs Transport ;
|
||||||
- n'effectue aucun décodage Program et n'écrit aucun fait CORE/DECODE/SPECIALIZED.
|
- n'effectue aucun décodage Program et n'écrit aucun fait STRUCTURAL/DECODED/DOMAIN.
|
||||||
|
|
||||||
Le caller desktop spécialisé actuel est `ksp-app-backfill-desk`. Il compose Config, le pool HTTP et Store, puis remet ces ressources au runtime Backfill. Il peut retenir le checkpoint terminal uniquement en mémoire Rust pour un Resume in-session ; cette rétention applicative ne transforme pas le checkpoint en garantie de reprise durable après redémarrage.
|
Le caller desktop spécialisé actuel est `ksp-app-backfill-desk`. Il compose Config, le pool HTTP et Store, puis remet ces ressources au runtime Backfill. Il peut retenir le checkpoint terminal uniquement en mémoire Rust pour un Resume in-session ; cette rétention applicative ne transforme pas le checkpoint en garantie de reprise durable après redémarrage.
|
||||||
|
|
||||||
Cette verticale `0.3.6`/`0.3.7` est **la première stratégie de backfill**, pas la définition générale du backfill KSP. Son discovery `getSignaturesForAddress` + hydration `getTransaction` est HTTP parce que cette méthode a été choisie pour le premier vertical slice. Après stabilisation du worker live et de sa Desk, `0.3.12` doit réauditer `ksp-job-backfill-lib` et `ksp-app-backfill-desk` pour intégrer les autres stratégies historiques/catch-up pertinentes identifiées par l'audit RAW Transaction de `0.3.9`.
|
Cette verticale `0.3.6`/`0.3.7` est **la première stratégie de backfill**, pas la définition générale du backfill KSP. Son discovery `getSignaturesForAddress` + hydration `getTransaction` est HTTP parce que cette méthode a été choisie pour le premier vertical slice. Après stabilisation du worker live et de sa Desk, `0.3.16` doit réauditer `ksp-job-backfill-lib` et `ksp-app-backfill-desk` pour intégrer les autres stratégies historiques/catch-up pertinentes identifiées par l'audit RAW Transaction de `0.3.9`.
|
||||||
|
|
||||||
### Worker RAW Transaction live
|
### Worker RAW Transaction live
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ La présence d'une voie dans l'architecture signifie qu'elle doit pouvoir être
|
|||||||
Pour chaque voie, l'audit couvre au minimum :
|
Pour chaque voie, l'audit couvre au minimum :
|
||||||
|
|
||||||
| Dimension | Question à trancher |
|
| Dimension | Question à trancher |
|
||||||
|----------------------|-----------------------------------------------------------------------|
|
|----------------------|-----------------------------------------------------------------------------|
|
||||||
| transport/protocole | HTTP, WS standard, extension provider, Yellowstone ou autre ? |
|
| transport/protocole | HTTP, WS standard, extension provider, Yellowstone ou autre ? |
|
||||||
| réseau | Mainnet, Devnet, Testnet réellement disponibles et utiles ? |
|
| réseau | Mainnet, Devnet, Testnet réellement disponibles et utiles ? |
|
||||||
| disponibilité | gratuite/payante/provider-dependent ; limites actuelles à réauditer ? |
|
| disponibilité | gratuite/payante/provider-dependent ; limites actuelles à réauditer ? |
|
||||||
@@ -179,7 +179,7 @@ Pour chaque voie, l'audit couvre au minimum :
|
|||||||
| commitment/finality | quels niveaux sont disponibles et comment les interpréter ? |
|
| commitment/finality | quels niveaux sont disponibles et comment les interpréter ? |
|
||||||
| provenance | quelles métadonnées sûres alimentent `RawTransactionObservation` ? |
|
| provenance | quelles métadonnées sûres alimentent `RawTransactionObservation` ? |
|
||||||
| limites/quota | RPS, connexions, subscriptions, credits ou autres limites actuelles ? |
|
| limites/quota | RPS, connexions, subscriptions, credits ou autres limites actuelles ? |
|
||||||
| gap KSP Transport | surface déjà disponible ou adaptation nécessaire en `0.3.10` ? |
|
| gap KSP Transport | surface déjà disponible ou adaptation nécessaire dans `0.3.10` à `0.3.14` ? |
|
||||||
| gap KSP Config | profil/secret/capability déjà disponible ou adaptation nécessaire ? |
|
| gap KSP Config | profil/secret/capability déjà disponible ou adaptation nécessaire ? |
|
||||||
| usage | continuous ingest, gap repair, historical backfill ou combinaison ? |
|
| usage | continuous ingest, gap repair, historical backfill ou combinaison ? |
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ Décisions déjà acquises :
|
|||||||
```text
|
```text
|
||||||
KSP_SECRET_HELIUS_API_KEY existe déjà côté environnement KSP
|
KSP_SECRET_HELIUS_API_KEY existe déjà côté environnement KSP
|
||||||
Helius HTTP et WS Mainnet/Devnet doivent être considérés comme futures sources candidates
|
Helius HTTP et WS Mainnet/Devnet doivent être considérés comme futures sources candidates
|
||||||
les profils/endpoints réellement nécessaires sont ajoutés seulement en 0.3.10
|
les profils/endpoints réellement nécessaires sont ajoutés seulement dans la release Worker qui les consomme, principalement 0.3.13/0.3.14
|
||||||
aucune URL Helius nouvelle n'est ajoutée pendant 0.3.8
|
aucune URL Helius nouvelle n'est ajoutée pendant 0.3.8
|
||||||
Config reste l'unique propriétaire des secrets et de leur résolution
|
Config reste l'unique propriétaire des secrets et de leur résolution
|
||||||
les fonctionnalités standard et advanced/enhanced sont capability-gated, jamais supposées par le seul nom du provider
|
les fonctionnalités standard et advanced/enhanced sont capability-gated, jamais supposées par le seul nom du provider
|
||||||
@@ -204,7 +204,7 @@ L'archive kbot3 doit être relue uniquement comme **référence fonctionnelle**
|
|||||||
|
|
||||||
L'audit `0.3.9` a conclu que `mainnet` est l'identité logique canonique KSP du réseau de production Solana. `mainnet-beta` reste un alias legacy/externe ou un libellé provider lorsqu'une API externe l'emploie réellement ; il ne constitue plus l'identité persistée cible de Store/RAW/Config.
|
L'audit `0.3.9` a conclu que `mainnet` est l'identité logique canonique KSP du réseau de production Solana. `mainnet-beta` reste un alias legacy/externe ou un libellé provider lorsqu'une API externe l'emploie réellement ; il ne constitue plus l'identité persistée cible de Store/RAW/Config.
|
||||||
|
|
||||||
Depuis `0.3.9-pre.006-fix.003`, les profils Mainnet engagés dans Config/Store/Transport utilisent `mainnet`, de même que les tests et exemples runtime associés. KSP ne crée donc pas deux identités persistées pour le même cluster. Les anciennes données N1 RAW portant `mainnet-beta` sont considérées comme expérimentales et peuvent être droppées/recréées ; aucune migration destructive n'est imposée avant finalisation des Jobs/Workers RAW.
|
Depuis `0.3.9-pre.006-fix.003`, les profils Mainnet engagés dans Config/Store/Transport utilisent `mainnet`, de même que les tests et exemples runtime associés. KSP ne crée donc pas deux identités persistées pour le même cluster. Les anciennes données D1 RAW portant `mainnet-beta` sont considérées comme expérimentales et peuvent être droppées/recréées ; aucune migration destructive n'est imposée avant finalisation des Jobs/Workers RAW.
|
||||||
|
|
||||||
Les frontières externes restent libres de documenter ou d'accepter un nom provider legacy lorsque nécessaire, sans recopier ce nom dans `RawNetworkId` canonique.
|
Les frontières externes restent libres de documenter ou d'accepter un nom provider legacy lorsque nécessaire, sans recopier ce nom dans `RawNetworkId` canonique.
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ La déduplication ne doit donc pas supprimer la provenance utile sous prétexte
|
|||||||
|
|
||||||
La Desk prévue après le worker choisit et supervise les **source(s)/méthode(s)** offertes par la composition réellement disponible. Elle ne possède pas la logique de discovery, hydration, déduplication, replay ou persistance.
|
La Desk prévue après le worker choisit et supervise les **source(s)/méthode(s)** offertes par la composition réellement disponible. Elle ne possède pas la logique de discovery, hydration, déduplication, replay ou persistance.
|
||||||
|
|
||||||
Elle doit pouvoir représenter selon les capacités finales de `0.3.10` :
|
Elle doit pouvoir représenter selon les capacités finales du Worker après `0.3.14` :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
une source unique
|
une source unique
|
||||||
@@ -237,11 +237,11 @@ une stratégie live + gap repair
|
|||||||
|
|
||||||
Le détail des RAW persistés reste la responsabilité de Store Desk.
|
Le détail des RAW persistés reste la responsabilité de Store Desk.
|
||||||
|
|
||||||
## CORE
|
## STRUCTURAL
|
||||||
|
|
||||||
### Pipeline RAW -> CORE
|
### Pipeline RAW -> STRUCTURAL
|
||||||
|
|
||||||
La normalisation CORE est générique Solana :
|
La normalisation STRUCTURAL est générique Solana :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
@@ -250,20 +250,20 @@ D1 RAW
|
|||||||
Solana generic normalizer
|
Solana generic normalizer
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
```
|
```
|
||||||
|
|
||||||
Dépendances interdites :
|
Dépendances interdites :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CORE normalizer -X-> ksp-program-api
|
STRUCTURAL normalizer -X-> ksp-program-api
|
||||||
CORE normalizer -X-> ksp-program-lib
|
STRUCTURAL normalizer -X-> ksp-program-lib
|
||||||
CORE normalizer -X-> ksp-materializer-api
|
STRUCTURAL normalizer -X-> ksp-materializer-api
|
||||||
```
|
```
|
||||||
|
|
||||||
`ksp-interface-lib` peut fournir les wires Solana génériques nécessaires à la structure blockchain.
|
`ksp-interface-lib` peut fournir les wires Solana génériques nécessaires à la structure blockchain.
|
||||||
|
|
||||||
### Job replay CORE
|
### STRUCTURAL job
|
||||||
|
|
||||||
Un job borné peut rejouer :
|
Un job borné peut rejouer :
|
||||||
|
|
||||||
@@ -271,21 +271,21 @@ Un job borné peut rejouer :
|
|||||||
RAW persisted range
|
RAW persisted range
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
CORE normalizer
|
STRUCTURAL normalizer
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
```
|
```
|
||||||
|
|
||||||
sans redemander les données au réseau.
|
sans redemander les données au réseau.
|
||||||
|
|
||||||
### Worker CORE
|
### STRUCTURAL worker
|
||||||
|
|
||||||
Un worker CORE continu peut consommer le backlog RAW nouvellement persisté et produire CORE.
|
Le STRUCTURAL worker continu peut consommer le backlog RAW nouvellement persisté et produire STRUCTURAL.
|
||||||
|
|
||||||
Le Store reste source de vérité du backlog ; les notifications ne sont qu'un wake-up.
|
Le Store reste source de vérité du backlog ; les notifications ne sont qu'un wake-up.
|
||||||
|
|
||||||
## DECODE et SPECIALIZED
|
## DECODED et DOMAIN
|
||||||
|
|
||||||
### Introduction par groupe fonctionnel
|
### Introduction par groupe fonctionnel
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ KSP ne crée pas d'abord un unique « worker decoder de tout Solana » puis tous
|
|||||||
Chaque groupe prioritaire introduit les capacités nécessaires :
|
Chaque groupe prioritaire introduit les capacités nécessaires :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CORE inputs du groupe
|
STRUCTURAL inputs du groupe
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
Program decoder
|
Program decoder
|
||||||
@@ -303,10 +303,10 @@ Program decoder
|
|||||||
decoded facts
|
decoded facts
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
generic materialization / DECODE persistence
|
generic materialization / DECODED persistence
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
SPECIALIZED projection si utile
|
DOMAIN projection si utile
|
||||||
```
|
```
|
||||||
|
|
||||||
Puis le même groupe avance vers préparation d'exécution, policy, execution et scénarios Devnet.
|
Puis le même groupe avance vers préparation d'exécution, policy, execution et scénarios Devnet.
|
||||||
@@ -499,10 +499,10 @@ Le Worker conserve seul son runtime continu, ses sources actives, sa continuité
|
|||||||
|
|
||||||
Les événements Interface peuvent servir de signal provider-neutral à la composition live, mais ne constituent jamais le backlog durable. Après crash ou perte d'un événement, la reprise s'appuie sur Store et sur les primitives de replay/hydratation appropriées.
|
Les événements Interface peuvent servir de signal provider-neutral à la composition live, mais ne constituent jamais le backlog durable. Après crash ou perte d'un événement, la reprise s'appuie sur Store et sur les primitives de replay/hydratation appropriées.
|
||||||
|
|
||||||
### CORE replay/worker
|
### STRUCTURAL job/worker
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CORE processor
|
STRUCTURAL job / STRUCTURAL worker
|
||||||
-> ksp-interface-lib si wires génériques nécessaires
|
-> ksp-interface-lib si wires génériques nécessaires
|
||||||
-> ksp-store-api
|
-> ksp-store-api
|
||||||
-> ksp-core-lib
|
-> ksp-core-lib
|
||||||
@@ -511,7 +511,7 @@ CORE processor
|
|||||||
|
|
||||||
Pas de Program API.
|
Pas de Program API.
|
||||||
|
|
||||||
### Groupes DECODE/SPECIALIZED
|
### Groupes DECODED/DOMAIN
|
||||||
|
|
||||||
Le composant de composition du groupe peut utiliser :
|
Le composant de composition du groupe peut utiliser :
|
||||||
|
|
||||||
@@ -530,5 +530,5 @@ selon les capacités réellement introduites.
|
|||||||
- politique d'alias externe `mainnet-beta` à matérialiser uniquement aux frontières qui en ont réellement besoin, sans créer une seconde identité Store ;
|
- politique d'alias externe `mainnet-beta` à matérialiser uniquement aux frontières qui en ont réellement besoin, sans créer une seconde identité Store ;
|
||||||
- modèle de claim/lease PostgreSQL pour les futurs processors continus ;
|
- modèle de claim/lease PostgreSQL pour les futurs processors continus ;
|
||||||
- taille de batch et stratégie backpressure des workers de processing ;
|
- taille de batch et stratégie backpressure des workers de processing ;
|
||||||
- découpage des workers DECODE/SPECIALIZED par groupe lorsque les premiers groupes existent ;
|
- découpage des workers DECODED/DOMAIN par groupe lorsque les premiers groupes existent ;
|
||||||
- mécanisme IPC des applications de contrôle futures.
|
- mécanisme IPC des applications de contrôle futures.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md -->
|
<!-- file: docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# Applications, services, scenarios et control plane
|
# Applications, services, scenarios et control plane
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ Elle ne doit pas réimplémenter :
|
|||||||
|
|
||||||
## Applications de validation par couche
|
## Applications de validation par couche
|
||||||
|
|
||||||
KSP peut ajouter une petite application spécialisée à la fin d'une couche RAW ou CORE lorsque cela permet de valider et exploiter réellement la couche avant de passer à la suivante. Ces applications lisent les contrats KSP et ne recopient pas les processors dans Tauri.
|
KSP peut ajouter une petite application spécialisée à la fin d'une couche RAW ou STRUCTURAL lorsque cela permet de valider et exploiter réellement la couche avant de passer à la suivante. Ces applications lisent les contrats KSP et ne recopient pas les processors dans Tauri.
|
||||||
|
|
||||||
À partir des vertical slices Program, les applications restent attachées aux besoins réels : demos de scenarios pour l'exécution et Market Desk pour les projections de marché.
|
À partir des vertical slices Program, les applications restent attachées aux besoins réels : demos de scenarios pour l'exécution et Market Desk pour les projections de marché.
|
||||||
|
|
||||||
@@ -197,13 +197,13 @@ Exemples conceptuels :
|
|||||||
```text
|
```text
|
||||||
ksp-worker-raw-transaction-ingest-lib # premier runtime worker réutilisable retenu
|
ksp-worker-raw-transaction-ingest-lib # premier runtime worker réutilisable retenu
|
||||||
future autonomous raw-ingest binary # seulement si un besoin de service séparé le justifie
|
future autonomous raw-ingest binary # seulement si un besoin de service séparé le justifie
|
||||||
future CORE worker
|
future STRUCTURAL worker
|
||||||
future group-specific DECODE/SPECIALIZED workers when justified
|
future group-specific DECODED/DOMAIN workers when justified
|
||||||
```
|
```
|
||||||
|
|
||||||
Le premier worker RAW est volontairement prévu comme bibliothèque réutilisable afin qu'une Desk ou un futur host puisse le construire sans dupliquer sa logique. Un binaire autonome n'est pas créé par convention seule ; s'il apparaît, il reste un host mince au-dessus de la même bibliothèque et de `ksp-worker-api`.
|
Le premier worker RAW est volontairement prévu comme bibliothèque réutilisable afin qu'une Desk ou un futur host puisse le construire sans dupliquer sa logique. Un binaire autonome n'est pas créé par convention seule ; s'il apparaît, il reste un host mince au-dessus de la même bibliothèque et de `ksp-worker-api`.
|
||||||
|
|
||||||
RAW et CORE reçoivent leurs workers à la fin de leur couche respective. Les workers DECODE/SPECIALIZED ne sont plus tous anticipés comme une chaîne globale fixe : leur granularité doit émerger des premiers vertical slices Program.
|
RAW reçoit son worker d’ingestion à la fin de sa couche ; STRUCTURAL reçoit ensuite son STRUCTURAL worker lorsque sa persistence/backlog sont disponibles. Les workers DECODED/DOMAIN ne sont plus tous anticipés comme une chaîne globale fixe : leur granularité doit émerger des premiers vertical slices Program.
|
||||||
|
|
||||||
Le binaire, lorsqu'il existe, doit rester mince.
|
Le binaire, lorsqu'il existe, doit rester mince.
|
||||||
|
|
||||||
@@ -247,16 +247,16 @@ transport / acquisition
|
|||||||
D1 RAW
|
D1 RAW
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D2 CORE
|
D2 STRUCTURAL
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D3 DECODE
|
D3 DECODED
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
D4 SPECIALIZED
|
D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Ce schéma décrit les **frontières de données**, pas quatre workers globaux imposés. RAW et CORE peuvent disposer de workers horizontaux propres à leur couche. À partir de DECODE, la granularité des workers/processors émerge des groupes fonctionnels verticaux réellement introduits ; plusieurs groupes peuvent donc posséder des lifecycle hosts distincts sans qu'un `W3` ou `W4` universel existe.
|
Ce schéma décrit les **frontières de données**, pas quatre workers globaux imposés. RAW et STRUCTURAL peuvent disposer de workers horizontaux propres à leur couche. À partir de DECODED, la granularité des workers/processors émerge des groupes fonctionnels verticaux réellement introduits ; plusieurs groupes peuvent donc posséder des lifecycle hosts distincts sans qu'un `W3` ou `W4` universel existe.
|
||||||
|
|
||||||
Les notifications accélèrent le réveil mais ne créent pas une connexion fonctionnelle worker-to-worker.
|
Les notifications accélèrent le réveil mais ne créent pas une connexion fonctionnelle worker-to-worker.
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ Le data plane transporte/persiste les données Solana et les résultats de proce
|
|||||||
ksp-onchain-transport-lib
|
ksp-onchain-transport-lib
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
RAW -> CORE -> DECODE -> SPECIALIZED
|
RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
avec notifications de données persistées comme wake-up.
|
avec notifications de données persistées comme wake-up.
|
||||||
@@ -493,7 +493,7 @@ Elle ne modifie jamais directement l'état interne du worker dans PostgreSQL. El
|
|||||||
|
|
||||||
Le premier déploiement peut piloter `ksp-worker-raw-transaction-ingest-lib` in-process si cela reste le choix le plus simple et le plus sûr. La sémantique de `ksp-worker-api` doit néanmoins rester compatible avec un futur proxy vers un worker autonome ; l'IPC/remote control ne doit pas être anticipé artificiellement avant besoin réel.
|
Le premier déploiement peut piloter `ksp-worker-raw-transaction-ingest-lib` in-process si cela reste le choix le plus simple et le plus sûr. La sémantique de `ksp-worker-api` doit néanmoins rester compatible avec un futur proxy vers un worker autonome ; l'IPC/remote control ne doit pas être anticipé artificiellement avant besoin réel.
|
||||||
|
|
||||||
La Desk RAW Transaction doit laisser choisir **une ou plusieurs sources/méthodes** parmi les capabilities effectivement configurées/admissibles par `0.3.10`, et superviser leur état sans dupliquer discovery/hydration/déduplication/recovery dans Tauri. Store Desk reste la surface d'inspection détaillée des RAW persistés.
|
La Desk RAW Transaction doit laisser choisir **une ou plusieurs sources/méthodes** parmi les capabilities effectivement configurées/admissibles par le Worker finalisé en `0.3.14`, et superviser leur état sans dupliquer discovery/hydration/déduplication/recovery dans Tauri. Store Desk reste la surface d'inspection détaillée des RAW persistés.
|
||||||
|
|
||||||
## Configuration desired vs effective
|
## Configuration desired vs effective
|
||||||
|
|
||||||
@@ -516,11 +516,11 @@ Une app spécialisée peut éditer une configuration puis demander son applicati
|
|||||||
|
|
||||||
Après les groupes Meteora/Raydium/Pump/Orca, KSP prévoit une première application spécialisée candidate `ksp-app-market-desk`.
|
Après les groupes Meteora/Raydium/Pump/Orca, KSP prévoit une première application spécialisée candidate `ksp-app-market-desk`.
|
||||||
|
|
||||||
V1 peut afficher tokens, pools/markets, liquidité, trades/swaps, prix, volumes, OHLC et activité live/récente à partir des projections SPECIALIZED et des contrats KSP. Elle ne dépend pas directement des SDK/protocoles DEX pour reconstruire leurs modèles dans l'UI.
|
V1 peut afficher tokens, pools/markets, liquidité, trades/swaps, prix, volumes, OHLC et activité live/récente à partir des projections DOMAIN et des contrats KSP. Elle ne dépend pas directement des SDK/protocoles DEX pour reconstruire leurs modèles dans l'UI.
|
||||||
|
|
||||||
Après Jupiter/OKX, la même application est enrichie avec routes, legs, DEX impliqués, fees/slippage et comparaison quote/execution lorsqu'elle existe.
|
Après Jupiter/OKX, la même application est enrichie avec routes, legs, DEX impliqués, fees/slippage et comparaison quote/execution lorsqu'elle existe.
|
||||||
|
|
||||||
Les OHLC sont matérialisés dans SPECIALIZED et consommés par l'application; ils ne sont pas recalculés à partir de tout l'historique lors de chaque rendu.
|
Les OHLC sont matérialisés dans DOMAIN et consommés par l'application; ils ne sont pas recalculés à partir de tout l'historique lors de chaque rendu.
|
||||||
|
|
||||||
## Future orchestrator
|
## Future orchestrator
|
||||||
|
|
||||||
@@ -575,7 +575,7 @@ control/application adapters
|
|||||||
Data plane séparé :
|
Data plane séparé :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
transport -> RAW -> CORE -> DECODE -> SPECIALIZED
|
transport -> RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
Aucun payload de processing n'a besoin de transiter via l'UI/control plane.
|
Aucun payload de processing n'a besoin de transiter via l'UI/control plane.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md -->
|
<!-- file: docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Acquisition et alimentation `RawTransaction`
|
# Acquisition et alimentation `RawTransaction`
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ Une source concrète peut fournir plusieurs capabilities. Une capability peut ê
|
|||||||
## 5. Matrice des méthodes et de leur applicabilité
|
## 5. Matrice des méthodes et de leur applicabilité
|
||||||
|
|
||||||
| Famille / méthode | Contenu obtenu | RAW complet | Usage Worker live | Usage Job Backfill | Remarque |
|
| Famille / méthode | Contenu obtenu | RAW complet | Usage Worker live | Usage Job Backfill | Remarque |
|
||||||
|-----------------------------------------------------------------|---------------------------------------|------------------------------|------------------------------------------------------------|---------------------------------------------|------------------------------------------------------|
|
|-----------------------------------------------------------------|---------------------------------------|------------------------------|------------------------------------------------------------|---------------------------------------------|------------------------------------------------------------|
|
||||||
| HTTP `getTransaction(signature)` | transaction depuis signature connue | oui si disponible | oui, hydration d'un signal live | oui, hydration historique | dépend de la rétention du RPC |
|
| HTTP `getTransaction(signature)` | transaction depuis signature connue | oui si disponible | oui, hydration d'un signal live | oui, hydration historique | dépend de la rétention du RPC |
|
||||||
| HTTP `getSignaturesForAddress` + `getTransaction` | discovery adressée puis transaction | oui après hydration | non comme campagne de scan | oui, stratégie historique principale | naturellement paramétré par adresse/programme |
|
| HTTP `getSignaturesForAddress` + `getTransaction` | discovery adressée puis transaction | oui après hydration | non comme campagne de scan | oui, stratégie historique principale | naturellement paramétré par adresse/programme |
|
||||||
| HTTP `getBlocks` / `getBlocksWithLimit` | slots confirmés | non | oui pour suivre/recoller le frontier courant si nécessaire | oui pour énumérer une plage historique | discovery par slots |
|
| HTTP `getBlocks` / `getBlocksWithLimit` | slots confirmés | non | oui pour suivre/recoller le frontier courant si nécessaire | oui pour énumérer une plage historique | discovery par slots |
|
||||||
@@ -247,8 +247,8 @@ Une source concrète peut fournir plusieurs capabilities. Une capability peut ê
|
|||||||
| HTTP `getSlot` / `getFirstAvailableBlock` / `minimumLedgerSlot` | bornes de ledger | non | oui, continuité | oui, admission d'une campagne | aucune transaction directe |
|
| HTTP `getSlot` / `getFirstAvailableBlock` / `minimumLedgerSlot` | bornes de ledger | non | oui, continuité | oui, admission d'une campagne | aucune transaction directe |
|
||||||
| WS `logsSubscribe` | signature + logs | non | oui, discovery live + hydration | non pour historique pur | `mentions` standard limité à un pubkey |
|
| WS `logsSubscribe` | signature + logs | non | oui, discovery live + hydration | non pour historique pur | `mentions` standard limité à un pubkey |
|
||||||
| WS `signatureSubscribe` | statut d'une signature connue | non | oui, confirmation ciblée interne | possible pour une requête ciblée en attente | one-shot |
|
| WS `signatureSubscribe` | statut d'une signature connue | non | oui, confirmation ciblée interne | possible pour une requête ciblée en attente | one-shot |
|
||||||
| WS `blockSubscribe` full | bloc + transactions | oui | oui | non sans mécanisme de replay historique | méthode standard instable |
|
| WS `blockSubscribe` full/base64, legacy-v0 | bloc + transactions | oui, parité RAW v1 prouvée | oui, direct pour le sous-ensemble qualifié | non sans mécanisme de replay historique | méthode standard instable ; capability validator-dependent |
|
||||||
| Helius `transactionSubscribe` full | transaction + meta | oui | oui | non comme source historique | extension provider |
|
| Helius `transactionSubscribe` full/base64 | transaction + meta + signature/index | non sans hydration | oui, signal riche puis hydration HTTP | non comme source historique | absence de blockTime/version dans l’enveloppe qualifiée |
|
||||||
| Yellowstone `transactions` | transaction exécutée + meta | oui | oui | oui si replay borné demandé/disponible | filters server-side |
|
| Yellowstone `transactions` | transaction exécutée + meta | oui | oui | oui si replay borné demandé/disponible | filters server-side |
|
||||||
| Yellowstone `blocks` avec transactions | bloc + transactions | oui | oui | oui si replay borné demandé/disponible | utile au live et au backfill |
|
| Yellowstone `blocks` avec transactions | bloc + transactions | oui | oui | oui si replay borné demandé/disponible | utile au live et au backfill |
|
||||||
| Yellowstone `transactions_status` | signature/status/error | non | oui + hydration | oui si replay/filtre borné | discovery/statut uniquement |
|
| Yellowstone `transactions_status` | signature/status/error | non | oui + hydration | oui si replay/filtre borné | discovery/statut uniquement |
|
||||||
@@ -273,9 +273,8 @@ Sources capables de produire directement un matériau transactionnel complet :
|
|||||||
```text
|
```text
|
||||||
Yellowstone transactions
|
Yellowstone transactions
|
||||||
Yellowstone blocks avec transactions
|
Yellowstone blocks avec transactions
|
||||||
WS blockSubscribe full
|
WS blockSubscribe full/base64 legacy-v0 qualifié
|
||||||
Helius transactionSubscribe full
|
provider stream compatible full transaction après parité prouvée
|
||||||
provider stream compatible full transaction
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Chemin logique :
|
Chemin logique :
|
||||||
@@ -539,13 +538,13 @@ KSP possède déjà :
|
|||||||
get_signatures_for_address
|
get_signatures_for_address
|
||||||
get_transaction / get_transaction_observed
|
get_transaction / get_transaction_observed
|
||||||
get_blocks / get_blocks_with_limit
|
get_blocks / get_blocks_with_limit
|
||||||
get_block
|
get_block / get_block_observed
|
||||||
get_slot
|
get_slot
|
||||||
get_first_available_block
|
get_first_available_block
|
||||||
minimum_ledger_slot
|
minimum_ledger_slot
|
||||||
```
|
```
|
||||||
|
|
||||||
Gap principal : `getBlock` n'a pas encore d'équivalent `get_block_observed`. Pour un pool multi-endpoint, la provenance provider/endpoint exacte doit être conservée avant de persister une observation.
|
`get_block_observed` conserve désormais la provenance sûre provider/endpoint du winner réel d’un pool HTTP, symétriquement à `get_transaction_observed`. La projection d’une transaction full issue d’un bloc vers le matériau RAW commun reste volontairement hors de Transport : l’adapter productif appartient au producer (Worker/Job) afin de ne créer aucun edge `Transport -> ksp-raw-transaction-lib`.
|
||||||
|
|
||||||
### 12.2 Transport WebSocket
|
### 12.2 Transport WebSocket
|
||||||
|
|
||||||
@@ -562,6 +561,8 @@ Helius transactionSubscribe
|
|||||||
|
|
||||||
Le runtime WS possède des queues bornées et une logique de reconnect/resubscribe. Un reconnect ne constitue toutefois pas un replay adressable.
|
Le runtime WS possède des queues bornées et une logique de reconnect/resubscribe. Un reconnect ne constitue toutefois pas un replay adressable.
|
||||||
|
|
||||||
|
La qualification `pre.005` ferme deux cas distincts : `blockSubscribe` standard en `full/base64` avec `maxSupportedTransactionVersion = 0` produit, sur fixture identique, les mêmes bytes/hash RAW v1 que `getBlock`; Helius `transactionSubscribe` full/base64 conserve l’identité, le slot, le transaction wire, la meta et l’index mais ne transporte pas `blockTime` ni `version` dans l’enveloppe qualifiée. Helius reste donc un signal live riche à hydrater par HTTP avant persistence RAW directe.
|
||||||
|
|
||||||
### 12.3 Yellowstone gRPC
|
### 12.3 Yellowstone gRPC
|
||||||
|
|
||||||
Le moteur KSP expose déjà les familles nécessaires :
|
Le moteur KSP expose déjà les familles nécessaires :
|
||||||
@@ -609,13 +610,13 @@ priorités/composition
|
|||||||
|
|
||||||
Les prix/tiers d'audit ne doivent pas devenir une politique runtime.
|
Les prix/tiers d'audit ne doivent pas devenir une politique runtime.
|
||||||
|
|
||||||
`mainnet` est l'identité réseau durable KSP. `mainnet-beta` reste uniquement un alias de compatibilité/historique ou un libellé externe lorsqu'un provider/API l'emploie. Depuis `0.3.9-pre.006-fix.003`, les profils Config/Store/Transport Mainnet engagés utilisent `mainnet` comme identité logique, et les exemples/tests associés ont été normalisés. Les endpoints publics Solana engagés suivent également la nomenclature Mainnet courante (`https://api.mainnet.solana.com` et `wss://api.mainnet.solana.com`). Aucune migration de données N1 n'est exigée : les données RAW Mainnet encore expérimentales peuvent être droppées/recréées si elles portent l'ancienne identité.
|
`mainnet` est l'identité réseau durable KSP. `mainnet-beta` reste uniquement un alias de compatibilité/historique ou un libellé externe lorsqu'un provider/API l'emploie. Depuis `0.3.9-pre.006-fix.003`, les profils Config/Store/Transport Mainnet engagés utilisent `mainnet` comme identité logique, et les exemples/tests associés ont été normalisés. Les endpoints publics Solana engagés suivent également la nomenclature Mainnet courante (`https://api.mainnet.solana.com` et `wss://api.mainnet.solana.com`). Aucune migration de données D1 n'est exigée : les données RAW Mainnet encore expérimentales peuvent être droppées/recréées si elles portent l'ancienne identité.
|
||||||
|
|
||||||
## 13. Normalisation RAW commune sans couplage Job/Worker
|
## 13. Normalisation RAW commune sans couplage Job/Worker
|
||||||
|
|
||||||
La canonicalisation RAW v1 actuellement prouvée dans `ksp-job-backfill-lib::conversion` ne doit ni y rester enfermée ni être copiée dans le Worker.
|
La canonicalisation RAW v1 actuellement prouvée dans `ksp-job-backfill-lib::conversion` ne doit ni y rester enfermée ni être copiée dans le Worker.
|
||||||
|
|
||||||
Le handoff retient une petite crate source-neutral commune, à matérialiser pendant `0.3.10` :
|
Le handoff retient la crate source-neutral commune désormais matérialisée :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ksp-raw-transaction-lib
|
ksp-raw-transaction-lib
|
||||||
@@ -650,18 +651,69 @@ backend Store physique
|
|||||||
Graphe conceptuel :
|
Graphe conceptuel :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ksp-job-backfill-lib --------------------> ksp-raw-transaction-lib ----> Store façade
|
ksp-job-backfill-lib --------------------> ksp-raw-transaction-lib ----> ksp-store-api
|
||||||
ksp-worker-raw-transaction-ingest-lib ---> ksp-raw-transaction-lib ----> Store façade
|
ksp-worker-raw-transaction-ingest-lib ---> ksp-raw-transaction-lib ----> ksp-store-api
|
||||||
|
|
||||||
|
ksp-job-backfill-lib --------------------> ksp-store-lib
|
||||||
|
ksp-worker-raw-transaction-ingest-lib ---> ksp-store-lib
|
||||||
ksp-job-backfill-lib --------------------> ksp-onchain-transport-lib
|
ksp-job-backfill-lib --------------------> ksp-onchain-transport-lib
|
||||||
ksp-worker-raw-transaction-ingest-lib ---> ksp-onchain-transport-lib
|
ksp-worker-raw-transaction-ingest-lib ---> ksp-onchain-transport-lib
|
||||||
|
|
||||||
aucun edge Job <-> Worker
|
aucun edge Job <-> Worker
|
||||||
|
aucun edge Transport <-> ksp-raw-transaction-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
La migration doit préserver exactement les golden bytes/hash RAW v1 déjà prouvés. Aucun RAW v2 n'est justifié.
|
La migration doit préserver exactement les golden bytes/hash RAW v1 déjà prouvés. Aucun RAW v2 n'est justifié.
|
||||||
|
|
||||||
## 14. Handoff `0.3.10` : Worker live
|
### 13.1 Transaction wire Legacy/V0/V1
|
||||||
|
|
||||||
|
`pre.006` matérialise dans `ksp-raw-transaction-lib` un modèle wire Solana source-neutral, indépendant de Transport et des crates runtime Solana. Il sérialise exactement les formes Legacy, V0 et V1 puis expose aussi la représentation Base64.
|
||||||
|
|
||||||
|
Pour V1, le contrat suit SIMD-0385 tel qu'audité le 7 septembre 2026 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0x81
|
||||||
|
LegacyHeader
|
||||||
|
TransactionConfigMask u32 LE
|
||||||
|
LifetimeSpecifier [32]
|
||||||
|
NumInstructions u8
|
||||||
|
NumAddresses u8
|
||||||
|
Addresses
|
||||||
|
ConfigValues
|
||||||
|
InstructionHeaders
|
||||||
|
InstructionPayloads
|
||||||
|
Signatures terminales
|
||||||
|
```
|
||||||
|
|
||||||
|
Les limites common verrouillées sont : transaction `<= 4096` octets, `<= 12` signatures, `<= 64` adresses, `<= 64` instructions, indexes bornés, aucune ALT en V1 et aucune donnée après les signatures. La présence de `Message.config` Yellowstone distingue V1 ; `versioned=true` sans config reste V0, et `versioned=false` sans config reste Legacy.
|
||||||
|
|
||||||
|
L'extracteur de signature Base64 common reconnaît désormais V1 au premier octet `0x81` et parcourt structurellement le message jusqu'aux signatures terminales au lieu d'appliquer le short-vector signatures-first de Legacy/V0.
|
||||||
|
|
||||||
|
Le format V1 est encore documenté upstream comme pre-release/feature-gated ; KSP implémente donc la capacité de lecture/canonicalisation sans déclarer son activation sur un cluster particulier.
|
||||||
|
|
||||||
|
### 13.2 Qualification Yellowstone `pre.006`
|
||||||
|
|
||||||
|
Un canari cross-layer test-only dans Backfill monte un serveur Geyser local, ouvre le vrai `YellowstoneGrpcChannel::open_standard_subscribe`, reçoit successivement une update `Transaction` V1 et une update `Block` contenant la même transaction, puis projette les DTO Transport vers le modèle wire common uniquement dans le test.
|
||||||
|
|
||||||
|
Le gate prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction update -> slot/index + transaction wire V1 exact
|
||||||
|
Block update -> slot/index + même transaction wire V1 exact + block_time
|
||||||
|
Transport -> aucun edge production vers ksp-raw-transaction-lib
|
||||||
|
Backfill -> tonic/yellowstone-grpc-proto strictement dev-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne qualifie pas un RAW-direct complet Yellowstone : une update `Transaction` ne porte pas `block_time`, et la représentation protobuf Yellowstone de la meta n'est pas démontrée byte-identical avec la projection JSON HTTP servant au RAW v1 actuel. La décision `TR-C4` reste donc conservative :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone Transaction/Block -> signal structuré + transaction wire fidèle
|
||||||
|
RAW v1 complet -> hydration HTTP avant persistence
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun adapter productif n'est ajouté en `pre.006`. Conformément à `TR-C2`, l'adapter `Transport DTO -> common` demeure réservé au Worker concret, désormais ouvert en `0.3.11`.
|
||||||
|
|
||||||
|
## 14. Handoff Worker live `0.3.11` à `0.3.14`
|
||||||
|
|
||||||
### 14.1 Contrat fonctionnel
|
### 14.1 Contrat fonctionnel
|
||||||
|
|
||||||
@@ -697,8 +749,8 @@ Yellowstone transactions
|
|||||||
Yellowstone blocks
|
Yellowstone blocks
|
||||||
Yellowstone status + hydration
|
Yellowstone status + hydration
|
||||||
WS logsSubscribe + HTTP getTransaction
|
WS logsSubscribe + HTTP getTransaction
|
||||||
WS blockSubscribe full
|
WS blockSubscribe full/base64 legacy-v0 direct qualifié
|
||||||
Helius transactionSubscribe full
|
Helius transactionSubscribe full/base64 + HTTP hydration
|
||||||
HTTP live block polling
|
HTTP live block polling
|
||||||
HTTP hydration
|
HTTP hydration
|
||||||
replay Yellowstone pour continuité du run
|
replay Yellowstone pour continuité du run
|
||||||
@@ -708,9 +760,9 @@ sources EARLY via adapter extensible
|
|||||||
### 14.3 Gaps Transport Worker
|
### 14.3 Gaps Transport Worker
|
||||||
|
|
||||||
| ID | Adaptation | Motif |
|
| ID | Adaptation | Motif |
|
||||||
|--------|-----------------------------------------------------------------------------------|------------------------------------------------------|
|
|--------|---------------------------------------------------------------------------------------|------------------------------------------------------|
|
||||||
| `TR-B` | `get_block_observed` symétrique de `get_transaction_observed` | provenance exacte en pool HTTP |
|
| `TR-B` | fermé : `get_block_observed` symétrique de `get_transaction_observed` | provenance exacte en pool HTTP |
|
||||||
| `TR-C` | projection source-neutral des transactions full WS/Yellowstone | éviter plusieurs canonicalizers filaires |
|
| `TR-C` | projection source-neutral des transactions full WS/Yellowstone ; Helius via hydration | éviter plusieurs canonicalizers filaires |
|
||||||
| `TR-D` | métadonnées sûres d'acquisition live au moment de la conversion | observation uniforme |
|
| `TR-D` | métadonnées sûres d'acquisition live au moment de la conversion | observation uniforme |
|
||||||
| `TR-E` | conserver/exploiter `from_slot`, replay info et snapshots de continuité existants | ne pas créer un second moteur Yellowstone |
|
| `TR-E` | conserver/exploiter `from_slot`, replay info et snapshots de continuité existants | ne pas créer un second moteur Yellowstone |
|
||||||
| `TR-F` | adapters EARLY uniquement quand leur protocole est réellement implémenté | réserver la capability sans tout coder immédiatement |
|
| `TR-F` | adapters EARLY uniquement quand leur protocole est réellement implémenté | réserver la capability sans tout coder immédiatement |
|
||||||
@@ -731,7 +783,7 @@ settings techniques nécessaires
|
|||||||
|
|
||||||
Elle peut activer plusieurs providers/endpoints simultanément. Les secrets restent Config-owned, avec réutilisation unique de `KSP_SECRET_HELIUS_API_KEY` pour les surfaces Helius concernées.
|
Elle peut activer plusieurs providers/endpoints simultanément. Les secrets restent Config-owned, avec réutilisation unique de `KSP_SECRET_HELIUS_API_KEY` pour les surfaces Helius concernées.
|
||||||
|
|
||||||
## 15. Handoff `0.3.12` : Job Backfill multi-stratégie
|
## 15. Handoff `0.3.16` : Job Backfill multi-stratégie
|
||||||
|
|
||||||
Le Job doit conserver son vertical slice existant puis ajouter des stratégies historiques choisies selon requête + capabilities + configuration.
|
Le Job doit conserver son vertical slice existant puis ajouter des stratégies historiques choisies selon requête + capabilities + configuration.
|
||||||
|
|
||||||
@@ -756,18 +808,17 @@ L'exhaustivité de l'architecture ne signifie pas que chaque intégration vendor
|
|||||||
Ordre conseillé :
|
Ordre conseillé :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
0.3.10 P0 : normalisation RAW commune
|
0.3.10 P0 : normalisation RAW commune + observed getBlock + preuves cross-source
|
||||||
0.3.10 P0 : observed getBlock + adapters source-neutral
|
0.3.11 P0 : Worker foundation/runtime + persistence déterministe
|
||||||
0.3.10 P0 : Yellowstone transactions/blocks live
|
0.3.12 P0 : Yellowstone transactions/blocks/status + hydration + replay continuity
|
||||||
0.3.10 P0 : WS logs + hydration, blockSubscribe, Helius transactionSubscribe
|
0.3.13 P0 : WS logs + hydration, blockSubscribe, Helius transactionSubscribe, HTTP live polling
|
||||||
0.3.10 P0 : HTTP live block polling/hydration
|
0.3.13 P1 : multi-source convergence, dedup, provenance, content conflict, backpressure
|
||||||
0.3.10 P1 : multi-source concurrency, dedup, provenance, continuity repair
|
0.3.14 P0 : continuity repair complet + hardening + smokes gratuits accessibles
|
||||||
0.3.10 P1 : smokes gratuits Mainnet/Devnet/Testnet
|
0.3.14 P2 : EARLY adapters accessibles seulement si prouvés
|
||||||
0.3.10 P2 : EARLY adapters accessibles
|
0.3.16 P0 : block scan historique
|
||||||
0.3.12 P0 : block scan historique
|
0.3.16 P0 : replay Yellowstone borné
|
||||||
0.3.12 P0 : replay Yellowstone borné
|
0.3.16 P1 : provider history/archive
|
||||||
0.3.12 P1 : provider history/archive
|
0.3.16 P1 : Old Faithful
|
||||||
0.3.12 P1 : Old Faithful
|
|
||||||
plus tard : substrats directs et sources vendor-specific sans accès actuel
|
plus tard : substrats directs et sources vendor-specific sans accès actuel
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -801,7 +852,7 @@ Aucun code, DTO, Config, endpoint, secret, dépendance ou convention de version
|
|||||||
|
|
||||||
## 18. Sources externes auditées
|
## 18. Sources externes auditées
|
||||||
|
|
||||||
Sources consultées le **4 septembre 2026**. Cette liste consolide les anciens audits A/B dans un seul registre documentaire.
|
Sources consultées le **4 septembre 2026** et revalidées pour Transaction V1 le **7 septembre 2026**. Cette liste consolide les anciens audits A/B dans un seul registre documentaire.
|
||||||
|
|
||||||
### 18.1 Solana standard
|
### 18.1 Solana standard
|
||||||
|
|
||||||
@@ -825,8 +876,11 @@ Sources consultées le **4 septembre 2026**. Cette liste consolide les anciens a
|
|||||||
### 18.2 Yellowstone et providers
|
### 18.2 Yellowstone et providers
|
||||||
|
|
||||||
| Source | URL |
|
| Source | URL |
|
||||||
|----------------------------------|---------------------------------------------------------------------------------------------------|
|
|----------------------------------|--------------------------------------------------------------------------------------------------------------|
|
||||||
| Yellowstone proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto |
|
| Yellowstone proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto |
|
||||||
|
| Yellowstone storage proto | https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto |
|
||||||
|
| SIMD-0385 Transaction V1 | https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md |
|
||||||
|
| Transaction V1 examples | https://github.com/solana-foundation/transaction-v1-examples |
|
||||||
| Yellowstone README | https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md |
|
| Yellowstone README | https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md |
|
||||||
| Yellowstone changelog | https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md |
|
| Yellowstone changelog | https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md |
|
||||||
| Helius data streaming | https://www.helius.dev/docs/data-streaming |
|
| Helius data streaming | https://www.helius.dev/docs/data-streaming |
|
||||||
@@ -879,8 +933,12 @@ support vs preuve = axes séparés ; NON PROUVÉ n'implique pas REJET
|
|||||||
provider model = capabilities/configuration, jamais enum provider fermé
|
provider model = capabilities/configuration, jamais enum provider fermé
|
||||||
network identity = mainnet canonique ; mainnet-beta alias legacy/externe
|
network identity = mainnet canonique ; mainnet-beta alias legacy/externe
|
||||||
RAW canonicalisation = lower-layer commune, sans edge Job <-> Worker
|
RAW canonicalisation = lower-layer commune, sans edge Job <-> Worker
|
||||||
0.3.10 = Worker live multi-source + adaptations communes nécessaires
|
Transaction V1 = wire source-neutral Legacy/V0/V1 dans common ; activation cluster non supposée
|
||||||
0.3.12 = Job Backfill multi-stratégie historique
|
Yellowstone pre.006 = transaction wire V1 qualifié ; RAW complet via hydration HTTP
|
||||||
|
TR-C2 = adapter productif Transport DTO -> common réservé au Worker concret
|
||||||
|
0.3.10 = common RAW + preuves cross-source
|
||||||
|
0.3.11 à 0.3.14 = Worker live multi-source par responsabilités bornées
|
||||||
|
0.3.16 = Job Backfill multi-stratégie historique
|
||||||
```
|
```
|
||||||
|
|
||||||
`0.3.9` n'implémente aucune nouvelle stratégie d'acquisition. Il ferme l'architecture et l'inventaire nécessaires aux releases suivantes.
|
`0.3.9` n'implémente aucune nouvelle stratégie d'acquisition. Il ferme l'architecture et l'inventaire nécessaires aux releases suivantes.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 95 -->
|
<!-- version: 97 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# Séquence des releases fonctionnelles KSP
|
||||||
|
|
||||||
@@ -524,7 +524,7 @@ Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel. SOL/EUR et
|
|||||||
|
|
||||||
La dependency direction reste strictement `Interface -> Core`; aucun serde/codec générique, `solana-instruction`, runtime réseau ou logging n'est introduit. La façade crate-root est verrouillée par canaris public API, consumer externe, release completeness et dependency firewall. [`../../crates/ksp-interface-lib/README.md`](../../crates/ksp-interface-lib/README.md) et [`../../crates/ksp-interface-lib/USAGE.md`](../../crates/ksp-interface-lib/USAGE.md) deviennent les références durables de cette foundation. Le gate technique final `pre.006` est vert avant réconciliation documentaire.
|
La dependency direction reste strictement `Interface -> Core`; aucun serde/codec générique, `solana-instruction`, runtime réseau ou logging n'est introduit. La façade crate-root est verrouillée par canaris public API, consumer externe, release completeness et dependency firewall. [`../../crates/ksp-interface-lib/README.md`](../../crates/ksp-interface-lib/README.md) et [`../../crates/ksp-interface-lib/USAGE.md`](../../crates/ksp-interface-lib/USAGE.md) deviennent les références durables de cette foundation. Le gate technique final `pre.006` est vert avant réconciliation documentaire.
|
||||||
|
|
||||||
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant. Les codecs/layouts spécifiques restent conditionnés à un vertical réel, tandis que les wires génériques d'acquisition/CORE restent reportés à `0.3.2+`.
|
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant. Les codecs/layouts spécifiques restent conditionnés à un vertical réel. La décomposition STRUCTURAL générique reste reportée à la série STRUCTURAL ouverte seulement après fermeture de la couche RAW ; elle n'est plus associée à un ancien numéro `0.3.2+` devenu obsolète.
|
||||||
|
|
||||||
### `0.2.14` — Program API foundation
|
### `0.2.14` — Program API foundation
|
||||||
|
|
||||||
@@ -552,50 +552,62 @@ Le hardening final verrouille l'inventaire crate-root exact, le passage d'une `P
|
|||||||
|
|
||||||
Restent explicitement reportés : `ksp-program-lib`, payload canonique D3, registry runtime, identity/version/coverage génériques, autres familles de decoder et `ProgramExecutionPreparer`. Ils seront introduits uniquement par les vertical slices qui démontreront leurs contrats réels.
|
Restent explicitement reportés : `ksp-program-lib`, payload canonique D3, registry runtime, identity/version/coverage génériques, autres familles de decoder et `ProgramExecutionPreparer`. Ils seront introduits uniquement par les vertical slices qui démontreront leurs contrats réels.
|
||||||
|
|
||||||
## Architecture durable : RAW -> CORE -> DECODE -> SPECIALIZED
|
## Architecture durable : RAW -> STRUCTURAL -> DECODED -> DOMAIN
|
||||||
|
|
||||||
La chaîne de données est :
|
La chaîne de données est :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
D1 RAW
|
D1 RAW
|
||||||
-> D2 CORE
|
-> D2 STRUCTURAL
|
||||||
-> D3 DECODE
|
-> D3 DECODED
|
||||||
-> D4 SPECIALIZED
|
-> D4 DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
RAW et CORE ne nécessitent aucun decoder Program.
|
RAW et STRUCTURAL ne nécessitent aucun decoder Program.
|
||||||
|
|
||||||
`RAW -> CORE` est une normalisation générique Solana ; le premier decoder intervient à `CORE -> DECODE`.
|
`RAW -> STRUCTURAL` est une normalisation générique Solana ; le premier decoder intervient à `STRUCTURAL -> DECODED`.
|
||||||
|
|
||||||
## Série `0.3.x` — RAW / acquisition persistée
|
## Série `0.3.x` — RAW / acquisition persistée
|
||||||
|
|
||||||
Début décidé :
|
La séquence effective a évolué par sizing et validation réels. La référence détaillée reste `ROADMAP.md`; cette synthèse conserve uniquement les frontières fonctionnelles :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
0.3.1 ksp-store-api + ksp-store-lib, RAW only
|
0.3.1 ksp-store-api RAW
|
||||||
0.3.2 ksp-interface-lib, wires génériques acquisition/CORE
|
0.3.2 Store runtime + backend PostgreSQL foundation
|
||||||
0.3.3 ksp-job-api + backfill
|
0.3.3 persistence RawTransaction
|
||||||
0.3.4 application backfill/RAW
|
0.3.4 persistence RawAccountState
|
||||||
|
0.3.5 Interface acquisition events
|
||||||
|
0.3.6 ksp-job-api + RawTransaction Backfill
|
||||||
|
0.3.7 ksp-app-backfill-desk
|
||||||
|
0.3.8 ksp-app-store-desk RAW
|
||||||
|
0.3.9 ksp-worker-api + audit acquisition RawTransaction
|
||||||
|
0.3.10 common ksp-raw-transaction-lib + preuves cross-source
|
||||||
|
0.3.11-0.3.14 Worker RAW ingest découpé par responsabilité
|
||||||
|
0.3.15 ksp-app-raw-transaction-ingest-desk
|
||||||
|
0.3.16 Backfill multi-source / multi-stratégie
|
||||||
```
|
```
|
||||||
|
|
||||||
La suite de la série termine la couche RAW avec worker/service live et outils d'exploitation utiles avant l'ouverture de CORE.
|
Cette série ferme la couche RAW et ses outils d'exploitation avant l'ouverture fonctionnelle de STRUCTURAL. Le Worker live RAW reste distinct des jobs historiques et ne constitue pas encore une transformation RAW -> STRUCTURAL.
|
||||||
|
|
||||||
`0.3.1` ne doit pas créer par anticipation les modèles/tables DECODE/SPECIALIZED.
|
Aucune release RAW ne crée par anticipation la persistence DECODED/DOMAIN.
|
||||||
|
|
||||||
## Série CORE suivante
|
## Série STRUCTURAL suivante
|
||||||
|
|
||||||
Objectif : rendre CORE exploitable sans aucun decoder Program :
|
Objectif : rendre la couche STRUCTURAL exploitable sans aucun decoder Program. Elle décompose les entrées RAW réellement décomposables en unités Solana génériques plus fines destinées au décodage ultérieur :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RAW persisted
|
RAW persisted
|
||||||
-> Solana generic normalization
|
-> Solana generic structural decomposition
|
||||||
-> CORE persistence
|
-> STRUCTURAL persistence
|
||||||
-> RAW->CORE replay/backfill
|
-> RAW -> STRUCTURAL replay/backfill
|
||||||
-> CORE worker/service
|
-> STRUCTURAL job borné/rejouable
|
||||||
-> CORE inspection/control app
|
-> STRUCTURAL worker/service continu
|
||||||
|
-> STRUCTURAL inspection/control dans ksp-app-store-desk lorsque pertinent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Séries DECODE/SPECIALIZED/EXECUTION suivantes
|
Le nom de couche historique `CORE` est abandonné. `Core` reste réservé à `ksp-core-lib`, à son domaine fondamental et aux noms propres tels que « Solana Core Programs ».
|
||||||
|
|
||||||
|
## Séries DECODED/DOMAIN/EXECUTION suivantes
|
||||||
|
|
||||||
À partir du décodage, KSP progresse par vertical slices complets et non par grandes couches de crates isolées :
|
À partir du décodage, KSP progresse par vertical slices complets et non par grandes couches de crates isolées :
|
||||||
|
|
||||||
@@ -638,7 +650,7 @@ Après les DEX prioritaires, introduire une petite `ksp-app-market-desk` consomm
|
|||||||
|
|
||||||
Après Jupiter/OKX, enrichir la même app avec routes, legs, DEX impliqués, fees/slippage et quote/execution lorsque disponible.
|
Après Jupiter/OKX, enrichir la même app avec routes, legs, DEX impliqués, fees/slippage et quote/execution lorsque disponible.
|
||||||
|
|
||||||
L'app ne réimplémente pas les SDK/protocoles DEX ; elle consomme les faits SPECIALIZED normalisés.
|
L'app ne réimplémente pas les SDK/protocoles DEX ; elle consomme les faits DOMAIN normalisés.
|
||||||
|
|
||||||
## Discipline de sizing
|
## Discipline de sizing
|
||||||
|
|
||||||
|
|||||||
1213
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
Normal file
1213
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
<!-- file: docs/rules/RULES_DEPENDENCIES.md -->
|
||||||
<!-- version: 17 -->
|
<!-- version: 18 -->
|
||||||
|
|
||||||
# Règles des dépendances KSP
|
# Règles des dépendances KSP
|
||||||
|
|
||||||
@@ -100,13 +100,13 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
|||||||
## Pipelines spécialisés
|
## Pipelines spécialisés
|
||||||
|
|
||||||
- **DEP-PIPE-001** — KSP ne crée pas de `ksp-pipeline-lib` monolithique.
|
- **DEP-PIPE-001** — KSP ne crée pas de `ksp-pipeline-lib` monolithique.
|
||||||
- **DEP-PIPE-002** — Les frontières canoniques de processing sont `RAW -> CORE -> DECODE -> SPECIALIZED`. Une crate pipeline dédiée n'est créée que lorsqu'une logique doit réellement être réutilisée entre plusieurs lifecycle hosts (worker/job/app/test) ; aucune liste globale de quatre crates pipeline n'est imposée par symétrie.
|
- **DEP-PIPE-002** — Les frontières canoniques de processing sont `RAW -> STRUCTURAL -> DECODED -> DOMAIN`. Une crate pipeline dédiée n'est créée que lorsqu'une logique doit réellement être réutilisée entre plusieurs lifecycle hosts (worker/job/app/test) ; aucune liste globale de quatre crates pipeline n'est imposée par symétrie.
|
||||||
- **DEP-PIPE-003** — Un pipeline de processing dépend des APIs nécessaires à sa frontière et non des implémentations officielles correspondantes lorsque l'API permet l'injection/composition.
|
- **DEP-PIPE-003** — Un pipeline de processing dépend des APIs nécessaires à sa frontière et non des implémentations officielles correspondantes lorsque l'API permet l'injection/composition.
|
||||||
- **DEP-PIPE-004** — Les pipelines spécialisés ne dépendent pas de `ksp-worker-api` ou `ksp-job-api`; worker et job possèdent le lifecycle.
|
- **DEP-PIPE-004** — Les pipelines spécialisés ne dépendent pas de `ksp-worker-api` ou `ksp-job-api`; worker et job possèdent le lifecycle.
|
||||||
- **DEP-PIPE-005** — Worker live et job de replay/backfill réutilisent le même pipeline pour une même frontière durable afin d'éviter la duplication de logique.
|
- **DEP-PIPE-005** — Worker live et job de replay/backfill réutilisent le même pipeline pour une même frontière durable afin d'éviter la duplication de logique.
|
||||||
- **DEP-PIPE-006** — Le pipeline raw ingestion peut dépendre des modèles homogènes de `ksp-onchain-transport-lib` et de `ksp-store-api`, mais pas de `ksp-store-lib`.
|
- **DEP-PIPE-006** — Le pipeline raw ingestion peut dépendre des modèles homogènes de `ksp-onchain-transport-lib` et de `ksp-store-api`, mais pas de `ksp-store-lib`.
|
||||||
- **DEP-PIPE-007** — La transformation `RAW -> CORE` est Solana-générique et ne dépend ni de `ksp-program-api`, ni de `ksp-program-lib`, ni de `ksp-materializer-api`; les premiers contrats Program interviennent seulement à partir de `CORE -> DECODE`.
|
- **DEP-PIPE-007** — La transformation `RAW -> STRUCTURAL` est Solana-générique et ne dépend ni de `ksp-program-api`, ni de `ksp-program-lib`, ni de `ksp-materializer-api`; les premiers contrats Program interviennent seulement à partir de `STRUCTURAL -> DECODED`.
|
||||||
- **DEP-PIPE-008** — À partir de `CORE -> DECODE`, les pipelines/processors verticaux peuvent dépendre de `ksp-program-api` et de `ksp-materializer-api` selon leur rôle, sans dépendre par défaut des implémentations officielles correspondantes lorsque l'injection/composition suffit. `DECODE -> SPECIALIZED` utilise de même les contrats de matérialisation/projection nécessaires sans imposer une implémentation globale unique.
|
- **DEP-PIPE-008** — À partir de `STRUCTURAL -> DECODED`, les pipelines/processors verticaux peuvent dépendre de `ksp-program-api` et de `ksp-materializer-api` selon leur rôle, sans dépendre par défaut des implémentations officielles correspondantes lorsque l'injection/composition suffit. `DECODED -> DOMAIN` utilise de même les contrats de matérialisation/projection nécessaires sans imposer une implémentation globale unique.
|
||||||
|
|
||||||
## Worker / Job lifecycle
|
## Worker / Job lifecycle
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ Elles complètent les règles Rust générales et le graphe de `docs/architectur
|
|||||||
- **DEP-WORKER-003** — La logique réutilisable d'un worker/job dépend en priorité des APIs KSP (`ksp-store-api`, `ksp-program-api`, `ksp-materializer-api`, etc.) et reçoit les implémentations par composition. Le binaire/service mince peut câbler `ksp-store-lib` ou les implémentations officielles nécessaires sans transférer cet ownership à la logique du worker/job.
|
- **DEP-WORKER-003** — La logique réutilisable d'un worker/job dépend en priorité des APIs KSP (`ksp-store-api`, `ksp-program-api`, `ksp-materializer-api`, etc.) et reçoit les implémentations par composition. Le binaire/service mince peut câbler `ksp-store-lib` ou les implémentations officielles nécessaires sans transférer cet ownership à la logique du worker/job.
|
||||||
- **DEP-JOB-001** — `ksp-job-api` ne dépend ni de `ksp-worker-api` ni de `ksp-worker-control-lib`.
|
- **DEP-JOB-001** — `ksp-job-api` ne dépend ni de `ksp-worker-api` ni de `ksp-worker-control-lib`.
|
||||||
- **DEP-JOB-002** — Un orchestrateur futur peut consommer séparément les APIs/contrôles workers et jobs sans introduire un lifecycle parent commun.
|
- **DEP-JOB-002** — Un orchestrateur futur peut consommer séparément les APIs/contrôles workers et jobs sans introduire un lifecycle parent commun.
|
||||||
- **DEP-JOB-003** — Lorsqu'une même transformation existe en live et en replay, jobs et workers réutilisent la même logique de transformation au lieu de dupliquer `RAW -> CORE`, `CORE -> DECODE` ou `DECODE -> SPECIALIZED`.
|
- **DEP-JOB-003** — Lorsqu'une même transformation existe en live et en replay, jobs et workers réutilisent la même logique de transformation au lieu de dupliquer `RAW -> STRUCTURAL`, `STRUCTURAL -> DECODED` ou `DECODED -> DOMAIN`.
|
||||||
|
|
||||||
## Services, applications et control plane
|
## Services, applications et control plane
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_KSP.md -->
|
<!-- file: docs/rules/RULES_KSP.md -->
|
||||||
<!-- version: 39 -->
|
<!-- version: 40 -->
|
||||||
|
|
||||||
# Règles spécifiques à KSP
|
# Règles spécifiques à KSP
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
- **KSP-WORKER-005** — `ksp-worker-raw-retriever` est le worker d'acquisition raw live/quasi-live. Il ne décode pas, ne matérialise pas et ne réalise pas de replay/backfill historique.
|
- **KSP-WORKER-005** — `ksp-worker-raw-retriever` est le worker d'acquisition raw live/quasi-live. Il ne décode pas, ne matérialise pas et ne réalise pas de replay/backfill historique.
|
||||||
- **KSP-WORKER-006** — `ksp-worker-raw-retriever` persiste les données raw puis notifie leur disponibilité selon les contrats de données normalisés.
|
- **KSP-WORKER-006** — `ksp-worker-raw-retriever` persiste les données raw puis notifie leur disponibilité selon les contrats de données normalisés.
|
||||||
- **KSP-WORKER-007** — `ksp-worker-raw-retriever` doit pouvoir faire évoluer à chaud les listeners et la sélection des données qu'il rapatrie/stocke.
|
- **KSP-WORKER-007** — `ksp-worker-raw-retriever` doit pouvoir faire évoluer à chaud les listeners et la sélection des données qu'il rapatrie/stocke.
|
||||||
- **KSP-WORKER-008** — Les workers de processing ne sont pas figés à l'avance sous une chaîne globale `core -> generic materializer -> domain projector`. RAW et CORE peuvent disposer de workers horizontaux propres à leur couche ; à partir de DECODE, les workers/processors sont introduits au besoin avec chaque groupe fonctionnel vertical afin que décodage, matérialisation, projection spécialisée et validation d'exécution évoluent ensemble.
|
- **KSP-WORKER-008** — Les workers de processing ne sont pas figés à l'avance sous une chaîne globale `structural -> generic materializer -> domain projector`. RAW et STRUCTURAL peuvent disposer de workers horizontaux propres à leur couche ; à partir de DECODED, les workers/processors sont introduits au besoin avec chaque groupe fonctionnel vertical afin que décodage, matérialisation, projection spécialisée et validation d'exécution évoluent ensemble.
|
||||||
- **KSP-WORKER-009** — Les workers de processing utilisent notification comme wake-up mais reconstruisent leur backlog depuis le Store.
|
- **KSP-WORKER-009** — Les workers de processing utilisent notification comme wake-up mais reconstruisent leur backlog depuis le Store.
|
||||||
- **KSP-WORKER-010** — `ksp-worker-raw-retriever` distingue une configuration desired et une configuration effective lors des reconfigurations à chaud.
|
- **KSP-WORKER-010** — `ksp-worker-raw-retriever` distingue une configuration desired et une configuration effective lors des reconfigurations à chaud.
|
||||||
- **KSP-WORKER-011** — Un cursor de scan est une optimisation ; les processing outcomes durables constituent la preuve qu'un input a été traité pour un processor/version/capability.
|
- **KSP-WORKER-011** — Un cursor de scan est une optimisation ; les processing outcomes durables constituent la preuve qu'un input a été traité pour un processor/version/capability.
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
- **KSP-JOB-006** — D'autres jobs peuvent être introduits pour metadata, quotes ou autres travaux ponctuels lorsqu'un besoin réel le justifie.
|
- **KSP-JOB-006** — D'autres jobs peuvent être introduits pour metadata, quotes ou autres travaux ponctuels lorsqu'un besoin réel le justifie.
|
||||||
- **KSP-JOB-007** — Aucune `ksp-job-control-lib` commune n'est prévue actuellement ; elle ne sera créée que si une duplication concrète entre plusieurs jobs le justifie.
|
- **KSP-JOB-007** — Aucune `ksp-job-control-lib` commune n'est prévue actuellement ; elle ne sera créée que si une duplication concrète entre plusieurs jobs le justifie.
|
||||||
- **KSP-JOB-008** — Le contrôle/gouvernance des jobs reste séparé du contrôle des workers.
|
- **KSP-JOB-008** — Le contrôle/gouvernance des jobs reste séparé du contrôle des workers.
|
||||||
- **KSP-JOB-009** — Aucun inventaire global de jobs de replay DECODE/SPECIALIZED n'est figé à l'avance. `RAW -> CORE` peut introduire un job de replay Core lorsque la couche CORE est ouverte ; à partir de DECODE, les jobs de replay sont introduits avec les groupes/capacités verticaux qui en ont réellement besoin, sans imposer des jobs génériques `generic-materialization` / `domain-projection` pour tout Solana.
|
- **KSP-JOB-009** — Aucun inventaire global de jobs de replay DECODED/DOMAIN n'est figé à l'avance. `RAW -> STRUCTURAL` peut introduire un job de replay STRUCTURAL lorsque la couche STRUCTURAL est ouverte ; à partir de DECODED, les jobs de replay sont introduits avec les groupes/capacités verticaux qui en ont réellement besoin, sans imposer des jobs génériques `generic-materialization` / `domain-projection` pour tout Solana.
|
||||||
- **KSP-JOB-010** — Les jobs de replay réutilisent exactement le pipeline spécialisé de la frontière correspondante.
|
- **KSP-JOB-010** — Les jobs de replay réutilisent exactement le pipeline spécialisé de la frontière correspondante.
|
||||||
- **KSP-JOB-011** — Le backfill conserve un checkpoint de progression dans la source historique en plus des outcomes de persistence D1.
|
- **KSP-JOB-011** — Le backfill conserve un checkpoint de progression dans la source historique en plus des outcomes de persistence D1.
|
||||||
- **KSP-JOB-012** — Replay normal/reprise et force replay sont deux intentions distinctes ; un force replay conserve provenance/historique et ne supprime pas silencieusement le résultat courant.
|
- **KSP-JOB-012** — Replay normal/reprise et force replay sont deux intentions distinctes ; un force replay conserve provenance/historique et ne supprime pas silencieusement le résultat courant.
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
## Pipelines et scénarios
|
## Pipelines et scénarios
|
||||||
|
|
||||||
- **KSP-PIPE-001** — KSP ne crée pas de `ksp-pipeline-lib` monolithique.
|
- **KSP-PIPE-001** — KSP ne crée pas de `ksp-pipeline-lib` monolithique.
|
||||||
- **KSP-PIPE-002** — Les frontières canoniques de données/processing sont `RAW -> CORE -> DECODE -> SPECIALIZED`. Les pipelines RAW et CORE peuvent être développés horizontalement jusqu'à leur acquisition/persistence/replay/worker/app ; à partir de DECODE, KSP progresse par groupes fonctionnels verticaux et ne pré-déclare pas une chaîne globale de crates pipeline pour tous les protocoles.
|
- **KSP-PIPE-002** — Les frontières canoniques de données/processing sont `RAW -> STRUCTURAL -> DECODED -> DOMAIN`. Les pipelines RAW et STRUCTURAL peuvent être développés horizontalement jusqu'à leur acquisition/persistence/replay/worker/app ; à partir de DECODED, KSP progresse par groupes fonctionnels verticaux et ne pré-déclare pas une chaîne globale de crates pipeline pour tous les protocoles.
|
||||||
- **KSP-PIPE-003** — Un pipeline spécialisé contient la logique réutilisable d'une frontière mais aucun lifecycle worker/job.
|
- **KSP-PIPE-003** — Un pipeline spécialisé contient la logique réutilisable d'une frontière mais aucun lifecycle worker/job.
|
||||||
- **KSP-PIPE-004** — Les pipelines utilisent les APIs Program/Materializer/Store lorsque ces frontières doivent être injectables ; les implémentations officielles sont composées par workers/jobs.
|
- **KSP-PIPE-004** — Les pipelines utilisent les APIs Program/Materializer/Store lorsque ces frontières doivent être injectables ; les implémentations officielles sont composées par workers/jobs.
|
||||||
- **KSP-PIPE-005** — Le traitement est at-least-once avec persistence idempotente et outcomes durables, plutôt qu'une promesse exactly-once distribuée.
|
- **KSP-PIPE-005** — Le traitement est at-least-once avec persistence idempotente et outcomes durables, plutôt qu'une promesse exactly-once distribuée.
|
||||||
@@ -262,5 +262,5 @@
|
|||||||
- **KSP-REL-016** — Une prerelease vise environ 15 à 20 minutes de travail effectif. Le `pre.001` dimensionne aussi la release concrète entière : une release doit pouvoir être ouverte, développée, validée et clôturée dans une seule session de chat. Si cette clôture paraît incertaine, la release est scindée avant l'implémentation fonctionnelle lourde ; une version volontairement répartie sur plusieurs sessions est interdite.
|
- **KSP-REL-016** — Une prerelease vise environ 15 à 20 minutes de travail effectif. Le `pre.001` dimensionne aussi la release concrète entière : une release doit pouvoir être ouverte, développée, validée et clôturée dans une seule session de chat. Si cette clôture paraît incertaine, la release est scindée avant l'implémentation fonctionnelle lourde ; une version volontairement répartie sur plusieurs sessions est interdite.
|
||||||
- **KSP-TRANSPORT-006** — Pour une surface de transport explicitement ciblée, KSP inventorie et implémente toutes les méthodes/opérations exposées par la documentation normative retenue, sauf impossibilité technique explicitement documentée. L'inventaire couvre aussi les sections officielles séparées `deprecated`/`obsolete` et `unstable`/`experimental` lorsqu'elles existent. Les opérations deprecated/obsolete encore réellement fonctionnelles et unstable/experimental restent utilisables mais émettent un `warn` via `ksp-logging-lib` à chaque utilisation concernée ; leur statut est décrit par une metadata centralisée et non par des warnings dispersés.
|
- **KSP-TRANSPORT-006** — Pour une surface de transport explicitement ciblée, KSP inventorie et implémente toutes les méthodes/opérations exposées par la documentation normative retenue, sauf impossibilité technique explicitement documentée. L'inventaire couvre aussi les sections officielles séparées `deprecated`/`obsolete` et `unstable`/`experimental` lorsqu'elles existent. Les opérations deprecated/obsolete encore réellement fonctionnelles et unstable/experimental restent utilisables mais émettent un `warn` via `ksp-logging-lib` à chaque utilisation concernée ; leur statut est décrit par une metadata centralisée et non par des warnings dispersés.
|
||||||
- **KSP-TRANSPORT-007** — La complétude d'un wrapper de transport standard couvre toute la surface sémantique de requête auditée : paramètres, options de configuration, variantes/overloads courants, formes legacy encore supportées et contraintes déterministes connues. Les formes de réponse pertinentes sont conservées losslessly, y compris les variantes, `null` et omissions significatives. KSP peut canonicaliser des syntaxes strictement équivalentes et conserver des sous-arbres wire riches via `serde_json::Value` tant qu'aucune information n'est perdue ; toute limitation volontaire d'une possibilité normative/runtime supportée doit être explicitement justifiée et documentée.
|
- **KSP-TRANSPORT-007** — La complétude d'un wrapper de transport standard couvre toute la surface sémantique de requête auditée : paramètres, options de configuration, variantes/overloads courants, formes legacy encore supportées et contraintes déterministes connues. Les formes de réponse pertinentes sont conservées losslessly, y compris les variantes, `null` et omissions significatives. KSP peut canonicaliser des syntaxes strictement équivalentes et conserver des sous-arbres wire riches via `serde_json::Value` tant qu'aucune information n'est perdue ; toute limitation volontaire d'une possibilité normative/runtime supportée doit être explicitement justifiée et documentée.
|
||||||
- **KSP-FLOW-001** — La progression durable canonique est `RAW -> CORE -> DECODE -> SPECIALIZED`. RAW et CORE ne nécessitent aucun decoder Program ; le passage RAW -> CORE reste une normalisation générique de la blockchain Solana. À partir de DECODE, KSP progresse verticalement par groupe fonctionnel à travers wire, décodage, matérialisation, projection spécialisée si utile, préparation d'exécution, policy, exécution et scénarios de validation.
|
- **KSP-FLOW-001** — La progression durable canonique est `RAW -> STRUCTURAL -> DECODED -> DOMAIN`. RAW et STRUCTURAL ne nécessitent aucun decoder Program ; le passage RAW -> STRUCTURAL reste une décomposition/normalisation structurelle générique de la blockchain Solana. Le nom de couche historique `CORE` est abandonné pour D2 et ne doit pas être réintroduit ; cette règle ne renomme ni `ksp-core-lib`, ni le domaine Core fondamental, ni les noms propres tels que « Solana Core Programs ». À partir de DECODED, KSP progresse verticalement par groupe fonctionnel à travers wire, décodage, matérialisation, projection spécialisée si utile, préparation d'exécution, policy, exécution et scénarios de validation.
|
||||||
- **KSP-FLOW-002** — Un programme ou composant satellite nécessaire à la compréhension, la matérialisation ou l'exécution correcte d'un protocole appartient au groupe de ce protocole. Il n'est pas reporté artificiellement dans une catégorie `trading-adjacent`.
|
- **KSP-FLOW-002** — Un programme ou composant satellite nécessaire à la compréhension, la matérialisation ou l'exécution correcte d'un protocole appartient au groupe de ce protocole. Il n'est pas reporté artificiellement dans une catégorie `trading-adjacent`.
|
||||||
|
|||||||
1063
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
Normal file
1063
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
Normal file
File diff suppressed because it is too large
Load Diff
636
prompts/030-V0_3_11_START_PROMPT.md
Normal file
636
prompts/030-V0_3_11_START_PROMPT.md
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
<!-- file: prompts/030-V0_3_11_START_PROMPT.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Prompt de démarrage `0.3.11` — fondation runtime du Worker `RawTransaction` ingest
|
||||||
|
|
||||||
|
## 1. Identité de la release et base exacte requise
|
||||||
|
|
||||||
|
Ouvrir **uniquement** `0.3.11` depuis la release stable/taggée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.3.10
|
||||||
|
```
|
||||||
|
|
||||||
|
La base de travail fournie par l'opérateur est autoritaire sur les souvenirs de session, snippets, anciennes archives et deltas intermédiaires. Avant toute modification, vérifier au minimum :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.10
|
||||||
|
deltas/0.3.10/rel.001.md présent
|
||||||
|
crates/ksp-raw-transaction-lib présent et documenté
|
||||||
|
crates/ksp-worker-api présent et stable
|
||||||
|
crates/ksp-job-backfill-lib migré vers ksp-raw-transaction-lib
|
||||||
|
ksp-worker-raw-transaction-ingest-lib absent sauf divergence explicitement auditée
|
||||||
|
```
|
||||||
|
|
||||||
|
Release ouverte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Première livraison attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.11-pre.001
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.001` est obligatoirement un gate de **lecture + audit + brainstorming + sizing + planification**. Il ne commence pas l'implémentation lourde du Worker. Si le périmètre ci-dessous ne paraît pas clôturable dans une seule session ou si une tranche dépasse environ 15–20 minutes de travail effectif, la release doit être rescindée avant le développement lourd.
|
||||||
|
|
||||||
|
## 2. Mission et résultat attendu
|
||||||
|
|
||||||
|
### 2.1 Mission de `0.3.11`
|
||||||
|
|
||||||
|
Introduire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-worker-raw-transaction-ingest-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
comme premier Worker concret KSP de la chaîne d'acquisition `RawTransaction`, mais limiter cette release à sa **fondation runtime source-neutral et déterministe**.
|
||||||
|
|
||||||
|
Résultat cible à réauditer pendant `pre.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate + dependency firewall
|
||||||
|
settings techniques source-neutral
|
||||||
|
identité Worker concrète
|
||||||
|
handle start/stop
|
||||||
|
lifecycle et terminaison
|
||||||
|
supervisor privé
|
||||||
|
channels bornés
|
||||||
|
pipeline central d'admission
|
||||||
|
canonicalisation via ksp-raw-transaction-lib
|
||||||
|
persistence via ksp-store-lib
|
||||||
|
déduplication/idempotence déterministe
|
||||||
|
snapshots latest-value sûrs
|
||||||
|
projection vers ksp-worker-api
|
||||||
|
hardening shutdown/fault/backpressure de fondation
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune source live complexe n'est un critère de sortie de `0.3.11`. Les voies Yellowstone, WS standard, Helius et HTTP live sont réparties sur `0.3.12`–`0.3.14`.
|
||||||
|
|
||||||
|
### 2.2 Sémantique Worker à préserver
|
||||||
|
|
||||||
|
Le Worker est un service continu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
start
|
||||||
|
-> initialise son run live
|
||||||
|
-> démarre les tâches privées nécessaires
|
||||||
|
-> admet et persiste les acquisitions reçues par ses sources techniques
|
||||||
|
-> publie des snapshots latest-value indépendants des lecteurs
|
||||||
|
stop
|
||||||
|
-> arrête les nouvelles admissions
|
||||||
|
-> demande l'arrêt coopératif des tâches
|
||||||
|
-> draine seulement le travail déjà admis dans une borne explicite
|
||||||
|
-> publie un terminal sûr
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Worker ne reçoit aucune requête métier historique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pas de signature cible
|
||||||
|
pas de program_id/adresse métier
|
||||||
|
pas de before/after
|
||||||
|
pas de plage historique caller-owned
|
||||||
|
pas de historical limit
|
||||||
|
pas de BackfillRequest
|
||||||
|
pas de BackfillCheckpoint
|
||||||
|
pas de JobId
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-job-backfill-lib` et le Worker sont deux producteurs indépendants du même Store. Ils ne s'appellent pas, ne s'attendent pas, ne se supervisent pas et ne partagent aucune orchestration fonctionnelle.
|
||||||
|
|
||||||
|
## 3. Sources de vérité internes obligatoires — ordre de lecture
|
||||||
|
|
||||||
|
### 3.1 Gouvernance générale
|
||||||
|
|
||||||
|
Lire intégralement, dans cet ordre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RULES.md
|
||||||
|
ROADMAP.md
|
||||||
|
CHANGELOG.md
|
||||||
|
docs/000-README.md
|
||||||
|
|
||||||
|
docs/rules/RULES_GENERAL.md
|
||||||
|
docs/rules/RULES_KSP.md
|
||||||
|
docs/rules/RULES_RUST.md
|
||||||
|
docs/rules/RULES_DEPENDENCIES.md
|
||||||
|
docs/rules/RULES_DOCUMENTATION.md
|
||||||
|
docs/rules/FILE_CONTRACTS.md
|
||||||
|
docs/rules/VERSION_WORKFLOW.md
|
||||||
|
docs/rules/PROMPT_STRUCTURE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt complète ces règles ; il ne les remplace pas.
|
||||||
|
|
||||||
|
Rappels bloquants pour toute modification Rust :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Rust 2024
|
||||||
|
unsafe interdit
|
||||||
|
unwrap / expect / panic interdits selon les règles KSP
|
||||||
|
? interdit en production
|
||||||
|
retours explicites ; clippy::implicit_return deny
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pas de pub mod
|
||||||
|
pub/pub(crate) partagés reexportés jusqu'à la crate root
|
||||||
|
accès partagés via crate::Item, y compris intra-crate
|
||||||
|
item strictement module-local => private
|
||||||
|
unit tests sous unit_tests/
|
||||||
|
integration tests sous tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Après toute modification Rust :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour tout Markdown touché :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
```
|
||||||
|
|
||||||
|
Une commande non exécutée n'est jamais déclarée PASS.
|
||||||
|
|
||||||
|
### 3.2 Architecture durable acquisition / Worker / Store
|
||||||
|
|
||||||
|
Lire intégralement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/architecture/002-LAYERS_AND_DEPENDENCIES.md
|
||||||
|
docs/architecture/003-COMPONENT_CONTRACTS.md
|
||||||
|
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||||
|
docs/architecture/005-DEPENDENCY_GRAPH.md
|
||||||
|
docs/architecture/008-DATA_MATERIALIZATION_AND_STORE.md
|
||||||
|
docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md
|
||||||
|
docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md
|
||||||
|
docs/architecture/011-RAW_TRANSACTION_ACQUISITION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Vocabulaire durable des couches Store :
|
||||||
|
|
||||||
|
```text
|
||||||
|
D1 RAW
|
||||||
|
D2 STRUCTURAL
|
||||||
|
D3 DECODED
|
||||||
|
D4 DOMAIN
|
||||||
|
```
|
||||||
|
|
||||||
|
`N1–N4` restent les niveaux architecturaux de composants. `CORE` n'est plus le nom de la couche D2 ; ne pas renommer pour autant `ksp-core-lib` ni les vrais usages du domaine Core.
|
||||||
|
|
||||||
|
### 3.3 Handoff autoritaire de `0.3.10`
|
||||||
|
|
||||||
|
Lire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||||
|
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||||
|
crates/ksp-raw-transaction-lib/Cargo.toml
|
||||||
|
crates/ksp-raw-transaction-lib/README.md
|
||||||
|
crates/ksp-raw-transaction-lib/USAGE.md
|
||||||
|
crates/ksp-raw-transaction-lib/src/
|
||||||
|
crates/ksp-raw-transaction-lib/tests/
|
||||||
|
crates/ksp-raw-transaction-lib/unit_tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Les sections 8, 9, 10, 12, 14, 15, 16 et 17 du plan `031` sont le handoff principal pour `0.3.11`. Elles sont des entrées à réauditer sur la base stable réelle, pas une autorisation à tout implémenter d'un bloc.
|
||||||
|
|
||||||
|
### 3.4 Worker API générique
|
||||||
|
|
||||||
|
Lire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-worker-api/Cargo.toml
|
||||||
|
crates/ksp-worker-api/README.md
|
||||||
|
crates/ksp-worker-api/USAGE.md
|
||||||
|
crates/ksp-worker-api/src/
|
||||||
|
crates/ksp-worker-api/tests/
|
||||||
|
|
||||||
|
docs/plans/030-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT_PLAN.md
|
||||||
|
docs/validation/026-V0_3_9_WORKER_API_RAW_TRANSACTION_AUDIT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Surface générique à préserver :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WorkerId
|
||||||
|
WorkerKindCode
|
||||||
|
WorkerState
|
||||||
|
WorkerHealth
|
||||||
|
WorkerActivity
|
||||||
|
WorkerLifecycle
|
||||||
|
WorkerStopToken
|
||||||
|
WorkerSnapshotSequence
|
||||||
|
WorkerSnapshot
|
||||||
|
WorkerSnapshotFuture
|
||||||
|
WorkerSnapshotSource
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-worker-api` dépend uniquement de `ksp-core-lib`, reste runtime-neutral et ne fournit pas de `start()/stop()` universel. Le premier Worker concret ne doit pas élargir cette API pour des besoins Solana-specific sauf défaut réellement transversal démontré par l'audit.
|
||||||
|
|
||||||
|
### 3.5 Store et Backfill comme frontières de comportement
|
||||||
|
|
||||||
|
Lire au minimum :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-store-api/Cargo.toml
|
||||||
|
crates/ksp-store-api/src/
|
||||||
|
crates/ksp-store-api/tests/
|
||||||
|
crates/ksp-store-lib/README.md
|
||||||
|
crates/ksp-store-lib/USAGE.md
|
||||||
|
crates/ksp-store-lib/src/
|
||||||
|
|
||||||
|
crates/ksp-job-backfill-lib/Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/README.md
|
||||||
|
crates/ksp-job-backfill-lib/USAGE.md
|
||||||
|
crates/ksp-job-backfill-lib/src/
|
||||||
|
crates/ksp-job-backfill-lib/tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Backfill est une référence fonctionnelle utile pour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
canonicalisation common déjà consommée
|
||||||
|
persistance atomique/idempotente
|
||||||
|
content conflict
|
||||||
|
cancellation
|
||||||
|
latest-value
|
||||||
|
bornes de concurrence
|
||||||
|
redaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Il n'est **pas** un parent architectural du Worker et sa sémantique de scope/checkpoint/campagne ne doit pas être copiée.
|
||||||
|
|
||||||
|
## 4. Sources externes et fraîcheur à réauditer
|
||||||
|
|
||||||
|
`0.3.11` n'ajoute pas de source live complexe par défaut. Il n'est donc pas nécessaire d'ouvrir une nouvelle matrice provider pour coder immédiatement.
|
||||||
|
|
||||||
|
En revanche, `pre.001` doit vérifier les versions stables réellement courantes de toute dépendance externe que la fondation Worker pourrait ajouter ou activer, notamment si l'audit conclut qu'un runtime Tokio/Futures direct est nécessaire. Utiliser les sources primaires du projet/crate et vérifier les features minimales réellement requises.
|
||||||
|
|
||||||
|
Ne pas conserver une version historique simplement parce qu'elle apparaît dans un ancien prompt. Ne pas ajouter de SDK provider.
|
||||||
|
|
||||||
|
Les surfaces provider/protocole ne sont réauditées dans `0.3.11` que si une décision de fondation dépend réellement d'elles ; les audits live détaillés appartiennent principalement à `0.3.12`–`0.3.14`.
|
||||||
|
|
||||||
|
## 5. État validé à préserver
|
||||||
|
|
||||||
|
### 5.1 Common RAW
|
||||||
|
|
||||||
|
`ksp-raw-transaction-lib` reste la lower-layer D1 commune :
|
||||||
|
|
||||||
|
```text
|
||||||
|
source-neutral
|
||||||
|
sans runtime async
|
||||||
|
sans Transport
|
||||||
|
sans Config
|
||||||
|
sans Job/Worker
|
||||||
|
sans backend Store physique
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Worker l'utilise ; il ne duplique pas la canonicalisation RAW v1.
|
||||||
|
|
||||||
|
### 5.2 Persistence
|
||||||
|
|
||||||
|
Le Worker persiste via :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-store-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
et non via un backend physique. `ksp-store-api` reste la couche de contrats/modèles ; les consumers runtime ordinaires ne contournent pas la façade Store pour écrire directement dans PostgreSQL.
|
||||||
|
|
||||||
|
Identité de convergence :
|
||||||
|
|
||||||
|
```text
|
||||||
|
(network, signature)
|
||||||
|
```
|
||||||
|
|
||||||
|
Une entité existante n'autorise pas la suppression d'une observation provenant d'une autre source. Un contenu canonique divergent pour la même identité reste un conflit explicite ; aucun provider gagnant n'est choisi implicitement.
|
||||||
|
|
||||||
|
### 5.3 Réseau
|
||||||
|
|
||||||
|
```text
|
||||||
|
mainnet = identité KSP canonique
|
||||||
|
mainnet-beta = alias legacy/externe uniquement
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Worker reçoit un réseau déjà résolu/cohérent ; il ne lit pas l'environnement directement.
|
||||||
|
|
||||||
|
### 5.4 Config et secrets
|
||||||
|
|
||||||
|
Frontière obligatoire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-worker-raw-transaction-ingest-lib -X-> ksp-config-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Worker reçoit des settings techniques source-neutral et des ressources déjà préparées. Aucun `.env`, `std::env`, URL secrète, token, API key ou header sensible n'est lu directement par la crate Worker.
|
||||||
|
|
||||||
|
## 6. Décisions acquises et questions réellement ouvertes
|
||||||
|
|
||||||
|
### 6.1 Décisions acquises
|
||||||
|
|
||||||
|
Conserver :
|
||||||
|
|
||||||
|
```text
|
||||||
|
caller fournit un runtime Tokio actif si Tokio est retenu
|
||||||
|
le Worker ne crée pas son propre runtime
|
||||||
|
JoinHandle privés
|
||||||
|
request_stop idempotent
|
||||||
|
lecteurs de snapshot sans influence sur le lifecycle
|
||||||
|
queues/channels bornés uniquement
|
||||||
|
aucun drop silencieux en saturation
|
||||||
|
start sans paramètre historique métier
|
||||||
|
snapshot sans transaction/signature/URL/secret/payload distant
|
||||||
|
Job et Worker indépendants
|
||||||
|
pas de source live complexe requise pour fermer 0.3.11
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Questions à fermer en `pre.001`
|
||||||
|
|
||||||
|
Auditer avant codage :
|
||||||
|
|
||||||
|
```text
|
||||||
|
surface publique exacte minimale de RawTransactionIngestSettings
|
||||||
|
shape exacte de RawTransactionSourceId / SourceSettings / capability / role en fondation
|
||||||
|
quels types sont publics et lesquels restent privés jusqu'aux vraies sources 0.3.12+
|
||||||
|
forme exacte du start et du handle
|
||||||
|
forme exacte d'attente terminale sans fuite nominale Tokio inutile
|
||||||
|
modèle privé de supervisor et tâches
|
||||||
|
seam déterministe permettant de tester le pipeline sans source live complexe
|
||||||
|
choix exact channel(s), capacité(s), deadlines et drain bounds
|
||||||
|
ownership des timestamps/frontiers de fondation
|
||||||
|
projection exacte WorkerSnapshot commun <-> snapshot concret
|
||||||
|
comportement de fault sur Store error / conflict / source harness failure
|
||||||
|
besoin réel d'une dépendance tokio/futures directe et features minimales
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne pas figer prématurément des champs Yellowstone/WS/Helius dans les types publics de `0.3.11`.
|
||||||
|
|
||||||
|
## 7. Objectifs/livrables et hors périmètre
|
||||||
|
|
||||||
|
### 7.1 Livrables attendus de la release
|
||||||
|
|
||||||
|
Sous réserve du sizing `pre.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-worker-raw-transaction-ingest-lib/
|
||||||
|
README.md
|
||||||
|
USAGE.md
|
||||||
|
surface crate-root minimale
|
||||||
|
settings/identity/lifecycle/handle/snapshot concrets
|
||||||
|
runtime privé borné
|
||||||
|
pipeline déterministe admission -> common RAW -> Store
|
||||||
|
fixtures/harness déterministes sans réseau obligatoire
|
||||||
|
unit/integration/public/release/security tests
|
||||||
|
plan 0.3.11
|
||||||
|
validation 0.3.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `USAGE.md` reste version-neutral.
|
||||||
|
|
||||||
|
### 7.2 Hors périmètre de `0.3.11`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone live complet
|
||||||
|
from_slot/replay réel et continuité Yellowstone
|
||||||
|
WS standard live
|
||||||
|
Helius transactionSubscribe live
|
||||||
|
HTTP live block polling
|
||||||
|
multi-provider live convergence complète
|
||||||
|
gap repair multi-source complet
|
||||||
|
feed EARLY/shred/deshred
|
||||||
|
Desk Tauri d'ingestion
|
||||||
|
Backfill multi-source
|
||||||
|
D2 STRUCTURAL
|
||||||
|
nouvelle migration Store sauf nécessité démontrée et explicitement rescindée
|
||||||
|
nouveau document Config métier Worker imposant un edge Config -> Worker concret
|
||||||
|
```
|
||||||
|
|
||||||
|
Si une de ces responsabilités devient indispensable à la fondation, le sizing doit expliquer pourquoi et redécouper la trajectoire avant de coder.
|
||||||
|
|
||||||
|
## 8. Contraintes sécurité/API/architecture spécifiques
|
||||||
|
|
||||||
|
Le Worker concret doit respecter au minimum :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Debug/Display sûrs et bornés
|
||||||
|
aucune signature brute dans logs/snapshots publics
|
||||||
|
aucun payload transaction/meta/log
|
||||||
|
aucune URL/token/header/credential
|
||||||
|
aucun remote error text arbitraire
|
||||||
|
ErrorCode statiques et domain-scoped
|
||||||
|
source ids/codes bornés et safe
|
||||||
|
counter arithmetic non-wrapping ou explicitement saturante/checked selon le contrat
|
||||||
|
channels bornés
|
||||||
|
shutdown coopératif et borné
|
||||||
|
aucune tâche orpheline après terminal
|
||||||
|
aucune queue d'événements pour remplacer latest-value
|
||||||
|
aucun cache en mémoire utilisé comme vérité de correction à la place du Store
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour le conflit de contenu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
stopper les nouvelles admissions
|
||||||
|
ne pas réécrire l'entité existante
|
||||||
|
ne pas choisir une source gagnante
|
||||||
|
publier un terminal/fault sûr
|
||||||
|
ne jamais exposer payload/signature/hash divergents dans les diagnostics publics
|
||||||
|
```
|
||||||
|
|
||||||
|
Le tracing passe par `ksp-logging-lib` si la crate Worker en dépend ; aucun bypass direct vers une autre façade de logging n'est introduit.
|
||||||
|
|
||||||
|
## 9. Première mission `pre.001` — audit/sizing obligatoire
|
||||||
|
|
||||||
|
`pre.001` doit produire **avant toute implémentation lourde** :
|
||||||
|
|
||||||
|
1. vérification exacte de la base `v0.3.10` et des deltas/rel ;
|
||||||
|
2. lecture complète des sources internes listées ci-dessus ;
|
||||||
|
3. inventaire de la surface actuelle de `ksp-worker-api`, `ksp-raw-transaction-lib`, `ksp-store-lib` et des patterns Backfill réutilisables conceptuellement ;
|
||||||
|
4. audit du graphe Cargo cible et des dépendances externes éventuellement nécessaires ;
|
||||||
|
5. brainstorming des modèles publics/privés, du supervisor, de la cancellation, du drain, des channels, de l'admission/persistence et des snapshots ;
|
||||||
|
6. définition d'un harness déterministe sans réseau qui prouve le runtime de fondation ;
|
||||||
|
7. menace/sécurité : payloads, secrets, erreurs distantes, tâches orphelines, deadlocks, saturation, cancellation races, Store conflicts ;
|
||||||
|
8. sizing réel de chaque tranche sous le budget 15–20 minutes ;
|
||||||
|
9. décision explicite : `0.3.11` reste clôturable dans une session ou doit être rescindée avant codage ;
|
||||||
|
10. création/révision du plan et de la validation de release.
|
||||||
|
|
||||||
|
Documents attendus pour le gate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md
|
||||||
|
docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le delta `pre.001` doit consigner les questions fermées, questions reportées, dépendances réellement nécessaires, validations futures et forecast recalibré.
|
||||||
|
|
||||||
|
Critère de sortie : la première tranche fonctionnelle suivante doit pouvoir être décrite précisément sans avoir à improviser son architecture pendant le codage.
|
||||||
|
|
||||||
|
## 10. Prévision souple initiale des prereleases
|
||||||
|
|
||||||
|
Cette prévision est un **point de départ à recalibrer par `pre.001`**, pas un engagement de numérotation.
|
||||||
|
|
||||||
|
### `pre.001` — audit / brainstorming / sizing
|
||||||
|
|
||||||
|
Lecture, inventaire réel, dépendances, public/private surface, runtime model, threat model, harness déterministe, plan/validation et décision de maintien ou rescission de la release.
|
||||||
|
|
||||||
|
### `pre.002` — crate + dependency firewall + settings foundation
|
||||||
|
|
||||||
|
Créer le squelette minimal, les IDs/codes/settings source-neutral réellement nécessaires et verrouiller les edges de dépendances. Ne pas ouvrir de source réseau complexe.
|
||||||
|
|
||||||
|
### `pre.003` — lifecycle / handle / start-stop privé
|
||||||
|
|
||||||
|
Matérialiser l'ownership runtime, supervisor privé, stop idempotent et terminal borné avec harness déterministe minimal.
|
||||||
|
|
||||||
|
### `pre.004` — channels bornés + admission pipeline
|
||||||
|
|
||||||
|
Introduire le chemin source déterministe -> admission bornée -> canonicalisation common, sans protocole provider concret.
|
||||||
|
|
||||||
|
### `pre.005` — persistence/déduplication Store
|
||||||
|
|
||||||
|
Fermer le chemin common RAW -> `ksp-store-lib`, outcomes new/idempotent/conflict et comportement de fault déterministe.
|
||||||
|
|
||||||
|
### `pre.006` — snapshots / source supervision foundation
|
||||||
|
|
||||||
|
Fermer latest-value concret, projection Worker API, compteurs sûrs et health/activity de fondation sans prétendre aux états de continuité live non encore prouvés.
|
||||||
|
|
||||||
|
### `pre.007` — hardening technique de fondation
|
||||||
|
|
||||||
|
Races stop/fault, saturation, drain, store failure, slow/no listeners, tâches terminales et inventaires externes. Scinder si cette tranche dépasse le budget.
|
||||||
|
|
||||||
|
### `pre.008` — gate technique final
|
||||||
|
|
||||||
|
Workspace complet, Clippy strict, suites ciblées, graphes Cargo et duplicate tree. Aucun nouveau scope fonctionnel.
|
||||||
|
|
||||||
|
### `pre.009` — réconciliation documentaire
|
||||||
|
|
||||||
|
README/USAGE, plan, validation, architecture/références réellement concernées. Aucun CHANGELOG/ROADMAP/prompt suivant.
|
||||||
|
|
||||||
|
### `pre.010` — préparation de publication
|
||||||
|
|
||||||
|
Prompt `0.3.12`, CHANGELOG, ROADMAP et fichiers mécaniques uniquement.
|
||||||
|
|
||||||
|
### `rel.001`
|
||||||
|
|
||||||
|
Publication stable mécanique sans rattrapage.
|
||||||
|
|
||||||
|
Si `pre.001` conclut que cette trajectoire n'est pas clôturable dans une seule session, **rescinder `0.3.11` avant `pre.002`** au lieu de laisser une prerelease grossir.
|
||||||
|
|
||||||
|
## 11. Versionnement, deltas, commits et tags
|
||||||
|
|
||||||
|
Règles obligatoires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison prerelease : 0.3.11-pre.NNN
|
||||||
|
Cargo : 0.3.11-pre.N
|
||||||
|
fix code/runtime : 0.3.11-pre.N.fix.M
|
||||||
|
fix doc-only : ne bump pas Cargo
|
||||||
|
delta : deltas/0.3.11/pre.NNN.md ou pre.NNN-fix.NNN.md
|
||||||
|
commit : v0.3.11-pre.NNN[-fix.NNN]
|
||||||
|
tag prerelease : aucun
|
||||||
|
tag stable final : v0.3.11 seulement après rel.001 validée
|
||||||
|
```
|
||||||
|
|
||||||
|
Toute nouvelle prerelease non-fix synchronise `workspace.package.version`, même documentaire. Tout fichier modifié incrémente son header de version selon les règles du dépôt.
|
||||||
|
|
||||||
|
Les archives d'échange restent des deltas minimaux ; ne jamais livrer une copie complète du repository lorsqu'un delta suffit.
|
||||||
|
|
||||||
|
## 12. Procédure d'application et validation opérateur
|
||||||
|
|
||||||
|
Après application d'un delta technique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
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
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis exécuter les tests ciblés de la tranche et conserver les logs opérateur réels. Ne jamais transformer une commande non lancée en PASS.
|
||||||
|
|
||||||
|
Si un gate révèle une erreur, créer un fix appartenant strictement à la responsabilité de la tranche fautive ; ne pas absorber un défaut runtime dans la réconciliation documentaire ou la publication prep.
|
||||||
|
|
||||||
|
## 13. Validations Rust / runtime / graphes pertinentes
|
||||||
|
|
||||||
|
À partir de la matérialisation de la crate Worker, le gate cible inclut progressivement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-worker-raw-transaction-ingest-lib
|
||||||
|
cargo test -p ksp-raw-transaction-lib
|
||||||
|
cargo test -p ksp-store-lib
|
||||||
|
cargo test -p ksp-worker-api
|
||||||
|
|
||||||
|
cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal
|
||||||
|
cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
À la fermeture technique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace --all-targets --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun smoke provider live n'est obligatoire pour fermer `0.3.11` tant qu'aucune source live complexe n'est dans son scope. Les futures releases ajoutent leurs propres smokes uniquement lorsque leur runtime source est effectivement matérialisé.
|
||||||
|
|
||||||
|
## 14. Critères de clôture de `0.3.11`
|
||||||
|
|
||||||
|
La release peut se fermer lorsque, sur la base réellement obtenue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate Worker concrète présente et documentée
|
||||||
|
dependency firewall prouvé
|
||||||
|
start/stop/lifecycle/terminal bornés
|
||||||
|
aucune tâche privée orpheline après terminal
|
||||||
|
settings source-neutral bornés
|
||||||
|
pipeline déterministe sans queue unbounded
|
||||||
|
canonicalisation exclusivement via ksp-raw-transaction-lib
|
||||||
|
persistence exclusivement via ksp-store-lib
|
||||||
|
new/idempotent/conflict prouvés sans payload leak
|
||||||
|
snapshots concrete + Worker API latest-value prouvés
|
||||||
|
slow/no listeners sans influence sur lifecycle
|
||||||
|
hardening cancellation/saturation/store failure/fault vert
|
||||||
|
gates workspace/clippy/tests/trees verts
|
||||||
|
README/USAGE/plan/validation réconciliés
|
||||||
|
prompt 0.3.12 produit dans la dernière prerelease
|
||||||
|
aucune source live complexe revendiquée comme support de 0.3.11 sans preuve
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. Release suivante envisagée
|
||||||
|
|
||||||
|
`0.3.12` doit reprendre uniquement après publication stable de `0.3.11` et réauditer la mission :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Yellowstone transaction/block/status
|
||||||
|
projection productive vers common RAW
|
||||||
|
hydration HTTP lorsque le matériau est incomplet
|
||||||
|
source health
|
||||||
|
from_slot / replay info
|
||||||
|
run frontier
|
||||||
|
continuité propre au run
|
||||||
|
fixtures déterministes + smokes accessibles
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt `0.3.12` sera produit à la fermeture de `0.3.11` depuis la base réellement stabilisée. Il ne doit pas être figé à l'avance au-delà du handoff déjà documenté.
|
||||||
|
|
||||||
|
## 16. Instruction d'ouverture
|
||||||
|
|
||||||
|
Au début de la prochaine session :
|
||||||
|
|
||||||
|
1. vérifier que la base correspond exactement au tag stable `v0.3.10` ;
|
||||||
|
2. lire les règles, l'architecture, le plan/validation `0.3.10` et les quatre crates de référence avant toute modification ;
|
||||||
|
3. réauditer le graphe de dépendances et les versions externes éventuellement nécessaires ;
|
||||||
|
4. produire `pre.001` avec brainstorming, sizing, plan `032` et validation `028` ;
|
||||||
|
5. **ne pas créer le runtime Worker lourd ni une source live avant fermeture de ce gate** ;
|
||||||
|
6. rescinder `0.3.11` immédiatement si son périmètre réel n'est pas clôturable dans une seule session.
|
||||||
Reference in New Issue
Block a user