Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09aacc36ab | |||
| 15b129ad04 | |||
| 6ddf2b4995 | |||
| dcabceb52f | |||
| 51bcf870d3 | |||
| c6c5793c61 | |||
| d98ee736be | |||
| 39dbd86f1e | |||
| c938b2409e | |||
| 5b30bb9948 | |||
| 813a45385a | |||
| 21ed5f88da | |||
| 3198cc5666 | |||
| 4568d94a62 | |||
| d32d8de3a5 | |||
| a9fb6a7ac8 | |||
| 433e69272a | |||
| affa18a6e9 | |||
| 05aec88178 | |||
| b32de48c0b | |||
| 15e3402e1c | |||
| dca0a4b809 | |||
| 20320dba7b | |||
| e4bbc78a41 | |||
| feb9befb35 | |||
| a038194679 | |||
| 3172cda241 | |||
| 61fba107ef | |||
| 835de48cb7 | |||
| 21de590aea | |||
| 3081bd1085 | |||
| bdcab1e1a7 |
10
.env.example
10
.env.example
@@ -1,5 +1,5 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 6
|
# version: 8
|
||||||
|
|
||||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||||
@@ -34,6 +34,14 @@ KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
|
|||||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||||
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
||||||
|
|
||||||
|
# PublicNode Mainnet personal token used as secret x-token metadata by the committed Mainnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real value only in the process environment or local .env; do not assume that a Testnet token is authorized on Mainnet.
|
||||||
|
# KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
|
# PublicNode Testnet personal token used as secret x-token metadata by the committed Testnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real value only in the process environment or local .env; do not assume that a Mainnet token is authorized on Testnet.
|
||||||
|
# KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
# Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json.
|
# Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json.
|
||||||
# Keep the real credential only in the process environment or local .env; never commit it.
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,10 +1,18 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
||||||
|
|
||||||
|
## 0.2.9 — Yellowstone gRPC standard/provider-neutral — 2026-08-24
|
||||||
|
|
||||||
|
`0.2.9` ajoute à `ksp-onchain-transport-lib` un moteur Yellowstone gRPC partagé construit directement sur Tonic/Protobuf, avec `yellowstone-grpc-proto` comme contrat wire upstream et sans `yellowstone-grpc-client` dans le runtime KSP. La surface Solana standard reste provider-neutral et couvre les sept unary retenues (`SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid`, `GetVersion`), `Subscribe` bidi, les familles accounts/slots/transactions/transaction status/blocks/block meta/entry, les neuf variantes d'update standard retenues, les filtres/common fields actuels, le backpressure, les limites de messages, le half-close/shutdown, ainsi qu'un reconnect/replay prudent depuis le dernier slot observé. Aucune garantie exactly-once, lossless ou d'ordre global gapless n'est introduite ; gaps et duplicates restent observables. `SubscribeDeshred` reste explicitement hors du standard KSP de cette release.
|
||||||
|
|
||||||
|
Config passe à `std.transport` V3, backward-readable avec V1 HTTP et V2 HTTP+WebSocket, et sépare les axes `provider` / `protocol` ainsi que `metadata` / `secret_metadata` sans créer de dépendance Transport -> Config/env. PublicNode constitue la première intégration provider : profils Yellowstone Mainnet et Testnet sur `https://solana-yellowstone-grpc.publicnode.com:443` et `https://solana-testnet-yellowstone-grpc.publicnode.com:443`, avec authentification `x-token` résolue par Config depuis des variables `KSP_SECRET_*`. Le smoke Transport pur reçoit les credentials uniquement par stdin opérateur, n'en journalise aucun et valide en live `Subscribe -> Slot` sur les deux réseaux ; le même personal token a été utilisé avec succès pour Mainnet et Testnet, tout en conservant deux entrées Config distinctes par flexibilité opérationnelle.
|
||||||
|
|
||||||
|
La clôture conserve les surfaces antérieures HTTP (**52 current typed + 14 historiques**), WebSocket Solana standard (**9 familles / 18 opérations**) et Helius LaserStream WebSocket. Le gate final comprend **383/383** tests unit Transport, **49/49** public API, **43/43** release completeness, **4/4** doctests, `cargo test --workspace`, les canaris de dépendances, le smoke PublicNode live **2/2 PASS** et l'inspection des graphes Cargo (`yellowstone-grpc-proto 12.6.0`, `tonic` / `tonic-prost 0.14.6`, `prost` / `prost-types 0.14.4`). `prompts/015-V0_2_10_START_PROMPT.md` ouvre ensuite `0.2.10 — OrbitFlare Yellowstone gRPC` uniquement depuis le tag stable `v0.2.9`, avec audit provider actuel, threat model et sizing en `pre.001` avant toute extension provider lourde.
|
||||||
|
|
||||||
## 0.2.8 — Helius LaserStream WebSocket — 2026-08-23
|
## 0.2.8 — Helius LaserStream WebSocket — 2026-08-23
|
||||||
|
|
||||||
`0.2.8` étend `ksp-onchain-transport-lib` avec une façade `HeliusLaserStreamWsSession` dédiée qui réutilise le même `WsSession` physique/actor que le WebSocket Solana standard, sans second client, socket, registry ou scheduler. La surface stable Helius réutilise les sept familles standard actuellement retenues (`account`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates`) et ajoute l’extension typée `transactionSubscribe` / `transactionUnsubscribe`; `block` et `vote` restent absents de la façade Helius et `slotsUpdates` conserve son statut unstable. Le heartbeat provider est possédé par l’actor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`.
|
`0.2.8` étend `ksp-onchain-transport-lib` avec une façade `HeliusLaserStreamWsSession` dédiée qui réutilise le même `WsSession` physique/actor que le WebSocket Solana standard, sans second client, socket, registry ou scheduler. La surface stable Helius réutilise les sept familles standard actuellement retenues (`account`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates`) et ajoute l’extension typée `transactionSubscribe` / `transactionUnsubscribe`; `block` et `vote` restent absents de la façade Helius et `slotsUpdates` conserve son statut unstable. Le heartbeat provider est possédé par l’actor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`.
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 236
|
# version: 261
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.8"
|
version = "0.2.9"
|
||||||
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"
|
||||||
@@ -15,31 +15,35 @@ publish = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
argon2 = { version = "^0.5", default-features = false }
|
argon2 = { version = "^0.5", default-features = false }
|
||||||
|
base64 = { version = "^0.23" }
|
||||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||||
|
chrono = { version = "^0.4", default-features = false }
|
||||||
directories = { version = "^6.0" }
|
directories = { version = "^6.0" }
|
||||||
ed25519-dalek = { version = "^3.0", default-features = false }
|
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||||
getrandom = { version = "^0.4", default-features = false }
|
|
||||||
base64 = { version = "^0.23" }
|
|
||||||
fs2 = { version = "^0.4" }
|
fs2 = { version = "^0.4" }
|
||||||
futures-util = { version = "^0.3", default-features = false }
|
futures-util = { version = "^0.3", default-features = false }
|
||||||
serde = { version = "^1.0" }
|
getrandom = { version = "^0.4", default-features = false }
|
||||||
serde_json = { version = "^1.0" }
|
http = { version = "^1.5", default-features = false }
|
||||||
jsonschema = { version = "^0.51", default-features = false }
|
jsonschema = { version = "^0.51", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
|
serde = { version = "^1.0" }
|
||||||
|
serde_json = { version = "^1.0" }
|
||||||
solana-keypair = { version = "^3.1", default-features = false }
|
solana-keypair = { version = "^3.1", default-features = false }
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
|
tauri = { version = "^2.11" }
|
||||||
|
tauri-build = { version = "^2.6" }
|
||||||
|
tauri-plugin-dialog = { version = "^2.7" }
|
||||||
|
tauri-plugin-tracing = { version = "^0.3" }
|
||||||
|
tempfile = { version = "^3.27" }
|
||||||
tracing = { version = "^0.1", default-features = false }
|
tracing = { version = "^0.1", default-features = false }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
tokio = { version = "^1.53", default-features = false }
|
tokio = { version = "^1.53", default-features = false }
|
||||||
tokio-tungstenite = { version = "^0.30", default-features = false }
|
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||||
tempfile = { version = "^3.27" }
|
tonic = { version = "^0.14", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
tonic-prost = { version = "^0.14", default-features = false }
|
||||||
tauri = { version = "^2.11" }
|
|
||||||
tauri-build = { version = "^2.6" }
|
|
||||||
tauri-plugin-dialog = { version = "^2.7" }
|
|
||||||
tauri-plugin-tracing = { version = "^0.3" }
|
|
||||||
ts-rs = { version = "^12.0" }
|
ts-rs = { version = "^12.0" }
|
||||||
|
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
|
||||||
zeroize = { version = "^1.9" }
|
zeroize = { version = "^1.9" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
|
|||||||
23
ROADMAP.md
23
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 83 -->
|
<!-- version: 86 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -53,11 +53,22 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
- [X] `0.2.6` — `ksp-app-wallet-desk` + `.kspwallet` V2 stables : composition Config/Wallet/HTTP/Logging, lifecycle VIEW/OWNER, balance, administration/import/export, wire binaire V2, APIs multi-version, migration V1 -> V2 explicite et runtime Tauri packagé user-writable validés ; bundles Linux `.deb`/`.rpm`/`.AppImage` produits avant publication. Plan clôturé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
|
- [X] `0.2.6` — `ksp-app-wallet-desk` + `.kspwallet` V2 stables : composition Config/Wallet/HTTP/Logging, lifecycle VIEW/OWNER, balance, administration/import/export, wire binaire V2, APIs multi-version, migration V1 -> V2 explicite et runtime Tauri packagé user-writable validés ; bundles Linux `.deb`/`.rpm`/`.AppImage` produits avant publication. Plan clôturé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
|
||||||
- [X] `0.2.7` — WebSocket Solana standard stable : 9 familles subscribe/unsubscribe typées (18/18 opérations), sessions physiques multiples explicites, subscriptions logiques typées, lifecycle/reconnect/resubscribe/backpressure/shutdown bornés, Config V2, non-régression HTTP 52+14, compliance finale, smoke WebSocket Devnet et audit de dépendances validés ; publication `rel.001` et prompt `0.2.8` prêts.
|
- [X] `0.2.7` — WebSocket Solana standard stable : 9 familles subscribe/unsubscribe typées (18/18 opérations), sessions physiques multiples explicites, subscriptions logiques typées, lifecycle/reconnect/resubscribe/backpressure/shutdown bornés, Config V2, non-régression HTTP 52+14, compliance finale, smoke WebSocket Devnet et audit de dépendances validés ; publication `rel.001` et prompt `0.2.8` prêts.
|
||||||
- [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur l’actor WebSocket partagé, sept familles standard réutilisées (`account/logs/program/root/signature/slot/slotsUpdates`) + `transactionSubscribe`/`transactionUnsubscribe`, `block/vote` absents, heartbeat Ping 60 s Helius-only, Config V2/secrets redacted, lifecycle adversarial, compliance HTTP 52+14 / Standard WS 18/18 et graphes Cargo finaux validés ; prompt `0.2.9` prêt.
|
- [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur l’actor WebSocket partagé, sept familles standard réutilisées (`account/logs/program/root/signature/slot/slotsUpdates`) + `transactionSubscribe`/`transactionUnsubscribe`, `block/vote` absents, heartbeat Ping 60 s Helius-only, Config V2/secrets redacted, lifecycle adversarial, compliance HTTP 52+14 / Standard WS 18/18 et graphes Cargo finaux validés ; prompt `0.2.9` prêt.
|
||||||
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
- [X] `0.2.9` — Yellowstone gRPC standard/provider-neutral stable : moteur Tonic/Protobuf KSP partagé, sept unary standard retenues, `Subscribe` bidi et neuf variantes d’update, lifecycle/backpressure/reconnect/replay bornés sans promesse lossless, Config Transport V3 backward V1/V2 avec provider/protocol séparés, profils PublicNode Mainnet/Testnet authentifiés par `x-token`, smoke live `Subscribe -> Slot` 2/2 PASS et graphes Cargo finaux inspectés ; `SubscribeDeshred` reste hors scope.
|
||||||
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
- [ ] `0.2.10` — Ajouter l’intégration Yellowstone gRPC OrbitFlare sur le moteur/standard acquis, avec audit explicite des auth/capabilities/restrictions/extensions et Devnet comme cible gratuite prioritaire.
|
||||||
- [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
|
- [ ] `0.2.11` — Ajouter l’intégration Helius LaserStream gRPC sur le moteur Yellowstone partagé, avec audit de compatibilité/replay/auth/extensions, sans confondre cette surface avec LaserStream WebSocket.
|
||||||
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
- [ ] `0.2.12` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
||||||
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
- [ ] `0.2.13` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
|
||||||
|
- [ ] `0.2.14` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
||||||
|
- [ ] `0.2.15` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
||||||
|
|
||||||
|
### TODO/IDEAS — providers Yellowstone non planifiés
|
||||||
|
|
||||||
|
- [ ] **TODO** — eRPC : réauditer accès, auth/IP policy, capabilities et produits complémentaires avant toute décision d’implémentation.
|
||||||
|
- [ ] **TODO** — Triton : réauditer la frontière Yellowstone upstream / extensions Triton, notamment Deshred et futures extensions.
|
||||||
|
- [ ] **TODO** — Alchemy : réauditer auth, replay, limites et capabilities Yellowstone avant toute intégration.
|
||||||
|
- [ ] **TODO** — QuickNode : réauditer auth, compression, `from_slot`, filtres et limites de plan.
|
||||||
|
- [ ] **TODO** — Chainstack : réauditer networks, auth, add-on et capabilities Yellowstone.
|
||||||
|
- [ ] **IDEAS** — Tatum, Shyft, Solinfra, NodeFlare et autres providers : conserver comme candidats exploratoires sans numéro de release ni engagement d’implémentation.
|
||||||
|
|
||||||
### Règles Transport pour toute la série
|
### Règles Transport pour toute la série
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "urn:ksp:schema:std.transport:v2",
|
"$id": "urn:ksp:schema:std.transport:v3",
|
||||||
"title": "KSP standard HTTP + WebSocket Transport configuration",
|
"title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/$defs/documentV1"
|
"$ref": "#/$defs/documentV1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/$defs/documentV2"
|
"$ref": "#/$defs/documentV2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/documentV3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"$defs": {
|
"$defs": {
|
||||||
@@ -455,6 +458,275 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"grpcPositiveMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 300000
|
||||||
|
},
|
||||||
|
"grpcCapacity": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65536
|
||||||
|
},
|
||||||
|
"grpcMessageSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 536870912
|
||||||
|
},
|
||||||
|
"grpcReconnect": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"max_retries",
|
||||||
|
"initial_backoff_ms",
|
||||||
|
"max_backoff_ms"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"max_retries": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"initial_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"max_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcReconnectOverride": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"properties": {
|
||||||
|
"max_retries": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"initial_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"max_backoff_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcSession": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"connect_timeout_ms",
|
||||||
|
"unary_timeout_ms",
|
||||||
|
"close_timeout_ms",
|
||||||
|
"reconnect",
|
||||||
|
"request_channel_capacity",
|
||||||
|
"update_channel_capacity",
|
||||||
|
"max_inbound_message_size_bytes",
|
||||||
|
"max_outbound_message_size_bytes"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"connect_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"unary_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"close_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"reconnect": {
|
||||||
|
"$ref": "#/$defs/grpcReconnect"
|
||||||
|
},
|
||||||
|
"request_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"update_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"max_inbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
},
|
||||||
|
"max_outbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcSessionOverride": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1,
|
||||||
|
"properties": {
|
||||||
|
"connect_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"unary_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"close_timeout_ms": {
|
||||||
|
"$ref": "#/$defs/grpcPositiveMs"
|
||||||
|
},
|
||||||
|
"reconnect": {
|
||||||
|
"$ref": "#/$defs/grpcReconnectOverride"
|
||||||
|
},
|
||||||
|
"request_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"update_channel_capacity": {
|
||||||
|
"$ref": "#/$defs/grpcCapacity"
|
||||||
|
},
|
||||||
|
"max_inbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
},
|
||||||
|
"max_outbound_message_size_bytes": {
|
||||||
|
"$ref": "#/$defs/grpcMessageSize"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"key",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128,
|
||||||
|
"pattern": "^[0-9a-z_.-]+$"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 8192
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpcEndpoint": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"enabled",
|
||||||
|
"provider",
|
||||||
|
"cluster",
|
||||||
|
"protocol",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"$ref": "#/$defs/descriptor"
|
||||||
|
},
|
||||||
|
"protocol": {
|
||||||
|
"const": "solana_yellowstone"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 8192
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 64,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcMetadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"secret_metadata": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 64,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcMetadata"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"$ref": "#/$defs/grpcSessionOverride"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profileV3": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"profile_id",
|
||||||
|
"endpoints",
|
||||||
|
"ws_endpoints"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"profile_id": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/httpEndpoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ws_endpoints": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/wsEndpoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grpc_endpoints": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 128,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/grpcEndpoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentV3": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"format_version",
|
||||||
|
"retry",
|
||||||
|
"ws_defaults",
|
||||||
|
"grpc_defaults",
|
||||||
|
"default_profile",
|
||||||
|
"profiles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"format_version": {
|
||||||
|
"const": 3
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"$ref": "#/$defs/retry"
|
||||||
|
},
|
||||||
|
"ws_defaults": {
|
||||||
|
"$ref": "#/$defs/wsSession"
|
||||||
|
},
|
||||||
|
"grpc_defaults": {
|
||||||
|
"$ref": "#/$defs/grpcSession"
|
||||||
|
},
|
||||||
|
"default_profile": {
|
||||||
|
"$ref": "#/$defs/profileId"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/profileV3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"format_version": 2,
|
"format_version": 3,
|
||||||
"retry": {
|
"retry": {
|
||||||
"max_retries": 2,
|
"max_retries": 2,
|
||||||
"initial_backoff_ms": 100,
|
"initial_backoff_ms": 100,
|
||||||
@@ -22,6 +22,20 @@
|
|||||||
"max_frame_size_bytes": 16777216,
|
"max_frame_size_bytes": 16777216,
|
||||||
"max_write_buffer_size_bytes": 1048576
|
"max_write_buffer_size_bytes": 1048576
|
||||||
},
|
},
|
||||||
|
"grpc_defaults": {
|
||||||
|
"connect_timeout_ms": 10000,
|
||||||
|
"unary_timeout_ms": 10000,
|
||||||
|
"close_timeout_ms": 5000,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 5,
|
||||||
|
"initial_backoff_ms": 250,
|
||||||
|
"max_backoff_ms": 5000
|
||||||
|
},
|
||||||
|
"request_channel_capacity": 128,
|
||||||
|
"update_channel_capacity": 256,
|
||||||
|
"max_inbound_message_size_bytes": 67108864,
|
||||||
|
"max_outbound_message_size_bytes": 67108864
|
||||||
|
},
|
||||||
"default_profile": "devnet_public",
|
"default_profile": "devnet_public",
|
||||||
"profiles": [
|
"profiles": [
|
||||||
{
|
{
|
||||||
@@ -105,6 +119,120 @@
|
|||||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "publicnode_mainnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_mainnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "publicnode_testnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_testnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"url": "https://api.testnet.solana.com",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_testnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "wss://api.testnet.solana.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "publicnode_solana_testnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "publicnode",
|
||||||
|
"cluster": "testnet",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-app-config-desk/Cargo.toml
|
# file: crates/ksp-app-config-desk/Cargo.toml
|
||||||
# version: 8
|
# version: 9
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-app-config-desk"
|
name = "ksp-app-config-desk"
|
||||||
@@ -22,6 +22,7 @@ path = "src/main.rs"
|
|||||||
tauri-build.workspace = true
|
tauri-build.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { workspace = true, features = ["std", "now"] }
|
||||||
fs2.workspace = true
|
fs2.workspace = true
|
||||||
ksp-config-lib = { path = "../ksp-config-lib" }
|
ksp-config-lib = { path = "../ksp-config-lib" }
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
@@ -30,7 +31,6 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tauri.workspace = true
|
tauri.workspace = true
|
||||||
tauri-plugin-tracing.workspace = true
|
tauri-plugin-tracing.workspace = true
|
||||||
chrono = { workspace = true, features = ["std", "now"] }
|
|
||||||
tokio = { workspace = true, features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
ts-rs.workspace = true
|
ts-rs.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-config-lib/Cargo.toml
|
# file: crates/ksp-config-lib/Cargo.toml
|
||||||
# version: 7
|
# version: 8
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-config-lib"
|
name = "ksp-config-lib"
|
||||||
@@ -9,12 +9,12 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
directories.workspace = true
|
directories.workspace = true
|
||||||
|
jsonschema.workspace = true
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
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" }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
jsonschema.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/README.md -->
|
<!-- file: crates/ksp-config-lib/README.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# ksp-config-lib
|
# ksp-config-lib
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
|
|||||||
|
|
||||||
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
||||||
|
|
||||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs HTTP et WebSocket : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
|
|||||||
- [`TODO.md`](TODO.md) — points explicitement différés ;
|
- [`TODO.md`](TODO.md) — points explicitement différés ;
|
||||||
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
|
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
|
||||||
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
|
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
|
||||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ;
|
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
|
||||||
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
||||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
|
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
|
||||||
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# Utilisation de ksp-config-lib
|
# Utilisation de ksp-config-lib
|
||||||
|
|
||||||
@@ -125,9 +125,9 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce
|
|||||||
|
|
||||||
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
||||||
|
|
||||||
### 4.1 Construire le Transport HTTP + WebSocket depuis Config
|
### 4.1 Construire le Transport HTTP + WebSocket + Yellowstone gRPC depuis Config
|
||||||
|
|
||||||
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
|
Config possède l'adapter du document `std.transport` vers les contrats runtime de `ksp-onchain-transport-lib` :
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
|
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
|
||||||
@@ -137,18 +137,30 @@ let transport = match engine.load_resolved_transport_config(std::option::Option:
|
|||||||
|
|
||||||
let http_settings = transport.http_settings();
|
let http_settings = transport.http_settings();
|
||||||
let ws_settings = transport.ws_settings();
|
let ws_settings = transport.ws_settings();
|
||||||
let _ = (http_settings, ws_settings);
|
let grpc_settings = transport.grpc_settings();
|
||||||
|
let _ = (http_settings, ws_settings, grpc_settings);
|
||||||
```
|
```
|
||||||
|
|
||||||
`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis accepte les protocoles WebSocket `kind = "solana_standard"` et `kind = "helius_laserstream"`. Ce second discriminateur appartient exclusivement au namespace WebSocket et mappe vers `WsProtocolKind::HeliusLaserStream`; il ne préfigure aucun contrat LaserStream gRPC. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`.
|
`std.transport` V3 conserve intégralement les shapes V1/V2, ajoute `grpc_defaults` au niveau global et permet `profiles[].grpc_endpoints[]`. Les profils V3 peuvent rester HTTP + WebSocket seulement : l'absence de `grpc_endpoints` mappe vers `None` et n'invente aucun endpoint. La lecture V1 HTTP-only et V2 HTTP + WebSocket reste stricte et backward-compatible.
|
||||||
|
|
||||||
Pour Helius LaserStream WebSocket, l'exemple versionné couvre explicitement les deux réseaux supportés par ce contrat : mainnet via `wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}` et devnet via `wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}`. Les deux réseaux vivent dans des profils Config distincts afin de ne pas mélanger des clusters dans un même profil logique. Config reste l'unique propriétaire de `KSP_SECRET_HELIUS_API_KEY` : il résout la clé dans l'URL effective et transmet au Transport un `WsEndpointUrl` utilisable au runtime. Dans `safe_value`, Config conserve les segments littéraux non sensibles d'une chaîne composée et remplace uniquement chaque segment secret par `********` ; les projections deviennent donc respectivement `wss://mainnet.helius-rpc.com/?api-key=********` et `wss://devnet.helius-rpc.com/?api-key=********`. Les représentations `Debug` restent sûres et n'exposent jamais la clé réelle. Transport ne lit jamais directement l'environnement.
|
Un endpoint gRPC V3 sépare explicitement :
|
||||||
|
|
||||||
Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket.
|
```text
|
||||||
|
provider = description de l'opérateur/exécution, par exemple publicnode
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
metadata = metadata non secrète
|
||||||
|
secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*/KSPB_SECRET_*
|
||||||
|
```
|
||||||
|
|
||||||
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
|
`protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
|
||||||
|
|
||||||
La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle.
|
||||||
|
|
||||||
|
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
|
||||||
|
|
||||||
|
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
|
||||||
|
|
||||||
|
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
||||||
|
|
||||||
### 4.2 Résoudre le répertoire Wallet depuis Config
|
### 4.2 Résoudre le répertoire Wallet depuis Config
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/lib.rs
|
// file: crates/ksp-config-lib/src/lib.rs
|
||||||
// version: 17
|
// version: 18
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -9,8 +9,9 @@
|
|||||||
//!
|
//!
|
||||||
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
||||||
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
||||||
//! provenance. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface
|
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
|
||||||
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
|
//! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
|
||||||
|
//! JSON/`.env` persistence.
|
||||||
|
|
||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
mod composite;
|
mod composite;
|
||||||
@@ -154,9 +155,9 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
|||||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||||
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||||
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||||
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
|
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
|
||||||
/// Default physical filename for the standard Wallet configuration document.
|
/// Default physical filename for the standard Wallet configuration document.
|
||||||
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
|
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
|
||||||
@@ -168,13 +169,13 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
|||||||
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||||
/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||||
/// Logical file identifier for the standard Logging configuration document.
|
/// Logical file identifier for the standard Logging configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||||
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||||
/// Logical file identifier for the standard Wallet configuration document.
|
/// Logical file identifier for the standard Wallet configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_WALLET;
|
pub use self::registry::FILE_ID_STD_WALLET;
|
||||||
@@ -188,7 +189,7 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
|||||||
pub use self::sensitivity::ResolvedConfigJson;
|
pub use self::sensitivity::ResolvedConfigJson;
|
||||||
/// One resolved Config string preserving real/safe representations and provenance.
|
/// One resolved Config string preserving real/safe representations and provenance.
|
||||||
pub use self::sensitivity::ResolvedConfigText;
|
pub use self::sensitivity::ResolvedConfigText;
|
||||||
/// Effective standard Transport configuration mapped to HTTP and optional WebSocket runtime settings.
|
/// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
|
||||||
pub use self::transport::ResolvedTransportConfig;
|
pub use self::transport::ResolvedTransportConfig;
|
||||||
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
||||||
pub use self::wallet::ResolvedWalletConfig;
|
pub use self::wallet::ResolvedWalletConfig;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/registry.rs
|
// file: crates/ksp-config-lib/src/registry.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||||
@@ -11,9 +11,9 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
|||||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||||
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||||
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||||
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
|
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
|
||||||
/// Default physical filename for the standard Wallet configuration document.
|
/// Default physical filename for the standard Wallet configuration document.
|
||||||
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
|
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
|
||||||
@@ -25,13 +25,13 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w
|
|||||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||||
/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||||
/// Logical file identifier for the standard Logging configuration document.
|
/// Logical file identifier for the standard Logging configuration document.
|
||||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||||
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||||
/// Logical file identifier for the standard Wallet configuration document.
|
/// Logical file identifier for the standard Wallet configuration document.
|
||||||
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
|
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/ksp-config-lib/src/transport.rs
|
// file: crates/ksp-config-lib/src/transport.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts.
|
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime contracts.
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct ResolvedTransportConfig {
|
pub struct ResolvedTransportConfig {
|
||||||
file_id: crate::ConfigFileId,
|
file_id: crate::ConfigFileId,
|
||||||
@@ -11,6 +11,7 @@ pub struct ResolvedTransportConfig {
|
|||||||
effective: crate::ResolvedConfigJson,
|
effective: crate::ResolvedConfigJson,
|
||||||
settings: ksp_onchain_transport_lib::HttpTransportSettings,
|
settings: ksp_onchain_transport_lib::HttpTransportSettings,
|
||||||
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
||||||
|
grpc_settings: std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedTransportConfig {
|
impl ResolvedTransportConfig {
|
||||||
@@ -61,7 +62,7 @@ impl ResolvedTransportConfig {
|
|||||||
return &self.settings;
|
return &self.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns validated WebSocket Transport settings when the selected document uses format V2.
|
/// Returns validated WebSocket Transport settings when the selected document uses format V2 or V3.
|
||||||
///
|
///
|
||||||
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
|
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -69,19 +70,42 @@ impl ResolvedTransportConfig {
|
|||||||
return self.ws_settings.as_ref();
|
return self.ws_settings.as_ref();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns validated Yellowstone gRPC Transport settings when the selected V3 profile declares gRPC endpoints.
|
||||||
|
///
|
||||||
|
/// V1/V2 documents and V3 profiles without `grpc_endpoints` return [`std::option::Option::None`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn grpc_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings> {
|
||||||
|
return self.grpc_settings.as_ref();
|
||||||
|
}
|
||||||
|
|
||||||
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
|
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
|
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
|
||||||
return self.settings;
|
return self.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consumes this resolved Config and returns both HTTP and optional WebSocket runtime settings.
|
/// Consumes this resolved Config and returns HTTP plus optional WebSocket runtime settings.
|
||||||
|
///
|
||||||
|
/// This compatibility accessor intentionally preserves the V2 tuple shape. Use [`Self::into_all_transport_settings`] when Yellowstone gRPC settings are
|
||||||
|
/// also required.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_transport_settings(
|
pub fn into_transport_settings(
|
||||||
self,
|
self,
|
||||||
) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) {
|
) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) {
|
||||||
return (self.settings, self.ws_settings);
|
return (self.settings, self.ws_settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consumes this resolved Config and returns HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_all_transport_settings(
|
||||||
|
self,
|
||||||
|
) -> (
|
||||||
|
ksp_onchain_transport_lib::HttpTransportSettings,
|
||||||
|
std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
||||||
|
std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
|
||||||
|
) {
|
||||||
|
return (self.settings, self.ws_settings, self.grpc_settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for ResolvedTransportConfig {
|
impl std::fmt::Debug for ResolvedTransportConfig {
|
||||||
@@ -94,15 +118,17 @@ impl std::fmt::Debug for ResolvedTransportConfig {
|
|||||||
.field("selection_source", &self.selection_source)
|
.field("selection_source", &self.selection_source)
|
||||||
.field("effective", &self.effective)
|
.field("effective", &self.effective)
|
||||||
.field("has_ws_settings", &self.ws_settings.is_some())
|
.field("has_ws_settings", &self.ws_settings.is_some())
|
||||||
|
.field("has_grpc_settings", &self.grpc_settings.is_some())
|
||||||
.finish_non_exhaustive();
|
.finish_non_exhaustive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::ConfigDocumentEngine {
|
impl crate::ConfigDocumentEngine {
|
||||||
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket runtime settings.
|
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket and Yellowstone
|
||||||
|
/// gRPC runtime settings.
|
||||||
///
|
///
|
||||||
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
|
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
|
||||||
/// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints.
|
/// because Transport URL wrappers own runtime redaction. V1 remains HTTP-only, V2 adds WebSocket and V3 can additionally map Yellowstone gRPC.
|
||||||
pub fn load_resolved_transport_config(
|
pub fn load_resolved_transport_config(
|
||||||
&self,
|
&self,
|
||||||
requested_profile: std::option::Option<&str>,
|
requested_profile: std::option::Option<&str>,
|
||||||
@@ -121,7 +147,8 @@ impl crate::ConfigDocumentEngine {
|
|||||||
return resolve_transport_profile(&profile, environment);
|
return resolve_transport_profile(&profile, environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket runtime adapters while preserving selection provenance.
|
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket and Yellowstone gRPC runtime adapters while preserving
|
||||||
|
/// selection provenance.
|
||||||
///
|
///
|
||||||
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`.
|
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`.
|
||||||
pub fn resolve_transport_config_profile(
|
pub fn resolve_transport_config_profile(
|
||||||
@@ -148,9 +175,13 @@ struct EffectiveTransportSource {
|
|||||||
retry: EffectiveRetrySource,
|
retry: EffectiveRetrySource,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
|
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
|
||||||
|
#[serde(default)]
|
||||||
|
grpc_defaults: std::option::Option<EffectiveGrpcSessionSource>,
|
||||||
endpoints: std::vec::Vec<EffectiveEndpointSource>,
|
endpoints: std::vec::Vec<EffectiveEndpointSource>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
|
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
|
||||||
|
#[serde(default)]
|
||||||
|
grpc_endpoints: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@@ -255,6 +286,72 @@ struct EffectiveWsReconnectOverrideSource {
|
|||||||
max_backoff_ms: std::option::Option<u64>,
|
max_backoff_ms: std::option::Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcReconnectSource {
|
||||||
|
max_retries: u32,
|
||||||
|
initial_backoff_ms: u64,
|
||||||
|
max_backoff_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcReconnectOverrideSource {
|
||||||
|
max_retries: std::option::Option<u32>,
|
||||||
|
initial_backoff_ms: std::option::Option<u64>,
|
||||||
|
max_backoff_ms: std::option::Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcSessionSource {
|
||||||
|
connect_timeout_ms: u64,
|
||||||
|
unary_timeout_ms: u64,
|
||||||
|
close_timeout_ms: u64,
|
||||||
|
reconnect: EffectiveGrpcReconnectSource,
|
||||||
|
request_channel_capacity: usize,
|
||||||
|
update_channel_capacity: usize,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcSessionOverrideSource {
|
||||||
|
connect_timeout_ms: std::option::Option<u64>,
|
||||||
|
unary_timeout_ms: std::option::Option<u64>,
|
||||||
|
close_timeout_ms: std::option::Option<u64>,
|
||||||
|
reconnect: std::option::Option<EffectiveGrpcReconnectOverrideSource>,
|
||||||
|
request_channel_capacity: std::option::Option<usize>,
|
||||||
|
update_channel_capacity: std::option::Option<usize>,
|
||||||
|
max_inbound_message_size_bytes: std::option::Option<usize>,
|
||||||
|
max_outbound_message_size_bytes: std::option::Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcMetadataSource {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveGrpcEndpointSource {
|
||||||
|
name: String,
|
||||||
|
enabled: bool,
|
||||||
|
provider: String,
|
||||||
|
cluster: String,
|
||||||
|
protocol: String,
|
||||||
|
url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
|
||||||
|
#[serde(default)]
|
||||||
|
secret_metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
|
||||||
|
#[serde(default)]
|
||||||
|
session: std::option::Option<EffectiveGrpcSessionOverrideSource>,
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
|
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
|
||||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile");
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile");
|
||||||
let effective = profile.resolve_effective_environment_detailed(environment);
|
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||||
@@ -294,16 +391,26 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
let grpc_settings = map_optional_grpc_settings(format_version, source.grpc_defaults, source.grpc_endpoints, &effective, profile);
|
||||||
|
let grpc_settings = match grpc_settings {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
let ws_endpoint_count = match ws_settings.as_ref() {
|
let ws_endpoint_count = match ws_settings.as_ref() {
|
||||||
std::option::Option::Some(value) => value.endpoints().len(),
|
std::option::Option::Some(value) => value.endpoints().len(),
|
||||||
std::option::Option::None => 0_usize,
|
std::option::Option::None => 0_usize,
|
||||||
};
|
};
|
||||||
|
let grpc_endpoint_count = match grpc_settings.as_ref() {
|
||||||
|
std::option::Option::Some(value) => value.endpoints().len(),
|
||||||
|
std::option::Option::None => 0_usize,
|
||||||
|
};
|
||||||
ksp_logging_lib::debug!(
|
ksp_logging_lib::debug!(
|
||||||
target: crate::TRACING_TARGET,
|
target: crate::TRACING_TARGET,
|
||||||
profile_id = profile.profile_id(),
|
profile_id = profile.profile_id(),
|
||||||
format_version,
|
format_version,
|
||||||
http_endpoint_count = settings.endpoints().len(),
|
http_endpoint_count = settings.endpoints().len(),
|
||||||
ws_endpoint_count,
|
ws_endpoint_count,
|
||||||
|
grpc_endpoint_count,
|
||||||
"mapped standard Transport Config to runtime settings"
|
"mapped standard Transport Config to runtime settings"
|
||||||
);
|
);
|
||||||
return std::result::Result::Ok(ResolvedTransportConfig {
|
return std::result::Result::Ok(ResolvedTransportConfig {
|
||||||
@@ -314,6 +421,7 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
|||||||
effective,
|
effective,
|
||||||
settings,
|
settings,
|
||||||
ws_settings,
|
ws_settings,
|
||||||
|
grpc_settings,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,18 +436,22 @@ fn map_optional_ws_settings(
|
|||||||
if defaults.is_some() || sources.is_some() {
|
if defaults.is_some() || sources.is_some() {
|
||||||
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
|
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
|
||||||
} else {
|
} else {
|
||||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapped backward-compatible Transport V1 without WebSocket settings");
|
ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
profile_id = profile.profile_id(),
|
||||||
|
"mapped backward-compatible Transport V1 without WebSocket settings"
|
||||||
|
);
|
||||||
std::result::Result::Ok(std::option::Option::None)
|
std::result::Result::Ok(std::option::Option::None)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
2 => {
|
2 | 3 => {
|
||||||
let defaults = match defaults {
|
let defaults = match defaults {
|
||||||
std::option::Option::Some(value) => value,
|
std::option::Option::Some(value) => value,
|
||||||
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 requires ws_defaults")),
|
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 requires ws_defaults")),
|
||||||
};
|
};
|
||||||
let sources = match sources {
|
let sources = match sources {
|
||||||
std::option::Option::Some(value) => value,
|
std::option::Option::Some(value) => value,
|
||||||
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 profile requires ws_endpoints")),
|
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 profile requires ws_endpoints")),
|
||||||
};
|
};
|
||||||
let endpoints = map_ws_endpoints(sources, &defaults, profile);
|
let endpoints = map_ws_endpoints(sources, &defaults, profile);
|
||||||
let endpoints = match endpoints {
|
let endpoints = match endpoints {
|
||||||
@@ -360,6 +472,283 @@ fn map_optional_ws_settings(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_optional_grpc_settings(
|
||||||
|
format_version: u32,
|
||||||
|
defaults: std::option::Option<EffectiveGrpcSessionSource>,
|
||||||
|
sources: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>> {
|
||||||
|
return match format_version {
|
||||||
|
1 | 2 => {
|
||||||
|
if defaults.is_some() || sources.is_some() {
|
||||||
|
std::result::Result::Err(effective_error(profile, "Transport V1/V2 must not define Yellowstone gRPC settings"))
|
||||||
|
} else {
|
||||||
|
std::result::Result::Ok(std::option::Option::None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
3 => {
|
||||||
|
let defaults = match defaults {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V3 requires grpc_defaults")),
|
||||||
|
};
|
||||||
|
let sources = match sources {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||||
|
};
|
||||||
|
let endpoints = map_grpc_endpoints(sources, &defaults, effective, profile);
|
||||||
|
let endpoints = match endpoints {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let settings = ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings::new(endpoints);
|
||||||
|
if let std::result::Result::Err(error) = settings.validate() {
|
||||||
|
return std::result::Result::Err(transport_contract_error(
|
||||||
|
profile,
|
||||||
|
"effective Yellowstone gRPC Transport settings fail the Transport runtime contract",
|
||||||
|
&error,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(settings))
|
||||||
|
},
|
||||||
|
_ => std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_grpc_endpoints(
|
||||||
|
sources: std::vec::Vec<EffectiveGrpcEndpointSource>,
|
||||||
|
defaults: &EffectiveGrpcSessionSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>> {
|
||||||
|
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>::with_capacity(sources.len());
|
||||||
|
for (endpoint_index, source) in sources.into_iter().enumerate() {
|
||||||
|
let endpoint_name = source.name.clone();
|
||||||
|
if source.protocol != "solana_yellowstone" {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "effective gRPC protocol is unsupported")
|
||||||
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
.with_context("grpc_protocol", source.protocol),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(source.url);
|
||||||
|
let url = match url {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective Yellowstone gRPC endpoint URL is invalid", &error)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let session = map_grpc_session_settings(defaults, source.session.as_ref(), profile, endpoint_name.as_str());
|
||||||
|
let session = match session {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let metadata = map_grpc_metadata(source.metadata, source.secret_metadata, effective, profile, endpoint_name.as_str(), endpoint_index);
|
||||||
|
let metadata = match metadata {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
source.name,
|
||||||
|
source.enabled,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new(source.provider),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(source.cluster),
|
||||||
|
url,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
let endpoint = endpoint.with_metadata(metadata);
|
||||||
|
let endpoint = match endpoint {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective Yellowstone gRPC metadata fail the Transport runtime contract", &error)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_grpc_session_settings(
|
||||||
|
defaults: &EffectiveGrpcSessionSource,
|
||||||
|
overrides: std::option::Option<&EffectiveGrpcSessionOverrideSource>,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
endpoint_name: &str,
|
||||||
|
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings> {
|
||||||
|
let mut connect_timeout_ms = defaults.connect_timeout_ms;
|
||||||
|
let mut unary_timeout_ms = defaults.unary_timeout_ms;
|
||||||
|
let mut close_timeout_ms = defaults.close_timeout_ms;
|
||||||
|
let mut reconnect_max_retries = defaults.reconnect.max_retries;
|
||||||
|
let mut reconnect_initial_backoff_ms = defaults.reconnect.initial_backoff_ms;
|
||||||
|
let mut reconnect_max_backoff_ms = defaults.reconnect.max_backoff_ms;
|
||||||
|
let mut request_channel_capacity = defaults.request_channel_capacity;
|
||||||
|
let mut update_channel_capacity = defaults.update_channel_capacity;
|
||||||
|
let mut max_inbound_message_size_bytes = defaults.max_inbound_message_size_bytes;
|
||||||
|
let mut max_outbound_message_size_bytes = defaults.max_outbound_message_size_bytes;
|
||||||
|
if let std::option::Option::Some(overrides) = overrides {
|
||||||
|
if let std::option::Option::Some(value) = overrides.connect_timeout_ms {
|
||||||
|
connect_timeout_ms = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.unary_timeout_ms {
|
||||||
|
unary_timeout_ms = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.close_timeout_ms {
|
||||||
|
close_timeout_ms = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(reconnect) = overrides.reconnect.as_ref() {
|
||||||
|
if let std::option::Option::Some(value) = reconnect.max_retries {
|
||||||
|
reconnect_max_retries = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = reconnect.initial_backoff_ms {
|
||||||
|
reconnect_initial_backoff_ms = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = reconnect.max_backoff_ms {
|
||||||
|
reconnect_max_backoff_ms = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.request_channel_capacity {
|
||||||
|
request_channel_capacity = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.update_channel_capacity {
|
||||||
|
update_channel_capacity = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.max_inbound_message_size_bytes {
|
||||||
|
max_inbound_message_size_bytes = value;
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(value) = overrides.max_outbound_message_size_bytes {
|
||||||
|
max_outbound_message_size_bytes = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reconnect = ksp_onchain_transport_lib::YellowstoneGrpcReconnectSettings::new(
|
||||||
|
reconnect_max_retries,
|
||||||
|
std::time::Duration::from_millis(reconnect_initial_backoff_ms),
|
||||||
|
std::time::Duration::from_millis(reconnect_max_backoff_ms),
|
||||||
|
);
|
||||||
|
let settings = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::new(
|
||||||
|
std::time::Duration::from_millis(connect_timeout_ms),
|
||||||
|
std::time::Duration::from_millis(unary_timeout_ms),
|
||||||
|
std::time::Duration::from_millis(close_timeout_ms),
|
||||||
|
reconnect,
|
||||||
|
request_channel_capacity,
|
||||||
|
update_channel_capacity,
|
||||||
|
max_inbound_message_size_bytes,
|
||||||
|
max_outbound_message_size_bytes,
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = settings.validate() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective Yellowstone gRPC session settings fail the Transport runtime contract", &error)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_grpc_metadata(
|
||||||
|
public_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
|
||||||
|
secret_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
endpoint_name: &str,
|
||||||
|
endpoint_index: usize,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>> {
|
||||||
|
let mut metadata = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>::with_capacity(public_sources.len() + secret_sources.len());
|
||||||
|
for (metadata_index, source) in public_sources.into_iter().enumerate() {
|
||||||
|
let pointer = format!("/grpc_endpoints/{endpoint_index}/metadata/{metadata_index}/value");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), false, profile, endpoint_name) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public(source.key, source.value);
|
||||||
|
let entry = match entry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective public Yellowstone gRPC metadata are invalid", &error)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
metadata.push(entry);
|
||||||
|
}
|
||||||
|
for (metadata_index, source) in secret_sources.into_iter().enumerate() {
|
||||||
|
let pointer = format!("/grpc_endpoints/{endpoint_index}/secret_metadata/{metadata_index}/value");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), true, profile, endpoint_name) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(source.key, source.value);
|
||||||
|
let entry = match entry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective secret Yellowstone gRPC metadata are invalid", &error)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
metadata.push(entry);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_metadata_provenance(
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
pointer: &str,
|
||||||
|
secret_class: bool,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
endpoint_name: &str,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let provenance = match effective.provenance_at(pointer) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "Yellowstone gRPC metadata provenance is unavailable")
|
||||||
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut has_secret_environment = false;
|
||||||
|
for item in provenance {
|
||||||
|
let variable_name = match item.variable_name() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||||
|
let sensitivity = match sensitivity {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if sensitivity.is_secret() {
|
||||||
|
has_secret_environment = true;
|
||||||
|
if !secret_class {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "public Yellowstone gRPC metadata must not derive from a secret environment variable")
|
||||||
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if secret_class {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "secret Yellowstone gRPC metadata may reference only secret environment variables")
|
||||||
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if secret_class && !has_secret_environment {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "secret Yellowstone gRPC metadata require secret environment provenance")
|
||||||
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
fn map_endpoints(
|
fn map_endpoints(
|
||||||
sources: std::vec::Vec<EffectiveEndpointSource>,
|
sources: std::vec::Vec<EffectiveEndpointSource>,
|
||||||
profile: &crate::ResolvedConfigProfile,
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||||
// version: 22
|
// version: 23
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||||
//! Logging/Transport adapters and management contracts.
|
//! Logging/Transport adapters and management contracts.
|
||||||
@@ -254,7 +254,9 @@ fn transport_adapter_contract_is_available_from_crate_root() {
|
|||||||
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
|
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
|
||||||
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings;
|
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings;
|
||||||
let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings;
|
let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings;
|
||||||
|
let _grpc_settings = ksp_config_lib::ResolvedTransportConfig::grpc_settings;
|
||||||
let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings;
|
let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings;
|
||||||
|
let _into_all_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_all_transport_settings;
|
||||||
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
|
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
|
||||||
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
|
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
|
||||||
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
|
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
|
||||||
|
|||||||
242
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
Normal file
242
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"format_version": 3,
|
||||||
|
"retry": {
|
||||||
|
"max_retries": 2,
|
||||||
|
"initial_backoff_ms": 100,
|
||||||
|
"max_backoff_ms": 2000
|
||||||
|
},
|
||||||
|
"ws_defaults": {
|
||||||
|
"command_timeout_ms": 8000,
|
||||||
|
"close_timeout_ms": 4000,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 5,
|
||||||
|
"initial_backoff_ms": 250,
|
||||||
|
"max_backoff_ms": 5000
|
||||||
|
},
|
||||||
|
"resubscribe": "active_subscriptions",
|
||||||
|
"command_queue_capacity": 64,
|
||||||
|
"notification_queue_capacity": 96,
|
||||||
|
"max_active_subscriptions": 256,
|
||||||
|
"max_pending_requests": 48,
|
||||||
|
"max_message_size_bytes": 33554432,
|
||||||
|
"max_frame_size_bytes": 8388608,
|
||||||
|
"max_write_buffer_size_bytes": 524288
|
||||||
|
},
|
||||||
|
"grpc_defaults": {
|
||||||
|
"connect_timeout_ms": 6000,
|
||||||
|
"unary_timeout_ms": 7000,
|
||||||
|
"close_timeout_ms": 4000,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 4,
|
||||||
|
"initial_backoff_ms": 200,
|
||||||
|
"max_backoff_ms": 3000
|
||||||
|
},
|
||||||
|
"request_channel_capacity": 64,
|
||||||
|
"update_channel_capacity": 96,
|
||||||
|
"max_inbound_message_size_bytes": 33554432,
|
||||||
|
"max_outbound_message_size_bytes": 16777216
|
||||||
|
},
|
||||||
|
"default_profile": "grpc_secret_ok",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "grpc_secret_ok",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "https://fallback.invalid",
|
||||||
|
"connect_timeout_ms": 1000,
|
||||||
|
"request_timeout_ms": 3000,
|
||||||
|
"max_idle_connections_per_host": 2,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 1,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 5,
|
||||||
|
"max_concurrent_requests": 2,
|
||||||
|
"pause_after_rate_limit_ms": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "wss://fallback.invalid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_grpc",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://grpc.invalid",
|
||||||
|
"metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-ksp-label",
|
||||||
|
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-ksp-token",
|
||||||
|
"value": "Bearer ${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"session": {
|
||||||
|
"connect_timeout_ms": 4500,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 7
|
||||||
|
},
|
||||||
|
"update_channel_capacity": 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "grpc_public_from_secret_invalid",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "https://fallback.invalid",
|
||||||
|
"connect_timeout_ms": 1000,
|
||||||
|
"request_timeout_ms": 3000,
|
||||||
|
"max_idle_connections_per_host": 2,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 1,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 5,
|
||||||
|
"max_concurrent_requests": 2,
|
||||||
|
"pause_after_rate_limit_ms": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "wss://fallback.invalid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_grpc",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://grpc.invalid",
|
||||||
|
"metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-ksp-public",
|
||||||
|
"value": "${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"secret_metadata": [],
|
||||||
|
"session": {
|
||||||
|
"connect_timeout_ms": 4500,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 7
|
||||||
|
},
|
||||||
|
"update_channel_capacity": 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "grpc_secret_from_public_invalid",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "https://fallback.invalid",
|
||||||
|
"connect_timeout_ms": 1000,
|
||||||
|
"request_timeout_ms": 3000,
|
||||||
|
"max_idle_connections_per_host": 2,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 1,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 5,
|
||||||
|
"max_concurrent_requests": 2,
|
||||||
|
"pause_after_rate_limit_ms": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "wss://fallback.invalid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_grpc",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "https://grpc.invalid",
|
||||||
|
"metadata": [],
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-ksp-secret",
|
||||||
|
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"session": {
|
||||||
|
"connect_timeout_ms": 4500,
|
||||||
|
"reconnect": {
|
||||||
|
"max_retries": 7
|
||||||
|
},
|
||||||
|
"update_channel_capacity": 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
// version: 6
|
// version: 10
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||||
@@ -41,6 +41,7 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
|||||||
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
|
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
|
||||||
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
|
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
|
||||||
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
|
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
|
||||||
|
assert!(resolved.grpc_settings().is_none(), "V2 fixture must remain Yellowstone gRPC-free");
|
||||||
let ws = resolved.ws_settings();
|
let ws = resolved.ws_settings();
|
||||||
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
|
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
|
||||||
if let std::option::Option::Some(ws) = ws {
|
if let std::option::Option::Some(ws) = ws {
|
||||||
@@ -90,6 +91,7 @@ fn v1_transport_fixture_remains_backward_readable_and_http_only() {
|
|||||||
assert_eq!(resolved.settings().endpoints().len(), 1);
|
assert_eq!(resolved.settings().endpoints().len(), 1);
|
||||||
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid");
|
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid");
|
||||||
assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings");
|
assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings");
|
||||||
|
assert!(resolved.grpc_settings().is_none(), "V1 must not invent Yellowstone gRPC runtime settings");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +111,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
|||||||
assert_eq!(default.profile_id(), "devnet_public");
|
assert_eq!(default.profile_id(), "devnet_public");
|
||||||
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
|
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||||
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
||||||
|
assert!(default.grpc_settings().is_none(), "Devnet profile must not invent a Yellowstone gRPC endpoint");
|
||||||
let ws = default.ws_settings();
|
let ws = default.ws_settings();
|
||||||
assert!(ws.is_some(), "committed V2 Devnet profile should expose WebSocket settings");
|
assert!(ws.is_some(), "committed V3 Devnet profile should expose WebSocket settings");
|
||||||
if let std::option::Option::Some(ws) = ws {
|
if let std::option::Option::Some(ws) = ws {
|
||||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
|
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
|
||||||
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||||
@@ -121,8 +124,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
|||||||
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||||
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
|
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
|
||||||
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
|
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
|
||||||
|
assert!(mainnet.grpc_settings().is_none(), "generic Mainnet profile must stay distinct from the PublicNode gRPC profile");
|
||||||
let ws = mainnet.ws_settings();
|
let ws = mainnet.ws_settings();
|
||||||
assert!(ws.is_some(), "committed V2 Mainnet profile should expose WebSocket settings");
|
assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
|
||||||
if let std::option::Option::Some(ws) = ws {
|
if let std::option::Option::Some(ws) = ws {
|
||||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
|
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
|
||||||
}
|
}
|
||||||
@@ -130,7 +134,147 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_without_polling() {
|
fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_network_scoped_secret_x_tokens() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for (profile_id, endpoint_name, cluster, url, environment_name, canary) in [
|
||||||
|
(
|
||||||
|
"publicnode_mainnet",
|
||||||
|
"publicnode_solana_mainnet_yellowstone",
|
||||||
|
"mainnet-beta",
|
||||||
|
"https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN",
|
||||||
|
"PUBLICNODE-MAINNET-GRPC-X-TOKEN-CANARY",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"publicnode_testnet",
|
||||||
|
"publicnode_solana_testnet_yellowstone",
|
||||||
|
"testnet",
|
||||||
|
"https://solana-testnet-yellowstone-grpc.publicnode.com:443",
|
||||||
|
"KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN",
|
||||||
|
"PUBLICNODE-TESTNET-GRPC-X-TOKEN-CANARY",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert(environment_name.to_owned(), canary.to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::Some(profile_id), &environment);
|
||||||
|
assert!(resolved.is_ok(), "committed PublicNode V3 profile {profile_id} should map from its network-scoped token: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
assert_eq!(resolved.profile_id(), profile_id);
|
||||||
|
assert!(resolved.ws_settings().is_some());
|
||||||
|
let grpc = resolved.grpc_settings();
|
||||||
|
assert!(grpc.is_some(), "PublicNode profile {profile_id} should expose Yellowstone gRPC settings");
|
||||||
|
if let std::option::Option::Some(grpc) = grpc {
|
||||||
|
assert_eq!(grpc.endpoints().len(), 1);
|
||||||
|
let endpoint = &grpc.endpoints()[0];
|
||||||
|
assert_eq!(endpoint.name(), endpoint_name);
|
||||||
|
assert_eq!(endpoint.provider().as_str(), "publicnode");
|
||||||
|
assert_eq!(endpoint.cluster().as_str(), cluster);
|
||||||
|
assert_eq!(endpoint.url().as_str(), url);
|
||||||
|
assert!(endpoint.url().uses_tls());
|
||||||
|
assert_eq!(endpoint.metadata().len(), 1);
|
||||||
|
assert_eq!(endpoint.metadata()[0].key(), "x-token");
|
||||||
|
assert!(endpoint.metadata()[0].is_secret());
|
||||||
|
let endpoint_debug = format!("{endpoint:?}");
|
||||||
|
assert!(!endpoint_debug.contains(canary), "PublicNode {cluster} x-token must stay redacted from endpoint Debug");
|
||||||
|
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(10_000));
|
||||||
|
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(10_000));
|
||||||
|
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(5_000));
|
||||||
|
assert_eq!(endpoint.session().reconnect().max_retries(), 5);
|
||||||
|
assert!(grpc.validate().is_ok(), "Config-produced Yellowstone gRPC settings should satisfy Transport validation");
|
||||||
|
let debug = format!("{grpc:?}");
|
||||||
|
assert!(!debug.contains("publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs");
|
||||||
|
assert!(!debug.contains(canary), "Transport settings Debug must not expose the network-scoped PublicNode x-token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v3_grpc_metadata_separates_public_and_secret_provenance() {
|
||||||
|
let engine = v3_fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canary = "grpc-secret-canary";
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_GRPC_TOKEN".to_owned(), canary.to_owned());
|
||||||
|
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "operator-public".to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_ok(), "V3 gRPC metadata fixture should map: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let grpc = resolved.grpc_settings();
|
||||||
|
assert!(grpc.is_some());
|
||||||
|
if let std::option::Option::Some(grpc) = grpc {
|
||||||
|
let endpoint = &grpc.endpoints()[0];
|
||||||
|
assert_eq!(endpoint.metadata().len(), 2);
|
||||||
|
assert_eq!(endpoint.metadata()[0].key(), "x-ksp-label");
|
||||||
|
assert!(!endpoint.metadata()[0].is_secret());
|
||||||
|
assert_eq!(endpoint.metadata()[1].key(), "x-ksp-token");
|
||||||
|
assert!(endpoint.metadata()[1].is_secret());
|
||||||
|
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(4_500));
|
||||||
|
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(7_000));
|
||||||
|
assert_eq!(endpoint.session().reconnect().max_retries(), 7);
|
||||||
|
assert_eq!(endpoint.session().update_channel_capacity(), 32);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
resolved.effective().safe_value().pointer("/grpc_endpoints/0/metadata/0/value").and_then(serde_json::Value::as_str),
|
||||||
|
std::option::Option::Some("operator-public")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.effective().safe_value().pointer("/grpc_endpoints/0/secret_metadata/0/value").and_then(serde_json::Value::as_str),
|
||||||
|
std::option::Option::Some("Bearer ********")
|
||||||
|
);
|
||||||
|
let secret_provenance = resolved.effective().provenance_at("/grpc_endpoints/0/secret_metadata/0/value");
|
||||||
|
assert!(secret_provenance.is_some());
|
||||||
|
if let std::option::Option::Some(provenance) = secret_provenance {
|
||||||
|
assert_eq!(provenance.len(), 2);
|
||||||
|
assert_eq!(provenance[0], crate::ConfigValueProvenance::DocumentLiteral);
|
||||||
|
assert_eq!(provenance[1].variable_name(), std::option::Option::Some("KSP_SECRET_GRPC_TOKEN"));
|
||||||
|
}
|
||||||
|
let debug = format!("{resolved:?}");
|
||||||
|
assert!(!debug.contains(canary));
|
||||||
|
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v3_grpc_metadata_rejects_crossed_sensitivity_classes() {
|
||||||
|
let engine = v3_fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_GRPC_TOKEN".to_owned(), "secret-canary".to_owned());
|
||||||
|
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "public-canary".to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let public_from_secret = engine.load_resolved_transport_config(std::option::Option::Some("grpc_public_from_secret_invalid"), &environment);
|
||||||
|
let secret_from_public = engine.load_resolved_transport_config(std::option::Option::Some("grpc_secret_from_public_invalid"), &environment);
|
||||||
|
assert!(public_from_secret.is_err(), "public metadata must reject KSP_SECRET provenance");
|
||||||
|
assert!(secret_from_public.is_err(), "secret_metadata must reject non-secret environment provenance");
|
||||||
|
if let std::result::Result::Err(error) = public_from_secret {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||||
|
assert!(!format!("{error:?}").contains("secret-canary"));
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = secret_from_public {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||||
|
assert!(!format!("{error:?}").contains("public-canary"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_v3_websocket_endpoint_preserves_v2_compatibility_constructor_without_polling() {
|
||||||
let engine = committed_engine();
|
let engine = committed_engine();
|
||||||
let engine = match engine {
|
let engine = match engine {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
@@ -138,11 +282,11 @@ fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_with
|
|||||||
};
|
};
|
||||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||||
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment);
|
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment);
|
||||||
assert!(resolved.is_ok(), "committed V2 Transport profile should map: {resolved:?}");
|
assert!(resolved.is_ok(), "committed V3 Transport profile should map: {resolved:?}");
|
||||||
if let std::result::Result::Ok(resolved) = resolved {
|
if let std::result::Result::Ok(resolved) = resolved {
|
||||||
let (http, ws) = resolved.into_transport_settings();
|
let (http, ws) = resolved.into_transport_settings();
|
||||||
assert_eq!(http.endpoints().len(), 1);
|
assert_eq!(http.endpoints().len(), 1);
|
||||||
assert!(ws.is_some(), "committed V2 Transport profile should expose WebSocket settings");
|
assert!(ws.is_some(), "committed V3 Transport profile should expose WebSocket settings");
|
||||||
if let std::option::Option::Some(ws) = ws {
|
if let std::option::Option::Some(ws) = ws {
|
||||||
assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation");
|
assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation");
|
||||||
assert_eq!(ws.endpoints().len(), 1);
|
assert_eq!(ws.endpoints().len(), 1);
|
||||||
@@ -170,6 +314,7 @@ fn transport_profile_preserves_global_and_profile_origin() {
|
|||||||
if let std::result::Result::Ok(profile) = profile {
|
if let std::result::Result::Ok(profile) = profile {
|
||||||
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
|
assert_eq!(profile.origin("grpc_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||||
assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||||
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
@@ -421,6 +566,22 @@ fn v1_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
|||||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn v3_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
let workspace = workspace_root();
|
||||||
|
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures_v3");
|
||||||
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas"));
|
||||||
|
let bootstrap = match bootstrap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
let registry = match registry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
|
}
|
||||||
|
|
||||||
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
let workspace = workspace_root();
|
let workspace = workspace_root();
|
||||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
// version: 5
|
// version: 7
|
||||||
|
|
||||||
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
||||||
|
|
||||||
@@ -62,11 +62,17 @@ fn transport_manifest_preserves_ksp_dependency_firewall() {
|
|||||||
assert!(manifest.contains("ksp-core-lib"));
|
assert!(manifest.contains("ksp-core-lib"));
|
||||||
assert!(manifest.contains("ksp-logging-lib"));
|
assert!(manifest.contains("ksp-logging-lib"));
|
||||||
assert!(manifest.contains("futures-util = { workspace = true, features = [\"sink\", \"std\"] }"));
|
assert!(manifest.contains("futures-util = { workspace = true, features = [\"sink\", \"std\"] }"));
|
||||||
|
assert!(manifest.contains("http.workspace = true"));
|
||||||
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
|
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
|
||||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }"));
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }"));
|
||||||
assert!(manifest.contains("tokio-tungstenite = { workspace = true, features = [\"connect\", \"rustls-tls-webpki-roots\"] }"));
|
assert!(manifest.contains("tokio-tungstenite = { workspace = true, features = [\"connect\", \"rustls-tls-webpki-roots\"] }"));
|
||||||
|
assert!(manifest.contains("tonic = { workspace = true, features = [\"channel\", \"tls-aws-lc\", \"tls-webpki-roots\"] }"));
|
||||||
|
assert!(manifest.contains("tonic-prost.workspace = true"));
|
||||||
|
assert!(manifest.contains("yellowstone-grpc-proto.workspace = true"));
|
||||||
assert!(manifest.contains("[dev-dependencies]"));
|
assert!(manifest.contains("[dev-dependencies]"));
|
||||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }"));
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }"));
|
||||||
|
assert!(manifest.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"));
|
||||||
|
assert!(manifest.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -86,7 +92,20 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
|
|||||||
let dependency_names = manifest_dependency_names(dependencies);
|
let dependency_names = manifest_dependency_names(dependencies);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dependency_names,
|
dependency_names,
|
||||||
std::vec!["futures-util", "ksp-core-lib", "ksp-logging-lib", "reqwest", "serde", "serde_json", "tokio", "tokio-tungstenite"]
|
std::vec![
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"ksp-core-lib",
|
||||||
|
"ksp-logging-lib",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
|
"tonic",
|
||||||
|
"tonic-prost",
|
||||||
|
"yellowstone-grpc-proto",
|
||||||
|
]
|
||||||
);
|
);
|
||||||
let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1);
|
let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1);
|
||||||
assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist");
|
assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist");
|
||||||
@@ -98,7 +117,7 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
|
|||||||
std::option::Option::Some(value) => value,
|
std::option::Option::Some(value) => value,
|
||||||
std::option::Option::None => return,
|
std::option::Option::None => return,
|
||||||
};
|
};
|
||||||
assert_eq!(manifest_dependency_names(dev_dependencies), std::vec!["tokio"]);
|
assert_eq!(manifest_dependency_names(dev_dependencies), std::vec!["tokio", "tonic", "yellowstone-grpc-proto"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> {
|
fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-logging-lib/Cargo.toml
|
# file: crates/ksp-logging-lib/Cargo.toml
|
||||||
# version: 5
|
# version: 6
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-logging-lib"
|
name = "ksp-logging-lib"
|
||||||
@@ -10,8 +10,8 @@ repository.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
tracing = { workspace = true, features = ["std"] }
|
tracing = { workspace = true, features = ["std"] }
|
||||||
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
|
|
||||||
tracing-appender.workspace = true
|
tracing-appender.workspace = true
|
||||||
|
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
# version: 6
|
# version: 9
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-onchain-transport-lib"
|
name = "ksp-onchain-transport-lib"
|
||||||
@@ -8,17 +8,23 @@ edition.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
futures-util = { workspace = true, features = ["sink", "std"] }
|
||||||
|
http.workspace = true
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
futures-util = { workspace = true, features = ["sink", "std"] }
|
|
||||||
reqwest = { workspace = true, features = ["rustls"] }
|
reqwest = { workspace = true, features = ["rustls"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
|
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
|
||||||
tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] }
|
tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] }
|
||||||
|
tonic = { workspace = true, features = ["channel", "tls-aws-lc", "tls-webpki-roots"] }
|
||||||
|
tonic-prost.workspace = true
|
||||||
|
yellowstone-grpc-proto.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
||||||
|
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||||
|
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 22 -->
|
||||||
|
|
||||||
# `ksp-onchain-transport-lib`
|
# `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Elle fournit le transport HTTP JSON-RPC complet et le moteur WebSocket Solana standard ; les extensions provider-specific et gRPC sont ajoutées séparément lorsqu’une release les cible.
|
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Elle fournit le transport HTTP JSON-RPC complet, le moteur WebSocket Solana standard et la foundation Yellowstone gRPC standard/provider-neutral. Les extensions provider-specific restent ajoutées séparément lorsqu’une release les cible.
|
||||||
|
|
||||||
## Responsabilités
|
## Responsabilités
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ La crate possède :
|
|||||||
- l'exécution générique des méthodes standard supportées ;
|
- l'exécution générique des méthodes standard supportées ;
|
||||||
- les wrappers typés HTTP et WebSocket explicitement livrés par KSP ;
|
- les wrappers typés HTTP et WebSocket explicitement livrés par KSP ;
|
||||||
- les sessions physiques WebSocket, subscriptions logiques, reconnect/resubscribe et backpressure bornés ;
|
- les sessions physiques WebSocket, subscriptions logiques, reconnect/resubscribe et backpressure bornés ;
|
||||||
|
- les settings, channels, unary et sessions `Subscribe` Yellowstone gRPC standard ;
|
||||||
|
- le reconnect/replay Yellowstone prudent avec observabilité de gaps/duplicates sans promesse lossless ;
|
||||||
- les snapshots runtime sûrs ;
|
- les snapshots runtime sûrs ;
|
||||||
- l'observabilité Transport via `ksp-logging-lib`.
|
- l'observabilité Transport via `ksp-logging-lib`.
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ ksp-config-lib
|
|||||||
-> ksp-logging-lib
|
-> ksp-logging-lib
|
||||||
-> reqwest / tokio / serde
|
-> reqwest / tokio / serde
|
||||||
-> tokio-tungstenite / futures-util
|
-> tokio-tungstenite / futures-util
|
||||||
|
-> tonic / tonic-prost / yellowstone-grpc-proto
|
||||||
```
|
```
|
||||||
|
|
||||||
La direction inverse est interdite :
|
La direction inverse est interdite :
|
||||||
@@ -217,6 +220,32 @@ Pour Helius, l’actor envoie automatiquement un control frame WebSocket `Ping`
|
|||||||
|
|
||||||
LaserStream **gRPC** reste un backend distinct, hors de cette façade, de `WsProtocolKind` et de la Config WebSocket `helius_laserstream`.
|
LaserStream **gRPC** reste un backend distinct, hors de cette façade, de `WsProtocolKind` et de la Config WebSocket `helius_laserstream`.
|
||||||
|
|
||||||
|
|
||||||
|
## Yellowstone gRPC standard
|
||||||
|
|
||||||
|
La foundation `0.2.9` ajoute un troisième backend réseau distinct de HTTP et WebSocket. Le moteur est KSP-owned : `yellowstone-grpc-proto` fournit le wire publié, tandis que Tonic reste encapsulé derrière les types crate-root KSP. Aucun client Tonic brut ni type protobuf upstream n’est réexporté.
|
||||||
|
|
||||||
|
La surface publique principale comprend :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcEndpointUrl / YellowstoneGrpcEndpointSettings
|
||||||
|
YellowstoneGrpcSessionSettings / YellowstoneGrpcReconnectSettings
|
||||||
|
YellowstoneGrpcTransportSettings
|
||||||
|
YellowstoneGrpcChannel
|
||||||
|
SolanaYellowstoneGrpcUnaryClient
|
||||||
|
YellowstoneSubscribeRequest
|
||||||
|
SolanaYellowstoneGrpcSubscribeSession
|
||||||
|
YellowstoneGrpcSubscribeSnapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Les sept unary standards retenus sont `SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid` et `GetVersion`. `Subscribe` couvre accounts, slots, transactions, transaction status, blocks, block metadata et entries, avec `commitment`, `accounts_data_slice`, `ping` et `from_slot`. `SubscribeDeshred` reste hors scope de la foundation standard.
|
||||||
|
|
||||||
|
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de l’observabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
|
||||||
|
|
||||||
|
Config Transport V3 peut mapper des `grpc_endpoints` vers ces settings sans inverser la dépendance. `protocol = solana_yellowstone` décrit le wire standard, tandis que `provider` reste un descripteur distinct. Les profils committés `publicnode_mainnet` et `publicnode_testnet` utilisent le standard sans façade PublicNode spécifique. Leur personal token est injecté comme metadata secrète `x-token` depuis Config ; Transport ne lit jamais les variables KSP.
|
||||||
|
|
||||||
|
Le smoke live opt-in ouvre un `Subscribe` slots authentifié sur PublicNode Mainnet et Testnet, attend un update `Slot` non nul puis ferme de manière bornée. Les deux endpoints sont versionnés. Le harness reçoit ses personal tokens sur stdin ; aucun secret n’est committé ni placé dans l’URL. Le même token a été validé par l’opérateur sur les deux réseaux, même si Config conserve deux variables distinctes pour laisser cette policy flexible.
|
||||||
|
|
||||||
## Résilience
|
## Résilience
|
||||||
|
|
||||||
L'admission est calculée par couple endpoint/rôle. Le pool applique :
|
L'admission est calculée par couple endpoint/rôle. Le pool applique :
|
||||||
@@ -253,7 +282,7 @@ La configuration Logging de référence conserve un fichier dédié Transport à
|
|||||||
|
|
||||||
Les tests par défaut sont déterministes et n'exigent pas Internet : fixtures JSON et serveur HTTP local couvrent requêtes, réponses, retry, 429, timeout, redaction et routing.
|
Les tests par défaut sont déterministes et n'exigent pas Internet : fixtures JSON et serveur HTTP local couvrent requêtes, réponses, retry, 429, timeout, redaction et routing.
|
||||||
|
|
||||||
Trois smokes Devnet opt-in sont séparés par responsabilité :
|
Quatre smokes réseau opt-in sont séparés par responsabilité :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Transport HTTP pur : settings programmatiques -> HttpTransportPool
|
Transport HTTP pur : settings programmatiques -> HttpTransportPool
|
||||||
@@ -270,13 +299,17 @@ Transport WebSocket pur : settings programmatiques -> WsSession
|
|||||||
|
|
||||||
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
||||||
-> getHealth/getGenesisHash/getVersion/getBalance
|
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||||
|
|
||||||
|
Transport Yellowstone gRPC : settings programmatiques + x-token secret
|
||||||
|
-> PublicNode Mainnet + Testnet
|
||||||
|
-> TLS -> Subscribe slots -> Slot non nul -> close borné
|
||||||
```
|
```
|
||||||
|
|
||||||
Le smoke HTTP Transport utilise pour sa branche Token la forme Devnet documentée `getTokenAccountsByOwner(owner, { programId }, { commitment: finalized, encoding: jsonParsed })`. L'owner est une Pubkey ordinaire de l'exemple officiel ; aucune présence de token account n'est exigée, donc une liste vide reste valide.
|
Le smoke HTTP Transport utilise pour sa branche Token la forme Devnet documentée `getTokenAccountsByOwner(owner, { programId }, { commitment: finalized, encoding: jsonParsed })`. L'owner est une Pubkey ordinaire de l'exemple officiel ; aucune présence de token account n'est exigée, donc une liste vide reste valide.
|
||||||
|
|
||||||
Le smoke WebSocket Transport cible uniquement la famille stable `slotSubscribe` sur l'endpoint public Devnet `wss://api.devnet.solana.com`. Il borne connexion, attente de notification, unsubscribe et fermeture ; il ne transforme aucune famille unstable en gate live.
|
Le smoke WebSocket Transport cible uniquement la famille stable `slotSubscribe` sur l'endpoint public Devnet `wss://api.devnet.solana.com`. Il borne connexion, attente de notification, unsubscribe et fermeture ; il ne transforme aucune famille unstable en gate live.
|
||||||
|
|
||||||
Les trois tests sont `ignored` par défaut. Les deux smokes Transport appartiennent durablement à cette crate ; le smoke cross-crates hébergé dans Config reste transitoire jusqu'à l'existence d'une surface KSP d'intégration/orchestration appropriée. Un rate-limit, refus externe ou incident Devnet n'est pas assimilé automatiquement à une régression locale.
|
Les quatre tests sont `ignored` par défaut ; le test Yellowstone contient deux cas live Mainnet/Testnet. Les trois familles de smokes Transport appartiennent durablement à cette crate ; le smoke cross-crates hébergé dans Config reste transitoire jusqu'à l'existence d'une surface KSP d'intégration/orchestration appropriée. Un rate-limit, refus externe ou incident Devnet n'est pas assimilé automatiquement à une régression locale.
|
||||||
|
|
||||||
Aucun smoke Helius live supplémentaire n’est committé en `0.2.8-pre.010`. Un tel test devrait à la fois obtenir `KSP_SECRET_HELIUS_API_KEY` via Config et exercer Transport ; l’ajouter dans Transport violerait l’ownership environnement/secret, tandis que l’ajouter dans Config étendrait l’exception cross-crates que le projet veut au contraire résorber. La première surface KSP d’intégration/orchestration dédiée devra héberger ce smoke. Le scénario live recommandé est alors `helius_devnet -> HeliusLaserStreamWsSession -> slotSubscribe -> notification -> unsubscribe -> close`; `transactionSubscribe` reste un smoke optionnel dépendant des droits provider et ne devient pas un gate stable de release.
|
Aucun smoke Helius live supplémentaire n’est committé en `0.2.8-pre.010`. Un tel test devrait à la fois obtenir `KSP_SECRET_HELIUS_API_KEY` via Config et exercer Transport ; l’ajouter dans Transport violerait l’ownership environnement/secret, tandis que l’ajouter dans Config étendrait l’exception cross-crates que le projet veut au contraire résorber. La première surface KSP d’intégration/orchestration dédiée devra héberger ce smoke. Le scénario live recommandé est alors `helius_devnet -> HeliusLaserStreamWsSession -> slotSubscribe -> notification -> unsubscribe -> close`; `transactionSubscribe` reste un smoke optionnel dépendant des droits provider et ne devient pas un gate stable de release.
|
||||||
|
|
||||||
@@ -290,4 +323,6 @@ Aucun smoke Helius live supplémentaire n’est committé en `0.2.8-pre.010`. Un
|
|||||||
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée Transactions ;
|
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée Transactions ;
|
||||||
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
|
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
|
||||||
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
|
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
|
||||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP + WebSocket V2, avec lecture backward V1 HTTP-only.
|
- [`../../docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md`](../../docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md) — plan Yellowstone gRPC standard/provider-neutral et PublicNode ;
|
||||||
|
- [`../../docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md`](../../docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md) — matrice de compliance Yellowstone ;
|
||||||
|
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard Transport V3 HTTP + WebSocket + gRPC, avec lecture backward V1/V2.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 22 -->
|
||||||
|
|
||||||
# Utilisation de `ksp-onchain-transport-lib`
|
# Utilisation de `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
@@ -257,7 +257,111 @@ Les limites de taille et de capacité sont des policies KSP configurables par `W
|
|||||||
|
|
||||||
Le snapshot expose seulement l'identité locale, les metadata logiques de l'endpoint, l'état, les compteurs sûrs et les projections locales de subscriptions. L'URL et les remote subscription IDs ne sont jamais projetés. La disparition de tous les handles de session déclenche le cleanup actor best-effort ; `close().await` reste la voie normale de shutdown.
|
Le snapshot expose seulement l'identité locale, les metadata logiques de l'endpoint, l'état, les compteurs sûrs et les projections locales de subscriptions. L'URL et les remote subscription IDs ne sont jamais projetés. La disparition de tous les handles de session déclenche le cleanup actor best-effort ; `close().await` reste la voie normale de shutdown.
|
||||||
|
|
||||||
## 4. Appels typés
|
## 4. Yellowstone gRPC standard
|
||||||
|
|
||||||
|
### Construction programmatique et unary
|
||||||
|
|
||||||
|
Transport peut ouvrir directement un endpoint Yellowstone sans Config :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let grpc_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(
|
||||||
|
"https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
|
) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let x_token_metadata = match ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(
|
||||||
|
"x-token",
|
||||||
|
x_token,
|
||||||
|
) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let grpc_endpoint = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"publicnode_mainnet_yellowstone",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("mainnet-beta"),
|
||||||
|
grpc_url,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
)
|
||||||
|
.with_metadata(vec![x_token_metadata])
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let grpc_channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&grpc_endpoint).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let grpc = grpc_channel.standard_unary_client();
|
||||||
|
let version = grpc.get_version().await;
|
||||||
|
let slot = grpc
|
||||||
|
.get_slot(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed))
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
L’URL reste sensible : `Debug`, erreurs KSP et snapshots n’en exposent pas la valeur. Les metadata publiques/secrètes se construisent avec `YellowstoneGrpcMetadataEntry`; Transport ne lit jamais l’environnement. PublicNode requiert actuellement une metadata secrète `x-token` pour les endpoints Yellowstone validés ; ici `x_token` représente une valeur déjà obtenue par un caller sécurisé. En usage normal, Config construit cette metadata depuis les variables `KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN` ou `KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN`. Les appels unary montrés ci-dessus illustrent la surface KSP standard ; le smoke PublicNode de release ne prétend pas valider leur entitlement provider et gate uniquement `Subscribe` slots.
|
||||||
|
|
||||||
|
### Config Transport V3
|
||||||
|
|
||||||
|
Avec `ksp-config-lib`, un profil V3 peut exposer les trois transports sans casser l’accesseur historique HTTP + WS :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resolved = match engine.load_resolved_transport_config(Some("publicnode_mainnet"), &environment) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let grpc_settings = match resolved.grpc_settings() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(ksp_core_lib::Error::new(
|
||||||
|
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
|
||||||
|
"selected profile has no Yellowstone gRPC endpoint",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let endpoint = match grpc_settings.endpoints().iter().find(|candidate| candidate.enabled()) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(ksp_core_lib::Error::new(
|
||||||
|
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
|
||||||
|
"selected profile has no enabled Yellowstone gRPC endpoint",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
`protocol = solana_yellowstone` est validé par Config et reste distinct du descripteur `provider`. Une valeur provider n’autorise pas Transport à introduire une API provider-specific sans divergence réelle.
|
||||||
|
|
||||||
|
### Subscribe bidirectionnel
|
||||||
|
|
||||||
|
Une session standard part d’une requête typed complète :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
if let Err(error) = request.insert_slot_filter(
|
||||||
|
name,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new(),
|
||||||
|
) {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
request.set_commitment(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||||
|
let mut stream = match grpc_channel.open_standard_subscribe(request).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let update = stream.next_update().await;
|
||||||
|
let snapshot = stream.snapshot();
|
||||||
|
let closed = stream.close().await;
|
||||||
|
```
|
||||||
|
|
||||||
|
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, gaps, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
|
||||||
|
|
||||||
|
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
|
||||||
|
|
||||||
|
## 5. Appels typés
|
||||||
|
|
||||||
Les wrappers typés se trouvent directement sur `HttpTransportPool`.
|
Les wrappers typés se trouvent directement sur `HttpTransportPool`.
|
||||||
|
|
||||||
@@ -317,7 +421,7 @@ let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).awa
|
|||||||
|
|
||||||
`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. 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.
|
||||||
|
|
||||||
## 5. Exécution JSON-RPC standard générique
|
## 6. Exécution JSON-RPC standard générique
|
||||||
|
|
||||||
Une méthode courante auditée peut être appelée via son descriptor :
|
Une méthode courante auditée peut être appelée via son descriptor :
|
||||||
|
|
||||||
@@ -331,7 +435,7 @@ Cette API retourne un `serde_json::Value`. Elle reste utile pour les extensions
|
|||||||
|
|
||||||
Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif.
|
Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif.
|
||||||
|
|
||||||
## 6. Sélection et admission sans exécuter la requête
|
## 7. Sélection et admission sans exécuter la requête
|
||||||
|
|
||||||
Pour inspecter le routing :
|
Pour inspecter le routing :
|
||||||
|
|
||||||
@@ -346,13 +450,13 @@ Dans le même bloc, `acquire_for_method()` réserve réellement la capacité RPS
|
|||||||
|
|
||||||
`HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau.
|
`HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau.
|
||||||
|
|
||||||
## 7. Snapshots runtime
|
## 8. Snapshots runtime
|
||||||
|
|
||||||
`HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime.
|
`HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime.
|
||||||
|
|
||||||
Les URLs d'endpoint n'y apparaissent jamais.
|
Les URLs d'endpoint n'y apparaissent jamais.
|
||||||
|
|
||||||
## 8. Retry et write submissions
|
## 9. Retry et write submissions
|
||||||
|
|
||||||
La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`.
|
La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`.
|
||||||
|
|
||||||
@@ -360,7 +464,7 @@ Les reads/simulations classés `RetrySafe` peuvent être réessayés dans le bud
|
|||||||
|
|
||||||
Pour une opération `WriteSubmission / NeverAfterDispatch`, un timeout ou autre résultat ambigu après dispatch arrête la resoumission automatique. Le consumer métier ne doit pas contourner cette protection avec une boucle de retry externe aveugle.
|
Pour une opération `WriteSubmission / NeverAfterDispatch`, un timeout ou autre résultat ambigu après dispatch arrête la resoumission automatique. Le consumer métier ne doit pas contourner cette protection avec une boucle de retry externe aveugle.
|
||||||
|
|
||||||
## 9. Logging
|
## 10. Logging
|
||||||
|
|
||||||
Les événements Transport utilisent le target :
|
Les événements Transport utilisent le target :
|
||||||
|
|
||||||
@@ -372,7 +476,7 @@ Ne jamais journaliser l'URL complète, un token provider, un body massif, une tr
|
|||||||
|
|
||||||
La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement.
|
La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement.
|
||||||
|
|
||||||
## 10. Smokes Devnet opt-in
|
## 11. Smokes réseau opt-in
|
||||||
|
|
||||||
Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics :
|
Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics :
|
||||||
|
|
||||||
@@ -390,6 +494,31 @@ cargo test -p ksp-onchain-transport-lib --test websocket_devnet_smoke -- --ignor
|
|||||||
|
|
||||||
Il n'utilise ni `blockSubscribe`, ni `slotsUpdatesSubscribe`, ni `voteSubscribe` : ces familles restent unstable et leur disponibilité dépend des capabilities du validator. Le smoke live n'est donc pas un gate de disponibilité de ces extensions.
|
Il n'utilise ni `blockSubscribe`, ni `slotsUpdatesSubscribe`, ni `voteSubscribe` : ces familles restent unstable et leur disponibilité dépend des capabilities du validator. Le smoke live n'est donc pas un gate de disponibilité de ces extensions.
|
||||||
|
|
||||||
|
Le smoke **Transport Yellowstone gRPC PublicNode** reste indépendant de Config mais nécessite un personal token opérateur. Il teste Mainnet et Testnet en ouvrant `Subscribe`, en demandant les updates `slots`, en attendant un `YellowstoneSubscribeUpdate::Slot` non nul puis en fermant la session de manière bornée.
|
||||||
|
|
||||||
|
Pour éviter de placer les secrets dans les arguments ou l'URL, le harness lit deux lignes sur stdin : Mainnet puis Testnet. Elles peuvent contenir la même valeur ; l'opérateur a validé un même personal token sur les deux réseaux.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
|
||||||
|
echo
|
||||||
|
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
|
||||||
|
echo
|
||||||
|
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
|
||||||
|
| cargo test -p ksp-onchain-transport-lib \
|
||||||
|
--test yellowstone_publicnode_smoke \
|
||||||
|
-- --ignored --nocapture
|
||||||
|
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Endpoints validés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
Testnet https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
Les profils Config conservent deux variables secrètes distinctes afin d'autoriser des credentials différents si nécessaire ; cette séparation ne signifie pas que PublicNode impose actuellement un token différent par réseau. Un timeout KSP de half-close après réception du slot est accepté par le smoke comme fermeture bornée du provider ; aucune absence de slot ni autre erreur n'est masquée.
|
||||||
|
|
||||||
Le smoke de **composition Config -> Transport** reste également disponible :
|
Le smoke de **composition Config -> Transport** reste également disponible :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -400,7 +529,7 @@ Il valide le profil committé `devnet_public` et les quatre canaris foundation.
|
|||||||
|
|
||||||
### Smoke Helius live
|
### Smoke Helius live
|
||||||
|
|
||||||
Aucun nouveau test Helius live n’est committé en `0.2.8-pre.010`. La raison est architecturale : Transport ne peut pas lire `KSP_SECRET_HELIUS_API_KEY` ni dépendre de Config, et Config ne doit pas devenir la destination générale des futurs smokes `Config + autre crate`. Créer un quatrième smoke dans l’une de ces deux crates contournerait donc une frontière déjà documentée.
|
Aucun nouveau test Helius live n’est committé en `0.2.8-pre.010`. La raison est architecturale : Transport ne peut pas lire `KSP_SECRET_HELIUS_API_KEY` ni dépendre de Config, et Config ne doit pas devenir la destination générale des futurs smokes `Config + autre crate`. Créer un smoke Helius supplémentaire dans l’une de ces deux crates contournerait donc une frontière déjà documentée.
|
||||||
|
|
||||||
Lorsque la surface KSP d’intégration/orchestration dédiée existera, le smoke live minimal recommandé sera :
|
Lorsque la surface KSP d’intégration/orchestration dédiée existera, le smoke live minimal recommandé sera :
|
||||||
|
|
||||||
@@ -416,7 +545,7 @@ Config helius_devnet
|
|||||||
|
|
||||||
Ce scénario utilise une méthode standard stable sur l’endpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre d’une entitlement particulière de `transactionSubscribe`. Un smoke `transactionSubscribe` pourra être ajouté séparément comme opt-in provider-specific si l’environnement opérateur possède les droits nécessaires ; il ne doit pas devenir un gate réseau obligatoire de la release.
|
Ce scénario utilise une méthode standard stable sur l’endpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre d’une entitlement particulière de `transactionSubscribe`. Un smoke `transactionSubscribe` pourra être ajouté séparément comme opt-in provider-specific si l’environnement opérateur possède les droits nécessaires ; il ne doit pas devenir un gate réseau obligatoire de la release.
|
||||||
|
|
||||||
Les endpoints publics/provider sont des dépendances externes. Un rate-limit, refus d’auth, entitlement absente ou incident réseau n’est pas assimilé automatiquement à une régression locale ; les fixtures HTTP/WebSocket locales et les gates déterministes restent autoritaires.
|
Les endpoints publics/provider sont des dépendances externes. Un rate-limit, refus d’auth, entitlement absente ou incident réseau n’est pas assimilé automatiquement à une régression locale ; les fixtures HTTP/WebSocket/gRPC locales et les gates déterministes restent autoritaires.
|
||||||
|
|
||||||
Pour auditer les dépendances, inspecter également le graphe effectif après résolution Cargo :
|
Pour auditer les dépendances, inspecter également le graphe effectif après résolution Cargo :
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||||
// version: 4
|
// version: 7
|
||||||
|
|
||||||
/// Error code used when no logical endpoint can satisfy a request.
|
/// Error code used when no logical endpoint can satisfy a request.
|
||||||
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
|
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
|
||||||
|
/// Error code used when a bounded Yellowstone gRPC request/update queue is exhausted.
|
||||||
|
pub const ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_backpressure_overflow");
|
||||||
|
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
|
||||||
|
pub const ERROR_CODE_GRPC_CHANNEL_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_channel_failed");
|
||||||
|
/// Error code used when a Yellowstone gRPC subscribe session is no longer available to the caller.
|
||||||
|
pub const ERROR_CODE_GRPC_SESSION_CLOSED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_session_closed");
|
||||||
|
/// Error code used when a Yellowstone gRPC endpoint returns a remote gRPC status.
|
||||||
|
pub const ERROR_CODE_GRPC_STATUS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_status");
|
||||||
/// Error code used when an HTTP connection cannot be established.
|
/// Error code used when an HTTP connection cannot be established.
|
||||||
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
|
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
|
||||||
/// Error code used when an HTTP request fails after a connection exists.
|
/// Error code used when an HTTP request fails after a connection exists.
|
||||||
|
|||||||
191
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal file
191
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||||
|
// version: 4
|
||||||
|
|
||||||
|
/// Prepared or connected Yellowstone gRPC channel owned by KSP Transport.
|
||||||
|
///
|
||||||
|
/// The underlying Tonic channel, endpoint URL and request metadata remain private. Callers use the KSP-owned typed Yellowstone surfaces layered on this
|
||||||
|
/// physical channel instead of receiving a raw Tonic escape hatch.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct YellowstoneGrpcChannel {
|
||||||
|
endpoint_name: std::string::String,
|
||||||
|
provider: crate::YellowstoneGrpcProviderName,
|
||||||
|
cluster: crate::YellowstoneGrpcClusterName,
|
||||||
|
channel: tonic::transport::Channel,
|
||||||
|
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
session: crate::YellowstoneGrpcSessionSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcChannel {
|
||||||
|
/// Prepares one lazy HTTP/2 channel without establishing a network connection.
|
||||||
|
///
|
||||||
|
/// HTTPS endpoints receive the KSP TLS configuration immediately, so invalid local TLS setup is rejected before a typed client is created. Tonic lazy
|
||||||
|
/// channels require an active Tokio runtime even though no socket is opened yet.
|
||||||
|
pub fn prepare(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
|
if let std::result::Result::Err(error) = settings.validate() {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if !settings.enabled() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot prepare a channel")
|
||||||
|
.with_context("endpoint_name", settings.name()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tokio::runtime::Handle::try_current().is_err() {
|
||||||
|
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel preparation requires an active Tokio runtime"));
|
||||||
|
}
|
||||||
|
let endpoint = match build_tonic_endpoint(settings) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let channel = endpoint.connect_lazy();
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_name = settings.name(),
|
||||||
|
provider = settings.provider().as_str(),
|
||||||
|
cluster = settings.cluster().as_str(),
|
||||||
|
tls = settings.url().uses_tls(),
|
||||||
|
metadata_count = settings.metadata().len(),
|
||||||
|
"prepared lazy Yellowstone gRPC channel"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(Self::from_parts(settings, channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establishes one Yellowstone gRPC HTTP/2 channel with bounded connect timeout and configured TLS roots.
|
||||||
|
pub async fn connect(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
|
if let std::result::Result::Err(error) = settings.validate() {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if !settings.enabled() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot connect")
|
||||||
|
.with_context("endpoint_name", settings.name()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tokio::runtime::Handle::try_current().is_err() {
|
||||||
|
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC connection requires an active Tokio runtime"));
|
||||||
|
}
|
||||||
|
let endpoint = match build_tonic_endpoint(settings) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let channel = match endpoint.connect().await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_name = settings.name(),
|
||||||
|
provider = settings.provider().as_str(),
|
||||||
|
cluster = settings.cluster().as_str(),
|
||||||
|
tls = settings.url().uses_tls(),
|
||||||
|
"Yellowstone gRPC channel connection failed"
|
||||||
|
);
|
||||||
|
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel connection failed"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_name = settings.name(),
|
||||||
|
provider = settings.provider().as_str(),
|
||||||
|
cluster = settings.cluster().as_str(),
|
||||||
|
tls = settings.url().uses_tls(),
|
||||||
|
"connected Yellowstone gRPC channel"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(Self::from_parts(settings, channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe logical endpoint name.
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoint_name(&self) -> &str {
|
||||||
|
return self.endpoint_name.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open provider descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
|
||||||
|
return &self.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open cluster descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
|
||||||
|
return &self.cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the standard Solana Yellowstone unary facade over this physical channel.
|
||||||
|
#[must_use]
|
||||||
|
pub fn standard_unary_client(&self) -> crate::SolanaYellowstoneGrpcUnaryClient {
|
||||||
|
return crate::SolanaYellowstoneGrpcUnaryClient::new(self.channel.clone(), self.metadata.clone(), self.session.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens one standard Solana Yellowstone bidirectional `Subscribe` session over this physical channel.
|
||||||
|
pub async fn open_standard_subscribe(
|
||||||
|
&self,
|
||||||
|
initial_request: crate::YellowstoneSubscribeRequest,
|
||||||
|
) -> ksp_core_lib::Result<crate::SolanaYellowstoneGrpcSubscribeSession> {
|
||||||
|
return crate::open_yellowstone_subscribe_session(
|
||||||
|
self.channel.clone(),
|
||||||
|
self.metadata.clone(),
|
||||||
|
self.session.clone(),
|
||||||
|
self.endpoint_name.clone(),
|
||||||
|
self.provider.clone(),
|
||||||
|
self.cluster.clone(),
|
||||||
|
initial_request,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(settings: &crate::YellowstoneGrpcEndpointSettings, channel: tonic::transport::Channel) -> Self {
|
||||||
|
return Self {
|
||||||
|
endpoint_name: settings.name().to_owned(),
|
||||||
|
provider: settings.provider().clone(),
|
||||||
|
cluster: settings.cluster().clone(),
|
||||||
|
channel,
|
||||||
|
metadata: settings.metadata().to_vec(),
|
||||||
|
session: settings.session().clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for YellowstoneGrpcChannel {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("YellowstoneGrpcChannel")
|
||||||
|
.field("endpoint_name", &self.endpoint_name)
|
||||||
|
.field("provider", &self.provider)
|
||||||
|
.field("cluster", &self.cluster)
|
||||||
|
.field("metadata_count", &self.metadata.len())
|
||||||
|
.field("channel", &"<private>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tonic_endpoint(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<tonic::transport::Endpoint> {
|
||||||
|
let endpoint = match tonic::transport::Endpoint::from_shared(settings.url().as_str().to_owned()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC endpoint URI cannot be prepared by the HTTP/2 transport"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let endpoint = endpoint.connect_timeout(settings.session().connect_timeout()).buffer_size(settings.session().request_channel_capacity());
|
||||||
|
if settings.url().uses_tls() {
|
||||||
|
let tls = tonic::transport::ClientTlsConfig::new().with_webpki_roots().timeout(settings.session().connect_timeout());
|
||||||
|
return match endpoint.tls_config(tls) {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC TLS configuration failed before connection"))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grpc_channel_error(settings: &crate::YellowstoneGrpcEndpointSettings, message: &str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, message)
|
||||||
|
.with_context("endpoint_name", settings.name())
|
||||||
|
.with_context("provider", settings.provider().as_str())
|
||||||
|
.with_context("cluster", settings.cluster().as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/grpc_channel.rs"]
|
||||||
|
mod tests;
|
||||||
666
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal file
666
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
const DEFAULT_GRPC_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const DEFAULT_GRPC_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
const DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024;
|
||||||
|
const DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024;
|
||||||
|
const DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(250);
|
||||||
|
const DEFAULT_GRPC_RECONNECT_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const DEFAULT_GRPC_RECONNECT_MAX_RETRIES: u32 = 5;
|
||||||
|
const DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY: usize = 128;
|
||||||
|
const DEFAULT_GRPC_UNARY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
const DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY: usize = 256;
|
||||||
|
const MAX_GRPC_CHANNEL_CAPACITY: usize = 65_536;
|
||||||
|
const MAX_GRPC_DESCRIPTOR_LENGTH_BYTES: usize = 128;
|
||||||
|
const MAX_GRPC_ENDPOINT_COUNT: usize = 128;
|
||||||
|
const MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES: usize = 8 * 1024;
|
||||||
|
const MAX_GRPC_MESSAGE_SIZE_BYTES: usize = 512 * 1024 * 1024;
|
||||||
|
const MAX_GRPC_METADATA_ENTRY_COUNT: usize = 64;
|
||||||
|
const MAX_GRPC_METADATA_KEY_LENGTH_BYTES: usize = 128;
|
||||||
|
const MAX_GRPC_METADATA_VALUE_LENGTH_BYTES: usize = 8 * 1024;
|
||||||
|
const MAX_GRPC_RECONNECT_RETRIES: u32 = 100;
|
||||||
|
const MAX_GRPC_RUNTIME_DURATION: std::time::Duration = std::time::Duration::from_secs(300);
|
||||||
|
|
||||||
|
/// Runtime Yellowstone gRPC endpoint URL owned by Transport.
|
||||||
|
///
|
||||||
|
/// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcEndpointUrl {
|
||||||
|
value: std::string::String,
|
||||||
|
uses_tls: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcEndpointUrl {
|
||||||
|
/// Parses and validates one Yellowstone gRPC endpoint URL.
|
||||||
|
pub fn parse(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating Yellowstone gRPC endpoint URL");
|
||||||
|
let value = value.into();
|
||||||
|
if value.len() > MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL exceeds the KSP length bound")
|
||||||
|
.with_context("field", "grpc_endpoints.url"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let parsed = match reqwest::Url::parse(value.as_str()) {
|
||||||
|
std::result::Result::Ok(parsed) => parsed,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected invalid Yellowstone gRPC endpoint URL");
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL is invalid")
|
||||||
|
.with_context("field", "grpc_endpoints.url")
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
field = "grpc_endpoints.url",
|
||||||
|
scheme = parsed.scheme(),
|
||||||
|
"rejected Yellowstone gRPC endpoint URL with unsupported scheme"
|
||||||
|
);
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must use http or https")
|
||||||
|
.with_context("field", "grpc_endpoints.url")
|
||||||
|
.with_context("scheme", parsed.scheme()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if parsed.host_str().is_none() {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected Yellowstone gRPC endpoint URL without host");
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must contain a host")
|
||||||
|
.with_context("field", "grpc_endpoints.url"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, scheme = parsed.scheme(), "validated Yellowstone gRPC endpoint URL syntax");
|
||||||
|
return std::result::Result::Ok(Self { value, uses_tls: parsed.scheme() == "https" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sensitive runtime URL text.
|
||||||
|
///
|
||||||
|
/// Callers must not write this value to logs, generic diagnostics or snapshots.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this endpoint URL requires TLS.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn uses_tls(&self) -> bool {
|
||||||
|
return self.uses_tls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for YellowstoneGrpcEndpointUrl {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.write_str("YellowstoneGrpcEndpointUrl(<redacted>)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One validated ASCII metadata entry attached to Yellowstone gRPC requests.
|
||||||
|
///
|
||||||
|
/// Metadata values are intentionally omitted from [`std::fmt::Debug`] for both public and secret entries. Secret entries are additionally marked sensitive on
|
||||||
|
/// the Tonic metadata value before transmission so the HTTP/2 stack avoids indexing them where supported. Binary `*-bin` metadata is not part of the
|
||||||
|
/// `0.2.9-pre.003` contract.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcMetadataEntry {
|
||||||
|
key: std::string::String,
|
||||||
|
value: std::string::String,
|
||||||
|
secret: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcMetadataEntry {
|
||||||
|
/// Creates one non-secret ASCII metadata entry.
|
||||||
|
pub fn public(key: impl std::convert::Into<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||||
|
return Self::new(key.into(), value.into(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates one secret ASCII metadata entry with redacted diagnostics.
|
||||||
|
pub fn secret(key: impl std::convert::Into<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||||
|
return Self::new(key.into(), value.into(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the validated metadata key.
|
||||||
|
#[must_use]
|
||||||
|
pub fn key(&self) -> &str {
|
||||||
|
return self.key.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the value must be treated as secret by Transport.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_secret(&self) -> bool {
|
||||||
|
return self.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends this validated value to an internal Tonic metadata map while preserving its sensitivity marker.
|
||||||
|
pub(crate) fn append_to(&self, metadata: &mut tonic::metadata::MetadataMap) -> ksp_core_lib::Result<()> {
|
||||||
|
let key = match tonic::metadata::MetadataKey::<tonic::metadata::Ascii>::from_bytes(self.key.as_bytes()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid")
|
||||||
|
.with_context("field", "grpc_endpoint.metadata.key"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut value = match tonic::metadata::AsciiMetadataValue::try_from(self.value.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value is invalid")
|
||||||
|
.with_context("field", "grpc_endpoint.metadata.value")
|
||||||
|
.with_context("metadata_key", self.key.as_str()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
value.set_sensitive(self.secret);
|
||||||
|
metadata.append(key, value);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(key: std::string::String, value: std::string::String, secret: bool) -> ksp_core_lib::Result<Self> {
|
||||||
|
if key.is_empty()
|
||||||
|
|| key.len() > MAX_GRPC_METADATA_KEY_LENGTH_BYTES
|
||||||
|
|| key != key.to_ascii_lowercase()
|
||||||
|
|| key.starts_with("grpc-")
|
||||||
|
|| key.ends_with("-bin")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key violates the KSP ASCII metadata contract")
|
||||||
|
.with_context("field", "grpc_endpoint.metadata.key"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tonic::metadata::MetadataKey::<tonic::metadata::Ascii>::from_bytes(key.as_bytes()).is_err() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid")
|
||||||
|
.with_context("field", "grpc_endpoint.metadata.key"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if value.len() > MAX_GRPC_METADATA_VALUE_LENGTH_BYTES || tonic::metadata::AsciiMetadataValue::try_from(value.as_str()).is_err() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value violates the KSP ASCII metadata contract")
|
||||||
|
.with_context("field", "grpc_endpoint.metadata.value")
|
||||||
|
.with_context("metadata_key", key.as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(Self { key, value, secret });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for YellowstoneGrpcMetadataEntry {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("YellowstoneGrpcMetadataEntry")
|
||||||
|
.field("key", &self.key)
|
||||||
|
.field("secret", &self.secret)
|
||||||
|
.field("value", &"<redacted>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcProviderName {
|
||||||
|
value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcProviderName {
|
||||||
|
/// Creates an open provider descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
||||||
|
return Self { value: value.into() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the provider descriptor text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcClusterName {
|
||||||
|
value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcClusterName {
|
||||||
|
/// Creates an open cluster descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
||||||
|
return Self { value: value.into() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cluster descriptor text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcReconnectSettings {
|
||||||
|
max_retries: u32,
|
||||||
|
initial_backoff: std::time::Duration,
|
||||||
|
max_backoff: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcReconnectSettings {
|
||||||
|
/// Creates bounded reconnect settings.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self {
|
||||||
|
return Self { max_retries, initial_backoff, max_backoff };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of reconnect attempts allowed after one live channel or stream is lost.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_retries(&self) -> u32 {
|
||||||
|
return self.max_retries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the initial reconnect backoff.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn initial_backoff(&self) -> std::time::Duration {
|
||||||
|
return self.initial_backoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum reconnect backoff.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_backoff(&self) -> std::time::Duration {
|
||||||
|
return self.max_backoff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for YellowstoneGrpcReconnectSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::new(DEFAULT_GRPC_RECONNECT_MAX_RETRIES, DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF, DEFAULT_GRPC_RECONNECT_MAX_BACKOFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime limits and lifecycle settings for one Yellowstone gRPC physical channel/session path.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcSessionSettings {
|
||||||
|
connect_timeout: std::time::Duration,
|
||||||
|
unary_timeout: std::time::Duration,
|
||||||
|
close_timeout: std::time::Duration,
|
||||||
|
reconnect: crate::YellowstoneGrpcReconnectSettings,
|
||||||
|
request_channel_capacity: usize,
|
||||||
|
update_channel_capacity: usize,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcSessionSettings {
|
||||||
|
/// Creates complete runtime settings for one physical Yellowstone gRPC channel/session path.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub const fn new(
|
||||||
|
connect_timeout: std::time::Duration,
|
||||||
|
unary_timeout: std::time::Duration,
|
||||||
|
close_timeout: std::time::Duration,
|
||||||
|
reconnect: crate::YellowstoneGrpcReconnectSettings,
|
||||||
|
request_channel_capacity: usize,
|
||||||
|
update_channel_capacity: usize,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
connect_timeout,
|
||||||
|
unary_timeout,
|
||||||
|
close_timeout,
|
||||||
|
reconnect,
|
||||||
|
request_channel_capacity,
|
||||||
|
update_channel_capacity,
|
||||||
|
max_inbound_message_size_bytes,
|
||||||
|
max_outbound_message_size_bytes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the connection establishment timeout.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn connect_timeout(&self) -> std::time::Duration {
|
||||||
|
return self.connect_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default unary request timeout.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn unary_timeout(&self) -> std::time::Duration {
|
||||||
|
return self.unary_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded graceful-close timeout.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn close_timeout(&self) -> std::time::Duration {
|
||||||
|
return self.close_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the reconnect policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn reconnect(&self) -> &crate::YellowstoneGrpcReconnectSettings {
|
||||||
|
return &self.reconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded outgoing Subscribe request channel capacity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn request_channel_capacity(&self) -> usize {
|
||||||
|
return self.request_channel_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded incoming update delivery channel capacity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn update_channel_capacity(&self) -> usize {
|
||||||
|
return self.update_channel_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum inbound protobuf message size accepted by KSP.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_inbound_message_size_bytes(&self) -> usize {
|
||||||
|
return self.max_inbound_message_size_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum outbound protobuf message size accepted by KSP.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_outbound_message_size_bytes(&self) -> usize {
|
||||||
|
return self.max_outbound_message_size_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates KSP-owned runtime bounds without reading Config or environment state.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_duration(self.connect_timeout, "grpc_session.connect_timeout") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_duration(self.unary_timeout, "grpc_session.unary_timeout") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_duration(self.close_timeout, "grpc_session.close_timeout") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if self.reconnect.max_retries() > MAX_GRPC_RECONNECT_RETRIES {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC reconnect retry count exceeds the KSP runtime bound", "grpc_session.reconnect.max_retries");
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.initial_backoff(), "grpc_session.reconnect.initial_backoff") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.max_backoff(), "grpc_session.reconnect.max_backoff") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if self.reconnect.initial_backoff() > self.reconnect.max_backoff() {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC reconnect initial backoff must not exceed maximum backoff", "grpc_session.reconnect");
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_capacity(self.request_channel_capacity, "grpc_session.request_channel_capacity") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_capacity(self.update_channel_capacity, "grpc_session.update_channel_capacity") {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_message_size(self.max_inbound_message_size_bytes, "grpc_session.max_inbound_message_size_bytes")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) =
|
||||||
|
validate_grpc_message_size(self.max_outbound_message_size_bytes, "grpc_session.max_outbound_message_size_bytes")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
request_channel_capacity = self.request_channel_capacity,
|
||||||
|
update_channel_capacity = self.update_channel_capacity,
|
||||||
|
max_inbound_message_size_bytes = self.max_inbound_message_size_bytes,
|
||||||
|
max_outbound_message_size_bytes = self.max_outbound_message_size_bytes,
|
||||||
|
reconnect_max_retries = self.reconnect.max_retries(),
|
||||||
|
"validated Yellowstone gRPC session settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for YellowstoneGrpcSessionSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::new(
|
||||||
|
DEFAULT_GRPC_CONNECT_TIMEOUT,
|
||||||
|
DEFAULT_GRPC_UNARY_TIMEOUT,
|
||||||
|
DEFAULT_GRPC_CLOSE_TIMEOUT,
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::default(),
|
||||||
|
DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY,
|
||||||
|
DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY,
|
||||||
|
DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES,
|
||||||
|
DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime settings for one named Yellowstone gRPC endpoint.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcEndpointSettings {
|
||||||
|
name: std::string::String,
|
||||||
|
enabled: bool,
|
||||||
|
provider: crate::YellowstoneGrpcProviderName,
|
||||||
|
cluster: crate::YellowstoneGrpcClusterName,
|
||||||
|
url: crate::YellowstoneGrpcEndpointUrl,
|
||||||
|
session: crate::YellowstoneGrpcSessionSettings,
|
||||||
|
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcEndpointSettings {
|
||||||
|
/// Creates explicit settings for one logical Yellowstone gRPC endpoint.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
name: impl std::convert::Into<std::string::String>,
|
||||||
|
enabled: bool,
|
||||||
|
provider: crate::YellowstoneGrpcProviderName,
|
||||||
|
cluster: crate::YellowstoneGrpcClusterName,
|
||||||
|
url: crate::YellowstoneGrpcEndpointUrl,
|
||||||
|
session: crate::YellowstoneGrpcSessionSettings,
|
||||||
|
) -> Self {
|
||||||
|
return Self { name: name.into(), enabled, provider, cluster, url, session, metadata: std::vec::Vec::new() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the logical endpoint name.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
return self.name.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this endpoint can be used to prepare a physical channel.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn enabled(&self) -> bool {
|
||||||
|
return self.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open provider descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
|
||||||
|
return &self.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open cluster descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
|
||||||
|
return &self.cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sensitive Yellowstone gRPC endpoint URL wrapper.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn url(&self) -> &crate::YellowstoneGrpcEndpointUrl {
|
||||||
|
return &self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the effective runtime settings for the channel/session path.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn session(&self) -> &crate::YellowstoneGrpcSessionSettings {
|
||||||
|
return &self.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns metadata entries in declaration order without exposing their values.
|
||||||
|
#[must_use]
|
||||||
|
pub fn metadata(&self) -> &[crate::YellowstoneGrpcMetadataEntry] {
|
||||||
|
return self.metadata.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces request metadata after validating KSP bounds and ASCII metadata rules.
|
||||||
|
pub fn with_metadata(mut self, metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>) -> ksp_core_lib::Result<Self> {
|
||||||
|
if metadata.len() > MAX_GRPC_METADATA_ENTRY_COUNT {
|
||||||
|
return grpc_invalid_settings_value("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata");
|
||||||
|
}
|
||||||
|
self.metadata = metadata;
|
||||||
|
if let std::result::Result::Err(error) = self.validate() {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates this endpoint without performing network I/O.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return validate_grpc_endpoint(self, "grpc_endpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneGrpcTransportSettings {
|
||||||
|
endpoints: std::vec::Vec<crate::YellowstoneGrpcEndpointSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneGrpcTransportSettings {
|
||||||
|
/// Creates complete Yellowstone gRPC runtime settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(endpoints: std::vec::Vec<crate::YellowstoneGrpcEndpointSettings>) -> Self {
|
||||||
|
return Self { endpoints };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns configured Yellowstone gRPC endpoints in declaration order.
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoints(&self) -> &[crate::YellowstoneGrpcEndpointSettings] {
|
||||||
|
return self.endpoints.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates structural runtime invariants without reading Config or environment state.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, endpoint_count = self.endpoints.len(), "validating Yellowstone gRPC transport settings");
|
||||||
|
if self.endpoints.is_empty() {
|
||||||
|
return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be configured", "grpc_endpoints");
|
||||||
|
}
|
||||||
|
if self.endpoints.len() > MAX_GRPC_ENDPOINT_COUNT {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC endpoint count exceeds the KSP bound", "grpc_endpoints");
|
||||||
|
}
|
||||||
|
let mut enabled_endpoint_count = 0_usize;
|
||||||
|
for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() {
|
||||||
|
let field_prefix = format!("grpc_endpoints[{endpoint_index}]");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_endpoint(endpoint, field_prefix.as_str()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if endpoint.enabled() {
|
||||||
|
enabled_endpoint_count += 1;
|
||||||
|
}
|
||||||
|
for previous in &self.endpoints[..endpoint_index] {
|
||||||
|
if previous.name() == endpoint.name() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint names must be unique")
|
||||||
|
.with_context("field", format!("grpc_endpoints[{endpoint_index}].name"))
|
||||||
|
.with_context("endpoint_name", endpoint.name()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabled_endpoint_count == 0 {
|
||||||
|
return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be enabled", "grpc_endpoints.enabled");
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_count = self.endpoints.len(),
|
||||||
|
enabled_endpoint_count,
|
||||||
|
"validated Yellowstone gRPC transport settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_endpoint(endpoint: &crate::YellowstoneGrpcEndpointSettings, field_prefix: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
let name_field = format!("{field_prefix}.name");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.name(), name_field.as_str()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let provider_field = format!("{field_prefix}.provider");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.provider().as_str(), provider_field.as_str()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let cluster_field = format!("{field_prefix}.cluster");
|
||||||
|
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = endpoint.session().validate() {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if endpoint.metadata().len() > MAX_GRPC_METADATA_ENTRY_COUNT {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata");
|
||||||
|
}
|
||||||
|
ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_name = endpoint.name(),
|
||||||
|
provider = endpoint.provider().as_str(),
|
||||||
|
cluster = endpoint.cluster().as_str(),
|
||||||
|
enabled = endpoint.enabled(),
|
||||||
|
"validated Yellowstone gRPC endpoint settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not be empty", field);
|
||||||
|
}
|
||||||
|
if value.trim() != value {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not contain leading or trailing whitespace", field);
|
||||||
|
}
|
||||||
|
if value.len() > MAX_GRPC_DESCRIPTOR_LENGTH_BYTES {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC transport descriptor exceeds the KSP length bound", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_duration(value: std::time::Duration, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value.is_zero() {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC runtime duration must be greater than zero", field);
|
||||||
|
}
|
||||||
|
if value > MAX_GRPC_RUNTIME_DURATION {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC runtime duration exceeds the KSP bound", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_capacity(value: usize, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value == 0 {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC channel capacity must be greater than zero", field);
|
||||||
|
}
|
||||||
|
if value > MAX_GRPC_CHANNEL_CAPACITY {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC channel capacity exceeds the KSP bound", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_grpc_message_size(value: usize, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value == 0 {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC message size must be greater than zero", field);
|
||||||
|
}
|
||||||
|
if value > MAX_GRPC_MESSAGE_SIZE_BYTES {
|
||||||
|
return grpc_invalid_settings("Yellowstone gRPC message size exceeds the KSP bound", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grpc_invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings");
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grpc_invalid_settings_value<T>(message: &str, field: &str) -> ksp_core_lib::Result<T> {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings");
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/grpc_settings.rs"]
|
||||||
|
mod tests;
|
||||||
1146
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
Normal file
1146
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
Normal file
File diff suppressed because it is too large
Load Diff
3740
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
Normal file
3740
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
Normal file
File diff suppressed because it is too large
Load Diff
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal file
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/grpc_unary.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
const MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES: usize = 256;
|
||||||
|
const MAX_YELLOWSTONE_VERSION_LENGTH_BYTES: usize = 512;
|
||||||
|
const PATH_GET_BLOCK_HEIGHT: &str = "/geyser.Geyser/GetBlockHeight";
|
||||||
|
const PATH_GET_LATEST_BLOCKHASH: &str = "/geyser.Geyser/GetLatestBlockhash";
|
||||||
|
const PATH_GET_SLOT: &str = "/geyser.Geyser/GetSlot";
|
||||||
|
const PATH_GET_VERSION: &str = "/geyser.Geyser/GetVersion";
|
||||||
|
const PATH_IS_BLOCKHASH_VALID: &str = "/geyser.Geyser/IsBlockhashValid";
|
||||||
|
const PATH_PING: &str = "/geyser.Geyser/Ping";
|
||||||
|
const PATH_SUBSCRIBE_REPLAY_INFO: &str = "/geyser.Geyser/SubscribeReplayInfo";
|
||||||
|
|
||||||
|
/// Standard Solana Yellowstone unary client layered on one KSP-owned physical gRPC channel.
|
||||||
|
///
|
||||||
|
/// This N2 facade deliberately exposes no raw Tonic client and no upstream protobuf types. Streaming `Subscribe` is not part of `0.2.9-pre.003`.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SolanaYellowstoneGrpcUnaryClient {
|
||||||
|
channel: tonic::transport::Channel,
|
||||||
|
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
unary_timeout: std::time::Duration,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolanaYellowstoneGrpcUnaryClient {
|
||||||
|
/// Creates the internal unary facade state from a KSP-owned physical channel and validated endpoint settings.
|
||||||
|
pub(crate) fn new(
|
||||||
|
channel: tonic::transport::Channel,
|
||||||
|
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
|
||||||
|
settings: crate::YellowstoneGrpcSessionSettings,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
channel,
|
||||||
|
metadata,
|
||||||
|
unary_timeout: settings.unary_timeout(),
|
||||||
|
max_inbound_message_size_bytes: settings.max_inbound_message_size_bytes(),
|
||||||
|
max_outbound_message_size_bytes: settings.max_outbound_message_size_bytes(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first slot retained by the endpoint for replay when advertised.
|
||||||
|
pub async fn subscribe_replay_info(&self) -> ksp_core_lib::Result<crate::YellowstoneReplayInfo> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse =
|
||||||
|
match self.unary("SubscribeReplayInfo", PATH_SUBSCRIBE_REPLAY_INFO, yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest {}).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneReplayInfo::new(response.first_available));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the standard Yellowstone unary `Ping` RPC and verifies the echoed count.
|
||||||
|
pub async fn ping(&self, count: i32) -> ksp_core_lib::Result<crate::YellowstonePong> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::PongResponse =
|
||||||
|
match self.unary("Ping", PATH_PING, yellowstone_grpc_proto::geyser::PingRequest { count }).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if response.count != count {
|
||||||
|
return invalid_unary_response("Ping", "Yellowstone Ping response count does not match the request");
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::YellowstonePong::new(response.count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest blockhash advertised by the Yellowstone endpoint at an optional commitment.
|
||||||
|
pub async fn get_latest_blockhash(
|
||||||
|
&self,
|
||||||
|
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||||
|
) -> ksp_core_lib::Result<crate::YellowstoneLatestBlockhash> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse = match self
|
||||||
|
.unary(
|
||||||
|
"GetLatestBlockhash",
|
||||||
|
PATH_GET_LATEST_BLOCKHASH,
|
||||||
|
yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest { commitment: commitment_to_wire(commitment) },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if response.blockhash.is_empty() || response.blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES {
|
||||||
|
return invalid_unary_response("GetLatestBlockhash", "Yellowstone latest blockhash response violates the KSP text bound");
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneLatestBlockhash::new(response.slot, response.blockhash, response.last_valid_block_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current block height at an optional commitment.
|
||||||
|
pub async fn get_block_height(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneBlockHeight> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::GetBlockHeightResponse = match self
|
||||||
|
.unary(
|
||||||
|
"GetBlockHeight",
|
||||||
|
PATH_GET_BLOCK_HEIGHT,
|
||||||
|
yellowstone_grpc_proto::geyser::GetBlockHeightRequest { commitment: commitment_to_wire(commitment) },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneBlockHeight::new(response.block_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current slot at an optional commitment.
|
||||||
|
pub async fn get_slot(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneSlot> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::GetSlotResponse =
|
||||||
|
match self.unary("GetSlot", PATH_GET_SLOT, yellowstone_grpc_proto::geyser::GetSlotRequest { commitment: commitment_to_wire(commitment) }).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneSlot::new(response.slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks one opaque Solana blockhash at an optional commitment.
|
||||||
|
pub async fn is_blockhash_valid(
|
||||||
|
&self,
|
||||||
|
blockhash: impl std::convert::Into<std::string::String>,
|
||||||
|
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||||
|
) -> ksp_core_lib::Result<crate::YellowstoneBlockhashValidity> {
|
||||||
|
let blockhash = blockhash.into();
|
||||||
|
if blockhash.is_empty() || blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES || blockhash.trim() != blockhash {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone blockhash must be non-empty, bounded and untrimmed")
|
||||||
|
.with_context("grpc_operation", "IsBlockhashValid")
|
||||||
|
.with_context("field", "blockhash"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let response: yellowstone_grpc_proto::geyser::IsBlockhashValidResponse = match self
|
||||||
|
.unary(
|
||||||
|
"IsBlockhashValid",
|
||||||
|
PATH_IS_BLOCKHASH_VALID,
|
||||||
|
yellowstone_grpc_proto::geyser::IsBlockhashValidRequest { blockhash, commitment: commitment_to_wire(commitment) },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneBlockhashValidity::new(response.slot, response.valid));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the endpoint's Yellowstone/validator version string after applying a deterministic KSP text bound.
|
||||||
|
pub async fn get_version(&self) -> ksp_core_lib::Result<crate::YellowstoneVersionInfo> {
|
||||||
|
let response: yellowstone_grpc_proto::geyser::GetVersionResponse =
|
||||||
|
match self.unary("GetVersion", PATH_GET_VERSION, yellowstone_grpc_proto::geyser::GetVersionRequest {}).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if response.version.is_empty() || response.version.len() > MAX_YELLOWSTONE_VERSION_LENGTH_BYTES {
|
||||||
|
return invalid_unary_response("GetVersion", "Yellowstone version response violates the KSP text bound");
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::YellowstoneVersionInfo::new(response.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unary<Request, Response>(&self, operation: &'static str, path: &'static str, message: Request) -> ksp_core_lib::Result<Response>
|
||||||
|
where
|
||||||
|
Request: tonic_prost::prost::Message + Send + Sync + 'static,
|
||||||
|
Response: tonic_prost::prost::Message + std::default::Default + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let path = match path.parse::<http::uri::PathAndQuery>() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "internal Yellowstone gRPC method path is invalid")
|
||||||
|
.with_context("grpc_operation", operation),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut grpc = tonic::client::Grpc::new(self.channel.clone())
|
||||||
|
.max_decoding_message_size(self.max_inbound_message_size_bytes)
|
||||||
|
.max_encoding_message_size(self.max_outbound_message_size_bytes);
|
||||||
|
let future = async {
|
||||||
|
if grpc.ready().await.is_err() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel is not ready for unary dispatch")
|
||||||
|
.with_context("grpc_operation", operation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut request = tonic::Request::new(message);
|
||||||
|
for entry in &self.metadata {
|
||||||
|
if let std::result::Result::Err(error) = entry.append_to(request.metadata_mut()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = match grpc.unary(request, path, tonic_prost::ProstCodec::<Request, Response>::default()).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(status) => return std::result::Result::Err(grpc_status_error(operation, status)),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(response.into_inner());
|
||||||
|
};
|
||||||
|
return match tokio::time::timeout(self.unary_timeout, future).await {
|
||||||
|
std::result::Result::Ok(result) => result,
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, "Yellowstone gRPC unary operation exceeded the KSP deadline")
|
||||||
|
.with_context("grpc_operation", operation),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SolanaYellowstoneGrpcUnaryClient {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("SolanaYellowstoneGrpcUnaryClient")
|
||||||
|
.field("metadata_count", &self.metadata.len())
|
||||||
|
.field("unary_timeout", &self.unary_timeout)
|
||||||
|
.field("max_inbound_message_size_bytes", &self.max_inbound_message_size_bytes)
|
||||||
|
.field("max_outbound_message_size_bytes", &self.max_outbound_message_size_bytes)
|
||||||
|
.field("channel", &"<private>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replay availability advertised by `SubscribeReplayInfo`.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneReplayInfo {
|
||||||
|
first_available: std::option::Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneReplayInfo {
|
||||||
|
/// Creates a replay-info projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(first_available: std::option::Option<u64>) -> Self {
|
||||||
|
return Self { first_available };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first replayable slot when the endpoint advertises one.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn first_available(&self) -> std::option::Option<u64> {
|
||||||
|
return self.first_available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Echo returned by the Yellowstone unary `Ping` RPC.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstonePong {
|
||||||
|
count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstonePong {
|
||||||
|
/// Creates a ping response projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(count: i32) -> Self {
|
||||||
|
return Self { count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the echoed ping count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn count(&self) -> i32 {
|
||||||
|
return self.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Latest blockhash response returned by Yellowstone.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneLatestBlockhash {
|
||||||
|
slot: u64,
|
||||||
|
blockhash: std::string::String,
|
||||||
|
last_valid_block_height: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneLatestBlockhash {
|
||||||
|
/// Creates a latest-blockhash projection.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(slot: u64, blockhash: std::string::String, last_valid_block_height: u64) -> Self {
|
||||||
|
return Self { slot, blockhash, last_valid_block_height };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the response slot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn slot(&self) -> u64 {
|
||||||
|
return self.slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the opaque Solana blockhash text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn blockhash(&self) -> &str {
|
||||||
|
return self.blockhash.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last valid block height associated with the blockhash.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn last_valid_block_height(&self) -> u64 {
|
||||||
|
return self.last_valid_block_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current block height returned by Yellowstone.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneBlockHeight {
|
||||||
|
block_height: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneBlockHeight {
|
||||||
|
/// Creates a block-height projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(block_height: u64) -> Self {
|
||||||
|
return Self { block_height };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the block height.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn block_height(&self) -> u64 {
|
||||||
|
return self.block_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current slot returned by Yellowstone.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneSlot {
|
||||||
|
slot: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneSlot {
|
||||||
|
/// Creates a slot projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(slot: u64) -> Self {
|
||||||
|
return Self { slot };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the slot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn slot(&self) -> u64 {
|
||||||
|
return self.slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of checking one blockhash with Yellowstone.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneBlockhashValidity {
|
||||||
|
slot: u64,
|
||||||
|
valid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneBlockhashValidity {
|
||||||
|
/// Creates a blockhash-validity projection.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(slot: u64, valid: bool) -> Self {
|
||||||
|
return Self { slot, valid };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the response slot.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn slot(&self) -> u64 {
|
||||||
|
return self.slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the blockhash is valid at the requested commitment.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn valid(&self) -> bool {
|
||||||
|
return self.valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded Yellowstone endpoint version information.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct YellowstoneVersionInfo {
|
||||||
|
version: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YellowstoneVersionInfo {
|
||||||
|
/// Creates a version projection.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(version: std::string::String) -> Self {
|
||||||
|
return Self { version };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded endpoint version string.
|
||||||
|
#[must_use]
|
||||||
|
pub fn version(&self) -> &str {
|
||||||
|
return self.version.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
|
||||||
|
return commitment.map(|value| {
|
||||||
|
return match value {
|
||||||
|
crate::SolanaCommitment::Processed => yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32,
|
||||||
|
crate::SolanaCommitment::Confirmed => yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed as i32,
|
||||||
|
crate::SolanaCommitment::Finalized => yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grpc_status_error(operation: &'static str, status: tonic::Status) -> ksp_core_lib::Error {
|
||||||
|
let code = status.code().to_string();
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, grpc_operation = operation, grpc_code = code.as_str(), "Yellowstone gRPC unary endpoint returned a status");
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_STATUS, "Yellowstone gRPC endpoint returned a gRPC status")
|
||||||
|
.with_context("grpc_operation", operation)
|
||||||
|
.with_context("grpc_code", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_unary_response<T>(operation: &'static str, message: &str) -> ksp_core_lib::Result<T> {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_operation", operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/grpc_unary.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/client.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_client.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
/// Passive runtime availability reported for one logical HTTP endpoint or role.
|
/// Passive runtime availability reported for one logical HTTP endpoint or role.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -510,5 +510,5 @@ fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/client.rs"]
|
#[path = "../unit_tests/http_client.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/executor.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_executor.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
const HTTP_BAD_GATEWAY: u16 = 502;
|
const HTTP_BAD_GATEWAY: u16 = 502;
|
||||||
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
||||||
@@ -228,5 +228,5 @@ fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ks
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/executor.rs"]
|
#[path = "../unit_tests/http_executor.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/pool.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_pool.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
/// Safe snapshot of the logical HTTP endpoint pool.
|
/// Safe snapshot of the logical HTTP endpoint pool.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -616,5 +616,5 @@ fn request_timeout(role: &crate::HttpRoleName, request_kind: &crate::HttpRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/pool.rs"]
|
#[path = "../unit_tests/http_pool.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_resilience.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
|
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
|
||||||
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
|
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
|
||||||
@@ -401,5 +401,5 @@ fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std:
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/resilience.rs"]
|
#[path = "../unit_tests/http_resilience.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_settings.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
/// Runtime HTTP endpoint URL owned by Transport.
|
/// Runtime HTTP endpoint URL owned by Transport.
|
||||||
///
|
///
|
||||||
@@ -582,5 +582,5 @@ fn invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/settings.rs"]
|
#[path = "../unit_tests/http_settings.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 34
|
// version: 44
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -31,14 +31,34 @@
|
|||||||
//! `0.2.8-pre.005` adds the typed Helius `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live
|
//! `0.2.8-pre.005` adds the typed Helius `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live
|
||||||
//! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
|
//! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
|
||||||
//! per-subscription backpressure path.
|
//! per-subscription backpressure path.
|
||||||
|
//! `0.2.9-pre.002` opens the Yellowstone gRPC N1 engine foundation with Transport-owned redacted settings, bounded reconnect/channel/message policies, the
|
||||||
|
//! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types.
|
||||||
|
//! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs
|
||||||
|
//! through KSP-owned DTOs.
|
||||||
|
//! `0.2.9-pre.004` materializes the provider-neutral standard `SubscribeRequest` foundation: all seven named filter maps, global filter-name bounds/uniqueness,
|
||||||
|
//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot filters land in `pre.005`; transaction/block filters remain
|
||||||
|
//! staged for `pre.007–008`.
|
||||||
|
//! `0.2.9-pre.006` normalizes the five unambiguously HTTP-owned private implementation modules with an `http_` prefix while preserving shared `rpc_*`,
|
||||||
|
//! JSON-RPC, error and constants modules.
|
||||||
|
//! `0.2.9-pre.007` completes the standard transaction/transaction-status filters and storage-wire projections; `pre.008` completes Blocks, block-meta and
|
||||||
|
//! entry projections. `0.2.9-pre.009` promotes those protobuf bridges into runtime and opens one KSP-owned bounded bidirectional `Subscribe` session with
|
||||||
|
//! request mutation, automatic server-Ping reply, observable Pong, normal server half-close, terminal backpressure and bounded graceful shutdown.
|
||||||
|
//! `0.2.9-pre.010` activates the bounded KSP-owned reconnect policy, deterministic replay from the latest accepted request and highest observed slot,
|
||||||
|
//! conservative ReplayInfo-based continuity-gap observability and bounded duplicate observation without claiming exactly-once or lossless delivery.
|
||||||
|
|
||||||
mod client;
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
mod executor;
|
mod grpc_channel;
|
||||||
|
mod grpc_settings;
|
||||||
|
mod grpc_stream;
|
||||||
|
mod grpc_subscribe;
|
||||||
|
mod grpc_unary;
|
||||||
|
mod http_client;
|
||||||
|
mod http_executor;
|
||||||
|
mod http_pool;
|
||||||
|
mod http_resilience;
|
||||||
|
mod http_settings;
|
||||||
mod json_rpc;
|
mod json_rpc;
|
||||||
mod pool;
|
|
||||||
mod resilience;
|
|
||||||
mod rpc_accounts;
|
mod rpc_accounts;
|
||||||
mod rpc_blocks;
|
mod rpc_blocks;
|
||||||
mod rpc_canary;
|
mod rpc_canary;
|
||||||
@@ -48,7 +68,6 @@ mod rpc_economics;
|
|||||||
mod rpc_method;
|
mod rpc_method;
|
||||||
mod rpc_tokens;
|
mod rpc_tokens;
|
||||||
mod rpc_transactions;
|
mod rpc_transactions;
|
||||||
mod settings;
|
|
||||||
mod ws_accounts;
|
mod ws_accounts;
|
||||||
mod ws_blocks;
|
mod ws_blocks;
|
||||||
mod ws_cluster;
|
mod ws_cluster;
|
||||||
@@ -60,16 +79,16 @@ mod ws_settings;
|
|||||||
mod ws_subscription;
|
mod ws_subscription;
|
||||||
mod ws_transactions;
|
mod ws_transactions;
|
||||||
|
|
||||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
|
||||||
pub use self::client::HttpEndpointAvailability;
|
|
||||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
|
||||||
pub use self::client::HttpEndpointClient;
|
|
||||||
/// Safe routing snapshot for one configured endpoint role.
|
|
||||||
pub use self::client::HttpEndpointRoleSnapshot;
|
|
||||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
|
||||||
pub use self::client::HttpEndpointSnapshot;
|
|
||||||
/// Error code used when no logical endpoint can satisfy a request.
|
/// Error code used when no logical endpoint can satisfy a request.
|
||||||
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
||||||
|
/// Error code used when bounded Yellowstone gRPC runtime capacity is exhausted.
|
||||||
|
pub use self::error::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW;
|
||||||
|
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
|
||||||
|
pub use self::error::ERROR_CODE_GRPC_CHANNEL_FAILED;
|
||||||
|
/// Error code used when a Yellowstone gRPC subscribe session is no longer available.
|
||||||
|
pub use self::error::ERROR_CODE_GRPC_SESSION_CLOSED;
|
||||||
|
/// Error code used when a Yellowstone gRPC endpoint returns a remote status.
|
||||||
|
pub use self::error::ERROR_CODE_GRPC_STATUS;
|
||||||
/// Error code used when an HTTP connection cannot be established.
|
/// Error code used when an HTTP connection cannot be established.
|
||||||
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
||||||
/// Error code used when an HTTP request fails after connection establishment.
|
/// Error code used when an HTTP request fails after connection establishment.
|
||||||
@@ -102,6 +121,192 @@ pub use self::error::ERROR_CODE_WS_CONNECTION_FAILED;
|
|||||||
pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR;
|
pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR;
|
||||||
/// Error code used when a WebSocket session is no longer available.
|
/// Error code used when a WebSocket session is no longer available.
|
||||||
pub use self::error::ERROR_CODE_WS_SESSION_CLOSED;
|
pub use self::error::ERROR_CODE_WS_SESSION_CLOSED;
|
||||||
|
/// Yellowstone gRPC channel owned by KSP Transport.
|
||||||
|
pub use self::grpc_channel::YellowstoneGrpcChannel;
|
||||||
|
/// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcClusterName;
|
||||||
|
/// Runtime settings for one named Yellowstone gRPC endpoint.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcEndpointSettings;
|
||||||
|
/// Runtime Yellowstone gRPC endpoint URL with redacted diagnostics.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcEndpointUrl;
|
||||||
|
/// Validated public or secret ASCII metadata attached to Yellowstone gRPC requests.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcMetadataEntry;
|
||||||
|
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcProviderName;
|
||||||
|
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
|
||||||
|
/// Runtime limits and lifecycle settings for one Yellowstone gRPC channel/session path.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
|
||||||
|
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
|
||||||
|
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
|
||||||
|
/// Standard Yellowstone bidirectional Subscribe session.
|
||||||
|
pub use self::grpc_stream::SolanaYellowstoneGrpcSubscribeSession;
|
||||||
|
/// Safe Yellowstone reconnect/replay continuity snapshot.
|
||||||
|
pub use self::grpc_stream::YellowstoneGrpcSubscribeSnapshot;
|
||||||
|
/// Safe Yellowstone bidirectional Subscribe lifecycle state.
|
||||||
|
pub use self::grpc_stream::YellowstoneGrpcSubscribeState;
|
||||||
|
/// One validated standard Yellowstone account predicate.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountFilterPredicate;
|
||||||
|
/// Typed account payload carried by one standard Yellowstone account update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountInfo;
|
||||||
|
/// Lamport comparison used by standard Yellowstone account filters.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountLamportsFilter;
|
||||||
|
/// Validated standard Yellowstone account memcmp predicate.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountMemcmp;
|
||||||
|
/// Encoding selected by one Yellowstone account memcmp predicate.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountMemcmpEncoding;
|
||||||
|
/// Standard Yellowstone account-update projection owned by KSP.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountUpdate;
|
||||||
|
/// One standard Yellowstone account-data slice.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneAccountsDataSlice;
|
||||||
|
/// Metadata-only standard Yellowstone block update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneBlockMetaUpdate;
|
||||||
|
/// Rewards container carried by Yellowstone block and block-meta updates.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneBlockRewards;
|
||||||
|
/// Full standard Yellowstone block update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneBlockUpdate;
|
||||||
|
/// One compiled instruction from the Yellowstone Solana-storage transaction wire.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneCompiledInstruction;
|
||||||
|
/// Wire-preserving KSP representation of a standard Yellowstone Cuckoo filter.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneCuckooFilter;
|
||||||
|
/// Hash algorithm carried by a standard Yellowstone Cuckoo filter.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneCuckooHashAlgorithm;
|
||||||
|
/// One Yellowstone block-entry payload reused by block and standalone entry updates.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneEntryInfo;
|
||||||
|
/// Standalone standard Yellowstone entry update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneEntryUpdate;
|
||||||
|
/// Fixed-width 32-byte hash from the Yellowstone Solana-storage transaction wire.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneHashBytes;
|
||||||
|
/// One inner instruction from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneInnerInstruction;
|
||||||
|
/// One indexed inner-instruction group from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneInnerInstructions;
|
||||||
|
/// One address-table lookup from a Yellowstone transaction message.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneMessageAddressTableLookup;
|
||||||
|
/// Return-data payload from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneReturnData;
|
||||||
|
/// One reward entry from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneReward;
|
||||||
|
/// Reward classification from the Yellowstone Solana-storage wire.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneRewardType;
|
||||||
|
/// Current standard Yellowstone slot status.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSlotStatus;
|
||||||
|
/// Standard Yellowstone slot-update projection owned by KSP.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSlotUpdate;
|
||||||
|
/// Solana transaction body carried by Yellowstone storage protobuf messages.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneStoredTransaction;
|
||||||
|
/// Complete account-family filter group for standard Yellowstone Subscribe.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter;
|
||||||
|
/// Complete block-family filter group for standard Yellowstone Subscribe.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeBlockFilter;
|
||||||
|
/// Empty filter marker activating the standard Yellowstone blocks-meta family.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeBlocksMetaFilter;
|
||||||
|
/// Empty filter marker activating the standard Yellowstone entry family.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeEntryFilter;
|
||||||
|
/// Validated globally unique logical filter name for standard Yellowstone Subscribe maps.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeFilterName;
|
||||||
|
/// Optional ping mutation carried by the standard Yellowstone Subscribe request.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribePing;
|
||||||
|
/// Standard Yellowstone server Ping update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribePingUpdate;
|
||||||
|
/// Standard Yellowstone server Pong update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribePongUpdate;
|
||||||
|
/// Provider-neutral standard Yellowstone Subscribe request.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
|
||||||
|
/// Complete slot-family filter group for standard Yellowstone Subscribe.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
|
||||||
|
/// Complete transaction-family filter shared by transactions and transaction-status maps.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
|
||||||
|
/// Any standard Yellowstone Subscribe update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneSubscribeUpdate;
|
||||||
|
/// Optional token-account owner expansion for current Yellowstone transaction filters.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTokenAccountExpansion;
|
||||||
|
/// One pre/post token balance from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTokenBalance;
|
||||||
|
/// Optional Transaction V1 inline budget configuration from Yellowstone Solana-storage.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionConfig;
|
||||||
|
/// Opaque runtime transaction error bytes from Yellowstone Solana-storage.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionError;
|
||||||
|
/// Complete transaction info carried by Yellowstone transaction and block updates.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionInfo;
|
||||||
|
/// Complete current Yellowstone transaction message.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionMessage;
|
||||||
|
/// Solana transaction message header from Yellowstone Solana-storage.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionMessageHeader;
|
||||||
|
/// Fixed-width transaction signature attached to Yellowstone updates.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionSignature;
|
||||||
|
/// Validated base58 transaction-signature selector for Yellowstone transaction filters.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionSignatureSelector;
|
||||||
|
/// Complete Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionStatusMeta;
|
||||||
|
/// Lightweight Yellowstone transaction-status update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionStatusUpdate;
|
||||||
|
/// Full Yellowstone transaction update.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneTransactionUpdate;
|
||||||
|
/// UI token amount from Yellowstone transaction status metadata.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneUiTokenAmount;
|
||||||
|
/// Timestamp attached to standard Yellowstone update envelopes.
|
||||||
|
pub use self::grpc_subscribe::YellowstoneUpdateTimestamp;
|
||||||
|
/// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel.
|
||||||
|
pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient;
|
||||||
|
/// Block height returned by the standard Yellowstone unary surface.
|
||||||
|
pub use self::grpc_unary::YellowstoneBlockHeight;
|
||||||
|
/// Result of a standard Yellowstone blockhash-validity check.
|
||||||
|
pub use self::grpc_unary::YellowstoneBlockhashValidity;
|
||||||
|
/// Latest blockhash returned by the standard Yellowstone unary surface.
|
||||||
|
pub use self::grpc_unary::YellowstoneLatestBlockhash;
|
||||||
|
/// Echo returned by the standard Yellowstone unary Ping RPC.
|
||||||
|
pub use self::grpc_unary::YellowstonePong;
|
||||||
|
/// Replay availability advertised by the standard Yellowstone unary surface.
|
||||||
|
pub use self::grpc_unary::YellowstoneReplayInfo;
|
||||||
|
/// Current slot returned by the standard Yellowstone unary surface.
|
||||||
|
pub use self::grpc_unary::YellowstoneSlot;
|
||||||
|
/// Bounded endpoint version returned by the standard Yellowstone unary surface.
|
||||||
|
pub use self::grpc_unary::YellowstoneVersionInfo;
|
||||||
|
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||||
|
pub use self::http_client::HttpEndpointAvailability;
|
||||||
|
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||||
|
pub use self::http_client::HttpEndpointClient;
|
||||||
|
/// Safe routing snapshot for one configured endpoint role.
|
||||||
|
pub use self::http_client::HttpEndpointRoleSnapshot;
|
||||||
|
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||||
|
pub use self::http_client::HttpEndpointSnapshot;
|
||||||
|
/// Result of one logical endpoint selection.
|
||||||
|
pub use self::http_pool::HttpEndpointSelection;
|
||||||
|
/// Runtime admission permit for one HTTP request.
|
||||||
|
pub use self::http_pool::HttpRequestPermit;
|
||||||
|
/// Shareable logical HTTP endpoint pool with priority routing, admission limits and bounded deadlines.
|
||||||
|
pub use self::http_pool::HttpTransportPool;
|
||||||
|
/// Safe snapshot of the logical HTTP endpoint pool.
|
||||||
|
pub use self::http_pool::HttpTransportPoolSnapshot;
|
||||||
|
/// Dispatch knowledge used to prevent ambiguous automatic resubmission.
|
||||||
|
pub use self::http_resilience::HttpDispatchState;
|
||||||
|
/// Transport-level cause considered by the bounded retry policy.
|
||||||
|
pub use self::http_resilience::HttpRetryCause;
|
||||||
|
/// Result of evaluating one bounded transport retry opportunity.
|
||||||
|
pub use self::http_resilience::HttpRetryDecision;
|
||||||
|
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
||||||
|
pub use self::http_resilience::evaluate_transport_retry;
|
||||||
|
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
||||||
|
pub use self::http_settings::HttpClusterName;
|
||||||
|
/// Runtime settings for one role declared by an HTTP endpoint.
|
||||||
|
pub use self::http_settings::HttpEndpointRoleSettings;
|
||||||
|
/// Runtime settings for one named Solana HTTP endpoint.
|
||||||
|
pub use self::http_settings::HttpEndpointSettings;
|
||||||
|
/// Runtime HTTP endpoint URL with redacted diagnostics.
|
||||||
|
pub use self::http_settings::HttpEndpointUrl;
|
||||||
|
/// Open provider descriptor used by HTTP endpoint settings.
|
||||||
|
pub use self::http_settings::HttpProviderName;
|
||||||
|
/// Open request-kind descriptor used by logical endpoint capabilities.
|
||||||
|
pub use self::http_settings::HttpRequestKind;
|
||||||
|
/// Bounded retry settings owned by the HTTP transport runtime.
|
||||||
|
pub use self::http_settings::HttpRetrySettings;
|
||||||
|
/// Local limits attached to one logical HTTP endpoint role.
|
||||||
|
pub use self::http_settings::HttpRoleLimits;
|
||||||
|
/// Open logical endpoint role descriptor.
|
||||||
|
pub use self::http_settings::HttpRoleName;
|
||||||
|
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||||
|
pub use self::http_settings::HttpTransportSettings;
|
||||||
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
|
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
|
||||||
pub use self::json_rpc::JsonRpcErrorObject;
|
pub use self::json_rpc::JsonRpcErrorObject;
|
||||||
/// Validated JSON-RPC 2.0 error response.
|
/// Validated JSON-RPC 2.0 error response.
|
||||||
@@ -116,22 +321,6 @@ pub use self::json_rpc::JsonRpcSuccessResponse;
|
|||||||
pub use self::json_rpc::parse_json_rpc_response_text;
|
pub use self::json_rpc::parse_json_rpc_response_text;
|
||||||
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
||||||
pub use self::json_rpc::parse_json_rpc_response_value;
|
pub use self::json_rpc::parse_json_rpc_response_value;
|
||||||
/// Result of one logical endpoint selection.
|
|
||||||
pub use self::pool::HttpEndpointSelection;
|
|
||||||
/// Runtime admission permit for one HTTP request.
|
|
||||||
pub use self::pool::HttpRequestPermit;
|
|
||||||
/// Shareable logical HTTP endpoint pool with priority routing, admission limits and bounded deadlines.
|
|
||||||
pub use self::pool::HttpTransportPool;
|
|
||||||
/// Safe snapshot of the logical HTTP endpoint pool.
|
|
||||||
pub use self::pool::HttpTransportPoolSnapshot;
|
|
||||||
/// Dispatch knowledge used to prevent ambiguous automatic resubmission.
|
|
||||||
pub use self::resilience::HttpDispatchState;
|
|
||||||
/// Transport-level cause considered by the bounded retry policy.
|
|
||||||
pub use self::resilience::HttpRetryCause;
|
|
||||||
/// Result of evaluating one bounded transport retry opportunity.
|
|
||||||
pub use self::resilience::HttpRetryDecision;
|
|
||||||
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
|
||||||
pub use self::resilience::evaluate_transport_retry;
|
|
||||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||||
pub use self::rpc_accounts::SolanaAccount;
|
pub use self::rpc_accounts::SolanaAccount;
|
||||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||||
@@ -304,26 +493,6 @@ pub use self::rpc_transactions::SolanaTransactionEncoding;
|
|||||||
pub use self::rpc_transactions::SolanaTransactionVersion;
|
pub use self::rpc_transactions::SolanaTransactionVersion;
|
||||||
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
|
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
|
||||||
pub use self::rpc_transactions::SolanaWireField;
|
pub use self::rpc_transactions::SolanaWireField;
|
||||||
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
|
||||||
pub use self::settings::HttpClusterName;
|
|
||||||
/// Runtime settings for one role declared by an HTTP endpoint.
|
|
||||||
pub use self::settings::HttpEndpointRoleSettings;
|
|
||||||
/// Runtime settings for one named Solana HTTP endpoint.
|
|
||||||
pub use self::settings::HttpEndpointSettings;
|
|
||||||
/// Runtime HTTP endpoint URL with redacted diagnostics.
|
|
||||||
pub use self::settings::HttpEndpointUrl;
|
|
||||||
/// Open provider descriptor used by HTTP endpoint settings.
|
|
||||||
pub use self::settings::HttpProviderName;
|
|
||||||
/// Open request-kind descriptor used by logical endpoint capabilities.
|
|
||||||
pub use self::settings::HttpRequestKind;
|
|
||||||
/// Bounded retry settings owned by the HTTP transport runtime.
|
|
||||||
pub use self::settings::HttpRetrySettings;
|
|
||||||
/// Local limits attached to one logical HTTP endpoint role.
|
|
||||||
pub use self::settings::HttpRoleLimits;
|
|
||||||
/// Open logical endpoint role descriptor.
|
|
||||||
pub use self::settings::HttpRoleName;
|
|
||||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
|
||||||
pub use self::settings::HttpTransportSettings;
|
|
||||||
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
|
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
|
||||||
pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
||||||
/// One `programNotification` payload preserving contextual and non-contextual upstream forms.
|
/// One `programNotification` payload preserving contextual and non-contextual upstream forms.
|
||||||
@@ -411,18 +580,24 @@ pub use self::ws_transactions::SolanaSignatureSubscribeConfig;
|
|||||||
|
|
||||||
/// Owning tracing target for events emitted by the on-chain transport crate.
|
/// Owning tracing target for events emitted by the on-chain transport crate.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
/// Internal Yellowstone Subscribe session opener used by the physical channel.
|
||||||
|
pub(crate) use self::grpc_stream::open_yellowstone_subscribe_session;
|
||||||
|
/// Internal Yellowstone Subscribe request wire conversion shared with the stream engine.
|
||||||
|
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_request_to_wire;
|
||||||
|
/// Internal Yellowstone Subscribe update decoder shared with the stream engine.
|
||||||
|
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_update_from_wire;
|
||||||
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
|
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
|
||||||
pub(crate) use self::resilience::HttpConcurrencyPermit;
|
pub(crate) use self::http_resilience::HttpConcurrencyPermit;
|
||||||
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
|
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
|
||||||
pub(crate) use self::resilience::HttpRoleRuntime;
|
pub(crate) use self::http_resilience::HttpRoleRuntime;
|
||||||
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
|
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
|
||||||
pub(crate) use self::resilience::RoleAdmissionAttempt;
|
pub(crate) use self::http_resilience::RoleAdmissionAttempt;
|
||||||
|
/// Validates endpoint settings.
|
||||||
|
pub(crate) use self::http_settings::validate_endpoint_settings;
|
||||||
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
||||||
pub(crate) use self::rpc_common::decode_wire_json;
|
pub(crate) use self::rpc_common::decode_wire_json;
|
||||||
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
||||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||||
/// Validates endpoint settings.
|
|
||||||
pub(crate) use self::settings::validate_endpoint_settings;
|
|
||||||
/// Crate-internal command surface shared by the physical session and typed subscription handle.
|
/// Crate-internal command surface shared by the physical session and typed subscription handle.
|
||||||
pub(crate) use self::ws_session::WsSessionCommand;
|
pub(crate) use self::ws_session::WsSessionCommand;
|
||||||
/// Crate-internal notification dispatch result.
|
/// Crate-internal notification dispatch result.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
/// Commitment level accepted by typed Solana HTTP and WebSocket adapters.
|
/// Commitment level accepted by typed Solana HTTP, WebSocket and Yellowstone gRPC adapters.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum SolanaCommitment {
|
pub enum SolanaCommitment {
|
||||||
/// Query the most recent processed bank.
|
/// Query the most recent processed bank.
|
||||||
|
|||||||
@@ -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: 38
|
// version: 48
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -760,3 +760,255 @@ fn public_v0_2_8_pre_009_helius_slots_updates_surface_reuses_shared_typed_contra
|
|||||||
let _shared_slot_update = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotUpdate>();
|
let _shared_slot_update = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotUpdate>();
|
||||||
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates");
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root() {
|
||||||
|
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000")
|
||||||
|
.expect("public Yellowstone gRPC URL parser must accept HTTP fixture endpoint");
|
||||||
|
let session = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default();
|
||||||
|
assert!(session.validate().is_ok());
|
||||||
|
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"fixture",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
url,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
let settings = ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings::new(std::vec![endpoint.clone()]);
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||||
|
assert!(channel.is_ok());
|
||||||
|
let channel = match channel {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(channel.endpoint_name(), "fixture");
|
||||||
|
assert_eq!(channel.provider().as_str(), "fixture-provider");
|
||||||
|
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.domain(), "onchain_transport");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.code(), "grpc_channel_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_003_yellowstone_metadata_and_seven_unary_contracts_are_available_from_crate_root() {
|
||||||
|
let public = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture");
|
||||||
|
assert!(public.is_ok());
|
||||||
|
let secret = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "secret");
|
||||||
|
assert!(secret.is_ok());
|
||||||
|
let replay = ksp_onchain_transport_lib::YellowstoneReplayInfo::new(std::option::Option::Some(1));
|
||||||
|
assert_eq!(replay.first_available(), std::option::Option::Some(1));
|
||||||
|
let pong = ksp_onchain_transport_lib::YellowstonePong::new(2);
|
||||||
|
assert_eq!(pong.count(), 2);
|
||||||
|
let latest = ksp_onchain_transport_lib::YellowstoneLatestBlockhash::new(3, "hash".to_owned(), 4);
|
||||||
|
assert_eq!(latest.slot(), 3);
|
||||||
|
assert_eq!(latest.blockhash(), "hash");
|
||||||
|
assert_eq!(latest.last_valid_block_height(), 4);
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::YellowstoneBlockHeight::new(5).block_height(), 5);
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::YellowstoneSlot::new(6).slot(), 6);
|
||||||
|
let validity = ksp_onchain_transport_lib::YellowstoneBlockhashValidity::new(7, true);
|
||||||
|
assert_eq!(validity.slot(), 7);
|
||||||
|
assert!(validity.valid());
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::YellowstoneVersionInfo::new("v".to_owned()).version(), "v");
|
||||||
|
let _replay_info = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::subscribe_replay_info;
|
||||||
|
let _ping = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::ping;
|
||||||
|
let _latest_blockhash = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_latest_blockhash;
|
||||||
|
let _block_height = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_block_height;
|
||||||
|
let _slot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_slot;
|
||||||
|
async fn call_is_blockhash_valid(
|
||||||
|
client: &ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient,
|
||||||
|
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneBlockhashValidity> {
|
||||||
|
return client.is_blockhash_valid("fixture-blockhash", std::option::Option::None).await;
|
||||||
|
}
|
||||||
|
let _is_blockhash_valid = call_is_blockhash_valid;
|
||||||
|
let _version = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_version;
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.domain(), "onchain_transport");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.code(), "grpc_status");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_004_yellowstone_subscribe_common_contract_is_available_from_crate_root() {
|
||||||
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
let account_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("accounts").expect("account filter name must validate");
|
||||||
|
let slot_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots").expect("slot filter name must validate");
|
||||||
|
let transaction_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transactions").expect("transaction filter name must validate");
|
||||||
|
let transaction_status_name =
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transaction-status").expect("transaction-status filter name must validate");
|
||||||
|
let block_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("block filter name must validate");
|
||||||
|
let block_meta_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks-meta").expect("block-meta filter name must validate");
|
||||||
|
let entry_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("entry filter name must validate");
|
||||||
|
assert!(request.insert_account_filter(account_name, ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_slot_filter(slot_name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_transaction_filter(transaction_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()).is_ok());
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.insert_transaction_status_filter(transaction_status_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new())
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(request.insert_block_filter(block_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_blocks_meta_filter(block_meta_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_entry_filter(entry_name, ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new()).is_ok());
|
||||||
|
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||||
|
let slice = ksp_onchain_transport_lib::YellowstoneAccountsDataSlice::new(4, 32).expect("account-data slice must validate");
|
||||||
|
assert!(request.push_accounts_data_slice(slice).is_ok());
|
||||||
|
request.set_ping(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
|
||||||
|
request.set_from_slot(std::option::Option::Some(99));
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
assert_eq!(request.account_filter_count(), 1);
|
||||||
|
assert_eq!(request.slot_filter_count(), 1);
|
||||||
|
assert_eq!(request.transaction_filter_count(), 1);
|
||||||
|
assert_eq!(request.transaction_status_filter_count(), 1);
|
||||||
|
assert_eq!(request.block_filter_count(), 1);
|
||||||
|
assert_eq!(request.blocks_meta_filter_count(), 1);
|
||||||
|
assert_eq!(request.entry_filter_count(), 1);
|
||||||
|
assert_eq!(request.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||||
|
assert_eq!(request.accounts_data_slices(), &[slice]);
|
||||||
|
assert_eq!(request.ping(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
|
||||||
|
assert_eq!(request.from_slot(), std::option::Option::Some(99));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_005_yellowstone_accounts_slots_contract_is_available_from_crate_root() {
|
||||||
|
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
|
||||||
|
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
|
||||||
|
let mut accounts = ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new();
|
||||||
|
assert!(accounts.push_account(account).is_ok());
|
||||||
|
assert!(accounts.push_owner(owner).is_ok());
|
||||||
|
let memcmp = ksp_onchain_transport_lib::YellowstoneAccountMemcmp::bytes(0, vec![1_u8, 2, 3]).expect("public memcmp must validate");
|
||||||
|
assert!(accounts.push_filter(ksp_onchain_transport_lib::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
|
||||||
|
accounts.set_nonempty_txn_signature(std::option::Option::Some(true));
|
||||||
|
let cuckoo =
|
||||||
|
ksp_onchain_transport_lib::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 1, ksp_onchain_transport_lib::YellowstoneCuckooHashAlgorithm::SipHash)
|
||||||
|
.expect("public cuckoo filter must validate");
|
||||||
|
accounts.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
|
||||||
|
let mut slots = ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new();
|
||||||
|
slots.set_filter_by_commitment(std::option::Option::Some(true));
|
||||||
|
slots.set_interslot_updates(std::option::Option::Some(true));
|
||||||
|
let timestamp = ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1, 2).expect("public timestamp must validate");
|
||||||
|
let signature = ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([3_u8; 64]);
|
||||||
|
assert_eq!(timestamp.nanos(), 2);
|
||||||
|
assert_eq!(signature.as_bytes(), &[3_u8; 64]);
|
||||||
|
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(true));
|
||||||
|
let _account_info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountInfo>();
|
||||||
|
let _account_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
|
||||||
|
let _slot_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
|
||||||
|
let _slot_status = ksp_onchain_transport_lib::YellowstoneSlotStatus::FirstShredReceived;
|
||||||
|
let _lamports = ksp_onchain_transport_lib::YellowstoneAccountLamportsFilter::Gt(10);
|
||||||
|
let _encoding = ksp_onchain_transport_lib::YellowstoneAccountMemcmpEncoding::Bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_007_yellowstone_transactions_contract_is_available_from_crate_root() {
|
||||||
|
let mut filter = ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new();
|
||||||
|
filter.set_vote(std::option::Option::Some(false));
|
||||||
|
filter.set_failed(std::option::Option::Some(true));
|
||||||
|
let selector = ksp_onchain_transport_lib::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
|
||||||
|
filter.set_signature(std::option::Option::Some(selector));
|
||||||
|
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
|
||||||
|
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
|
||||||
|
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
|
||||||
|
filter.set_token_accounts(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
|
||||||
|
assert_eq!(filter.token_accounts(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
|
||||||
|
let _hash = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneHashBytes>();
|
||||||
|
let _instruction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneCompiledInstruction>();
|
||||||
|
let _lookup = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneMessageAddressTableLookup>();
|
||||||
|
let _config = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionConfig>();
|
||||||
|
let _message = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionMessage>();
|
||||||
|
let _stored = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneStoredTransaction>();
|
||||||
|
let _meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusMeta>();
|
||||||
|
let _info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionInfo>();
|
||||||
|
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
|
||||||
|
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
|
||||||
|
let _error = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionError>();
|
||||||
|
let _inner = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneInnerInstructions>();
|
||||||
|
let _token = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTokenBalance>();
|
||||||
|
let _return_data = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReturnData>();
|
||||||
|
let _reward = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReward>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_008_yellowstone_blocks_contract_is_available_from_crate_root() {
|
||||||
|
let account = ksp_core_lib::Pubkey::new_from_array([71_u8; 32]);
|
||||||
|
let mut block = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new();
|
||||||
|
assert!(block.push_account_include(account).is_ok());
|
||||||
|
block.set_include_transactions(std::option::Option::Some(true));
|
||||||
|
block.set_include_accounts(std::option::Option::Some(false));
|
||||||
|
block.set_include_entries(std::option::Option::Some(true));
|
||||||
|
assert_eq!(block.account_include(), &[account]);
|
||||||
|
assert_eq!(block.include_transactions(), std::option::Option::Some(true));
|
||||||
|
assert_eq!(block.include_accounts(), std::option::Option::Some(false));
|
||||||
|
assert_eq!(block.include_entries(), std::option::Option::Some(true));
|
||||||
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.insert_block_filter(ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("filter name must validate"), block,)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.insert_blocks_meta_filter(
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("block-meta").expect("filter name must validate"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new(),
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.insert_entry_filter(
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("filter name must validate"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new(),
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert_eq!(request.block_filter_count(), 1);
|
||||||
|
assert_eq!(request.blocks_meta_filter_count(), 1);
|
||||||
|
assert_eq!(request.entry_filter_count(), 1);
|
||||||
|
let _block_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockUpdate>();
|
||||||
|
let _block_meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockMetaUpdate>();
|
||||||
|
let _block_rewards = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockRewards>();
|
||||||
|
let _entry_info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryInfo>();
|
||||||
|
let _entry_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryUpdate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_009_yellowstone_bidi_session_contract_is_available_from_crate_root() {
|
||||||
|
fn assert_send<T: Send>() {
|
||||||
|
let _ = std::marker::PhantomData::<T>;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert_send::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
|
||||||
|
assert_send::<ksp_onchain_transport_lib::YellowstoneSubscribeUpdate>();
|
||||||
|
let _open = ksp_onchain_transport_lib::YellowstoneGrpcChannel::open_standard_subscribe;
|
||||||
|
let _session = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
|
||||||
|
let _ping = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribePingUpdate>();
|
||||||
|
let _pong = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribePongUpdate>();
|
||||||
|
let _states = [
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Active,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Closing,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Closed,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Failed,
|
||||||
|
];
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW.domain(), "onchain_transport");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW.code(), "grpc_backpressure_overflow");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.domain(), "onchain_transport");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.code(), "grpc_session_closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_root() {
|
||||||
|
fn assert_copy<T: Copy>() {
|
||||||
|
let _ = std::marker::PhantomData::<T>;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert_copy::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||||
|
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||||
|
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
|
||||||
|
let _session_snapshot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession::snapshot;
|
||||||
|
let _reconnect_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::reconnect_count;
|
||||||
|
let _gap_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::continuity_gap_count;
|
||||||
|
let _duplicate_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::duplicate_update_count;
|
||||||
|
let _replay_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_attempt_count;
|
||||||
|
let _requested = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_requested_from_slot;
|
||||||
|
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
|
||||||
|
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
// version: 32
|
// version: 41
|
||||||
|
|
||||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||||
|
|
||||||
@@ -996,3 +996,351 @@ fn release_v0_2_8_pre_010_live_smoke_policy_preserves_secret_and_dependency_owne
|
|||||||
let env_example = std::fs::read_to_string(workspace.join(".env.example")).expect(".env.example must be readable");
|
let env_example = std::fs::read_to_string(workspace.join(".env.example")).expect(".env.example must be readable");
|
||||||
assert!(env_example.contains("KSP_SECRET_HELIUS_API_KEY"));
|
assert!(env_example.contains("KSP_SECRET_HELIUS_API_KEY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_002_materializes_minimal_yellowstone_engine_without_provider_or_ws_coupling() {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport test must resolve workspace root");
|
||||||
|
let root_manifest = std::fs::read_to_string(workspace.join("Cargo.toml")).expect("workspace manifest must be readable");
|
||||||
|
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
|
||||||
|
let settings_source = include_str!("../src/grpc_settings.rs");
|
||||||
|
let channel_source = include_str!("../src/grpc_channel.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
assert!(root_manifest.contains("tonic = { version = \"^0.14\", default-features = false }"));
|
||||||
|
assert!(root_manifest.contains("yellowstone-grpc-proto = { version = \"^12.6\", default-features = false }"));
|
||||||
|
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\""));
|
||||||
|
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
|
||||||
|
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
|
||||||
|
assert!(!transport_manifest.contains("ksp-config-lib"));
|
||||||
|
assert!(settings_source.contains("YellowstoneGrpcEndpointUrl"));
|
||||||
|
assert!(settings_source.contains("YellowstoneGrpcSessionSettings"));
|
||||||
|
assert!(settings_source.contains("YellowstoneGrpcTransportSettings"));
|
||||||
|
assert!(!settings_source.contains("WsProtocolKind"));
|
||||||
|
assert!(!settings_source.contains("KSP_SECRET_"));
|
||||||
|
assert!(channel_source.contains("tonic::transport::Endpoint::from_shared"));
|
||||||
|
assert!(channel_source.contains("connect_lazy"));
|
||||||
|
assert!(!channel_source.contains("WsSession"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _published_wire = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_methods_without_subscribe() {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport test must resolve workspace root");
|
||||||
|
let root_manifest = std::fs::read_to_string(workspace.join("Cargo.toml")).expect("workspace manifest must be readable");
|
||||||
|
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
|
||||||
|
let settings_source = include_str!("../src/grpc_settings.rs");
|
||||||
|
let channel_source = include_str!("../src/grpc_channel.rs");
|
||||||
|
let unary_source = include_str!("../src/grpc_unary.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
assert!(root_manifest.contains("http = { version = \"^1.5\", default-features = false }"));
|
||||||
|
assert!(root_manifest.contains("tonic-prost = { version = \"^0.14\", default-features = false }"));
|
||||||
|
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\", \"tls-aws-lc\", \"tls-webpki-roots\"] }"));
|
||||||
|
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"));
|
||||||
|
assert!(transport_manifest.contains("tonic-prost.workspace = true"));
|
||||||
|
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
|
||||||
|
assert!(transport_manifest.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"));
|
||||||
|
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
|
||||||
|
assert!(settings_source.contains("YellowstoneGrpcMetadataEntry"));
|
||||||
|
assert!(settings_source.contains("set_sensitive"));
|
||||||
|
assert!(channel_source.contains("ClientTlsConfig"));
|
||||||
|
assert!(channel_source.contains("with_webpki_roots"));
|
||||||
|
assert!(channel_source.contains("pub async fn connect"));
|
||||||
|
for path in [
|
||||||
|
"/geyser.Geyser/SubscribeReplayInfo",
|
||||||
|
"/geyser.Geyser/Ping",
|
||||||
|
"/geyser.Geyser/GetLatestBlockhash",
|
||||||
|
"/geyser.Geyser/GetBlockHeight",
|
||||||
|
"/geyser.Geyser/GetSlot",
|
||||||
|
"/geyser.Geyser/IsBlockhashValid",
|
||||||
|
"/geyser.Geyser/GetVersion",
|
||||||
|
] {
|
||||||
|
assert!(unary_source.contains(path), "missing standard Yellowstone unary path: {path}");
|
||||||
|
}
|
||||||
|
assert!(!unary_source.contains("const PATH_SUBSCRIBE: "));
|
||||||
|
assert!(!unary_source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!unary_source.contains("PublicNode"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _client = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() {
|
||||||
|
let source = include_str!("../src/grpc_subscribe.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for wire_field in [
|
||||||
|
"accounts:",
|
||||||
|
"slots:",
|
||||||
|
"transactions:",
|
||||||
|
"transactions_status:",
|
||||||
|
"blocks:",
|
||||||
|
"blocks_meta:",
|
||||||
|
"entry:",
|
||||||
|
"commitment:",
|
||||||
|
"accounts_data_slice:",
|
||||||
|
"ping:",
|
||||||
|
"from_slot:",
|
||||||
|
] {
|
||||||
|
assert!(source.contains(wire_field), "missing standard Yellowstone SubscribeRequest field: {wire_field}");
|
||||||
|
}
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT"));
|
||||||
|
assert!(source.contains("offset.checked_add(length)"));
|
||||||
|
assert!(source.contains("globally unique"));
|
||||||
|
assert!(source.contains("SubscribeRequestFilterBlocksMeta"));
|
||||||
|
assert!(source.contains("SubscribeRequestFilterEntry"));
|
||||||
|
assert!(source.contains("YellowstoneSubscribeAccountFilter"));
|
||||||
|
assert!(source.contains("YellowstoneSubscribeSlotFilter"));
|
||||||
|
assert!(source.contains("YellowstoneSubscribeTransactionFilter"));
|
||||||
|
assert!(source.contains("YellowstoneSubscribeBlockFilter"));
|
||||||
|
assert!(!source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!source.contains("PublicNode"));
|
||||||
|
assert!(!source.contains("OrbitFlare"));
|
||||||
|
assert!(!source.contains("Helius"));
|
||||||
|
assert!(!source.contains("pub fn to_wire"));
|
||||||
|
assert!(!source.contains("pub(crate) fn to_wire"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _request = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeRequest>();
|
||||||
|
let _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter>();
|
||||||
|
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter>();
|
||||||
|
let _transaction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
|
||||||
|
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_005_accounts_and_slots_contract_remains_complete() {
|
||||||
|
let source = include_str!("../src/grpc_subscribe.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for required in [
|
||||||
|
"account: self.accounts",
|
||||||
|
"owner: self.owners",
|
||||||
|
"filters: self.filters",
|
||||||
|
"nonempty_txn_signature",
|
||||||
|
"cuckoo_accounts_filter",
|
||||||
|
"SubscribeRequestFilterAccountsFilterMemcmp",
|
||||||
|
"DataSize",
|
||||||
|
"TokenAccountState",
|
||||||
|
"YellowstoneAccountLamportsFilter",
|
||||||
|
"filter_by_commitment",
|
||||||
|
"interslot_updates",
|
||||||
|
"YellowstoneAccountUpdate",
|
||||||
|
"YellowstoneSlotUpdate",
|
||||||
|
"YellowstoneTransactionSignature",
|
||||||
|
"SlotFirstShredReceived",
|
||||||
|
"SlotCompleted",
|
||||||
|
"SlotCreatedBank",
|
||||||
|
"SlotDead",
|
||||||
|
"dead_error",
|
||||||
|
] {
|
||||||
|
assert!(source.contains(required), "missing pre.005 Accounts/Slots contract token: {required}");
|
||||||
|
}
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_CUCKOO_DATA_LENGTH_BYTES"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES"));
|
||||||
|
assert!(source.contains("MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES"));
|
||||||
|
assert!(source.contains("fn decode_account_update"));
|
||||||
|
assert!(source.contains("fn decode_slot_update"));
|
||||||
|
assert!(!source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!source.contains("PublicNode"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
|
||||||
|
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules() {
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
let http_client = include_str!("../src/http_client.rs");
|
||||||
|
let http_executor = include_str!("../src/http_executor.rs");
|
||||||
|
let http_pool = include_str!("../src/http_pool.rs");
|
||||||
|
let http_resilience = include_str!("../src/http_resilience.rs");
|
||||||
|
let http_settings = include_str!("../src/http_settings.rs");
|
||||||
|
for declaration in ["mod http_client;", "mod http_executor;", "mod http_pool;", "mod http_resilience;", "mod http_settings;"] {
|
||||||
|
assert!(crate_root.contains(declaration), "missing HTTP-owned private module declaration: {declaration}");
|
||||||
|
}
|
||||||
|
for historical in ["mod client;", "mod executor;", "mod pool;", "mod resilience;", "mod settings;"] {
|
||||||
|
assert!(!crate_root.contains(historical), "historical ambiguous private module declaration remains: {historical}");
|
||||||
|
}
|
||||||
|
assert!(http_client.contains("HttpEndpointClient"));
|
||||||
|
assert!(http_executor.contains("execute_standard_rpc"));
|
||||||
|
assert!(http_pool.contains("HttpTransportPool"));
|
||||||
|
assert!(http_resilience.contains("HttpRetryDecision"));
|
||||||
|
assert!(http_settings.contains("HttpTransportSettings"));
|
||||||
|
// Shared/protocol-oriented modules deliberately keep their existing names.
|
||||||
|
for shared in ["mod constants;", "mod error;", "mod json_rpc;", "mod rpc_common;", "mod rpc_accounts;", "mod rpc_blocks;", "mod rpc_transactions;"] {
|
||||||
|
assert!(crate_root.contains(shared), "shared/protocol module was incorrectly HTTP-prefixed: {shared}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_007_transactions_contract_remains_complete() {
|
||||||
|
let source = include_str!("../src/grpc_subscribe.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for required in [
|
||||||
|
"YellowstoneSubscribeTransactionFilter",
|
||||||
|
"YellowstoneTransactionSignatureSelector",
|
||||||
|
"YellowstoneTokenAccountExpansion",
|
||||||
|
"account_include",
|
||||||
|
"account_exclude",
|
||||||
|
"account_required",
|
||||||
|
"cuckoo_account_include",
|
||||||
|
"token_accounts",
|
||||||
|
"YellowstoneTransactionUpdate",
|
||||||
|
"YellowstoneTransactionStatusUpdate",
|
||||||
|
"YellowstoneTransactionStatusMeta",
|
||||||
|
"YellowstoneTransactionMessage",
|
||||||
|
"YellowstoneMessageAddressTableLookup",
|
||||||
|
"YellowstoneTransactionConfig",
|
||||||
|
"compute_units_consumed",
|
||||||
|
"cost_units",
|
||||||
|
"inner_instructions_none",
|
||||||
|
"log_messages_none",
|
||||||
|
"return_data_none",
|
||||||
|
"decode_transaction_update",
|
||||||
|
"decode_transaction_status_update",
|
||||||
|
] {
|
||||||
|
assert!(source.contains(required), "missing pre.007 Transactions contract token: {required}");
|
||||||
|
}
|
||||||
|
assert!(source.contains("base58_decoded_length"));
|
||||||
|
assert!(source.contains("YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES"));
|
||||||
|
assert!(!source.contains("pub async fn subscribe("));
|
||||||
|
assert!(!source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!source.contains("PublicNode"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _filter = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
|
||||||
|
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
|
||||||
|
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_008_standard_blocks_contract_remains_complete() {
|
||||||
|
let source = include_str!("../src/grpc_subscribe.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for required in [
|
||||||
|
"YellowstoneSubscribeBlockFilter",
|
||||||
|
"account_include",
|
||||||
|
"include_transactions",
|
||||||
|
"include_accounts",
|
||||||
|
"include_entries",
|
||||||
|
"cuckoo_account_include",
|
||||||
|
"YellowstoneBlockRewards",
|
||||||
|
"YellowstoneBlockUpdate",
|
||||||
|
"YellowstoneBlockMetaUpdate",
|
||||||
|
"YellowstoneEntryInfo",
|
||||||
|
"YellowstoneEntryUpdate",
|
||||||
|
"executed_transaction_count",
|
||||||
|
"updated_account_count",
|
||||||
|
"entries_count",
|
||||||
|
"starting_transaction_index",
|
||||||
|
"decode_block_update",
|
||||||
|
"decode_block_meta_update",
|
||||||
|
"decode_entry_update",
|
||||||
|
"decode_block_rewards",
|
||||||
|
] {
|
||||||
|
assert!(source.contains(required), "missing pre.008 Blocks contract token: {required}");
|
||||||
|
}
|
||||||
|
assert!(source.contains("MAX_GRPC_BLOCK_VECTOR_COUNT"));
|
||||||
|
assert!(source.contains("base58_decoded_length(value.as_str())"));
|
||||||
|
assert!(source.contains("std::vec::Vec<crate::YellowstoneTransactionInfo>"));
|
||||||
|
assert!(source.contains("std::vec::Vec<crate::YellowstoneAccountInfo>"));
|
||||||
|
assert!(source.contains("std::vec::Vec<crate::YellowstoneEntryInfo>"));
|
||||||
|
assert!(source.contains("fn decode_block_update"));
|
||||||
|
assert!(!source.contains("pub async fn subscribe("));
|
||||||
|
assert!(!source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!source.contains("PublicNode"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockUpdate>();
|
||||||
|
let _meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockMetaUpdate>();
|
||||||
|
let _entry = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryUpdate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_009_bounded_standard_bidi_session_contract_remains_complete() {
|
||||||
|
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||||
|
let subscribe_source = include_str!("../src/grpc_subscribe.rs");
|
||||||
|
let channel_source = include_str!("../src/grpc_channel.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for required in [
|
||||||
|
"const PATH_SUBSCRIBE: &str = \"/geyser.Geyser/Subscribe\"",
|
||||||
|
".streaming(",
|
||||||
|
"tokio::sync::mpsc::channel",
|
||||||
|
"tokio::sync::watch::channel",
|
||||||
|
"try_send",
|
||||||
|
"AUTO_SUBSCRIBE_PING_ID",
|
||||||
|
"ping_request_wire",
|
||||||
|
"finish_client_half_close",
|
||||||
|
"timeout_at",
|
||||||
|
"max_decoding_message_size",
|
||||||
|
"max_encoding_message_size",
|
||||||
|
"YellowstoneGrpcSubscribeState",
|
||||||
|
"SolanaYellowstoneGrpcSubscribeSession",
|
||||||
|
] {
|
||||||
|
assert!(stream_source.contains(required), "missing pre.009 bidi runtime token: {required}");
|
||||||
|
}
|
||||||
|
for required in [
|
||||||
|
"YellowstoneSubscribeUpdate",
|
||||||
|
"YellowstoneSubscribePingUpdate",
|
||||||
|
"YellowstoneSubscribePongUpdate",
|
||||||
|
"yellowstone_subscribe_request_to_wire",
|
||||||
|
"yellowstone_subscribe_update_from_wire",
|
||||||
|
] {
|
||||||
|
assert!(subscribe_source.contains(required), "missing pre.009 runtime wire bridge token: {required}");
|
||||||
|
}
|
||||||
|
assert!(channel_source.contains("pub async fn open_standard_subscribe"));
|
||||||
|
assert!(!stream_source.contains("unbounded_channel"));
|
||||||
|
assert!(!stream_source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!stream_source.contains("PublicNode"));
|
||||||
|
assert!(!stream_source.contains("OrbitFlare"));
|
||||||
|
assert!(!stream_source.contains("Helius"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _session = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
|
||||||
|
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Active;
|
||||||
|
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeUpdate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_9_pre_010_adds_bounded_reconnect_replay_and_conservative_continuity_observability() {
|
||||||
|
let stream_source = include_str!("../src/grpc_stream.rs");
|
||||||
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
|
for required in [
|
||||||
|
"YellowstoneGrpcSubscribeState::Reconnecting",
|
||||||
|
"YellowstoneGrpcSubscribeSnapshot",
|
||||||
|
"reconnect_subscribe_stream",
|
||||||
|
"replay_first_available",
|
||||||
|
"SubscribeReplayInfo",
|
||||||
|
"last_requested_from_slot",
|
||||||
|
"last_observed_slot",
|
||||||
|
"reconnect_count",
|
||||||
|
"replay_attempt_count",
|
||||||
|
"continuity_gap_count",
|
||||||
|
"duplicate_update_count",
|
||||||
|
"MAX_RECENT_UPDATE_IDENTITIES",
|
||||||
|
"first_available > requested",
|
||||||
|
"max_optional_slot",
|
||||||
|
"reconnect budget is exhausted",
|
||||||
|
"request mutation",
|
||||||
|
] {
|
||||||
|
assert!(stream_source.contains(required), "missing pre.010 reconnect/replay contract token: {required}");
|
||||||
|
}
|
||||||
|
assert!(stream_source.contains("still delivered to the caller"));
|
||||||
|
assert!(stream_source.contains("does not claim exactly-once delivery"));
|
||||||
|
assert!(!stream_source.contains("unbounded_channel"));
|
||||||
|
assert!(!stream_source.contains("SubscribeDeshred"));
|
||||||
|
assert!(!stream_source.contains("PublicNode"));
|
||||||
|
assert!(!stream_source.contains("OrbitFlare"));
|
||||||
|
assert!(!stream_source.contains("Helius"));
|
||||||
|
assert!(!crate_root.contains("pub use tonic"));
|
||||||
|
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||||
|
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
|
||||||
|
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
// version: 5
|
||||||
|
|
||||||
|
//! Opt-in live PublicNode Mainnet/Testnet smokes for authenticated provider-neutral Yellowstone gRPC Subscribe.
|
||||||
|
|
||||||
|
use std::io::IsTerminal; // rust-rules: trait-import
|
||||||
|
|
||||||
|
struct PublicNodeTokens {
|
||||||
|
mainnet: std::string::String,
|
||||||
|
testnet: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PUBLICNODE_X_TOKENS: std::sync::OnceLock<PublicNodeTokens> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn read_token_line(label: &str) -> std::string::String {
|
||||||
|
let mut token = std::string::String::new();
|
||||||
|
match std::io::stdin().read_line(&mut token) {
|
||||||
|
std::result::Result::Ok(_) => {},
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode {label} x-token must be readable from smoke stdin: {error}"),
|
||||||
|
}
|
||||||
|
let token = token.trim().to_owned();
|
||||||
|
assert!(!token.is_empty(), "PublicNode {label} x-token provided on smoke stdin must not be empty");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publicnode_x_tokens() -> &'static PublicNodeTokens {
|
||||||
|
return PUBLICNODE_X_TOKENS.get_or_init(|| {
|
||||||
|
assert!(
|
||||||
|
!std::io::stdin().is_terminal(),
|
||||||
|
"pipe two PublicNode personal x-tokens to this ignored smoke on stdin: Mainnet first, Testnet second; never pass them as command-line arguments"
|
||||||
|
);
|
||||||
|
return PublicNodeTokens { mainnet: read_token_line("Mainnet"), testnet: read_token_line("Testnet") };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publicnode_endpoint(
|
||||||
|
name: &str,
|
||||||
|
cluster: &str,
|
||||||
|
url: &str,
|
||||||
|
x_token: &str,
|
||||||
|
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||||
|
let 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),
|
||||||
|
};
|
||||||
|
let session = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::new(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcReconnectSettings::new(0, std::time::Duration::from_millis(250), std::time::Duration::from_secs(2)),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
16 * 1024 * 1024,
|
||||||
|
4 * 1024 * 1024,
|
||||||
|
);
|
||||||
|
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
name,
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(cluster),
|
||||||
|
url,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
let metadata = match ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-token", x_token) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return endpoint.with_metadata(vec![metadata]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slot_request() -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneSubscribeRequest> {
|
||||||
|
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
|
||||||
|
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
match request.insert_slot_filter(name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()) {
|
||||||
|
std::result::Result::Ok(()) => {},
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_publicnode_slot_stream(name: &str, cluster: &str, url: &str, x_token: &str) {
|
||||||
|
let endpoint = match publicnode_endpoint(name, cluster, url, x_token) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("programmatic PublicNode Yellowstone settings must accept secret x-token metadata: {error:?}"),
|
||||||
|
};
|
||||||
|
let endpoint_debug = format!("{endpoint:?}");
|
||||||
|
assert!(!endpoint_debug.contains(x_token), "PublicNode x-token must not appear in endpoint Debug");
|
||||||
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode Yellowstone TLS connection must succeed: {error:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(channel.endpoint_name(), name);
|
||||||
|
assert_eq!(channel.provider().as_str(), "publicnode");
|
||||||
|
assert_eq!(channel.cluster().as_str(), cluster);
|
||||||
|
let request = match slot_request() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode Yellowstone slot request must be valid: {error:?}"),
|
||||||
|
};
|
||||||
|
let mut session = match channel.open_standard_subscribe(request).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode Yellowstone authenticated Subscribe must open: {error:?}"),
|
||||||
|
};
|
||||||
|
let slot_result = tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||||
|
loop {
|
||||||
|
let next_update = match session.next_update().await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode Yellowstone Subscribe update must decode: {error:?}"),
|
||||||
|
};
|
||||||
|
match next_update {
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(update)) => return update.slot(),
|
||||||
|
std::option::Option::Some(_) => {},
|
||||||
|
std::option::Option::None => panic!("PublicNode Yellowstone Subscribe ended before a slot update"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let slot = match slot_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("PublicNode Yellowstone Subscribe must publish a slot update before the smoke deadline: {error}"),
|
||||||
|
};
|
||||||
|
assert!(slot > 0);
|
||||||
|
let close_timeout_result = tokio::time::timeout(std::time::Duration::from_secs(7), session.close()).await;
|
||||||
|
let close_result = match close_timeout_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
panic!("KSP Yellowstone Subscribe close must remain bounded beyond the configured five-second provider half-close deadline: {error}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match close_result {
|
||||||
|
std::result::Result::Ok(()) => {},
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
assert_eq!(
|
||||||
|
error.code(),
|
||||||
|
ksp_onchain_transport_lib::ERROR_CODE_TIMEOUT,
|
||||||
|
"after a live slot was observed, PublicNode close may time out waiting for provider half-close but must not fail for another reason: {error:?}"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
#[ignore = "opt-in live PublicNode Mainnet Yellowstone gRPC smoke; reads the first personal x-token line from stdin and performs an external TLS/Subscribe request"]
|
||||||
|
async fn publicnode_mainnet_yellowstone_streams_slots_with_network_scoped_secret_x_token() {
|
||||||
|
let tokens = publicnode_x_tokens();
|
||||||
|
assert_publicnode_slot_stream(
|
||||||
|
"publicnode_mainnet_yellowstone",
|
||||||
|
"mainnet-beta",
|
||||||
|
"https://solana-yellowstone-grpc.publicnode.com:443",
|
||||||
|
tokens.mainnet.as_str(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
#[ignore = "opt-in live PublicNode Testnet Yellowstone gRPC smoke; reads the second personal x-token line from stdin and performs an external TLS/Subscribe request"]
|
||||||
|
async fn publicnode_testnet_yellowstone_streams_slots_with_network_scoped_secret_x_token() {
|
||||||
|
let tokens = publicnode_x_tokens();
|
||||||
|
assert_publicnode_slot_stream(
|
||||||
|
"publicnode_testnet_yellowstone",
|
||||||
|
"testnet",
|
||||||
|
"https://solana-testnet-yellowstone-grpc.publicnode.com:443",
|
||||||
|
tokens.testnet.as_str(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
60
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal file
60
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSettings {
|
||||||
|
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value).expect("fixture Yellowstone gRPC URL must parse");
|
||||||
|
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"fixture",
|
||||||
|
enabled,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
parsed,
|
||||||
|
crate::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() {
|
||||||
|
let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY");
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||||
|
assert!(channel.is_ok());
|
||||||
|
let channel = match channel {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(channel.endpoint_name(), "fixture");
|
||||||
|
assert_eq!(channel.provider().as_str(), "fixture-provider");
|
||||||
|
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||||
|
let rendered = format!("{channel:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("127.0.0.1"));
|
||||||
|
let _client = channel.standard_unary_client();
|
||||||
|
let _wire_type = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_channel_prepare_requires_active_tokio_runtime_without_panicking() {
|
||||||
|
let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY");
|
||||||
|
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let rendered = format!("{result:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("127.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_channel_prepare_rejects_disabled_endpoint_before_network_io() {
|
||||||
|
let endpoint = endpoint(false, "http://127.0.0.1:10000");
|
||||||
|
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn grpc_channel_prepare_configures_https_without_exposing_url() {
|
||||||
|
let endpoint = endpoint(true, "https://example.invalid:443/GRPC-SECRET-CANARY");
|
||||||
|
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let rendered = format!("{result:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("example.invalid"));
|
||||||
|
}
|
||||||
198
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal file
198
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
fn endpoint(name: &str, enabled: bool, url: &str, session: crate::YellowstoneGrpcSessionSettings) -> crate::YellowstoneGrpcEndpointSettings {
|
||||||
|
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture Yellowstone gRPC URL must parse");
|
||||||
|
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
name,
|
||||||
|
enabled,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
parsed,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_endpoint_url_accepts_http_and_https_and_redacts_debug() {
|
||||||
|
for value in ["http://127.0.0.1:10000", "https://yellowstone.example.invalid:443/path?token=GRPC-SECRET-CANARY"] {
|
||||||
|
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value);
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
let parsed = match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
assert_eq!(parsed.as_str(), value);
|
||||||
|
let rendered = format!("{parsed:?}");
|
||||||
|
assert_eq!(rendered, "YellowstoneGrpcEndpointUrl(<redacted>)");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_endpoint_url_rejects_non_http_schemes_without_echoing_secret() {
|
||||||
|
let result = crate::YellowstoneGrpcEndpointUrl::parse("wss://GRPC-SECRET-CANARY@example.invalid/socket");
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = match result {
|
||||||
|
std::result::Result::Ok(_) => return,
|
||||||
|
std::result::Result::Err(error) => error,
|
||||||
|
};
|
||||||
|
let rendered = error.to_string();
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!format!("{error:?}").contains("GRPC-SECRET-CANARY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_endpoint_url_rejects_excessive_length_without_echoing_payload() {
|
||||||
|
let value = format!("https://example.invalid/{}", "S".repeat(super::MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES + 1));
|
||||||
|
let result = crate::YellowstoneGrpcEndpointUrl::parse(value);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = match result {
|
||||||
|
std::result::Result::Ok(_) => return,
|
||||||
|
std::result::Result::Err(error) => error,
|
||||||
|
};
|
||||||
|
assert!(!format!("{error:?}").contains(&"S".repeat(64)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_session_defaults_are_bounded_and_validate() {
|
||||||
|
let settings = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
assert!(!settings.connect_timeout().is_zero());
|
||||||
|
assert!(!settings.unary_timeout().is_zero());
|
||||||
|
assert!(!settings.close_timeout().is_zero());
|
||||||
|
assert!(settings.request_channel_capacity() > 0);
|
||||||
|
assert!(settings.update_channel_capacity() > 0);
|
||||||
|
assert!(settings.max_inbound_message_size_bytes() > 0);
|
||||||
|
assert!(settings.max_outbound_message_size_bytes() > 0);
|
||||||
|
assert!(settings.reconnect().max_retries() <= super::MAX_GRPC_RECONNECT_RETRIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_session_rejects_zero_excessive_and_reversed_runtime_bounds() {
|
||||||
|
let zero = crate::YellowstoneGrpcSessionSettings::new(
|
||||||
|
std::time::Duration::ZERO,
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::default(),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert!(zero.validate().is_err());
|
||||||
|
let excessive = crate::YellowstoneGrpcSessionSettings::new(
|
||||||
|
super::MAX_GRPC_RUNTIME_DURATION + std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(
|
||||||
|
super::MAX_GRPC_RECONNECT_RETRIES + 1,
|
||||||
|
std::time::Duration::from_millis(1),
|
||||||
|
std::time::Duration::from_millis(2),
|
||||||
|
),
|
||||||
|
super::MAX_GRPC_CHANNEL_CAPACITY + 1,
|
||||||
|
1,
|
||||||
|
super::MAX_GRPC_MESSAGE_SIZE_BYTES + 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert!(excessive.validate().is_err());
|
||||||
|
let reversed = crate::YellowstoneGrpcSessionSettings::new(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(5, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert!(reversed.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_transport_settings_validate_unique_enabled_endpoints_and_redact_urls() {
|
||||||
|
let first = endpoint("first", true, "https://GRPC-SECRET-CANARY@example.invalid:443", crate::YellowstoneGrpcSessionSettings::default());
|
||||||
|
let second = endpoint("second", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||||
|
let settings = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), second]);
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
let rendered = format!("{settings:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("example.invalid"));
|
||||||
|
let duplicate = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), first]);
|
||||||
|
assert!(duplicate.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_transport_settings_reject_excessive_descriptor_and_endpoint_count() {
|
||||||
|
let oversized_name = "n".repeat(super::MAX_GRPC_DESCRIPTOR_LENGTH_BYTES + 1);
|
||||||
|
let oversized = endpoint(oversized_name.as_str(), true, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||||
|
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![oversized]).validate().is_err());
|
||||||
|
let parsed = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("fixture Yellowstone gRPC URL must parse");
|
||||||
|
let endpoints = (0..=super::MAX_GRPC_ENDPOINT_COUNT)
|
||||||
|
.map(|index| {
|
||||||
|
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
format!("endpoint-{index}"),
|
||||||
|
true,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
parsed.clone(),
|
||||||
|
crate::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert!(crate::YellowstoneGrpcTransportSettings::new(endpoints).validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_transport_settings_require_one_enabled_endpoint() {
|
||||||
|
let disabled = endpoint("disabled", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
|
||||||
|
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![disabled]).validate().is_err());
|
||||||
|
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![]).validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_metadata_validates_ascii_bounds_sensitivity_and_redacted_debug() {
|
||||||
|
let public = crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "VISIBLE-CANARY").expect("public metadata must validate");
|
||||||
|
let secret = crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("secret metadata must validate");
|
||||||
|
assert_eq!(public.key(), "x-ksp-public");
|
||||||
|
assert!(!public.is_secret());
|
||||||
|
assert!(secret.is_secret());
|
||||||
|
let rendered = format!("{public:?} {secret:?}");
|
||||||
|
assert!(!rendered.contains("VISIBLE-CANARY"));
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||||
|
assert!(secret.append_to(&mut metadata).is_ok());
|
||||||
|
let appended = metadata.get("x-ksp-token").expect("secret metadata must be appended");
|
||||||
|
assert!(appended.is_sensitive());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_metadata_rejects_reserved_binary_uppercase_malformed_and_oversized_values() {
|
||||||
|
assert!(crate::YellowstoneGrpcMetadataEntry::public("grpc-timeout", "1").is_err());
|
||||||
|
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bin", "1").is_err());
|
||||||
|
assert!(crate::YellowstoneGrpcMetadataEntry::public("X-KSP-UPPER", "1").is_err());
|
||||||
|
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bad", "line\nfeed").is_err());
|
||||||
|
assert!(crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-large", "S".repeat(super::MAX_GRPC_METADATA_VALUE_LENGTH_BYTES + 1)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_endpoint_metadata_count_is_bounded_and_url_tracks_tls_scheme() {
|
||||||
|
let http = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("HTTP URL must parse");
|
||||||
|
let https = crate::YellowstoneGrpcEndpointUrl::parse("https://example.invalid:443").expect("HTTPS URL must parse");
|
||||||
|
assert!(!http.uses_tls());
|
||||||
|
assert!(https.uses_tls());
|
||||||
|
let metadata = (0..=super::MAX_GRPC_METADATA_ENTRY_COUNT)
|
||||||
|
.map(|index| {
|
||||||
|
return crate::YellowstoneGrpcMetadataEntry::public(format!("x-ksp-{index}"), "value").expect("generated metadata must be valid");
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let endpoint = crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"metadata-bound",
|
||||||
|
true,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
http,
|
||||||
|
crate::YellowstoneGrpcSessionSettings::default(),
|
||||||
|
);
|
||||||
|
assert!(endpoint.with_metadata(metadata).is_err());
|
||||||
|
}
|
||||||
685
crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
Normal file
685
crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum FixtureMode {
|
||||||
|
RoundTrip,
|
||||||
|
ClientHalfClose,
|
||||||
|
HostileClose,
|
||||||
|
Flood,
|
||||||
|
RemoteStatus,
|
||||||
|
Malformed,
|
||||||
|
Oversized,
|
||||||
|
Idle,
|
||||||
|
ReconnectReplay,
|
||||||
|
ReplayGap,
|
||||||
|
ReconnectExhausted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FixtureGeyser {
|
||||||
|
mode: FixtureMode,
|
||||||
|
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = super::MpscStream<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>>;
|
||||||
|
type SubscribeDeshredStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, 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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let subscribe_call = self.subscribe_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst).saturating_add(1);
|
||||||
|
if matches!(self.mode, FixtureMode::ReconnectExhausted) && subscribe_call > 1 {
|
||||||
|
return std::result::Result::Err(tonic::Status::unavailable("GRPC-RECONNECT-SECRET-CANARY"));
|
||||||
|
}
|
||||||
|
let mut inbound = request.into_inner();
|
||||||
|
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(64);
|
||||||
|
let mode = self.mode;
|
||||||
|
let half_close_seen = self.half_close_seen.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let initial = inbound.message().await;
|
||||||
|
let initial = match initial {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
match mode {
|
||||||
|
FixtureMode::RoundTrip => {
|
||||||
|
assert!(initial.slots.contains_key("initial"));
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(ping_update())).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ping = inbound.message().await;
|
||||||
|
let ping = match ping {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
assert_eq!(ping.ping.map(|value| value.id), std::option::Option::Some(1));
|
||||||
|
assert!(ping.accounts.is_empty());
|
||||||
|
assert!(ping.slots.is_empty());
|
||||||
|
assert!(ping.transactions.is_empty());
|
||||||
|
assert!(ping.transactions_status.is_empty());
|
||||||
|
assert!(ping.blocks.is_empty());
|
||||||
|
assert!(ping.blocks_meta.is_empty());
|
||||||
|
assert!(ping.entry.is_empty());
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(pong_update(1))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mutation = inbound.message().await;
|
||||||
|
let mutation = match mutation {
|
||||||
|
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
assert!(mutation.accounts.contains_key("mutated"));
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(901))).await;
|
||||||
|
},
|
||||||
|
FixtureMode::ClientHalfClose => {
|
||||||
|
let half_close = inbound.message().await;
|
||||||
|
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||||
|
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FixtureMode::HostileClose => {
|
||||||
|
let half_close = inbound.message().await;
|
||||||
|
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||||
|
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
drop(outbound_tx);
|
||||||
|
},
|
||||||
|
FixtureMode::Flood => {
|
||||||
|
for slot in 0_u64..32_u64 {
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(slot_update(1_000 + slot))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FixtureMode::RemoteStatus => {
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Err(tonic::Status::permission_denied("GRPC-STREAM-REMOTE-SECRET-CANARY"))).await;
|
||||||
|
},
|
||||||
|
FixtureMode::Malformed => {
|
||||||
|
let _ = outbound_tx
|
||||||
|
.send(std::result::Result::Ok(yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: std::vec::Vec::new(),
|
||||||
|
update_oneof: std::option::Option::None,
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
},
|
||||||
|
FixtureMode::Oversized => {
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(77))).await;
|
||||||
|
},
|
||||||
|
FixtureMode::Idle => {
|
||||||
|
let _ = inbound.message().await;
|
||||||
|
},
|
||||||
|
FixtureMode::ReconnectReplay => {
|
||||||
|
if subscribe_call == 1 {
|
||||||
|
assert_eq!(initial.from_slot, std::option::Option::None);
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
|
||||||
|
} else {
|
||||||
|
assert_eq!(subscribe_call, 2);
|
||||||
|
assert_eq!(initial.from_slot, std::option::Option::Some(500));
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(slot_update(500))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(slot_update(501))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let half_close = inbound.message().await;
|
||||||
|
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||||
|
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FixtureMode::ReplayGap => {
|
||||||
|
if subscribe_call == 1 {
|
||||||
|
assert_eq!(initial.from_slot, std::option::Option::None);
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
|
||||||
|
} else {
|
||||||
|
assert_eq!(subscribe_call, 2);
|
||||||
|
assert_eq!(initial.from_slot, std::option::Option::Some(505));
|
||||||
|
if outbound_tx.send(std::result::Result::Ok(slot_update(505))).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let half_close = inbound.message().await;
|
||||||
|
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
||||||
|
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FixtureMode::ReconnectExhausted => {
|
||||||
|
assert_eq!(subscribe_call, 1);
|
||||||
|
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(700))).await;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(super::MpscStream::new(outbound_rx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 KSP 0.2.9"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let first_available = match self.mode {
|
||||||
|
FixtureMode::ReconnectReplay | FixtureMode::ReconnectExhausted => std::option::Option::Some(400),
|
||||||
|
FixtureMode::ReplayGap => std::option::Option::Some(505),
|
||||||
|
_ => return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside this fixture mode")),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse { first_available }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 this 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 this 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 this 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 this 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 this 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 this fixture"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FixtureServer {
|
||||||
|
endpoint_url: std::string::String,
|
||||||
|
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixtureServer {
|
||||||
|
async fn start(mode: FixtureMode) -> Self {
|
||||||
|
let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse");
|
||||||
|
let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind");
|
||||||
|
let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address");
|
||||||
|
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
|
||||||
|
let half_close_seen = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let subscribe_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||||
|
let service_half_close_seen = half_close_seen.clone();
|
||||||
|
let service_subscribe_calls = subscribe_calls.clone();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser {
|
||||||
|
mode,
|
||||||
|
half_close_seen: service_half_close_seen,
|
||||||
|
subscribe_calls: service_subscribe_calls,
|
||||||
|
});
|
||||||
|
let result = tonic::transport::Server::builder()
|
||||||
|
.serve_with_incoming_shutdown(service, incoming, async move {
|
||||||
|
let _ = shutdown_receiver.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
});
|
||||||
|
return Self {
|
||||||
|
endpoint_url: format!("http://{local_address}"),
|
||||||
|
half_close_seen,
|
||||||
|
subscribe_calls,
|
||||||
|
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 verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> {
|
||||||
|
let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok());
|
||||||
|
let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok());
|
||||||
|
if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-STREAM-SECRET-CANARY") {
|
||||||
|
return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_settings(
|
||||||
|
url: &str,
|
||||||
|
request_capacity: usize,
|
||||||
|
update_capacity: usize,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
) -> crate::YellowstoneGrpcEndpointSettings {
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
return fixture_settings_with_reconnect(
|
||||||
|
url,
|
||||||
|
request_capacity,
|
||||||
|
update_capacity,
|
||||||
|
max_inbound_message_size_bytes,
|
||||||
|
max_outbound_message_size_bytes,
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(0, defaults.reconnect().initial_backoff(), defaults.reconnect().max_backoff()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_settings_with_reconnect(
|
||||||
|
url: &str,
|
||||||
|
request_capacity: usize,
|
||||||
|
update_capacity: usize,
|
||||||
|
max_inbound_message_size_bytes: usize,
|
||||||
|
max_outbound_message_size_bytes: usize,
|
||||||
|
reconnect: crate::YellowstoneGrpcReconnectSettings,
|
||||||
|
) -> crate::YellowstoneGrpcEndpointSettings {
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let session = crate::YellowstoneGrpcSessionSettings::new(
|
||||||
|
defaults.connect_timeout(),
|
||||||
|
defaults.unary_timeout(),
|
||||||
|
std::time::Duration::from_millis(250),
|
||||||
|
reconnect,
|
||||||
|
request_capacity,
|
||||||
|
update_capacity,
|
||||||
|
max_inbound_message_size_bytes,
|
||||||
|
max_outbound_message_size_bytes,
|
||||||
|
);
|
||||||
|
let metadata = std::vec![
|
||||||
|
crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"),
|
||||||
|
crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-STREAM-SECRET-CANARY").expect("fixture secret metadata must be valid"),
|
||||||
|
];
|
||||||
|
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"fixture-stream",
|
||||||
|
true,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.with_metadata(metadata)
|
||||||
|
.expect("fixture metadata settings must validate");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initial_request() -> crate::YellowstoneSubscribeRequest {
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
request
|
||||||
|
.insert_slot_filter(
|
||||||
|
crate::YellowstoneSubscribeFilterName::new("initial").expect("fixture filter name must validate"),
|
||||||
|
crate::YellowstoneSubscribeSlotFilter::new(),
|
||||||
|
)
|
||||||
|
.expect("fixture slot filter must insert");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutated_request() -> crate::YellowstoneSubscribeRequest {
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
request
|
||||||
|
.insert_account_filter(
|
||||||
|
crate::YellowstoneSubscribeFilterName::new("mutated").expect("fixture filter name must validate"),
|
||||||
|
crate::YellowstoneSubscribeAccountFilter::new(),
|
||||||
|
)
|
||||||
|
.expect("fixture account filter must insert");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ping_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: std::vec::Vec::new(),
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Ping(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdatePing {},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pong_update(id: i32) -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: std::vec::Vec::new(),
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Pong(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdatePong { id },
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slot_update(slot: u64) -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: std::vec!["initial".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
|
||||||
|
slot,
|
||||||
|
parent: std::option::Option::Some(slot.saturating_sub(1)),
|
||||||
|
status: yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed as i32,
|
||||||
|
dead_error: std::option::Option::None,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_bidi_round_trip_mutates_request_replies_to_ping_and_observes_server_half_close() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::RoundTrip).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
assert_eq!(session.state(), crate::YellowstoneGrpcSubscribeState::Active);
|
||||||
|
let ping = session.next_update().await.expect("Ping update must decode").expect("Ping update must be present");
|
||||||
|
assert!(matches!(ping, crate::YellowstoneSubscribeUpdate::Ping(_)));
|
||||||
|
let pong = session.next_update().await.expect("Pong update must decode").expect("Pong update must be present");
|
||||||
|
match pong {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Pong(value) => assert_eq!(value.id(), 1),
|
||||||
|
_ => panic!("fixture must return Pong"),
|
||||||
|
}
|
||||||
|
session.try_update(&mutated_request()).expect("request mutation must enter bounded queue");
|
||||||
|
let slot = session.next_update().await.expect("slot update must decode").expect("slot update must be present");
|
||||||
|
match slot {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 901),
|
||||||
|
_ => panic!("fixture must return Slot"),
|
||||||
|
}
|
||||||
|
assert!(session.next_update().await.expect("server half-close must be normal").is_none());
|
||||||
|
assert_eq!(session.state(), crate::YellowstoneGrpcSubscribeState::Closed);
|
||||||
|
session.close().await.expect("already half-closed fixture must close cleanly");
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_explicit_close_half_closes_request_stream_before_deadline() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ClientHalfClose).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
session.close().await.expect("graceful half-close must complete");
|
||||||
|
assert!(server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst));
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::HostileClose).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let error = session.close().await.expect_err("hostile server must hit the bounded graceful close deadline");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
|
||||||
|
assert!(server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst));
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_session_drop_best_effort_half_closes_request_stream() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ClientHalfClose).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
drop(session);
|
||||||
|
let observed = tokio::time::timeout(std::time::Duration::from_secs(1), async {
|
||||||
|
loop {
|
||||||
|
if server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("fixture must observe dropped-session half-close before timeout");
|
||||||
|
assert!(observed);
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_slow_receiver_overflow_is_terminal_and_observable() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::Flood).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 2, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||||
|
assert!(session.next_update().await.expect("first queued update must remain readable").is_some());
|
||||||
|
assert!(session.next_update().await.expect("second queued update must remain readable").is_some());
|
||||||
|
let error = session.next_update().await.expect_err("overflow must become terminal after bounded queued updates drain");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW);
|
||||||
|
let rendered = format!("{error:?} {session:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-STREAM-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
let _ = session.close().await;
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_stream_remote_status_is_safe_and_terminal() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::RemoteStatus).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let error = session.next_update().await.expect_err("remote Status must fail stream");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
|
||||||
|
let rendered = format!("{error:?} {session:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-STREAM-REMOTE-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("GRPC-STREAM-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
let _ = session.close().await;
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_malformed_update_is_rejected_without_raw_wire_escape() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::Malformed).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let error = session.next_update().await.expect_err("missing update oneof must be rejected");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||||
|
let _ = session.close().await;
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_inbound_message_limit_is_enforced_by_tonic_stream_decoder() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::Oversized).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, 1, defaults.max_outbound_message_size_bytes());
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let error = session.next_update().await.expect_err("message above configured inbound bound must fail decoding");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
|
||||||
|
let rendered = format!("{error:?} {session:?}");
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
let _ = session.close().await;
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_outbound_mutation_size_is_rejected_before_queue_dispatch() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::Idle).await;
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, 64 * 1024 * 1024, 1);
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let session = channel
|
||||||
|
.open_standard_subscribe(crate::YellowstoneSubscribeRequest::new())
|
||||||
|
.await
|
||||||
|
.expect("empty initial request must fit one-byte fixture bound");
|
||||||
|
let error = session.try_update(&mutated_request()).expect_err("encoded mutation above configured bound must be rejected before dispatch");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW);
|
||||||
|
session.close().await.expect("idle fixture must observe client half-close");
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_reconnect_replays_from_last_observed_slot_and_counts_duplicate_identity() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings_with_reconnect(
|
||||||
|
server.endpoint_url.as_str(),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
defaults.max_inbound_message_size_bytes(),
|
||||||
|
defaults.max_outbound_message_size_bytes(),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
|
||||||
|
);
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let first = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||||
|
match first {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
|
||||||
|
_ => panic!("fixture must return first Slot"),
|
||||||
|
}
|
||||||
|
let duplicate = session.next_update().await.expect("replayed duplicate must decode").expect("replayed duplicate must be present");
|
||||||
|
match duplicate {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
|
||||||
|
_ => panic!("fixture must return replayed Slot"),
|
||||||
|
}
|
||||||
|
let resumed = session.next_update().await.expect("resumed slot must decode").expect("resumed slot must be present");
|
||||||
|
match resumed {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 501),
|
||||||
|
_ => panic!("fixture must return resumed Slot"),
|
||||||
|
}
|
||||||
|
let snapshot = session.snapshot();
|
||||||
|
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Active);
|
||||||
|
assert_eq!(snapshot.reconnect_count(), 1);
|
||||||
|
assert_eq!(snapshot.replay_attempt_count(), 1);
|
||||||
|
assert_eq!(snapshot.continuity_gap_count(), 0);
|
||||||
|
assert_eq!(snapshot.duplicate_update_count(), 1);
|
||||||
|
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(500));
|
||||||
|
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(501));
|
||||||
|
session.close().await.expect("reconnected stream must close cleanly");
|
||||||
|
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 2);
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_replay_info_proves_and_clamps_retention_gap_without_lossless_claim() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ReplayGap).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings_with_reconnect(
|
||||||
|
server.endpoint_url.as_str(),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
defaults.max_inbound_message_size_bytes(),
|
||||||
|
defaults.max_outbound_message_size_bytes(),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
|
||||||
|
);
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||||
|
let resumed = session.next_update().await.expect("clamped replay must decode").expect("clamped replay must be present");
|
||||||
|
match resumed {
|
||||||
|
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 505),
|
||||||
|
_ => panic!("fixture must return clamped Slot"),
|
||||||
|
}
|
||||||
|
let snapshot = session.snapshot();
|
||||||
|
assert_eq!(snapshot.reconnect_count(), 1);
|
||||||
|
assert_eq!(snapshot.replay_attempt_count(), 1);
|
||||||
|
assert_eq!(snapshot.continuity_gap_count(), 1);
|
||||||
|
assert_eq!(snapshot.duplicate_update_count(), 0);
|
||||||
|
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(505));
|
||||||
|
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(505));
|
||||||
|
session.close().await.expect("gap fixture must close cleanly");
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_reconnect_budget_exhaustion_is_terminal_and_safe() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ReconnectExhausted).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings_with_reconnect(
|
||||||
|
server.endpoint_url.as_str(),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
defaults.max_inbound_message_size_bytes(),
|
||||||
|
defaults.max_outbound_message_size_bytes(),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(2, std::time::Duration::from_millis(5), std::time::Duration::from_millis(10)),
|
||||||
|
);
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||||
|
let error = session.next_update().await.expect_err("failed reconnect attempts must exhaust the bounded budget");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_CHANNEL_FAILED);
|
||||||
|
let snapshot = session.snapshot();
|
||||||
|
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Failed);
|
||||||
|
assert_eq!(snapshot.reconnect_count(), 0);
|
||||||
|
assert_eq!(snapshot.replay_attempt_count(), 2);
|
||||||
|
assert_eq!(snapshot.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_GRPC_CHANNEL_FAILED));
|
||||||
|
let rendered = format!("{error:?} {session:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-RECONNECT-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 3);
|
||||||
|
let _ = session.close().await;
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_shutdown_interrupts_reconnect_backoff_and_mutation_is_rejected_during_reconnect() {
|
||||||
|
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let settings = fixture_settings_with_reconnect(
|
||||||
|
server.endpoint_url.as_str(),
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
defaults.max_inbound_message_size_bytes(),
|
||||||
|
defaults.max_outbound_message_size_bytes(),
|
||||||
|
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(500), std::time::Duration::from_millis(500)),
|
||||||
|
);
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
|
||||||
|
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(1), async {
|
||||||
|
loop {
|
||||||
|
if session.state() == crate::YellowstoneGrpcSubscribeState::Reconnecting {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("fixture must enter reconnecting state");
|
||||||
|
let mutation_error = session.try_update(&mutated_request()).expect_err("request mutation must be rejected during reconnect");
|
||||||
|
assert_eq!(mutation_error.code(), crate::ERROR_CODE_GRPC_SESSION_CLOSED);
|
||||||
|
session.close().await.expect("shutdown must interrupt reconnect backoff without opening another stream");
|
||||||
|
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
777
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
Normal file
777
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
|
||||||
|
// version: 5
|
||||||
|
|
||||||
|
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
|
||||||
|
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minimal_transaction_info(signature_byte: u8, index: u64) -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
let meta = yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta { fee: 5_000, ..std::default::Default::default() };
|
||||||
|
return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||||
|
signature: vec![signature_byte; 64],
|
||||||
|
is_vote: false,
|
||||||
|
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
|
||||||
|
signatures: vec![vec![signature_byte; 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: 0,
|
||||||
|
}),
|
||||||
|
account_keys: vec![vec![1_u8; 32]],
|
||||||
|
recent_blockhash: vec![2_u8; 32],
|
||||||
|
instructions: vec![],
|
||||||
|
versioned: false,
|
||||||
|
address_table_lookups: vec![],
|
||||||
|
config: std::option::Option::None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
meta: std::option::Option::Some(meta),
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() {
|
||||||
|
assert!(crate::YellowstoneSubscribeFilterName::new("").is_err());
|
||||||
|
assert!(crate::YellowstoneSubscribeFilterName::new(" leading").is_err());
|
||||||
|
assert!(crate::YellowstoneSubscribeFilterName::new("trailing ").is_err());
|
||||||
|
assert!(crate::YellowstoneSubscribeFilterName::new("line\nbreak").is_err());
|
||||||
|
assert!(crate::YellowstoneSubscribeFilterName::new("x".repeat(129)).is_err());
|
||||||
|
let name = filter_name("account-primary");
|
||||||
|
assert_eq!(name.as_str(), "account-primary");
|
||||||
|
assert!(!format!("{name:?}").contains("account-primary"));
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
assert!(request.insert_account_filter(name.clone(), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
|
||||||
|
let duplicate = request.insert_slot_filter(name, crate::YellowstoneSubscribeSlotFilter::new());
|
||||||
|
assert!(duplicate.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() {
|
||||||
|
let empty = crate::YellowstoneSubscribeRequest::new().to_wire().expect("empty request must encode");
|
||||||
|
assert!(empty.accounts.is_empty());
|
||||||
|
assert!(empty.slots.is_empty());
|
||||||
|
assert!(empty.transactions.is_empty());
|
||||||
|
assert!(empty.transactions_status.is_empty());
|
||||||
|
assert!(empty.blocks.is_empty());
|
||||||
|
assert!(empty.blocks_meta.is_empty());
|
||||||
|
assert!(empty.entry.is_empty());
|
||||||
|
assert_eq!(empty.commitment, std::option::Option::None);
|
||||||
|
assert!(empty.accounts_data_slice.is_empty());
|
||||||
|
assert_eq!(empty.ping, std::option::Option::None);
|
||||||
|
assert_eq!(empty.from_slot, std::option::Option::None);
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
assert!(request.insert_account_filter(filter_name("accounts"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_slot_filter(filter_name("slots"), crate::YellowstoneSubscribeSlotFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_transaction_filter(filter_name("transactions"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_transaction_status_filter(filter_name("transaction-status"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_block_filter(filter_name("blocks"), crate::YellowstoneSubscribeBlockFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_blocks_meta_filter(filter_name("blocks-meta"), crate::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
|
||||||
|
assert!(request.insert_entry_filter(filter_name("entry"), crate::YellowstoneSubscribeEntryFilter::new()).is_ok());
|
||||||
|
let wire = request.to_wire().expect("named empty request must encode");
|
||||||
|
assert_eq!(wire.accounts.len(), 1);
|
||||||
|
assert_eq!(wire.slots.len(), 1);
|
||||||
|
assert_eq!(wire.transactions.len(), 1);
|
||||||
|
assert_eq!(wire.transactions_status.len(), 1);
|
||||||
|
assert_eq!(wire.blocks.len(), 1);
|
||||||
|
assert_eq!(wire.blocks_meta.len(), 1);
|
||||||
|
assert_eq!(wire.entry.len(), 1);
|
||||||
|
assert_eq!(wire.accounts.get("accounts"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default()));
|
||||||
|
assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default()));
|
||||||
|
assert_eq!(
|
||||||
|
wire.transactions.get("transactions"),
|
||||||
|
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wire.transactions_status.get("transaction-status"),
|
||||||
|
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default())
|
||||||
|
);
|
||||||
|
assert_eq!(wire.blocks.get("blocks"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default()));
|
||||||
|
assert!(wire.blocks_meta.contains_key("blocks-meta"));
|
||||||
|
assert!(wire.entry.contains_key("entry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_subscribe_common_fields_preserve_optional_and_ordered_wire_semantics() {
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
request.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||||
|
let first = crate::YellowstoneAccountsDataSlice::new(8, 16).expect("first slice must validate");
|
||||||
|
let second = crate::YellowstoneAccountsDataSlice::new(64, 0).expect("zero-length slice remains representable");
|
||||||
|
assert!(request.push_accounts_data_slice(first).is_ok());
|
||||||
|
assert!(request.push_accounts_data_slice(second).is_ok());
|
||||||
|
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
|
||||||
|
request.set_from_slot(std::option::Option::Some(42));
|
||||||
|
assert_eq!(request.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||||
|
assert_eq!(request.accounts_data_slices(), &[first, second]);
|
||||||
|
assert_eq!(request.ping(), std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
|
||||||
|
assert_eq!(request.from_slot(), std::option::Option::Some(42));
|
||||||
|
let wire = request.to_wire().expect("common fields must encode");
|
||||||
|
assert_eq!(wire.commitment, std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32));
|
||||||
|
assert_eq!(wire.accounts_data_slice.len(), 2);
|
||||||
|
assert_eq!(wire.accounts_data_slice[0].offset, 8);
|
||||||
|
assert_eq!(wire.accounts_data_slice[0].length, 16);
|
||||||
|
assert_eq!(wire.accounts_data_slice[1].offset, 64);
|
||||||
|
assert_eq!(wire.accounts_data_slice[1].length, 0);
|
||||||
|
assert_eq!(wire.ping.map(|ping| return ping.id), std::option::Option::Some(-7));
|
||||||
|
assert_eq!(wire.from_slot, std::option::Option::Some(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() {
|
||||||
|
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024).is_ok());
|
||||||
|
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024 + 1).is_err());
|
||||||
|
assert!(crate::YellowstoneAccountsDataSlice::new(u64::MAX, 1).is_err());
|
||||||
|
let mut slices = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
for index in 0..128_u64 {
|
||||||
|
let slice = crate::YellowstoneAccountsDataSlice::new(index, 1).expect("bounded fixture slice must validate");
|
||||||
|
assert!(slices.push_accounts_data_slice(slice).is_ok());
|
||||||
|
}
|
||||||
|
let excess = crate::YellowstoneAccountsDataSlice::new(129, 1).expect("excess fixture slice itself must validate");
|
||||||
|
assert!(slices.push_accounts_data_slice(excess).is_err());
|
||||||
|
let mut filters = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
for index in 0..1_024_u32 {
|
||||||
|
let name = crate::YellowstoneSubscribeFilterName::new(format!("f{index}")).expect("bounded filter name must validate");
|
||||||
|
assert!(filters.insert_account_filter(name, crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
|
||||||
|
}
|
||||||
|
let excess_name = crate::YellowstoneSubscribeFilterName::new("excess").expect("excess filter name must validate independently");
|
||||||
|
assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() {
|
||||||
|
let mut request = crate::YellowstoneSubscribeRequest::new();
|
||||||
|
assert!(request.insert_account_filter(filter_name("sensitive-label"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
|
||||||
|
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(9)));
|
||||||
|
request.set_from_slot(std::option::Option::Some(77));
|
||||||
|
let debug = format!("{request:?}");
|
||||||
|
assert!(!debug.contains("sensitive-label"));
|
||||||
|
assert!(debug.contains("account_filter_count"));
|
||||||
|
assert!(debug.contains("from_slot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_account_and_slot_filters_encode_complete_current_wire() {
|
||||||
|
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
|
||||||
|
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
|
||||||
|
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
|
||||||
|
assert!(filter.push_account(account).is_ok());
|
||||||
|
assert!(filter.push_owner(owner).is_ok());
|
||||||
|
let raw = crate::YellowstoneAccountMemcmp::bytes(4, vec![1_u8, 2, 3]).expect("raw memcmp must validate");
|
||||||
|
let base58 = crate::YellowstoneAccountMemcmp::base58(8, "1234").expect("base58 memcmp must validate");
|
||||||
|
let base64 = crate::YellowstoneAccountMemcmp::base64(12, "AQID==").expect("base64 memcmp must validate");
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(raw)).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base58)).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base64)).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::DataSize(165)).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::TokenAccountState(true)).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Eq(1))).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Ne(2))).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Lt(3))).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Gt(4))).is_ok());
|
||||||
|
filter.set_nonempty_txn_signature(std::option::Option::Some(true));
|
||||||
|
let cuckoo =
|
||||||
|
crate::YellowstoneCuckooFilter::new(vec![9_u8; 16], 4, 4, 8, 77, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo fixture must validate");
|
||||||
|
filter.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
|
||||||
|
let wire = filter.to_wire();
|
||||||
|
assert_eq!(wire.account, vec![account.to_string()]);
|
||||||
|
assert_eq!(wire.owner, vec![owner.to_string()]);
|
||||||
|
assert_eq!(wire.filters.len(), 9);
|
||||||
|
match wire.filters[0].filter.as_ref().expect("raw memcmp oneof must be present") {
|
||||||
|
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Memcmp(value) => assert_eq!(
|
||||||
|
value.data,
|
||||||
|
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Bytes(vec![1_u8, 2, 3]))
|
||||||
|
),
|
||||||
|
_ => panic!("first predicate must stay memcmp"),
|
||||||
|
}
|
||||||
|
match wire.filters[5].filter.as_ref().expect("lamports oneof must be present") {
|
||||||
|
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Lamports(value) => {
|
||||||
|
assert_eq!(value.cmp, std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Eq(1)))
|
||||||
|
},
|
||||||
|
_ => panic!("sixth predicate must stay lamports"),
|
||||||
|
}
|
||||||
|
assert_eq!(wire.nonempty_txn_signature, std::option::Option::Some(true));
|
||||||
|
let cuckoo = wire.cuckoo_accounts_filter.expect("cuckoo filter must be present");
|
||||||
|
assert_eq!(cuckoo.data, vec![9_u8; 16]);
|
||||||
|
assert_eq!(cuckoo.bucket_count, 4);
|
||||||
|
assert_eq!(cuckoo.entries_per_bucket, 4);
|
||||||
|
assert_eq!(cuckoo.fingerprint_bits, 8);
|
||||||
|
assert_eq!(cuckoo.hash_seed, 77);
|
||||||
|
assert_eq!(cuckoo.hash_algorithm, yellowstone_grpc_proto::geyser::CuckooHashAlgorithm::SipHash as i32);
|
||||||
|
let mut slots = crate::YellowstoneSubscribeSlotFilter::new();
|
||||||
|
slots.set_filter_by_commitment(std::option::Option::Some(false));
|
||||||
|
slots.set_interslot_updates(std::option::Option::Some(true));
|
||||||
|
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(false));
|
||||||
|
assert_eq!(slots.interslot_updates(), std::option::Option::Some(true));
|
||||||
|
let slots_wire = slots.to_wire();
|
||||||
|
assert_eq!(slots_wire.filter_by_commitment, std::option::Option::Some(false));
|
||||||
|
assert_eq!(slots_wire.interslot_updates, std::option::Option::Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_account_filter_bounds_and_debug_are_provider_neutral() {
|
||||||
|
assert!(crate::YellowstoneCuckooFilter::new(vec![], 0, 4, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
|
||||||
|
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 0, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
|
||||||
|
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 7, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
|
||||||
|
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 12, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_ok());
|
||||||
|
assert!(crate::YellowstoneAccountMemcmp::base58(0, "contains whitespace").is_err());
|
||||||
|
assert!(crate::YellowstoneAccountMemcmp::base64(0, "line\nbreak").is_err());
|
||||||
|
let memcmp = crate::YellowstoneAccountMemcmp::bytes(5, vec![7_u8, 8, 9]).expect("memcmp must validate");
|
||||||
|
let debug = format!("{memcmp:?}");
|
||||||
|
assert!(debug.contains("payload_length"));
|
||||||
|
assert!(!debug.contains("7, 8, 9"));
|
||||||
|
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
|
||||||
|
assert!(filter.push_account(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
|
||||||
|
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
|
||||||
|
let filter_debug = format!("{filter:?}");
|
||||||
|
assert!(filter_debug.contains("account_count"));
|
||||||
|
assert!(!filter_debug.contains(&ksp_core_lib::Pubkey::new_from_array([3_u8; 32]).to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_account_update_decodes_complete_wire_and_redacts_payload_debug() {
|
||||||
|
let signature = vec![6_u8; 64];
|
||||||
|
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["accounts-main".to_owned(), "accounts-owner".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
|
||||||
|
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
|
||||||
|
pubkey: vec![1_u8; 32],
|
||||||
|
lamports: 42,
|
||||||
|
owner: vec![2_u8; 32],
|
||||||
|
executable: true,
|
||||||
|
rent_epoch: 9,
|
||||||
|
data: vec![0xAA_u8, 0xBB, 0xCC],
|
||||||
|
write_version: 7,
|
||||||
|
txn_signature: std::option::Option::Some(signature),
|
||||||
|
}),
|
||||||
|
slot: 123,
|
||||||
|
is_startup: true,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 1_700_000_000, nanos: 123_456_789 }),
|
||||||
|
};
|
||||||
|
let update = super::decode_account_update(wire).expect("account update fixture must decode");
|
||||||
|
assert_eq!(update.filters()[0].as_str(), "accounts-main");
|
||||||
|
assert_eq!(update.filters()[1].as_str(), "accounts-owner");
|
||||||
|
assert_eq!(update.created_at().expect("timestamp must be present").seconds(), 1_700_000_000);
|
||||||
|
assert_eq!(update.created_at().expect("timestamp must be present").nanos(), 123_456_789);
|
||||||
|
assert_eq!(update.slot(), 123);
|
||||||
|
assert!(update.is_startup());
|
||||||
|
assert_eq!(update.account().pubkey(), &ksp_core_lib::Pubkey::new_from_array([1_u8; 32]));
|
||||||
|
assert_eq!(update.account().owner(), &ksp_core_lib::Pubkey::new_from_array([2_u8; 32]));
|
||||||
|
assert_eq!(update.account().lamports(), 42);
|
||||||
|
assert!(update.account().executable());
|
||||||
|
assert_eq!(update.account().rent_epoch(), 9);
|
||||||
|
assert_eq!(update.account().data(), &[0xAA_u8, 0xBB, 0xCC]);
|
||||||
|
assert_eq!(update.account().write_version(), 7);
|
||||||
|
assert_eq!(update.account().transaction_signature().expect("signature must be present").as_bytes(), &[6_u8; 64]);
|
||||||
|
let debug = format!("{update:?}");
|
||||||
|
assert!(!debug.contains("accounts-main"));
|
||||||
|
assert!(!debug.contains("170, 187, 204"));
|
||||||
|
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_account_update_rejects_malformed_fixed_width_and_envelope_fields() {
|
||||||
|
let malformed_pubkey = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["accounts".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
|
||||||
|
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
|
||||||
|
pubkey: vec![1_u8; 31],
|
||||||
|
lamports: 0,
|
||||||
|
owner: vec![2_u8; 32],
|
||||||
|
executable: false,
|
||||||
|
rent_epoch: 0,
|
||||||
|
data: vec![],
|
||||||
|
write_version: 0,
|
||||||
|
txn_signature: std::option::Option::None,
|
||||||
|
}),
|
||||||
|
slot: 0,
|
||||||
|
is_startup: false,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_account_update(malformed_pubkey).is_err());
|
||||||
|
let malformed_signature = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["accounts".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
|
||||||
|
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
|
||||||
|
pubkey: vec![1_u8; 32],
|
||||||
|
lamports: 0,
|
||||||
|
owner: vec![2_u8; 32],
|
||||||
|
executable: false,
|
||||||
|
rent_epoch: 0,
|
||||||
|
data: vec![],
|
||||||
|
write_version: 0,
|
||||||
|
txn_signature: std::option::Option::Some(vec![9_u8; 63]),
|
||||||
|
}),
|
||||||
|
slot: 0,
|
||||||
|
is_startup: false,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_account_update(malformed_signature).is_err());
|
||||||
|
let missing_info = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["accounts".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount { account: std::option::Option::None, slot: 0, is_startup: false },
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_account_update(missing_info).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_slot_update_preserves_all_current_statuses_and_bounds_dead_error() {
|
||||||
|
let statuses = [
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed, crate::YellowstoneSlotStatus::Processed),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotConfirmed, crate::YellowstoneSlotStatus::Confirmed),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFinalized, crate::YellowstoneSlotStatus::Finalized),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFirstShredReceived, crate::YellowstoneSlotStatus::FirstShredReceived),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCompleted, crate::YellowstoneSlotStatus::Completed),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCreatedBank, crate::YellowstoneSlotStatus::CreatedBank),
|
||||||
|
(yellowstone_grpc_proto::geyser::SlotStatus::SlotDead, crate::YellowstoneSlotStatus::Dead),
|
||||||
|
];
|
||||||
|
for (wire_status, expected) in statuses {
|
||||||
|
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["slots".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
|
||||||
|
slot: 88,
|
||||||
|
parent: std::option::Option::Some(87),
|
||||||
|
status: wire_status as i32,
|
||||||
|
dead_error: if expected == crate::YellowstoneSlotStatus::Dead {
|
||||||
|
std::option::Option::Some("fork rejected".to_owned())
|
||||||
|
} else {
|
||||||
|
std::option::Option::None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 5, nanos: 6 }),
|
||||||
|
};
|
||||||
|
let update = super::decode_slot_update(wire).expect("slot update fixture must decode");
|
||||||
|
assert_eq!(update.status(), expected);
|
||||||
|
assert_eq!(update.slot(), 88);
|
||||||
|
assert_eq!(update.parent(), std::option::Option::Some(87));
|
||||||
|
assert_eq!(update.filters()[0].as_str(), "slots");
|
||||||
|
if expected == crate::YellowstoneSlotStatus::Dead {
|
||||||
|
assert_eq!(update.dead_error(), std::option::Option::Some("fork rejected"));
|
||||||
|
assert!(!format!("{update:?}").contains("fork rejected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let unknown = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["slots".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
|
||||||
|
slot: 1,
|
||||||
|
parent: std::option::Option::None,
|
||||||
|
status: 99,
|
||||||
|
dead_error: std::option::Option::None,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_slot_update(unknown).is_err());
|
||||||
|
let oversized = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["slots".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
|
||||||
|
slot: 1,
|
||||||
|
parent: std::option::Option::None,
|
||||||
|
status: yellowstone_grpc_proto::geyser::SlotStatus::SlotDead as i32,
|
||||||
|
dead_error: std::option::Option::Some("x".repeat(16 * 1024 + 1)),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_slot_update(oversized).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_transaction_filters_encode_complete_current_wire_and_redact_selectors() {
|
||||||
|
let mut filter = crate::YellowstoneSubscribeTransactionFilter::new();
|
||||||
|
filter.set_vote(std::option::Option::Some(false));
|
||||||
|
filter.set_failed(std::option::Option::Some(true));
|
||||||
|
let signature = crate::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
|
||||||
|
filter.set_signature(std::option::Option::Some(signature));
|
||||||
|
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
|
||||||
|
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
|
||||||
|
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
|
||||||
|
let cuckoo =
|
||||||
|
crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 7, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo filter must validate");
|
||||||
|
filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo));
|
||||||
|
filter.set_token_accounts(std::option::Option::Some(crate::YellowstoneTokenAccountExpansion::BalanceChanged));
|
||||||
|
let wire = filter.to_wire();
|
||||||
|
assert_eq!(wire.vote, std::option::Option::Some(false));
|
||||||
|
assert_eq!(wire.failed, std::option::Option::Some(true));
|
||||||
|
assert!(wire.signature.is_some());
|
||||||
|
assert_eq!(wire.account_include.len(), 1);
|
||||||
|
assert_eq!(wire.account_exclude.len(), 1);
|
||||||
|
assert_eq!(wire.account_required.len(), 1);
|
||||||
|
assert!(wire.cuckoo_account_include.is_some());
|
||||||
|
assert_eq!(wire.token_accounts, std::option::Option::Some(yellowstone_grpc_proto::geyser::TokenAccountExpansionControlFlag::BalanceChanged as i32));
|
||||||
|
let debug = format!("{filter:?}");
|
||||||
|
assert!(debug.contains("account_include_count"));
|
||||||
|
assert!(!debug.contains(&"1".repeat(32)));
|
||||||
|
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
|
||||||
|
assert!(crate::YellowstoneTransactionSignatureSelector::new("contains-0-O-I-l").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_transaction_update_decodes_current_storage_wire_including_v1_config_and_meta() {
|
||||||
|
let confirmed = 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], vec![8_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: 2,
|
||||||
|
num_readonly_signed_accounts: 1,
|
||||||
|
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, 1],
|
||||||
|
data: vec![4_u8, 5, 6],
|
||||||
|
}],
|
||||||
|
versioned: true,
|
||||||
|
address_table_lookups: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::MessageAddressTableLookup {
|
||||||
|
account_key: vec![4_u8; 32],
|
||||||
|
writable_indexes: vec![1_u8, 2],
|
||||||
|
readonly_indexes: vec![3_u8],
|
||||||
|
}],
|
||||||
|
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(10),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
|
||||||
|
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![11_u8, 12] }),
|
||||||
|
fee: 5_000,
|
||||||
|
pre_balances: vec![100, 200],
|
||||||
|
post_balances: vec![90, 210],
|
||||||
|
inner_instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions {
|
||||||
|
index: 0,
|
||||||
|
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstruction {
|
||||||
|
program_id_index: 1,
|
||||||
|
accounts: vec![0_u8],
|
||||||
|
data: vec![13_u8, 14],
|
||||||
|
stack_height: std::option::Option::Some(2),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
inner_instructions_none: false,
|
||||||
|
log_messages: vec!["Program log: fixture".to_owned()],
|
||||||
|
log_messages_none: false,
|
||||||
|
pre_token_balances: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::TokenBalance {
|
||||||
|
account_index: 0,
|
||||||
|
mint: "mint-fixture".to_owned(),
|
||||||
|
ui_token_amount: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UiTokenAmount {
|
||||||
|
ui_amount: 1.5,
|
||||||
|
decimals: 6,
|
||||||
|
amount: "1500000".to_owned(),
|
||||||
|
ui_amount_string: "1.5".to_owned(),
|
||||||
|
}),
|
||||||
|
owner: "owner-fixture".to_owned(),
|
||||||
|
program_id: "program-fixture".to_owned(),
|
||||||
|
}],
|
||||||
|
post_token_balances: vec![],
|
||||||
|
rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward {
|
||||||
|
pubkey: ksp_core_lib::Pubkey::new_from_array([5_u8; 32]).to_string(),
|
||||||
|
lamports: 17,
|
||||||
|
post_balance: 18,
|
||||||
|
reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Staking as i32,
|
||||||
|
commission: "5".to_owned(),
|
||||||
|
commission_bps: "500".to_owned(),
|
||||||
|
}],
|
||||||
|
loaded_writable_addresses: vec![vec![6_u8; 32]],
|
||||||
|
loaded_readonly_addresses: vec![vec![7_u8; 32]],
|
||||||
|
return_data: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::ReturnData {
|
||||||
|
program_id: vec![8_u8; 32],
|
||||||
|
data: vec![15_u8, 16],
|
||||||
|
}),
|
||||||
|
return_data_none: false,
|
||||||
|
compute_units_consumed: std::option::Option::Some(123),
|
||||||
|
cost_units: std::option::Option::Some(456),
|
||||||
|
}),
|
||||||
|
index: 3,
|
||||||
|
};
|
||||||
|
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["transactions-main".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(confirmed), slot: 42 },
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 100, nanos: 200 }),
|
||||||
|
};
|
||||||
|
let update = super::decode_transaction_update(wire).expect("transaction update fixture must decode");
|
||||||
|
assert_eq!(update.slot(), 42);
|
||||||
|
assert_eq!(update.filters()[0].as_str(), "transactions-main");
|
||||||
|
assert_eq!(update.transaction().signature().as_bytes(), &[9_u8; 64]);
|
||||||
|
assert_eq!(format!("{:?}", update.transaction().signature()), "YellowstoneTransactionSignature(<redacted>)");
|
||||||
|
assert_eq!(update.transaction().index(), 3);
|
||||||
|
assert_eq!(update.transaction().transaction().signatures().len(), 2);
|
||||||
|
assert!(update.transaction().transaction().message().versioned());
|
||||||
|
let config = update.transaction().transaction().message().config().expect("v1 config must be preserved");
|
||||||
|
assert_eq!(config.priority_fee(), std::option::Option::Some(7));
|
||||||
|
assert_eq!(config.heap_size(), std::option::Option::Some(10));
|
||||||
|
assert_eq!(update.transaction().meta().fee(), 5_000);
|
||||||
|
assert_eq!(update.transaction().meta().error().expect("error must be present").as_bytes(), &[11_u8, 12]);
|
||||||
|
assert_eq!(update.transaction().meta().inner_instructions()[0].instructions()[0].stack_height(), std::option::Option::Some(2));
|
||||||
|
assert_eq!(update.transaction().meta().pre_token_balances()[0].ui_token_amount().expect("token amount must be present").amount(), "1500000");
|
||||||
|
let token_debug = format!("{:?}", update.transaction().meta().pre_token_balances()[0]);
|
||||||
|
assert!(!token_debug.contains("mint-fixture"));
|
||||||
|
assert!(!token_debug.contains("owner-fixture"));
|
||||||
|
assert!(!token_debug.contains("program-fixture"));
|
||||||
|
assert_eq!(update.transaction().meta().rewards()[0].reward_type(), crate::YellowstoneRewardType::Staking);
|
||||||
|
assert_eq!(update.transaction().meta().loaded_writable_addresses()[0], ksp_core_lib::Pubkey::new_from_array([6_u8; 32]));
|
||||||
|
assert_eq!(update.transaction().meta().return_data().expect("return data must be present").data(), &[15_u8, 16]);
|
||||||
|
assert_eq!(update.transaction().meta().compute_units_consumed(), std::option::Option::Some(123));
|
||||||
|
assert_eq!(update.transaction().meta().cost_units(), std::option::Option::Some(456));
|
||||||
|
let debug = format!("{update:?}");
|
||||||
|
assert!(!debug.contains("Program log: fixture"));
|
||||||
|
assert!(!debug.contains("11, 12"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_transaction_status_update_preserves_error_and_rejects_malformed_signature() {
|
||||||
|
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["status".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
|
||||||
|
slot: 55,
|
||||||
|
signature: vec![2_u8; 64],
|
||||||
|
is_vote: true,
|
||||||
|
index: 4,
|
||||||
|
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![99_u8] }),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
let update = super::decode_transaction_status_update(wire).expect("transaction-status update must decode");
|
||||||
|
assert_eq!(update.slot(), 55);
|
||||||
|
assert!(update.is_vote());
|
||||||
|
assert_eq!(update.index(), 4);
|
||||||
|
assert_eq!(update.signature().as_bytes(), &[2_u8; 64]);
|
||||||
|
assert_eq!(update.error().expect("status error must be present").as_bytes(), &[99_u8]);
|
||||||
|
assert!(!format!("{update:?}").contains("99"));
|
||||||
|
let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["status".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
|
||||||
|
slot: 1,
|
||||||
|
signature: vec![0_u8; 63],
|
||||||
|
is_vote: false,
|
||||||
|
index: 0,
|
||||||
|
err: std::option::Option::None,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_transaction_status_update(malformed).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_block_filter_encodes_complete_current_wire_and_redacts_selectors() {
|
||||||
|
let mut filter = crate::YellowstoneSubscribeBlockFilter::new();
|
||||||
|
let account = ksp_core_lib::Pubkey::new_from_array([31_u8; 32]);
|
||||||
|
assert!(filter.push_account_include(account).is_ok());
|
||||||
|
filter.set_include_transactions(std::option::Option::Some(true));
|
||||||
|
filter.set_include_accounts(std::option::Option::Some(false));
|
||||||
|
filter.set_include_entries(std::option::Option::Some(true));
|
||||||
|
let cuckoo = crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 9, crate::YellowstoneCuckooHashAlgorithm::SipHash)
|
||||||
|
.expect("block Cuckoo filter must validate");
|
||||||
|
filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo));
|
||||||
|
let wire = filter.to_wire();
|
||||||
|
assert_eq!(wire.account_include, vec![account.to_string()]);
|
||||||
|
assert_eq!(wire.include_transactions, std::option::Option::Some(true));
|
||||||
|
assert_eq!(wire.include_accounts, std::option::Option::Some(false));
|
||||||
|
assert_eq!(wire.include_entries, std::option::Option::Some(true));
|
||||||
|
assert!(wire.cuckoo_account_include.is_some());
|
||||||
|
let debug = format!("{filter:?}");
|
||||||
|
assert!(debug.contains("account_include_count"));
|
||||||
|
assert!(!debug.contains(&account.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_block_update_reuses_transaction_account_entry_dtos_and_preserves_server_counts() {
|
||||||
|
let blockhash = ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string();
|
||||||
|
let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string();
|
||||||
|
let reward_pubkey = ksp_core_lib::Pubkey::new_from_array([23_u8; 32]);
|
||||||
|
let account_pubkey = ksp_core_lib::Pubkey::new_from_array([24_u8; 32]);
|
||||||
|
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["blocks-main".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
|
||||||
|
slot: 500,
|
||||||
|
blockhash: blockhash.clone(),
|
||||||
|
rewards: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Rewards {
|
||||||
|
rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward {
|
||||||
|
pubkey: reward_pubkey.to_string(),
|
||||||
|
lamports: 10,
|
||||||
|
post_balance: 11,
|
||||||
|
reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Fee as i32,
|
||||||
|
commission: "".to_owned(),
|
||||||
|
commission_bps: "".to_owned(),
|
||||||
|
}],
|
||||||
|
num_partitions: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::NumPartitions { num_partitions: 3 }),
|
||||||
|
}),
|
||||||
|
block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_700_000_000 }),
|
||||||
|
block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 499 }),
|
||||||
|
transactions: vec![minimal_transaction_info(7, 2)],
|
||||||
|
parent_slot: 499,
|
||||||
|
parent_blockhash: parent_blockhash.clone(),
|
||||||
|
executed_transaction_count: 12,
|
||||||
|
updated_account_count: 34,
|
||||||
|
accounts: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
|
||||||
|
pubkey: vec![24_u8; 32],
|
||||||
|
lamports: 77,
|
||||||
|
owner: vec![25_u8; 32],
|
||||||
|
executable: false,
|
||||||
|
rent_epoch: 4,
|
||||||
|
data: vec![1_u8, 2, 3],
|
||||||
|
write_version: 8,
|
||||||
|
txn_signature: std::option::Option::Some(vec![7_u8; 64]),
|
||||||
|
}],
|
||||||
|
entries_count: 56,
|
||||||
|
entries: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
|
||||||
|
slot: 500,
|
||||||
|
index: 4,
|
||||||
|
num_hashes: 5,
|
||||||
|
hash: vec![26_u8; 32],
|
||||||
|
executed_transaction_count: 6,
|
||||||
|
starting_transaction_index: 7,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 12, nanos: 34 }),
|
||||||
|
};
|
||||||
|
let update = super::decode_block_update(wire).expect("block update fixture must decode");
|
||||||
|
assert_eq!(update.filters()[0].as_str(), "blocks-main");
|
||||||
|
assert_eq!(update.slot(), 500);
|
||||||
|
assert_eq!(update.blockhash(), blockhash);
|
||||||
|
assert_eq!(update.parent_slot(), 499);
|
||||||
|
assert_eq!(update.parent_blockhash(), parent_blockhash);
|
||||||
|
assert_eq!(update.block_time(), std::option::Option::Some(1_700_000_000));
|
||||||
|
assert_eq!(update.block_height(), std::option::Option::Some(499));
|
||||||
|
assert_eq!(update.executed_transaction_count(), 12);
|
||||||
|
assert_eq!(update.transactions().len(), 1);
|
||||||
|
assert_eq!(update.transactions()[0].index(), 2);
|
||||||
|
assert_eq!(update.updated_account_count(), 34);
|
||||||
|
assert_eq!(update.accounts().len(), 1);
|
||||||
|
assert_eq!(update.accounts()[0].pubkey(), &account_pubkey);
|
||||||
|
assert_eq!(update.entries_count(), 56);
|
||||||
|
assert_eq!(update.entries().len(), 1);
|
||||||
|
assert_eq!(update.entries()[0].starting_transaction_index(), 7);
|
||||||
|
let rewards = update.rewards().expect("block rewards must be present");
|
||||||
|
assert_eq!(rewards.rewards().len(), 1);
|
||||||
|
assert_eq!(rewards.num_partitions(), std::option::Option::Some(3));
|
||||||
|
assert_eq!(rewards.rewards()[0].pubkey(), &reward_pubkey);
|
||||||
|
let debug = format!("{update:?}");
|
||||||
|
assert!(!debug.contains(&blockhash));
|
||||||
|
assert!(!debug.contains(&parent_blockhash));
|
||||||
|
assert!(!debug.contains(&reward_pubkey.to_string()));
|
||||||
|
assert!(!debug.contains(&account_pubkey.to_string()));
|
||||||
|
let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["blocks-main".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
|
||||||
|
slot: 1,
|
||||||
|
blockhash: "not-a-solana-hash".to_owned(),
|
||||||
|
rewards: std::option::Option::None,
|
||||||
|
block_time: std::option::Option::None,
|
||||||
|
block_height: std::option::Option::None,
|
||||||
|
transactions: vec![],
|
||||||
|
parent_slot: 0,
|
||||||
|
parent_blockhash: ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string(),
|
||||||
|
executed_transaction_count: 0,
|
||||||
|
updated_account_count: 0,
|
||||||
|
accounts: vec![],
|
||||||
|
entries_count: 0,
|
||||||
|
entries: vec![],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_block_update(malformed).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yellowstone_block_meta_and_entry_updates_preserve_optional_and_legacy_entry_fields() {
|
||||||
|
let blockhash = ksp_core_lib::Pubkey::new_from_array([41_u8; 32]).to_string();
|
||||||
|
let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([42_u8; 32]).to_string();
|
||||||
|
let meta_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["meta".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::BlockMeta(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateBlockMeta {
|
||||||
|
slot: 900,
|
||||||
|
blockhash: blockhash.clone(),
|
||||||
|
rewards: std::option::Option::None,
|
||||||
|
block_time: std::option::Option::None,
|
||||||
|
block_height: std::option::Option::None,
|
||||||
|
parent_slot: 899,
|
||||||
|
parent_blockhash: parent_blockhash.clone(),
|
||||||
|
executed_transaction_count: 17,
|
||||||
|
entries_count: 18,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
let meta = super::decode_block_meta_update(meta_wire).expect("block-meta update must decode");
|
||||||
|
assert_eq!(meta.slot(), 900);
|
||||||
|
assert_eq!(meta.blockhash(), blockhash);
|
||||||
|
assert_eq!(meta.parent_blockhash(), parent_blockhash);
|
||||||
|
assert_eq!(meta.rewards(), std::option::Option::None);
|
||||||
|
assert_eq!(meta.block_time(), std::option::Option::None);
|
||||||
|
assert_eq!(meta.block_height(), std::option::Option::None);
|
||||||
|
assert_eq!(meta.executed_transaction_count(), 17);
|
||||||
|
assert_eq!(meta.entries_count(), 18);
|
||||||
|
assert!(!format!("{meta:?}").contains(&blockhash));
|
||||||
|
let entry_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["entries".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
|
||||||
|
slot: 900,
|
||||||
|
index: 3,
|
||||||
|
num_hashes: 4,
|
||||||
|
hash: vec![43_u8; 32],
|
||||||
|
executed_transaction_count: 5,
|
||||||
|
starting_transaction_index: 0,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 6, nanos: 7 }),
|
||||||
|
};
|
||||||
|
let entry = super::decode_entry_update(entry_wire).expect("entry update must decode");
|
||||||
|
assert_eq!(entry.filters()[0].as_str(), "entries");
|
||||||
|
assert_eq!(entry.entry().slot(), 900);
|
||||||
|
assert_eq!(entry.entry().index(), 3);
|
||||||
|
assert_eq!(entry.entry().num_hashes(), 4);
|
||||||
|
assert_eq!(entry.entry().hash().as_bytes(), &[43_u8; 32]);
|
||||||
|
assert_eq!(entry.entry().executed_transaction_count(), 5);
|
||||||
|
assert_eq!(entry.entry().starting_transaction_index(), 0);
|
||||||
|
assert!(!format!("{entry:?}").contains("43, 43"));
|
||||||
|
let malformed_entry = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||||
|
filters: vec!["entries".to_owned()],
|
||||||
|
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(
|
||||||
|
yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
|
||||||
|
slot: 1,
|
||||||
|
index: 0,
|
||||||
|
num_hashes: 0,
|
||||||
|
hash: vec![0_u8; 31],
|
||||||
|
executed_transaction_count: 0,
|
||||||
|
starting_transaction_index: 0,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
created_at: std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(super::decode_entry_update(malformed_entry).is_err());
|
||||||
|
}
|
||||||
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal file
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
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 = std::pin::Pin<
|
||||||
|
std::boxed::Box<dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>> + Send + 'static>,
|
||||||
|
>;
|
||||||
|
type SubscribeDeshredStream = std::pin::Pin<
|
||||||
|
std::boxed::Box<
|
||||||
|
dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>> + Send + 'static,
|
||||||
|
>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
async fn subscribe(
|
||||||
|
&self,
|
||||||
|
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
|
||||||
|
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
|
||||||
|
return std::result::Result::Err(tonic::Status::unimplemented("streaming is outside the pre.003 fixture"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 KSP 0.2.9"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse {
|
||||||
|
first_available: std::option::Option::Some(100),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ping(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let count = request.into_inner().count;
|
||||||
|
if count == -999 {
|
||||||
|
return std::result::Result::Err(tonic::Status::permission_denied("GRPC-REMOTE-SECRET-CANARY"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::PongResponse { count }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse {
|
||||||
|
slot: 101,
|
||||||
|
blockhash: "fixture-blockhash".to_owned(),
|
||||||
|
last_valid_block_height: 999,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetBlockHeightResponse { block_height: 202 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if request.get_ref().commitment == std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32) {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
} else if let std::result::Result::Err(error) =
|
||||||
|
require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetSlotResponse { slot: 303 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if request.get_ref().blockhash != "fixture-blockhash" {
|
||||||
|
return std::result::Result::Err(tonic::Status::invalid_argument("unexpected fixture blockhash"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::IsBlockhashValidResponse { slot: 404, valid: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetVersionResponse {
|
||||||
|
version: "fixture-yellowstone-12.6".to_owned(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FixtureServer {
|
||||||
|
endpoint_url: std::string::String,
|
||||||
|
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixtureServer {
|
||||||
|
async fn start() -> Self {
|
||||||
|
let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse");
|
||||||
|
let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind");
|
||||||
|
let local_address = incoming.local_addr().expect("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 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 verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> {
|
||||||
|
let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok());
|
||||||
|
let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok());
|
||||||
|
if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-SECRET-CANARY") {
|
||||||
|
return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_commitment(actual: std::option::Option<i32>, expected: yellowstone_grpc_proto::geyser::CommitmentLevel) -> std::result::Result<(), tonic::Status> {
|
||||||
|
if actual != std::option::Option::Some(expected as i32) {
|
||||||
|
return std::result::Result::Err(tonic::Status::invalid_argument("fixture commitment mismatch"));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_settings(url: &str, unary_timeout: std::time::Duration) -> crate::YellowstoneGrpcEndpointSettings {
|
||||||
|
let defaults = crate::YellowstoneGrpcSessionSettings::default();
|
||||||
|
let session = crate::YellowstoneGrpcSessionSettings::new(
|
||||||
|
defaults.connect_timeout(),
|
||||||
|
unary_timeout,
|
||||||
|
defaults.close_timeout(),
|
||||||
|
defaults.reconnect().clone(),
|
||||||
|
defaults.request_channel_capacity(),
|
||||||
|
defaults.update_channel_capacity(),
|
||||||
|
defaults.max_inbound_message_size_bytes(),
|
||||||
|
defaults.max_outbound_message_size_bytes(),
|
||||||
|
);
|
||||||
|
let metadata = std::vec![
|
||||||
|
crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"),
|
||||||
|
crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("fixture secret metadata must be valid"),
|
||||||
|
];
|
||||||
|
return crate::YellowstoneGrpcEndpointSettings::new(
|
||||||
|
"fixture",
|
||||||
|
true,
|
||||||
|
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
|
||||||
|
crate::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.with_metadata(metadata)
|
||||||
|
.expect("fixture metadata settings must validate");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_unary_fixture_covers_all_seven_standard_methods_and_metadata() {
|
||||||
|
let server = FixtureServer::start().await;
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let client = channel.standard_unary_client();
|
||||||
|
let replay = client.subscribe_replay_info().await.expect("ReplayInfo fixture must succeed");
|
||||||
|
assert_eq!(replay.first_available(), std::option::Option::Some(100));
|
||||||
|
let pong = client.ping(7).await.expect("Ping fixture must succeed");
|
||||||
|
assert_eq!(pong.count(), 7);
|
||||||
|
let latest = client
|
||||||
|
.get_latest_blockhash(std::option::Option::Some(crate::SolanaCommitment::Confirmed))
|
||||||
|
.await
|
||||||
|
.expect("latest blockhash fixture must succeed");
|
||||||
|
assert_eq!(latest.slot(), 101);
|
||||||
|
assert_eq!(latest.blockhash(), "fixture-blockhash");
|
||||||
|
assert_eq!(latest.last_valid_block_height(), 999);
|
||||||
|
let height = client.get_block_height(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("block height fixture must succeed");
|
||||||
|
assert_eq!(height.block_height(), 202);
|
||||||
|
let slot = client.get_slot(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("slot fixture must succeed");
|
||||||
|
assert_eq!(slot.slot(), 303);
|
||||||
|
let validity = client
|
||||||
|
.is_blockhash_valid("fixture-blockhash", std::option::Option::Some(crate::SolanaCommitment::Confirmed))
|
||||||
|
.await
|
||||||
|
.expect("blockhash validity fixture must succeed");
|
||||||
|
assert_eq!(validity.slot(), 404);
|
||||||
|
assert!(validity.valid());
|
||||||
|
let version = client.get_version().await.expect("version fixture must succeed");
|
||||||
|
assert_eq!(version.version(), "fixture-yellowstone-12.6");
|
||||||
|
let rendered = format!("{client:?} {channel:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_remote_status_does_not_copy_message_details_or_metadata() {
|
||||||
|
let server = FixtureServer::start().await;
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let error = channel.standard_unary_client().ping(-999).await.expect_err("fixture must return remote status");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
|
||||||
|
let rendered = format!("{error:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-REMOTE-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_unary_timeout_is_bounded_and_safe() {
|
||||||
|
let server = FixtureServer::start().await;
|
||||||
|
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_millis(5));
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
|
||||||
|
let error = channel
|
||||||
|
.standard_unary_client()
|
||||||
|
.get_slot(std::option::Option::Some(crate::SolanaCommitment::Processed))
|
||||||
|
.await
|
||||||
|
.expect_err("delayed fixture must exceed unary timeout");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
|
||||||
|
let rendered = format!("{error:?}");
|
||||||
|
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(server.endpoint_url.as_str()));
|
||||||
|
server.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn yellowstone_invalid_blockhash_is_rejected_before_io() {
|
||||||
|
let settings = fixture_settings("http://127.0.0.1:1", std::time::Duration::from_secs(1));
|
||||||
|
let channel = crate::YellowstoneGrpcChannel::prepare(&settings).expect("lazy fixture channel must prepare");
|
||||||
|
let error = channel
|
||||||
|
.standard_unary_client()
|
||||||
|
.is_blockhash_valid(" ", std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect_err("invalid blockhash must be rejected locally");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/http_client.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/executor.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/http_executor.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
|
fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
|
||||||
let role = crate::HttpEndpointRoleSettings::new(
|
let role = crate::HttpEndpointRoleSettings::new(
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/http_pool.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||||
return crate::HttpEndpointRoleSettings::new(
|
return crate::HttpEndpointRoleSettings::new(
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/http_resilience.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||||
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/http_settings.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||||
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
|
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-wallet-lib/Cargo.toml
|
# file: crates/ksp-wallet-lib/Cargo.toml
|
||||||
# version: 6
|
# version: 7
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-wallet-lib"
|
name = "ksp-wallet-lib"
|
||||||
@@ -9,12 +9,12 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
|
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||||
|
base64.workspace = true
|
||||||
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
|
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||||
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
|
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
|
||||||
getrandom.workspace = true
|
getrandom.workspace = true
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
base64.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
solana-keypair.workspace = true
|
solana-keypair.workspace = true
|
||||||
|
|||||||
139
deltas/0.2.9/pre.001-fix.001.md
Normal file
139
deltas/0.2.9/pre.001-fix.001.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.001-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.001-fix.001` — sizing/session + architecture provider Yellowstone
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.001
|
||||||
|
workspace.package.version = 0.2.9-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce correctif est **documentaire uniquement**. Conformément à `VER-ID-008`, il ne modifie pas `workspace.package.version`.
|
||||||
|
|
||||||
|
## 2. Motif du correctif
|
||||||
|
|
||||||
|
La première livraison `pre.001` contenait bien une mention générale du budget `15–20 minutes`, mais elle ne rendait pas assez visible le contrat normatif complet de `PROMPT_STRUCTURE.md` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
une prerelease estimée > 15–20 min doit être scindée
|
||||||
|
une release concrète doit rester ouvrable et clôturable dans une seule session de chat
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle traitait aussi PublicNode et OrbitFlare principalement comme des environnements de smoke derrière un seul backend Yellowstone provider-neutral. Cette représentation ne reprenait pas suffisamment la séparation déjà validée pour WebSocket entre moteur partagé, protocole standard et adaptation provider.
|
||||||
|
|
||||||
|
## 3. Décisions corrigées
|
||||||
|
|
||||||
|
### 3.1 Dimensionnement
|
||||||
|
|
||||||
|
Le forecast `0.2.9` affiche désormais explicitement pour **chaque prerelease** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
budget nominal = 15–20 minutes maximum de travail effectif
|
||||||
|
```
|
||||||
|
|
||||||
|
et le gate global :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 complète <= une session de chat
|
||||||
|
```
|
||||||
|
|
||||||
|
Si une tranche dépasse ce budget ou si la clôture de la release dans la session devient incertaine, le scope est scindé avant implémentation lourde supplémentaire.
|
||||||
|
|
||||||
|
Le forecast est recalibré à **11 prereleases prévues** avant `rel.001`, soit environ `165–220 minutes` de travail effectif nominal hors temps d'attente des commandes.
|
||||||
|
|
||||||
|
### 3.2 Architecture Yellowstone
|
||||||
|
|
||||||
|
`0.2.9` distingue désormais trois niveaux dans `ksp-onchain-transport-lib` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 moteur client Yellowstone gRPC partagé
|
||||||
|
N2 façade Solana Yellowstone standard
|
||||||
|
N3 première intégration provider PublicNode / Allnodes-backed
|
||||||
|
```
|
||||||
|
|
||||||
|
Le moteur reste Yellowstone-specific ; KSP ne crée pas une abstraction gRPC universelle ni un serveur/plugin Geyser.
|
||||||
|
|
||||||
|
La règle provider est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun second actor/channel/stream par provider
|
||||||
|
aucune façade spécialisée vide
|
||||||
|
façade/type provider public uniquement si une différence réelle de contrat le justifie
|
||||||
|
```
|
||||||
|
|
||||||
|
PublicNode devient donc la **première intégration concrète** de `0.2.9`, avec Mainnet et Testnet comme cibles live opt-in. Il réutilise le moteur et le wire standard.
|
||||||
|
|
||||||
|
### 3.3 Releases providers suivantes
|
||||||
|
|
||||||
|
La séquence fonctionnelle est recalibrée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 moteur Yellowstone + Solana standard + PublicNode
|
||||||
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
0.2.12 eRPC Yellowstone gRPC, conditionnel après réaudit accès/capabilities
|
||||||
|
0.2.13 off-chain price transport
|
||||||
|
0.2.14 Price Desk + intégration prix Wallet Desk
|
||||||
|
0.2.15 interface/wire foundation
|
||||||
|
0.2.16 program-api foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque future release provider réutilise `0.2.9`. Si le gate d'un provider montre qu'il ne diffère que par une URL interchangeable, il ne doit pas provoquer une façade artificielle ; son scope peut être réduit/fusionné avant implémentation.
|
||||||
|
|
||||||
|
### 3.4 Rust/toolchain
|
||||||
|
|
||||||
|
Les numéros de versions Rust upstream/MSRV ne sont plus suivis comme information de planification ordinaire.
|
||||||
|
|
||||||
|
KSP utilise la **Rust stable courante de l'opérateur**. Le seul gate utile est que les dépendances sélectionnées compilent avec cette stable. Un minimum Rust d'une dépendance n'est documenté que s'il devient un blocage réel.
|
||||||
|
|
||||||
|
## 4. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.9/pre.001-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers non modifiés intentionnellement
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
crates/**
|
||||||
|
config/**
|
||||||
|
.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt `014` reste l'autorité historique ayant ouvert `pre.001`; le présent fix trace le recalibrage résultant du gate et devient l'état de planification courant.
|
||||||
|
|
||||||
|
## 7. Validation
|
||||||
|
|
||||||
|
À exécuter pour le correctif documentaire :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo check/clippy/test` supplémentaire n'est requis par le contenu du fix puisqu'aucun artefact code/build/runtime/config n'est modifié ; la baseline technique reste celle enregistrée par `pre.001`.
|
||||||
|
|
||||||
|
## 8. État de sortie
|
||||||
|
|
||||||
|
```text
|
||||||
|
forecast visible et conforme à PROMPT_STRUCTURE.md
|
||||||
|
15–20 min max par prerelease explicités
|
||||||
|
release <= une session explicitée
|
||||||
|
moteur Yellowstone séparé de la façade standard
|
||||||
|
PublicNode promu de smoke à première intégration provider
|
||||||
|
OrbitFlare/Helius/eRPC reportés dans des releases dédiées
|
||||||
|
séquence prix/interface/program décalée en conséquence
|
||||||
|
aucun changement runtime/dependency
|
||||||
|
```
|
||||||
154
deltas/0.2.9/pre.001-fix.002.md
Normal file
154
deltas/0.2.9/pre.001-fix.002.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.001-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.001-fix.002` — provider divergence + waitlist Yellowstone
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.001-fix.001
|
||||||
|
workspace.package.version = 0.2.9-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce correctif est **documentaire uniquement**. Conformément au workflow de version, il ne modifie pas `workspace.package.version`.
|
||||||
|
|
||||||
|
## 2. Motif du correctif
|
||||||
|
|
||||||
|
Le cadrage `pre.001-fix.001` avait correctement séparé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 moteur Yellowstone partagé
|
||||||
|
N2 façade Solana Yellowstone standard
|
||||||
|
N3 intégration provider
|
||||||
|
```
|
||||||
|
|
||||||
|
mais la formulation « sans duplication du moteur ni du wire standard » pouvait être comprise comme une hypothèse d'équivalence wire/capabilities entre Yellowstone upstream et tous les providers.
|
||||||
|
|
||||||
|
Cette hypothèse n'est pas acceptable : un provider peut implémenter seulement un sous-ensemble du standard, imposer des limites/auth/lifecycle différents ou proposer des extensions propres.
|
||||||
|
|
||||||
|
Le correctif ferme aussi la liste des providers à implémenter à court terme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 PublicNode
|
||||||
|
0.2.10 OrbitFlare
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
```
|
||||||
|
|
||||||
|
Les autres providers ne reçoivent plus de numéro de release réservé.
|
||||||
|
|
||||||
|
## 3. Décisions corrigées
|
||||||
|
|
||||||
|
### 3.1 Règle N1 / N2 / N3
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 = moteur client Yellowstone unique
|
||||||
|
N2 = contrat Solana Yellowstone standard KSP
|
||||||
|
N3 = adaptation provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Invariant N1 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun second actor/channel/stream moteur par provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Règle N2/N3 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
réutiliser N2 lorsqu'une capacité provider est réellement compatible
|
||||||
|
restreindre explicitement N2 lorsqu'une capacité standard est absente/non supportée
|
||||||
|
ajouter une extension typed N3 lorsqu'un provider étend le protocole/wire
|
||||||
|
porter en N3 les différences auth/metadata/compression/keepalive/replay/from_slot/limites/lifecycle
|
||||||
|
ne jamais présumer une équivalence provider/standard sans preuve
|
||||||
|
```
|
||||||
|
|
||||||
|
Une divergence réelle n'est donc ni dupliquée arbitrairement ni masquée derrière le standard.
|
||||||
|
|
||||||
|
### 3.2 Providers actuellement planifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 moteur Yellowstone + standard Solana + PublicNode
|
||||||
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque release provider commence par un delta audit avec Yellowstone upstream courant et ne matérialise que ses différences réelles.
|
||||||
|
|
||||||
|
### 3.3 Providers en attente
|
||||||
|
|
||||||
|
Les providers suivants sont retirés de la séquence numérotée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TODO eRPC
|
||||||
|
TODO Triton
|
||||||
|
TODO Alchemy
|
||||||
|
TODO QuickNode
|
||||||
|
TODO Chainstack
|
||||||
|
IDEAS Tatum
|
||||||
|
IDEAS Shyft
|
||||||
|
IDEAS Solinfra
|
||||||
|
IDEAS NodeFlare
|
||||||
|
IDEAS autres providers
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun Config profile, type public, dépendance, smoke ou forecast n'est préparé pour eux tant qu'une décision explicite d'implémentation n'est pas prise.
|
||||||
|
|
||||||
|
### 3.4 Séquence fonctionnelle libérée
|
||||||
|
|
||||||
|
La suppression de la réservation `0.2.12 eRPC` ramène la suite active à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.12 off-chain price transport
|
||||||
|
0.2.13 Price Desk + intégration prix Wallet Desk
|
||||||
|
0.2.14 interface/wire foundation
|
||||||
|
0.2.15 program-api foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
ROADMAP.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.9/pre.001-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers non modifiés intentionnellement
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
crates/**
|
||||||
|
config/**
|
||||||
|
.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt `014` reste l'autorité historique d'ouverture ; le plan et les deltas successifs enregistrent les décisions prises pendant le gate.
|
||||||
|
|
||||||
|
## 7. Validation
|
||||||
|
|
||||||
|
Correctif documentaire :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo check/clippy/test` supplémentaire n'est requis par ce correctif tant qu'aucun artefact code/build/runtime/config n'est modifié.
|
||||||
|
|
||||||
|
## 8. État de sortie
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 moteur unique explicite
|
||||||
|
N2 standard non présumé universel
|
||||||
|
N3 peut réutiliser/restreindre/étendre
|
||||||
|
PublicNode/OrbitFlare/Helius seuls providers planifiés
|
||||||
|
reste des providers en TODO/IDEAS non numérotés
|
||||||
|
off-chain price revient en 0.2.12
|
||||||
|
aucun changement runtime/dependency
|
||||||
|
```
|
||||||
377
deltas/0.2.9/pre.001.md
Normal file
377
deltas/0.2.9/pre.001.md
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.001` — audit/sizing Yellowstone gRPC standard
|
||||||
|
|
||||||
|
## 1. Base requise et vérifiée
|
||||||
|
|
||||||
|
Archive autoritaire fournie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
khadhroony-solana-project-v0.2.8-full-from-gitea.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
État vérifié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8
|
||||||
|
deltas/0.2.8/rel.001.md présent
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md présent
|
||||||
|
prompt fourni = byte-identique au prompt embarqué
|
||||||
|
metadata .git absente de l'archive
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette livraison ouvre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.1
|
||||||
|
commit attendu = v0.2.9-pre.001
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
`pre.001` exécute le gate obligatoire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
lecture base/règles/architecture
|
||||||
|
baseline stable
|
||||||
|
réaudit Yellowstone actuel
|
||||||
|
matrice service/proto
|
||||||
|
licences/dependencies/MSRV
|
||||||
|
architecture provider-neutral
|
||||||
|
threat model/resources/backpressure/replay
|
||||||
|
sizing et forecast
|
||||||
|
récensement des providers gRPC gratuits durables
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune implémentation gRPC lourde n'est ajoutée.
|
||||||
|
|
||||||
|
## 3. Baseline opérateur acquise
|
||||||
|
|
||||||
|
Preuve fournie sur `v0.2.8` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK / clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo tree -p ksp-onchain-transport-lib fourni
|
||||||
|
cargo tree --duplicates fourni
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport pendant le workspace test :
|
||||||
|
|
||||||
|
```text
|
||||||
|
335 unit
|
||||||
|
41 public_api
|
||||||
|
34 release_completeness
|
||||||
|
4 doctests
|
||||||
|
```
|
||||||
|
|
||||||
|
Les live smokes restent opt-in/ignored par défaut.
|
||||||
|
|
||||||
|
## 4. Réaudit Yellowstone du 2026-08-23
|
||||||
|
|
||||||
|
Divergence importante par rapport au snapshot du start prompt :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompt snapshot release = v14.2.2+solana.4.1.0
|
||||||
|
release GitHub courante = v15.1.2+solana.4.2.0, publiée 2026-08-18, Rust 1.96.1
|
||||||
|
client crate courant = yellowstone-grpc-client 13.3.0
|
||||||
|
proto crate courant = yellowstone-grpc-proto 12.6.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Service Geyser courant inventorié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Subscribe IN
|
||||||
|
SubscribeDeshred OUT — Triton extension / pre-execution
|
||||||
|
SubscribeReplayInfo IN
|
||||||
|
Ping IN
|
||||||
|
GetLatestBlockhash IN
|
||||||
|
GetBlockHeight IN
|
||||||
|
GetSlot IN
|
||||||
|
IsBlockhashValid IN
|
||||||
|
GetVersion IN
|
||||||
|
```
|
||||||
|
|
||||||
|
`SubscribeRequest` standard retenu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
accounts
|
||||||
|
slots
|
||||||
|
transactions
|
||||||
|
transactions_status
|
||||||
|
blocks
|
||||||
|
blocks_meta
|
||||||
|
entry
|
||||||
|
commitment
|
||||||
|
accounts_data_slice
|
||||||
|
ping
|
||||||
|
from_slot
|
||||||
|
```
|
||||||
|
|
||||||
|
Capacités nouvelles/actuelles retenues car présentes dans le proto publié standard :
|
||||||
|
|
||||||
|
```text
|
||||||
|
CuckooFilter account/block/transaction include
|
||||||
|
TokenAccountExpansionControlFlag ALL/BALANCE_CHANGED
|
||||||
|
lamports eq/ne/lt/gt
|
||||||
|
slot interslot updates
|
||||||
|
```
|
||||||
|
|
||||||
|
`SubscribeUpdate` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account
|
||||||
|
slot
|
||||||
|
transaction
|
||||||
|
transaction_status
|
||||||
|
block
|
||||||
|
ping
|
||||||
|
pong
|
||||||
|
block_meta
|
||||||
|
entry
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Licence et dépendances
|
||||||
|
|
||||||
|
Upstream :
|
||||||
|
|
||||||
|
```text
|
||||||
|
repository default = AGPL-3.0-only
|
||||||
|
client subtree = Apache-2.0
|
||||||
|
proto subtree = Apache-2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Stratégies comparées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
A client upstream + proto -> rejetée comme default
|
||||||
|
B proto publié + client KSP autour de Tonic -> RETENUE
|
||||||
|
C proto vendored KSP -> fallback seulement
|
||||||
|
```
|
||||||
|
|
||||||
|
Raisons de B :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pas de copie proto
|
||||||
|
wire officiel disponible
|
||||||
|
licence Apache compatible MIT
|
||||||
|
reconnect/backpressure/redaction KSP-owned
|
||||||
|
upstream raw types cachés
|
||||||
|
stack tonic 0.14 cohérente avec HTTP/2 moderne existant
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dependency n'est ajoutée dans `pre.001`. `pre.002` doit matérialiser les features minimales et produire les graphes Cargo.
|
||||||
|
|
||||||
|
## 6. Replay/continuity
|
||||||
|
|
||||||
|
L'upstream a corrigé en juillet 2026 un cas où `from_slot` pour blocks pouvait être accepté puis reprendre live avec un **state gap** ; l'autoreconnect upstream a aussi été renforcé pour traiter l'equivocation entre nodes.
|
||||||
|
|
||||||
|
KSP ferme donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconnect/resubscribe = oui, borné et KSP-owned
|
||||||
|
from_slot = oui
|
||||||
|
ReplayInfo = oui
|
||||||
|
exactly-once = non garanti
|
||||||
|
lossless = non garanti
|
||||||
|
gaps/duplicates = observables
|
||||||
|
node divergence = couverture explicite seulement si prouvée
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Config / architecture
|
||||||
|
|
||||||
|
Décisions :
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend gRPC distinct de HTTP/WS
|
||||||
|
pas de WsProtocolKind gRPC
|
||||||
|
pas de raw Tonic client public
|
||||||
|
Transport owns Yellowstone
|
||||||
|
Config -> Transport seulement
|
||||||
|
metadata publique et metadata sensible séparées
|
||||||
|
Transport ne lit aucun env
|
||||||
|
```
|
||||||
|
|
||||||
|
Le schéma V2 actuel étant fermé, l'ajout gRPC cible :
|
||||||
|
|
||||||
|
```text
|
||||||
|
format_version = 3
|
||||||
|
globals.grpc_defaults
|
||||||
|
profiles[].grpc_endpoints[]
|
||||||
|
metadata[]
|
||||||
|
secret_metadata[]
|
||||||
|
```
|
||||||
|
|
||||||
|
V1 et V2 restent backward-readable.
|
||||||
|
|
||||||
|
## 8. Providers gRPC gratuits durables
|
||||||
|
|
||||||
|
### PublicNode / Allnodes-backed — priorité 1
|
||||||
|
|
||||||
|
Confirmé officiellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Solana Mainnet Yellowstone GRPC
|
||||||
|
Solana Testnet GRPC
|
||||||
|
Mainnet endpoint = solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
service annoncé free/public
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision : **dans `0.2.9`**, mais pas sous forme d'adapter public spécifique. PublicNode sert de premier smoke live standard provider-neutral.
|
||||||
|
|
||||||
|
Le hostname Testnet exact n'est pas figé avant vérification officielle/live.
|
||||||
|
|
||||||
|
### OrbitFlare — priorité 2
|
||||||
|
|
||||||
|
Plan Free officiel :
|
||||||
|
|
||||||
|
```text
|
||||||
|
$0/mo
|
||||||
|
10 RPS
|
||||||
|
1 TPS
|
||||||
|
gRPC Devnet only
|
||||||
|
Unlimited credit limits
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision : **dans `0.2.9` comme second smoke live**, via generic secret metadata Config -> Transport. Aucun type `OrbitFlareGrpc*` public.
|
||||||
|
|
||||||
|
### Tatum — tertiaire
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet Yellowstone gRPC
|
||||||
|
Free plan durable
|
||||||
|
3 RPS
|
||||||
|
100K lifetime credits
|
||||||
|
5 subscriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision : opérateur optionnel, pas gate durable principal à cause du quota lifetime.
|
||||||
|
|
||||||
|
### Vérifiés non gratuits / non applicables
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius Free sans gRPC
|
||||||
|
Shyft Free = No gRPC Access
|
||||||
|
Alchemy PAYG/Enterprise requis
|
||||||
|
QuickNode Scale/Business ou add-on payant
|
||||||
|
Chainstack add-on Yellowstone payant, Growth+
|
||||||
|
ERPC Geyser payant ; trial seulement 1 jour
|
||||||
|
NodeFlare plan Yellowstone mensuel payant
|
||||||
|
Bitquery CoreCast custom, pas Yellowstone standard ; stream free non garanti
|
||||||
|
```
|
||||||
|
|
||||||
|
Candidat à revalider :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Solinfra = free tier + Yellowstone annoncés publiquement, mais accès gRPC inclus dans Free non confirmé
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Threat model
|
||||||
|
|
||||||
|
Le plan ferme explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata/URI credential leak
|
||||||
|
Tonic Status arbitraire
|
||||||
|
oversized messages
|
||||||
|
stream flood/backpressure
|
||||||
|
filter explosion / collisions de noms
|
||||||
|
unknown enum/oneof
|
||||||
|
server/client half-close
|
||||||
|
reconnect loop
|
||||||
|
node divergence
|
||||||
|
late updates après mutation
|
||||||
|
from_slot gaps/duplicates
|
||||||
|
TLS failures
|
||||||
|
unary timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Forecast recalibré
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 DONE audit/sizing/provider/deps/license
|
||||||
|
pre.002 deps + settings/errors/facade + cargo tree
|
||||||
|
pre.003 TLS/metadata + local fixture + 7 unary
|
||||||
|
pre.004 Subscribe common + Cuckoo/token/from_slot/bounds
|
||||||
|
pre.005 Accounts + Slots
|
||||||
|
pre.006 Transactions + transaction_status
|
||||||
|
pre.007 Blocks + block_meta + entry
|
||||||
|
pre.008 bidi lifecycle/backpressure/half-close/shutdown
|
||||||
|
pre.009 reconnect/replay/gaps/duplicates/equivocation
|
||||||
|
pre.010 Config V3
|
||||||
|
pre.011 PublicNode/OrbitFlare live + compliance + final cargo graphs
|
||||||
|
pre.012 docs/workspace final + prompt 0.2.10
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque tranche vise ~15–20 minutes ; `pre.012` n'est pas une deadline.
|
||||||
|
|
||||||
|
Critères de split : conflit MSRV/stack réseau, explosion DTO transaction/block, nouvelle évolution proto matérielle, replay trop large, ou comportement provider-specific contaminant le contrat standard.
|
||||||
|
|
||||||
|
## 11. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.9/pre.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. Fichiers volontairement inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
ROADMAP.md
|
||||||
|
CHANGELOG.md
|
||||||
|
.env.example
|
||||||
|
config/**
|
||||||
|
crates/**
|
||||||
|
docs/architecture/**
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun gRPC runtime/config/dependency n'est introduit avant le gate positif de cette tranche.
|
||||||
|
|
||||||
|
## 14. Validations du sandbox de préparation
|
||||||
|
|
||||||
|
Le sandbox a pu exécuter :
|
||||||
|
|
||||||
|
```text
|
||||||
|
inspection archive/version/rel/prompt OK
|
||||||
|
comparaison prompt fourni / prompt archive OK
|
||||||
|
lecture règles/architecture/plans/code/config OK
|
||||||
|
réaudit upstream Yellowstone/provider/dependencies OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK / clean après overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne dispose pas de Cargo ; aucun nouveau `cargo check/clippy/test/tree` n'est déclaré réussi localement après le changement de version/documentation.
|
||||||
|
|
||||||
|
## 15. Validation opérateur attendue avant commit
|
||||||
|
|
||||||
|
Comme `pre.001` ne modifie aucun Rust/dependency/config runtime, exécuter au minimum :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu : aucune nouvelle dependency gRPC avant `pre.002`.
|
||||||
105
deltas/0.2.9/pre.002-fix.001.md
Normal file
105
deltas/0.2.9/pre.002-fix.001.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.002-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.002-fix.001` — Clippy + runtime Tokio du channel lazy
|
||||||
|
|
||||||
|
## 1. Déclencheur
|
||||||
|
|
||||||
|
Le premier gate opérateur de `pre.002` a fourni les preuves suivantes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS avec warning channel dead_code
|
||||||
|
cargo clippy --workspace --all-targets FAIL implicit_return dans grpc_settings test
|
||||||
|
cargo test -p ksp-onchain-transport-lib FAIL 344/345 : connect_lazy hors runtime Tokio
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies PASS 3/3
|
||||||
|
cargo tree Transport / duplicates fournis et inspectés
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test gRPC a révélé que `tonic::transport::Endpoint::connect_lazy()` nécessite un reactor Tokio actif et peut paniquer s'il est appelé depuis un contexte synchrone sans runtime. Ce comportement doit être borné dans la surface KSP au lieu d'être laissé comme précondition implicite Tonic.
|
||||||
|
|
||||||
|
## 2. Corrections
|
||||||
|
|
||||||
|
### Clippy
|
||||||
|
|
||||||
|
La closure qui génère l'inventaire d'endpoints excessif dans `unit_tests/grpc_settings.rs` utilise désormais un `return` explicite, conformément à `clippy::implicit_return = deny`.
|
||||||
|
|
||||||
|
### Runtime Tokio
|
||||||
|
|
||||||
|
`YellowstoneGrpcChannel::prepare()` vérifie maintenant :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tokio::runtime::Handle::try_current()
|
||||||
|
```
|
||||||
|
|
||||||
|
avant l'appel à `connect_lazy()`. En l'absence de runtime actif, KSP retourne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
onchain_transport/grpc_channel_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
avec uniquement `endpoint_name` comme contexte sûr. L'URL et le message interne Tonic ne sont pas réémis.
|
||||||
|
|
||||||
|
Le test positif devient un `#[tokio::test(flavor = "current_thread")]`. Un nouveau test synchrone prouve que l'absence de runtime est retournée comme erreur sans exposer l'URL canari.
|
||||||
|
|
||||||
|
### Champ Tonic
|
||||||
|
|
||||||
|
Le channel **reste un champ de production**. Il n'est pas placé sous `#[cfg(test)]`, car `pre.002` matérialise réellement la ressource physique N1 qui sera consommée en `pre.003`.
|
||||||
|
|
||||||
|
Le champ privé est temporairement nommé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
_channel: tonic::transport::Channel
|
||||||
|
```
|
||||||
|
|
||||||
|
pour exprimer qu'il est intentionnellement conservé mais pas encore consommé par une opération de production dans cette tranche. Cela supprime le warning `dead_code` sans modifier la shape production/test.
|
||||||
|
|
||||||
|
## 3. Graphe Cargo observé
|
||||||
|
|
||||||
|
Le gate opérateur confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic 0.14.6
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
prost/prost-types 0.14.4
|
||||||
|
solana-pubkey 4.3.0 unifié
|
||||||
|
http 1.5.0
|
||||||
|
hyper 1.11.0
|
||||||
|
hyper-util 0.1.20
|
||||||
|
tower 0.5.3
|
||||||
|
bytes 1.12.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Les occurrences Prost 0.14.4 dans `--duplicates` viennent des unités runtime/build (`prost-build`, `tonic-prost-build`) de la même version ; aucune seconde version concurrente n'est introduite.
|
||||||
|
|
||||||
|
## 4. Version
|
||||||
|
|
||||||
|
Le fix touche le code/runtime ; la version workspace devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.2.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu après validation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9-pre.002-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Validation requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Le passage à `pre.003` est interdit tant que ces gates ne sont pas verts ou qu'un nouvel écart n'est pas traité par un fix supplémentaire.
|
||||||
68
deltas/0.2.9/pre.002-fix.002.md
Normal file
68
deltas/0.2.9/pre.002-fix.002.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.002-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.002-fix.002` — canari public API sous runtime Tokio
|
||||||
|
|
||||||
|
## 1. Déclencheur
|
||||||
|
|
||||||
|
Le second gate opérateur, après `pre.002-fix.001`, confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test -p ksp-onchain-transport-lib unit PASS 346/346
|
||||||
|
public_api FAIL 41/42
|
||||||
|
dependency canary Core PASS 3/3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le seul échec restant est le canari :
|
||||||
|
|
||||||
|
```text
|
||||||
|
public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root
|
||||||
|
```
|
||||||
|
|
||||||
|
Il appelle `YellowstoneGrpcChannel::prepare()` depuis un `#[test]` synchrone. Depuis `fix.001`, `prepare()` exige volontairement un runtime Tokio actif avant `tonic::transport::Endpoint::connect_lazy()` ; le canari public API n'avait pas encore été aligné sur cette précondition KSP explicite.
|
||||||
|
|
||||||
|
## 2. Correction
|
||||||
|
|
||||||
|
Le test d'intégration devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn public_v0_2_9_pre_002_...
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun code N1 de production n'est modifié. En particulier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcChannel::prepare() inchangé
|
||||||
|
_channel de production conservé
|
||||||
|
aucun cfg(test) ajouté au channel
|
||||||
|
aucune dépendance/feature Cargo ajoutée
|
||||||
|
aucun TLS/metadata/unary anticipé
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Version
|
||||||
|
|
||||||
|
Le correctif touche le code de test et fait partie du signal technique de la prerelease :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.2.fix.2
|
||||||
|
commit attendu = v0.2.9-pre.002-fix.002
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Validation requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo n'ont pas changé avec `fix.002`; ils ne nécessitent pas une nouvelle inspection structurelle, mais peuvent être relancés si l'opérateur veut conserver une preuve complète du gate final `pre.002`.
|
||||||
259
deltas/0.2.9/pre.002.md
Normal file
259
deltas/0.2.9/pre.002.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.002` — moteur Yellowstone : dépendances + settings/errors + channel minimal
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise uniquement la première fondation **N1 moteur Yellowstone gRPC** validée par `pre.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
proto Yellowstone publié
|
||||||
|
+ dépendances Tonic minimales
|
||||||
|
+ settings runtime bornés/redacted
|
||||||
|
+ error code initial gRPC
|
||||||
|
+ channel Tonic lazy privé
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle ne commence pas encore N2 fonctionnel : aucun unary Yellowstone, aucune metadata/auth, aucune fixture serveur gRPC, aucun `Subscribe`, aucun lifecycle bidi et aucune intégration PublicNode ne sont introduits ici.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu après validation opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Dépendances matérialisées
|
||||||
|
|
||||||
|
Stratégie B conservée, mais avec le feature set le plus étroit utile à cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace root:
|
||||||
|
tonic = ^0.14, default-features = false
|
||||||
|
yellowstone-grpc-proto = ^12.6, default-features = false
|
||||||
|
|
||||||
|
ksp-onchain-transport-lib:
|
||||||
|
tonic workspace = true, features = ["channel"]
|
||||||
|
yellowstone-grpc-proto workspace = true, aucune feature locale
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicitement absents :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-client
|
||||||
|
tonic-prost direct KSP
|
||||||
|
prost direct KSP
|
||||||
|
prost-types direct KSP
|
||||||
|
tokio-stream nouveau
|
||||||
|
feature tonic du proto
|
||||||
|
feature tonic-compression du proto
|
||||||
|
TLS Tonic
|
||||||
|
```
|
||||||
|
|
||||||
|
La crate publiée `yellowstone-grpc-proto` fournit les messages Protobuf générés utiles sans que KSP copie les `.proto`. Son build utilise son outillage `protoc` vendored ; aucun `protoc` système ni `build.rs` KSP n'est ajouté.
|
||||||
|
|
||||||
|
Le feature optionnel `tonic` de la crate proto n'est pas activé en `pre.002` : le moteur ne consomme encore aucun client RPC généré. `pre.003` devra justifier explicitement toute feature/dependency supplémentaire nécessaire aux unary, TLS et metadata.
|
||||||
|
|
||||||
|
## 3. Settings Yellowstone N1
|
||||||
|
|
||||||
|
Ajout de `grpc_settings.rs` avec les contrats publics crate-root :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcEndpointUrl
|
||||||
|
YellowstoneGrpcProviderName
|
||||||
|
YellowstoneGrpcClusterName
|
||||||
|
YellowstoneGrpcReconnectSettings
|
||||||
|
YellowstoneGrpcSessionSettings
|
||||||
|
YellowstoneGrpcEndpointSettings
|
||||||
|
YellowstoneGrpcTransportSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Principes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport-owned
|
||||||
|
aucun Config/env
|
||||||
|
aucun WsProtocolKind/WsEndpointSettings
|
||||||
|
URL http/https distincte de HTTP/WS et Debug redacted
|
||||||
|
provider/cluster = descripteurs ouverts, pas des enums commerciaux fermés
|
||||||
|
URL/descripteurs/nombre d’endpoints bornés
|
||||||
|
connect/unary/close timeouts bornés
|
||||||
|
reconnect attempts/backoff bornés
|
||||||
|
request/update channel capacities bornées
|
||||||
|
max inbound/outbound message sizes bornées
|
||||||
|
au moins un endpoint configuré et enabled
|
||||||
|
endpoint names uniques
|
||||||
|
```
|
||||||
|
|
||||||
|
Les defaults servent uniquement de runtime defaults N1 ; ils ne deviennent pas des quotas provider.
|
||||||
|
|
||||||
|
## 4. Channel minimal
|
||||||
|
|
||||||
|
Ajout de `YellowstoneGrpcChannel` dans `grpc_channel.rs`.
|
||||||
|
|
||||||
|
`prepare()` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
valide les settings avant I/O
|
||||||
|
refuse un endpoint disabled
|
||||||
|
construit tonic::transport::Endpoint depuis l'URL sensible
|
||||||
|
applique connect timeout / unary timeout / request buffer
|
||||||
|
utilise connect_lazy()
|
||||||
|
ne réalise aucune connexion réseau
|
||||||
|
ne configure encore ni TLS ni metadata
|
||||||
|
ne recopie pas les erreurs URI Tonic arbitraires dans KspError
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `tonic::transport::Channel` réel est un champ privé et n'a aucun getter public. `Debug` n'expose ni URL, ni channel brut, ni credential potentiel.
|
||||||
|
|
||||||
|
Cette surface ne constitue pas encore `YellowstoneGrpcSession` : elle prépare seulement la ressource physique minimale qui sera utilisée par `pre.003+`.
|
||||||
|
|
||||||
|
## 5. Erreurs et observabilité
|
||||||
|
|
||||||
|
Nouveau code :
|
||||||
|
|
||||||
|
```text
|
||||||
|
onchain_transport/grpc_channel_failed
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs de préparation conservent uniquement des contexts allowlistés sûrs (`endpoint_name`) et n'injectent pas le message d'erreur Tonic/URI dans le contrat KSP.
|
||||||
|
|
||||||
|
Les logs passent exclusivement par `ksp-logging-lib` et n'émettent que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint_name
|
||||||
|
provider
|
||||||
|
cluster
|
||||||
|
bornes numériques non sensibles
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune URL n'est loggée.
|
||||||
|
|
||||||
|
## 6. Canaries ajoutés
|
||||||
|
|
||||||
|
Unit tests Transport :
|
||||||
|
|
||||||
|
```text
|
||||||
|
gRPC URL http/https + Debug redacted
|
||||||
|
gRPC URL non-http ou surdimensionnée rejetée sans secret echo
|
||||||
|
descripteurs et endpoint inventory plafonnés
|
||||||
|
defaults session bornés
|
||||||
|
zero/excessive/reversed runtime settings rejetés
|
||||||
|
endpoint inventory unique/enabled
|
||||||
|
channel lazy préparé sans réseau
|
||||||
|
channel Debug sans URL/secret
|
||||||
|
endpoint disabled rejeté avant I/O
|
||||||
|
raw proto disponible uniquement côté interne/test
|
||||||
|
```
|
||||||
|
|
||||||
|
Public API :
|
||||||
|
|
||||||
|
```text
|
||||||
|
settings N1 constructibles depuis crate root
|
||||||
|
YellowstoneGrpcChannel constructible sans Config
|
||||||
|
nouvel error code public KSP
|
||||||
|
aucun raw client Tonic requis par le consumer
|
||||||
|
```
|
||||||
|
|
||||||
|
Release completeness / workspace dependency canaries :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dépendances root/member exactes
|
||||||
|
yellowstone-grpc-client absent
|
||||||
|
Transport -X-> Config
|
||||||
|
aucun couplage Ws*
|
||||||
|
aucun pub use tonic/yellowstone_grpc_proto
|
||||||
|
channel = from_shared + connect_lazy
|
||||||
|
TLS/metadata explicitement absents de pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Documentation
|
||||||
|
|
||||||
|
Les versions opérateur alignées de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
sont conservées comme base de mise en forme et avancées pour enregistrer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
feature set réellement matérialisé
|
||||||
|
séparation stricte pre.002 / pre.003
|
||||||
|
surface N1 réellement ouverte
|
||||||
|
état des canaries
|
||||||
|
Cargo gates encore opérateur
|
||||||
|
```
|
||||||
|
|
||||||
|
Le forecast reste inchangé en nombre de tranches : `pre.003` porte toujours TLS/metadata + fixture locale + 7 unary.
|
||||||
|
|
||||||
|
## 8. Validation exécutée dans l'environnement de préparation
|
||||||
|
|
||||||
|
Exécuté réellement :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
L'environnement de préparation ne possède pas Cargo/Rust. Les commandes Cargo ci-dessous ne sont donc **pas déclarées réussies**.
|
||||||
|
|
||||||
|
## 9. Gate opérateur requis avant commit
|
||||||
|
|
||||||
|
Après application du delta :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspecter particulièrement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.x réellement résolu
|
||||||
|
tonic 0.14.x réellement résolu
|
||||||
|
prost / prost-types / tonic-prost transitifs
|
||||||
|
bytes / http / hyper / hyper-util / tower
|
||||||
|
rustls / tokio-rustls
|
||||||
|
solana-pubkey unifié ou doublons justifiés
|
||||||
|
aucune arrivée de yellowstone-grpc-client
|
||||||
|
features Tonic réellement activées
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la compilation démontre que `yellowstone-grpc-proto` sans feature `tonic` ou Tonic `channel` ne suffit pas à la tranche actuelle, ne pas élargir silencieusement : produire un `pre.002-fix.001` avec la dépendance minimale démontrée.
|
||||||
|
|
||||||
|
## 10. Fichiers ajoutés/modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
|
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/error.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.9/pre.002.md
|
||||||
|
```
|
||||||
82
deltas/0.2.9/pre.003-fix.001.md
Normal file
82
deltas/0.2.9/pre.003-fix.001.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.003-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.003-fix.001` — conformité Clippy de la fixture unary
|
||||||
|
|
||||||
|
## 1. Déclencheur
|
||||||
|
|
||||||
|
Le premier gate opérateur `pre.003` confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets FAIL
|
||||||
|
11 x clippy::implicit_return dans unit_tests/grpc_unary.rs
|
||||||
|
cargo test -p ksp-onchain-transport-lib PASS
|
||||||
|
unit 354/354
|
||||||
|
public_api 43/43
|
||||||
|
release_completeness 36/36
|
||||||
|
doctests 4/4
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
PASS 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le problème est donc limité à la conformité Clippy de la fixture locale ; aucune défaillance fonctionnelle TLS/metadata/unary n'est observée.
|
||||||
|
|
||||||
|
## 2. Diagnostic
|
||||||
|
|
||||||
|
Neuf diagnostics pointent les méthodes de l'implémentation `Geyser` annotée `#[tonic::async_trait]`. Les corps source ont déjà leurs retours explicites ; le lint vise les wrappers/tails async issus de l'expansion de macro et propose un `return async fn ...` non applicable au code Rust source.
|
||||||
|
|
||||||
|
Deux diagnostics supplémentaires concernent les closures `and_then` utilisées pour lire les metadata de fixture ; ces retours sont directement sous contrôle KSP.
|
||||||
|
|
||||||
|
## 3. Correction
|
||||||
|
|
||||||
|
```text
|
||||||
|
impl FixtureGeyser :
|
||||||
|
allow(clippy::implicit_return) strictement local et commenté
|
||||||
|
aucune relaxation au niveau crate/workspace
|
||||||
|
|
||||||
|
closures metadata :
|
||||||
|
return explicite conforme à la politique workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun changement de production :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcChannel inchangé
|
||||||
|
SolanaYellowstoneGrpcUnaryClient inchangé
|
||||||
|
TLS/metadata/settings inchangés
|
||||||
|
7 unary inchangés
|
||||||
|
aucune dépendance/feature Cargo ajoutée ou retirée
|
||||||
|
Subscribe/PublicNode/Config V3 toujours hors tranche
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.3.fix.1
|
||||||
|
commit attendu = v0.2.9-pre.003-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Validation requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Les dépendances et features ne changent pas avec ce fix. Les graphes Cargo demandés par `pre.003` restent néanmoins à fournir/inspecter si ce n'est pas déjà fait :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -e features
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
294
deltas/0.2.9/pre.003.md
Normal file
294
deltas/0.2.9/pre.003.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.003` — TLS + metadata générique + fixture locale + 7 unary Yellowstone
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche poursuit le moteur Yellowstone après la fermeture opérateur complète de `pre.002-fix.002`. Elle matérialise exactement le forecast prévu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 : connexion HTTP/2 réelle + TLS WebPKI + metadata ASCII publique/secrète redacted
|
||||||
|
N2 : sept unary RPCs Yellowstone standard typed derrière une façade KSP
|
||||||
|
tests : fixture Geyser locale couvrant le wire réel, metadata, Status hostile et timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
Restent hors tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Subscribe / streaming bidi
|
||||||
|
SubscribeDeshred
|
||||||
|
PublicNode / N3 provider
|
||||||
|
Config V3
|
||||||
|
reconnect/resubscribe/replay lifecycle complet
|
||||||
|
```
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu après validation opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9-pre.003
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Preuve héritée — fermeture de `pre.002`
|
||||||
|
|
||||||
|
Le gate opérateur final fourni pour `0.2.9-pre.2.fix.2` est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test -p ksp-onchain-transport-lib PASS
|
||||||
|
unit 346/346
|
||||||
|
public_api 42/42
|
||||||
|
release_completeness 35/35
|
||||||
|
doctests 4/4
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
PASS 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.003` part donc d'une base technique fermée, sans dette reportée de `pre.002`.
|
||||||
|
|
||||||
|
## 3. Dépendances et features
|
||||||
|
|
||||||
|
La stratégie B reste inchangée : le runtime KSP utilise les messages publiés `yellowstone-grpc-proto` et un client KSP autour de Tonic ; `yellowstone-grpc-client` reste absent.
|
||||||
|
|
||||||
|
Ajouts workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
http = ^1.5, default-features = false
|
||||||
|
tonic-prost = ^0.14, default-features = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Features Transport runtime :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic = channel + tls-aws-lc + tls-webpki-roots
|
||||||
|
tonic-prost = runtime ProstCodec
|
||||||
|
yellowstone-grpc-proto = aucune feature locale
|
||||||
|
```
|
||||||
|
|
||||||
|
Features uniquement dev/test :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic = codegen + server
|
||||||
|
yellowstone-grpc-proto = tonic
|
||||||
|
```
|
||||||
|
|
||||||
|
La feature `tonic` de la crate proto est donc utilisée uniquement pour générer `GeyserServer` dans la fixture locale. Le runtime ne consomme ni le client généré upstream ni sa sémantique de lifecycle.
|
||||||
|
|
||||||
|
Toujours absents comme dépendances KSP directes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-client
|
||||||
|
prost
|
||||||
|
prost-types
|
||||||
|
```
|
||||||
|
|
||||||
|
Le choix `tls-aws-lc` reste aligné avec la stack Rustls déjà présente via Reqwest ; le gate Cargo opérateur doit confirmer l'absence de duplication injustifiée.
|
||||||
|
|
||||||
|
## 4. N1 — TLS, connexion réelle et metadata
|
||||||
|
|
||||||
|
`YellowstoneGrpcChannel` possède maintenant deux chemins explicites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prepare() -> connect_lazy(), aucun socket
|
||||||
|
connect() -> établissement réel du channel HTTP/2
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour un endpoint `https://`, le builder applique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ClientTlsConfig
|
||||||
|
WebPKI roots
|
||||||
|
TLS handshake timeout borné
|
||||||
|
connect timeout borné
|
||||||
|
request buffer borné
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs d'établissement restent projetées vers `onchain_transport/grpc_channel_failed` sans recopier URI ou message arbitraire Tonic.
|
||||||
|
|
||||||
|
Ajout de `YellowstoneGrpcMetadataEntry` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ASCII uniquement
|
||||||
|
clé <= 128 bytes
|
||||||
|
valeur <= 8 KiB
|
||||||
|
<= 64 entrées par endpoint
|
||||||
|
clés grpc-* refusées
|
||||||
|
metadata binaire *-bin hors contrat pre.003
|
||||||
|
public et secret séparés explicitement
|
||||||
|
valeurs toujours absentes de Debug
|
||||||
|
secret -> MetadataValue::set_sensitive(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport ne connaît aucun nom `KSP_SECRET_*`, ne lit pas l'environnement et ne lie aucune clé provider particulière au standard N1/N2.
|
||||||
|
|
||||||
|
## 5. N2 — sept unary standard
|
||||||
|
|
||||||
|
Ajout de `SolanaYellowstoneGrpcUnaryClient`, construit uniquement depuis `YellowstoneGrpcChannel`. Le Tonic `Channel`, `Grpc`, les Requests et les messages protobuf restent privés.
|
||||||
|
|
||||||
|
Surface exacte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SubscribeReplayInfo
|
||||||
|
Ping
|
||||||
|
GetLatestBlockhash
|
||||||
|
GetBlockHeight
|
||||||
|
GetSlot
|
||||||
|
IsBlockhashValid
|
||||||
|
GetVersion
|
||||||
|
```
|
||||||
|
|
||||||
|
Les chemins wire sont les chemins `geyser.Geyser` officiels. `SolanaCommitment` existant est réutilisé et mappé vers `Processed / Confirmed / Finalized`.
|
||||||
|
|
||||||
|
DTOs publics KSP :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneReplayInfo
|
||||||
|
YellowstonePong
|
||||||
|
YellowstoneLatestBlockhash
|
||||||
|
YellowstoneBlockHeight
|
||||||
|
YellowstoneSlot
|
||||||
|
YellowstoneBlockhashValidity
|
||||||
|
YellowstoneVersionInfo
|
||||||
|
```
|
||||||
|
|
||||||
|
Bornes/sécurité :
|
||||||
|
|
||||||
|
```text
|
||||||
|
max inbound/outbound appliqués au dispatcher Tonic
|
||||||
|
une seule deadline KSP couvre readiness + metadata + unary dispatch
|
||||||
|
blockhash request localement borné/validé avant I/O
|
||||||
|
blockhash/version response textuellement bornés
|
||||||
|
Ping exige l'echo exact du count
|
||||||
|
```
|
||||||
|
|
||||||
|
Nouveau code d'erreur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
onchain_transport/grpc_status
|
||||||
|
```
|
||||||
|
|
||||||
|
Un `tonic::Status` distant n'est jamais copié dans `KspError` : seuls l'opération et le code gRPC sont conservés. Message/details/metadata provider restent hors diagnostic public.
|
||||||
|
|
||||||
|
## 6. Fixture Geyser locale
|
||||||
|
|
||||||
|
Les tests dev activent le serveur généré officiel et démarrent un `GeyserServer` sur `127.0.0.1:0`. Les deux RPCs streaming obligatoires du trait retournent volontairement `Unimplemented`, car ils restent hors `pre.003`.
|
||||||
|
|
||||||
|
La fixture vérifie réellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
les 7 paths unary
|
||||||
|
commitment wire
|
||||||
|
metadata publique reçue
|
||||||
|
metadata secrète reçue
|
||||||
|
replay optional first_available
|
||||||
|
Ping exact
|
||||||
|
latest blockhash / height / slot / validity / version
|
||||||
|
Status hostile contenant un canari secret non propagé
|
||||||
|
timeout unary local déterministe
|
||||||
|
blockhash invalide rejeté avant I/O
|
||||||
|
Debug client/channel sans URL ni secret
|
||||||
|
```
|
||||||
|
|
||||||
|
La fixture est locale et déterministe ; aucun provider externe n'est requis pour fermer cette tranche.
|
||||||
|
|
||||||
|
## 7. Canaries structurels
|
||||||
|
|
||||||
|
Les canaries vérifient en plus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
feature set runtime/dev exact
|
||||||
|
yellowstone-grpc-client absent
|
||||||
|
proto runtime sans feature tonic
|
||||||
|
server/codegen uniquement dev/test
|
||||||
|
aucun pub use tonic/yellowstone_grpc_proto
|
||||||
|
sept chemins unary exacts
|
||||||
|
aucun Subscribe standard ajouté
|
||||||
|
aucun SubscribeDeshred ajouté
|
||||||
|
aucun PublicNode ajouté
|
||||||
|
Transport -X-> Config/env/WS pour la surface gRPC
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Documentation
|
||||||
|
|
||||||
|
Mise à jour de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
La matrice enregistre la fermeture réelle de `pre.002` et place les sept unary/TLS/metadata/fixture en candidate `pre.003`. Le forecast reste inchangé : `pre.004` ouvre seulement la foundation `Subscribe`.
|
||||||
|
|
||||||
|
## 9. Validation exécutée dans l'environnement de préparation
|
||||||
|
|
||||||
|
Exécuté réellement après les modifications source/documentaires :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
L'environnement de préparation ne fournit pas Cargo/Rust ; aucune commande Cargo n'est déclarée réussie localement.
|
||||||
|
|
||||||
|
## 10. Gate opérateur requis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -e features
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes doivent être réinspectés car `http`, `tonic-prost` et les features TLS/server/codegen changent le graphe de `pre.002`. Contrôler notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tonic / tonic-prost
|
||||||
|
prost / prost-types
|
||||||
|
http / hyper / hyper-util / tower
|
||||||
|
rustls / tokio-rustls / aws-lc
|
||||||
|
yellowstone-grpc-proto runtime vs dev feature unification
|
||||||
|
solana-pubkey
|
||||||
|
yellowstone-grpc-client absent
|
||||||
|
router/gzip/zstd non activés par la surface KSP ; server/codegen uniquement via dev/test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Fichiers ajoutés/modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
|
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/error.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.9/pre.003.md
|
||||||
|
```
|
||||||
64
deltas/0.2.9/pre.004-fix.001.md
Normal file
64
deltas/0.2.9/pre.004-fix.001.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.004-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.004-fix.001` — suppression des warnings `dead_code` des conversions wire test-only
|
||||||
|
|
||||||
|
## 1. Déclencheur
|
||||||
|
|
||||||
|
Le premier gate opérateur `pre.004` est fonctionnellement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS + 10 warnings dead_code
|
||||||
|
cargo clippy --workspace --all-targets PASS + 10 warnings dead_code
|
||||||
|
Transport unit 359/359 PASS
|
||||||
|
Transport public_api 44/44 PASS
|
||||||
|
Transport release_completeness 37/37 PASS
|
||||||
|
Transport doctests 4/4 PASS
|
||||||
|
Core workspace_dependencies 3/3 PASS
|
||||||
|
cargo test --workspace PASS + mêmes warnings de compilation
|
||||||
|
```
|
||||||
|
|
||||||
|
Les dix warnings concernent exclusivement les conversions protobuf privées de `grpc_subscribe.rs` (`to_wire` et `commitment_to_wire`).
|
||||||
|
|
||||||
|
## 2. Diagnostic
|
||||||
|
|
||||||
|
`pre.004` matérialise le contrat `SubscribeRequest` et vérifie son wire dans les unit tests, mais le stream bidi n'est volontairement ouvert qu'en `pre.008`. Ces helpers ont donc un consommateur test aujourd'hui et aucun consommateur runtime encore.
|
||||||
|
|
||||||
|
Contrairement au channel N1 de `pre.002`, ces fonctions ne représentent pas un état runtime devant exister avant leur consommation. Les compiler en production maintenant ne fait qu'introduire du code mort temporaire.
|
||||||
|
|
||||||
|
## 3. Correction
|
||||||
|
|
||||||
|
```text
|
||||||
|
#[cfg(test)] sur les 9 méthodes to_wire de grpc_subscribe.rs
|
||||||
|
#[cfg(test)] sur commitment_to_wire
|
||||||
|
aucune modification des types publics
|
||||||
|
aucune modification des validations/bornes
|
||||||
|
aucune modification du protobuf produit par les tests
|
||||||
|
aucune dépendance/feature Cargo modifiée
|
||||||
|
```
|
||||||
|
|
||||||
|
Lors de `pre.008`, quand la session bidi consommera réellement ces conversions, le `cfg(test)` sera retiré des helpers nécessaires au runtime.
|
||||||
|
|
||||||
|
## 4. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.4.fix.1
|
||||||
|
commit attendu = v0.2.9-pre.004-fix.001
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Validation requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo tree` supplémentaire n'est requis : le fix ne modifie ni dépendance ni feature.
|
||||||
250
deltas/0.2.9/pre.004.md
Normal file
250
deltas/0.2.9/pre.004.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.004` — fondation `SubscribeRequest` Yellowstone standard
|
||||||
|
|
||||||
|
## 1. Base et signal de version
|
||||||
|
|
||||||
|
Base obligatoire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.3.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur fourni ferme `pre.003-fix.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
Transport unit 354/354
|
||||||
|
Transport public_api 43/43
|
||||||
|
Transport release_completeness 36/36
|
||||||
|
Transport doctests 4/4
|
||||||
|
cargo tree -p ... fourni/relu
|
||||||
|
cargo tree -p ... -e features fourni/relu
|
||||||
|
cargo tree -p ... --duplicates fourni/relu
|
||||||
|
cargo tree --duplicates fourni/relu
|
||||||
|
```
|
||||||
|
|
||||||
|
Le graphe pertinent reste unifié sur `tonic 0.14.6`, `tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, `yellowstone-grpc-proto 12.6.0` et `solana-pubkey 4.3.0`.
|
||||||
|
|
||||||
|
Cette tranche non-fix synchronise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.4
|
||||||
|
commit attendu = v0.2.9-pre.004
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Scope exact
|
||||||
|
|
||||||
|
`pre.004` matérialise uniquement la fondation N2 du `SubscribeRequest` standard publié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
accounts
|
||||||
|
slots
|
||||||
|
transactions
|
||||||
|
transactions_status
|
||||||
|
blocks
|
||||||
|
blocks_meta
|
||||||
|
entry
|
||||||
|
commitment
|
||||||
|
accounts_data_slice
|
||||||
|
ping
|
||||||
|
from_slot
|
||||||
|
```
|
||||||
|
|
||||||
|
Le proto `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant implémentation et conserve exactement ces champs top-level.
|
||||||
|
|
||||||
|
Sont explicitement hors tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
champs détaillés Accounts / Slots -> pre.005
|
||||||
|
champs détaillés Transactions / transaction_status -> pre.006
|
||||||
|
champs détaillés Blocks / block_meta / entry updates -> pre.007
|
||||||
|
stream bidi / mutation / Ping-Pong runtime -> pre.008
|
||||||
|
reconnect / replay / gaps / duplicates -> pre.009
|
||||||
|
Config V3 / PublicNode -> pre.010/011
|
||||||
|
SubscribeDeshred -> OUT 0.2.9 standard
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Contrat KSP ajouté
|
||||||
|
|
||||||
|
Nouveaux types publics :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneSubscribeFilterName
|
||||||
|
YellowstoneAccountsDataSlice
|
||||||
|
YellowstoneSubscribePing
|
||||||
|
YellowstoneSubscribeAccountFilter
|
||||||
|
YellowstoneSubscribeSlotFilter
|
||||||
|
YellowstoneSubscribeTransactionFilter
|
||||||
|
YellowstoneSubscribeBlockFilter
|
||||||
|
YellowstoneSubscribeBlocksMetaFilter
|
||||||
|
YellowstoneSubscribeEntryFilter
|
||||||
|
YellowstoneSubscribeRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
Les quatre filtres de famille riches sont volontairement des shells typés en `pre.004`. Ils permettent de matérialiser les sept maps et les entrées nommées vides sans anticiper les champs propres aux tranches suivantes.
|
||||||
|
|
||||||
|
Le wire `yellowstone_grpc_proto::geyser::SubscribeRequest` reste privé : aucun type Tonic/Prost/Yellowstone n'est réexporté au crate root.
|
||||||
|
|
||||||
|
## 4. Sémantique maps / noms
|
||||||
|
|
||||||
|
KSP conserve sept maps logiques indépendantes.
|
||||||
|
|
||||||
|
```text
|
||||||
|
map vide = aucun filtre actif dans la famille
|
||||||
|
entrée nommée + message vide = activation explicite d'un groupe vide supporté par le proto
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Protobuf ne distingue pas une map top-level omise d'une map vide ; KSP ne prétend donc pas préserver une différence wire inexistante.
|
||||||
|
|
||||||
|
Les noms sont :
|
||||||
|
|
||||||
|
```text
|
||||||
|
non vides
|
||||||
|
trim exact
|
||||||
|
sans caractère de contrôle
|
||||||
|
<= 128 octets UTF-8
|
||||||
|
uniques globalement entre les sept maps
|
||||||
|
```
|
||||||
|
|
||||||
|
L'unicité globale évite l'ambiguïté lorsque Yellowstone renvoie seulement les noms correspondants dans `SubscribeUpdate.filters[]`.
|
||||||
|
|
||||||
|
Le nombre total de groupes nommés est plafonné à `1024` sur l'ensemble des sept maps.
|
||||||
|
|
||||||
|
## 5. Champs communs et bounds
|
||||||
|
|
||||||
|
`commitment` réutilise `SolanaCommitment` et mappe exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Processed -> PROCESSED
|
||||||
|
Confirmed -> CONFIRMED
|
||||||
|
Finalized -> FINALIZED
|
||||||
|
```
|
||||||
|
|
||||||
|
`accounts_data_slice` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ordre d'insertion conservé
|
||||||
|
nombre <= 128
|
||||||
|
length <= 64 MiB
|
||||||
|
offset + length doit rester représentable en u64
|
||||||
|
length = 0 reste représentable
|
||||||
|
```
|
||||||
|
|
||||||
|
`ping` conserve l'identifiant `i32` exact.
|
||||||
|
|
||||||
|
`from_slot` conserve l'optional `u64` sans lui attribuer encore une promesse de replay/lossless ; sa sémantique lifecycle reste en `pre.009`.
|
||||||
|
|
||||||
|
## 6. Diagnostics et sécurité
|
||||||
|
|
||||||
|
`Debug` de `YellowstoneSubscribeFilterName` est redacted.
|
||||||
|
|
||||||
|
`Debug` de `YellowstoneSubscribeRequest` expose uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
compteurs par famille
|
||||||
|
commitment
|
||||||
|
nombre de data slices
|
||||||
|
ping
|
||||||
|
from_slot
|
||||||
|
```
|
||||||
|
|
||||||
|
Il n'expose aucun nom de filtre ni futur payload account/transaction/block.
|
||||||
|
|
||||||
|
Les erreurs déterministes utilisent `ERROR_CODE_INVALID_RPC_PARAMETERS` et n'incluent pas les valeurs de noms/payloads rejetés.
|
||||||
|
|
||||||
|
## 7. Dépendances
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est modifiée dans `pre.004`.
|
||||||
|
|
||||||
|
Les graphes fournis pour `pre.003-fix.001` restent l'autorité courante ; aucun nouveau `cargo tree` n'est requis spécifiquement par ce delta sauf anomalie de compilation.
|
||||||
|
|
||||||
|
## 8. Tests/canaries ajoutés
|
||||||
|
|
||||||
|
Unit tests :
|
||||||
|
|
||||||
|
```text
|
||||||
|
nom vide/whitespace/control/oversized rejeté
|
||||||
|
Debug nom redacted
|
||||||
|
unicité globale cross-family
|
||||||
|
request vide -> sept maps vides + common absent
|
||||||
|
sept entrées nommées vides -> wire exact
|
||||||
|
commitment/ping/from_slot exacts
|
||||||
|
ordre data slices conservé
|
||||||
|
zero-length slice conservée
|
||||||
|
slice length/overflow/count bornés
|
||||||
|
filter-group count borné
|
||||||
|
request Debug sans filter name
|
||||||
|
```
|
||||||
|
|
||||||
|
Public API : construction des sept maps et champs communs depuis le crate root.
|
||||||
|
|
||||||
|
Release completeness :
|
||||||
|
|
||||||
|
```text
|
||||||
|
onze champs top-level présents dans l'adapter
|
||||||
|
bounds communs matérialisés
|
||||||
|
aucun SubscribeDeshred
|
||||||
|
aucun PublicNode/OrbitFlare/Helius runtime
|
||||||
|
aucun raw Tonic/Yellowstone reexport
|
||||||
|
to_wire protobuf non public
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
|
||||||
|
deltas/0.2.9/pre.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Validation de préparation
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
L'environnement de préparation ne fournit pas Cargo/Rust ; aucune compilation n'est déclarée réussie localement.
|
||||||
|
|
||||||
|
## 12. Gate opérateur attendu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu si aucun fix n'est nécessaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 359
|
||||||
|
Transport public_api 44
|
||||||
|
Transport release_completeness 37
|
||||||
|
Transport doctests 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Les live smokes restent ignored/opt-in.
|
||||||
61
deltas/0.2.9/pre.005-fix.001.md
Normal file
61
deltas/0.2.9/pre.005-fix.001.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.005-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.005-fix.001`
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Corriger le premier gate opérateur de `0.2.9-pre.005` sans modifier le contrat fonctionnel Accounts + Slots.
|
||||||
|
|
||||||
|
## Preuve opérateur avant fix
|
||||||
|
|
||||||
|
Le gate `pre.005` confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
workspace Rust audit PASS / clean
|
||||||
|
cargo check --workspace PASS + 4 dead_code warnings
|
||||||
|
cargo clippy --workspace --all-targets FAIL: implicit_return + wrong_self_convention
|
||||||
|
Transport unit 364/364 PASS
|
||||||
|
Transport public_api 45/45 PASS
|
||||||
|
Transport release_completeness 38/38 PASS
|
||||||
|
Transport doctests 4/4 PASS
|
||||||
|
Core dependency canary 3/3 PASS
|
||||||
|
cargo test --workspace PASS + 4 dead_code warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
| Surface | Correction | Justification |
|
||||||
|
|-------------------------------------------|--------------------------------------------|--------------------------------------------------------------------------------|
|
||||||
|
| quatre constantes de bounds update | ajout `#[cfg(test)]` | uniquement consommées par les décodeurs Account/Slot test-only avant `pre.008` |
|
||||||
|
| `YellowstoneSubscribeSlotFilter::to_wire` | receiver `self` | type `Copy`, conformité `clippy::wrong_self_convention` |
|
||||||
|
| `dead_error.as_ref().is_some_and(...)` | `return` explicite dans la closure | conformité `-D clippy::implicit-return` |
|
||||||
|
| `016` + `012` | réalignement de tous les tableaux Markdown | préserve le format visuel manuel demandé par l'opérateur |
|
||||||
|
| workspace version | `0.2.9-pre.5.fix.1` | signal technique de fix |
|
||||||
|
|
||||||
|
## Non-changements
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun changement du contrat public Accounts/Slots
|
||||||
|
aucun changement du wire protobuf
|
||||||
|
aucune dépendance/feature Cargo
|
||||||
|
aucun stream bidi
|
||||||
|
aucune famille Transactions/Blocks
|
||||||
|
aucun PublicNode / Config V3
|
||||||
|
aucun allow(dead_code)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gate attendu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : `check`, Clippy et workspace passent sans les quatre warnings `dead_code` de `pre.005`.
|
||||||
121
deltas/0.2.9/pre.005.md
Normal file
121
deltas/0.2.9/pre.005.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.005` — Yellowstone standard Accounts + Slots
|
||||||
|
|
||||||
|
## Base et signal de version
|
||||||
|
|
||||||
|
Base autoritaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.4.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Signal technique de cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.5
|
||||||
|
commit attendu = v0.2.9-pre.005
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.004-fix.001` est vert sans les warnings `dead_code` précédents ; `pre.004` est donc fermée.
|
||||||
|
|
||||||
|
## Périmètre réalisé
|
||||||
|
|
||||||
|
Cette tranche complète **uniquement Accounts + Slots** du contrat Yellowstone `Subscribe` standard actuel.
|
||||||
|
|
||||||
|
Accounts request :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account[]
|
||||||
|
owner[]
|
||||||
|
filters[] :
|
||||||
|
memcmp bytes/base58/base64
|
||||||
|
datasize
|
||||||
|
token_account_state
|
||||||
|
lamports eq/ne/lt/gt
|
||||||
|
nonempty_txn_signature?
|
||||||
|
cuckoo_accounts_filter?
|
||||||
|
```
|
||||||
|
|
||||||
|
Slots request :
|
||||||
|
|
||||||
|
```text
|
||||||
|
filter_by_commitment?
|
||||||
|
interslot_updates?
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates KSP :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Account : filters/created_at + account info + slot + is_startup
|
||||||
|
Slot : filters/created_at + slot + parent? + status + dead_error?
|
||||||
|
```
|
||||||
|
|
||||||
|
Les sept statuts Slot actuels sont couverts : processed, confirmed, finalized, first-shred-received, completed, created-bank et dead.
|
||||||
|
|
||||||
|
## Cuckoo standard
|
||||||
|
|
||||||
|
Le Cuckoo account filter est traité comme capacité du standard Yellowstone courant, pas comme extension provider. KSP expose son propre DTO avec données, dimensions, fingerprint bits, seed et algorithme ; l'algorithme courant matérialisé est SipHash. Aucun type upstream n'est réexporté.
|
||||||
|
|
||||||
|
## Validation et sécurité
|
||||||
|
|
||||||
|
Bornes KSP provider-neutral ajoutées sur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account/owner selectors
|
||||||
|
account predicates
|
||||||
|
memcmp binary/text
|
||||||
|
Cuckoo payload/dimensions/fingerprint
|
||||||
|
account-data update
|
||||||
|
update filter names/count
|
||||||
|
timestamp nanos
|
||||||
|
transaction signature fixe 64 octets
|
||||||
|
dead_error
|
||||||
|
```
|
||||||
|
|
||||||
|
Les pubkeys/owners de wire doivent faire exactement 32 octets et les signatures 64 octets. Un enum Slot inconnu, un oneof ou champ obligatoire absent et les payloads hors bornes deviennent des erreurs KSP déterministes ; aucun payload provider arbitraire n'est recopié dans les diagnostics. Les Debug KSP n'exposent pas les sélecteurs, données memcmp/Cuckoo/account-data ni le texte `dead_error`.
|
||||||
|
|
||||||
|
## Frontière runtime
|
||||||
|
|
||||||
|
Le stream bidi reste hors tranche. Les conversions protobuf request et les décodeurs Account/Slot restent donc compilés seulement pour les tests jusqu'à `pre.008`, où ils acquerront leur premier consommateur runtime. Cela évite les warnings `dead_code` sans dupliquer l'implémentation.
|
||||||
|
|
||||||
|
## Explicitement hors `pre.005`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transactions + transaction_status
|
||||||
|
Blocks + block_meta + entry
|
||||||
|
Ping/Pong lifecycle et mutation bidi
|
||||||
|
backpressure/shutdown stream
|
||||||
|
reconnect/resubscribe/replay
|
||||||
|
Config V3
|
||||||
|
PublicNode provider integration
|
||||||
|
SubscribeDeshred
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. Les graphes inspectés en `pre.003` restent applicables.
|
||||||
|
|
||||||
|
## Validation attendue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo tree` supplémentaire n'est requis pour cette tranche si le manifest reste inchangé.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.006 Transactions + transaction_status
|
||||||
|
pre.007 Blocks + block_meta + entry
|
||||||
|
pre.008 stream bidi + mutation + Ping/Pong + half-close + backpressure + shutdown
|
||||||
|
```
|
||||||
167
deltas/0.2.9/pre.006.md
Normal file
167
deltas/0.2.9/pre.006.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.006` — namespace privé HTTP explicite
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.5.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.005-fix.001` est intégralement vert : fmt, audit Rust, check, Clippy, Transport 364 unit + 45 public API + 38 release-completeness + 4 doctests, dependency canary Core 3/3 et workspace complet.
|
||||||
|
|
||||||
|
## 2. Objectif
|
||||||
|
|
||||||
|
Réduire l'ambiguïté croissante dans `ksp-onchain-transport-lib` maintenant que HTTP, WebSocket et Yellowstone gRPC coexistent dans la même crate.
|
||||||
|
|
||||||
|
Cinq modules privés sont exclusivement propriétaires de la pile HTTP et reçoivent un préfixe explicite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
client.rs -> http_client.rs
|
||||||
|
executor.rs -> http_executor.rs
|
||||||
|
pool.rs -> http_pool.rs
|
||||||
|
resilience.rs -> http_resilience.rs
|
||||||
|
settings.rs -> http_settings.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Les unit tests miroirs suivent exactement les mêmes renommages.
|
||||||
|
|
||||||
|
## 3. Frontière du renommage
|
||||||
|
|
||||||
|
Le renommage n'est pas appliqué mécaniquement à tous les anciens modules.
|
||||||
|
|
||||||
|
`rpc_accounts`, `rpc_blocks`, `rpc_transactions` et `rpc_common` portent déjà des DTOs/types Solana réutilisés par WebSocket et/ou gRPC. Le préfixe `http_` y serait donc architecturalement faux. Les autres `rpc_*` restent dans la même famille cohérente.
|
||||||
|
|
||||||
|
`json_rpc` décrit une couche de protocole/enveloppe et conserve son nom. `constants` et `error` sont transverses à plusieurs transports et restent également inchangés.
|
||||||
|
|
||||||
|
Aucun type ou nom public n'est renommé : les surfaces HTTP publiques utilisent déjà des noms `Http*` et restent réexportées depuis le crate root.
|
||||||
|
|
||||||
|
## 4. Forecast recalibré
|
||||||
|
|
||||||
|
L'ancien `pre.006` fonctionnel est décalé afin de ne pas mélanger ce refactor de fichiers avec l'ajout Transactions/transaction_status.
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.006 namespace privé HTTP explicite
|
||||||
|
pre.007 Transactions + transaction_status
|
||||||
|
pre.008 Blocks + block_meta + entry
|
||||||
|
pre.009 bidi/backpressure/half-close/shutdown
|
||||||
|
pre.010 reconnect/replay/gaps/duplicates
|
||||||
|
pre.011 Config V3 + protocol/provider + profils PublicNode
|
||||||
|
pre.012 PublicNode live + compliance + docs/prompt 0.2.10
|
||||||
|
rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Le contenu fonctionnel des tranches décalées ne change pas.
|
||||||
|
|
||||||
|
## 5. Fichiers ajoutés par renommage
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_client.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_executor.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_pool.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_resilience.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/http_client.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/http_executor.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/http_pool.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/http_resilience.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/http_settings.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers supprimés
|
||||||
|
|
||||||
|
Un ZIP overlay ne peut pas supprimer ces chemins. L'opérateur doit donc les retirer explicitement après extraction :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/client.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/pool.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/resilience.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/executor.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/settings.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Commande opérateur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm \
|
||||||
|
crates/ksp-onchain-transport-lib/src/{client,executor,pool,resilience,settings}.rs \
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/{client,executor,pool,resilience,settings}.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.9/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`workspace.package.version` devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Canaries
|
||||||
|
|
||||||
|
Le nouveau canari release-completeness vérifie :
|
||||||
|
|
||||||
|
- les cinq déclarations privées `mod http_*` ;
|
||||||
|
- l'absence des cinq anciennes déclarations ambiguës ;
|
||||||
|
- la présence des types/fonctions HTTP structurants dans les nouveaux fichiers ;
|
||||||
|
- le maintien volontaire des modules partagés/protocolaires `rpc_*`, `json_rpc`, `constants`, `error` ;
|
||||||
|
- l'absence de changement de surface publique requise par ce refactor.
|
||||||
|
|
||||||
|
## 9. Documentation Markdown
|
||||||
|
|
||||||
|
Les tableaux touchés dans `016` et `012` sont reformattés selon le comportement JetBrains RustRover : largeur de chaque colonne basée sur son contenu le plus large, puis exactement un espace de padding de part et d'autre du contenu avant les pipes.
|
||||||
|
|
||||||
|
## 10. Validations exécutées lors de la préparation
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrôles statiques supplémentaires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
5 nouveaux modules HTTP source présents
|
||||||
|
5 nouveaux unit tests miroirs présents
|
||||||
|
5 anciens modules source absents du worktree final
|
||||||
|
5 anciens unit tests absents du worktree final
|
||||||
|
aucun renommage des modules rpc_* partagés
|
||||||
|
aucun changement de dépendance Cargo
|
||||||
|
aucune modification de public_api.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Validations non exécutées dans l'environnement de préparation
|
||||||
|
|
||||||
|
Cargo/Rust ne sont pas disponibles dans l'environnement de préparation. L'opérateur doit exécuter après extraction **et suppression des anciens chemins** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo tree` n'est requis : aucune dépendance ni feature ne change.
|
||||||
|
|
||||||
|
## 12. Verdict
|
||||||
|
|
||||||
|
`pre.006` est une candidate structurelle sans changement fonctionnel ni API publique. Sa fermeture exige l'absence effective des dix anciens chemins et un gate Cargo intégralement vert.
|
||||||
165
deltas/0.2.9/pre.007.md
Normal file
165
deltas/0.2.9/pre.007.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.007.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.007` — Yellowstone Transactions + `transaction_status`
|
||||||
|
|
||||||
|
## 1. Base et gate précédent
|
||||||
|
|
||||||
|
Base exacte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.006
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur `pre.006` est fermé : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 release-completeness + 4 doctests, dependency canary Core 3/3 PASS.
|
||||||
|
|
||||||
|
Version technique de cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu après gate vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9-pre.007
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag Git stable n'est créé pour cette prerelease.
|
||||||
|
|
||||||
|
## 2. Objet de la tranche
|
||||||
|
|
||||||
|
`pre.007` complète exclusivement les deux familles Yellowstone standard `transactions` et `transactions_status` du `SubscribeRequest`/`SubscribeUpdate` courant. Elle n'ouvre toujours pas le stream bidi runtime.
|
||||||
|
|
||||||
|
Le proto publié `yellowstone-grpc-proto 12.6.0` a été réaudité avant implémentation. Le même `SubscribeRequestFilterTransactions` est utilisé par les deux maps et expose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
vote?
|
||||||
|
failed?
|
||||||
|
signature?
|
||||||
|
account_include[]
|
||||||
|
account_exclude[]
|
||||||
|
account_required[]
|
||||||
|
cuckoo_account_include?
|
||||||
|
token_accounts? = ALL | BALANCE_CHANGED
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Contrat request KSP
|
||||||
|
|
||||||
|
La tranche matérialise `YellowstoneSubscribeTransactionFilter` et les types associés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneTransactionSignatureSelector
|
||||||
|
YellowstoneTokenAccountExpansion
|
||||||
|
YellowstoneCuckooFilter réutilisé
|
||||||
|
```
|
||||||
|
|
||||||
|
La signature textuelle est :
|
||||||
|
|
||||||
|
- non vide ;
|
||||||
|
- bornée à 128 octets ;
|
||||||
|
- strictement Base58 ;
|
||||||
|
- décodée par KSP pour vérifier une largeur exacte de 64 octets ;
|
||||||
|
- absente des diagnostics et du `Debug`.
|
||||||
|
|
||||||
|
Les listes include/exclude/required conservent l'ordre d'insertion et réutilisent le bound déterministe des sélecteurs Accounts. Le Cuckoo transaction réutilise le contrat standard KSP déjà introduit en `pre.005`.
|
||||||
|
|
||||||
|
## 4. Updates et `solana-storage.proto`
|
||||||
|
|
||||||
|
Les nouveaux DTOs KSP couvrent les variantes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SubscribeUpdateTransaction
|
||||||
|
SubscribeUpdateTransactionStatus
|
||||||
|
```
|
||||||
|
|
||||||
|
et projettent sans raw reexport upstream les structures utiles :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transaction / Message / MessageHeader
|
||||||
|
CompiledInstruction / MessageAddressTableLookup
|
||||||
|
TransactionConfig V1
|
||||||
|
TransactionStatusMeta / TransactionError
|
||||||
|
InnerInstructions / InnerInstruction
|
||||||
|
TokenBalance / UiTokenAmount
|
||||||
|
ReturnData / Reward
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `Message.config` optionnel actuel est conservé avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
priority_fee?
|
||||||
|
compute_unit_limit?
|
||||||
|
loaded_accounts_data_size_limit?
|
||||||
|
heap_size?
|
||||||
|
```
|
||||||
|
|
||||||
|
`TransactionStatusMeta` conserve séparément les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none`, ainsi que les payloads correspondants. Les champs optionnels `compute_units_consumed` et `cost_units`, les loaded addresses, rewards et token balances sont également préservés.
|
||||||
|
|
||||||
|
`TransactionError.err` reste un payload opaque borné ; KSP n'introduit aucun décodage JSON ou Program arbitraire.
|
||||||
|
|
||||||
|
## 5. Validation et sûreté
|
||||||
|
|
||||||
|
Les décodeurs test-only vérifient notamment :
|
||||||
|
|
||||||
|
| Élément | Politique `pre.007` |
|
||||||
|
|------------------------------------------------------|----------------------------------------------------------------------------|
|
||||||
|
| signatures wire | exactement 64 octets |
|
||||||
|
| hash / account keys / loaded addresses / program ids | exactement 32 octets |
|
||||||
|
| vecteurs transaction/meta | bornes KSP avant projection |
|
||||||
|
| instruction / return-data payloads | bornés |
|
||||||
|
| logs et textes provider | count/length bornés |
|
||||||
|
| error bytes | opaques et bornés |
|
||||||
|
| `Debug` | pas de signature, pubkey, log, instruction data ou error bytes arbitraires |
|
||||||
|
|
||||||
|
Les conversions protobuf request et les décodeurs update restent `#[cfg(test)]` jusqu'à `pre.009`, premier consommateur runtime prévu lors de l'ouverture du stream bidi.
|
||||||
|
|
||||||
|
## 6. Frontières préservées
|
||||||
|
|
||||||
|
Toujours hors `pre.007` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Blocks + block_meta + entry pre.008
|
||||||
|
bidi/backpressure/Ping-Pong/shutdown pre.009
|
||||||
|
reconnect/replay/gaps/duplicates pre.010
|
||||||
|
Config V3 + PublicNode profiles pre.011
|
||||||
|
PublicNode live/compliance pre.012
|
||||||
|
SubscribeDeshred OUT 0.2.9 standard
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun provider N3 n'est ajouté. Aucune dépendance ni feature Cargo n'est modifiée.
|
||||||
|
|
||||||
|
## 7. Preuves ajoutées
|
||||||
|
|
||||||
|
Tests Transport ajoutés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction filter exact wire + redaction + Base58 width
|
||||||
|
transaction update storage fixture + TransactionConfig V1 + meta
|
||||||
|
transaction_status update + error + malformed signature
|
||||||
|
public API root contract
|
||||||
|
release-completeness exact scope
|
||||||
|
```
|
||||||
|
|
||||||
|
Compte cible après compilation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit ~367
|
||||||
|
Transport public API 46
|
||||||
|
Transport release completeness 40
|
||||||
|
Transport doctests 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Gate opérateur requis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `cargo tree` supplémentaire n'est requis : aucune dépendance ou feature ne change dans cette tranche.
|
||||||
59
deltas/0.2.9/pre.008-fix.001.md
Normal file
59
deltas/0.2.9/pre.008-fix.001.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.008-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.008-fix.001` — hygiène Clippy fixture Blocks
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Corriger l'unique warning Clippy observé au premier gate opérateur de `pre.008`, sans modifier le contrat fonctionnel Blocks + `block_meta` + `entry`.
|
||||||
|
|
||||||
|
## 2. Preuve opérateur avant fix
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
workspace Rust audit PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS + 1 warning field_reassign_with_default
|
||||||
|
Transport unit 370/370 PASS
|
||||||
|
Transport public_api 47/47 PASS
|
||||||
|
Transport release_completeness 41/41 PASS
|
||||||
|
Transport doctests 4/4 PASS
|
||||||
|
Core dependency canary 3/3 PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Correction
|
||||||
|
|
||||||
|
| Surface | Correction | Justification |
|
||||||
|
|---------------------------------------------|--------------------------------------------------------------|------------------------------------------------|
|
||||||
|
| `minimal_transaction_info()` fixture Blocks | `TransactionStatusMeta { fee: 5_000, ..Default::default() }` | supprime `clippy::field_reassign_with_default` |
|
||||||
|
| workspace version | `0.2.9-pre.8.fix.1` | signal technique du delta fix |
|
||||||
|
| `016` + `012` | preuve du gate et statut `fix.001` | traçabilité de fermeture de `pre.008` |
|
||||||
|
|
||||||
|
## 4. Non-changements
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun changement runtime N1/N2
|
||||||
|
aucun changement du filtre Blocks
|
||||||
|
aucun changement des DTOs Block/BlockMeta/Entry
|
||||||
|
aucun changement du wire protobuf
|
||||||
|
aucune dépendance/feature Cargo
|
||||||
|
aucun allow Clippy
|
||||||
|
aucun stream bidi
|
||||||
|
aucun reconnect/replay
|
||||||
|
aucun PublicNode / Config V3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Gate attendu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : même gate vert, cette fois sans warning `field_reassign_with_default`.
|
||||||
73
deltas/0.2.9/pre.008.md
Normal file
73
deltas/0.2.9/pre.008.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.008.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.008` — Yellowstone Blocks + block_meta + entry
|
||||||
|
|
||||||
|
## 1. Base et signal technique
|
||||||
|
|
||||||
|
Base : `0.2.9-pre.007`, fermée sur gate opérateur intégralement vert.
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Scope
|
||||||
|
|
||||||
|
La tranche complète uniquement la dernière famille de filtres/updates standard avant l’ouverture bidi : Blocks, `block_meta` et `entry`.
|
||||||
|
|
||||||
|
| Surface | Matérialisation |
|
||||||
|
|----------------------------|-----------------|
|
||||||
|
| filtre Blocks | complet |
|
||||||
|
| `blocks_meta` | marker inchangé |
|
||||||
|
| `entry` | marker inchangé |
|
||||||
|
| `SubscribeUpdateBlock` | DTO + decode |
|
||||||
|
| `SubscribeUpdateBlockMeta` | DTO + decode |
|
||||||
|
| `SubscribeUpdateEntry` | DTO + decode |
|
||||||
|
| stream bidi / lifecycle | OUT |
|
||||||
|
| reconnect / replay | OUT |
|
||||||
|
| PublicNode / Config V3 | OUT |
|
||||||
|
| `SubscribeDeshred` | OUT `0.2.9` |
|
||||||
|
|
||||||
|
## 3. Filtre Blocks
|
||||||
|
|
||||||
|
`YellowstoneSubscribeBlockFilter` matérialise exactement le wire courant : `account_include[]`, `include_transactions?`, `include_accounts?`, `include_entries?` et `cuckoo_account_include?`. Les account selectors utilisent `ksp_core_lib::Pubkey`, restent ordonnés et bornés ; `Debug` ne rend aucune adresse.
|
||||||
|
|
||||||
|
## 4. Updates Blocks
|
||||||
|
|
||||||
|
`YellowstoneBlockUpdate` conserve la metadata de bloc, rewards, temps/hauteur optionnels, parent, compteurs et payloads optionnels. Les transactions réutilisent `YellowstoneTransactionInfo`, les comptes réutilisent `YellowstoneAccountInfo`, et les entries utilisent `YellowstoneEntryInfo`.
|
||||||
|
|
||||||
|
`YellowstoneBlockMetaUpdate` conserve la variante metadata-only. `YellowstoneEntryUpdate` enveloppe la même `YellowstoneEntryInfo` réutilisée dans les blocs. `starting_transaction_index` reste explicite, y compris sa valeur legacy `0`.
|
||||||
|
|
||||||
|
Les compteurs serveur sont indépendants des tailles de vecteurs : aucune égalité artificielle n’est imposée quand les flags `include_*` demandent l’omission d’un payload.
|
||||||
|
|
||||||
|
## 5. Validation et sécurité
|
||||||
|
|
||||||
|
Les blockhash textuels sont bornés, trim-exacts et Base58-décodés vers exactement 32 octets ; les hash d’entrée sont exactement 32 octets. Les collections Block/Rewards sont bornées. `Debug` ne copie aucun blockhash, parent blockhash, reward pubkey, account selector, données account, transaction, instruction ou entry hash.
|
||||||
|
|
||||||
|
Les conversions protobuf et décodeurs restent sous `#[cfg(test)]` jusqu’à l’ouverture du stream runtime en `pre.009`.
|
||||||
|
|
||||||
|
## 6. Tests/canaries ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
block filter exact wire + redaction
|
||||||
|
block update complet avec Transaction/Account/Entry réutilisés
|
||||||
|
server counts distincts des payload-vector lengths
|
||||||
|
block-meta optional states
|
||||||
|
entry starting_transaction_index + malformed hash
|
||||||
|
public API canary pre.008
|
||||||
|
release completeness canary pre.008
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Gate opérateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n’est modifiée ; aucun `cargo tree` supplémentaire n’est requis.
|
||||||
75
deltas/0.2.9/pre.009-fix.001.md
Normal file
75
deltas/0.2.9/pre.009-fix.001.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.009-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.009-fix.001` — hygiène Clippy du flux bidi
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Supprimer les deux warnings Clippy observés au gate opérateur de `pre.009`, sans modifier le protocole Yellowstone, le lifecycle bidi, les bornes ni les dépendances.
|
||||||
|
|
||||||
|
## 2. Gate opérateur avant fix
|
||||||
|
|
||||||
|
| Gate | Résultat |
|
||||||
|
|------------------------------------------|-------------------|
|
||||||
|
| `cargo fmt --all` | PASS |
|
||||||
|
| audit Rust workspace | PASS / clean |
|
||||||
|
| `cargo check --workspace` | PASS |
|
||||||
|
| `cargo clippy --workspace --all-targets` | PASS + 2 warnings |
|
||||||
|
| Transport unit | 379/379 PASS |
|
||||||
|
| Transport `public_api` | 48/48 PASS |
|
||||||
|
| Transport `release_completeness` | 42/42 PASS |
|
||||||
|
| Transport doctests | 4/4 PASS |
|
||||||
|
| Core dependency canary | 3/3 PASS |
|
||||||
|
| `cargo test --workspace` | PASS |
|
||||||
|
|
||||||
|
Warnings :
|
||||||
|
|
||||||
|
```text
|
||||||
|
clippy::large_enum_variant
|
||||||
|
YellowstoneSubscribeUpdate::Transaction >= 664 bytes
|
||||||
|
YellowstoneSubscribeUpdate::Block >= 280 bytes
|
||||||
|
|
||||||
|
clippy::extra_unused_type_parameters
|
||||||
|
assert_send<T: Send>() dans tests/public_api.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Correctifs
|
||||||
|
|
||||||
|
### 3.1 `YellowstoneSubscribeUpdate::Transaction`
|
||||||
|
|
||||||
|
La variante devient :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Transaction(Box<YellowstoneTransactionUpdate>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le décodeur alloue cette indirection uniquement pour les updates transaction. Le DTO `YellowstoneTransactionUpdate`, le wire protobuf et les autres variantes ne changent pas. La queue bidi n’a plus à dimensionner chaque élément sur la variante transaction de ~664 octets.
|
||||||
|
|
||||||
|
### 3.2 Canari `Send`
|
||||||
|
|
||||||
|
Le helper de test conserve `T: Send` et matérialise `PhantomData<T>` dans son corps. Le paramètre générique devient réellement utilisé sans construction réseau ni changement de contrat.
|
||||||
|
|
||||||
|
## 4. Frontières inchangées
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconnect / resubscribe / replay OUT -> pre.010
|
||||||
|
SubscribeDeshred OUT 0.2.9
|
||||||
|
PublicNode / Config V3 OUT pre.009
|
||||||
|
dépendances / features inchangées
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `#[allow(clippy::...)]` n’est ajouté.
|
||||||
|
|
||||||
|
## 5. Gate attendu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu : Clippy sans warning et compteurs fonctionnels inchangés à 379 unit / 48 public API / 42 release-completeness / 4 doctests.
|
||||||
68
deltas/0.2.9/pre.009.md
Normal file
68
deltas/0.2.9/pre.009.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.009.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.009` — bidi standard + backpressure + half-close + shutdown
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.008-fix.001
|
||||||
|
gate opérateur final : fmt/audit/check/Clippy/workspace PASS sans warning
|
||||||
|
Transport : 370 unit + 47 public API + 41 release-completeness + 4 doctests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changements
|
||||||
|
|
||||||
|
- ouvre `/geyser.Geyser/Subscribe` avec le channel Tonic N1 existant ;
|
||||||
|
- ajoute `SolanaYellowstoneGrpcSubscribeSession` et son état public sûr ;
|
||||||
|
- branche directement une `mpsc` request bornée dans le stream client Tonic ;
|
||||||
|
- ajoute `try_update()` avec validation, borne protobuf et erreurs Full/Closed structurées ;
|
||||||
|
- promeut les conversions request et décodeurs `pre.004–008` de test-only vers runtime privé ;
|
||||||
|
- expose `YellowstoneSubscribeUpdate` couvrant les neuf variantes standard ;
|
||||||
|
- répond automatiquement au Ping serveur avec un request ping-only `id=1` et conserve Pong observable ;
|
||||||
|
- traite le server half-close comme terminaison normale de `pre.009` ;
|
||||||
|
- ajoute client half-close, shutdown borné, Drop best-effort et overflow terminal ;
|
||||||
|
- conserve les `Status` distants sous forme de diagnostics KSP sûrs sans recopier message/details/metadata ;
|
||||||
|
- ajoute les codes `grpc_backpressure_overflow` et `grpc_session_closed`.
|
||||||
|
|
||||||
|
## Fixture locale
|
||||||
|
|
||||||
|
| Cas | Preuve |
|
||||||
|
|------------------------|-------------|
|
||||||
|
| round-trip bidi | PASS source |
|
||||||
|
| mutation request | PASS source |
|
||||||
|
| Ping -> ping id=1 | PASS source |
|
||||||
|
| Pong observable | PASS source |
|
||||||
|
| server half-close | PASS source |
|
||||||
|
| client half-close | PASS source |
|
||||||
|
| shutdown hostile borné | PASS source |
|
||||||
|
| Drop session | PASS source |
|
||||||
|
| slow receiver overflow | PASS source |
|
||||||
|
| remote Status sûr | PASS source |
|
||||||
|
| update malformed | PASS source |
|
||||||
|
| inbound oversized | PASS source |
|
||||||
|
| outbound oversized | PASS source |
|
||||||
|
|
||||||
|
## Frontières
|
||||||
|
|
||||||
|
```text
|
||||||
|
OUT pre.009 : reconnect/resubscribe/from_slot replay policy/gaps/duplicates
|
||||||
|
OUT 0.2.9 : SubscribeDeshred
|
||||||
|
OUT pre.009 : PublicNode / Config V3 / provider facade
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ou feature Cargo n'est ajoutée. Le runtime n'utilise toujours pas `yellowstone-grpc-client` et n'expose aucun type Tonic/protobuf brut.
|
||||||
|
|
||||||
|
## Validation candidate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `cargo tree` n'est pas requis : aucune dépendance ni feature n'a changé.
|
||||||
101
deltas/0.2.9/pre.010-fix.001.md
Normal file
101
deltas/0.2.9/pre.010-fix.001.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.010-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.010-fix.001` — Clippy reconnect + cohérence crate-root
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.010
|
||||||
|
workspace.package.version = 0.2.9-pre.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur fourni confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS + 2 warnings
|
||||||
|
Transport unit 383/383 PASS
|
||||||
|
Transport public_api 49/49 PASS
|
||||||
|
Transport release_completeness 43/43 PASS
|
||||||
|
Transport doctests 4/4 PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Motif
|
||||||
|
|
||||||
|
Deux `clippy::collapsible_if` subsistent dans `grpc_stream.rs` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ContinuityTracker::observe() éviction du cache borné
|
||||||
|
run_subscribe_actor() Ping -> send_automatic_ping
|
||||||
|
```
|
||||||
|
|
||||||
|
La revue de code détecte également une incohérence documentaire dans `src/lib.rs` : le commentaire crate-root reste au niveau `pre.009` et affirme encore que reconnect/replay sont hors tranche, alors que `pre.010` les a matérialisés.
|
||||||
|
|
||||||
|
## 3. Correctifs
|
||||||
|
|
||||||
|
### 3.1 Clippy
|
||||||
|
|
||||||
|
Les deux `if` imbriqués sont convertis en let-chains équivalentes. Aucun `#[allow(clippy::...)]` n'est ajouté.
|
||||||
|
|
||||||
|
Aucune sémantique n'est modifiée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cache duplicate toujours borné à 512 identités
|
||||||
|
Ping automatique même send_automatic_ping, mêmes erreurs terminales
|
||||||
|
reconnect inchangé
|
||||||
|
ReplayInfo/from_slot inchangés
|
||||||
|
gaps/duplicates compteurs inchangés
|
||||||
|
backpressure/shutdown inchangés
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Documentation crate-root
|
||||||
|
|
||||||
|
`ksp-onchain-transport-lib/src/lib.rs` décrit maintenant explicitement `0.2.9-pre.010` : reconnect borné KSP-owned, reprise depuis le dernier request/slot observé, gap prudent via ReplayInfo et duplicates observables sans promesse exactly-once/lossless.
|
||||||
|
|
||||||
|
### 3.3 Signal de version
|
||||||
|
|
||||||
|
Le correctif touche du Rust ; conformément à `VER-ID-007`/`VER-ID-010` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.10.fix.1
|
||||||
|
commit attendu = v0.2.9-pre.010-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Documentation de release
|
||||||
|
|
||||||
|
Mise à jour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier gate opérateur et le statut du fix y sont enregistrés. `pre.011` reste explicitement non commencée avant fermeture sans warning du présent fix.
|
||||||
|
|
||||||
|
## 5. Frontières inchangées
|
||||||
|
|
||||||
|
```text
|
||||||
|
OUT fix.001 : Config V3 / PublicNode / provider facade
|
||||||
|
OUT 0.2.9 : SubscribeDeshred
|
||||||
|
aucune dépendance / feature Cargo ajoutée
|
||||||
|
aucun wire protobuf modifié
|
||||||
|
aucun contrat public reconnect/replay modifié
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Gate opérateur attendu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu : Clippy sans warning, Transport 383 unit / 49 public API / 43 release-completeness / 4 doctests et workspace vert. Aucun `cargo tree` supplémentaire n'est requis, le graphe Cargo étant inchangé.
|
||||||
94
deltas/0.2.9/pre.010.md
Normal file
94
deltas/0.2.9/pre.010.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.010.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.010` — reconnect borné + replay prudent + continuité observable
|
||||||
|
|
||||||
|
## Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.009-fix.001
|
||||||
|
gate opérateur final : fmt/audit/check/Clippy/workspace PASS sans warning
|
||||||
|
Transport : 379 unit + 48 public API + 42 release-completeness + 4 doctests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changements runtime
|
||||||
|
|
||||||
|
- ajoute l'état public `YellowstoneGrpcSubscribeState::Reconnecting` ;
|
||||||
|
- ajoute `YellowstoneGrpcSubscribeSnapshot` avec compteurs sûrs de reconnect/replay/continuité ;
|
||||||
|
- active le `YellowstoneGrpcReconnectSettings` déjà introduit en `pre.002` pour le stream `Subscribe` ;
|
||||||
|
- rouvre le stream avec un backoff exponentiel borné et un nombre d'essais borné ;
|
||||||
|
- conserve le dernier `YellowstoneSubscribeRequest` complet accepté ;
|
||||||
|
- après perte du stream, resoumet ce request déterministement avec un `from_slot` au moins égal au plus haut slot déjà observé ;
|
||||||
|
- consulte `SubscribeReplayInfo` avant les tentatives de replay lorsque `from_slot` est disponible ;
|
||||||
|
- si `first_available > from_slot`, incrémente `continuity_gap_count` et clamp le replay à `first_available` ;
|
||||||
|
- maintient un cache borné de 512 identités d'updates pour observer les duplicates autour d'un replay ;
|
||||||
|
- les duplicates restent livrés : aucun contrat exactly-once n'est inventé ;
|
||||||
|
- rejette les mutations request pendant `Reconnecting` pour éviter une application ambiguë entre deux streams physiques ;
|
||||||
|
- un shutdown pendant le backoff interrompt immédiatement la boucle et interdit une nouvelle ouverture ;
|
||||||
|
- un épuisement du budget devient terminal avec un code KSP sûr `grpc_channel_failed`.
|
||||||
|
|
||||||
|
## Sémantique de continuité
|
||||||
|
|
||||||
|
```text
|
||||||
|
from_slot de reconnect = max(from_slot explicite du dernier request, last_observed_slot)
|
||||||
|
ReplayInfo = information de rétention ; pas preuve de replay complet
|
||||||
|
continuity gap = couverture de replay indisponible, comptée seulement si first_available > slot demandé
|
||||||
|
slot manquant entre deux updates filtrés = jamais assimilé automatiquement à un gap
|
||||||
|
duplicate = identité KSP bornée déjà observée ; update toujours livrée
|
||||||
|
exactly-once = non garanti
|
||||||
|
lossless = non garanti
|
||||||
|
ordre global sans gap = non garanti
|
||||||
|
```
|
||||||
|
|
||||||
|
Identités retenues pour l'observation bornée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Account slot + pubkey + write_version
|
||||||
|
Slot slot + status
|
||||||
|
Transaction slot + signature
|
||||||
|
TransactionStatus slot + signature
|
||||||
|
Block slot + blockhash
|
||||||
|
BlockMeta slot + blockhash
|
||||||
|
Entry slot + index + hash
|
||||||
|
Ping/Pong hors déduplication
|
||||||
|
```
|
||||||
|
|
||||||
|
La couverture d'une éventuelle divergence de node reste volontairement limitée : deux Block/BlockMeta de même slot mais de blockhash différent ne sont pas classés duplicate. KSP ne généralise pas cette preuve aux familles qui ne transportent pas de blockhash.
|
||||||
|
|
||||||
|
## Fixture locale
|
||||||
|
|
||||||
|
| Cas | Preuve |
|
||||||
|
|------------------------------------------------|-------------|
|
||||||
|
| reconnect après server half-close | PASS source |
|
||||||
|
| `from_slot = last_observed_slot` | PASS source |
|
||||||
|
| ReplayInfo disponible | PASS source |
|
||||||
|
| replay duplicate observable | PASS source |
|
||||||
|
| couverture replay indisponible prouvée + clamp | PASS source |
|
||||||
|
| budget reconnect épuisé | PASS source |
|
||||||
|
| mutation pendant reconnect rejetée | PASS source |
|
||||||
|
| shutdown pendant backoff | PASS source |
|
||||||
|
| endpoint/Status secret non réémis | PASS source |
|
||||||
|
| anciens gates bidi avec reconnect `0` | PASS source |
|
||||||
|
|
||||||
|
## Frontières
|
||||||
|
|
||||||
|
```text
|
||||||
|
OUT pre.010 : Config V3 / PublicNode / provider facade
|
||||||
|
OUT 0.2.9 : SubscribeDeshred
|
||||||
|
aucune nouvelle dépendance / feature Cargo
|
||||||
|
aucun yellowstone-grpc-client runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation candidate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `cargo tree` n'est pas requis : aucune dépendance ni feature n'a changé.
|
||||||
284
deltas/0.2.9/pre.011.md
Normal file
284
deltas/0.2.9/pre.011.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.011.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.011` — Config Transport V3 + PublicNode Mainnet + consolidation documentaire
|
||||||
|
|
||||||
|
## 1. Base et version
|
||||||
|
|
||||||
|
Base immédiate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.010-fix.001
|
||||||
|
Cargo workspace = 0.2.9-pre.10.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Version candidate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.011
|
||||||
|
Cargo workspace = 0.2.9-pre.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur fourni pour `pre.010-fix.001` est entièrement vert : fmt/audit/check/Clippy sans warning, Transport 383 unit + 49 public API + 43 release-completeness + 4 doctests, dependency canary 3/3 et `cargo test --workspace` PASS.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise la frontière Config décidée par `pre.001` puis différée jusqu'après le lifecycle/reconnect Yellowstone :
|
||||||
|
|
||||||
|
```text
|
||||||
|
std.transport V3
|
||||||
|
Config -> YellowstoneGrpcTransportSettings
|
||||||
|
provider et protocol distincts
|
||||||
|
metadata publique et secrète avec provenance contrôlée
|
||||||
|
premier profil PublicNode Mainnet standard
|
||||||
|
backward V1/V2 strict
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle profite aussi du changement de phase de la release pour remettre le plan `016` et la validation `012` dans un état maintenable. Les détails historiques ne sont pas supprimés : ils restent dans les deltas immuables `deltas/0.2.9/`.
|
||||||
|
|
||||||
|
## 3. Config Transport V3
|
||||||
|
|
||||||
|
### 3.1 Schema
|
||||||
|
|
||||||
|
`config/schemas/std.transport.schema.json` devient `urn:ksp:schema:std.transport:v3` et conserve trois branches strictes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
V1 = HTTP-only
|
||||||
|
V2 = HTTP + WebSocket
|
||||||
|
V3 = HTTP + WebSocket + Yellowstone gRPC optionnel par profil
|
||||||
|
```
|
||||||
|
|
||||||
|
V3 ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
grpc_defaults
|
||||||
|
grpc_endpoints[]?
|
||||||
|
```
|
||||||
|
|
||||||
|
`grpc_defaults` mappe uniquement des knobs déjà possédés par `YellowstoneGrpcSessionSettings` : timeouts, reconnect, channel capacities et max message sizes. Les bounds des filtres Subscribe restent un contrat Transport et ne sont pas transformés en configuration arbitraire.
|
||||||
|
|
||||||
|
Chaque endpoint gRPC contient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
name
|
||||||
|
enabled
|
||||||
|
provider
|
||||||
|
cluster
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url
|
||||||
|
metadata[]?
|
||||||
|
secret_metadata[]?
|
||||||
|
session?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Mapping Config -> Transport
|
||||||
|
|
||||||
|
`ResolvedTransportConfig` ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
grpc_settings()
|
||||||
|
into_all_transport_settings()
|
||||||
|
```
|
||||||
|
|
||||||
|
L'accesseur historique suivant est volontairement inchangé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
into_transport_settings() -> (HttpTransportSettings, Option<WsTransportSettings>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cela évite une rupture source silencieuse des consumers V2.
|
||||||
|
|
||||||
|
V1 et V2 retournent toujours `None` pour gRPC. Un profil V3 qui n'a pas de `grpc_endpoints` retourne également `None` au lieu d'inventer un Transport gRPC vide.
|
||||||
|
|
||||||
|
### 3.3 Protocol vs provider
|
||||||
|
|
||||||
|
Config exige actuellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
```
|
||||||
|
|
||||||
|
Le champ `provider` reste descriptif et mappe vers `YellowstoneGrpcProviderName`.
|
||||||
|
|
||||||
|
Donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
PublicNode != protocole
|
||||||
|
PublicNode != nouvelle façade Transport
|
||||||
|
PublicNode = environnement d'exécution du standard Yellowstone
|
||||||
|
```
|
||||||
|
|
||||||
|
Toute future divergence provider doit rester un delta N3 explicite.
|
||||||
|
|
||||||
|
### 3.4 Metadata et provenance
|
||||||
|
|
||||||
|
Deux classes sont matérialisées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata
|
||||||
|
secret_metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
Règles avant mapping :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata + provenance KSP_SECRET_*/KSPB_SECRET_* -> reject
|
||||||
|
secret_metadata sans provenance secret -> reject
|
||||||
|
secret_metadata + provenance non-secret env -> reject
|
||||||
|
secret_metadata + provenance secret -> accept
|
||||||
|
```
|
||||||
|
|
||||||
|
Les segments littéraux autour d'un placeholder secret restent permis. Config possède la provenance et la projection sûre ; Transport reçoit ensuite seulement les `YellowstoneGrpcMetadataEntry` déjà classifiées.
|
||||||
|
|
||||||
|
Aucun nom d'environnement n'est lu dans Transport.
|
||||||
|
|
||||||
|
## 4. Profil PublicNode Mainnet
|
||||||
|
|
||||||
|
La surface publique réauditée le 2026-08-24 confirme Yellowstone gRPC Solana Mainnet et affiche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
`YellowstoneGrpcEndpointUrl` exige une URL `http/https`; le document Config représente donc ce même endpoint TLS comme `https://solana-yellowstone-grpc.publicnode.com:443`.
|
||||||
|
|
||||||
|
Le document committé ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_mainnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = mainnet-beta
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
metadata = aucune
|
||||||
|
```
|
||||||
|
|
||||||
|
Le profil générique `mainnet_public` reste distinct et ne reçoit pas implicitement un endpoint PublicNode.
|
||||||
|
|
||||||
|
PublicNode expose également Solana Testnet gRPC, mais le hostname exact n'a pas été obtenu depuis une source suffisamment autoritative/indexable pendant cette tranche. Aucun endpoint supposé n'est versionné. `pre.012` réaudite cette valeur avant tout profil/smoke Testnet.
|
||||||
|
|
||||||
|
## 5. Tests ajoutés/étendus
|
||||||
|
|
||||||
|
La fixture V3 couvre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
mapping gRPC valide
|
||||||
|
merge grpc_defaults + session overrides
|
||||||
|
metadata publique issue de KSP_PUBLIC_*
|
||||||
|
secret_metadata issue de KSP_SECRET_*
|
||||||
|
safe_value avec segment secret redacted
|
||||||
|
rejet public <- secret
|
||||||
|
rejet secret <- public
|
||||||
|
```
|
||||||
|
|
||||||
|
Le config committé couvre le profil `publicnode_mainnet` et vérifie la construction d'un `YellowstoneGrpcTransportSettings` valide sans URL dans son `Debug`.
|
||||||
|
|
||||||
|
Le canari public API inclut les nouveaux accessors sans supprimer les anciens.
|
||||||
|
|
||||||
|
## 6. Consolidation plan/validation
|
||||||
|
|
||||||
|
`docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md` et `docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md` sont réorganisés autour de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
état courant
|
||||||
|
scope/architecture stabilisés
|
||||||
|
matrices protocole encore normatives
|
||||||
|
Config V3
|
||||||
|
PublicNode
|
||||||
|
lifecycle/replay
|
||||||
|
historique compact des gates fermés
|
||||||
|
forecast restant
|
||||||
|
critères de clôture
|
||||||
|
```
|
||||||
|
|
||||||
|
Les longues sections append-only propres à chaque prerelease sont remplacées par une table compacte renvoyant aux deltas immuables. Aucun delta historique n'est modifié.
|
||||||
|
|
||||||
|
Les tableaux Markdown sont reformattés selon la convention JetBrains RustRover utilisée dans le workspace : largeur calculée sur la cellule la plus large et un espace de padding autour du contenu.
|
||||||
|
|
||||||
|
## 7. Documentation Config synchronisée
|
||||||
|
|
||||||
|
La README et l'USAGE de `ksp-config-lib`, ainsi que les commentaires du registre, sont mis à jour pour ne plus présenter `std.transport` comme un document uniquement V2 HTTP + WebSocket.
|
||||||
|
|
||||||
|
La documentation finale spécifique Transport/Yellowstone reste un gate de `pre.012`.
|
||||||
|
|
||||||
|
## 8. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
config/std.transport.json
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
crates/ksp-config-lib/README.md
|
||||||
|
crates/ksp-config-lib/USAGE.md
|
||||||
|
crates/ksp-config-lib/src/lib.rs
|
||||||
|
crates/ksp-config-lib/src/registry.rs
|
||||||
|
crates/ksp-config-lib/src/transport.rs
|
||||||
|
crates/ksp-config-lib/tests/public_api.rs
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.9/pre.011.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance Cargo n'est ajoutée ou modifiée.
|
||||||
|
|
||||||
|
## 9. Validation source disponible avant livraison
|
||||||
|
|
||||||
|
Exécuté dans l'environnement de préparation :
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
JSON Schema Draft 2020-12 meta-validation: PASS
|
||||||
|
config/std.transport.json against V3 schema: PASS
|
||||||
|
fixture V2 against V3 schema: PASS
|
||||||
|
fixture V1 against V3 schema: PASS
|
||||||
|
fixture V3 against V3 schema: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo/Rustfmt ne sont pas disponibles dans l'environnement de préparation. Aucune compilation ou test Cargo n'est donc déclaré réussi pour `pre.011` avant gate opérateur.
|
||||||
|
|
||||||
|
## 10. Gate opérateur requis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-config-lib
|
||||||
|
cargo test -p ksp-config-lib --test public_api
|
||||||
|
cargo test -p ksp-config-lib --test ownership
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attentes de candidate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config unit 113 attendus (110 baseline + 3 V3)
|
||||||
|
Config public API 15 attendus
|
||||||
|
Config ownership 5 attendus
|
||||||
|
Transport unit >= 383
|
||||||
|
Transport public API >= 49
|
||||||
|
Transport completeness >= 43
|
||||||
|
Transport doctests 4
|
||||||
|
workspace dependencies 3
|
||||||
|
Clippy aucun warning
|
||||||
|
workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces valeurs ne deviennent des résultats qu'après exécution opérateur.
|
||||||
|
|
||||||
|
## 11. Suite
|
||||||
|
|
||||||
|
Après fermeture de `pre.011` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.012 PublicNode live + Testnet endpoint re-audit + compliance finale + cargo graphs + docs/prompt suivant
|
||||||
|
rel.001 publication stable stricte
|
||||||
|
```
|
||||||
|
|
||||||
|
Si `pre.012` ne peut pas fermer proprement tous les gates dans une seule tranche, continuer avec `pre.013+` plutôt que compacter artificiellement la clôture.
|
||||||
103
deltas/0.2.9/pre.012-fix.001.md
Normal file
103
deltas/0.2.9/pre.012-fix.001.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Delta `0.2.9-pre.012-fix.001` — séparation normative de la fermeture
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.9-pre.012
|
||||||
|
Cargo : 0.2.9-pre.12
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce correctif est purement documentaire/normatif. Conformément à `VER-ID-008`, `workspace.package.version` reste `0.2.9-pre.12`.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Éviter que les dernières prereleases d'une release mélangent smokes/live, réconciliation README/USAGE/validation et préparation CHANGELOG/ROADMAP/prompt.
|
||||||
|
|
||||||
|
La fermeture standard devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.N-2 gate technique/live si nécessaire
|
||||||
|
pre.N-1 réconciliation documentaire finale
|
||||||
|
pre.N prompt suivant + CHANGELOG + ROADMAP uniquement
|
||||||
|
rel.001 publication stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Les fichiers mécaniques `Cargo.toml` et delta restent autorisés lorsqu'ils sont exigés par le workflow.
|
||||||
|
|
||||||
|
## Preuve opérateur `pre.012` reçue
|
||||||
|
|
||||||
|
Le log opérateur du 2026-08-24 confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt/audit/check/clippy PASS sans warning
|
||||||
|
Markdown tables PASS, 87 tableaux / 252 fichiers
|
||||||
|
Config unit 113/113
|
||||||
|
Config public API 15/15
|
||||||
|
Config ownership 5/5
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
workspace dependencies 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
PublicNode live smoke non exécuté, test ignored
|
||||||
|
cargo tree final non présent dans le log fourni
|
||||||
|
```
|
||||||
|
|
||||||
|
## Décisions normatives
|
||||||
|
|
||||||
|
- la dernière prerelease avant `rel.NNN` est limitée fonctionnellement au prompt de la release suivante, `CHANGELOG.md` et `ROADMAP.md` ;
|
||||||
|
- la prerelease précédente possède la réconciliation finale des README/USAGE, plans, validations et références durables ;
|
||||||
|
- lorsqu'un smoke/live final existe, il possède une prerelease technique dédiée avant la réconciliation documentaire ;
|
||||||
|
- si aucun smoke n'est requis, seule cette tranche est omise ; les deux dernières responsabilités restent séparées ;
|
||||||
|
- un `fix` reste local au couloir de sa prerelease et n'absorbe jamais une autre responsabilité de fermeture ;
|
||||||
|
- si un défaut d'un couloir antérieur apparaît tard, une nouvelle prerelease dédiée est ouverte et les couloirs postérieurs sont rejoués ;
|
||||||
|
- `rel.NNN` ne sert pas de rattrapage technique ou documentaire.
|
||||||
|
|
||||||
|
## Recalibrage `0.2.9`
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.013 smoke PublicNode Mainnet + graphes Cargo finaux
|
||||||
|
pre.014 réconciliation plan/validation/README/USAGE/références
|
||||||
|
pre.015 finalisation prompt 0.2.10 + CHANGELOG + ROADMAP
|
||||||
|
rel.001 publication stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt et les README/USAGE déjà préparés en `pre.012` ne sont pas supprimés de l'historique : ils seront respectivement finalisés en `pre.015` et réconciliés en `pre.014`.
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/rules/PROMPT_STRUCTURE.md
|
||||||
|
docs/rules/VERSION_WORKFLOW.md
|
||||||
|
docs/rules/FILE_CONTRACTS.md
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.9/pre.012-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Validations à exécuter pour ce fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_markdown_tables.py docs/rules/PROMPT_STRUCTURE.md docs/rules/VERSION_WORKFLOW.md docs/rules/FILE_CONTRACTS.md docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md deltas/0.2.9/pre.012-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
L'audit Rust/Cargo complet n'est pas requis par ce fix documentaire lui-même ; le prochain gate technique est `pre.013`.
|
||||||
|
|
||||||
|
## Validations non exécutées dans ce delta
|
||||||
|
|
||||||
|
```text
|
||||||
|
smoke PublicNode Mainnet live
|
||||||
|
cargo tree final
|
||||||
|
```
|
||||||
|
|
||||||
|
Elles appartiennent désormais explicitement à `pre.013`.
|
||||||
257
deltas/0.2.9/pre.012.md
Normal file
257
deltas/0.2.9/pre.012.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.012.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.012` — fermeture technique Yellowstone + PublicNode + documentation
|
||||||
|
|
||||||
|
## Statut
|
||||||
|
|
||||||
|
Candidate de fermeture technique `0.2.9` construite sur `0.2.9-pre.011`, dont le gate opérateur est entièrement vert.
|
||||||
|
|
||||||
|
Cette tranche ne change pas la sémantique runtime Yellowstone fermée en `pre.010`. Elle ajoute le smoke live PublicNode Mainnet opt-in, ferme le réaudit documentaire final, synchronise README/USAGE, prépare le prompt `0.2.10 — OrbitFlare Yellowstone gRPC` et rend la mise en forme des tableaux Markdown normative et mécaniquement vérifiable.
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.12
|
||||||
|
```
|
||||||
|
|
||||||
|
## Base opérateur confirmée
|
||||||
|
|
||||||
|
`pre.011` a été validée avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
scripts/audit_rust_workspace_rules.py PASS, 0 export candidate
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
Config unit 113/113
|
||||||
|
Config public API 15/15
|
||||||
|
Config ownership 5/5
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport release completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
workspace dependencies 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Réaudit Yellowstone final
|
||||||
|
|
||||||
|
Le réaudit du 2026-08-24 distingue désormais explicitement les numéros de `master` et les releases GitHub publiées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
latest release GitHub indexée v14.2.2+solana.4.1.0
|
||||||
|
master yellowstone-grpc-geyser 15.1.2
|
||||||
|
master yellowstone-grpc-client 13.3.0
|
||||||
|
master yellowstone-grpc-proto 12.6.0
|
||||||
|
master Agave Geyser / storage 4.2.0
|
||||||
|
master Tonic / Prost 0.14.x
|
||||||
|
```
|
||||||
|
|
||||||
|
La surface N2 utilisée par KSP reste matériellement compatible : service `Geyser`, `Subscribe`, sept unary retenus, `SubscribeRequest` courant et neuf variantes `SubscribeUpdate`. `SubscribeDeshred` reste hors scope.
|
||||||
|
|
||||||
|
La licence du subtree `yellowstone-grpc-proto` reste déclarée Apache-2.0 dans `LICENSING.md` upstream.
|
||||||
|
|
||||||
|
## PublicNode Mainnet
|
||||||
|
|
||||||
|
Ajout d'un smoke Transport pur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke :
|
||||||
|
|
||||||
|
```text
|
||||||
|
est ignored par défaut
|
||||||
|
construit ses settings programmatiquement
|
||||||
|
n'utilise ni Config ni variable d'environnement
|
||||||
|
n'utilise aucun credential
|
||||||
|
cible https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
ouvre le channel TLS Yellowstone standard
|
||||||
|
appelle GetVersion
|
||||||
|
appelle GetSlot avec commitment confirmed
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne crée aucune façade `PublicNodeGrpc*` et réutilise le contrat provider-neutral de `0.2.9`.
|
||||||
|
|
||||||
|
## PublicNode Testnet
|
||||||
|
|
||||||
|
Le service Testnet gRPC est annoncé par PublicNode, mais l'hostname exact n'est toujours pas disponible dans une source PublicNode suffisamment autoritative et exploitable.
|
||||||
|
|
||||||
|
Décision :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun hostname déduit
|
||||||
|
aucun profil Testnet
|
||||||
|
aucun smoke Testnet
|
||||||
|
statut documentaire = EXTERNAL BLOCK / NOT GUESSED
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette limite externe ne bloque pas la fondation Mainnet et ne doit pas être transformée en endpoint supposé.
|
||||||
|
|
||||||
|
## Règles des tableaux Markdown
|
||||||
|
|
||||||
|
`docs/rules/RULES_DOCUMENTATION.md` formalise désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pipe littéral ou échappé interdit dans le contenu d'une cellule
|
||||||
|
pipe réservé aux séparateurs structurels
|
||||||
|
largeur de colonne déterminée par le contenu le plus large
|
||||||
|
tous les séparateurs verticaux alignés
|
||||||
|
exactement un espace de marge autour du contenu le plus large
|
||||||
|
cellules plus courtes complétées uniquement pour conserver l'alignement
|
||||||
|
ligne séparatrice dimensionnée exactement à la colonne
|
||||||
|
tableau entier réaligné lorsqu'une ligne est touchée
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajout du canari :
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/audit_markdown_tables.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Il audite les fichiers ou répertoires Markdown explicitement fournis, ignore les blocs de code fenced et rejette les tables qui ne respectent pas le contrat KSP.
|
||||||
|
|
||||||
|
La version de `docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md` corrigée manuellement par l'opérateur avant le commit `pre.011` a été conservée comme base ; `pre.012` ne réintroduit pas l'ancien format.
|
||||||
|
|
||||||
|
## Normalisation documentaire rétroactive modifiable
|
||||||
|
|
||||||
|
Le premier audit étendu de la nouvelle règle a révélé 26 écarts historiques dans neuf documents **modifiables** : principalement du padding antérieur au contrat actuel et un tableau HTTP utilisant des pipes échappés dans ses cellules.
|
||||||
|
|
||||||
|
Ces documents ont été normalisés sans changer leurs décisions :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/formats/KSPWALLET_V1.md
|
||||||
|
docs/formats/KSPWALLET_V2.md
|
||||||
|
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||||
|
docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
|
||||||
|
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||||
|
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
|
||||||
|
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans le plan HTTP `008`, les anciens `\|` utilisés pour exprimer une alternative ont été reformulés avec `/`, conformément à la nouvelle interdiction des pipes internes.
|
||||||
|
|
||||||
|
Un scan du repository a encore signalé uniquement onze écarts de padding dans d'anciens deltas `0.1.4`. Ils ne sont **pas** modifiés : les deltas déjà publiés sont immuables. `DOC-TABLE-006` formalise cette priorité ; tous les nouveaux deltas restent soumis au format courant.
|
||||||
|
|
||||||
|
## Documentation Transport
|
||||||
|
|
||||||
|
`README.md` et `USAGE.md` documentent maintenant la foundation Yellowstone complète :
|
||||||
|
|
||||||
|
```text
|
||||||
|
settings/channel/unary/Subscribe
|
||||||
|
reconnect/replay prudent
|
||||||
|
Config V3
|
||||||
|
PublicNode Mainnet
|
||||||
|
smoke live opt-in
|
||||||
|
limite Testnet non inventée
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan et validation
|
||||||
|
|
||||||
|
Le plan `016` et la validation `012` restent structurés par état courant plutôt que par appendices successifs.
|
||||||
|
|
||||||
|
Mises à jour principales :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.011 fermé par gate opérateur
|
||||||
|
pre.012 active candidate
|
||||||
|
récconciliation master vs release GitHub
|
||||||
|
PublicNode Testnet fermé comme limite externe documentée
|
||||||
|
smoke Mainnet ajouté mais non déclaré exécuté
|
||||||
|
cargo tree final explicitement pending operator
|
||||||
|
workspace final explicitement pending operator
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompt suivant
|
||||||
|
|
||||||
|
Ajout :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
La release suivante active est `0.2.10 — OrbitFlare Yellowstone gRPC`.
|
||||||
|
|
||||||
|
Le prompt impose un `pre.001` audit/sizing avant code provider et cible notamment les questions ouvertes suivantes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
IP whitelist vs X_TOKEN vs control-plane X-ORBIT-KEY
|
||||||
|
endpoints régionaux / Devnet / transport security
|
||||||
|
support réel des unary/replay/from_slot
|
||||||
|
heartbeat proactif recommandé par OrbitFlare
|
||||||
|
réutilisation N1/N2 sans second actor/client
|
||||||
|
Config V3 et secret ownership
|
||||||
|
smoke architecture-safe
|
||||||
|
```
|
||||||
|
|
||||||
|
`0.2.11` reste Helius LaserStream gRPC.
|
||||||
|
|
||||||
|
## Fichiers ajoutés ou modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/rules/RULES_DOCUMENTATION.md
|
||||||
|
scripts/audit_markdown_tables.py
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
docs/formats/KSPWALLET_V1.md
|
||||||
|
docs/formats/KSPWALLET_V2.md
|
||||||
|
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
|
||||||
|
docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
|
||||||
|
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
|
||||||
|
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
|
||||||
|
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
prompts/000-README.md
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
deltas/0.2.9/pre.012.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gate local disponible dans l'environnement de préparation
|
||||||
|
|
||||||
|
Exécuté avant livraison de l'archive :
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
Markdown table audit: clean (87 table(s), 116 file(s))
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo et Rustfmt ne sont pas disponibles dans l'environnement de préparation ; aucune commande Cargo n'est donc déclarée réussie pour la candidate `pre.012`.
|
||||||
|
|
||||||
|
## Gate opérateur requis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-config-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
cargo test --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke live séparé :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Si tous les gates locaux sont verts et que le smoke Mainnet est consigné, `0.2.9` peut passer à sa tranche de publication stable. Un défaut découvert dans cette candidate crée `pre.012-fix.001` ; l'historique précédent reste immuable.
|
||||||
182
deltas/0.2.9/pre.013-fix.001.md
Normal file
182
deltas/0.2.9/pre.013-fix.001.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.013-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.013-fix.001` — smoke PublicNode streaming + Testnet
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.9-pre.013
|
||||||
|
Cargo : 0.2.9-pre.13
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate déterministe opérateur de `pre.013` est vert jusqu'au smoke live :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
Rust workspace audit clean / 0 export candidate
|
||||||
|
Markdown table audit clean, 87 tableaux / 254 fichiers
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport release completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
workspace dependency canary 3/3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke initial atteint le service PublicNode Mainnet mais `GetVersion` retourne `PERMISSION_DENIED`.
|
||||||
|
|
||||||
|
## 2. Correction du diagnostic
|
||||||
|
|
||||||
|
Cette réponse ne suffit pas à conclure qu'un `x-token` PublicNode est requis.
|
||||||
|
|
||||||
|
La surface Yellowstone upstream permet à un opérateur de désactiver les méthodes unary indépendamment du streaming `Subscribe`. PublicNode expose son service comme gateway Yellowstone gRPC public et aucune procédure d'authentification n'est retenue comme contrat KSP tant qu'elle n'est pas établie par une source provider suffisamment forte.
|
||||||
|
|
||||||
|
Conséquence :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun x-token inventé
|
||||||
|
aucun secret PublicNode ajouté à Config
|
||||||
|
aucune variable d'environnement d'authentification ajoutée
|
||||||
|
smoke recentré sur Subscribe, fonctionnalité centrale de 0.2.9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Endpoint Testnet acquis
|
||||||
|
|
||||||
|
L'opérateur a confirmé le host/port Yellowstone gRPC Testnet :
|
||||||
|
|
||||||
|
```text
|
||||||
|
solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP utilise l'URI TLS :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Config V3
|
||||||
|
|
||||||
|
`config/std.transport.json` conserve le profil Mainnet PublicNode sans metadata d'authentification et ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_testnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = testnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
metadata = none
|
||||||
|
```
|
||||||
|
|
||||||
|
Comme pour le profil Mainnet, les axes HTTP/WS du profil restent des transports Solana standards distincts du provider gRPC.
|
||||||
|
|
||||||
|
Le canari Config existant couvre désormais les deux profils PublicNode et impose explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata gRPC vide
|
||||||
|
provider/cluster/URL exacts
|
||||||
|
validation Transport verte
|
||||||
|
Debug sans URL endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Smoke Transport pur
|
||||||
|
|
||||||
|
Le smoke opt-in ne teste plus les méthodes unary provider-dépendantes `GetVersion` / `GetSlot`.
|
||||||
|
|
||||||
|
Il ouvre maintenant la surface standard réellement visée par la release :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TLS
|
||||||
|
Subscribe bidirectionnel
|
||||||
|
filtre slots standard
|
||||||
|
réception d'un YellowstoneSubscribeUpdate::Slot
|
||||||
|
slot > 0
|
||||||
|
close borné
|
||||||
|
```
|
||||||
|
|
||||||
|
Deux cas live sont présents :
|
||||||
|
|
||||||
|
```text
|
||||||
|
PublicNode Mainnet Yellowstone Subscribe sans metadata d'authentification
|
||||||
|
PublicNode Testnet Yellowstone Subscribe sans metadata d'authentification
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test reste dans `ksp-onchain-transport-lib`, construit ses settings programmatiquement et ne dépend ni de Config ni d'un secret opérateur.
|
||||||
|
|
||||||
|
## 6. Signal de version
|
||||||
|
|
||||||
|
Le correctif modifie une config runtime et un test Rust :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.13.fix.1
|
||||||
|
commit attendu = v0.2.9-pre.013-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Gate opérateur
|
||||||
|
|
||||||
|
### 7.1 Déterministe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-config-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendus minima :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config unit 113
|
||||||
|
Config public API 15
|
||||||
|
Config ownership 5
|
||||||
|
Transport unit 383
|
||||||
|
Transport public API 49
|
||||||
|
Transport release completeness 43
|
||||||
|
Transport doctests 4
|
||||||
|
workspace dependencies 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Smoke live
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun token ou secret ne doit être nécessaire pour ce smoke. Si `Subscribe` retourne à son tour `PERMISSION_DENIED`, l'hypothèse d'une authentification provider redevient ouverte et doit alors être établie avant tout nouveau correctif Config.
|
||||||
|
|
||||||
|
### 7.3 Graphes et workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Frontières
|
||||||
|
|
||||||
|
Ce fix reste strictement dans le couloir technique/live de `pre.013` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun README/USAGE modifié
|
||||||
|
aucun plan/validation modifié
|
||||||
|
aucun CHANGELOG/ROADMAP modifié
|
||||||
|
aucun prompt modifié
|
||||||
|
aucun provider-specific engine ajouté
|
||||||
|
aucun secret committé
|
||||||
|
```
|
||||||
|
|
||||||
|
La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire, uniquement après fermeture verte de ce fix.
|
||||||
176
deltas/0.2.9/pre.013-fix.002.md
Normal file
176
deltas/0.2.9/pre.013-fix.002.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.013-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.013-fix.002` — authentification PublicNode Yellowstone
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.9-pre.013-fix.001
|
||||||
|
Cargo : 0.2.9-pre.13.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le second smoke opérateur invalide l'hypothèse retenue dans `fix.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet SubscribeOpen -> PERMISSION_DENIED
|
||||||
|
Testnet SubscribeOpen -> PERMISSION_DENIED
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux endpoints sont donc atteignables au niveau TLS/gRPC mais refusent aussi la surface `Subscribe` sans autorisation.
|
||||||
|
|
||||||
|
## 2. Diagnostic retenu
|
||||||
|
|
||||||
|
Les éléments concordants sont désormais suffisants pour traiter PublicNode Yellowstone comme une surface à personal token :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = publicnode
|
||||||
|
auth wire = metadata ASCII x-token
|
||||||
|
secret = oui
|
||||||
|
URL = sans credential
|
||||||
|
```
|
||||||
|
|
||||||
|
La page PublicNode expose les endpoints Yellowstone Mainnet/Testnet. Une capture récente de la page PublicNode expose en outre un lien `Get token` vers le flow Allnodes `https://www.allnodes.com/publicnode`. Des implémentations Yellowstone récentes visant explicitement PublicNode rapportent le même comportement `PERMISSION_DENIED` sans personal token et utilisent `x-token`.
|
||||||
|
|
||||||
|
Le provisioning effectif du token reste un gate externe/opérateur : KSP ne génère, ne devine et ne versionne aucun credential provider.
|
||||||
|
|
||||||
|
## 3. Config V3
|
||||||
|
|
||||||
|
Les profils :
|
||||||
|
|
||||||
|
```text
|
||||||
|
publicnode_mainnet
|
||||||
|
publicnode_testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
conservent leurs URLs TLS sans secret et ajoutent chacun :
|
||||||
|
|
||||||
|
```json
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
L'absence de `KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN` rend donc volontairement le profil PublicNode explicite non résolvable au lieu d'envoyer silencieusement une requête anonyme vouée à `PERMISSION_DENIED`.
|
||||||
|
|
||||||
|
`.env.example` inventorie la variable sans valeur réelle.
|
||||||
|
|
||||||
|
## 4. Canari Config
|
||||||
|
|
||||||
|
Le canari des profils PublicNode fournit un canary secret via `ConfigEnvironment`, vérifie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet/Testnet exacts
|
||||||
|
metadata count = 1
|
||||||
|
metadata key = x-token
|
||||||
|
metadata class = secret
|
||||||
|
Debug = sans canary
|
||||||
|
Transport validation = PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les règles génériques V3 de provenance `secret_metadata` restent inchangées.
|
||||||
|
|
||||||
|
## 5. Smoke Transport pur
|
||||||
|
|
||||||
|
Le smoke reste dans `ksp-onchain-transport-lib` et ne dépend pas de Config.
|
||||||
|
|
||||||
|
Pour ne pas violer la frontière `Transport -X-> process environment` et pour ne pas exposer le token dans les arguments/process list, le harness lit une seule ligne secrète depuis son `stdin`. Le même personal token est réutilisé par les deux tests Mainnet/Testnet via un `OnceLock`, puis injecté avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcMetadataEntry::secret("x-token", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le token n'est jamais loggé et un canari runtime vérifie qu'il n'apparaît pas dans `Debug`.
|
||||||
|
|
||||||
|
Le smoke continue de valider :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TLS
|
||||||
|
Subscribe bidirectionnel
|
||||||
|
filtre slots
|
||||||
|
première update Slot
|
||||||
|
slot > 0
|
||||||
|
close borné
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Signal de version
|
||||||
|
|
||||||
|
Le correctif modifie Config runtime et un test Rust :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.13.fix.2
|
||||||
|
commit attendu = v0.2.9-pre.013-fix.002
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Gate opérateur
|
||||||
|
|
||||||
|
### 7.1 Déterministe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-config-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Provisioning externe
|
||||||
|
|
||||||
|
Obtenir le personal token PublicNode via le flow Allnodes/PublicNode :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://www.allnodes.com/publicnode
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne jamais le committer, le coller dans l'URL ou le passer comme argument de processus.
|
||||||
|
|
||||||
|
### 7.3 Smoke live
|
||||||
|
|
||||||
|
Le token est saisi dans une variable shell non exportée, puis envoyé au harness via stdin :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'PublicNode Yellowstone x-token: ' PUBLICNODE_TOKEN
|
||||||
|
echo
|
||||||
|
printf '%s\n' "$PUBLICNODE_TOKEN" | cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture --test-threads=1
|
||||||
|
unset PUBLICNODE_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu après provisioning valide :
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Si le provisioning provider est inaccessible ou si le token reste refusé, le smoke reste `EXTERNAL BLOCK`; ne pas transformer ce blocage en faux PASS.
|
||||||
|
|
||||||
|
### 7.4 Graphes et workspace
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Frontières
|
||||||
|
|
||||||
|
Ce fix reste strictement dans le couloir technique/live de `pre.013` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun README/USAGE modifié
|
||||||
|
aucun plan/validation modifié
|
||||||
|
aucun CHANGELOG/ROADMAP modifié
|
||||||
|
aucun prompt modifié
|
||||||
|
aucun provider-specific engine ajouté
|
||||||
|
aucun credential committé
|
||||||
|
```
|
||||||
|
|
||||||
|
La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire uniquement après fermeture du gate technique, ou après qualification explicite d'un blocage provider externe.
|
||||||
116
deltas/0.2.9/pre.013-fix.003.md
Normal file
116
deltas/0.2.9/pre.013-fix.003.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.013-fix.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.013-fix.003` — credentials PublicNode par réseau + fermeture live bornée
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.013-fix.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche reste strictement dans le couloir technique/live de fin de release. Elle ne modifie ni README/USAGE, ni plan/validation, ni CHANGELOG/ROADMAP, ni le prompt de la release suivante.
|
||||||
|
|
||||||
|
## 2. Motif opérateur
|
||||||
|
|
||||||
|
Deux exécutions live de `yellowstone_publicnode_smoke` avec deux personal tokens PublicNode différents ont montré que l'hypothèse de `fix.002` « un token commun Mainnet/Testnet » n'est pas suffisamment générale :
|
||||||
|
|
||||||
|
- avec le premier token fourni au smoke, `SubscribeOpen` a retourné `PERMISSION_DENIED` sur Mainnet et Testnet ;
|
||||||
|
- avec le second token, Mainnet a encore retourné `PERMISSION_DENIED` ;
|
||||||
|
- avec ce même second token, Testnet a franchi `SubscribeOpen`, reçu le slot attendu, puis `close()` a retourné le timeout borné de cinq secondes parce que le provider n'a pas terminé son half-close avant la deadline KSP.
|
||||||
|
|
||||||
|
Ces observations ne suffisent pas à affirmer que PublicNode documente des tokens obligatoirement liés à un réseau. Elles imposent en revanche que KSP puisse fournir des credentials distincts à Mainnet et Testnet et que le smoke ne suppose plus leur interchangeabilité.
|
||||||
|
|
||||||
|
## 3. Version technique
|
||||||
|
|
||||||
|
`workspace.package.version` devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.13.fix.3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Config V3 — secrets PublicNode séparés
|
||||||
|
|
||||||
|
Le placeholder partagé est supprimé au profit de deux variables indépendantes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
|
||||||
|
KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Le profil `publicnode_mainnet` consomme uniquement la première ; le profil `publicnode_testnet` consomme uniquement la seconde. Les deux restent injectées comme metadata gRPC secrète `x-token`, jamais dans l'URL.
|
||||||
|
|
||||||
|
`.env.example` inventorie les deux noms sans valeur réelle. Le test Config construit chaque profil avec uniquement son propre canary d'environnement afin de vérifier cette séparation.
|
||||||
|
|
||||||
|
## 5. Smoke PublicNode
|
||||||
|
|
||||||
|
Le smoke Transport reste programmatique et indépendant de Config/env KSP. Son stdin contient maintenant exactement deux lignes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ligne 1 = token Mainnet
|
||||||
|
ligne 2 = token Testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux valeurs sont lues une seule fois dans un `OnceLock` et sont affectées explicitement au réseau correspondant. L'ordre d'exécution des deux tests n'influence donc pas l'association token/réseau.
|
||||||
|
|
||||||
|
Après ouverture de `Subscribe` et réception d'un `YellowstoneSubscribeUpdate::Slot` avec `slot > 0`, la fermeture live accepte deux résultats :
|
||||||
|
|
||||||
|
1. fermeture gracieuse `Ok(())` du provider ;
|
||||||
|
2. `ERROR_CODE_TIMEOUT` produit par la deadline KSP de half-close.
|
||||||
|
|
||||||
|
Le second résultat ne masque pas un échec fonctionnel : il n'est accepté qu'après réception prouvée d'un slot et un `timeout` externe de sept secondes vérifie que `session.close()` reste lui-même borné. Toute autre erreur de fermeture reste un échec du smoke.
|
||||||
|
|
||||||
|
Le runtime Yellowstone n'est pas modifié par cette adaptation provider/live ; ses contrats déterministes de fermeture restent inchangés.
|
||||||
|
|
||||||
|
## 6. Gate opérateur ciblé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-config-lib
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis, avec les deux personal tokens réellement générés :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
|
||||||
|
echo
|
||||||
|
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
|
||||||
|
echo
|
||||||
|
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
|
||||||
|
| cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
|
||||||
|
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis les graphes et le gate workspace de fermeture technique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
.env.example
|
||||||
|
Cargo.toml
|
||||||
|
config/std.transport.json
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
deltas/0.2.9/pre.013-fix.003.md
|
||||||
|
```
|
||||||
136
deltas/0.2.9/pre.013-fix.004.md
Normal file
136
deltas/0.2.9/pre.013-fix.004.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.013-fix.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.013-fix.004` — conformité Clippy du smoke PublicNode final
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.013-fix.003
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche reste strictement dans le couloir technique/live de fin de release. Elle ne modifie ni Config, ni README/USAGE, ni plan/validation, ni CHANGELOG/ROADMAP, ni le prompt de la release suivante.
|
||||||
|
|
||||||
|
## 2. Motif opérateur
|
||||||
|
|
||||||
|
La validation opérateur de `pre.013-fix.003` confirme le comportement live final du smoke PublicNode :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet Subscribe + slot PASS
|
||||||
|
Testnet Subscribe + slot PASS
|
||||||
|
2 passed / 0 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle confirme aussi :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean / 0 export candidate / clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo test -p ksp-config-lib 113 unit + 5 ownership + 15 public API PASS
|
||||||
|
cargo test -p ksp-onchain-transport-lib 383 unit + 49 public API + 43 completeness + 4 doctests PASS
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies 3/3 PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
cargo tree -p ksp-onchain-transport-lib exécuté
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates exécuté
|
||||||
|
cargo tree --duplicates exécuté
|
||||||
|
```
|
||||||
|
|
||||||
|
Le seul gate en échec est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
Clippy relève onze violations localisées exclusivement dans `tests/yellowstone_publicnode_smoke.rs` : huit `clippy::expect_used`, deux `clippy::question_mark_used` et un `clippy::implicit_return`.
|
||||||
|
|
||||||
|
Aucun défaut fonctionnel du moteur Yellowstone, de Config ou du live PublicNode n'est révélé par ce gate.
|
||||||
|
|
||||||
|
## 3. Version technique
|
||||||
|
|
||||||
|
`workspace.package.version` devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.13.fix.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Correction du smoke
|
||||||
|
|
||||||
|
Le smoke conserve exactement le même contrat fonctionnel et opérateur que `fix.003` :
|
||||||
|
|
||||||
|
- deux lignes de token sur stdin, Mainnet puis Testnet ;
|
||||||
|
- metadata secrète `x-token` ;
|
||||||
|
- ouverture TLS puis `Subscribe` ;
|
||||||
|
- attente bornée d'un `YellowstoneSubscribeUpdate::Slot` ;
|
||||||
|
- `slot > 0` obligatoire ;
|
||||||
|
- fermeture gracieuse ou timeout KSP de half-close accepté uniquement après réception du slot ;
|
||||||
|
- aucune lecture Config/env KSP depuis Transport ;
|
||||||
|
- aucun secret dans URL, arguments ou diagnostics.
|
||||||
|
|
||||||
|
Seule la forme Rust du harness est corrigée :
|
||||||
|
|
||||||
|
- tous les `expect()` des helpers non annotés `#[test]` sont remplacés par des `match` explicites ;
|
||||||
|
- les deux propagations `?` sont remplacées par des branches `Result::Err` explicites ;
|
||||||
|
- la sortie du `async` d'attente de slot utilise `return update.slot()` ;
|
||||||
|
- la borne externe de fermeture est contrôlée par `match` plutôt que `expect()` ;
|
||||||
|
- aucun `#[allow(clippy::expect_used)]`, `#[allow(clippy::question_mark_used)]` ou `#[allow(clippy::implicit_return)]` n'est ajouté.
|
||||||
|
|
||||||
|
Le runtime Transport n'est pas modifié.
|
||||||
|
|
||||||
|
## 5. Inspection finale du graphe déjà exécutée
|
||||||
|
|
||||||
|
Le graphe Transport communiqué par l'opérateur confirme la stack gRPC attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic 0.14.6
|
||||||
|
tonic-prost 0.14.6
|
||||||
|
prost 0.14.4
|
||||||
|
prost-types 0.14.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `yellowstone-grpc-client` n'est introduit comme seconde abstraction runtime. Les doublons signalés par Cargo concernent notamment les contextes build/runtime et des transitifs génériques ; le graphe communiqué ne montre pas de seconde version concurrente de `tonic` ou `prost` dans la stack Yellowstone Transport.
|
||||||
|
|
||||||
|
## 6. Gate opérateur ciblé à rejouer
|
||||||
|
|
||||||
|
Le correctif ne changeant que le harness live et la version Cargo, le gate minimal est :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis le smoke live final, avec les credentials déjà validés :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
|
||||||
|
echo
|
||||||
|
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
|
||||||
|
echo
|
||||||
|
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
|
||||||
|
| cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
|
||||||
|
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois `cargo tree` n'ont pas besoin d'être rejoués si le payload appliqué correspond exactement à ce delta : aucune dépendance ni feature Cargo n'est modifiée.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
deltas/0.2.9/pre.013-fix.004.md
|
||||||
|
```
|
||||||
183
deltas/0.2.9/pre.013.md
Normal file
183
deltas/0.2.9/pre.013.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Delta `0.2.9-pre.013` — gate technique/live final
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.9-pre.012-fix.001
|
||||||
|
Cargo : 0.2.9-pre.12
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identité de la tranche
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.9-pre.013
|
||||||
|
Cargo : 0.2.9-pre.13
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette prerelease applique le couloir technique/live introduit par `pre.012-fix.001`. Elle ne porte aucune réconciliation README/USAGE/plan/validation et aucune finalisation CHANGELOG/ROADMAP/prompt.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Fermer les dernières preuves techniques externes de `0.2.9` avant la réconciliation documentaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
smoke PublicNode Mainnet Yellowstone gRPC live
|
||||||
|
inspection du graphe Transport direct
|
||||||
|
inspection des dépendances dupliquées Transport
|
||||||
|
inspection des dépendances dupliquées workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke existe déjà dans la base sous :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Il reste opt-in et ne contient aucun secret. `pre.013` ne crée pas un second smoke et ne déplace pas ce test vers Config.
|
||||||
|
|
||||||
|
## Vérification externe de l'endpoint
|
||||||
|
|
||||||
|
Le 2026-08-24, la surface officielle PublicNode expose toujours Solana Mainnet Yellowstone GRPC avec le host/port :
|
||||||
|
|
||||||
|
```text
|
||||||
|
solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP conserve l'URI TLS :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun endpoint Testnet n'est ajouté ou déduit dans cette tranche.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
workspace.package.version : 0.2.9-pre.12 -> 0.2.9-pre.13
|
||||||
|
|
||||||
|
deltas/0.2.9/pre.013.md
|
||||||
|
ajout du gate technique/live et des commandes opérateur
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun code runtime, test, schema, config, README, USAGE, plan, validation, CHANGELOG, ROADMAP ou prompt n'est modifié.
|
||||||
|
|
||||||
|
## Baseline déterministe héritée de `pre.012`
|
||||||
|
|
||||||
|
Le log opérateur reçu avant l'ouverture de cette tranche confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
Rust workspace audit clean / 0 export candidate
|
||||||
|
Markdown table audit clean
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
Config unit 113/113
|
||||||
|
Config public API 15/15
|
||||||
|
Config ownership 5/5
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport release completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
workspace dependency canary 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test PublicNode était alors uniquement compilé et listé comme `ignored`; cela ne vaut pas exécution live.
|
||||||
|
|
||||||
|
## Gate opérateur `pre.013`
|
||||||
|
|
||||||
|
### 1. Gate déterministe après changement de version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Seuils de non-régression :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 383 minimum
|
||||||
|
Transport public API 49 minimum
|
||||||
|
Transport release completeness 43 minimum
|
||||||
|
Transport doctests 4 minimum
|
||||||
|
workspace dependencies 3 minimum
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Smoke PublicNode Mainnet live
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Le verdict attendu est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test doit démontrer sans credentials :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TLS vers PublicNode Mainnet
|
||||||
|
GetVersion réussi
|
||||||
|
GetSlot Confirmed réussi
|
||||||
|
slot > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Un échec de compilation, mapping, TLS, protocole ou appel unary imputable à KSP ouvre `pre.013-fix.NNN`.
|
||||||
|
|
||||||
|
Un échec démontré comme incident externe PublicNode/réseau est documenté comme `EXTERNAL BLOCK` et n'est jamais maquillé en PASS.
|
||||||
|
|
||||||
|
### 3. Graphes Cargo finaux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Points à vérifier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pas de yellowstone-grpc-client runtime
|
||||||
|
pas de dépendance Transport -> Config
|
||||||
|
pas de tracing direct Transport
|
||||||
|
Tonic KSP-owned conservé
|
||||||
|
yellowstone-grpc-proto présent comme proto officiel
|
||||||
|
aucune nouvelle duplication introduite par pre.013
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Workspace complet
|
||||||
|
|
||||||
|
Après le smoke et l'inspection des graphes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critère de fermeture
|
||||||
|
|
||||||
|
`pre.013` est fermée seulement lorsque :
|
||||||
|
|
||||||
|
```text
|
||||||
|
gate déterministe vert sans warning
|
||||||
|
smoke PublicNode Mainnet réellement exécuté et vert, ou incident externe précisément établi
|
||||||
|
graphes Cargo inspectés
|
||||||
|
workspace complet vert
|
||||||
|
aucun correctif documentaire de clôture mélangé dans cette tranche
|
||||||
|
```
|
||||||
|
|
||||||
|
La tranche suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.014 — réconciliation documentaire finale
|
||||||
|
```
|
||||||
140
deltas/0.2.9/pre.014.md
Normal file
140
deltas/0.2.9/pre.014.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.014.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.014` — réconciliation documentaire finale Yellowstone
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.013-fix.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche est exclusivement le couloir de réconciliation documentaire défini par `VER-LIFECYCLE-006`. Elle ne modifie aucun code, test, schema, config exécutable, README global de publication, `CHANGELOG.md`, `ROADMAP.md` ou prompt suivant.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
Comme toute prerelease non-fix, la version Cargo est synchronisée mécaniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.14
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune sémantique runtime ne change.
|
||||||
|
|
||||||
|
## 3. Preuve technique réconciliée
|
||||||
|
|
||||||
|
Le couloir `pre.013` est fermé par `pre.013-fix.004` avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
Rust workspace audit PASS, 0 export candidate
|
||||||
|
Markdown audit PASS, 87 tableaux / 258 fichiers
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
Transport unit 383/383 PASS
|
||||||
|
Transport public API 49/49 PASS
|
||||||
|
Transport release completeness 43/43 PASS
|
||||||
|
Transport doctests 4/4 PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
PublicNode Mainnet Subscribe + Slot PASS
|
||||||
|
PublicNode Testnet Subscribe + Slot PASS
|
||||||
|
live smoke total 2/2 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo finaux ont été inspectés dans le couloir technique. `fix.004` n'ayant modifié ni dépendance ni feature, leur verdict reste applicable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic / tonic-prost 0.14.6
|
||||||
|
prost / prost-types 0.14.4
|
||||||
|
yellowstone-grpc-client absent du runtime KSP
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. PublicNode final
|
||||||
|
|
||||||
|
Endpoints validés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mainnet https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
Testnet https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
Les essais live ont montré qu'une connexion sans credential peut atteindre le service mais recevoir `PERMISSION_DENIED`. Les profils finaux utilisent donc une metadata gRPC secrète `x-token`.
|
||||||
|
|
||||||
|
Config conserve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
|
||||||
|
KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
L'opérateur a prouvé qu'un même personal token peut être utilisé pour les deux endpoints. Les variables restent distinctes par flexibilité opérationnelle ; KSP ne transforme pas ce choix en affirmation provider de tokens network-scoped.
|
||||||
|
|
||||||
|
Le smoke Transport pur reste indépendant de Config : ses deux tokens sont fournis sur stdin au harness et ne sont jamais versionnés, placés dans l'URL ou journalisés.
|
||||||
|
|
||||||
|
## 5. Smoke final documenté
|
||||||
|
|
||||||
|
Le smoke PublicNode ne teste plus `GetVersion/GetSlot` comme preuve provider, car ces unary peuvent être restreintes indépendamment du service de streaming. La preuve représentative de `0.2.9` est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TLS
|
||||||
|
-> metadata secret x-token
|
||||||
|
-> Subscribe
|
||||||
|
-> filtre slots
|
||||||
|
-> YellowstoneSubscribeUpdate::Slot
|
||||||
|
-> slot > 0
|
||||||
|
-> fermeture bornée
|
||||||
|
```
|
||||||
|
|
||||||
|
Mainnet et Testnet sont tous deux passés en live. Un timeout KSP de half-close n'est accepté par le harness qu'après réception prouvée du slot ; le runtime conserve son erreur explicite de timeout.
|
||||||
|
|
||||||
|
## 6. Documents réconciliés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
docs/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Les anciennes affirmations devenues fausses sont supprimées : PublicNode sans credential, smoke unary Mainnet, Testnet non confirmé/non versionné et gate `pre.013` encore pending.
|
||||||
|
|
||||||
|
## 7. Frontière avec `pre.015`
|
||||||
|
|
||||||
|
Cette tranche ne touche pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces trois surfaces appartiennent exclusivement à `pre.015`, hors `Cargo.toml` et son delta mécanique.
|
||||||
|
|
||||||
|
Si un défaut de code, smoke, Config ou autre document durable est découvert après le gate de `pre.014`, il ne doit pas être glissé dans `pre.015` : une nouvelle prerelease du couloir approprié doit être ouverte conformément aux règles de fermeture.
|
||||||
|
|
||||||
|
## 8. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
deltas/0.2.9/pre.014.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Gate opérateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun smoke live ni `cargo tree` n'est à rejouer dans `pre.014` si le delta est appliqué exactement : aucune surface technique ou dépendance n'est modifiée.
|
||||||
104
deltas/0.2.9/pre.015.md
Normal file
104
deltas/0.2.9/pre.015.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<!-- file: deltas/0.2.9/pre.015.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-pre.015` — préparation de publication minimale
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.014
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche est exclusivement le couloir de préparation de publication défini par les règles de fermeture KSP. Elle ne corrige aucun README, USAGE, plan, validation, règle, code, test, schema ou config.
|
||||||
|
|
||||||
|
## 2. Gate `pre.014` reçu
|
||||||
|
|
||||||
|
Le gate opérateur de `0.2.9-pre.014` reçu le 2026-08-24 confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
Rust workspace audit PASS, 0 export candidate
|
||||||
|
Markdown audit PASS, 87 tableaux / 259 fichiers
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.014` n'ayant modifié que la version Cargo et la documentation, le smoke PublicNode 2/2 et les graphes Cargo fermés dans `pre.013` n'ont pas été rejoués.
|
||||||
|
|
||||||
|
## 3. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.15
|
||||||
|
```
|
||||||
|
|
||||||
|
La modification est mécanique ; aucune sémantique runtime ne change.
|
||||||
|
|
||||||
|
## 4. `CHANGELOG.md`
|
||||||
|
|
||||||
|
Le changelog stable est préparé pour `0.2.9 — Yellowstone gRPC standard/provider-neutral` avec les faits consolidés : moteur Yellowstone partagé, surface Solana standard, Config V3, PublicNode Mainnet/Testnet avec `x-token`, smoke live 2/2, non-régressions et graphes finaux.
|
||||||
|
|
||||||
|
## 5. `ROADMAP.md`
|
||||||
|
|
||||||
|
`0.2.9` passe à l'état réalisé/validé dans la roadmap globale. La séquence suivante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
0.2.12 off-chain price transport
|
||||||
|
0.2.13 Price Desk + intégration prix Wallet Desk
|
||||||
|
0.2.14 interface/wire foundation
|
||||||
|
0.2.15 program-api foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Prompt `0.2.10`
|
||||||
|
|
||||||
|
Le draft `prompts/015-V0_2_10_START_PROMPT.md` est finalisé. Il intègre l'état réel de `v0.2.9` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
PublicNode Mainnet + Testnet validés
|
||||||
|
metadata x-token secrète via Config
|
||||||
|
Subscribe -> Slot 2/2 PASS
|
||||||
|
unary standard != entitlement garanti chez chaque provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Sa prévision souple est également corrigée pour réserver explicitement les trois couloirs de fermeture applicables :
|
||||||
|
|
||||||
|
```text
|
||||||
|
gate technique/live final éventuel
|
||||||
|
réconciliation documentaire finale
|
||||||
|
préparation de publication minimale prompt + CHANGELOG + ROADMAP
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt rappelle qu'un `fix` reste local à son couloir et que `rel.001` ne sert jamais de rattrapage.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
deltas/0.2.9/pre.015.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun autre fichier ne doit changer dans cette tranche.
|
||||||
|
|
||||||
|
## 8. Gate opérateur
|
||||||
|
|
||||||
|
Comme la tranche ne modifie aucun code/runtime/config/schema :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun smoke live ni `cargo tree` n'est à rejouer si le delta est appliqué exactement.
|
||||||
|
|
||||||
|
## 9. Suite
|
||||||
|
|
||||||
|
Si ce gate est vert, `0.2.9-rel.001` est la seule étape restante. Elle est limitée à la mécanique de publication stable : version Cargo `0.2.9`, delta `rel.001`, commit final et tag `v0.2.9`, sans rattrapage technique ou documentaire.
|
||||||
181
deltas/0.2.9/rel.001.md
Normal file
181
deltas/0.2.9/rel.001.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<!-- file: deltas/0.2.9/rel.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.9-rel.001` — publication stable Yellowstone gRPC standard/provider-neutral
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Base directe attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.015
|
||||||
|
workspace.package.version = 0.2.9-pre.15
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu pour cette livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Tag stable attendu après validation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Responsabilité de `rel.001`
|
||||||
|
|
||||||
|
Cette livraison effectue uniquement la mécanique de publication stable définie par `VER-LIFECYCLE-012`.
|
||||||
|
|
||||||
|
Elle :
|
||||||
|
|
||||||
|
- passe `workspace.package.version` de `0.2.9-pre.15` à `0.2.9` ;
|
||||||
|
- ajoute le présent delta `rel.001` ;
|
||||||
|
- ne corrige ni code, ni test, ni config/schema, ni README/USAGE, ni plan/validation, ni règle, ni prompt, ni `CHANGELOG.md`, ni `ROADMAP.md`.
|
||||||
|
|
||||||
|
Tout défaut appartenant à un couloir antérieur doit renvoyer vers une prerelease appropriée ; `rel.001` n'est jamais une tranche de rattrapage.
|
||||||
|
|
||||||
|
## 3. Gate final de `pre.015` acquis
|
||||||
|
|
||||||
|
Le gate opérateur reçu le **25 août 2026** sur `0.2.9-pre.15` est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||||
|
General Rust rule audit clean
|
||||||
|
Rust export completeness audit 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit clean
|
||||||
|
python3 scripts/audit_markdown_tables.py PASS, 87 tableaux / 260 fichiers
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS sans warning
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les suites Transport restent notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
unit 383 passed
|
||||||
|
public API 49 passed
|
||||||
|
release completeness 43 passed
|
||||||
|
doctests 4 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate technique/live final avait déjà été fermé dans le couloir `pre.013` avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
PublicNode Mainnet Yellowstone Subscribe -> Slot PASS
|
||||||
|
PublicNode Testnet Yellowstone Subscribe -> Slot PASS
|
||||||
|
smoke live total 2/2 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo finaux avaient également été inspectés dans ce couloir. `pre.014` et `pre.015` n'ont ensuite modifié aucune dépendance ni source runtime.
|
||||||
|
|
||||||
|
## 4. Version stable publiée
|
||||||
|
|
||||||
|
La version Cargo devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9
|
||||||
|
```
|
||||||
|
|
||||||
|
La release stable publiée est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 — Yellowstone gRPC standard/provider-neutral
|
||||||
|
```
|
||||||
|
|
||||||
|
Les acquis fonctionnels et documentaires ont été finalisés avant `rel.001`, notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
moteur Yellowstone gRPC partagé et provider-neutral
|
||||||
|
surface Solana Yellowstone standard typée
|
||||||
|
Config Transport V3
|
||||||
|
PublicNode Mainnet + Testnet
|
||||||
|
metadata secrète x-token
|
||||||
|
Subscribe -> Slot live 2/2 PASS
|
||||||
|
reconnect/replay borné avec observabilité conservative
|
||||||
|
pas de promesse exactly-once / lossless / gapless globale
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.9/rel.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Validations exécutées pour préparer le delta
|
||||||
|
|
||||||
|
Contrôles statiques de l'overlay :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml version 0.2.9
|
||||||
|
payload rel.001 2 fichiers exactement
|
||||||
|
aucun fichier de rattrapage présent
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune compilation supplémentaire n'est exécutée par la préparation d'archive elle-même ; la preuve complète de `pre.015` est acquise ci-dessus et le gate stable final reste à exécuter après application du delta.
|
||||||
|
|
||||||
|
## 9. Validation stable après application
|
||||||
|
|
||||||
|
Le changement étant limité au signal de version Cargo et au présent delta, exécuter avant commit/tag :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Les smokes PublicNode et les `cargo tree` n'ont pas à être rejoués si l'overlay appliqué est exactement `0.2.9-rel.001`, car aucune dépendance, Config, source ou logique de smoke ne change.
|
||||||
|
|
||||||
|
## 10. Commit et tag stable
|
||||||
|
|
||||||
|
Après succès du gate stable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commit : v0.2.9-rel.001
|
||||||
|
tag : v0.2.9
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag intermédiaire `rel.001` n'est requis.
|
||||||
|
|
||||||
|
Commandes Git typiques :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Cargo.toml deltas/0.2.9/rel.001.md
|
||||||
|
git commit -m "v0.2.9-rel.001"
|
||||||
|
git tag v0.2.9
|
||||||
|
git push
|
||||||
|
git push origin v0.2.9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Suite
|
||||||
|
|
||||||
|
Après publication du tag stable `v0.2.9`, ouvrir :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.001 — OrbitFlare Yellowstone gRPC
|
||||||
|
```
|
||||||
|
|
||||||
|
avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
La première tranche `0.2.10-pre.001` reste un audit/brainstorming/sizing avant toute implémentation lourde et sa prévision souple réserve explicitement les couloirs technique/live, documentaire et publication.
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/formats/KSPWALLET_V1.md -->
|
<!-- file: docs/formats/KSPWALLET_V1.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 15 -->
|
||||||
|
|
||||||
# `.kspwallet` V1 — spécification du format natif Wallet KSP
|
# `.kspwallet` V1 — spécification du format natif Wallet KSP
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/formats/KSPWALLET_V2.md -->
|
<!-- file: docs/formats/KSPWALLET_V2.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# `.kspwallet` V2 — spécification du wire binaire natif KSP
|
# `.kspwallet` V2 — spécification du wire binaire natif KSP
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/000-README.md -->
|
<!-- file: docs/plans/000-README.md -->
|
||||||
<!-- version: 59 -->
|
<!-- version: 60 -->
|
||||||
|
|
||||||
# Plans KSP
|
# Plans KSP
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
|||||||
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015`–`pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`.
|
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015`–`pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`.
|
||||||
- [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusqu’à `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve l’inventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`.
|
- [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusqu’à `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve l’inventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`.
|
||||||
- [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.8 — Helius LaserStream WebSocket`, ouvert par `pre.001`, fermé techniquement/documentairement par `pre.011` puis publié par `rel.001`; il conserve la surface finale Helius `7 standard + transaction`, heartbeat/Config/secrets, l’historique des fixes heartbeat, la stratégie live architecture-safe, les graphes Cargo finaux et la préparation du prompt `0.2.9`.
|
- [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.8 — Helius LaserStream WebSocket`, ouvert par `pre.001`, fermé techniquement/documentairement par `pre.011` puis publié par `rel.001`; il conserve la surface finale Helius `7 standard + transaction`, heartbeat/Config/secrets, l’historique des fixes heartbeat, la stratégie live architecture-safe, les graphes Cargo finaux et la préparation du prompt `0.2.9`.
|
||||||
|
- [`016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md`](016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md) — plan actif de `0.2.9 — Yellowstone gRPC standard/provider-neutral`, ouvert par `pre.001`; il fixe la stratégie `yellowstone-grpc-proto + Tonic KSP`, la surface `Subscribe + 7 unary`, l’exclusion de `SubscribeDeshred`, Config V3, les règles replay/backpressure et les smokes gratuits PublicNode puis OrbitFlare.
|
||||||
|
|
||||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 85 -->
|
<!-- version: 88 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# Séquence des releases fonctionnelles KSP
|
||||||
|
|
||||||
@@ -363,11 +363,13 @@ Par défaut :
|
|||||||
0.2.6 Wallet Desk
|
0.2.6 Wallet Desk
|
||||||
0.2.7 standard Solana WebSocket
|
0.2.7 standard Solana WebSocket
|
||||||
0.2.8 Helius LaserStream WebSocket
|
0.2.8 Helius LaserStream WebSocket
|
||||||
0.2.9 Yellowstone gRPC standard foundation
|
0.2.9 Yellowstone gRPC engine + Solana standard + PublicNode
|
||||||
0.2.10 off-chain price transport
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
0.2.11 price visualization desk + intégration prix dans Wallet Desk
|
0.2.11 Helius LaserStream gRPC
|
||||||
0.2.12 interface/wire foundation
|
0.2.12 off-chain price transport
|
||||||
0.2.13 program-api foundation
|
0.2.13 price visualization desk + intégration prix dans Wallet Desk
|
||||||
|
0.2.14 interface/wire foundation
|
||||||
|
0.2.15 program-api foundation
|
||||||
```
|
```
|
||||||
|
|
||||||
`0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1`–`0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2`–`0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées.
|
`0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1`–`0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2`–`0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées.
|
||||||
@@ -471,29 +473,57 @@ Le réaudit final Helius du 23 août 2026 retient `helius_laserstream` comme pro
|
|||||||
|
|
||||||
`pre.010` ferme README/USAGE, la stratégie smoke live architecture-safe et les graphes Cargo finaux ; `pre.011` ferme la candidate documentaire et prépare [`../../prompts/014-V0_2_9_START_PROMPT.md`](../../prompts/014-V0_2_9_START_PROMPT.md). `0.2.8-rel.001` publie ensuite cette surface stable sans nouvelle capacité runtime. Aucun SDK Helius, aucune dépendance gRPC et aucun replay historique WebSocket ne sont introduits.
|
`pre.010` ferme README/USAGE, la stratégie smoke live architecture-safe et les graphes Cargo finaux ; `pre.011` ferme la candidate documentaire et prépare [`../../prompts/014-V0_2_9_START_PROMPT.md`](../../prompts/014-V0_2_9_START_PROMPT.md). `0.2.8-rel.001` publie ensuite cette surface stable sans nouvelle capacité runtime. Aucun SDK Helius, aucune dépendance gRPC et aucun replay historique WebSocket ne sont introduits.
|
||||||
|
|
||||||
### `0.2.9` — Yellowstone gRPC standard
|
### `0.2.9` — Yellowstone gRPC engine + standard Solana + PublicNode
|
||||||
|
|
||||||
Mission : introduire un backend Yellowstone standard/provider-neutral.
|
Mission : construire un moteur client Yellowstone gRPC partagé dans `ksp-onchain-transport-lib`, exposer la façade Solana Yellowstone standard puis valider une première intégration concrète PublicNode Mainnet/Testnet sans recopier le moteur.
|
||||||
|
|
||||||
Le `pre.001` est un gate de sizing : inventorier toute la surface normative cible et scinder la release avant implémentation si sa clôture dans une session paraît incertaine.
|
Le `pre.001` est un gate de sizing : chaque prerelease vise 15–20 minutes de travail effectif et la release complète doit rester clôturable dans une seule session. Si cette clôture devient incertaine, `0.2.9` est scindée avant implémentation lourde supplémentaire.
|
||||||
|
|
||||||
Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices.
|
La séparation cible reprend le modèle WebSocket : moteur physique partagé -> protocole standard -> adaptations provider. PublicNode est la première intégration provider. Le moteur n'est jamais dupliqué ; le standard n'est réutilisé que pour les capacités réellement compatibles. Une couche provider peut restreindre le standard ou porter des extensions wire/lifecycle explicites.
|
||||||
|
|
||||||
### `0.2.10` / `0.2.11` — Off-chain price + app
|
### `0.2.10`–`0.2.11` — providers Yellowstone retenus
|
||||||
|
|
||||||
`0.2.10` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
|
```text
|
||||||
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
```
|
||||||
|
|
||||||
`0.2.11` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé.
|
Chaque release réutilise le moteur de `0.2.9`, audite son delta avec Yellowstone upstream et matérialise uniquement les restrictions/extensions réelles du provider.
|
||||||
|
|
||||||
|
### TODO/IDEAS — providers Yellowstone en attente
|
||||||
|
|
||||||
|
Aucune release n'est réservée pour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TODO eRPC
|
||||||
|
TODO Triton
|
||||||
|
TODO Alchemy
|
||||||
|
TODO QuickNode
|
||||||
|
TODO Chainstack
|
||||||
|
IDEAS Tatum
|
||||||
|
IDEAS Shyft
|
||||||
|
IDEAS Solinfra
|
||||||
|
IDEAS NodeFlare
|
||||||
|
IDEAS autres providers à réauditer
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces providers ne déplacent pas la séquence active et ne reçoivent ni façade, ni Config profile, ni smoke tant qu'une décision explicite d'implémentation n'est pas prise.
|
||||||
|
|
||||||
|
### `0.2.12` / `0.2.13` — Off-chain price + app
|
||||||
|
|
||||||
|
`0.2.12` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
|
||||||
|
|
||||||
|
`0.2.13` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé.
|
||||||
|
|
||||||
Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel.
|
Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel.
|
||||||
|
|
||||||
### `0.2.12` — Interface foundation
|
### `0.2.14` — Interface foundation
|
||||||
|
|
||||||
`ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes.
|
`ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes.
|
||||||
|
|
||||||
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant.
|
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant.
|
||||||
|
|
||||||
### `0.2.13` — Program API foundation
|
### `0.2.15` — Program API foundation
|
||||||
|
|
||||||
Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program.
|
Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# Plan `0.1.3` — Configuration foundation
|
# Plan `0.1.3` — Configuration foundation
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
|
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
|
||||||
<!-- version: 28 -->
|
<!-- version: 29 -->
|
||||||
|
|
||||||
# Plan `0.1.4` — `ksp-app-config-desk`
|
# Plan `0.1.4` — `ksp-app-config-desk`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
|
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
|
||||||
<!-- version: 12 -->
|
<!-- version: 13 -->
|
||||||
|
|
||||||
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
|
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
|
||||||
|
|
||||||
@@ -230,30 +230,30 @@ Colonnes : nom, catégorie, paramètres/config à préserver, forme de résultat
|
|||||||
|
|
||||||
| Méthode | Catégorie | Params / config | Résultat | Statut | Runtime | Bot3 | Release | Tests minimaux | Source |
|
| Méthode | Catégorie | Params / config | Résultat | Statut | Runtime | Bot3 | Release | Tests minimaux | Source |
|
||||||
|-------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------|----------|---------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------|
|
|-------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------|----------|---------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------|
|
||||||
| `getAccountInfo` | Accounts | pubkey ; config? {commitment, encoding, dataSlice, minContextSlot} | RpcResponse<Account\|null> | Stable | Supporté | typed_adapter | `0.2.2` | params/encoding/dataSlice ; account/null ; RPC error | [Solana](https://solana.com/docs/rpc/http/getaccountinfo) |
|
| `getAccountInfo` | Accounts | pubkey ; config? {commitment, encoding, dataSlice, minContextSlot} | RpcResponse<Account / null> | Stable | Supporté | typed_adapter | `0.2.2` | params/encoding/dataSlice ; account/null ; RPC error | [Solana](https://solana.com/docs/rpc/http/getaccountinfo) |
|
||||||
| `getBalance` | Accounts | pubkey ; config? {commitment, minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.1` | params/config ; value u64 ; context/error | [Solana](https://solana.com/docs/rpc/http/getbalance) |
|
| `getBalance` | Accounts | pubkey ; config? {commitment, minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.1` | params/config ; value u64 ; context/error | [Solana](https://solana.com/docs/rpc/http/getbalance) |
|
||||||
| `getLargestAccounts` | Accounts | config? {commitment, filter, sortResults} | RpcResponse<[LargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config/filter/sort ; array | [Solana](https://solana.com/docs/rpc/http/getlargestaccounts) |
|
| `getLargestAccounts` | Accounts | config? {commitment, filter, sortResults} | RpcResponse<[LargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config/filter/sort ; array | [Solana](https://solana.com/docs/rpc/http/getlargestaccounts) |
|
||||||
| `getMinimumBalanceForRentExemption` | Accounts | data_len ; config? {commitment} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | data length/config ; u64 | [Solana](https://solana.com/docs/rpc/http/getminimumbalanceforrentexemption) |
|
| `getMinimumBalanceForRentExemption` | Accounts | data_len ; config? {commitment} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | data length/config ; u64 | [Solana](https://solana.com/docs/rpc/http/getminimumbalanceforrentexemption) |
|
||||||
| `getMultipleAccounts` | Accounts | pubkeys <= 100 ; config? {commitment, minContextSlot, dataSlice, encoding} | RpcResponse<[Account\|null]> | Stable | Supporté | typed_adapter | `0.2.2` | max/params ; mixed account/null ; encoding | [Solana](https://solana.com/docs/rpc/http/getmultipleaccounts) |
|
| `getMultipleAccounts` | Accounts | pubkeys <= 100 ; config? {commitment, minContextSlot, dataSlice, encoding} | RpcResponse<[Account / null]> | Stable | Supporté | typed_adapter | `0.2.2` | max/params ; mixed account/null ; encoding | [Solana](https://solana.com/docs/rpc/http/getmultipleaccounts) |
|
||||||
| `getProgramAccounts` | Accounts | program pubkey ; config? {commitment, minContextSlot, withContext, encoding, dataSlice, filters, sortResults} | [KeyedAccount] ou RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | filters/dataSlice ; withContext false/true ; errors | [Solana](https://solana.com/docs/rpc/http/getprogramaccounts) |
|
| `getProgramAccounts` | Accounts | program pubkey ; config? {commitment, minContextSlot, withContext, encoding, dataSlice, filters, sortResults} | [KeyedAccount] ou RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | filters/dataSlice ; withContext false/true ; errors | [Solana](https://solana.com/docs/rpc/http/getprogramaccounts) |
|
||||||
| `getTokenAccountBalance` | Tokens | token account ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; token amount | [Solana](https://solana.com/docs/rpc/http/gettokenaccountbalance) |
|
| `getTokenAccountBalance` | Tokens | token account ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; token amount | [Solana](https://solana.com/docs/rpc/http/gettokenaccountbalance) |
|
||||||
| `getTokenAccountsByDelegate` | Tokens | delegate ; filter {mint\|programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) |
|
| `getTokenAccountsByDelegate` | Tokens | delegate ; filter {mint / programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) |
|
||||||
| `getTokenAccountsByOwner` | Tokens | owner ; filter {mint\|programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbyowner) |
|
| `getTokenAccountsByOwner` | Tokens | owner ; filter {mint / programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbyowner) |
|
||||||
| `getTokenLargestAccounts` | Tokens | mint ; config? {commitment} | RpcResponse<[TokenLargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenlargestaccounts) |
|
| `getTokenLargestAccounts` | Tokens | mint ; config? {commitment} | RpcResponse<[TokenLargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenlargestaccounts) |
|
||||||
| `getTokenSupply` | Tokens | mint ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; TokenAmount | [Solana](https://solana.com/docs/rpc/http/gettokensupply) |
|
| `getTokenSupply` | Tokens | mint ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; TokenAmount | [Solana](https://solana.com/docs/rpc/http/gettokensupply) |
|
||||||
| `getFeeForMessage` | Transactions | message base64 ; config? {commitment,minContextSlot} | RpcResponse<u64\|null> | Stable | Supporté | typed_adapter | `0.2.3` | message/config ; fee/null ; error | [Solana](https://solana.com/docs/rpc/http/getfeeformessage) |
|
| `getFeeForMessage` | Transactions | message base64 ; config? {commitment,minContextSlot} | RpcResponse<u64 / null> | Stable | Supporté | typed_adapter | `0.2.3` | message/config ; fee/null ; error | [Solana](https://solana.com/docs/rpc/http/getfeeformessage) |
|
||||||
| `getLatestBlockhash` | Transactions | config? {commitment,minContextSlot} | RpcResponse<{blockhash,lastValidBlockHeight}> | Stable | Supporté | typed_adapter | `0.2.3` | config ; object | [Solana](https://solana.com/docs/rpc/http/getlatestblockhash) |
|
| `getLatestBlockhash` | Transactions | config? {commitment,minContextSlot} | RpcResponse<{blockhash,lastValidBlockHeight}> | Stable | Supporté | typed_adapter | `0.2.3` | config ; object | [Solana](https://solana.com/docs/rpc/http/getlatestblockhash) |
|
||||||
| `getRecentPrioritizationFees` | Transactions | pubkeys? <= 128 | [{slot,prioritizationFee}] | Stable | Supporté | typed_adapter | `0.2.3` | no params / addresses ; max ; array | [Solana](https://solana.com/docs/rpc/http/getrecentprioritizationfees) |
|
| `getRecentPrioritizationFees` | Transactions | pubkeys? <= 128 | [{slot,prioritizationFee}] | Stable | Supporté | typed_adapter | `0.2.3` | no params / addresses ; max ; array | [Solana](https://solana.com/docs/rpc/http/getrecentprioritizationfees) |
|
||||||
| `getSignaturesForAddress` | Transactions | address ; config? {commitment,minContextSlot,limit,before,until} | [SignatureInfo] | Stable | Supporté | typed_adapter | `0.2.3` | pagination/config ; nullable memo/blockTime/status | [Solana](https://solana.com/docs/rpc/http/getsignaturesforaddress) |
|
| `getSignaturesForAddress` | Transactions | address ; config? {commitment,minContextSlot,limit,before,until} | [SignatureInfo] | Stable | Supporté | typed_adapter | `0.2.3` | pagination/config ; nullable memo/blockTime/status | [Solana](https://solana.com/docs/rpc/http/getsignaturesforaddress) |
|
||||||
| `getSignatureStatuses` | Transactions | signatures <= 256 ; config? {searchTransactionHistory} | RpcResponse<[SignatureStatus\|null]> | Stable | Supporté | typed_adapter | `0.2.3` | max ; cache/history ; object/null | [Solana](https://solana.com/docs/rpc/http/getsignaturestatuses) |
|
| `getSignatureStatuses` | Transactions | signatures <= 256 ; config? {searchTransactionHistory} | RpcResponse<[SignatureStatus / null]> | Stable | Supporté | typed_adapter | `0.2.3` | max ; cache/history ; object/null | [Solana](https://solana.com/docs/rpc/http/getsignaturestatuses) |
|
||||||
| `getTransaction` | Transactions | signature ; config? {commitment,maxSupportedTransactionVersion,encoding} ; legacy bare encoding deprecated | TransactionResponse\|null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.3` | config + legacy warning ; all encodings ; null ; error | [Solana](https://solana.com/docs/rpc/http/gettransaction) |
|
| `getTransaction` | Transactions | signature ; config? {commitment,maxSupportedTransactionVersion,encoding} ; legacy bare encoding deprecated | TransactionResponse / null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.3` | config + legacy warning ; all encodings ; null ; error | [Solana](https://solana.com/docs/rpc/http/gettransaction) |
|
||||||
| `getTransactionCount` | Transactions | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.3` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/gettransactioncount) |
|
| `getTransactionCount` | Transactions | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.3` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/gettransactioncount) |
|
||||||
| `isBlockhashValid` | Transactions | blockhash ; config? {commitment,minContextSlot} | RpcResponse<bool> | Stable | Supporté | typed_adapter | `0.2.3` | hash/config ; bool | [Solana](https://solana.com/docs/rpc/http/isblockhashvalid) |
|
| `isBlockhashValid` | Transactions | blockhash ; config? {commitment,minContextSlot} | RpcResponse<bool> | Stable | Supporté | typed_adapter | `0.2.3` | hash/config ; bool | [Solana](https://solana.com/docs/rpc/http/isblockhashvalid) |
|
||||||
| `requestAirdrop` | Transactions | pubkey ; lamports ; config? {commitment,recentBlockhash} | signature | Stable | Supporté | typed_adapter | `0.2.3` | serialization ; signature/error ; no-resend policy | [Solana](https://solana.com/docs/rpc/http/requestairdrop) |
|
| `requestAirdrop` | Transactions | pubkey ; lamports ; config? {commitment,recentBlockhash} | signature | Stable | Supporté | typed_adapter | `0.2.3` | serialization ; signature/error ; no-resend policy | [Solana](https://solana.com/docs/rpc/http/requestairdrop) |
|
||||||
| `sendTransaction` | Transactions | signed transaction ; config? {encoding,skipPreflight,preflightCommitment,maxRetries,minContextSlot} | signature | Stable | Supporté | typed_adapter | `0.2.3` | all configs ; RPC error ; timeout/no-resend | [Solana](https://solana.com/docs/rpc/http/sendtransaction) |
|
| `sendTransaction` | Transactions | signed transaction ; config? {encoding,skipPreflight,preflightCommitment,maxRetries,minContextSlot} | signature | Stable | Supporté | typed_adapter | `0.2.3` | all configs ; RPC error ; timeout/no-resend | [Solana](https://solana.com/docs/rpc/http/sendtransaction) |
|
||||||
| `simulateTransaction` | Transactions | transaction ; config? {commitment,encoding,replaceRecentBlockhash,sigVerify,minContextSlot,innerInstructions,accounts} | RpcResponse<SimulationResult> | Stable | Supporté | typed_adapter | `0.2.3` | configs compatibles/incompatibles ; logs/accounts/null/error | [Solana](https://solana.com/docs/rpc/http/simulatetransaction) |
|
| `simulateTransaction` | Transactions | transaction ; config? {commitment,encoding,replaceRecentBlockhash,sigVerify,minContextSlot,innerInstructions,accounts} | RpcResponse<SimulationResult> | Stable | Supporté | typed_adapter | `0.2.3` | configs compatibles/incompatibles ; logs/accounts/null/error | [Solana](https://solana.com/docs/rpc/http/simulatetransaction) |
|
||||||
| `getBlock` | Blocks | slot ; config? {commitment,encoding,transactionDetails,maxSupportedTransactionVersion,rewards} ; legacy bare encoding deprecated | BlockResponse\|null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.4` | config + legacy warning ; transactionDetails variants ; null | [Solana](https://solana.com/docs/rpc/http/getblock) |
|
| `getBlock` | Blocks | slot ; config? {commitment,encoding,transactionDetails,maxSupportedTransactionVersion,rewards} ; legacy bare encoding deprecated | BlockResponse / null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.4` | config + legacy warning ; transactionDetails variants ; null | [Solana](https://solana.com/docs/rpc/http/getblock) |
|
||||||
| `getBlockCommitment` | Blocks | slot | {commitment:[u64]\|null,totalStake:u64} | Stable | Supporté | typed_adapter | `0.2.4` | slot ; commitment null/array | [Solana](https://solana.com/docs/rpc/http/getblockcommitment) |
|
| `getBlockCommitment` | Blocks | slot | {commitment:[u64] / null,totalStake:u64} | Stable | Supporté | typed_adapter | `0.2.4` | slot ; commitment null/array | [Solana](https://solana.com/docs/rpc/http/getblockcommitment) |
|
||||||
| `getBlockHeight` | Blocks | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.4` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getblockheight) |
|
| `getBlockHeight` | Blocks | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.4` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getblockheight) |
|
||||||
| `getBlockProduction` | Blocks | config? {commitment,identity,range} | RpcResponse<BlockProduction> | Stable | Supporté | typed_adapter | `0.2.4` | identity/range/config ; map | [Solana](https://solana.com/docs/rpc/http/getblockproduction) |
|
| `getBlockProduction` | Blocks | config? {commitment,identity,range} | RpcResponse<BlockProduction> | Stable | Supporté | typed_adapter | `0.2.4` | identity/range/config ; map | [Solana](https://solana.com/docs/rpc/http/getblockproduction) |
|
||||||
| `getBlocks` | Blocks | startSlot ; endSlot? ou config? ; config? {commitment,minContextSlot} | [u64] | Stable | Supporté | typed_adapter | `0.2.4` | overload 1/2/3 params ; max range 500k ; empty | [Solana](https://solana.com/docs/rpc/http/getblocks) |
|
| `getBlocks` | Blocks | startSlot ; endSlot? ou config? ; config? {commitment,minContextSlot} | [u64] | Stable | Supporté | typed_adapter | `0.2.4` | overload 1/2/3 params ; max range 500k ; empty | [Solana](https://solana.com/docs/rpc/http/getblocks) |
|
||||||
@@ -267,19 +267,19 @@ Colonnes : nom, catégorie, paramètres/config à préserver, forme de résultat
|
|||||||
| `getEpochSchedule` | Cluster | aucun | EpochSchedule | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getepochschedule) |
|
| `getEpochSchedule` | Cluster | aucun | EpochSchedule | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getepochschedule) |
|
||||||
| `getGenesisHash` | Cluster | aucun | string base58 | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; hash string ; error | [Solana](https://solana.com/docs/rpc/http/getgenesishash) |
|
| `getGenesisHash` | Cluster | aucun | string base58 | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; hash string ; error | [Solana](https://solana.com/docs/rpc/http/getgenesishash) |
|
||||||
| `getHealth` | Cluster | aucun | "ok" ou RPC unhealthy error | Stable | Supporté | typed_adapter | `0.2.1` | healthy ; unhealthy RPC error | [Solana](https://solana.com/docs/rpc/http/gethealth) |
|
| `getHealth` | Cluster | aucun | "ok" ou RPC unhealthy error | Stable | Supporté | typed_adapter | `0.2.1` | healthy ; unhealthy RPC error | [Solana](https://solana.com/docs/rpc/http/gethealth) |
|
||||||
| `getHighestSnapshotSlot` | Cluster | aucun | {full:u64,incremental:u64\|null} | Stable | Supporté | typed_adapter | `0.2.2` | incremental null ; no snapshot/error | [Solana](https://solana.com/docs/rpc/http/gethighestsnapshotslot) |
|
| `getHighestSnapshotSlot` | Cluster | aucun | {full:u64,incremental:u64 / null} | Stable | Supporté | typed_adapter | `0.2.2` | incremental null ; no snapshot/error | [Solana](https://solana.com/docs/rpc/http/gethighestsnapshotslot) |
|
||||||
| `getIdentity` | Cluster | aucun | {identity:string} | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; identity | [Solana](https://solana.com/docs/rpc/http/getidentity) |
|
| `getIdentity` | Cluster | aucun | {identity:string} | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; identity | [Solana](https://solana.com/docs/rpc/http/getidentity) |
|
||||||
| `getLeaderSchedule` | Cluster | slot? \| config? \| null ; config? {commitment,identity} | map identity->[slot_index] \| null | Stable | Supporté | typed_adapter | `0.2.2` | all overloads ; filter ; null | [Solana](https://solana.com/docs/rpc/http/getleaderschedule) |
|
| `getLeaderSchedule` | Cluster | slot? / config? / null ; config? {commitment,identity} | map identity->[slot_index] / null | Stable | Supporté | typed_adapter | `0.2.2` | all overloads ; filter ; null | [Solana](https://solana.com/docs/rpc/http/getleaderschedule) |
|
||||||
| `getMaxRetransmitSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxretransmitslot) |
|
| `getMaxRetransmitSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxretransmitslot) |
|
||||||
| `getMaxShredInsertSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxshredinsertslot) |
|
| `getMaxShredInsertSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxshredinsertslot) |
|
||||||
| `getSlot` | Cluster | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getslot) |
|
| `getSlot` | Cluster | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getslot) |
|
||||||
| `getSlotLeader` | Cluster | config? {commitment,minContextSlot} | string pubkey | Stable | Supporté | typed_adapter | `0.2.2` | config ; pubkey | [Solana](https://solana.com/docs/rpc/http/getslotleader) |
|
| `getSlotLeader` | Cluster | config? {commitment,minContextSlot} | string pubkey | Stable | Supporté | typed_adapter | `0.2.2` | config ; pubkey | [Solana](https://solana.com/docs/rpc/http/getslotleader) |
|
||||||
| `getSlotLeaders` | Cluster | startSlot ; limit 1..5000 | [string pubkey] | Stable | Supporté | typed_adapter | `0.2.2` | limit boundaries ; array | [Solana](https://solana.com/docs/rpc/http/getslotleaders) |
|
| `getSlotLeaders` | Cluster | startSlot ; limit 1..5000 | [string pubkey] | Stable | Supporté | typed_adapter | `0.2.2` | limit boundaries ; array | [Solana](https://solana.com/docs/rpc/http/getslotleaders) |
|
||||||
| `getVersion` | Cluster | aucun | {solana-core:string,feature-set:u32\|null} | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; feature-set present/null | [Solana](https://solana.com/docs/rpc/http/getversion) |
|
| `getVersion` | Cluster | aucun | {solana-core:string,feature-set:u32 / null} | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; feature-set present/null | [Solana](https://solana.com/docs/rpc/http/getversion) |
|
||||||
| `getVoteAccounts` | Cluster | config? {commitment,votePubkey,keepUnstakedDelinquents,delinquentSlotDistance} | {current:[VoteAccount],delinquent:[VoteAccount]} | Stable | Supporté | typed_adapter | `0.2.2` | filters/config ; both sets | [Solana](https://solana.com/docs/rpc/http/getvoteaccounts) |
|
| `getVoteAccounts` | Cluster | config? {commitment,votePubkey,keepUnstakedDelinquents,delinquentSlotDistance} | {current:[VoteAccount],delinquent:[VoteAccount]} | Stable | Supporté | typed_adapter | `0.2.2` | filters/config ; both sets | [Solana](https://solana.com/docs/rpc/http/getvoteaccounts) |
|
||||||
| `getInflationGovernor` | Economics | config? {commitment} | InflationGovernor | Stable | Supporté | typed_adapter | `0.2.4` | config ; f64 fields | [Solana](https://solana.com/docs/rpc/http/getinflationgovernor) |
|
| `getInflationGovernor` | Economics | config? {commitment} | InflationGovernor | Stable | Supporté | typed_adapter | `0.2.4` | config ; f64 fields | [Solana](https://solana.com/docs/rpc/http/getinflationgovernor) |
|
||||||
| `getInflationRate` | Economics | aucun | {total,validator,foundation:f64,epoch:u64} | Stable | Supporté | typed_adapter | `0.2.4` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getinflationrate) |
|
| `getInflationRate` | Economics | aucun | {total,validator,foundation:f64,epoch:u64} | Stable | Supporté | typed_adapter | `0.2.4` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getinflationrate) |
|
||||||
| `getInflationReward` | Economics | addresses ; config? {commitment,epoch,minContextSlot} | [InflationReward\|null] | Stable | Supporté | typed_adapter | `0.2.4` | addresses/config ; reward/null ; commission null | [Solana](https://solana.com/docs/rpc/http/getinflationreward) |
|
| `getInflationReward` | Economics | addresses ; config? {commitment,epoch,minContextSlot} | [InflationReward / null] | Stable | Supporté | typed_adapter | `0.2.4` | addresses/config ; reward/null ; commission null | [Solana](https://solana.com/docs/rpc/http/getinflationreward) |
|
||||||
| `getStakeMinimumDelegation` | Economics | config? {commitment,minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.4` | config ; value | [Solana](https://solana.com/docs/rpc/http/getstakeminimumdelegation) |
|
| `getStakeMinimumDelegation` | Economics | config? {commitment,minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.4` | config ; value | [Solana](https://solana.com/docs/rpc/http/getstakeminimumdelegation) |
|
||||||
| `getSupply` | Economics | config? {commitment,excludeNonCirculatingAccountsList} | RpcResponse<Supply> | Stable | Supporté | typed_adapter | `0.2.4` | config true/false ; list semantics | [Solana](https://solana.com/docs/rpc/http/getsupply) |
|
| `getSupply` | Economics | config? {commitment,excludeNonCirculatingAccountsList} | RpcResponse<Supply> | Stable | Supporté | typed_adapter | `0.2.4` | config true/false ; list semantics | [Solana](https://solana.com/docs/rpc/http/getsupply) |
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
|
|
||||||
# Plan `0.2.5` — Wallet foundation
|
# Plan `0.2.5` — Wallet foundation
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
||||||
<!-- version: 17 -->
|
<!-- version: 18 -->
|
||||||
|
|
||||||
# Plan `0.2.7` — WebSocket Solana standard
|
# Plan `0.2.7` — WebSocket Solana standard
|
||||||
|
|
||||||
|
|||||||
808
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
Normal file
808
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
|
||||||
|
<!-- version: 25 -->
|
||||||
|
|
||||||
|
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
|
||||||
|
|
||||||
|
**Statut courant : le couloir technique `0.2.9-pre.013` est fermé par `pre.013-fix.004` : gates déterministes et workspace verts, graphes Cargo finaux inspectés, et smoke Yellowstone PublicNode authentifié exécuté avec succès sur Mainnet et Testnet (`2/2 PASS`). `pre.014` est la réconciliation documentaire finale ; seule `pre.015` restera ensuite pour le prompt `0.2.10`, `CHANGELOG.md` et `ROADMAP.md`, avant `rel.001`.**
|
||||||
|
|
||||||
|
## 1. Objet et autorité de la release
|
||||||
|
|
||||||
|
Base stable d'ouverture :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Release :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 — Yellowstone gRPC standard/provider-neutral
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate `pre.001`, ses fixes, les deltas techniques suivants et le code réel ont affiné le prompt de démarrage. L'ordre d'autorité utilisé pendant la release est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
règles normatives KSP
|
||||||
|
code et fichiers réellement livrés
|
||||||
|
nouveaux deltas immuables de 0.2.9
|
||||||
|
décisions courantes consolidées dans ce plan et la validation 012
|
||||||
|
prompt de démarrage pour les contraintes qui n'ont pas été explicitement supersédées
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deltas historiques ne sont jamais réécrits pour refléter une décision ultérieure.
|
||||||
|
|
||||||
|
## 2. Résultat final attendu
|
||||||
|
|
||||||
|
`0.2.9` doit fermer une première fondation Yellowstone gRPC exploitable sans devenir un SDK fournisseur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend gRPC distinct de HTTP et WebSocket
|
||||||
|
moteur Tonic/Protobuf privé dans ksp-onchain-transport-lib
|
||||||
|
façade Yellowstone standard provider-neutral
|
||||||
|
7 unary standard retenus
|
||||||
|
Subscribe standard avec les familles/accounts/slots/transactions/blocks retenues
|
||||||
|
9 variantes SubscribeUpdate courantes
|
||||||
|
stream bidirectionnel borné
|
||||||
|
backpressure, half-close et shutdown déterministes
|
||||||
|
reconnect KSP-owned et replay/from_slot prudent
|
||||||
|
aucune promesse exactly-once/lossless non prouvée
|
||||||
|
Config Transport V3 backward-readable V1/V2
|
||||||
|
provider et protocol distincts dans Config
|
||||||
|
première intégration PublicNode strictement standard
|
||||||
|
smoke live opt-in avec credential secret non versionné
|
||||||
|
non-régressions HTTP, WebSocket standard et Helius WebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Scope fermé par `pre.001`
|
||||||
|
|
||||||
|
### 3.1 Inclus
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 — moteur Yellowstone gRPC
|
||||||
|
N2 — standard Solana Yellowstone
|
||||||
|
N3 — première intégration PublicNode quand elle réutilise le standard sans divergence wire
|
||||||
|
TLS et metadata provider-neutral
|
||||||
|
7 unary standards retenus
|
||||||
|
Subscribe standard retenu
|
||||||
|
reconnect/replay observables mais non lossless
|
||||||
|
Config V3 si le mapping reste Config -> Transport
|
||||||
|
smokes PublicNode architecture-safe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Hors scope `0.2.9`
|
||||||
|
|
||||||
|
```text
|
||||||
|
SubscribeDeshred / pré-exécution
|
||||||
|
extensions Triton spécifiques
|
||||||
|
adapter Helius LaserStream gRPC spécifique
|
||||||
|
adapter OrbitFlare spécifique
|
||||||
|
pool/scheduler automatique complexe de sessions gRPC
|
||||||
|
serveur Geyser/plugin validator
|
||||||
|
Store/persistence/backfill historique
|
||||||
|
workers/jobs d'acquisition
|
||||||
|
replay lossless garanti
|
||||||
|
refonte HTTP ou WebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
Les intégrations provider futures ne dupliquent jamais le moteur N1. Une façade provider n'existe que si elle porte une divergence réelle : auth, capabilities, restriction, extension wire ou policy lifecycle.
|
||||||
|
|
||||||
|
## 4. Audit upstream et dépendances retenues
|
||||||
|
|
||||||
|
### 4.1 Snapshot normatif et réaudit final
|
||||||
|
|
||||||
|
Le gate d'ouverture puis le réaudit final du 2026-08-24 confirment que les numéros du plugin, du client, du proto et des releases GitHub évoluent indépendamment. Il ne faut donc pas transformer un numéro de `master` en numéro de release publié.
|
||||||
|
|
||||||
|
État observé au réaudit final :
|
||||||
|
|
||||||
|
```text
|
||||||
|
latest release GitHub indexée v14.2.2+solana.4.1.0 — 2026-07-27
|
||||||
|
master yellowstone-grpc-geyser 15.1.2
|
||||||
|
master yellowstone-grpc-client 13.3.0
|
||||||
|
master yellowstone-grpc-proto 12.6.0
|
||||||
|
master Agave Geyser / storage 4.2.0
|
||||||
|
master prost / prost-types / tonic 0.14.x
|
||||||
|
KSP yellowstone-grpc-proto direct ^12.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Le service `Geyser`, la shape de `SubscribeRequest` et les neuf variantes `SubscribeUpdate` retenues par KSP n'ont pas changé matériellement par rapport au gate `pre.001`. `SubscribeDeshred` reste explicitement hors scope `0.2.9`.
|
||||||
|
|
||||||
|
Les sources primaires restent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/releases
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/LICENSING.md
|
||||||
|
https://docs.rs/crate/yellowstone-grpc-proto/latest
|
||||||
|
https://docs.rs/crate/yellowstone-grpc-client/latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Le réaudit final a été exécuté en `pre.012`; aucun changement upstream matériel n’a remis en cause la surface retenue.
|
||||||
|
|
||||||
|
### 4.2 Licence
|
||||||
|
|
||||||
|
Le repository upstream est globalement `AGPL-3.0-only`, mais `LICENSING.md` affecte explicitement Apache-2.0 à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
examples/
|
||||||
|
yellowstone-grpc-client/
|
||||||
|
yellowstone-grpc-client-nodejs/
|
||||||
|
yellowstone-grpc-proto/
|
||||||
|
```
|
||||||
|
|
||||||
|
Décision :
|
||||||
|
|
||||||
|
```text
|
||||||
|
dépendance publiée yellowstone-grpc-proto = acceptée
|
||||||
|
.proto vendored dans KSP = non
|
||||||
|
source provenant des zones AGPL = non copiée
|
||||||
|
future copie upstream = nouveau gate provenance/licence obligatoire
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Stratégie client
|
||||||
|
|
||||||
|
| Stratégie | Décision | Raison principale |
|
||||||
|
|-------------------------------------------------------|--------------------|-----------------------------------------------------------------------|
|
||||||
|
| `yellowstone-grpc-client + yellowstone-grpc-proto` | non retenue | importerait trop de lifecycle/reconnect upstream et de surface client |
|
||||||
|
| `yellowstone-grpc-proto + client KSP autour de tonic` | **retenue** | wire officiel, moteur/lifecycle/redaction KSP-owned |
|
||||||
|
| proto/génération KSP vendored | fallback seulement | dette licence/synchronisation/build plus forte |
|
||||||
|
|
||||||
|
Matérialisation courante :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto ^12.6 runtime sans feature tonic
|
||||||
|
yellowstone-grpc-proto dev/test avec feature tonic pour GeyserServer fixture
|
||||||
|
tonic ^0.14 channel + TLS runtime ; codegen/server dev/test
|
||||||
|
tonic-prost ^0.14 ProstCodec bas niveau
|
||||||
|
http ^1.5 PathAndQuery interne
|
||||||
|
yellowstone-grpc-client absent
|
||||||
|
prost/prost-types aucune dépendance KSP directe
|
||||||
|
proto vendored absent
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo inspectés pendant `pre.002/pre.003` n'ont pas révélé de seconde génération incompatible à corriger. Le graph final est réinspecté en `pre.012`.
|
||||||
|
|
||||||
|
## 5. Matrice protocolaire fermée
|
||||||
|
|
||||||
|
### 5.1 Service `Geyser`
|
||||||
|
|
||||||
|
| RPC | Forme | Classification | Cible `0.2.9` | État |
|
||||||
|
|-----------------------|-------------|------------------------------------------------------|---------------|----------------------------|
|
||||||
|
| `Subscribe` | bidi stream | standard Yellowstone | oui | **DONE** `pre.009/pre.010` |
|
||||||
|
| `SubscribeDeshred` | bidi stream | extension/pré-exécution Triton publiée dans le proto | non | **OUT** |
|
||||||
|
| `SubscribeReplayInfo` | unary | standard | oui | **DONE** |
|
||||||
|
| `Ping` | unary | standard | oui | **DONE** |
|
||||||
|
| `GetLatestBlockhash` | unary | standard | oui | **DONE** |
|
||||||
|
| `GetBlockHeight` | unary | standard | oui | **DONE** |
|
||||||
|
| `GetSlot` | unary | standard | oui | **DONE** |
|
||||||
|
| `IsBlockhashValid` | unary | standard | oui | **DONE** |
|
||||||
|
| `GetVersion` | unary | standard | oui | **DONE** |
|
||||||
|
|
||||||
|
`SubscribeDeshred` reste explicitement exclu même s'il existe dans le proto publié : sa présence wire n'en fait pas une capacité provider-neutral de la fondation KSP.
|
||||||
|
|
||||||
|
### 5.2 `SubscribeRequest`
|
||||||
|
|
||||||
|
| Champ | Sémantique | État |
|
||||||
|
|-----------------------|----------------------------------------|-------------------------------|
|
||||||
|
| `accounts` | map nom -> filtre accounts | **DONE** |
|
||||||
|
| `slots` | map nom -> filtre slots | **DONE** |
|
||||||
|
| `transactions` | map nom -> filtre transactions | **DONE** |
|
||||||
|
| `transactions_status` | même famille de filtre transaction | **DONE** |
|
||||||
|
| `blocks` | map nom -> filtre blocks | **DONE** |
|
||||||
|
| `blocks_meta` | map nom -> filtre marqueur vide | **DONE** |
|
||||||
|
| `entry` | map nom -> filtre marqueur vide | **DONE** |
|
||||||
|
| `commitment` | optional Processed/Confirmed/Finalized | **DONE** |
|
||||||
|
| `accounts_data_slice` | repeated offset/length | **DONE** |
|
||||||
|
| `ping` | optional request ping/id | **DONE** |
|
||||||
|
| `from_slot` | optional u64 | **DONE**, sémantique prudente |
|
||||||
|
|
||||||
|
Accounts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account[]
|
||||||
|
owner[]
|
||||||
|
filters[]
|
||||||
|
nonempty_txn_signature?
|
||||||
|
cuckoo_accounts_filter?
|
||||||
|
|
||||||
|
memcmp { offset, oneof bytes | base58 | base64 }
|
||||||
|
datasize
|
||||||
|
token_account_state
|
||||||
|
lamports { oneof eq | ne | lt | gt }
|
||||||
|
```
|
||||||
|
|
||||||
|
Slots :
|
||||||
|
|
||||||
|
```text
|
||||||
|
filter_by_commitment?
|
||||||
|
interslot_updates?
|
||||||
|
statuses = processed | confirmed | finalized | first_shred_received | completed | created_bank | dead
|
||||||
|
```
|
||||||
|
|
||||||
|
Transactions et `transaction_status` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
vote?
|
||||||
|
failed?
|
||||||
|
signature?
|
||||||
|
account_include[]
|
||||||
|
account_exclude[]
|
||||||
|
account_required[]
|
||||||
|
cuckoo_account_include?
|
||||||
|
token_accounts? = ALL | BALANCE_CHANGED
|
||||||
|
```
|
||||||
|
|
||||||
|
Blocks :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account_include[]
|
||||||
|
include_transactions?
|
||||||
|
include_accounts?
|
||||||
|
include_entries?
|
||||||
|
cuckoo_account_include?
|
||||||
|
```
|
||||||
|
|
||||||
|
`blocks_meta` et `entry` conservent la distinction absence / map vide / filtre nommé vide.
|
||||||
|
|
||||||
|
Bornes KSP communes matérialisées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
filter groups nommés total <= 1024
|
||||||
|
filter name 1..128 octets, trim exact, sans contrôle
|
||||||
|
filter names uniques globalement entre les sept maps
|
||||||
|
accounts_data_slice count <= 128
|
||||||
|
accounts_data_slice length <= 64 MiB
|
||||||
|
offset + length sans overflow u64
|
||||||
|
```
|
||||||
|
|
||||||
|
Les bounds spécifiques Accounts/Transactions/Blocks sont ceux désormais testés dans leurs tranches respectives ; ils ne sont pas dupliqués comme knobs Config.
|
||||||
|
|
||||||
|
### 5.3 `SubscribeUpdate`
|
||||||
|
|
||||||
|
| Variante | Champs structurants conservés | État |
|
||||||
|
|----------------------|-----------------------------------------------------------------------------|----------|
|
||||||
|
| `account` | account info + slot + `is_startup` | **DONE** |
|
||||||
|
| `slot` | slot + parent? + status + dead_error? | **DONE** |
|
||||||
|
| `transaction` | signature/is_vote/transaction/meta/index + slot | **DONE** |
|
||||||
|
| `transaction_status` | slot/signature/is_vote/index/error | **DONE** |
|
||||||
|
| `block` | slot/hash/rewards/time/height/parent/counts + transactions/accounts/entries | **DONE** |
|
||||||
|
| `ping` | marker server ping | **DONE** |
|
||||||
|
| `pong` | id | **DONE** |
|
||||||
|
| `block_meta` | block metadata/counts sans tableaux complets | **DONE** |
|
||||||
|
| `entry` | slot/index/num_hashes/hash/transaction counts/index | **DONE** |
|
||||||
|
|
||||||
|
Le top-level conserve également `filters[]` et `created_at`. Les types Prost/Yellowstone générés restent privés.
|
||||||
|
|
||||||
|
### 5.4 Unary standards
|
||||||
|
|
||||||
|
| RPC | Request | Response KSP utile | État |
|
||||||
|
|-----------------------|---------------------------|------------------------------------------|----------|
|
||||||
|
| `SubscribeReplayInfo` | vide | `first_available?` | **DONE** |
|
||||||
|
| `Ping` | `count` | `count` | **DONE** |
|
||||||
|
| `GetLatestBlockhash` | `commitment?` | slot, blockhash, last_valid_block_height | **DONE** |
|
||||||
|
| `GetBlockHeight` | `commitment?` | block_height | **DONE** |
|
||||||
|
| `GetSlot` | `commitment?` | slot | **DONE** |
|
||||||
|
| `IsBlockhashValid` | blockhash + `commitment?` | slot + valid | **DONE** |
|
||||||
|
| `GetVersion` | vide | version bornée | **DONE** |
|
||||||
|
|
||||||
|
Ces capacités ne remplacent pas les wrappers Solana JSON-RPC HTTP.
|
||||||
|
|
||||||
|
## 6. Architecture runtime actuelle
|
||||||
|
|
||||||
|
### 6.1 Séparation des backends
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP HttpTransportSettings / pool HTTP
|
||||||
|
WebSocket engine WsSession actor partagé
|
||||||
|
Solana standard WS SolanaStandardWsSession
|
||||||
|
Helius LaserStream WS HeliusLaserStreamWsSession
|
||||||
|
|
||||||
|
Yellowstone gRPC engine YellowstoneGrpcChannel + moteur bidi KSP
|
||||||
|
Solana Yellowstone standard contrats typed KSP
|
||||||
|
provider descripteur d'exécution/capability, pas nouveau protocole
|
||||||
|
```
|
||||||
|
|
||||||
|
Interdictions :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pas de WsProtocolKind pour gRPC
|
||||||
|
pas de WsEndpointSettings pour gRPC
|
||||||
|
pas de client Tonic brut réexporté
|
||||||
|
pas de second moteur physique par provider
|
||||||
|
pas de façade provider vide qui ne ferait que renommer le standard
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Contrats publics principaux matérialisés
|
||||||
|
|
||||||
|
```text
|
||||||
|
YellowstoneGrpcEndpointUrl
|
||||||
|
YellowstoneGrpcProviderName
|
||||||
|
YellowstoneGrpcClusterName
|
||||||
|
YellowstoneGrpcMetadataEntry
|
||||||
|
YellowstoneGrpcReconnectSettings
|
||||||
|
YellowstoneGrpcSessionSettings
|
||||||
|
YellowstoneGrpcEndpointSettings
|
||||||
|
YellowstoneGrpcTransportSettings
|
||||||
|
YellowstoneGrpcChannel
|
||||||
|
|
||||||
|
YellowstoneSubscribeRequest + filtres typed
|
||||||
|
YellowstoneSubscribeUpdate + variantes typed
|
||||||
|
SolanaYellowstoneGrpcSubscribeSession
|
||||||
|
YellowstoneGrpcSubscribeSnapshot
|
||||||
|
7 unary typed
|
||||||
|
```
|
||||||
|
|
||||||
|
Le wire Tonic/Prost reste privé et n'est pas une escape hatch publique.
|
||||||
|
|
||||||
|
### 6.3 Credentials et diagnostics
|
||||||
|
|
||||||
|
Transport reçoit des valeurs déjà résolues par son consumer. Il ne connaît :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun KSP_SECRET_*
|
||||||
|
aucun KSP_PUBLIC_*
|
||||||
|
aucun std::env
|
||||||
|
aucun header commercial hardcodé dans le standard
|
||||||
|
```
|
||||||
|
|
||||||
|
Les URLs, metadata sensibles, messages/details de `tonic::Status` et payloads arbitraires ne sont pas recopiés dans `Debug`, `Display`, snapshots ou contexts KSP.
|
||||||
|
|
||||||
|
## 7. Lifecycle, backpressure et continuité
|
||||||
|
|
||||||
|
### 7.1 Stream bidi
|
||||||
|
|
||||||
|
Acquis depuis `pre.009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
une request mpsc bornée consommée par Tonic
|
||||||
|
une update queue bornée côté KSP
|
||||||
|
mutation du SubscribeRequest sur le même stream
|
||||||
|
Ping serveur -> réponse automatique appropriée
|
||||||
|
Pong décodé
|
||||||
|
server half-close observable
|
||||||
|
client explicit close borné
|
||||||
|
Drop best-effort sans panic
|
||||||
|
oversized inbound/outbound borné
|
||||||
|
slow receiver overflow terminal et observable
|
||||||
|
shutdown déterministe
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune queue non bornée et aucun drop silencieux n'est présenté comme lossless.
|
||||||
|
|
||||||
|
### 7.2 Reconnect/replay
|
||||||
|
|
||||||
|
Acquis depuis `pre.010` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconnect automatique oui, borné et KSP-owned
|
||||||
|
resubscribe dernier SubscribeRequest complet accepté
|
||||||
|
from_slot de reprise max(from_slot explicite, dernier slot observé) quand applicable
|
||||||
|
SubscribeReplayInfo informatif
|
||||||
|
first_available clamp/prouve un gap seulement s'il dépasse le slot demandé
|
||||||
|
exactly-once non garanti
|
||||||
|
lossless non garanti
|
||||||
|
ordre global sans gap non garanti
|
||||||
|
duplicate possible, compté, non supprimé silencieusement
|
||||||
|
gap compté seulement lorsqu'une preuve est disponible
|
||||||
|
shutdown during backoff interrompt la reconnexion
|
||||||
|
budget reconnect épuisé état terminal safe
|
||||||
|
```
|
||||||
|
|
||||||
|
Snapshot public safe :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconnect_count
|
||||||
|
replay_attempt_count
|
||||||
|
continuity_gap_count
|
||||||
|
duplicate_update_count
|
||||||
|
last_requested_from_slot
|
||||||
|
last_observed_slot
|
||||||
|
terminal state/error code safe
|
||||||
|
```
|
||||||
|
|
||||||
|
La présence de `from_slot` ou `SubscribeReplayInfo` n'autorise aucune promesse de replay historique complet.
|
||||||
|
|
||||||
|
## 8. Config Transport V3 — tranche `pre.011`
|
||||||
|
|
||||||
|
### 8.1 Compatibilité documentaire
|
||||||
|
|
||||||
|
Décision fermée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
V1 = HTTP-only, backward-readable
|
||||||
|
V2 = HTTP + WebSocket, backward-readable
|
||||||
|
V3 = HTTP + WebSocket + Yellowstone gRPC optionnel par profil
|
||||||
|
```
|
||||||
|
|
||||||
|
Le schema V3 conserve des branches strictes V1/V2 au lieu de relâcher leurs `additionalProperties`.
|
||||||
|
|
||||||
|
Shape V3 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
format_version = 3
|
||||||
|
retry
|
||||||
|
ws_defaults
|
||||||
|
grpc_defaults
|
||||||
|
default_profile
|
||||||
|
profiles[] {
|
||||||
|
profile_id
|
||||||
|
endpoints[]
|
||||||
|
ws_endpoints[]
|
||||||
|
grpc_endpoints[]? # optionnel par profil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
L'absence de `grpc_endpoints` dans un profil V3 signifie `None`, pas un `YellowstoneGrpcTransportSettings` vide inventé.
|
||||||
|
|
||||||
|
### 8.2 `grpc_defaults`
|
||||||
|
|
||||||
|
Les defaults Config correspondent uniquement à de vrais settings runtime Transport :
|
||||||
|
|
||||||
|
```text
|
||||||
|
connect_timeout_ms
|
||||||
|
unary_timeout_ms
|
||||||
|
close_timeout_ms
|
||||||
|
reconnect.max_retries
|
||||||
|
reconnect.initial_backoff_ms
|
||||||
|
reconnect.max_backoff_ms
|
||||||
|
request_channel_capacity
|
||||||
|
update_channel_capacity
|
||||||
|
max_inbound_message_size_bytes
|
||||||
|
max_outbound_message_size_bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
Les bounds de filtres Subscribe restent un contrat Transport fixe et ne deviennent pas des options Config sans besoin démontré.
|
||||||
|
|
||||||
|
### 8.3 Endpoint gRPC
|
||||||
|
|
||||||
|
Chaque `grpc_endpoints[]` porte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
name
|
||||||
|
enabled
|
||||||
|
provider
|
||||||
|
cluster
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url
|
||||||
|
metadata[]? # classe publique
|
||||||
|
secret_metadata[]? # classe secrète
|
||||||
|
session? # overrides bornés
|
||||||
|
```
|
||||||
|
|
||||||
|
Axes distincts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
protocol = contrat wire standard, actuellement solana_yellowstone
|
||||||
|
provider = environnement d'exécution descriptif, par exemple publicnode
|
||||||
|
```
|
||||||
|
|
||||||
|
`provider = publicnode` ne crée donc pas un `PublicNodeGrpcProtocol` ni une façade provider sans divergence réelle.
|
||||||
|
|
||||||
|
### 8.4 Provenance des metadata
|
||||||
|
|
||||||
|
Config est propriétaire de la résolution :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata -> interdit toute provenance KSP_SECRET_*/KSPB_SECRET_*
|
||||||
|
secret_metadata -> exige au moins une provenance secret et interdit une variable non-secret
|
||||||
|
littéraux autour d'un placeholder secret -> autorisés ; safe_value masque seulement le segment secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport reçoit ensuite `YellowstoneGrpcMetadataEntry` public/secret et ne connaît jamais le nom de variable d'environnement.
|
||||||
|
|
||||||
|
### 8.5 API Config sans rupture V2
|
||||||
|
|
||||||
|
L'API existante reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
http_settings()
|
||||||
|
ws_settings()
|
||||||
|
into_transport_settings() -> (HTTP, Option<WS>)
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.011` ajoute :
|
||||||
|
|
||||||
|
```text
|
||||||
|
grpc_settings()
|
||||||
|
into_all_transport_settings() -> (HTTP, Option<WS>, Option<Yellowstone gRPC>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le tuple historique n'est pas modifié silencieusement.
|
||||||
|
|
||||||
|
## 9. PublicNode dans `0.2.9`
|
||||||
|
|
||||||
|
### 9.1 Mainnet
|
||||||
|
|
||||||
|
Endpoint retenu et validé en live :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
Le profil committé est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_mainnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = mainnet-beta
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
metadata = x-token secret via KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tentatives sans metadata ont atteint le service mais ont reçu `PERMISSION_DENIED`. Le profil final fournit donc le personal token PublicNode comme metadata gRPC secrète `x-token`; Transport ne connaît ni le nom de variable KSP ni sa provenance.
|
||||||
|
|
||||||
|
### 9.2 Testnet
|
||||||
|
|
||||||
|
L'endpoint exact confirmé par l'opérateur et validé en live est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
```
|
||||||
|
|
||||||
|
Le profil committé est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_testnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = testnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
metadata = x-token secret via KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke final prouve qu'un même personal token opérateur peut fonctionner sur Mainnet et Testnet. KSP conserve néanmoins deux variables de configuration distinctes afin de ne pas imposer une hypothèse de portée provider à long terme ; elles peuvent recevoir la même valeur. Cette séparation est une liberté de configuration KSP, pas une assertion que PublicNode délivre des tokens network-scoped.
|
||||||
|
|
||||||
|
## 10. Threat model et bornes
|
||||||
|
|
||||||
|
Menaces couvertes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
credential dans URI/metadata
|
||||||
|
Status/message/details provider arbitraires
|
||||||
|
Debug dérivé de filtres/payloads
|
||||||
|
TLS/connect error qui réémet l'URI
|
||||||
|
message inbound/outbound hostile
|
||||||
|
stream flood / slow consumer
|
||||||
|
filter explosion / collision de noms
|
||||||
|
unknown enum/oneof
|
||||||
|
server/client half-close
|
||||||
|
reconnect loop
|
||||||
|
node divergent après reconnect
|
||||||
|
duplicate/gap après replay
|
||||||
|
mutation tardive du stream
|
||||||
|
```
|
||||||
|
|
||||||
|
Réponses :
|
||||||
|
|
||||||
|
```text
|
||||||
|
wrappers redacted
|
||||||
|
allowlist de contexts KSP
|
||||||
|
validation/bounds avant I/O
|
||||||
|
queues bornées
|
||||||
|
états terminaux observables
|
||||||
|
reconnect budget borné
|
||||||
|
aucune promesse de continuité non prouvée
|
||||||
|
Config sensitivity gate avant construction de metadata secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Smoke ownership
|
||||||
|
|
||||||
|
Ordre de preuve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport programmatic -> PublicNode Mainnet Yellowstone + secret x-token fourni au harness
|
||||||
|
Transport programmatic -> PublicNode Testnet Yellowstone + secret x-token fourni au harness
|
||||||
|
Config V3 -> Transport -> PublicNode via secret_metadata, jamais un smoke réseau placé dans Config par facilité
|
||||||
|
```
|
||||||
|
|
||||||
|
Un smoke Transport pur peut vivre dans `ksp-onchain-transport-lib/tests` puisqu'il construit ses settings programmatiquement.
|
||||||
|
|
||||||
|
Un smoke cross-crates Config -> Transport ne doit pas devenir une responsabilité durable de `ksp-config-lib`. S’il n’existe pas encore de surface d’intégration appropriée, la preuve live reste un smoke Transport programmatique avec secret fourni uniquement au harness opérateur.
|
||||||
|
|
||||||
|
Aucun secret provider n'est versionné.
|
||||||
|
|
||||||
|
## 12. État des tranches et historique compact
|
||||||
|
|
||||||
|
Les preuves détaillées restent dans les fichiers `deltas/0.2.9/*.md`. Le plan ne duplique plus leurs journaux complets.
|
||||||
|
|
||||||
|
| Tranche | Objet | État consolidé |
|
||||||
|
|-------------------|----------------------------------------------------------|----------------------|
|
||||||
|
| `pre.001` + fixes | audit upstream, licence, providers, architecture, sizing | **CLOSED** |
|
||||||
|
| `pre.002` + fixes | dépendances, settings/errors, channel minimal | **CLOSED** |
|
||||||
|
| `pre.003` + fix | TLS, metadata, fixture locale, 7 unary | **CLOSED** |
|
||||||
|
| `pre.004` + fix | Subscribe foundation/common | **CLOSED** |
|
||||||
|
| `pre.005` + fix | Accounts + Slots | **CLOSED** |
|
||||||
|
| `pre.006` | namespace privé HTTP explicite | **CLOSED** |
|
||||||
|
| `pre.007` | Transactions + transaction_status | **CLOSED** |
|
||||||
|
| `pre.008` + fix | Blocks + block_meta + entry | **CLOSED** |
|
||||||
|
| `pre.009` + fix | bidi, Ping/Pong, backpressure, half-close, shutdown | **CLOSED** |
|
||||||
|
| `pre.010` + fix | reconnect, from_slot, ReplayInfo, gaps/duplicates | **CLOSED** |
|
||||||
|
| `pre.011` | Config V3 + protocol/provider + PublicNode Mainnet | **CLOSED** |
|
||||||
|
| `pre.012` | réaudit + smoke ajouté + docs préparatoires | **LOCAL GATE PASS** |
|
||||||
|
| `pre.012-fix.001` | règles de séparation de fermeture + recalibrage | **CLOSED** |
|
||||||
|
| `pre.013` + fixes | gate technique/live PublicNode + graphes finaux | **CLOSED** |
|
||||||
|
| `pre.014` | réconciliation documentaire finale | **ACTIVE CANDIDATE** |
|
||||||
|
| `pre.015` | prompt + CHANGELOG + ROADMAP | **PLANNED** |
|
||||||
|
|
||||||
|
Gate opérateur déterministe de `pre.012` reçu le 2026-08-24 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt/audit Rust PASS sans warning
|
||||||
|
Markdown tables PASS, 87 tableaux / 252 fichiers
|
||||||
|
cargo check PASS
|
||||||
|
cargo clippy PASS sans warning
|
||||||
|
Config unit 113/113
|
||||||
|
Config public API 15/15
|
||||||
|
Config ownership 5/5
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
workspace dependencies 3/3
|
||||||
|
cargo test --workspace PASS
|
||||||
|
PublicNode live smoke NOT RUN, test ignored
|
||||||
|
cargo tree final NOT PROVIDED / NOT RUN in supplied log
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.012` reste historiquement une tranche mixte créée avant l'adoption de la nouvelle règle. Son fix ne réécrit pas cette histoire : le prompt `0.2.10` déjà présent est désormais considéré comme un brouillon à finaliser seulement en `pre.015`, et les README/USAGE préparés sont réconciliés par la présente `pre.014`.
|
||||||
|
|
||||||
|
Gate technique final `pre.013-fix.004` reçu le 2026-08-24 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt/audit Rust PASS sans warning
|
||||||
|
Markdown tables PASS, 87 tableaux / 258 fichiers
|
||||||
|
cargo check PASS
|
||||||
|
cargo clippy --all-targets PASS sans warning
|
||||||
|
Transport unit 383/383
|
||||||
|
Transport public API 49/49
|
||||||
|
Transport completeness 43/43
|
||||||
|
Transport doctests 4/4
|
||||||
|
cargo test --workspace PASS
|
||||||
|
cargo graphs finaux inspectés, stack Yellowstone unique tonic/prost 0.14
|
||||||
|
PublicNode Mainnet Subscribe + slot PASS
|
||||||
|
PublicNode Testnet Subscribe + slot PASS
|
||||||
|
live smoke total 2/2 PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes inspectés avant `fix.004` restent valides : ce fix n'a modifié ni dépendance ni feature Cargo. La stack Yellowstone résolue est `yellowstone-grpc-proto 12.6.0`, `tonic/tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, sans `yellowstone-grpc-client` runtime.
|
||||||
|
|
||||||
|
## 13. Forecast restant recalibré
|
||||||
|
|
||||||
|
### `pre.013` — gate technique/live final — **CLOSED**
|
||||||
|
|
||||||
|
Le couloir technique a finalement nécessité quatre fixes : découverte de l'auth PublicNode, ajout Testnet, adaptation de la fermeture live, puis conformité Clippy du harness. Le résultat consolidé est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version final du couloir = 0.2.9-pre.13.fix.4
|
||||||
|
PublicNode Mainnet Subscribe + slot PASS
|
||||||
|
PublicNode Testnet Subscribe + slot PASS
|
||||||
|
secret wire metadata x-token
|
||||||
|
secret versionné non
|
||||||
|
harness deux lignes stdin, valeurs identiques autorisées
|
||||||
|
close provider succès ou timeout KSP borné après slot reçu
|
||||||
|
graphes Cargo inspectés
|
||||||
|
clippy --all-targets PASS sans warning
|
||||||
|
workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les essais intermédiaires `PERMISSION_DENIED` sans credential sont conservés dans les deltas `pre.013-fix.001` à `fix.003`; ils ne sont pas réinterprétés comme une régression du moteur standard.
|
||||||
|
|
||||||
|
### `pre.014` — réconciliation documentaire finale — **ACTIVE CANDIDATE**
|
||||||
|
|
||||||
|
Payload :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.14
|
||||||
|
plan 016 réconcilié avec le gate technique/live réel
|
||||||
|
validation 012 fermée avec uniquement des preuves exécutées
|
||||||
|
README/USAGE Transport relus et corrigés
|
||||||
|
références durables liées à 0.2.9 réconciliées si nécessaire
|
||||||
|
audit Markdown final des documents touchés
|
||||||
|
aucune modification fonctionnelle/runtime
|
||||||
|
aucune finalisation CHANGELOG/ROADMAP/prompt suivant
|
||||||
|
```
|
||||||
|
|
||||||
|
Si une correction de code ou de smoke apparaît ici, `pre.014` ne l'absorbe pas : une nouvelle tranche technique est ouverte, puis la réconciliation documentaire est rejouée.
|
||||||
|
|
||||||
|
### `pre.015` — préparation de publication minimale
|
||||||
|
|
||||||
|
Payload fonctionnel strict :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9-pre.15
|
||||||
|
prompt 0.2.10 OrbitFlare finalisé
|
||||||
|
CHANGELOG.md synchronisé pour 0.2.9
|
||||||
|
ROADMAP.md état global 0.2.9 / suite synchronisé
|
||||||
|
delta pre.015
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun README, USAGE, plan, validation, règle, code, test, schema ou config n'est modifié dans cette tranche. Si l'un de ces fichiers nécessite encore une correction, la tranche de publication est décalée après une nouvelle prerelease dédiée.
|
||||||
|
|
||||||
|
### `rel.001` — publication stable
|
||||||
|
|
||||||
|
`rel.001` ne sert qu'à la mécanique de publication stable : version Cargo finale `0.2.9`, delta de release, commit final et tag `v0.2.9` après gate. Aucun rattrapage technique ou documentaire n'y est mélangé.
|
||||||
|
|
||||||
|
## 14. Critères de split
|
||||||
|
|
||||||
|
Scinder avant dette silencieuse si :
|
||||||
|
|
||||||
|
1. une évolution upstream matérielle invalide le wire retenu ;
|
||||||
|
2. PublicNode exige une divergence provider-specific significative ;
|
||||||
|
3. le replay nécessite un sous-système de fork/equivocation plus large que la foundation ;
|
||||||
|
4. une tranche dépasse nettement le budget nominal sans frontière claire ;
|
||||||
|
5. le gate technique/live montre une dette dépendance/licence ou une non-régression qui ne peut pas être corrigée proprement dans son couloir ;
|
||||||
|
6. une correction appartenant à un couloir déjà franchi est découverte pendant un couloir ultérieur.
|
||||||
|
|
||||||
|
Le noyau à préserver reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
moteur Yellowstone + façade Solana standard + Config provider-neutral + première intégration PublicNode minimale
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. Gates opérateur restants
|
||||||
|
|
||||||
|
Le gate `pre.013` est fermé. Gate `pre.014` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Gate `pre.015` : audit documentaire ciblé sur `prompts/`, `CHANGELOG.md` et `ROADMAP.md`, plus les validations minimales imposées par le changement de version Cargo. Aucun smoke n'est rejoué dans cette tranche en l'absence de changement technique.
|
||||||
|
|
||||||
|
## 16. Conditions de clôture `0.2.9`
|
||||||
|
|
||||||
|
```text
|
||||||
|
service/proto courant réconcilié
|
||||||
|
SubscribeDeshred explicitement OUT
|
||||||
|
7 unary verts
|
||||||
|
Subscribe standard et 9 updates verts
|
||||||
|
backend gRPC distinct de HTTP/WS
|
||||||
|
raw Tonic/Prost privé
|
||||||
|
secrets/metadata redacted
|
||||||
|
bounds/backpressure/shutdown verts
|
||||||
|
reconnect/replay documentés sans lossless implicite
|
||||||
|
Config V3 backward V1/V2
|
||||||
|
provider/protocol distincts
|
||||||
|
PublicNode Mainnet live Subscribe + slot PASS
|
||||||
|
PublicNode Testnet live Subscribe + slot PASS
|
||||||
|
auth PublicNode x-token secret documentée sans secret versionné
|
||||||
|
HTTP 52+14 non régressé
|
||||||
|
Standard WS 18/18 non régressé
|
||||||
|
Helius WS non régressé
|
||||||
|
cargo graphs finaux inspectés
|
||||||
|
README/USAGE finaux réconciliés en pre.014
|
||||||
|
validation 012 fermée en pre.014
|
||||||
|
prompt/CHANGELOG/ROADMAP seuls finalisés en pre.015
|
||||||
|
workspace final vert
|
||||||
|
```
|
||||||
|
|
||||||
|
## 17. Séquence après `0.2.9`
|
||||||
|
|
||||||
|
La séquence active a été recalibrée par les fixes de `pre.001`; cette décision est conservée pendant le nettoyage documentaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 moteur Yellowstone + Solana standard + PublicNode
|
||||||
|
0.2.10 OrbitFlare Yellowstone gRPC
|
||||||
|
0.2.11 Helius LaserStream gRPC
|
||||||
|
0.2.12 off-chain price transport
|
||||||
|
0.2.13 Price Desk + intégration prix Wallet Desk
|
||||||
|
0.2.14 interface/wire foundation
|
||||||
|
0.2.15 program-api foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
Les intégrations suivantes restent dans le backlog non numéroté tant qu'aucune décision d'implémentation ne les fait entrer dans la séquence active :
|
||||||
|
|
||||||
|
```text
|
||||||
|
TODO eRPC
|
||||||
|
TODO Triton
|
||||||
|
TODO Alchemy
|
||||||
|
TODO QuickNode
|
||||||
|
TODO Chainstack
|
||||||
|
IDEAS Tatum
|
||||||
|
IDEAS Shyft
|
||||||
|
IDEAS Solinfra
|
||||||
|
IDEAS NodeFlare
|
||||||
|
```
|
||||||
|
|
||||||
|
OrbitFlare reste le provider dédié `0.2.10` et Helius LaserStream gRPC `0.2.11` selon la séquence recalibrée par les fixes de `pre.001`. Chaque release doit réauditer auth, capabilities, restrictions, extensions wire, replay/from_slot et lifecycle au lieu de supposer une équivalence complète avec N2.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||||
<!-- version: 17 -->
|
<!-- version: 18 -->
|
||||||
|
|
||||||
# Contrats des fichiers
|
# Contrats des fichiers
|
||||||
|
|
||||||
@@ -20,13 +20,13 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
|
|||||||
| `.cargo/config.toml` | Définir les réglages Cargo propres au workspace qui ne relèvent pas du manifeste, notamment l'emplacement des artefacts de build. | Modifier lorsqu'un réglage Cargo commun change ; ne pas y placer de secret ni de configuration spécifique à une machine particulière. |
|
| `.cargo/config.toml` | Définir les réglages Cargo propres au workspace qui ne relèvent pas du manifeste, notamment l'emplacement des artefacts de build. | Modifier lorsqu'un réglage Cargo commun change ; ne pas y placer de secret ni de configuration spécifique à une machine particulière. |
|
||||||
| `rustfmt.toml` | Définir le formatage Rust commun. | Modifier comme changement normatif, avec justification dans le delta. |
|
| `rustfmt.toml` | Définir le formatage Rust commun. | Modifier comme changement normatif, avec justification dans le delta. |
|
||||||
| `clippy.toml` | Définir les paramètres Clippy communs. | Modifier comme changement normatif, avec justification dans le delta. |
|
| `clippy.toml` | Définir les paramètres Clippy communs. | Modifier comme changement normatif, avec justification dans le delta. |
|
||||||
| `ROADMAP.md` | Décrire les objectifs globaux et les grandes étapes prévues par phase/version, avec leur état synthétique. | Modifier lorsqu'un objectif, une grande étape, un report, une annulation ou un état global change ; ne pas y recopier le détail des prereleases prévu dans les plans de version. |
|
| `ROADMAP.md` | Décrire les objectifs globaux et les grandes étapes prévues par phase/version, avec leur état synthétique. | Modifier lorsqu'un objectif, une grande étape, un report, une annulation ou un état global change ; pour une clôture de release, réserver sa synchronisation finale à la dernière prerelease de publication et ne pas y recopier le détail des prereleases prévu dans les plans de version. |
|
||||||
| `CHANGELOG.md` | Résumer les releases stables dans un ordre chronologique décroissant, sous forme d'un ou plusieurs paragraphes par release. | Synchroniser lors de la phase documentaire finale ; ne pas dupliquer les deltas ni créer de changelog par crate/module. |
|
| `CHANGELOG.md` | Résumer les releases stables dans un ordre chronologique décroissant, sous forme d'un ou plusieurs paragraphes par release. | Synchroniser uniquement dans la dernière prerelease de préparation de publication ; ne pas le finaliser dans la prerelease de réconciliation README/USAGE et ne pas dupliquer les deltas ni créer de changelog par crate/module. |
|
||||||
|
|
||||||
## Répertoire `docs/`
|
## Répertoire `docs/`
|
||||||
|
|
||||||
| Fichier/famille | Responsabilité | Règle de modification |
|
| Fichier/famille | Responsabilité | Règle de modification |
|
||||||
|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `docs/000-README.md` | Indexer et expliquer la documentation tout en restant en tête des listings et arbres de fichiers. | Modifier lorsque l'organisation durable de `docs/` change ; `000-README.md` reste prioritaire lorsqu'un ordre numérique existe. |
|
| `docs/000-README.md` | Indexer et expliquer la documentation tout en restant en tête des listings et arbres de fichiers. | Modifier lorsque l'organisation durable de `docs/` change ; `000-README.md` reste prioritaire lorsqu'un ordre numérique existe. |
|
||||||
| `docs/formats/000-README.md` | Indexer les spécifications de formats durables KSP destinées à l'interopérabilité externe. | Modifier lorsqu'un format durable entre/sort de cette famille ou que son statut change ; conserver `000-README.md` comme point d'entrée. |
|
| `docs/formats/000-README.md` | Indexer les spécifications de formats durables KSP destinées à l'interopérabilité externe. | Modifier lorsqu'un format durable entre/sort de cette famille ou que son statut change ; conserver `000-README.md` comme point d'entrée. |
|
||||||
| `docs/formats/*.md` | Spécifier un wire KSP durable indépendamment de son implémentation, avec encodages, limites, parsing, auth et vecteurs. | Modifier avec traçabilité lorsqu'un contrat de format évolue ; après publication stable d'une version de format, toute incompatibilité de wire ouvre une nouvelle version de format plutôt qu'une tolérance silencieuse. |
|
| `docs/formats/*.md` | Spécifier un wire KSP durable indépendamment de son implémentation, avec encodages, limites, parsing, auth et vecteurs. | Modifier avec traçabilité lorsqu'un contrat de format évolue ; après publication stable d'une version de format, toute incompatibilité de wire ouvre une nouvelle version de format plutôt qu'une tolérance silencieuse. |
|
||||||
@@ -35,10 +35,10 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
|
|||||||
| `docs/architecture/000-README.md` | Indexer les documents décrivant l'architecture KSP décidée ou en cours de cadrage explicite. | Modifier lorsque la structure documentaire d'architecture change. |
|
| `docs/architecture/000-README.md` | Indexer les documents décrivant l'architecture KSP décidée ou en cours de cadrage explicite. | Modifier lorsque la structure documentaire d'architecture change. |
|
||||||
| `docs/architecture/*.md` | Décrire les objectifs, frontières, responsabilités et architecture courante ou explicitement proposée. | Ne pas utiliser comme journal de livraison ; distinguer clairement les décisions validées des hypothèses encore ouvertes. |
|
| `docs/architecture/*.md` | Décrire les objectifs, frontières, responsabilités et architecture courante ou explicitement proposée. | Ne pas utiliser comme journal de livraison ; distinguer clairement les décisions validées des hypothèses encore ouvertes. |
|
||||||
| `docs/plans/000-README.md` | Indexer les plans de versions/phases. | Modifier lorsque l'organisation des plans change. |
|
| `docs/plans/000-README.md` | Indexer les plans de versions/phases. | Modifier lorsque l'organisation des plans change. |
|
||||||
| `docs/plans/*.md` | Organiser une version ou phase complexe et, pour `pre.001`, détailler la prévision souple de ses prereleases. | Faire évoluer le plan lorsque la planification change ; prévoir des tranches intermédiaires bornées et redécouper toute tranche estimée trop lourde. |
|
| `docs/plans/*.md` | Organiser une version ou phase complexe et, pour `pre.001`, détailler la prévision souple de ses prereleases. | Faire évoluer le plan lorsque la planification change ; prévoir des tranches intermédiaires bornées et les couloirs de fermeture. Sa réconciliation finale appartient à l'avant-dernière prerelease documentaire, pas à la dernière prerelease de publication. |
|
||||||
| `docs/IDEAS.md` | Conserver les idées, pistes, questions et alternatives à explorer qui ne sont pas encore des engagements du roadmap. | Ajouter une idée dès qu'elle mérite d'être conservée ; mettre à jour son statut lorsqu'elle est explorée, retenue, rejetée ou transférée. |
|
| `docs/IDEAS.md` | Conserver les idées, pistes, questions et alternatives à explorer qui ne sont pas encore des engagements du roadmap. | Ajouter une idée dès qu'elle mérite d'être conservée ; mettre à jour son statut lorsqu'elle est explorée, retenue, rejetée ou transférée. |
|
||||||
| futurs documents de référence | Définir vocabulaire, identifiants et références canoniques. | Mettre à jour quand la référence canonique évolue. |
|
| futurs documents de référence | Définir vocabulaire, identifiants et références canoniques. | Mettre à jour quand la référence canonique évolue. |
|
||||||
| futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. |
|
| futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. La validation finale est réconciliée et fermée dans l'avant-dernière prerelease documentaire, avant la dernière prerelease de publication. |
|
||||||
|
|
||||||
## Répertoire `config/`
|
## Répertoire `config/`
|
||||||
|
|
||||||
@@ -62,9 +62,9 @@ Pour un document standard profilé, `default_profile` et `profiles` sont des cl
|
|||||||
## Répertoire `prompts/`
|
## Répertoire `prompts/`
|
||||||
|
|
||||||
| Fichier/famille | Responsabilité | Règle de modification |
|
| Fichier/famille | Responsabilité | Règle de modification |
|
||||||
|-----------------------------|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `prompts/000-README.md` | Point d'entrée des prompts et de leur cycle de vie. | Modifier lorsque l'organisation pratique des prompts change ; les règles normatives restent sous `docs/rules/PROMPT_STRUCTURE.md`. |
|
| `prompts/000-README.md` | Point d'entrée des prompts et de leur cycle de vie. | Modifier lorsque l'organisation pratique des prompts change ; les règles normatives restent sous `docs/rules/PROMPT_STRUCTURE.md`. |
|
||||||
| `prompts/*START_PROMPT*.md` | Conserver un prompt de reprise versionné et réutilisable pour ouvrir une phase/version de travail. | Le créer tôt sous forme de brouillon lorsque la trajectoire devient assez claire, le mettre à jour au fil des décisions, puis le finaliser pendant la phase documentaire de clôture avant son utilisation. |
|
| `prompts/*START_PROMPT*.md` | Conserver un prompt de reprise versionné et réutilisable pour ouvrir une phase/version de travail. | Le créer tôt sous forme de brouillon lorsque la trajectoire devient assez claire et le mettre à jour au fil des décisions ; sa finalisation appartient exclusivement à la dernière prerelease de préparation de publication, avec CHANGELOG et ROADMAP. |
|
||||||
|
|
||||||
## Répertoire `deltas/`
|
## Répertoire `deltas/`
|
||||||
|
|
||||||
@@ -72,6 +72,12 @@ Pour un document standard profilé, `default_profile` et `profiles` sont des cl
|
|||||||
|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `deltas/<X.Y.Z>/<delta-name>.md` | Tracer une livraison précise, sa base, son contenu, ses suppressions, validations et questions ouvertes, regroupée sous la version cible `X.Y.Z`. | Créé avec la livraison ; une livraison déjà publiée n'est pas réécrite silencieusement. Les prereleases utilisent `pre.NNN`, leurs correctifs `pre.NNN-fix.NNN`, et les publications de release utilisent `rel.NNN`. |
|
| `deltas/<X.Y.Z>/<delta-name>.md` | Tracer une livraison précise, sa base, son contenu, ses suppressions, validations et questions ouvertes, regroupée sous la version cible `X.Y.Z`. | Créé avec la livraison ; une livraison déjà publiée n'est pas réécrite silencieusement. Les prereleases utilisent `pre.NNN`, leurs correctifs `pre.NNN-fix.NNN`, et les publications de release utilisent `rel.NNN`. |
|
||||||
|
|
||||||
|
## Séparation des fichiers pendant la fermeture
|
||||||
|
|
||||||
|
La prerelease de réconciliation documentaire possède la dernière passe sur les README/USAGE, plans, validations et références durables de la release. Après son gate, ces familles sont considérées figées pour la préparation de publication.
|
||||||
|
|
||||||
|
La dernière prerelease ne rouvre pas ces documents : son payload fonctionnel est limité au prompt suivant, `CHANGELOG.md` et `ROADMAP.md`, en plus des fichiers mécaniques de version/delta exigés par le workflow. Si un document durable doit encore être corrigé, une nouvelle prerelease documentaire est ouverte et la tranche de publication finale est décalée.
|
||||||
|
|
||||||
## Rust
|
## Rust
|
||||||
|
|
||||||
| Fichier/famille | Responsabilité | Règle de modification |
|
| Fichier/famille | Responsabilité | Règle de modification |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/PROMPT_STRUCTURE.md -->
|
<!-- file: docs/rules/PROMPT_STRUCTURE.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# Structure des prompts KSP
|
# Structure des prompts KSP
|
||||||
|
|
||||||
@@ -19,8 +19,42 @@ Sauf raison explicitement documentée :
|
|||||||
|
|
||||||
- `pre.001` = lectures obligatoires + audit interne/externe + brainstorming + sizing + planification ;
|
- `pre.001` = lectures obligatoires + audit interne/externe + brainstorming + sizing + planification ;
|
||||||
- les prereleases intermédiaires = tranches bornées de développement/validation ;
|
- les prereleases intermédiaires = tranches bornées de développement/validation ;
|
||||||
- la dernière prerelease = validation finale + documentation + nettoyage/archivage + prompt de la release suivante ;
|
- la fermeture réserve des prereleases distinctes pour le gate technique/live, la réconciliation documentaire puis la préparation de publication ;
|
||||||
- une `fix` corrige l'étape réellement livrée sans réécrire l'historique.
|
- la dernière prerelease avant `rel.NNN` est une tranche de publication minimale, distincte de la réconciliation documentaire et des smokes ;
|
||||||
|
- une `fix` corrige uniquement la responsabilité de la tranche à laquelle il est rattaché et ne sert pas à absorber un autre couloir de fermeture.
|
||||||
|
|
||||||
|
## Séparation obligatoire des dernières prereleases
|
||||||
|
|
||||||
|
La queue de fermeture d'une release est structurée de manière à empêcher qu'un correctif de dernière minute mélange tests réseau, documentation durable et préparation de publication.
|
||||||
|
|
||||||
|
Lorsqu'un smoke final ou un autre gate live est pertinent, les trois dernières responsabilités sont ordonnées ainsi :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.N-2 gate technique/live : smoke(s), graphes ou vérifications finales directement liées au runtime
|
||||||
|
pre.N-1 réconciliation documentaire : plan, validation, README, USAGE et autres références durables concernées
|
||||||
|
pre.N préparation de publication : prompt suivant + CHANGELOG + ROADMAP uniquement
|
||||||
|
rel.001 mécanique de publication stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Les numéros `N-2`, `N-1` et `N` sont relatifs : l'insertion d'une tranche ou d'une correction décale la numérotation réelle sans affaiblir cette séparation.
|
||||||
|
|
||||||
|
Si aucun smoke/gate live final n'existe, le couloir `pre.N-2` est omis ; les deux dernières prereleases restent néanmoins séparées entre réconciliation documentaire et préparation de publication.
|
||||||
|
|
||||||
|
La dernière prerelease ne modifie fonctionnellement que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompt de démarrage de la release suivante
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Les fichiers mécaniques imposés par le workflow restent autorisés : `Cargo.toml` pour la version d'une prerelease non-fix et `deltas/<X.Y.Z>/pre.NNN.md` pour sa traçabilité. Aucun README, USAGE, plan, validation, code, test, schema, config ou règle normative ne doit être introduit ou corrigé dans cette dernière tranche.
|
||||||
|
|
||||||
|
La prerelease documentaire immédiatement précédente possède la réconciliation finale des documents durables de la release : README/USAGE, plan, validation, architecture/référence concernée et cohérence documentaire globale. Elle ne finalise ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt de la release suivante.
|
||||||
|
|
||||||
|
Le gate technique/live, lorsqu'il existe, précède cette réconciliation documentaire. Les smokes et leurs corrections restent donc isolés avant que les documents finaux soient figés.
|
||||||
|
|
||||||
|
Un `fix` reste local à son couloir. Si un défaut d'une responsabilité antérieure est découvert après avoir avancé, il ne doit pas être glissé dans le `fix` de la tranche courante : une nouvelle tranche dédiée à la responsabilité concernée est ouverte, puis les couloirs de fermeture postérieurs sont rejoués si nécessaire. La livraison `rel.NNN` ne sert jamais à absorber un correctif fonctionnel ou documentaire qui aurait dû être traité en prerelease.
|
||||||
|
|
||||||
## Dimensionnement
|
## Dimensionnement
|
||||||
|
|
||||||
@@ -28,6 +62,8 @@ Lors de `pre.001`, une prerelease intermédiaire estimée à plus d'environ **15
|
|||||||
|
|
||||||
La trajectoire initiale est **souple** : elle indique un nombre prévisionnel de prereleases, leur objectif et leur ordre, mais autorise l'insertion de tranches/fixes lorsqu'un audit ou une validation révèle un besoin réel. La fermeture ne doit jamais être forcée pour respecter un numéro prévu.
|
La trajectoire initiale est **souple** : elle indique un nombre prévisionnel de prereleases, leur objectif et leur ordre, mais autorise l'insertion de tranches/fixes lorsqu'un audit ou une validation révèle un besoin réel. La fermeture ne doit jamais être forcée pour respecter un numéro prévu.
|
||||||
|
|
||||||
|
La prévision `pre.001` réserve explicitement les couloirs de fermeture applicables : gate technique/live éventuel, réconciliation documentaire, puis préparation de publication minimale. Leur numérotation peut dériver, mais leur ordre et leur séparation de responsabilités restent normatifs.
|
||||||
|
|
||||||
## Structure obligatoire d'un prompt de démarrage
|
## Structure obligatoire d'un prompt de démarrage
|
||||||
|
|
||||||
Un prompt de nouvelle session contient explicitement, dans un ordre facile à retrouver :
|
Un prompt de nouvelle session contient explicitement, dans un ordre facile à retrouver :
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_DOCUMENTATION.md -->
|
<!-- file: docs/rules/RULES_DOCUMENTATION.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Règles de documentation
|
# Règles de documentation
|
||||||
|
|
||||||
@@ -32,6 +32,15 @@ Les règles `DOC-*` s'appliquent aux documents Markdown internes et à leur orga
|
|||||||
- **DOC-IDEAS-003** — Lorsqu'une idée devient un engagement, elle est transférée vers le roadmap ou un plan ; lorsqu'elle devient une décision durable, elle est reportée dans le document normatif ou architectural approprié. `IDEAS.md` conserve une trace concise de son issue.
|
- **DOC-IDEAS-003** — Lorsqu'une idée devient un engagement, elle est transférée vers le roadmap ou un plan ; lorsqu'elle devient une décision durable, elle est reportée dans le document normatif ou architectural approprié. `IDEAS.md` conserve une trace concise de son issue.
|
||||||
- **DOC-IDEAS-004** — `IDEAS.md` ne doit pas devenir un second roadmap ni une liste de tâches de développement promises.
|
- **DOC-IDEAS-004** — `IDEAS.md` ne doit pas devenir un second roadmap ni une liste de tâches de développement promises.
|
||||||
|
|
||||||
|
## Format des tableaux Markdown
|
||||||
|
|
||||||
|
- **DOC-TABLE-001** — Un tableau Markdown KSP utilise `|` uniquement comme séparateur structurel de colonnes. Un caractère `|` littéral, y compris sous forme échappée `\|`, est interdit dans le contenu d’une cellule ; reformuler le contenu, utiliser `/`, `et`, une liste ou un bloc de code hors tableau.
|
||||||
|
- **DOC-TABLE-002** — Toutes les lignes d’un même tableau ont leurs séparateurs verticaux aux mêmes positions. La largeur de chaque colonne est déterminée par le contenu le plus large de cette colonne, en comptant l’en-tête et les lignes de données.
|
||||||
|
- **DOC-TABLE-003** — Chaque cellule de contenu commence par exactement un espace après le séparateur `|` gauche. Dans la cellule qui porte le contenu le plus large d’une colonne, il existe aussi exactement un espace entre le contenu et le séparateur `|` droit. Les cellules plus courtes conservent l’unique espace gauche et reçoivent uniquement le padding droit nécessaire pour aligner les séparateurs verticaux.
|
||||||
|
- **DOC-TABLE-004** — La ligne séparatrice en tirets occupe exactement la même largeur que chaque colonne ; les marqueurs `:` de l’alignement Markdown restent autorisés lorsqu’ils sont intentionnels. Le résultat attendu est équivalent au reformatage de tableau produit par RustRover, mais la règle structurelle KSP prime sur l’éditeur utilisé.
|
||||||
|
- **DOC-TABLE-005** — Lorsqu’une modification touche une ligne d’un tableau, le tableau entier est réaligné avant livraison. `python3 scripts/audit_markdown_tables.py <fichiers-markdown-modifiés>` est le canari mécanique recommandé pour les fichiers concernés ; les blocs de code fenced ne sont pas interprétés comme des tableaux.
|
||||||
|
- **DOC-TABLE-006** — L’immuabilité des deltas déjà publiés prime sur un reformatage rétrospectif : un ancien fichier `deltas/` n’est jamais réécrit uniquement pour satisfaire une règle de présentation introduite ultérieurement. Tout nouveau delta et tout autre tableau modifiable créé ou touché doivent en revanche respecter immédiatement `DOC-TABLE-001` à `DOC-TABLE-005`.
|
||||||
|
|
||||||
## Contenu et exactitude
|
## Contenu et exactitude
|
||||||
|
|
||||||
- **DOC-CONTENT-001** — Une documentation décrit l'état réellement décidé ou validé et distingue explicitement les hypothèses, propositions, TODO et questions ouvertes.
|
- **DOC-CONTENT-001** — Une documentation décrit l'état réellement décidé ou validé et distingue explicitement les hypothèses, propositions, TODO et questions ouvertes.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/VERSION_WORKFLOW.md -->
|
<!-- file: docs/rules/VERSION_WORKFLOW.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Versionnement, sessions et livraisons
|
# Versionnement, sessions et livraisons
|
||||||
|
|
||||||
@@ -69,6 +69,14 @@ Les règles `VER-*` définissent la progression des versions KSP, les identifian
|
|||||||
|
|
||||||
- **VER-LIFECYCLE-001** — La première prerelease d'une nouvelle phase fonctionnelle est prioritairement consacrée au brainstorming, à l'inventaire, aux risques, dépendances, hors-périmètre, critères de validation et plan de travail.
|
- **VER-LIFECYCLE-001** — La première prerelease d'une nouvelle phase fonctionnelle est prioritairement consacrée au brainstorming, à l'inventaire, aux risques, dépendances, hors-périmètre, critères de validation et plan de travail.
|
||||||
- **VER-LIFECYCLE-002** — Une phase importante ne commence pas directement par des modifications fonctionnelles dispersées sans cadrage.
|
- **VER-LIFECYCLE-002** — Une phase importante ne commence pas directement par des modifications fonctionnelles dispersées sans cadrage.
|
||||||
- **VER-LIFECYCLE-003** — La dernière prerelease d'une phase est prioritairement consacrée aux validations finales, écarts résiduels, documentation finale, synthèse changelog et prompt de reprise.
|
- **VER-LIFECYCLE-003** — La dernière prerelease avant `rel.NNN` est une tranche de préparation de publication minimale. Hors `Cargo.toml` et delta obligatoires, elle ne modifie que le prompt de démarrage de la release suivante, `CHANGELOG.md` et `ROADMAP.md`.
|
||||||
- **VER-LIFECYCLE-004** — Le document de planification établi ou révisé pendant `pre.001` d'une version détaille une prévision souple des prereleases de cette version : objectifs de chaque tranche, ordre envisagé, dépendances, validations et éventuels hors-périmètre. Cette prévision peut être réorganisée lorsque la réflexion ou le développement le justifie ; le delta trace ces changements.
|
- **VER-LIFECYCLE-004** — Le document de planification établi ou révisé pendant `pre.001` d'une version détaille une prévision souple des prereleases de cette version : objectifs de chaque tranche, ordre envisagé, dépendances, validations et éventuels hors-périmètre. Cette prévision peut être réorganisée lorsque la réflexion ou le développement le justifie ; le delta trace ces changements.
|
||||||
- **VER-LIFECYCLE-005** — Le `ROADMAP.md` n'est pas obligé de reprendre une entrée par prerelease. Il décrit la trajectoire globale ; le plan de version porte le découpage prévisionnel plus fin des prereleases.
|
- **VER-LIFECYCLE-005** — Le `ROADMAP.md` n'est pas obligé de reprendre une entrée par prerelease. Il décrit la trajectoire globale ; le plan de version porte le découpage prévisionnel plus fin des prereleases.
|
||||||
|
|
||||||
|
- **VER-LIFECYCLE-006** — La prerelease immédiatement antérieure à la dernière prerelease de publication est dédiée à la réconciliation documentaire finale : plan, validation, README, USAGE et autres documents durables concernés. Elle ne finalise pas `CHANGELOG.md`, `ROADMAP.md` ni le prompt de la release suivante.
|
||||||
|
- **VER-LIFECYCLE-007** — Lorsqu'un smoke final, un test live ou un gate réseau/provider est requis, une prerelease technique dédiée précède la prerelease de réconciliation documentaire. Cette tranche peut également porter les graphes et contrôles techniques finaux directement liés au gate, mais elle ne mélange pas la réconciliation README/USAGE ni la préparation de publication.
|
||||||
|
- **VER-LIFECYCLE-008** — Lorsqu'aucun smoke/gate live final n'est pertinent, la tranche technique dédiée peut être omise ; les deux dernières responsabilités restent obligatoirement séparées entre réconciliation documentaire puis préparation de publication.
|
||||||
|
- **VER-LIFECYCLE-009** — Un correctif `pre.NNN-fix.MMM` reste strictement dans le périmètre de responsabilité de `pre.NNN`. Un fix de smoke ne corrige pas README/USAGE/CHANGELOG/prompt ; un fix documentaire ne contient pas de nouveau smoke/runtime ; un fix de publication ne contient pas de correction documentaire durable hors `CHANGELOG.md`, `ROADMAP.md` et prompt suivant.
|
||||||
|
- **VER-LIFECYCLE-010** — Si une anomalie appartenant à un couloir antérieur est découverte après son franchissement, elle ouvre une nouvelle prerelease dédiée à cette responsabilité au lieu d'être mélangée au fix de la tranche courante. Les couloirs postérieurs sont ensuite rejoués si nécessaire afin que la dernière prerelease reste une préparation de publication minimale.
|
||||||
|
- **VER-LIFECYCLE-011** — Le plan établi en `pre.001` réserve explicitement, dans sa prévision souple, le gate technique/live éventuel, la réconciliation documentaire et la préparation de publication. La numérotation peut évoluer, mais l'ordre de ces responsabilités ne doit pas être fusionné pour raccourcir artificiellement la release.
|
||||||
|
- **VER-LIFECYCLE-012** — Une livraison `rel.NNN` effectue la mécanique de publication stable et ne sert pas de tranche de rattrapage. Tout nouveau défaut fonctionnel, test live manquant, correction README/USAGE/validation, ou préparation CHANGELOG/ROADMAP/prompt non achevée renvoie vers une prerelease appropriée avant `rel.NNN`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/000-README.md -->
|
<!-- file: docs/validation/000-README.md -->
|
||||||
<!-- version: 23 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# Validations KSP
|
# Validations KSP
|
||||||
|
|
||||||
@@ -20,3 +20,4 @@ Documents :
|
|||||||
- [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert.
|
- [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert.
|
||||||
- [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances.
|
- [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances.
|
||||||
- [`011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.8`, ouverte par `pre.001`, consolidée jusqu’à `pre.011` puis publiée par `rel.001` : Helius `account/logs/program/root/signature/slot/slotsUpdates + transaction`, `block/vote` absents, heartbeat provider, sécurité/redaction, lifecycle adversarial, non-régressions HTTP/WS, stratégie smoke live et graphes Cargo finaux.
|
- [`011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.8`, ouverte par `pre.001`, consolidée jusqu’à `pre.011` puis publiée par `rel.001` : Helius `account/logs/program/root/signature/slot/slotsUpdates + transaction`, `block/vote` absents, heartbeat provider, sécurité/redaction, lifecycle adversarial, non-régressions HTTP/WS, stratégie smoke live et graphes Cargo finaux.
|
||||||
|
- [`012-V0_2_9_YELLOWSTONE_GRPC.md`](012-V0_2_9_YELLOWSTONE_GRPC.md) — matrice active ouverte par `0.2.9-pre.001` : service Geyser courant, `Subscribe` standard, 7 unary, filtres/updates, dépendances/licence, replay/backpressure, Config V3 et interopérabilité PublicNode/OrbitFlare.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
|
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
|
||||||
<!-- version: 17 -->
|
<!-- version: 18 -->
|
||||||
|
|
||||||
# Validation `0.2.7` — WebSocket Solana standard
|
# Validation `0.2.7` — WebSocket Solana standard
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Pour une paire unstable, l'unsubscribe associé est classé `Unstable pair` dans
|
|||||||
## 2. Matrice exhaustive des 18 opérations
|
## 2. Matrice exhaustive des 18 opérations
|
||||||
|
|
||||||
| # | Méthode | Type | Statut `pre.001` | Paramètres / résultat essentiels | Notification / paire | Stratégie de test | Source officielle | Compliance |
|
| # | Méthode | Type | Statut `pre.001` | Paramètres / résultat essentiels | Notification / paire | Stratégie de test | Source officielle | Compliance |
|
||||||
|---:|---------------------------|-------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|-------------------|
|
|---:|---------------------------|-------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|----------------|
|
||||||
| 1 | `accountSubscribe` | subscribe | Stable/documented | pubkey ; config `commitment`, `encoding`, `dataSlice` ; result numeric id ; `minContextSlot` upstream actuellement ignoré, donc non promis | `accountNotification` | fixture encodings/config + subscribe/notify | `https://solana.com/docs/rpc/websocket/accountsubscribe` | Done `pre.009` |
|
| 1 | `accountSubscribe` | subscribe | Stable/documented | pubkey ; config `commitment`, `encoding`, `dataSlice` ; result numeric id ; `minContextSlot` upstream actuellement ignoré, donc non promis | `accountNotification` | fixture encodings/config + subscribe/notify | `https://solana.com/docs/rpc/websocket/accountsubscribe` | Done `pre.009` |
|
||||||
| 2 | `accountUnsubscribe` | unsubscribe | Stable/documented | remote id ; `true` or RPC error unknown id | account pair | handle local -> remote id fixture | `https://solana.com/docs/rpc/websocket/accountunsubscribe` | Done `pre.009` |
|
| 2 | `accountUnsubscribe` | unsubscribe | Stable/documented | remote id ; `true` or RPC error unknown id | account pair | handle local -> remote id fixture | `https://solana.com/docs/rpc/websocket/accountunsubscribe` | Done `pre.009` |
|
||||||
| 3 | `blockSubscribe` | subscribe | **Unstable** | `all`/mentions filter ; confirmed/finalized ; encoding ; tx details ; max tx version ; showRewards | `blockNotification` | all options + null block/error + validator capability fixture | `https://solana.com/docs/rpc/websocket/blocksubscribe` | Done `pre.011` |
|
| 3 | `blockSubscribe` | subscribe | **Unstable** | `all`/mentions filter ; confirmed/finalized ; encoding ; tx details ; max tx version ; showRewards | `blockNotification` | all options + null block/error + validator capability fixture | `https://solana.com/docs/rpc/websocket/blocksubscribe` | Done `pre.011` |
|
||||||
@@ -207,7 +207,7 @@ Les votes observés sont gossip/pre-consensus ; aucune garantie d'entrée dans l
|
|||||||
## 7. Threat/security compliance initiale
|
## 7. Threat/security compliance initiale
|
||||||
|
|
||||||
| Invariant | Preuve attendue | Statut |
|
| Invariant | Preuve attendue | Statut |
|
||||||
|---------------------------------------------------|----------------------------------------------|--------------------------------------------------------|
|
|---------------------------------------------------|----------------------------------------------|----------------------------------------------------|
|
||||||
| URL/credentials absents de `Debug` | unit tests URL wrapper | **Done `pre.002`** |
|
| URL/credentials absents de `Debug` | unit tests URL wrapper | **Done `pre.002`** |
|
||||||
| URL/credentials absents des erreurs | validation URL + connection errors safe | **Done through `pre.004`** |
|
| URL/credentials absents des erreurs | validation URL + connection errors safe | **Done through `pre.004`** |
|
||||||
| URL/credentials absents des logs | actor logs only safe endpoint metadata | **Done through `pre.011`, source audit `pre.013`** |
|
| URL/credentials absents des logs | actor logs only safe endpoint metadata | **Done through `pre.011`, source audit `pre.013`** |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md -->
|
<!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md -->
|
||||||
<!-- version: 25 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# Validation `0.2.8` — Helius LaserStream WebSocket
|
# Validation `0.2.8` — Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
|||||||
468
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
Normal file
468
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
|
||||||
|
<!-- version: 27 -->
|
||||||
|
|
||||||
|
# Validation `0.2.9` — Yellowstone gRPC standard + PublicNode
|
||||||
|
|
||||||
|
**Statut courant : la validation technique `pre.013-fix.004` est fermée et verte : `clippy --all-targets`, workspace, graphes Cargo, puis smoke PublicNode authentifié Mainnet + Testnet `2/2 PASS`. `pre.014` réconcilie et ferme cette matrice documentaire ; seule la tranche `pre.015` de préparation de publication restera avant `rel.001`.**
|
||||||
|
|
||||||
|
## 1. Autorités et baseline courante
|
||||||
|
|
||||||
|
Autorités :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RULES.md + docs/rules/*
|
||||||
|
plan 016 courant
|
||||||
|
code réel Transport/Config
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md pour les contraintes non supersédées
|
||||||
|
deltas/0.2.9/* pour l'historique immuable et les décisions postérieures au prompt
|
||||||
|
upstream Yellowstone primaire
|
||||||
|
```
|
||||||
|
|
||||||
|
Baseline opérateur immédiatement avant `pre.012` (`0.2.9-pre.011`) :
|
||||||
|
|
||||||
|
| Gate | Résultat |
|
||||||
|
|------------------------------------------|--------------------------|
|
||||||
|
| `cargo fmt --all` | PASS |
|
||||||
|
| audit Rust workspace | PASS, 0 candidate export |
|
||||||
|
| `cargo check --workspace` | PASS |
|
||||||
|
| `cargo clippy --workspace --all-targets` | PASS sans warning |
|
||||||
|
| Config unit | 113/113 PASS |
|
||||||
|
| Config public API | 15/15 PASS |
|
||||||
|
| Config ownership | 5/5 PASS |
|
||||||
|
| Transport unit | 383/383 PASS |
|
||||||
|
| Transport public API | 49/49 PASS |
|
||||||
|
| Transport release completeness | 43/43 PASS |
|
||||||
|
| Transport doctests | 4/4 PASS |
|
||||||
|
| workspace dependency canary | 3/3 PASS |
|
||||||
|
| `cargo test --workspace` | PASS |
|
||||||
|
|
||||||
|
Cette baseline est le seuil de non-régression de `pre.012`.
|
||||||
|
|
||||||
|
## 2. Gate dépendances et licence
|
||||||
|
|
||||||
|
| Exigence | Décision / preuve | État |
|
||||||
|
|--------------------------------|------------------------------------------|----------|
|
||||||
|
| proto officiel sans vendoring | `yellowstone-grpc-proto ^12.6` | **PASS** |
|
||||||
|
| pas de client upstream runtime | `yellowstone-grpc-client` absent | **PASS** |
|
||||||
|
| runtime Tonic KSP-owned | `tonic ^0.14` + `tonic-prost ^0.14` | **PASS** |
|
||||||
|
| proto subtree compatible | Apache-2.0 selon `LICENSING.md` upstream | **PASS** |
|
||||||
|
| source AGPL copiée | aucune | **PASS** |
|
||||||
|
| raw client Tonic public | aucun | **PASS** |
|
||||||
|
| graph final | graphes `pre.013` inspectés | **PASS** |
|
||||||
|
|
||||||
|
Le graphe de dépendances n'est pas modifié par `pre.012`; aucune nouvelle dépendance Cargo n'est introduite dans cette tranche.
|
||||||
|
|
||||||
|
## 3. Matrice service `Geyser`
|
||||||
|
|
||||||
|
| RPC | Classification | Cible | Preuve | Verdict |
|
||||||
|
|-----------------------|--------------------------------|-------|---------------------------------|----------|
|
||||||
|
| `Subscribe` | standard | oui | stream bidi local + lifecycle | **PASS** |
|
||||||
|
| `SubscribeDeshred` | extension/pré-exécution Triton | non | exclusion documentée | **OUT** |
|
||||||
|
| `SubscribeReplayInfo` | standard unary | oui | fixture unary + reconnect tests | **PASS** |
|
||||||
|
| `Ping` | standard unary | oui | fixture exact echo | **PASS** |
|
||||||
|
| `GetLatestBlockhash` | standard unary | oui | fixture typed | **PASS** |
|
||||||
|
| `GetBlockHeight` | standard unary | oui | fixture typed | **PASS** |
|
||||||
|
| `GetSlot` | standard unary | oui | fixture typed | **PASS** |
|
||||||
|
| `IsBlockhashValid` | standard unary | oui | fixture typed + invalid input | **PASS** |
|
||||||
|
| `GetVersion` | standard unary | oui | fixture typed | **PASS** |
|
||||||
|
|
||||||
|
## 4. `SubscribeRequest` — coverage normative
|
||||||
|
|
||||||
|
### 4.1 Top-level
|
||||||
|
|
||||||
|
| Champ | Verdict | Preuve minimale |
|
||||||
|
|-----------------------|----------|----------------------------------|
|
||||||
|
| `accounts` | **PASS** | maps typed + wire exact |
|
||||||
|
| `slots` | **PASS** | maps typed + wire exact |
|
||||||
|
| `transactions` | **PASS** | maps typed + wire exact |
|
||||||
|
| `transactions_status` | **PASS** | same filter family, separate map |
|
||||||
|
| `blocks` | **PASS** | maps typed + wire exact |
|
||||||
|
| `blocks_meta` | **PASS** | named empty marker preserved |
|
||||||
|
| `entry` | **PASS** | named empty marker preserved |
|
||||||
|
| `commitment` | **PASS** | optional wire semantics |
|
||||||
|
| `accounts_data_slice` | **PASS** | order + bounds |
|
||||||
|
| `ping` | **PASS** | optional id |
|
||||||
|
| `from_slot` | **PASS** | optional + replay mutation |
|
||||||
|
|
||||||
|
### 4.2 Accounts
|
||||||
|
|
||||||
|
```text
|
||||||
|
account[]
|
||||||
|
owner[]
|
||||||
|
filters[]
|
||||||
|
nonempty_txn_signature?
|
||||||
|
cuckoo_accounts_filter?
|
||||||
|
memcmp bytes/base58/base64
|
||||||
|
datasize
|
||||||
|
token_account_state
|
||||||
|
lamports eq/ne/lt/gt
|
||||||
|
```
|
||||||
|
|
||||||
|
Verdict : **PASS** — wire complet retenu, bounds déterministes, payload Debug redacted, malformed fixed-width rejeté.
|
||||||
|
|
||||||
|
### 4.3 Slots
|
||||||
|
|
||||||
|
```text
|
||||||
|
filter_by_commitment?
|
||||||
|
interslot_updates?
|
||||||
|
processed
|
||||||
|
confirmed
|
||||||
|
finalized
|
||||||
|
first_shred_received
|
||||||
|
completed
|
||||||
|
created_bank
|
||||||
|
dead + dead_error?
|
||||||
|
```
|
||||||
|
|
||||||
|
Verdict : **PASS**.
|
||||||
|
|
||||||
|
### 4.4 Transactions et transaction_status
|
||||||
|
|
||||||
|
```text
|
||||||
|
vote?
|
||||||
|
failed?
|
||||||
|
signature?
|
||||||
|
account_include[]
|
||||||
|
account_exclude[]
|
||||||
|
account_required[]
|
||||||
|
cuckoo_account_include?
|
||||||
|
token_accounts? = ALL | BALANCE_CHANGED
|
||||||
|
```
|
||||||
|
|
||||||
|
Verdict : **PASS** — request wire, transaction storage/meta, status error et malformed signature couverts.
|
||||||
|
|
||||||
|
### 4.5 Blocks, block_meta, entry
|
||||||
|
|
||||||
|
```text
|
||||||
|
account_include[]
|
||||||
|
include_transactions?
|
||||||
|
include_accounts?
|
||||||
|
include_entries?
|
||||||
|
cuckoo_account_include?
|
||||||
|
blocks_meta marker
|
||||||
|
entry marker
|
||||||
|
```
|
||||||
|
|
||||||
|
Verdict : **PASS** — block complet, metadata, entries et champs optional/legacy couverts.
|
||||||
|
|
||||||
|
## 5. `SubscribeUpdate` — coverage normative
|
||||||
|
|
||||||
|
| Variante | Verdict |
|
||||||
|
|--------------------------------------|----------|
|
||||||
|
| account | **PASS** |
|
||||||
|
| slot | **PASS** |
|
||||||
|
| transaction | **PASS** |
|
||||||
|
| transaction_status | **PASS** |
|
||||||
|
| block | **PASS** |
|
||||||
|
| ping | **PASS** |
|
||||||
|
| pong | **PASS** |
|
||||||
|
| block_meta | **PASS** |
|
||||||
|
| entry | **PASS** |
|
||||||
|
| top-level `filters[]` / `created_at` | **PASS** |
|
||||||
|
|
||||||
|
Les raw protobufs ne sortent pas de la façade publique. Les décodeurs rejettent les formes structurellement invalides sans copier des payloads arbitraires dans les erreurs.
|
||||||
|
|
||||||
|
## 6. Settings, bounds et redaction
|
||||||
|
|
||||||
|
| Contrôle | État |
|
||||||
|
|-----------------------------------------------|--------------------|
|
||||||
|
| URL `http/https` seulement et longueur bornée | **PASS** |
|
||||||
|
| URL Debug redacted | **PASS** |
|
||||||
|
| metadata key/value/count bornés | **PASS** |
|
||||||
|
| metadata secret/public distincte | **PASS** Transport |
|
||||||
|
| timeouts non nuls et bornés | **PASS** |
|
||||||
|
| reconnect bounds cohérents | **PASS** |
|
||||||
|
| inbound/outbound message sizes | **PASS** |
|
||||||
|
| request/update queue capacities | **PASS** |
|
||||||
|
| filter group/name bounds | **PASS** |
|
||||||
|
| account/owner/memcmp/data slice bounds | **PASS** |
|
||||||
|
| transaction/block selectors bounds | **PASS** |
|
||||||
|
| remote `Status` message/details non recopiés | **PASS** |
|
||||||
|
| Transport -> env/config | absent ou **PASS** |
|
||||||
|
|
||||||
|
## 7. Lifecycle, backpressure et replay
|
||||||
|
|
||||||
|
| Exigence | Verdict | Preuve |
|
||||||
|
|------------------------------|-----------------|------------------------------------------------|
|
||||||
|
| stream bidi unique | **PASS** | round-trip fixture |
|
||||||
|
| mutation request | **PASS** | same request channel |
|
||||||
|
| server Ping / client reply | **PASS** | actor test |
|
||||||
|
| Pong decode | **PASS** | update decode |
|
||||||
|
| server half-close | **PASS** | state terminal observable |
|
||||||
|
| explicit client close | **PASS** | half-close avant deadline |
|
||||||
|
| hostile server shutdown | **PASS** | close timeout borné |
|
||||||
|
| slow receiver | **PASS** | overflow terminal observable |
|
||||||
|
| oversized inbound | **PASS** | Tonic decoder bound |
|
||||||
|
| oversized outbound | **PASS** | reject avant queue dispatch |
|
||||||
|
| reconnect backoff | **PASS** | budget borné |
|
||||||
|
| shutdown pendant backoff | **PASS** | interruption sans nouvelle connexion |
|
||||||
|
| resubscribe déterministe | **PASS** | last accepted full request |
|
||||||
|
| reprise `from_slot` | **PASS** | dernier slot observé + demande explicite |
|
||||||
|
| ReplayInfo `first_available` | **PASS** | clamp/gap seulement si prouvé |
|
||||||
|
| duplicate observability | **PASS** | compteur borné, pas de suppression silencieuse |
|
||||||
|
| exactly-once/lossless | **NON GARANTI** | contrat explicite |
|
||||||
|
|
||||||
|
Snapshot public requis et présent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reconnect_count
|
||||||
|
replay_attempt_count
|
||||||
|
continuity_gap_count
|
||||||
|
duplicate_update_count
|
||||||
|
last_requested_from_slot
|
||||||
|
last_observed_slot
|
||||||
|
state/error code safe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Gate Config V3 — `pre.011`
|
||||||
|
|
||||||
|
### 8.1 Schema et backward compatibility
|
||||||
|
|
||||||
|
Le gate opérateur `pre.011` ferme les exigences suivantes :
|
||||||
|
|
||||||
|
| Exigence | Preuve | Verdict |
|
||||||
|
|------------------------------------------|-----------------------------------------|----------|
|
||||||
|
| schema `$id` V3 | `urn:ksp:schema:std.transport:v3` | **PASS** |
|
||||||
|
| branches V1/V2 conservées | fixtures V1, V2 et document committé | **PASS** |
|
||||||
|
| V1 HTTP-only | `v1_transport_fixture...` | **PASS** |
|
||||||
|
| V2 HTTP+WS | fixture V2 + compatibilité constructeur | **PASS** |
|
||||||
|
| V3 HTTP+WS+gRPC | 113 tests Config | **PASS** |
|
||||||
|
| `grpc_endpoints` optionnel par profil V3 | profil `devnet_public` sans gRPC | **PASS** |
|
||||||
|
| propriétés inconnues refusées | branches schema strictes | **PASS** |
|
||||||
|
|
||||||
|
### 8.2 Mapping runtime
|
||||||
|
|
||||||
|
V3 doit mapper :
|
||||||
|
|
||||||
|
```text
|
||||||
|
grpc_defaults -> YellowstoneGrpcSessionSettings
|
||||||
|
grpc_endpoints[].url -> YellowstoneGrpcEndpointUrl
|
||||||
|
provider -> YellowstoneGrpcProviderName
|
||||||
|
cluster -> YellowstoneGrpcClusterName
|
||||||
|
protocol = solana_yellowstone -> gate Config explicite
|
||||||
|
metadata -> YellowstoneGrpcMetadataEntry::public
|
||||||
|
secret_metadata -> YellowstoneGrpcMetadataEntry::secret
|
||||||
|
session overrides -> merge avec grpc_defaults
|
||||||
|
```
|
||||||
|
|
||||||
|
API :
|
||||||
|
|
||||||
|
| Surface | Exigence | Statut source |
|
||||||
|
|---------------------------------|-------------------------------------|---------------|
|
||||||
|
| `http_settings()` | inchangée | **OK** |
|
||||||
|
| `ws_settings()` | V1 None, V2/V3 selon profil | **OK** |
|
||||||
|
| `grpc_settings()` | V1/V2 None, V3 optionnel | **PASS** |
|
||||||
|
| `into_transport_settings()` | tuple historique HTTP + WS inchangé | **PASS** |
|
||||||
|
| `into_all_transport_settings()` | nouveau tuple HTTP + WS + gRPC | **PASS** |
|
||||||
|
|
||||||
|
### 8.3 Provenance et secrets
|
||||||
|
|
||||||
|
Règles validées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
metadata publique + provenance KSP_SECRET_* -> reject
|
||||||
|
secret_metadata sans provenance secret -> reject
|
||||||
|
secret_metadata + variable KSP_PUBLIC_/KSP_* -> reject
|
||||||
|
secret_metadata + KSP_SECRET_/KSPB_SECRET_ -> accept
|
||||||
|
segments littéraux autour du secret -> accept
|
||||||
|
safe_value -> secret segment ********
|
||||||
|
Transport Debug -> URL/metadata secret absents
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests V3 démontrent le cas valide et les deux croisements invalides sans exposer les canaris.
|
||||||
|
|
||||||
|
### 8.4 PublicNode Mainnet
|
||||||
|
|
||||||
|
Profil committé et validé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_mainnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = mainnet-beta
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = https://solana-yellowstone-grpc.publicnode.com:443
|
||||||
|
secret_metadata = x-token <- ${KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mapping produit un endpoint TLS Yellowstone standard et une metadata secrète redacted. Le smoke final ouvre `Subscribe`, reçoit un update `Slot` non nul et ferme de manière bornée : **PASS**.
|
||||||
|
|
||||||
|
### 8.5 PublicNode Testnet
|
||||||
|
|
||||||
|
Profil committé et validé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = publicnode_testnet
|
||||||
|
provider = publicnode
|
||||||
|
cluster = testnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = https://solana-testnet-yellowstone-grpc.publicnode.com:443
|
||||||
|
secret_metadata = x-token <- ${KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le hostname exact a été fourni par l'opérateur puis validé par le smoke live. `Subscribe` reçoit un update `Slot` non nul : **PASS**.
|
||||||
|
|
||||||
|
Le même personal token opérateur a été validé sur les deux réseaux. Les deux variables KSP restent distinctes afin de permettre des valeurs différentes si la policy provider change ; cette modélisation ne constitue pas une preuve que les tokens PublicNode sont network-scoped.
|
||||||
|
|
||||||
|
## 9. Provider-neutrality
|
||||||
|
|
||||||
|
| Point | Verdict |
|
||||||
|
|-----------------------------------------------|------------------|
|
||||||
|
| type public `PublicNodeGrpc*` sans divergence | absent, **PASS** |
|
||||||
|
| protocol standard encodé comme provider | non, **PASS** |
|
||||||
|
| provider descriptif séparé | oui, **PASS** |
|
||||||
|
| auth PublicNode hardcodée dans Transport | non, **PASS** |
|
||||||
|
| Helius/OrbitFlare runtime gRPC dans `0.2.9` | non, **PASS** |
|
||||||
|
| provider extensions dans N2 | aucune, **PASS** |
|
||||||
|
|
||||||
|
## 10. Non-régressions obligatoires
|
||||||
|
|
||||||
|
Le gate final doit préserver :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP current typed 52/52
|
||||||
|
HTTP historical 14/14 Deprecated/Removed
|
||||||
|
KSP-TRANSPORT-007 vert
|
||||||
|
Standard WebSocket 9 familles / 18 opérations
|
||||||
|
Helius LaserStream WebSocket 7 familles standard + transaction + slotsUpdates, heartbeat provider-owned
|
||||||
|
Transport -> Config interdit
|
||||||
|
Transport -> std::env KSP_* interdit
|
||||||
|
tracing direct Transport interdit
|
||||||
|
```
|
||||||
|
|
||||||
|
La baseline `pre.011` reste conservée. Les changements `pre.013` ont porté uniquement le gate live, la Config PublicNode/Testnet et le harness de smoke ; le gate final `pre.013-fix.004` confirme les non-régressions workspace.
|
||||||
|
|
||||||
|
## 11. Historique des gates fermé
|
||||||
|
|
||||||
|
Les détails de commandes, warnings corrigés et fichiers exacts restent dans leurs deltas immuables.
|
||||||
|
|
||||||
|
| Tranche | Gate consolidé |
|
||||||
|
|-----------------------------------|---------------------------------------|
|
||||||
|
| `pre.001` + `fix.001` + `fix.002` | audit/sizing/providers/licence fermé |
|
||||||
|
| `pre.002` + `fix.001` + `fix.002` | moteur/settings/channel fermé |
|
||||||
|
| `pre.003` + `fix.001` | TLS/metadata/unary fermé |
|
||||||
|
| `pre.004` + `fix.001` | Subscribe common fermé |
|
||||||
|
| `pre.005` + `fix.001` | Accounts/Slots fermé |
|
||||||
|
| `pre.006` | namespace HTTP fermé |
|
||||||
|
| `pre.007` | Transactions fermé |
|
||||||
|
| `pre.008` + `fix.001` | Blocks fermé |
|
||||||
|
| `pre.009` + `fix.001` | bidi/backpressure fermé |
|
||||||
|
| `pre.010` + `fix.001` | reconnect/replay fermé sans warning |
|
||||||
|
| `pre.011` | Config V3/PublicNode mapping fermé |
|
||||||
|
| `pre.012` + `fix.001` | réaudit + règles de fermeture |
|
||||||
|
| `pre.013` + `fix.001`–`fix.004` | live PublicNode + graphes fermé |
|
||||||
|
| `pre.014` | réconciliation documentaire candidate |
|
||||||
|
|
||||||
|
Cette table remplace les anciens appendices numérotés successivement `19.x`, `20`, `21`, etc. qui rendaient le document ambigu.
|
||||||
|
|
||||||
|
## 12. Verdict opérateur consolidé jusqu'à `pre.013-fix.004`
|
||||||
|
|
||||||
|
Le gate technique final reçu le 2026-08-24 est :
|
||||||
|
|
||||||
|
| Exigence | Résultat opérateur |
|
||||||
|
|-----------------------------------------------|--------------------------------------|
|
||||||
|
| `cargo fmt --all` | **PASS** |
|
||||||
|
| audit Rust workspace | **PASS, 0 candidate export** |
|
||||||
|
| audit Markdown | **PASS, 87 tableaux / 258 fichiers** |
|
||||||
|
| `cargo check --workspace` | **PASS** |
|
||||||
|
| `cargo clippy --workspace --all-targets` | **PASS sans warning** |
|
||||||
|
| Transport unit | **383/383 PASS** |
|
||||||
|
| Transport public API | **49/49 PASS** |
|
||||||
|
| Transport release completeness | **43/43 PASS** |
|
||||||
|
| Transport doctests | **4/4 PASS** |
|
||||||
|
| Config unit dans workspace | **113/113 PASS** |
|
||||||
|
| Config ownership dans workspace | **5/5 PASS** |
|
||||||
|
| Config public API dans workspace | **15/15 PASS** |
|
||||||
|
| workspace dependencies | **3/3 PASS** |
|
||||||
|
| `cargo test --workspace` | **PASS** |
|
||||||
|
| graphes Cargo finaux | **PASS / inspectés** |
|
||||||
|
| smoke PublicNode Mainnet `Subscribe` + `Slot` | **PASS** |
|
||||||
|
| smoke PublicNode Testnet `Subscribe` + `Slot` | **PASS** |
|
||||||
|
| smoke live total | **2/2 PASS** |
|
||||||
|
|
||||||
|
Les graphes résolus confirment la stack attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic 0.14.6
|
||||||
|
tonic-prost 0.14.6
|
||||||
|
prost 0.14.4
|
||||||
|
prost-types 0.14.4
|
||||||
|
yellowstone-grpc-client absent du runtime KSP
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes ont été exécutés avant `pre.013-fix.004`; ils restent valides car `fix.004` n'a modifié ni dépendance ni feature Cargo.
|
||||||
|
|
||||||
|
## 13. Verdict PublicNode live final
|
||||||
|
|
||||||
|
Les essais intermédiaires ont établi deux faits utiles : les appels sans credential peuvent atteindre le service mais recevoir `PERMISSION_DENIED`, et la validation représentative de la foundation doit porter sur `Subscribe` plutôt que sur une unary provider éventuellement restreinte.
|
||||||
|
|
||||||
|
Le smoke final :
|
||||||
|
|
||||||
|
```text
|
||||||
|
reçoit deux personal tokens par stdin, Mainnet puis Testnet
|
||||||
|
accepte que les deux lignes contiennent la même valeur
|
||||||
|
construit une metadata secrète x-token
|
||||||
|
ouvre TLS puis Subscribe
|
||||||
|
filtre slots
|
||||||
|
attend un YellowstoneSubscribeUpdate::Slot sous timeout
|
||||||
|
exige slot > 0
|
||||||
|
ferme de manière bornée
|
||||||
|
n'expose aucun secret dans URL/Debug/arguments
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultat opérateur final : **Mainnet PASS, Testnet PASS, 2/2**.
|
||||||
|
|
||||||
|
Le timeout KSP de half-close est un résultat accepté par le harness uniquement après réception prouvée d'un slot ; le runtime conserve son contrat de timeout explicite.
|
||||||
|
|
||||||
|
## 14. Gate `pre.014` — réconciliation documentaire
|
||||||
|
|
||||||
|
Cette tranche ne modifie ni runtime, ni Config, ni schema, ni smoke. Elle synchronise le plan, la présente validation, README/USAGE Transport et les références durables avec les faits exécutés en `pre.013`.
|
||||||
|
|
||||||
|
Le verdict technique de `0.2.9` est donc **PASS**. Après le gate documentaire `pre.014`, seuls le prompt suivant, `CHANGELOG.md` et `ROADMAP.md` restent à finaliser en `pre.015`.
|
||||||
|
|
||||||
|
## 15. Gate `pre.015` — préparation de publication
|
||||||
|
|
||||||
|
Payload fonctionnel autorisé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Seuls les fichiers mécaniques de version et de traçabilité (`Cargo.toml`, delta `pre.015`) peuvent s'ajouter à ce payload. Aucun README, USAGE, plan, validation, règle, code, test, schema ou config ne doit être corrigé dans `pre.015`.
|
||||||
|
|
||||||
|
Si un tel défaut est découvert, une nouvelle prerelease dédiée est ouverte puis la préparation de publication est rejouée sous un nouveau numéro.
|
||||||
|
|
||||||
|
## 16. Verdict stable attendu avant `rel.001`
|
||||||
|
|
||||||
|
`0.2.9` peut passer à `rel.001` seulement après la séquence suivante :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.013 technique/live fermé
|
||||||
|
pre.014 réconciliation documentaire fermée
|
||||||
|
pre.015 prompt/CHANGELOG/ROADMAP fermé
|
||||||
|
```
|
||||||
|
|
||||||
|
Et si tous les invariants suivants sont vrais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
standard vs extension explicitement classifié
|
||||||
|
SubscribeDeshred OUT documenté
|
||||||
|
7 unary verts
|
||||||
|
Subscribe + 9 updates verts
|
||||||
|
resource/backpressure/lifecycle verts
|
||||||
|
reconnect/replay sans promesse lossless
|
||||||
|
Config V3 backward V1/V2
|
||||||
|
provider/protocol distincts
|
||||||
|
PublicNode Mainnet live Subscribe + Slot PASS
|
||||||
|
PublicNode Testnet live Subscribe + Slot PASS
|
||||||
|
x-token secret requis par les profils PublicNode, aucun secret versionné
|
||||||
|
HTTP/WS/Helius non régressés
|
||||||
|
dependency firewall vert
|
||||||
|
cargo graphs inspectés
|
||||||
|
README/USAGE réconciliés avant la dernière pre
|
||||||
|
validation 012 fermée avant la dernière pre
|
||||||
|
prompt/CHANGELOG/ROADMAP seuls dans la dernière pre
|
||||||
|
workspace final vert
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: prompts/000-README.md -->
|
<!-- file: prompts/000-README.md -->
|
||||||
<!-- version: 25 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# Prompts KSP
|
# Prompts KSP
|
||||||
|
|
||||||
@@ -35,3 +35,4 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
|
|||||||
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde.
|
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde.
|
||||||
- [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt historique consommé pour ouvrir puis conduire la release stable `0.2.8 — Helius LaserStream WebSocket` depuis `v0.2.7`; préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`, il imposait relecture des règles, réaudit Helius, gate `pre.001` strict audit/brainstorming/sizing, actor standard partagé, frontières provider/Config/secrets et forecast souple avant implémentation lourde.
|
- [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt historique consommé pour ouvrir puis conduire la release stable `0.2.8 — Helius LaserStream WebSocket` depuis `v0.2.7`; préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`, il imposait relecture des règles, réaudit Helius, gate `pre.001` strict audit/brainstorming/sizing, actor standard partagé, frontières provider/Config/secrets et forecast souple avant implémentation lourde.
|
||||||
- [`014-V0_2_9_START_PROMPT.md`](014-V0_2_9_START_PROMPT.md) — prompt actif après publication `0.2.8-rel.001` et tag stable `v0.2.8`, préparé par `0.2.8-pre.011`; il ouvre `0.2.9 — Yellowstone gRPC standard/provider-neutral` et impose la base stable autoritaire, la relecture ordonnée des règles/architectures/compliances Transport, un réaudit Yellowstone actuel (service/proto/releases/crates), un audit dépendances/licences/MSRV avant choix du client/proto, la séparation standard/provider/deshred, un gate `pre.001` strict audit/brainstorming/sizing et une prévision souple détaillée avant toute implémentation gRPC lourde.
|
- [`014-V0_2_9_START_PROMPT.md`](014-V0_2_9_START_PROMPT.md) — prompt actif après publication `0.2.8-rel.001` et tag stable `v0.2.8`, préparé par `0.2.8-pre.011`; il ouvre `0.2.9 — Yellowstone gRPC standard/provider-neutral` et impose la base stable autoritaire, la relecture ordonnée des règles/architectures/compliances Transport, un réaudit Yellowstone actuel (service/proto/releases/crates), un audit dépendances/licences/MSRV avant choix du client/proto, la séparation standard/provider/deshred, un gate `pre.001` strict audit/brainstorming/sizing et une prévision souple détaillée avant toute implémentation gRPC lourde.
|
||||||
|
- [`015-V0_2_10_START_PROMPT.md`](015-V0_2_10_START_PROMPT.md) — prompt préparé par `0.2.9-pre.012` pour ouvrir `0.2.10 — OrbitFlare Yellowstone gRPC` depuis la future base stable `v0.2.9`; il impose la réutilisation du moteur Yellowstone N1 et du standard N2, un gate `pre.001` d’audit actuel des endpoints/auth/capabilities/heartbeat OrbitFlare, la distinction control-plane/data-plane/IP whitelist/credentials, le sizing avant toute extension provider et la non-régression PublicNode/Yellowstone standard.
|
||||||
|
|||||||
1158
prompts/015-V0_2_10_START_PROMPT.md
Normal file
1158
prompts/015-V0_2_10_START_PROMPT.md
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user