Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afaa9c0239 | |||
| 7d456b41dc | |||
| 0edeed1c48 | |||
| be5e3464ee | |||
| 1967e845b0 | |||
| af807afff5 | |||
| 7f567bc1bb | |||
| 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 |
14
.env.example
14
.env.example
@@ -1,5 +1,5 @@
|
||||
# file: .env.example
|
||||
# version: 6
|
||||
# version: 9
|
||||
|
||||
# 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.
|
||||
@@ -34,6 +34,18 @@ 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.
|
||||
# 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
|
||||
|
||||
# OrbitFlare Solana Free License Key used as secret x-token metadata by the committed Devnet Yellowstone gRPC profile.
|
||||
# Keep the real ORBIT-* value only in the process environment or local .env; do not substitute the Customer API X-ORBIT-KEY.
|
||||
# KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN=replace-me
|
||||
|
||||
# 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.
|
||||
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,10 +1,26 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.2.10 — OrbitFlare Yellowstone gRPC — 2026-08-25
|
||||
|
||||
`0.2.10` ajoute OrbitFlare comme provider Yellowstone gRPC sur le moteur N1 et le standard N2 acquis dans `0.2.9`, sans modifier le moteur gRPC ni créer de façade provider inutile. L'audit et le smoke live ont établi le contrat réel du service Solana Free Devnet : endpoint `http://devnet.rpc.orbitflare.com:10000`, authentification par metadata gRPC secrète `x-token` portant la License Key OrbitFlare, et émission du Ping Yellowstone standard. Config Transport V3 représente ce provider avec `provider = orbitflare`, `cluster = devnet`, `protocol = solana_yellowstone` et `secret_metadata`, tandis que `.env.example` inventorie `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` sans valeur réelle.
|
||||
|
||||
La première tentative live sans metadata a volontairement échoué avec `Unauthenticated`, ce qui a conduit à `pre.002-fix.001` après audit des sources OrbitFlare actuelles. Le smoke corrigé reçoit la License Key uniquement par stdin opérateur, ouvre `Subscribe`, observe un Slot non nul et un `SubscribeUpdate::Ping`, puis ferme la session proprement. Le même smoke est repassé au gate technique final `pre.003`. La clôture conserve donc N1/N2 inchangés, n'ajoute ni heartbeat OrbitFlare, ni SDK provider, ni Config V4, et confirme que la réponse automatique au Ping standard existante suffit.
|
||||
|
||||
Le gate final conserve l'ensemble du workspace vert : audits Rust/Markdown, `cargo check`, Clippy, tests Config et Transport, `cargo test --workspace`, canaris de dépendances et graphes Cargo. Helius LaserStream gRPC, audité comme largement wire-compatible Yellowstone mais non raisonnablement live-testable sans abonnement payant, est reporté dans les TODO provider sans numéro de release. La séquence active passe donc à `0.2.11 — off-chain price transport`; `prompts/016-V0_2_11_START_PROMPT.md` l'ouvre exclusivement depuis le tag stable `v0.2.10` avec audit actuel des sources de prix, de leurs sémantiques et de leur testabilité avant toute implémentation lourde.
|
||||
|
||||
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` é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
|
||||
# version: 236
|
||||
# version: 268
|
||||
|
||||
[workspace]
|
||||
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"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.8"
|
||||
version = "0.2.10"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -15,31 +15,35 @@ publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
argon2 = { version = "^0.5", default-features = false }
|
||||
base64 = { version = "^0.23" }
|
||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||
chrono = { version = "^0.4", default-features = false }
|
||||
directories = { version = "^6.0" }
|
||||
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||
getrandom = { version = "^0.4", default-features = false }
|
||||
base64 = { version = "^0.23" }
|
||||
fs2 = { version = "^0.4" }
|
||||
futures-util = { version = "^0.3", default-features = false }
|
||||
serde = { version = "^1.0" }
|
||||
serde_json = { version = "^1.0" }
|
||||
getrandom = { version = "^0.4", default-features = false }
|
||||
http = { version = "^1.5", default-features = false }
|
||||
jsonschema = { version = "^0.51", 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-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-subscriber = { version = "^0.3", default-features = false }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false }
|
||||
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||
tempfile = { version = "^3.27" }
|
||||
chrono = { version = "^0.4", default-features = false }
|
||||
tauri = { version = "^2.11" }
|
||||
tauri-build = { version = "^2.6" }
|
||||
tauri-plugin-dialog = { version = "^2.7" }
|
||||
tauri-plugin-tracing = { version = "^0.3" }
|
||||
tonic = { version = "^0.14", default-features = false }
|
||||
tonic-prost = { version = "^0.14", default-features = false }
|
||||
ts-rs = { version = "^12.0" }
|
||||
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
|
||||
zeroize = { version = "^1.9" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
23
ROADMAP.md
23
ROADMAP.md
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 83 -->
|
||||
<!-- version: 87 -->
|
||||
|
||||
# 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.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.
|
||||
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
||||
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
||||
- [ ] `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.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.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
||||
- [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.
|
||||
- [X] `0.2.10` — OrbitFlare Yellowstone gRPC stable : profil Config V3 Devnet, License Key injectée comme metadata secrète `x-token`, smoke live `Subscribe -> Slot + Ping` validé deux fois, sans modification du moteur N1/N2 ni heartbeat provider.
|
||||
- [ ] `0.2.11` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix provider-neutral, au minimum SOL/USD et SOL/EUR, après audit actuel des sémantiques provider et de la testabilité live.
|
||||
- [ ] `0.2.12` — 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.13` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
||||
- [ ] `0.2.14` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
||||
|
||||
### TODO/IDEAS — providers Yellowstone non planifiés
|
||||
|
||||
- [ ] **TODO** — Helius LaserStream gRPC : réauditer lorsque l'accès live gRPC est raisonnablement disponible ; conserver N1/N2 Yellowstone inchangés, vérifier auth/endpoints/Subscribe/Ping/replay/from_slot/erreurs provider et traiter les preprocessed transactions comme extension Helius séparée.
|
||||
- [ ] **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
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "urn:ksp:schema:std.transport:v2",
|
||||
"title": "KSP standard HTTP + WebSocket Transport configuration",
|
||||
"$id": "urn:ksp:schema:std.transport:v3",
|
||||
"title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/documentV1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/documentV2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/documentV3"
|
||||
}
|
||||
],
|
||||
"$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": {
|
||||
"max_retries": 2,
|
||||
"initial_backoff_ms": 100,
|
||||
@@ -22,6 +22,20 @@
|
||||
"max_frame_size_bytes": 16777216,
|
||||
"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",
|
||||
"profiles": [
|
||||
{
|
||||
@@ -65,6 +79,64 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "orbitflare_devnet",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "solana_devnet_public",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.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_devnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
|
||||
}
|
||||
],
|
||||
"grpc_endpoints": [
|
||||
{
|
||||
"name": "orbitflare_solana_devnet_yellowstone",
|
||||
"enabled": true,
|
||||
"provider": "orbitflare",
|
||||
"cluster": "devnet",
|
||||
"protocol": "solana_yellowstone",
|
||||
"url": "http://devnet.rpc.orbitflare.com:10000",
|
||||
"metadata": [],
|
||||
"secret_metadata": [
|
||||
{
|
||||
"key": "x-token",
|
||||
"value": "${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"profile_id": "mainnet_public",
|
||||
"endpoints": [
|
||||
@@ -105,6 +177,120 @@
|
||||
"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
|
||||
# version: 8
|
||||
# version: 9
|
||||
|
||||
[package]
|
||||
name = "ksp-app-config-desk"
|
||||
@@ -22,6 +22,7 @@ path = "src/main.rs"
|
||||
tauri-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["std", "now"] }
|
||||
fs2.workspace = true
|
||||
ksp-config-lib = { path = "../ksp-config-lib" }
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
@@ -30,7 +31,6 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
chrono = { workspace = true, features = ["std", "now"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
ts-rs.workspace = true
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-config-lib/Cargo.toml
|
||||
# version: 7
|
||||
# version: 8
|
||||
|
||||
[package]
|
||||
name = "ksp-config-lib"
|
||||
@@ -9,12 +9,12 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
directories.workspace = true
|
||||
jsonschema.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
jsonschema.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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 ;
|
||||
- [`../../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.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/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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# 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.
|
||||
|
||||
### 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
|
||||
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 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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 17
|
||||
// version: 18
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![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
|
||||
//! 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
|
||||
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
|
||||
//! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
|
||||
//! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
|
||||
//! JSON/`.env` persistence.
|
||||
|
||||
mod bootstrap;
|
||||
mod composite;
|
||||
@@ -154,9 +155,9 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
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;
|
||||
/// 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;
|
||||
/// Default physical filename for the standard Wallet configuration document.
|
||||
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;
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
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;
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
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;
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
pub use self::registry::FILE_ID_STD_WALLET;
|
||||
@@ -188,7 +189,7 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
||||
pub use self::sensitivity::ResolvedConfigJson;
|
||||
/// One resolved Config string preserving real/safe representations and provenance.
|
||||
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;
|
||||
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
||||
pub use self::wallet::ResolvedWalletConfig;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
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";
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
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";
|
||||
/// 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";
|
||||
/// Default physical filename for the standard Wallet configuration document.
|
||||
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";
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
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";
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
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";
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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)]
|
||||
pub struct ResolvedTransportConfig {
|
||||
file_id: crate::ConfigFileId,
|
||||
@@ -11,6 +11,7 @@ pub struct ResolvedTransportConfig {
|
||||
effective: crate::ResolvedConfigJson,
|
||||
settings: ksp_onchain_transport_lib::HttpTransportSettings,
|
||||
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
||||
grpc_settings: std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
|
||||
}
|
||||
|
||||
impl ResolvedTransportConfig {
|
||||
@@ -61,7 +62,7 @@ impl ResolvedTransportConfig {
|
||||
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`].
|
||||
#[must_use]
|
||||
@@ -69,19 +70,42 @@ impl ResolvedTransportConfig {
|
||||
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.
|
||||
#[must_use]
|
||||
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
|
||||
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]
|
||||
pub fn into_transport_settings(
|
||||
self,
|
||||
) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) {
|
||||
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 {
|
||||
@@ -94,15 +118,17 @@ impl std::fmt::Debug for ResolvedTransportConfig {
|
||||
.field("selection_source", &self.selection_source)
|
||||
.field("effective", &self.effective)
|
||||
.field("has_ws_settings", &self.ws_settings.is_some())
|
||||
.field("has_grpc_settings", &self.grpc_settings.is_some())
|
||||
.finish_non_exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
/// 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(
|
||||
&self,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
@@ -121,7 +147,8 @@ impl crate::ConfigDocumentEngine {
|
||||
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`.
|
||||
pub fn resolve_transport_config_profile(
|
||||
@@ -148,9 +175,13 @@ struct EffectiveTransportSource {
|
||||
retry: EffectiveRetrySource,
|
||||
#[serde(default)]
|
||||
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
|
||||
#[serde(default)]
|
||||
grpc_defaults: std::option::Option<EffectiveGrpcSessionSource>,
|
||||
endpoints: std::vec::Vec<EffectiveEndpointSource>,
|
||||
#[serde(default)]
|
||||
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
|
||||
#[serde(default)]
|
||||
grpc_endpoints: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -255,6 +286,72 @@ struct EffectiveWsReconnectOverrideSource {
|
||||
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> {
|
||||
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);
|
||||
@@ -294,16 +391,26 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
||||
std::result::Result::Ok(value) => value,
|
||||
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() {
|
||||
std::option::Option::Some(value) => value.endpoints().len(),
|
||||
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!(
|
||||
target: crate::TRACING_TARGET,
|
||||
profile_id = profile.profile_id(),
|
||||
format_version,
|
||||
http_endpoint_count = settings.endpoints().len(),
|
||||
ws_endpoint_count,
|
||||
grpc_endpoint_count,
|
||||
"mapped standard Transport Config to runtime settings"
|
||||
);
|
||||
return std::result::Result::Ok(ResolvedTransportConfig {
|
||||
@@ -314,6 +421,7 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
||||
effective,
|
||||
settings,
|
||||
ws_settings,
|
||||
grpc_settings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,18 +436,22 @@ fn map_optional_ws_settings(
|
||||
if defaults.is_some() || sources.is_some() {
|
||||
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
|
||||
} 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)
|
||||
}
|
||||
},
|
||||
2 => {
|
||||
2 | 3 => {
|
||||
let defaults = match defaults {
|
||||
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 {
|
||||
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 = 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(
|
||||
sources: std::vec::Vec<EffectiveEndpointSource>,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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,
|
||||
//! 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);
|
||||
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_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_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_SCHEMA_STD_TRANSPORT, "schema.std.transport");
|
||||
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
|
||||
// version: 6
|
||||
// version: 12
|
||||
|
||||
#[test]
|
||||
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().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!(resolved.grpc_settings().is_none(), "V2 fixture must remain Yellowstone gRPC-free");
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
|
||||
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()[0].url().as_str(), "https://legacy.invalid");
|
||||
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.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||
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();
|
||||
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 {
|
||||
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);
|
||||
@@ -121,8 +124,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
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].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();
|
||||
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 {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
|
||||
}
|
||||
@@ -130,7 +134,193 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_without_polling() {
|
||||
fn committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc_with_license_x_token() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let canary = "ORBITFLARE-DEVNET-LICENSE-X-TOKEN-CANARY";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN".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("orbitflare_devnet"), &environment);
|
||||
assert!(resolved.is_ok(), "committed OrbitFlare Devnet Transport profile should map from its License Key x-token: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "orbitflare_devnet");
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
assert_eq!(resolved.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "OrbitFlare Devnet profile should retain the standard Solana Devnet WebSocket companion endpoint");
|
||||
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].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
}
|
||||
let grpc = resolved.grpc_settings();
|
||||
assert!(grpc.is_some(), "OrbitFlare Devnet profile should expose one provider-neutral Yellowstone gRPC endpoint");
|
||||
if let std::option::Option::Some(grpc) = grpc {
|
||||
assert_eq!(grpc.endpoints().len(), 1);
|
||||
let endpoint = &grpc.endpoints()[0];
|
||||
assert_eq!(endpoint.name(), "orbitflare_solana_devnet_yellowstone");
|
||||
assert_eq!(endpoint.provider().as_str(), "orbitflare");
|
||||
assert_eq!(endpoint.cluster().as_str(), "devnet");
|
||||
assert_eq!(endpoint.url().as_str(), "http://devnet.rpc.orbitflare.com:10000");
|
||||
assert!(!endpoint.url().uses_tls(), "OrbitFlare shared Devnet URL must remain explicitly HTTP/2 plaintext");
|
||||
assert_eq!(endpoint.metadata().len(), 1);
|
||||
assert_eq!(endpoint.metadata()[0].key(), "x-token");
|
||||
assert!(endpoint.metadata()[0].is_secret());
|
||||
assert!(grpc.validate().is_ok(), "Config-produced OrbitFlare Yellowstone gRPC settings should satisfy Transport validation");
|
||||
let debug = format!("{grpc:?}");
|
||||
assert!(!debug.contains("orbitflare.com"), "Transport settings Debug must not expose OrbitFlare gRPC endpoint URLs");
|
||||
assert!(!debug.contains(canary), "OrbitFlare License Key x-token must stay redacted from Transport settings Debug");
|
||||
assert!(!debug.contains("X-ORBIT-KEY"), "OrbitFlare Customer API credentials must never enter Yellowstone Transport settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -138,11 +328,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 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 {
|
||||
let (http, ws) = resolved.into_transport_settings();
|
||||
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 {
|
||||
assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation");
|
||||
assert_eq!(ws.endpoints().len(), 1);
|
||||
@@ -170,6 +360,7 @@ fn transport_profile_preserves_global_and_profile_origin() {
|
||||
if let std::result::Result::Ok(profile) = profile {
|
||||
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("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("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
@@ -421,6 +612,22 @@ fn v1_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
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> {
|
||||
let workspace = workspace_root();
|
||||
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
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
//! 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-logging-lib"));
|
||||
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("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("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("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]
|
||||
@@ -86,7 +92,20 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
|
||||
let dependency_names = manifest_dependency_names(dependencies);
|
||||
assert_eq!(
|
||||
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);
|
||||
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::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> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-logging-lib/Cargo.toml
|
||||
# version: 5
|
||||
# version: 6
|
||||
|
||||
[package]
|
||||
name = "ksp-logging-lib"
|
||||
@@ -10,8 +10,8 @@ repository.workspace = true
|
||||
[dependencies]
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
tracing = { workspace = true, features = ["std"] }
|
||||
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
# version: 6
|
||||
# version: 9
|
||||
|
||||
[package]
|
||||
name = "ksp-onchain-transport-lib"
|
||||
@@ -8,17 +8,23 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures-util = { workspace = true, features = ["sink", "std"] }
|
||||
http.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
futures-util = { workspace = true, features = ["sink", "std"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
|
||||
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]
|
||||
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
||||
tonic = { workspace = true, features = ["codegen", "server"] }
|
||||
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 24 -->
|
||||
|
||||
# `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
|
||||
|
||||
@@ -21,6 +21,8 @@ La crate possède :
|
||||
- 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 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 ;
|
||||
- l'observabilité Transport via `ksp-logging-lib`.
|
||||
|
||||
@@ -37,6 +39,7 @@ ksp-config-lib
|
||||
-> ksp-logging-lib
|
||||
-> reqwest / tokio / serde
|
||||
-> tokio-tungstenite / futures-util
|
||||
-> tonic / tonic-prost / yellowstone-grpc-proto
|
||||
```
|
||||
|
||||
La direction inverse est interdite :
|
||||
@@ -217,6 +220,33 @@ 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`.
|
||||
|
||||
## 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.
|
||||
|
||||
`0.2.10` ajoute le profil `orbitflare_devnet` sur le même moteur provider-neutral. L’endpoint validé est `http://devnet.rpc.orbitflare.com:10000`; la License Key `ORBIT-*` est fournie par Config via `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` et devient la metadata secrète `x-token`. Le smoke live dédié a validé `Subscribe` slots à commitment confirmed, un `Slot` non nul et un `SubscribeUpdate::Ping` standard. Cette preuve confirme qu’aucune façade OrbitFlare, aucun heartbeat provider et aucune modification N1/N2 ne sont nécessaires.
|
||||
|
||||
## Résilience
|
||||
|
||||
L'admission est calculée par couple endpoint/rôle. Le pool applique :
|
||||
@@ -253,7 +283,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.
|
||||
|
||||
Trois smokes Devnet opt-in sont séparés par responsabilité :
|
||||
Quatre smokes réseau opt-in sont séparés par responsabilité :
|
||||
|
||||
```text
|
||||
Transport HTTP pur : settings programmatiques -> HttpTransportPool
|
||||
@@ -270,13 +300,21 @@ Transport WebSocket pur : settings programmatiques -> WsSession
|
||||
|
||||
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
||||
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||
|
||||
Transport Yellowstone gRPC PublicNode : settings programmatiques + x-token secret
|
||||
-> Mainnet + Testnet
|
||||
-> TLS -> Subscribe slots -> Slot non nul -> close borné
|
||||
|
||||
Transport Yellowstone gRPC OrbitFlare : settings programmatiques + License Key x-token
|
||||
-> Devnet
|
||||
-> Subscribe slots -> Slot non nul + server Ping -> 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 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 cinq tests sont `ignored` par défaut ; le test PublicNode Yellowstone contient deux cas live Mainnet/Testnet et le test OrbitFlare contient un cas Devnet. 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.
|
||||
|
||||
@@ -290,4 +328,8 @@ 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/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 ;
|
||||
- [`../../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 ;
|
||||
- [`../../docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md`](../../docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md) — composition OrbitFlare Devnet sur le standard Yellowstone ;
|
||||
- [`../../docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md`](../../docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md) — validation finale OrbitFlare `x-token`, `Slot + Ping` et non-régression N1/N2 ;
|
||||
- [`../../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 -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 23 -->
|
||||
|
||||
# Utilisation de `ksp-onchain-transport-lib`
|
||||
|
||||
@@ -257,7 +257,141 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
### Profil OrbitFlare Devnet
|
||||
|
||||
Le profil committé `orbitflare_devnet` réutilise exactement le même accès Config -> Transport :
|
||||
|
||||
```rust
|
||||
let resolved = match engine.load_resolved_transport_config(Some("orbitflare_devnet"), &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 OrbitFlare 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 OrbitFlare profile has no enabled Yellowstone gRPC endpoint",
|
||||
)),
|
||||
};
|
||||
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
|
||||
```
|
||||
|
||||
Config résout `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` vers la metadata secrète `x-token`. Sa valeur effective est la License Key `ORBIT-*` du produit Solana ; `X-ORBIT-KEY` et le Bearer du Customer API ne doivent pas être utilisés pour Yellowstone. Transport ne lit jamais cette variable lui-même.
|
||||
|
||||
L’endpoint validé par `0.2.10` est `http://devnet.rpc.orbitflare.com:10000`. Il reste volontairement en `http` : KSP ne remplace pas le transport provider par `https` sans endpoint TLS explicitement fourni.
|
||||
|
||||
### 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`.
|
||||
|
||||
@@ -317,7 +451,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.
|
||||
|
||||
## 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 :
|
||||
|
||||
@@ -331,7 +465,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.
|
||||
|
||||
## 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 :
|
||||
|
||||
@@ -346,13 +480,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.
|
||||
|
||||
## 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.
|
||||
|
||||
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()`.
|
||||
|
||||
@@ -360,7 +494,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.
|
||||
|
||||
## 9. Logging
|
||||
## 10. Logging
|
||||
|
||||
Les événements Transport utilisent le target :
|
||||
|
||||
@@ -372,7 +506,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.
|
||||
|
||||
## 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 :
|
||||
|
||||
@@ -390,6 +524,51 @@ 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.
|
||||
|
||||
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 **Transport Yellowstone gRPC OrbitFlare** est lui aussi indépendant de Config. Il lit une seule License Key sur stdin, la classe comme metadata secrète `x-token`, ouvre le standard `Subscribe`, demande `slots` à commitment confirmed, attend un Slot non nul et un `SubscribeUpdate::Ping`, puis ferme la session de manière bornée :
|
||||
|
||||
```bash
|
||||
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY
|
||||
echo
|
||||
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" \
|
||||
| cargo test -p ksp-onchain-transport-lib \
|
||||
--test yellowstone_orbitflare_smoke \
|
||||
-- --ignored --nocapture
|
||||
unset ORBITFLARE_LICENSE_KEY
|
||||
```
|
||||
|
||||
Endpoint validé :
|
||||
|
||||
```text
|
||||
Devnet http://devnet.rpc.orbitflare.com:10000
|
||||
```
|
||||
|
||||
Le gate `0.2.10-pre.003` a passé ce scénario en live avec `Slot + Ping`. Le Ping reçu est le message Yellowstone standard auquel N1 sait déjà répondre sans remplacer la dernière requête complète mémorisée ; aucun heartbeat OrbitFlare supplémentaire n’est donc requis.
|
||||
|
||||
Le smoke de **composition Config -> Transport** reste également disponible :
|
||||
|
||||
```bash
|
||||
@@ -400,7 +579,7 @@ Il valide le profil committé `devnet_public` et les quatre canaris foundation.
|
||||
|
||||
### 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 :
|
||||
|
||||
@@ -416,7 +595,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.
|
||||
|
||||
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 :
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
/// 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");
|
||||
/// 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.
|
||||
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.
|
||||
|
||||
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
|
||||
// version: 7
|
||||
// file: crates/ksp-onchain-transport-lib/src/http_client.rs
|
||||
// version: 8
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint or role.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -510,5 +510,5 @@ fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/client.rs"]
|
||||
#[path = "../unit_tests/http_client.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
// version: 3
|
||||
// file: crates/ksp-onchain-transport-lib/src/http_executor.rs
|
||||
// version: 4
|
||||
|
||||
const HTTP_BAD_GATEWAY: u16 = 502;
|
||||
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
||||
@@ -228,5 +228,5 @@ fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ks
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/executor.rs"]
|
||||
#[path = "../unit_tests/http_executor.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/pool.rs
|
||||
// version: 7
|
||||
// file: crates/ksp-onchain-transport-lib/src/http_pool.rs
|
||||
// version: 8
|
||||
|
||||
/// Safe snapshot of the logical HTTP endpoint pool.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -616,5 +616,5 @@ fn request_timeout(role: &crate::HttpRoleName, request_kind: &crate::HttpRequest
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/pool.rs"]
|
||||
#[path = "../unit_tests/http_pool.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
|
||||
// version: 3
|
||||
// file: crates/ksp-onchain-transport-lib/src/http_resilience.rs
|
||||
// version: 4
|
||||
|
||||
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);
|
||||
@@ -401,5 +401,5 @@ fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std:
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/resilience.rs"]
|
||||
#[path = "../unit_tests/http_resilience.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
||||
// version: 6
|
||||
// file: crates/ksp-onchain-transport-lib/src/http_settings.rs
|
||||
// version: 7
|
||||
|
||||
/// Runtime HTTP endpoint URL owned by Transport.
|
||||
///
|
||||
@@ -582,5 +582,5 @@ fn invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
#[path = "../unit_tests/http_settings.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 34
|
||||
// version: 44
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![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
|
||||
//! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
|
||||
//! 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 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 pool;
|
||||
mod resilience;
|
||||
mod rpc_accounts;
|
||||
mod rpc_blocks;
|
||||
mod rpc_canary;
|
||||
@@ -48,7 +68,6 @@ mod rpc_economics;
|
||||
mod rpc_method;
|
||||
mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
mod ws_accounts;
|
||||
mod ws_blocks;
|
||||
mod ws_cluster;
|
||||
@@ -60,16 +79,16 @@ mod ws_settings;
|
||||
mod ws_subscription;
|
||||
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.
|
||||
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.
|
||||
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
||||
/// 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;
|
||||
/// Error code used when a WebSocket session is no longer available.
|
||||
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.
|
||||
pub use self::json_rpc::JsonRpcErrorObject;
|
||||
/// 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;
|
||||
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
||||
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.
|
||||
pub use self::rpc_accounts::SolanaAccount;
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
@@ -304,26 +493,6 @@ pub use self::rpc_transactions::SolanaTransactionEncoding;
|
||||
pub use self::rpc_transactions::SolanaTransactionVersion;
|
||||
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
|
||||
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.
|
||||
pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
||||
/// 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.
|
||||
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.
|
||||
pub(crate) use self::resilience::HttpConcurrencyPermit;
|
||||
pub(crate) use self::http_resilience::HttpConcurrencyPermit;
|
||||
/// 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
pub(crate) use self::ws_session::WsSessionCommand;
|
||||
/// Crate-internal notification dispatch result.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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.
|
||||
|
||||
@@ -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>();
|
||||
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
|
||||
// version: 32
|
||||
// version: 41
|
||||
|
||||
//! 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");
|
||||
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,150 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||
// version: 2
|
||||
|
||||
//! Opt-in live OrbitFlare Devnet characterization smoke for authenticated provider-neutral Yellowstone gRPC Subscribe.
|
||||
|
||||
use std::io::IsTerminal; // rust-rules: trait-import
|
||||
|
||||
fn orbitflare_license_x_token() -> std::string::String {
|
||||
assert!(
|
||||
!std::io::stdin().is_terminal(),
|
||||
"pipe the OrbitFlare Solana Free License Key to this ignored smoke on stdin; never pass it as a command-line argument"
|
||||
);
|
||||
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!("OrbitFlare License Key x-token must be readable from smoke stdin: {error}"),
|
||||
}
|
||||
let token = token.trim().to_owned();
|
||||
assert!(!token.is_empty(), "OrbitFlare License Key x-token provided on smoke stdin must not be empty");
|
||||
return token;
|
||||
}
|
||||
|
||||
fn orbitflare_devnet_endpoint(x_token: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||
let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://devnet.rpc.orbitflare.com:10000") {
|
||||
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(
|
||||
"orbitflare_devnet_yellowstone",
|
||||
true,
|
||||
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("orbitflare"),
|
||||
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||
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),
|
||||
}
|
||||
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||
return std::result::Result::Ok(request);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "opt-in live OrbitFlare Devnet Yellowstone gRPC characterization; reads one Solana Free License Key x-token from stdin, waits for Slot plus standard server Ping, then closes"]
|
||||
async fn orbitflare_devnet_yellowstone_streams_slots_and_emits_standard_server_ping_with_license_x_token() {
|
||||
let x_token = orbitflare_license_x_token();
|
||||
let endpoint = match orbitflare_devnet_endpoint(x_token.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("programmatic OrbitFlare Devnet Yellowstone settings must accept the License Key as secret x-token metadata: {error:?}")
|
||||
},
|
||||
};
|
||||
assert_eq!(endpoint.metadata().len(), 1);
|
||||
assert_eq!(endpoint.metadata()[0].key(), "x-token");
|
||||
assert!(endpoint.metadata()[0].is_secret());
|
||||
assert!(!endpoint.url().uses_tls(), "the documented OrbitFlare shared Devnet endpoint is intentionally HTTP/2 plaintext");
|
||||
let endpoint_debug = format!("{endpoint:?}");
|
||||
assert!(!endpoint_debug.contains("orbitflare.com"), "OrbitFlare endpoint URL must not appear in endpoint Debug");
|
||||
assert!(!endpoint_debug.contains(x_token.as_str()), "OrbitFlare License Key x-token must not appear in endpoint Debug");
|
||||
assert!(!endpoint_debug.contains("X-ORBIT-KEY"), "OrbitFlare Customer API credentials must not enter Yellowstone settings");
|
||||
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("OrbitFlare Devnet Yellowstone authenticated connection must succeed: {error:?}"),
|
||||
};
|
||||
assert_eq!(channel.endpoint_name(), "orbitflare_devnet_yellowstone");
|
||||
assert_eq!(channel.provider().as_str(), "orbitflare");
|
||||
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||
let request = match slot_request() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("OrbitFlare 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!("OrbitFlare Devnet standard Subscribe with License Key x-token must open: {error:?}"),
|
||||
};
|
||||
let deadline = tokio::time::sleep(std::time::Duration::from_secs(45));
|
||||
tokio::pin!(deadline);
|
||||
let mut slot_seen = false;
|
||||
let mut ping_seen = false;
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = &mut deadline => break,
|
||||
next_update = session.next_update() => {
|
||||
let next_update = match next_update {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("OrbitFlare Devnet Yellowstone Subscribe update must decode: {error:?}"),
|
||||
};
|
||||
match next_update {
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(update)) => {
|
||||
if update.slot() > 0 {
|
||||
slot_seen = true;
|
||||
}
|
||||
},
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Ping(_)) => ping_seen = true,
|
||||
std::option::Option::Some(_) => {},
|
||||
std::option::Option::None => break,
|
||||
}
|
||||
if slot_seen && ping_seen {
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
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 live characterization, OrbitFlare close may time out waiting for provider half-close but must not fail for another reason: {error:?}"
|
||||
);
|
||||
},
|
||||
}
|
||||
assert!(slot_seen, "OrbitFlare Devnet Yellowstone must publish at least one non-zero confirmed slot during the 45-second characterization window");
|
||||
assert!(
|
||||
ping_seen,
|
||||
"OrbitFlare Devnet must emit the standard Yellowstone server Ping during the 45-second characterization window before any provider-specific heartbeat is justified"
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
// version: 3
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/http_client.rs
|
||||
// version: 4
|
||||
|
||||
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
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
|
||||
// version: 2
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/http_executor.rs
|
||||
// version: 3
|
||||
|
||||
fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||
// version: 4
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/http_pool.rs
|
||||
// version: 5
|
||||
|
||||
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||
return crate::HttpEndpointRoleSettings::new(
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
||||
// version: 2
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/http_resilience.rs
|
||||
// version: 3
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
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
|
||||
// version: 2
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/http_settings.rs
|
||||
// version: 3
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
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
|
||||
# version: 6
|
||||
# version: 7
|
||||
|
||||
[package]
|
||||
name = "ksp-wallet-lib"
|
||||
@@ -9,12 +9,12 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||
base64.workspace = true
|
||||
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
|
||||
getrandom.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
base64.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
solana-keypair.workspace = true
|
||||
|
||||
327
deltas/0.2.10/pre.001.md
Normal file
327
deltas/0.2.10/pre.001.md
Normal file
@@ -0,0 +1,327 @@
|
||||
<!-- file: deltas/0.2.10/pre.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.001` — audit/sizing OrbitFlare Yellowstone gRPC
|
||||
|
||||
## 1. Base requise et vérifiée
|
||||
|
||||
Archive autoritaire fournie :
|
||||
|
||||
```text
|
||||
khadhroony-solana-project-v0.2.9-full-from-gitea.zip
|
||||
```
|
||||
|
||||
État vérifié :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.9
|
||||
deltas/0.2.9/rel.001.md présent
|
||||
prompts/015-V0_2_10_START_PROMPT.md présent
|
||||
metadata .git absente de l'archive Gitea
|
||||
```
|
||||
|
||||
Cette livraison ouvre :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10-pre.1
|
||||
commit attendu = v0.2.10-pre.001
|
||||
aucun tag prerelease
|
||||
```
|
||||
|
||||
## 2. Objet
|
||||
|
||||
`pre.001` exécute le gate documentaire obligatoire :
|
||||
|
||||
```text
|
||||
lecture base/règles/architecture
|
||||
baseline stable
|
||||
réaudit OrbitFlare actuel
|
||||
réaudit Yellowstone actuel
|
||||
auth control-plane/data-plane
|
||||
endpoints/network/security
|
||||
Free Devnet gRPC
|
||||
heartbeat
|
||||
capabilities/unary/replay
|
||||
limits/quotas
|
||||
architecture N1/N2 immuable
|
||||
threat model
|
||||
sizing et forecast
|
||||
```
|
||||
|
||||
Aucun code provider lourd, SDK OrbitFlare, metadata secrète ou heartbeat supplémentaire n'est ajouté.
|
||||
|
||||
## 3. Baseline opérateur acquise
|
||||
|
||||
Preuves fournies le 2026-08-25 sur `v0.2.9` :
|
||||
|
||||
```text
|
||||
cargo fmt --all OK
|
||||
python3 scripts/audit_rust_workspace_rules.py OK / clean
|
||||
python3 scripts/audit_markdown_tables.py OK / 87 tables / 261 files
|
||||
cargo check --workspace OK
|
||||
cargo clippy --workspace --all-targets OK
|
||||
cargo test --workspace OK
|
||||
cargo tree -p ksp-onchain-transport-lib --duplicates fourni
|
||||
```
|
||||
|
||||
Transport pendant le workspace test :
|
||||
|
||||
```text
|
||||
383 unit
|
||||
49 public_api
|
||||
43 release_completeness
|
||||
4 doctests
|
||||
```
|
||||
|
||||
Les live smokes restent opt-in/ignored par défaut.
|
||||
|
||||
Le `cargo tree -p ksp-onchain-transport-lib` complet n'a pas encore été fourni et doit être rejoué avant commit.
|
||||
|
||||
## 4. Graphe de dépendances inspecté
|
||||
|
||||
Versions utiles visibles :
|
||||
|
||||
```text
|
||||
yellowstone-grpc-proto 12.6.0
|
||||
tonic 0.14.6
|
||||
tonic-prost 0.14.6
|
||||
prost 0.14.4
|
||||
tokio 1.53.1
|
||||
http 1.5.0
|
||||
reqwest 0.13.4
|
||||
```
|
||||
|
||||
Aucune divergence Tonic/Prost/Yellowstone ne justifie une intervention. Les doublons restants sont sans impact matériel pour OrbitFlare.
|
||||
|
||||
## 5. Réaudit OrbitFlare du 2026-08-25
|
||||
|
||||
### Free Devnet
|
||||
|
||||
Le pricing actuel confirme :
|
||||
|
||||
```text
|
||||
Free 0 USD/mois
|
||||
RPC 10 RPS
|
||||
transactions 1 TPS
|
||||
gRPC Devnet only
|
||||
credit limits unlimited
|
||||
```
|
||||
|
||||
Le CLI documente :
|
||||
|
||||
```text
|
||||
RPC Devnet = http://devnet.rpc.orbitflare.com
|
||||
gRPC Devnet = http://devnet.rpc.orbitflare.com:10000
|
||||
```
|
||||
|
||||
Objectif opérationnel fermé : utiliser OrbitFlare comme provider gRPC Devnet gratuit pour les tests KSP futurs, en complément de PublicNode Mainnet/Testnet.
|
||||
|
||||
### Auth
|
||||
|
||||
Classification :
|
||||
|
||||
```text
|
||||
X-ORBIT-KEY Customer API only
|
||||
Bearer Customer API v2
|
||||
RPC api_key HTTP RPC only
|
||||
account API key ne prouve pas une auth Yellowstone
|
||||
gRPC Dashboard token possible selon licence/service
|
||||
x-token mécanisme Yellowstone plausible si token gRPC distinct
|
||||
aucune metadata documentée par SDK Go pour endpoints régionaux
|
||||
```
|
||||
|
||||
La clé de compte opérateur ne doit pas être fournie au dépôt ni injectée par supposition dans gRPC.
|
||||
|
||||
### Heartbeat
|
||||
|
||||
OrbitFlare recommande un `SubscribeRequest::Ping` périodique. Yellowstone upstream envoie déjà un `SubscribeUpdate::Ping` périodique et KSP N1 répond automatiquement par un Ping standard sans modifier le dernier full request mémorisé.
|
||||
|
||||
Décision :
|
||||
|
||||
```text
|
||||
aucun timer OrbitFlare ajouté au moteur
|
||||
aucun changement YellowstoneGrpcSessionSettings
|
||||
aucune modification grpc_stream.rs
|
||||
pre.002 doit observer le Ping serveur live OrbitFlare Devnet
|
||||
```
|
||||
|
||||
Si ce Ping est observé, la policy standard N1 est suffisante. Sinon, une divergence provider doit être démontrée puis composée au-dessus de N1 sans le modifier.
|
||||
|
||||
### Limits
|
||||
|
||||
```text
|
||||
50 connexions gRPC simultanées par IP sur shared
|
||||
subscriptions par connexion annoncées unlimited
|
||||
idle timeout environ 10 minutes
|
||||
RESOURCE_EXHAUSTED au dépassement
|
||||
backoff exponentiel conseillé
|
||||
```
|
||||
|
||||
Ces valeurs restent provider-owned.
|
||||
|
||||
## 6. Réaudit Yellowstone actuel
|
||||
|
||||
État observé :
|
||||
|
||||
```text
|
||||
release GitHub courante v15.1.2+solana.4.2.0
|
||||
yellowstone-grpc-proto publié 12.6.0
|
||||
```
|
||||
|
||||
Aucune évolution matérielle ne remet en cause le standard N2 de `0.2.9`. Aucune dependency n'est modifiée.
|
||||
|
||||
## 7. Invariant architectural supersédant l'hypothèse initiale
|
||||
|
||||
Décision opérateur explicite :
|
||||
|
||||
```text
|
||||
le moteur gRPC ne change jamais pour un provider
|
||||
le moteur WebSocket ne change jamais pour un provider
|
||||
les fonctionnalités provider se composent au-dessus du moteur
|
||||
```
|
||||
|
||||
Pour `0.2.10` :
|
||||
|
||||
```text
|
||||
N1 moteur Yellowstone = IMMUTABLE
|
||||
N2 standard Solana = IMMUTABLE
|
||||
N3 OrbitFlare = profile/capability/facade seulement si besoin démontré
|
||||
```
|
||||
|
||||
Le prompt `015` reste autoritatif sauf sur toute formulation qui permettrait de mettre une policy OrbitFlare directement dans le moteur partagé.
|
||||
|
||||
## 8. Config V3
|
||||
|
||||
La shape existante suffit :
|
||||
|
||||
```text
|
||||
profile_id = orbitflare_devnet
|
||||
provider = orbitflare
|
||||
cluster = devnet
|
||||
protocol = solana_yellowstone
|
||||
url = http://devnet.rpc.orbitflare.com:10000
|
||||
metadata = []
|
||||
secret_metadata = []
|
||||
```
|
||||
|
||||
Pas de V4. Pas de secret ajouté avant preuve live. Pas de champ heartbeat moteur.
|
||||
|
||||
## 9. Smoke prévu
|
||||
|
||||
`pre.002` doit commencer par un canari standard N2 :
|
||||
|
||||
```text
|
||||
connect OrbitFlare Devnet Free sans metadata
|
||||
Subscribe slots confirmed
|
||||
recevoir Slot non nul
|
||||
observer SubscribeUpdate::Ping pendant une fenêtre bornée de caractérisation
|
||||
fermer proprement
|
||||
```
|
||||
|
||||
Puis sonder séparément les unary et replay sans rendre le smoke minimal dépendant d'entitlements provider non garantis.
|
||||
|
||||
## 10. Forecast recalibré
|
||||
|
||||
Chemin standard probable :
|
||||
|
||||
```text
|
||||
pre.001 audit/sizing
|
||||
pre.002 Config Devnet + characterization smoke standard N2
|
||||
pre.003 gate technique/live final
|
||||
pre.004 réconciliation documentaire finale
|
||||
pre.005 publication minimale
|
||||
rel.001 stable
|
||||
```
|
||||
|
||||
Chemin avec divergence provider démontrée :
|
||||
|
||||
```text
|
||||
pre.001 audit/sizing
|
||||
pre.002 Config Devnet + characterization smoke
|
||||
pre.003 provider overlay minimal au-dessus de N1/N2
|
||||
pre.004 gate technique/live final
|
||||
pre.005 réconciliation documentaire finale
|
||||
pre.006 publication minimale
|
||||
rel.001 stable
|
||||
```
|
||||
|
||||
## 11. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||
deltas/0.2.10/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
|
||||
```
|
||||
|
||||
Aucune implémentation provider ou Config runtime n'est introduite dans `pre.001`.
|
||||
|
||||
## 14. Sources externes réauditées
|
||||
|
||||
```text
|
||||
https://docs.orbitflare.com/llms.txt
|
||||
https://docs.orbitflare.com/cli
|
||||
https://docs.orbitflare.com/authentication
|
||||
https://docs.orbitflare.com/data-streaming/yellowstone
|
||||
https://docs.orbitflare.com/data-streaming/yellowstone-quickstart
|
||||
https://docs.orbitflare.com/sdk/go-grpc
|
||||
https://orbitflare.com/pricing
|
||||
https://orbitflare.com/products/rpc-nodes
|
||||
https://orbitflare.com/products/solana-grpc
|
||||
https://github.com/rpcpool/yellowstone-grpc
|
||||
https://github.com/rpcpool/yellowstone-grpc/releases
|
||||
https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md
|
||||
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto
|
||||
```
|
||||
|
||||
## 15. Validations de préparation
|
||||
|
||||
Le sandbox a exécuté :
|
||||
|
||||
```text
|
||||
inspection archive/version/rel/prompt OK
|
||||
lecture code N1/N2/Config et docs 0.2.9 OK
|
||||
réaudit OrbitFlare/Yellowstone actuel OK
|
||||
inspection cargo tree --duplicates fourni par opérateur OK
|
||||
```
|
||||
|
||||
Le sandbox ne dispose pas de Cargo ; aucun `cargo check/clippy/test/tree` post-overlay n'est déclaré réussi localement.
|
||||
|
||||
## 16. Validation opérateur attendue avant commit
|
||||
|
||||
Comme `pre.001` change uniquement la version Cargo et la documentation :
|
||||
|
||||
```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.10
|
||||
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 : aucun changement de dependency et aucun changement sous `crates/`.
|
||||
196
deltas/0.2.10/pre.002-fix.001.md
Normal file
196
deltas/0.2.10/pre.002-fix.001.md
Normal file
@@ -0,0 +1,196 @@
|
||||
<!-- file: deltas/0.2.10/pre.002-fix.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.002-fix.001` — OrbitFlare License Key `x-token`
|
||||
|
||||
## 1. Base et identité
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.2.10-pre.002 commité
|
||||
```
|
||||
|
||||
Le fix porte une correction runtime/configuration de l’auth provider. La version Cargo devient donc :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10-pre.2.fix.1
|
||||
commit attendu = v0.2.10-pre.002-fix.001
|
||||
aucun tag prerelease
|
||||
```
|
||||
|
||||
## 2. Défaut démontré par le live `pre.002`
|
||||
|
||||
Le gate statique/workspace `pre.002` est vert. Le smoke live sans metadata a atteint OrbitFlare puis l’ouverture du stream a échoué de manière sûre :
|
||||
|
||||
```text
|
||||
grpc_operation = SubscribeOpen
|
||||
grpc_status = Unauthenticated
|
||||
grpc_code = The request does not have valid authentication credentials
|
||||
```
|
||||
|
||||
Le Dashboard opérateur montre un produit `Solana Free` avec License Key `ORBIT-*` et `API Key Mode Active`.
|
||||
|
||||
La référence Yellowstone OrbitFlare précise ensuite le contrat data-plane :
|
||||
|
||||
```text
|
||||
ORBITFLARE_LICENSE_KEY
|
||||
-> metadata gRPC x-token
|
||||
-> Yellowstone
|
||||
```
|
||||
|
||||
Le `X-ORBIT-KEY` et le Bearer Device Flow restent des credentials Customer API et ne sont pas utilisés par Transport Yellowstone.
|
||||
|
||||
## 3. Correction Config V3
|
||||
|
||||
`config/std.transport.json` conserve le même endpoint et ajoute uniquement la metadata secrète requise :
|
||||
|
||||
```text
|
||||
provider = orbitflare
|
||||
cluster = devnet
|
||||
protocol = solana_yellowstone
|
||||
url = http://devnet.rpc.orbitflare.com:10000
|
||||
metadata = []
|
||||
secret_metadata = x-token <- ${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}
|
||||
```
|
||||
|
||||
Aucun format Config V4 n’est créé.
|
||||
|
||||
`.env.example` inventorie désormais :
|
||||
|
||||
```text
|
||||
KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN
|
||||
```
|
||||
|
||||
La valeur locale attendue est la License Key `ORBIT-*`. Une `X-ORBIT-KEY` Customer API ne doit jamais être placée dans cette variable.
|
||||
|
||||
## 4. Preuve Config déterministe
|
||||
|
||||
Le test `committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc_with_license_x_token` injecte un canari secret via `ConfigEnvironment` et prouve :
|
||||
|
||||
```text
|
||||
profil explicite orbitflare_devnet
|
||||
companions HTTP/WS Devnet inchangés
|
||||
endpoint Yellowstone OrbitFlare inchangé
|
||||
metadata key = x-token
|
||||
metadata classée secret
|
||||
validation Transport PASS
|
||||
URL provider absente du Debug
|
||||
canari secret absent du Debug
|
||||
X-ORBIT-KEY absent du Transport
|
||||
```
|
||||
|
||||
## 5. Smoke live corrigé
|
||||
|
||||
`yellowstone_orbitflare_smoke.rs` ne dépend toujours pas de Config et ne lit aucun environnement. Il suit le pattern de sécurité déjà utilisé par les smokes PublicNode :
|
||||
|
||||
```text
|
||||
une License Key lue sur stdin
|
||||
aucun secret en argument de ligne de commande
|
||||
YellowstoneGrpcMetadataEntry::secret("x-token", ...)
|
||||
reconnect max_retries = 0
|
||||
Subscribe slots confirmed
|
||||
fenêtre Slot + Ping = 45 s
|
||||
close borné
|
||||
```
|
||||
|
||||
Le smoke vérifie également que la License Key n’apparaît pas dans `Debug`.
|
||||
|
||||
Commande opérateur :
|
||||
|
||||
```bash
|
||||
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY; echo
|
||||
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" | cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||
unset ORBITFLARE_LICENSE_KEY
|
||||
```
|
||||
|
||||
## 6. Invariant architectural
|
||||
|
||||
Toujours inchangés :
|
||||
|
||||
```text
|
||||
src/grpc_settings.rs
|
||||
src/grpc_channel.rs
|
||||
src/grpc_unary.rs
|
||||
src/grpc_subscribe.rs
|
||||
src/grpc_stream.rs
|
||||
```
|
||||
|
||||
Le standard Yellowstone N2 reste inchangé. Aucun SDK OrbitFlare runtime n’est ajouté.
|
||||
|
||||
L’auth provider est entièrement composée par la surface metadata secrète déjà existante.
|
||||
|
||||
## 7. Heartbeat
|
||||
|
||||
Ce fix ne matérialise aucun heartbeat OrbitFlare.
|
||||
|
||||
Le rerun authentifié doit d’abord qualifier :
|
||||
|
||||
```text
|
||||
Slot non nul
|
||||
SubscribeUpdate::Ping serveur
|
||||
```
|
||||
|
||||
Si les deux sont observés, N1 répond déjà automatiquement au Ping standard et aucune couche provider heartbeat n’est requise.
|
||||
|
||||
Si le stream authentifié fonctionne mais n’émet aucun Ping serveur dans la fenêtre de caractérisation, `pre.003` devient une tranche provider-specific au-dessus de N1/N2. Il reste interdit d’ajouter un timer OrbitFlare au moteur gRPC.
|
||||
|
||||
## 8. Validation disponible dans l’environnement de génération
|
||||
|
||||
Exécuté :
|
||||
|
||||
```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.10
|
||||
Markdown table audit: clean (99 table(s), 99 file(s))
|
||||
|
||||
config/std.transport.json
|
||||
JSON parse PASS
|
||||
JSON Schema validation PASS
|
||||
|
||||
comparaison N1 avec pre.002
|
||||
grpc_settings.rs inchangé
|
||||
grpc_channel.rs inchangé
|
||||
grpc_unary.rs inchangé
|
||||
grpc_subscribe.rs inchangé
|
||||
grpc_stream.rs inchangé
|
||||
```
|
||||
|
||||
Aucune commande Cargo n’est déclarée exécutée dans l’environnement de génération.
|
||||
|
||||
## 9. Gates opérateur
|
||||
|
||||
Après application du fix :
|
||||
|
||||
```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.10
|
||||
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
|
||||
```
|
||||
|
||||
Puis exécuter le live authentifié décrit en section 5.
|
||||
|
||||
## 10. Fichiers ajoutés/modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
.env.example
|
||||
config/std.transport.json
|
||||
crates/ksp-config-lib/unit_tests/transport.rs
|
||||
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||
deltas/0.2.10/pre.002-fix.001.md
|
||||
```
|
||||
|
||||
Le delta commité `deltas/0.2.10/pre.002.md` reste immuable.
|
||||
287
deltas/0.2.10/pre.002.md
Normal file
287
deltas/0.2.10/pre.002.md
Normal file
@@ -0,0 +1,287 @@
|
||||
<!-- file: deltas/0.2.10/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.002` — OrbitFlare Devnet Config + characterization smoke
|
||||
|
||||
## 1. Base et version
|
||||
|
||||
Base de travail :
|
||||
|
||||
```text
|
||||
0.2.10-pre.001
|
||||
```
|
||||
|
||||
Validation opérateur fournie sur cette base le 2026-08-25 :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
python3 scripts/audit_markdown_tables.py PASS / 97 tables / 233 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test --workspace PASS
|
||||
cargo tree -p ksp-onchain-transport-lib fourni
|
||||
cargo tree -p ksp-onchain-transport-lib --duplicates fourni
|
||||
```
|
||||
|
||||
Le graphe confirme notamment :
|
||||
|
||||
```text
|
||||
yellowstone-grpc-proto 12.6.0
|
||||
tonic 0.14.6
|
||||
tonic-prost 0.14.6
|
||||
prost 0.14.4
|
||||
tokio 1.53.1
|
||||
```
|
||||
|
||||
Cette livraison passe à :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10-pre.2
|
||||
commit attendu = v0.2.10-pre.002
|
||||
aucun tag prerelease
|
||||
```
|
||||
|
||||
## 2. Invariant architectural
|
||||
|
||||
Aucun moteur physique n'est modifié :
|
||||
|
||||
```text
|
||||
src/grpc_settings.rs inchangé
|
||||
src/grpc_channel.rs inchangé
|
||||
src/grpc_unary.rs inchangé
|
||||
src/grpc_subscribe.rs inchangé
|
||||
src/grpc_stream.rs inchangé
|
||||
```
|
||||
|
||||
Le standard Yellowstone N2 reste également inchangé.
|
||||
|
||||
OrbitFlare est matérialisé uniquement par :
|
||||
|
||||
```text
|
||||
Config provider profile
|
||||
provider-neutral Yellowstone smoke
|
||||
provider validation/documentation
|
||||
```
|
||||
|
||||
Aucun SDK OrbitFlare, second client Tonic, proto provider, heartbeat moteur ou metadata secrète n'est ajouté.
|
||||
|
||||
## 3. Profil Config V3 `orbitflare_devnet`
|
||||
|
||||
`config/std.transport.json` ajoute un profil dédié :
|
||||
|
||||
```text
|
||||
profile_id = orbitflare_devnet
|
||||
```
|
||||
|
||||
Il conserve les companions standards déjà utilisés sur Devnet :
|
||||
|
||||
```text
|
||||
HTTP = https://api.devnet.solana.com
|
||||
WS = wss://api.devnet.solana.com
|
||||
```
|
||||
|
||||
et ajoute exactement un endpoint Yellowstone :
|
||||
|
||||
```text
|
||||
name = orbitflare_solana_devnet_yellowstone
|
||||
provider = orbitflare
|
||||
cluster = devnet
|
||||
protocol = solana_yellowstone
|
||||
url = http://devnet.rpc.orbitflare.com:10000
|
||||
metadata = []
|
||||
secret_metadata = []
|
||||
```
|
||||
|
||||
Le format reste V3. `.env.example` n'est pas modifié et aucune API key de compte OrbitFlare n'est reclassifiée comme credential gRPC.
|
||||
|
||||
`ksp-config-lib` ajoute un test déterministe qui prouve :
|
||||
|
||||
```text
|
||||
sélection explicite orbitflare_devnet
|
||||
companions HTTP/WS Devnet préservés
|
||||
provider/cluster/protocol gRPC corrects
|
||||
URL plaintext conservée telle quelle
|
||||
aucune metadata inventée
|
||||
Debug gRPC sans URL provider
|
||||
absence de X-ORBIT-KEY dans Transport settings
|
||||
```
|
||||
|
||||
## 4. Smoke OrbitFlare Devnet
|
||||
|
||||
Nouveau test opt-in :
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||
```
|
||||
|
||||
Le test utilise directement les API publiques Yellowstone existantes de Transport, sans dépendre de Config :
|
||||
|
||||
```text
|
||||
endpoint Devnet OrbitFlare sans metadata
|
||||
reconnect max_retries = 0
|
||||
Subscribe slots
|
||||
commitment = confirmed
|
||||
fenêtre de caractérisation = 45 s
|
||||
preuve Slot non nul
|
||||
preuve SubscribeUpdate::Ping serveur
|
||||
close borné
|
||||
```
|
||||
|
||||
Le `Ping` reçu reste traité par le moteur N1 déjà livré en `0.2.9`, qui répond automatiquement sur le même stream. Le test n'ajoute aucune policy heartbeat.
|
||||
|
||||
Commande live :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Interprétation :
|
||||
|
||||
```text
|
||||
Slot + Ping PASS
|
||||
OrbitFlare reste standard ; aucune façade/policy provider heartbeat requise
|
||||
|
||||
connection/auth failure
|
||||
classifier le service/licence réel avant toute metadata
|
||||
|
||||
Slot PASS mais Ping absent
|
||||
qualifier une divergence heartbeat provider avant toute implémentation spécifique
|
||||
```
|
||||
|
||||
## 5. Audit Markdown étendu
|
||||
|
||||
`scripts/audit_markdown_tables.py` passe en version 2 et contrôle désormais aussi l'équivalent structurel de :
|
||||
|
||||
```text
|
||||
\n\s*\n\s*\n
|
||||
```
|
||||
|
||||
c'est-à-dire au moins deux lignes vides consécutives hors fenced code block.
|
||||
|
||||
Le contrôle :
|
||||
|
||||
```text
|
||||
considère une ligne whitespace-only comme vide
|
||||
signale fichier + première ligne + longueur de la séquence
|
||||
n'interprète pas les blancs internes aux ``` / ~~~ fenced blocks
|
||||
ignore les répertoires générés ou tiers en parcours récursif
|
||||
```
|
||||
|
||||
Répertoires récursifs exclus :
|
||||
|
||||
```text
|
||||
.git
|
||||
.idea
|
||||
.venv
|
||||
__pycache__
|
||||
dist
|
||||
node_modules
|
||||
target
|
||||
```
|
||||
|
||||
Cette exclusion rend le résultat déterministe entre une archive Gitea propre et un checkout développeur contenant des `node_modules`. Le nombre de fichiers audités peut donc diminuer par rapport à l'ancien script sans signifier une perte de couverture KSP-owned.
|
||||
|
||||
Une seule séquence de deux lignes vides existait dans le scope KSP actif de l'archive : elle est supprimée dans `crates/ksp-onchain-transport-lib/README.md`.
|
||||
|
||||
`docs/rules/RULES_DOCUMENTATION.md` formalise `DOC-BLANK-001` à `DOC-BLANK-003`, y compris l'immuabilité des anciens deltas publiés.
|
||||
|
||||
## 6. Documents de plan/validation
|
||||
|
||||
Le plan 017 et la validation 013 passent en version 2 pour enregistrer :
|
||||
|
||||
```text
|
||||
pre.001 operator gate fermé
|
||||
profil orbitflare_devnet matérialisé
|
||||
smoke de caractérisation matérialisé
|
||||
N1/N2 inchangés
|
||||
live Devnet encore PENDING
|
||||
```
|
||||
|
||||
Aucun README/USAGE OrbitFlare durable n'est finalisé ici ; cette responsabilité reste réservée au couloir documentaire final.
|
||||
|
||||
## 7. Validation disponible dans l'environnement de génération
|
||||
|
||||
Exécuté sans Cargo :
|
||||
|
||||
```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.10
|
||||
clean / 98 tables / 98 KSP-owned files
|
||||
|
||||
config/std.transport.json
|
||||
JSON parse PASS
|
||||
JSON Schema validation PASS
|
||||
```
|
||||
|
||||
Le nouvel audit Markdown a aussi été caractérisé avec des fixtures temporaires :
|
||||
|
||||
```text
|
||||
deux lignes vides hors fence FAIL attendu
|
||||
plusieurs lignes vides dans fence PASS attendu
|
||||
node_modules récursif ignoré attendu
|
||||
```
|
||||
|
||||
Aucune commande Cargo n'est déclarée exécutée dans l'environnement de génération.
|
||||
|
||||
## 8. Gates opérateur `pre.002`
|
||||
|
||||
Exécuter :
|
||||
|
||||
```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.10
|
||||
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
|
||||
```
|
||||
|
||||
Puis le live dédié :
|
||||
|
||||
```bash
|
||||
cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
`pre.002` ne ferme son gate provider que lorsque le résultat live est classifié. Un échec d'auth ou l'absence de `Ping` ne doit jamais être corrigé par modification du moteur N1.
|
||||
|
||||
## 9. Fichiers ajoutés/modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
config/std.transport.json
|
||||
crates/ksp-config-lib/unit_tests/transport.rs
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||
docs/rules/RULES_DOCUMENTATION.md
|
||||
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||
scripts/audit_markdown_tables.py
|
||||
deltas/0.2.10/pre.002.md
|
||||
```
|
||||
|
||||
## 10. Suite conditionnelle
|
||||
|
||||
Si le live retourne `Slot + Ping PASS` :
|
||||
|
||||
```text
|
||||
pre.003 = gate technique/live final
|
||||
```
|
||||
|
||||
Si une divergence provider réelle est démontrée :
|
||||
|
||||
```text
|
||||
pre.003 = tranche provider-specific minimale au-dessus de N1/N2
|
||||
pre.004 = gate technique/live final
|
||||
```
|
||||
|
||||
Le couloir documentaire final puis la prerelease de publication restent distincts dans les deux cas.
|
||||
252
deltas/0.2.10/pre.003.md
Normal file
252
deltas/0.2.10/pre.003.md
Normal file
@@ -0,0 +1,252 @@
|
||||
<!-- file: deltas/0.2.10/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.003` — gate technique/live final OrbitFlare Yellowstone
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
livraison : 0.2.10-pre.002-fix.001
|
||||
Cargo : 0.2.10-pre.2.fix.1
|
||||
commit : v0.2.10-pre.002-fix.001
|
||||
```
|
||||
|
||||
Identité de cette tranche :
|
||||
|
||||
```text
|
||||
livraison : 0.2.10-pre.003
|
||||
Cargo : 0.2.10-pre.3
|
||||
commit : v0.2.10-pre.003
|
||||
aucun tag prerelease
|
||||
```
|
||||
|
||||
Cette prerelease est exclusivement le couloir **technique/live final** de `0.2.10`. Elle ne porte aucune réconciliation README/USAGE/plan/validation, aucune finalisation CHANGELOG/ROADMAP et aucun prompt `0.2.11`.
|
||||
|
||||
## 2. Gate acquis de `pre.002-fix.001`
|
||||
|
||||
La validation opérateur reçue le 2026-08-25 ferme le défaut d'auth classifié dans `pre.002`.
|
||||
|
||||
Le gate déterministe est intégralement vert :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
python3 scripts/audit_rust_workspace_rules.py clean / 0 export candidate / clean
|
||||
python3 scripts/audit_markdown_tables.py PASS, 99 tables / 99 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test --workspace PASS
|
||||
Config unit 114/114 PASS
|
||||
Config ownership 5/5 PASS
|
||||
Config public API 15/15 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
|
||||
```
|
||||
|
||||
Le smoke OrbitFlare Devnet authentifié est également vert :
|
||||
|
||||
```text
|
||||
endpoint http://devnet.rpc.orbitflare.com:10000
|
||||
auth License Key ORBIT-* -> secret metadata x-token
|
||||
Subscribe PASS
|
||||
Slot > 0 PASS
|
||||
server Ping PASS
|
||||
close borné PASS
|
||||
résultat 1 passed / 0 failed
|
||||
```
|
||||
|
||||
Cette preuve ferme la branche de divergence heartbeat : OrbitFlare émet le `SubscribeUpdate::Ping` Yellowstone standard et le moteur N1 `0.2.9` possède déjà la réponse automatique déterministe. Aucun heartbeat provider, aucune façade OrbitFlare et aucune modification N1/N2 ne sont nécessaires.
|
||||
|
||||
## 3. Responsabilité de `pre.003`
|
||||
|
||||
`pre.003` ne matérialise aucun nouveau comportement. Elle rejoue et ferme les preuves techniques finales après le fix d'auth :
|
||||
|
||||
```text
|
||||
version Cargo prerelease synchronisée
|
||||
workspace complet sans warning
|
||||
invariants Rust/Markdown
|
||||
Config V3 OrbitFlare x-token non régressée
|
||||
Yellowstone N1/N2 non régressés
|
||||
HTTP 52 current + 14 historical non régressés
|
||||
WebSocket standard et Helius non régressés
|
||||
smoke live OrbitFlare Devnet final
|
||||
smokes PublicNode Mainnet/Testnet finaux si credentials opérateur disponibles
|
||||
graphes Cargo Transport et doublons inspectés
|
||||
aucun SDK OrbitFlare runtime
|
||||
aucune dépendance Transport -> Config
|
||||
```
|
||||
|
||||
Le payload de `pre.003` ne change ni code runtime, ni test, ni Config, ni schema, ni dépendance. Le seul changement technique est le signal de version Cargo obligatoire pour une prerelease non-fix.
|
||||
|
||||
## 4. Invariants à fermer
|
||||
|
||||
Les fichiers moteur Yellowstone restent hors modification :
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
|
||||
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
|
||||
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
|
||||
```
|
||||
|
||||
Le gate doit confirmer :
|
||||
|
||||
```text
|
||||
N1 gRPC provider-neutral inchangé
|
||||
N2 Yellowstone standard inchangé
|
||||
OrbitFlare composé par endpoint + provider + cluster + x-token Config V3
|
||||
X-ORBIT-KEY absent du data-plane Yellowstone
|
||||
License Key jamais exposée par Debug/CLI
|
||||
pas de format Config V4
|
||||
pas de provider proto
|
||||
pas de second raw Tonic client
|
||||
pas de orbitflare-sdk runtime
|
||||
```
|
||||
|
||||
## 5. Gate déterministe final
|
||||
|
||||
Après application de `pre.003` :
|
||||
|
||||
```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.10
|
||||
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
|
||||
```
|
||||
|
||||
Seuils minimaux attendus :
|
||||
|
||||
```text
|
||||
Config unit 114
|
||||
Config ownership 5
|
||||
Config public API 15
|
||||
Transport unit 383
|
||||
Transport public API 49
|
||||
Transport release completeness 43
|
||||
Transport doctests 4
|
||||
workspace dependency canary 3
|
||||
```
|
||||
|
||||
Toute régression imputable à KSP ouvre `pre.003-fix.NNN`. Elle ne doit pas être repoussée dans le couloir documentaire `pre.004`.
|
||||
|
||||
## 6. Smoke OrbitFlare Devnet final
|
||||
|
||||
Saisir la License Key sans l'inscrire dans l'historique :
|
||||
|
||||
```bash
|
||||
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY; echo
|
||||
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" \
|
||||
| cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||
unset ORBITFLARE_LICENSE_KEY
|
||||
```
|
||||
|
||||
Attendu :
|
||||
|
||||
```text
|
||||
orbitflare_devnet_yellowstone_streams_slots_and_emits_standard_server_ping_with_license_x_token ... ok
|
||||
1 passed
|
||||
0 failed
|
||||
0 ignored
|
||||
```
|
||||
|
||||
Le smoke doit encore prouver dans une même session bornée :
|
||||
|
||||
```text
|
||||
auth x-token acceptée
|
||||
Subscribe standard ouvert
|
||||
Slot non nul reçu
|
||||
SubscribeUpdate::Ping standard observé
|
||||
fermeture bornée
|
||||
```
|
||||
|
||||
Un incident externe OrbitFlare/réseau démontré comme tel est documenté comme `EXTERNAL BLOCK`. Un échec de mapping, metadata, moteur, protocol ou fermeture imputable à KSP ouvre un fix technique.
|
||||
|
||||
## 7. PublicNode et graphes finaux
|
||||
|
||||
Les smokes PublicNode Mainnet/Testnet restent les canaris provider du standard livré par `0.2.9`. Si les deux credentials opérateur sont disponibles pendant ce gate, les rejouer :
|
||||
|
||||
```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 lorsque rejoué :
|
||||
|
||||
```text
|
||||
2 passed
|
||||
0 failed
|
||||
0 ignored
|
||||
```
|
||||
|
||||
Inspecter ensuite :
|
||||
|
||||
```bash
|
||||
cargo tree -p ksp-onchain-transport-lib
|
||||
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||
cargo tree --duplicates
|
||||
```
|
||||
|
||||
Points obligatoires :
|
||||
|
||||
```text
|
||||
yellowstone-grpc-proto 12.6.x sur la stack attendue
|
||||
Tonic/Prost KSP-owned sans seconde génération incompatible
|
||||
aucun yellowstone-grpc-client runtime
|
||||
aucun orbitflare-sdk runtime
|
||||
aucune dépendance ksp-onchain-transport-lib -> ksp-config-lib
|
||||
aucun tracing direct Transport
|
||||
aucun nouveau doublon introduit par 0.2.10
|
||||
```
|
||||
|
||||
Les smokes PublicNode ne deviennent pas un blocker si leurs credentials ne sont simplement pas disponibles au moment du gate : leur non-régression déterministe reste couverte par les suites Transport/Config et le code live n'a pas changé. En revanche, tout échec effectivement observé lors d'un rerun doit être classifié, jamais ignoré.
|
||||
|
||||
## 8. Critère de fermeture technique
|
||||
|
||||
`pre.003` est fermée lorsque :
|
||||
|
||||
```text
|
||||
gate déterministe final vert
|
||||
OrbitFlare Devnet Subscribe -> Slot + Ping live PASS
|
||||
graphes Cargo finaux inspectés
|
||||
N1 inchangé
|
||||
N2 inchangé
|
||||
Config V3 x-token cohérente et redacted
|
||||
aucun provider overlay nécessaire
|
||||
aucun heartbeat provider nécessaire
|
||||
aucun SDK OrbitFlare ajouté
|
||||
aucun défaut technique reporté vers pre.004
|
||||
```
|
||||
|
||||
Une fois ce gate acquis, la prochaine tranche est obligatoirement :
|
||||
|
||||
```text
|
||||
0.2.10-pre.004 — réconciliation documentaire finale
|
||||
```
|
||||
|
||||
Elle portera plan, validation, README/USAGE et références durables. Elle ne doit pas porter de nouveau correctif technique.
|
||||
|
||||
## 9. Modifications de cette livraison
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
# version: 264 -> 265
|
||||
workspace.package.version: 0.2.10-pre.2.fix.1 -> 0.2.10-pre.3
|
||||
|
||||
deltas/0.2.10/pre.003.md
|
||||
ajout du présent gate technique/live final
|
||||
```
|
||||
|
||||
Aucun autre fichier n'est modifié.
|
||||
128
deltas/0.2.10/pre.004.md
Normal file
128
deltas/0.2.10/pre.004.md
Normal file
@@ -0,0 +1,128 @@
|
||||
<!-- file: deltas/0.2.10/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.004` — réconciliation documentaire finale OrbitFlare Yellowstone
|
||||
|
||||
## 1. Base
|
||||
|
||||
```text
|
||||
0.2.10-pre.003
|
||||
```
|
||||
|
||||
Cette tranche est exclusivement le couloir de réconciliation documentaire finale. Le gate technique/live de `pre.003` est vert ; aucun correctif runtime, Config exécutable, test, schema, dependency ou feature n'est introduit ici.
|
||||
|
||||
## 2. Version technique
|
||||
|
||||
Comme toute prerelease non-fix, la version Cargo est synchronisée mécaniquement :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10-pre.4
|
||||
```
|
||||
|
||||
Aucune sémantique runtime ne change.
|
||||
|
||||
## 3. Preuve technique réconciliée
|
||||
|
||||
Le gate opérateur `pre.003` du 2026-08-25 est fermé avec :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
Rust workspace audit PASS / 0 export candidate
|
||||
Markdown audit PASS / 99 tables / 100 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
Config unit 114/114 PASS
|
||||
Config ownership 5/5 PASS
|
||||
Config public API 15/15 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
|
||||
OrbitFlare Devnet Subscribe + Slot + Ping 1/1 PASS, 5.19 s
|
||||
```
|
||||
|
||||
Les graphes Cargo direct Transport, doublons Transport et doublons workspace ont été inspectés. La pile reste :
|
||||
|
||||
```text
|
||||
yellowstone-grpc-proto 12.6.0
|
||||
tonic / tonic-prost 0.14.6
|
||||
prost / prost-types 0.14.4
|
||||
```
|
||||
|
||||
Aucun `yellowstone-grpc-client` runtime, aucun `orbitflare-sdk` runtime et aucune dépendance `Transport -> Config` n'ont été ajoutés.
|
||||
|
||||
## 4. OrbitFlare final
|
||||
|
||||
Contrat durable réconcilié :
|
||||
|
||||
```text
|
||||
Devnet endpoint http://devnet.rpc.orbitflare.com:10000
|
||||
auth License Key ORBIT-* -> secret metadata x-token
|
||||
Config secret KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN
|
||||
Subscribe standard Yellowstone N2
|
||||
live Slot non nul + server Ping
|
||||
N1 inchangé
|
||||
N2 inchangé
|
||||
provider facade non nécessaire
|
||||
heartbeat N3 non nécessaire
|
||||
Config V4 non nécessaire
|
||||
```
|
||||
|
||||
Le premier échec `pre.002` sans metadata reste conservé dans son delta historique comme caractérisation ayant conduit au fix d'auth `pre.002-fix.001`.
|
||||
|
||||
## 5. Documents réconciliés
|
||||
|
||||
```text
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
docs/000-README.md
|
||||
```
|
||||
|
||||
Les anciennes affirmations `PENDING` sont remplacées par les résultats `pre.003` réellement observés. README/USAGE documentent désormais le profil OrbitFlare Devnet et son smoke live durable.
|
||||
|
||||
## 6. Frontière avec `pre.005`
|
||||
|
||||
Cette tranche ne touche pas :
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
prompt suivant
|
||||
```
|
||||
|
||||
La décision sur la release suivante est prise pendant cette réconciliation, sans générer encore son prompt : Helius LaserStream gRPC est reporté dans les TODO Yellowstone sans numéro de release faute d'accès live raisonnable ; la séquence active avance et `0.2.11` devient `off-chain price transport`, suivie de `0.2.12` Price Desk + intégration Wallet Desk, `0.2.13` interface/wire foundation et `0.2.14` program-api foundation. Le futur `ROADMAP.md` de `pre.005` devra refléter exactement ce report et cette renumérotation.
|
||||
|
||||
Une fois cette décision prise, `pre.005` reste strictement publication-minimal : prompt suivant + `CHANGELOG.md` + `ROADMAP.md` + signal Cargo + delta.
|
||||
|
||||
Si un défaut technique de `0.2.10` est découvert après ce gate documentaire, il ne doit pas être glissé dans `pre.005`; ouvrir une nouvelle prerelease du couloir approprié puis rejouer les tranches finales nécessaires.
|
||||
|
||||
## 7. Fichiers modifiés/ajoutés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
docs/000-README.md
|
||||
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||
crates/ksp-onchain-transport-lib/README.md
|
||||
crates/ksp-onchain-transport-lib/USAGE.md
|
||||
deltas/0.2.10/pre.004.md
|
||||
```
|
||||
|
||||
## 8. 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.10
|
||||
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 : aucune surface technique ou dépendance n'est modifiée.
|
||||
132
deltas/0.2.10/pre.005.md
Normal file
132
deltas/0.2.10/pre.005.md
Normal file
@@ -0,0 +1,132 @@
|
||||
<!-- file: deltas/0.2.10/pre.005.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-pre.005` — préparation de publication minimale
|
||||
|
||||
## 1. Base
|
||||
|
||||
```text
|
||||
0.2.10-pre.004
|
||||
```
|
||||
|
||||
Cette tranche est exclusivement le couloir de préparation de publication. Elle ne modifie aucun code, test, schema, config, README, USAGE, plan, validation ou règle normative.
|
||||
|
||||
## 2. Gate `pre.003` et réconciliation `pre.004`
|
||||
|
||||
Le gate technique final `pre.003` a confirmé :
|
||||
|
||||
```text
|
||||
Rust workspace audit PASS
|
||||
Markdown audit PASS
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test --workspace PASS
|
||||
OrbitFlare live Subscribe -> Slot+Ping PASS en 5.19 s
|
||||
graphes Cargo inspectés
|
||||
```
|
||||
|
||||
`pre.004` a ensuite réconcilié les documents durables : plan/validation OrbitFlare, README/USAGE Transport, index documentaire et séquence fonctionnelle. Helius LaserStream gRPC a été reporté dans les TODO faute d'accès live raisonnable sans abonnement payant, sans remise en cause de sa compatibilité wire Yellowstone largement observée.
|
||||
|
||||
## 3. Version technique
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10-pre.5
|
||||
```
|
||||
|
||||
La modification est mécanique ; aucune sémantique runtime ne change.
|
||||
|
||||
## 4. `CHANGELOG.md`
|
||||
|
||||
Le changelog stable est préparé pour `0.2.10 — OrbitFlare Yellowstone gRPC` avec les faits consolidés :
|
||||
|
||||
```text
|
||||
N1/N2 Yellowstone inchangés
|
||||
Config V3 orbitflare_devnet
|
||||
x-token = License Key via secret_metadata
|
||||
premier live sans metadata -> Unauthenticated
|
||||
fix auth documenté
|
||||
live Subscribe -> Slot + standard Ping PASS
|
||||
second live final PASS
|
||||
aucun heartbeat/provider SDK/Config V4 ajouté
|
||||
workspace + graphes finaux verts
|
||||
```
|
||||
|
||||
## 5. `ROADMAP.md`
|
||||
|
||||
`0.2.10` passe à l'état réalisé. Helius LaserStream gRPC quitte la séquence numérotée et rejoint les TODO Yellowstone. La séquence active devient :
|
||||
|
||||
```text
|
||||
0.2.11 off-chain price transport
|
||||
0.2.12 Price Desk + intégration prix Wallet Desk
|
||||
0.2.13 interface/wire foundation
|
||||
0.2.14 program-api foundation
|
||||
```
|
||||
|
||||
## 6. Audit préparatoire de la future `0.2.11`
|
||||
|
||||
Avant de rédiger le prompt suivant, un audit externe rapide au 2026-08-25 a comparé plusieurs familles de sources :
|
||||
|
||||
```text
|
||||
CoinGecko agrégateur, REST Demo, multi quote USD/EUR
|
||||
Jupiter V3 prix Solana heuristique issu des swaps, USD seulement
|
||||
Pyth Hermes oracle, REST/SSE, publish_time/confidence, auth en transition immédiate
|
||||
Birdeye market data Solana, spot/batch/historique, API key
|
||||
DexScreener prix DEX par pair/pool, accès public courant
|
||||
```
|
||||
|
||||
Ce snapshot ne choisit pas le provider. Il démontre surtout que les sémantiques diffèrent et que `0.2.11-pre.001` doit auditer/choisir explicitement le contrat prix, le modèle numérique, la fraîcheur et le provider initial avant code lourd.
|
||||
|
||||
Pyth annonce un changement d'authentification le 2026-08-26 à 16:00 UTC ; le prompt exige donc un réaudit immédiat de Pyth à l'ouverture et interdit de figer le snapshot de `pre.005`.
|
||||
|
||||
## 7. Prompt `0.2.11`
|
||||
|
||||
Ajout de :
|
||||
|
||||
```text
|
||||
prompts/016-V0_2_11_START_PROMPT.md
|
||||
```
|
||||
|
||||
Le prompt ouvre `0.2.11 — Off-chain price transport` uniquement depuis `v0.2.10` et impose `pre.001 = lecture + audit actuel providers + matrice sémantique + numeric model + threat model + sizing + planification`.
|
||||
|
||||
Le scope initial reste volontairement borné :
|
||||
|
||||
```text
|
||||
ksp-offchain-transport-lib
|
||||
prix SOL/USD + SOL/EUR
|
||||
premier provider live-testable sans abonnement payant dédié
|
||||
Config -> Off-chain Transport
|
||||
pas de Price Desk
|
||||
pas de Wallet Desk
|
||||
pas d'OHLCV/historique/quotes/metadata/multi-provider complexe
|
||||
```
|
||||
|
||||
## 8. Fichiers modifiés/ajoutés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
prompts/016-V0_2_11_START_PROMPT.md
|
||||
deltas/0.2.10/pre.005.md
|
||||
```
|
||||
|
||||
Aucun autre fichier ne doit changer dans cette tranche.
|
||||
|
||||
## 9. Gate opérateur
|
||||
|
||||
Comme cette 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.10
|
||||
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.
|
||||
|
||||
## 10. Suite
|
||||
|
||||
Si ce gate est vert, `0.2.10-rel.001` est la seule étape restante. Elle est limitée à la mécanique de publication stable : version Cargo `0.2.10`, delta `rel.001`, commit final et tag stable `v0.2.10`, sans rattrapage technique ou documentaire.
|
||||
197
deltas/0.2.10/rel.001.md
Normal file
197
deltas/0.2.10/rel.001.md
Normal file
@@ -0,0 +1,197 @@
|
||||
<!-- file: deltas/0.2.10/rel.001.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.10-rel.001` — publication stable OrbitFlare Yellowstone gRPC
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base directe attendue :
|
||||
|
||||
```text
|
||||
0.2.10-pre.005
|
||||
workspace.package.version = 0.2.10-pre.5
|
||||
```
|
||||
|
||||
Commit attendu pour cette livraison :
|
||||
|
||||
```text
|
||||
v0.2.10-rel.001
|
||||
```
|
||||
|
||||
Tag stable attendu après validation :
|
||||
|
||||
```text
|
||||
v0.2.10
|
||||
```
|
||||
|
||||
## 2. Responsabilité de `rel.001`
|
||||
|
||||
Cette livraison effectue uniquement la mécanique de publication stable définie par le cycle KSP.
|
||||
|
||||
Elle :
|
||||
|
||||
- passe `workspace.package.version` de `0.2.10-pre.5` à `0.2.10` ;
|
||||
- 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. Preuves techniques acquises avant publication
|
||||
|
||||
Le gate technique/live final `0.2.10-pre.003` a été validé le **25 août 2026** avec :
|
||||
|
||||
```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
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-config-lib PASS
|
||||
cargo test -p ksp-onchain-transport-lib PASS
|
||||
cargo test -p ksp-core-lib --test workspace_dependencies PASS
|
||||
cargo test --workspace PASS
|
||||
```
|
||||
|
||||
Les suites directement concernées restent notamment :
|
||||
|
||||
```text
|
||||
Config unit 114 passed
|
||||
Transport unit 383 passed
|
||||
Transport public API 49 passed
|
||||
Transport release completeness 43 passed
|
||||
Transport doctests 4 passed
|
||||
```
|
||||
|
||||
Le smoke live final OrbitFlare a également été rejoué en `pre.003` :
|
||||
|
||||
```text
|
||||
OrbitFlare Devnet Yellowstone
|
||||
x-token = License Key
|
||||
Subscribe -> Slot + standard server Ping
|
||||
1/1 PASS en 5.19 s
|
||||
```
|
||||
|
||||
Les graphes Cargo finaux ont été inspectés dans le même couloir. `pre.004` et `pre.005` n'ont ensuite modifié aucune source runtime, dépendance, config ou schema.
|
||||
|
||||
## 4. Version stable publiée
|
||||
|
||||
La version Cargo devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.10
|
||||
```
|
||||
|
||||
La release stable publiée est :
|
||||
|
||||
```text
|
||||
0.2.10 — OrbitFlare Yellowstone gRPC
|
||||
```
|
||||
|
||||
Les acquis fonctionnels et documentaires finalisés avant `rel.001` comprennent :
|
||||
|
||||
```text
|
||||
réutilisation stricte du moteur N1 Yellowstone gRPC existant
|
||||
réutilisation stricte du contrat N2 Yellowstone standard existant
|
||||
aucune modification provider du moteur gRPC
|
||||
profil Config V3 orbitflare_devnet
|
||||
endpoint Devnet http://devnet.rpc.orbitflare.com:10000
|
||||
authentification gRPC x-token via License Key secrète
|
||||
premier smoke sans metadata -> Unauthenticated, caractérisé puis corrigé
|
||||
live authentifié Subscribe -> Slot + standard Ping PASS
|
||||
aucun heartbeat provider supplémentaire
|
||||
aucun SDK OrbitFlare ajouté
|
||||
aucun Config V4
|
||||
```
|
||||
|
||||
La release confirme donc qu'OrbitFlare compose au-dessus de N1/N2 par configuration provider et metadata secrète, sans divergence protocolaire nécessitant une façade gRPC dédiée.
|
||||
|
||||
## 5. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
deltas/0.2.10/rel.001.md
|
||||
```
|
||||
|
||||
## 6. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
```
|
||||
|
||||
## 7. Fichiers supprimés
|
||||
|
||||
```text
|
||||
aucun
|
||||
```
|
||||
|
||||
## 8. Validations de préparation du delta
|
||||
|
||||
Contrôles statiques de l'overlay :
|
||||
|
||||
```text
|
||||
Cargo.toml version 0.2.10
|
||||
payload rel.001 2 fichiers exactement
|
||||
aucun fichier de rattrapage présent
|
||||
```
|
||||
|
||||
Aucune nouvelle compilation n'est requise pour construire l'archive elle-même. Le gate stable final doit toutefois être exécuté après application du delta et avant le commit/tag.
|
||||
|
||||
## 9. Validation stable après application
|
||||
|
||||
Exécuter avant publication :
|
||||
|
||||
```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.10
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le changement étant limité au signal de version Cargo et au présent delta :
|
||||
|
||||
- aucun smoke live OrbitFlare n'est à rejouer si l'overlay appliqué est exactement `0.2.10-rel.001` ;
|
||||
- aucun `cargo tree` n'est à rejouer ;
|
||||
- aucun smoke PublicNode n'est à rejouer.
|
||||
|
||||
## 10. Commit et tag stable
|
||||
|
||||
Après succès du gate stable :
|
||||
|
||||
```text
|
||||
commit : v0.2.10-rel.001
|
||||
tag : v0.2.10
|
||||
```
|
||||
|
||||
Aucun tag intermédiaire `rel.001` n'est requis.
|
||||
|
||||
Commandes Git typiques :
|
||||
|
||||
```bash
|
||||
git add Cargo.toml deltas/0.2.10/rel.001.md
|
||||
git commit -m "v0.2.10-rel.001"
|
||||
git tag v0.2.10
|
||||
git push
|
||||
git push origin v0.2.10
|
||||
```
|
||||
|
||||
## 11. Suite
|
||||
|
||||
Après publication du tag stable `v0.2.10`, ouvrir :
|
||||
|
||||
```text
|
||||
0.2.11-pre.001 — Off-chain price transport
|
||||
```
|
||||
|
||||
avec :
|
||||
|
||||
```text
|
||||
prompts/016-V0_2_11_START_PROMPT.md
|
||||
```
|
||||
|
||||
`pre.001` reste une tranche d'audit actuel des providers, sémantiques de prix, modèle numérique, fraîcheur/provenance, threat model et sizing avant toute implémentation lourde.
|
||||
|
||||
Helius LaserStream gRPC reste reporté dans les TODO Yellowstone jusqu'à disponibilité d'un accès live pertinent ; il ne fait plus partie de la séquence numérotée active.
|
||||
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 -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# `.kspwallet` V1 — spécification du format natif Wallet KSP
|
||||
|
||||
@@ -399,29 +399,29 @@ Les champs binaires sont les **octets décodés** du Base64url, jamais le texte
|
||||
|
||||
| Tag hex | Champ |
|
||||
|--------:|----------------------------------------------------|
|
||||
| `0001` | magic |
|
||||
| `0002` | format_version |
|
||||
| `0003` | owner_auth_public_key |
|
||||
| `0010` | view enabled |
|
||||
| `0011` | rôle VIEW littéral `view` |
|
||||
| `0012` | view slot_id ; longueur zéro lorsque VIEW disabled |
|
||||
| `0100` | slot_id |
|
||||
| `0101` | slot role |
|
||||
| `0102` | KDF algorithm |
|
||||
| `0103` | KDF version |
|
||||
| `0104` | KDF memory_kib |
|
||||
| `0105` | KDF iterations |
|
||||
| `0106` | KDF parallelism |
|
||||
| `0107` | KDF salt |
|
||||
| `0108` | wrap algorithm |
|
||||
| `0109` | wrap nonce |
|
||||
| `010A` | wrap ciphertext |
|
||||
| `0200` | compartment kind |
|
||||
| `0201` | compartment payload version |
|
||||
| `0202` | compartment algorithm |
|
||||
| `0203` | compartment nonce |
|
||||
| `0204` | compartment ciphertext |
|
||||
| `0500` | state-signature algorithm |
|
||||
| `0001` | magic |
|
||||
| `0002` | format_version |
|
||||
| `0003` | owner_auth_public_key |
|
||||
| `0010` | view enabled |
|
||||
| `0011` | rôle VIEW littéral `view` |
|
||||
| `0012` | view slot_id ; longueur zéro lorsque VIEW disabled |
|
||||
| `0100` | slot_id |
|
||||
| `0101` | slot role |
|
||||
| `0102` | KDF algorithm |
|
||||
| `0103` | KDF version |
|
||||
| `0104` | KDF memory_kib |
|
||||
| `0105` | KDF iterations |
|
||||
| `0106` | KDF parallelism |
|
||||
| `0107` | KDF salt |
|
||||
| `0108` | wrap algorithm |
|
||||
| `0109` | wrap nonce |
|
||||
| `010A` | wrap ciphertext |
|
||||
| `0200` | compartment kind |
|
||||
| `0201` | compartment payload version |
|
||||
| `0202` | compartment algorithm |
|
||||
| `0203` | compartment nonce |
|
||||
| `0204` | compartment ciphertext |
|
||||
| `0500` | state-signature algorithm |
|
||||
|
||||
Les tags ne remplacent pas l'ordre normatif ; **l'ordre ci-dessous est obligatoire**.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/formats/KSPWALLET_V2.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# `.kspwallet` V2 — spécification du wire binaire natif KSP
|
||||
|
||||
@@ -104,14 +104,14 @@ Une implémentation conforme doit vérifier les bornes **avant** toute allocatio
|
||||
|
||||
Le début de fichier est strictement :
|
||||
|
||||
| Ordre | Champ | Taille | Valeur / règle |
|
||||
| Ordre | Champ | Taille | Valeur / règle |
|
||||
|------:|---------------------------|-----------------:|------------------------------------------------|
|
||||
| 1 | `magic` | 9 | ASCII exact `KSPWALLET` |
|
||||
| 2 | `format_version` | 2 | `0x0002` |
|
||||
| 3 | `document_length` | 4 | longueur totale exacte du fichier |
|
||||
| 4 | `flags` | 2 | bit 0 = VIEW activé ; tous les autres bits = 0 |
|
||||
| 5 | `owner_auth_public_key` | 32 | clé publique Ed25519 OWNER |
|
||||
| 6 | `view_descriptor.slot_id` | 16 conditionnels | présent uniquement si `flags & 0x0001 != 0` |
|
||||
| 1 | `magic` | 9 | ASCII exact `KSPWALLET` |
|
||||
| 2 | `format_version` | 2 | `0x0002` |
|
||||
| 3 | `document_length` | 4 | longueur totale exacte du fichier |
|
||||
| 4 | `flags` | 2 | bit 0 = VIEW activé ; tous les autres bits = 0 |
|
||||
| 5 | `owner_auth_public_key` | 32 | clé publique Ed25519 OWNER |
|
||||
| 6 | `view_descriptor.slot_id` | 16 conditionnels | présent uniquement si `flags & 0x0001 != 0` |
|
||||
|
||||
Offsets fixes avant le descripteur conditionnel :
|
||||
|
||||
@@ -154,20 +154,20 @@ Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalit
|
||||
|
||||
Chaque key slot est encodé ainsi :
|
||||
|
||||
| Champ | Taille | Valeur / règle |
|
||||
| Champ | Taille | Valeur / règle |
|
||||
|--------------------------|---------:|-----------------------------------------|
|
||||
| `role` | 1 | `0x01` OWNER, `0x02` VIEW |
|
||||
| `slot_id` | 16 | identifiant binaire exact |
|
||||
| `kdf_algorithm` | 1 | `0x01` Argon2id |
|
||||
| `kdf_version` | 4 | `19` |
|
||||
| `memory_kib` | 4 | `1..1 048 576`, et `>= parallelism * 8` |
|
||||
| `iterations` | 4 | `1..64` |
|
||||
| `parallelism` | 4 | `1..64` |
|
||||
| `salt_length` | 1 | `16..64` |
|
||||
| `role` | 1 | `0x01` OWNER, `0x02` VIEW |
|
||||
| `slot_id` | 16 | identifiant binaire exact |
|
||||
| `kdf_algorithm` | 1 | `0x01` Argon2id |
|
||||
| `kdf_version` | 4 | `19` |
|
||||
| `memory_kib` | 4 | `1..1 048 576`, et `>= parallelism * 8` |
|
||||
| `iterations` | 4 | `1..64` |
|
||||
| `parallelism` | 4 | `1..64` |
|
||||
| `salt_length` | 1 | `16..64` |
|
||||
| `salt` | variable | exactement `salt_length` octets |
|
||||
| `wrap_algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
|
||||
| `wrap_nonce` | 24 | nonce exact |
|
||||
| `wrap_ciphertext_length` | 2 | `16..4096` |
|
||||
| `wrap_algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
|
||||
| `wrap_nonce` | 24 | nonce exact |
|
||||
| `wrap_ciphertext_length` | 2 | `16..4096` |
|
||||
| `wrap_ciphertext` | variable | exactement la longueur déclarée |
|
||||
|
||||
Ordre obligatoire :
|
||||
@@ -189,13 +189,13 @@ Toute divergence est invalide avant KDF/déchiffrement.
|
||||
|
||||
Chaque compartiment est encodé :
|
||||
|
||||
| Champ | Taille | Valeur / règle |
|
||||
| Champ | Taille | Valeur / règle |
|
||||
|---------------------|---------:|------------------------------------------------------|
|
||||
| `kind` | 1 | `0x01` OWNER-CONTROL, `0x02` METADATA, `0x03` SECRET |
|
||||
| `payload_version` | 4 | `1` pour le profil initial V2 |
|
||||
| `algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
|
||||
| `nonce` | 24 | nonce exact |
|
||||
| `ciphertext_length` | 4 | longueur exacte |
|
||||
| `kind` | 1 | `0x01` OWNER-CONTROL, `0x02` METADATA, `0x03` SECRET |
|
||||
| `payload_version` | 4 | `1` pour le profil initial V2 |
|
||||
| `algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
|
||||
| `nonce` | 24 | nonce exact |
|
||||
| `ciphertext_length` | 4 | longueur exacte |
|
||||
| `ciphertext` | variable | ciphertext + tag Poly1305 |
|
||||
|
||||
Bornes initiales :
|
||||
@@ -216,8 +216,8 @@ La fin du document est :
|
||||
|
||||
| Champ | Taille | Valeur / règle |
|
||||
|-----------------------------|-------:|---------------------------|
|
||||
| `state_signature.algorithm` | 1 | `0x01` Ed25519 |
|
||||
| `state_signature.signature` | 64 | signature detached exacte |
|
||||
| `state_signature.algorithm` | 1 | `0x01` Ed25519 |
|
||||
| `state_signature.signature` | 64 | signature detached exacte |
|
||||
|
||||
Aucun octet ne peut suivre ces 65 octets.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/000-README.md -->
|
||||
<!-- version: 59 -->
|
||||
<!-- version: 61 -->
|
||||
|
||||
# Plans KSP
|
||||
|
||||
@@ -24,6 +24,8 @@ 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`.
|
||||
- [`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`.
|
||||
- [`016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md`](016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md) — plan historique clôturé de la release stable `0.2.9 — Yellowstone gRPC standard/provider-neutral`, publiée par `rel.001`; il fixe le moteur N1, le standard N2, Config V3, le replay prudent et la validation PublicNode Mainnet/Testnet.
|
||||
- [`017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md`](017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md) — plan actif de `0.2.10 — OrbitFlare Yellowstone gRPC`, ouvert par `pre.001`; il confirme le Devnet gRPC gratuit, rend N1/N2 immuables pour la release, classe auth/heartbeat/limits et dimensionne un chemin standard prioritaire sans code provider inutile.
|
||||
|
||||
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 -->
|
||||
<!-- version: 85 -->
|
||||
<!-- version: 89 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -37,7 +37,7 @@ Séquence par défaut :
|
||||
0.1.4 ksp-app-config-desk
|
||||
```
|
||||
|
||||
`0.1.1`, `0.1.2`, `0.1.3`, `0.1.4` et `0.2.0` à `0.2.5` sont désormais des releases stables.
|
||||
`0.1.1`, `0.1.2`, `0.1.3`, `0.1.4` et `0.2.0` à `0.2.9` sont désormais des releases stables.
|
||||
|
||||
`0.1.4 — ksp-app-config-desk` établit le modèle de référence des futures applications Tauri KSP sans déplacer la logique Config dans l'application. Sa matrice finale a été validée avant `rel.001`, avec le build Tauri exécuté en dernière opération.
|
||||
|
||||
@@ -363,11 +363,12 @@ Par défaut :
|
||||
0.2.6 Wallet Desk
|
||||
0.2.7 standard Solana WebSocket
|
||||
0.2.8 Helius LaserStream WebSocket
|
||||
0.2.9 Yellowstone gRPC standard foundation
|
||||
0.2.10 off-chain price transport
|
||||
0.2.11 price visualization desk + intégration prix dans Wallet Desk
|
||||
0.2.12 interface/wire foundation
|
||||
0.2.13 program-api foundation
|
||||
0.2.9 Yellowstone gRPC engine + Solana standard + PublicNode
|
||||
0.2.10 OrbitFlare Yellowstone gRPC
|
||||
0.2.11 off-chain price transport
|
||||
0.2.12 price visualization desk + intégration prix dans Wallet Desk
|
||||
0.2.13 interface/wire foundation
|
||||
0.2.14 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.
|
||||
@@ -471,29 +472,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.
|
||||
|
||||
### `0.2.9` — Yellowstone gRPC standard
|
||||
### `0.2.9` — Yellowstone gRPC engine + standard Solana + PublicNode
|
||||
|
||||
Mission : introduire un backend Yellowstone standard/provider-neutral.
|
||||
Mission accomplie : `0.2.9-rel.001` publie le moteur Yellowstone N1, le standard Solana N2, Config Transport V3 et la première intégration provider-neutral PublicNode. Les smokes Mainnet et Testnet ont validé `Subscribe -> Slot` avec metadata `x-token` secrète sans façade PublicNode spécifique.
|
||||
|
||||
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.
|
||||
Cette release devient la fondation stable des providers suivants. Le moteur physique et la surface standard ne sont pas redéfinis par une release provider : une divergence provider se compose au-dessus, ou devient un blocker architectural explicite si elle ne peut pas être composée proprement.
|
||||
|
||||
Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices.
|
||||
### `0.2.10` — OrbitFlare Yellowstone gRPC
|
||||
|
||||
### `0.2.10` / `0.2.11` — Off-chain price + app
|
||||
Mission active : obtenir en priorité un provider Yellowstone gratuit sur **Devnet** pour les tests futurs, puis matérialiser uniquement les divergences OrbitFlare réellement prouvées. Le plan Free actuel annonce `gRPC Devnet only` et le CLI documente `http://devnet.rpc.orbitflare.com:10000`.
|
||||
|
||||
`0.2.10` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
|
||||
`0.2.10-pre.001` fixe un invariant renforcé : N1 gRPC et N2 Yellowstone restent immuables. L'auth Customer API, le token gRPC éventuel, le heartbeat, les quotas et les capabilities sont classifiés séparément. Le premier smoke doit tenter le standard N2 sans secret sur Devnet, observer un `Slot` et caractériser le `Ping` serveur Yellowstone avant toute façade/policy provider.
|
||||
|
||||
`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é.
|
||||
Le plan actif est [`017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md`](017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md) et la matrice active [`../validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md`](../validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md).
|
||||
|
||||
### TODO/IDEAS — providers Yellowstone en attente
|
||||
|
||||
Aucune release n'est réservée pour :
|
||||
|
||||
```text
|
||||
TODO Helius LaserStream gRPC — réauditer lorsque l'accès live gRPC est disponible
|
||||
TODO eRPC
|
||||
TODO Triton
|
||||
TODO Alchemy
|
||||
TODO QuickNode
|
||||
TODO Chainstack
|
||||
IDEAS Tatum
|
||||
IDEAS Shyft
|
||||
IDEAS Solinfra
|
||||
IDEAS NodeFlare
|
||||
IDEAS autres providers à réauditer
|
||||
```
|
||||
|
||||
Helius LaserStream gRPC est explicitement reporté : l'audit 2026-08-25 indique une compatibilité wire Yellowstone élevée et une intégration KSP probablement légère au-dessus de N1/N2, mais aucun accès live Helius gRPC n'est actuellement disponible sans plan payant. Il ne reçoit donc plus de numéro de release tant que l'auth, les endpoints, le Subscribe, le Ping, le replay/from_slot et les erreurs provider ne peuvent pas être validés live. Les extensions Helius non standard, notamment les preprocessed transactions, restent un sujet provider séparé.
|
||||
|
||||
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.11` / `0.2.12` — Off-chain price + app
|
||||
|
||||
`0.2.11` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
|
||||
|
||||
`0.2.12` 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.
|
||||
|
||||
### `0.2.12` — Interface foundation
|
||||
### `0.2.13` — Interface foundation
|
||||
|
||||
`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.
|
||||
|
||||
### `0.2.13` — Program API foundation
|
||||
### `0.2.14` — Program API foundation
|
||||
|
||||
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 -->
|
||||
<!-- version: 19 -->
|
||||
<!-- version: 20 -->
|
||||
|
||||
# Plan `0.1.3` — Configuration foundation
|
||||
|
||||
@@ -605,23 +605,23 @@ Décisions :
|
||||
|
||||
### 8.1 Écart `ksp-logging-lib 0.1.2` à fermer
|
||||
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
| Capacité | `0.1.2` | Requise par `std.logging.json` |
|
||||
|-----------------------------------|----------------:|-------------------------------------:|
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
| filtre global | oui | oui |
|
||||
| overrides par target | oui | oui |
|
||||
| lifecycle spans | oui | oui |
|
||||
| console stdout/stderr | oui | oui |
|
||||
| console enabled | via `Option` | oui explicite |
|
||||
| console ANSI configurable | non | oui |
|
||||
| format console configurable | non | oui |
|
||||
| plusieurs fichiers | non | oui |
|
||||
| rotation par fichier | un seul fichier | oui par sink |
|
||||
| format par fichier | non | oui |
|
||||
| filtre par sink/target | non | oui |
|
||||
| filtre par sink/domain | non | oui |
|
||||
| filtre par sink/niveau | non indépendant | oui |
|
||||
| hot reload transactionnel | oui | à conserver |
|
||||
| non-blocking/guards/drop counters | oui | à conserver et généraliser par sinks |
|
||||
|
||||
Ce tableau est un **gap identifié**, pas une invitation à déplacer Logging dans Config. La tranche qui le ferme modifie `ksp-logging-lib` uniquement dans son domaine propriétaire.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
|
||||
<!-- version: 28 -->
|
||||
<!-- version: 29 -->
|
||||
|
||||
# Plan `0.1.4` — `ksp-app-config-desk`
|
||||
|
||||
@@ -239,7 +239,7 @@ Chaque application Tauri desk KSP reçoit un couple de ports Vite/HMR propre. La
|
||||
|
||||
| Application | Port Vite HTTP | Port HMR |
|
||||
|-----------------------|---------------:|---------:|
|
||||
| `ksp-app-config-desk` | `1430` | `1431` |
|
||||
| `ksp-app-config-desk` | `1430` | `1431` |
|
||||
|
||||
Les applications suivantes incrémentent le couple de deux ports (`1432/1433`, puis `1434/1435`, etc.). Vite doit utiliser un port strict afin qu'une collision soit signalée au lieu de provoquer un basculement silencieux vers un autre port. Cette allocation permet de faire fonctionner simultanément plusieurs applications desk en mode développement.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- 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
|
||||
|
||||
@@ -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 |
|
||||
|-------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------|----------|---------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------|
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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 -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# Plan `0.2.5` — Wallet foundation
|
||||
|
||||
@@ -205,23 +205,23 @@ Les permissions/ACL et le contrôle de qui peut écrire le chemin relèvent du s
|
||||
|
||||
### 5.7 Matrice des garanties
|
||||
|
||||
| Garantie | Cryptographie du format | Types/capabilities KSP |
|
||||
| Garantie | Cryptographie du format | Types/capabilities KSP |
|
||||
|------------------------------------------------------------------------|-------------------------------------------------------:|-------------------------:|
|
||||
| confidentialité metadata verrouillées | oui | oui |
|
||||
| confidentialité secret Solana face à VIEW | oui | oui |
|
||||
| indépendance VIEW/OWNER | oui | oui |
|
||||
| VIEW ne signe pas | séparation de clés | oui |
|
||||
| VIEW ne modifie pas Pubkey/alias/notes via API | authentification OWNER des metadata | oui |
|
||||
| VIEW change son propre password | slot VIEW rewrappable sous la même capability metadata | oui, opération dédiée |
|
||||
| VIEW ne change pas password OWNER / activation VIEW / autres key slots | authentification OWNER de l'état de contrôle | oui |
|
||||
| metadata modifiées par VIEW détectées sous la même autorité OWNER | oui, `state_signature` | oui |
|
||||
| détection corruption/tampering partiel | oui | parsing strict |
|
||||
| signature Solana sans OWNER | impossible sous les primitives retenues | API absente hors OWNER |
|
||||
| export secret sans OWNER | secret non déverrouillable | API absente hors OWNER |
|
||||
| mutation Pubkey/alias/notes/OWNER-state sans OWNER | état non authentifiable | API absente hors OWNER |
|
||||
| no-clobber / atomic replace | non | propriété de persistence |
|
||||
| détection remplacement total par un autre wallet valide | hors garantie V1 | hors garantie V1 |
|
||||
| détection rollback total vers une copie valide | hors garantie V1 | hors garantie V1 |
|
||||
| confidentialité metadata verrouillées | oui | oui |
|
||||
| confidentialité secret Solana face à VIEW | oui | oui |
|
||||
| indépendance VIEW/OWNER | oui | oui |
|
||||
| VIEW ne signe pas | séparation de clés | oui |
|
||||
| VIEW ne modifie pas Pubkey/alias/notes via API | authentification OWNER des metadata | oui |
|
||||
| VIEW change son propre password | slot VIEW rewrappable sous la même capability metadata | oui, opération dédiée |
|
||||
| VIEW ne change pas password OWNER / activation VIEW / autres key slots | authentification OWNER de l'état de contrôle | oui |
|
||||
| metadata modifiées par VIEW détectées sous la même autorité OWNER | oui, `state_signature` | oui |
|
||||
| détection corruption/tampering partiel | oui | parsing strict |
|
||||
| signature Solana sans OWNER | impossible sous les primitives retenues | API absente hors OWNER |
|
||||
| export secret sans OWNER | secret non déverrouillable | API absente hors OWNER |
|
||||
| mutation Pubkey/alias/notes/OWNER-state sans OWNER | état non authentifiable | API absente hors OWNER |
|
||||
| no-clobber / atomic replace | non | propriété de persistence |
|
||||
| détection remplacement total par un autre wallet valide | hors garantie V1 | hors garantie V1 |
|
||||
| détection rollback total vers une copie valide | hors garantie V1 | hors garantie V1 |
|
||||
|
||||
Les ACL/permissions OS ne figurent volontairement pas dans cette matrice : elles ne participent pas au modèle de sécurité de `.kspwallet` V1.
|
||||
|
||||
@@ -388,9 +388,9 @@ Audit source notable : `solana-keypair 3.1.2` contient un bloc `unsafe` interne
|
||||
|
||||
| Primitive | Version publiée auditée | Décision V1 |
|
||||
|-----------|------------------------:|--------------------------------------------|
|
||||
| Argon2 | `0.5.3` | **retenu : Argon2id v19** |
|
||||
| scrypt | `0.12.0` | alternative maintenue, non ajoutée |
|
||||
| PBKDF2 | `0.13.0` | compatibilité/legacy seulement, non ajouté |
|
||||
| Argon2 | `0.5.3` | **retenu : Argon2id v19** |
|
||||
| scrypt | `0.12.0` | alternative maintenue, non ajoutée |
|
||||
| PBKDF2 | `0.13.0` | compatibilité/legacy seulement, non ajouté |
|
||||
|
||||
Les paramètres Argon2 de création ont été mesurés avec le benchmark opérateur de `pre.004` : `64 MiB / 3 / 1 = 1742 ms`, `128 MiB / 3 / 1 = 3459 ms`, `256 MiB / 3 / 1 = 6925 ms` sur la machine/profil testés le 2026-08-19. `pre.005` retient donc **64 MiB / 3 passes / 1 lane** comme profil initial de création KSP, avec un salt CSPRNG indépendant de 32 octets par slot. Ce choix n'est copié ni de bot3, ni d'un RFC, ni d'un default de crate. Le fichier sérialise tous les paramètres nécessaires afin qu'un ancien wallet conserve son profil historique même lorsque les defaults KSP seront durcis.
|
||||
|
||||
@@ -398,10 +398,10 @@ Le parseur impose des **bornes maximales** avant de lancer le KDF, afin qu'un fi
|
||||
|
||||
### 8.3 AEAD
|
||||
|
||||
| Primitive | Version publiée auditée | Décision V1 |
|
||||
| Primitive | Version publiée auditée | Décision V1 |
|
||||
|--------------------|--------------------------:|------------------|
|
||||
| XChaCha20-Poly1305 | `chacha20poly1305 0.11.0` | **retenu** |
|
||||
| AES-256-GCM-SIV | `aes-gcm-siv 0.12.0` | non retenu en V1 |
|
||||
| AES-256-GCM-SIV | `aes-gcm-siv 0.12.0` | non retenu en V1 |
|
||||
|
||||
XChaCha20-Poly1305 fournit une clé 256 bits et un nonce étendu 192 bits. Un nonce neuf est généré pour chaque wrapping/chiffrement. La crate RustCrypto documente un audit NCC Group sans constat significatif.
|
||||
|
||||
@@ -1107,20 +1107,20 @@ liens Markdown locaux audités
|
||||
|
||||
| Domaine | Taille | Risque principal |
|
||||
|-------------------------------|-------:|----------------------------------------------|
|
||||
| crate/API foundation | M | capability surface durable |
|
||||
| threat model | M | faux niveau de garantie |
|
||||
| format wire | L | strict parsing/versioning |
|
||||
| interop/test vectors | L | transcript exact multi-langages |
|
||||
| KDF/AEAD/key wrapping | L | paramètres + nonce/AAD |
|
||||
| VIEW/OWNER key slots | XL | indépendance et rotations |
|
||||
| auth crypto metadata niveau B | L/XL | clé admin + transcript + substitution limits |
|
||||
| persistence | L | no-clobber + crash semantics multi-OS |
|
||||
| signing | M | aucun secret getter |
|
||||
| password/key-slot rotation | L | rotation vs vraie révocation |
|
||||
| alias/notes | M | bornes + persistence |
|
||||
| import/export | M/L | extension sans secret API générale |
|
||||
| security/adversarial tests | XL | tamper/fault/diagnostics |
|
||||
| spec/README/USAGE | L | contrat externe autonome |
|
||||
| crate/API foundation | M | capability surface durable |
|
||||
| threat model | M | faux niveau de garantie |
|
||||
| format wire | L | strict parsing/versioning |
|
||||
| interop/test vectors | L | transcript exact multi-langages |
|
||||
| KDF/AEAD/key wrapping | L | paramètres + nonce/AAD |
|
||||
| VIEW/OWNER key slots | XL | indépendance et rotations |
|
||||
| auth crypto metadata niveau B | L/XL | clé admin + transcript + substitution limits |
|
||||
| persistence | L | no-clobber + crash semantics multi-OS |
|
||||
| signing | M | aucun secret getter |
|
||||
| password/key-slot rotation | L | rotation vs vraie révocation |
|
||||
| alias/notes | M | bornes + persistence |
|
||||
| import/export | M/L | extension sans secret API générale |
|
||||
| security/adversarial tests | XL | tamper/fault/diagnostics |
|
||||
| spec/README/USAGE | L | contrat externe autonome |
|
||||
|
||||
Conclusion : **pas de rescoping fonctionnel**, mais split supplémentaire avant crypto lourde.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Plan `0.2.7` — WebSocket Solana standard
|
||||
|
||||
@@ -361,10 +361,10 @@ Audit au 2026-08-22 :
|
||||
|
||||
| Candidate | Version auditée | Verdict | Motif |
|
||||
|---------------------|----------------:|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `tokio-tungstenite` | `0.30.0` | **retenue** | mature, Tokio-native, TLS rustls, continuité avec bot3 mais réauditée, contrôle de `WebSocketConfig`, client + serveur local de test |
|
||||
| `futures-util` | `0.3.34` | **retenue comme utilitaire** | `StreamExt`/`SinkExt`; features minimales `std,sink` |
|
||||
| `tokio-websockets` | `0.13.3` | alternative viable, non retenue | strict/minimal et performant, mais exige davantage d'assemblage/features et n'apporte pas de besoin fonctionnel supérieur démontré pour cette foundation |
|
||||
| `fastwebsockets` | `0.10.0` | non retenue | plus bas niveau ; peut déléguer davantage de compliance au caller, inutile pour la première foundation KSP |
|
||||
| `tokio-tungstenite` | `0.30.0` | **retenue** | mature, Tokio-native, TLS rustls, continuité avec bot3 mais réauditée, contrôle de `WebSocketConfig`, client + serveur local de test |
|
||||
| `futures-util` | `0.3.34` | **retenue comme utilitaire** | `StreamExt`/`SinkExt`; features minimales `std,sink` |
|
||||
| `tokio-websockets` | `0.13.3` | alternative viable, non retenue | strict/minimal et performant, mais exige davantage d'assemblage/features et n'apporte pas de besoin fonctionnel supérieur démontré pour cette foundation |
|
||||
| `fastwebsockets` | `0.10.0` | non retenue | plus bas niveau ; peut déléguer davantage de compliance au caller, inutile pour la première foundation KSP |
|
||||
|
||||
Landing matérialisé par **`pre.004`** :
|
||||
|
||||
|
||||
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.
|
||||
659
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
Normal file
659
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
Normal file
@@ -0,0 +1,659 @@
|
||||
<!-- file: docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Plan `0.2.10` — OrbitFlare Yellowstone gRPC
|
||||
|
||||
**Statut courant : le gate technique/live final `0.2.10-pre.003` est vert. OrbitFlare Devnet est validé avec la License Key `ORBIT-*` portée par la metadata secrète `x-token` ; le smoke final observe un Slot non nul puis le `SubscribeUpdate::Ping` Yellowstone standard et ferme proprement la session. N1 et N2 restent inchangés, aucune façade provider ni heartbeat N3 n’est nécessaire. `0.2.10-pre.004` est exclusivement la réconciliation documentaire finale.**
|
||||
|
||||
## 1. Base et autorité
|
||||
|
||||
Base stable autoritaire :
|
||||
|
||||
```text
|
||||
v0.2.9
|
||||
```
|
||||
|
||||
État vérifié dans l'archive Gitea fournie :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.9
|
||||
deltas/0.2.9/rel.001.md présent
|
||||
prompts/015-V0_2_10_START_PROMPT.md présent
|
||||
plan 016 Yellowstone présent
|
||||
validation 012 Yellowstone présente
|
||||
Config Transport V3 présente
|
||||
PublicNode Mainnet + Testnet présents
|
||||
```
|
||||
|
||||
Ordre d'autorité pendant `0.2.10` :
|
||||
|
||||
```text
|
||||
règles normatives KSP
|
||||
archive stable v0.2.9 et code réellement livré
|
||||
décisions du présent plan et validation 013
|
||||
deltas immuables de 0.2.10
|
||||
prompt 015 pour les contraintes non supersédées
|
||||
sources provider/upstream actuelles pour les faits externes
|
||||
```
|
||||
|
||||
Le présent gate supersède une hypothèse du prompt : une divergence lifecycle OrbitFlare ne doit jamais être résolue par modification du moteur N1. Si elle ne peut pas être proprement composée au-dessus du moteur, elle devient un blocker/split explicite.
|
||||
|
||||
## 2. Mission opérationnelle réelle
|
||||
|
||||
L'objectif prioritaire de `0.2.10` est de disposer d'au moins un provider Yellowstone **gratuit et durable sur Solana Devnet** pour les validations futures de KSP.
|
||||
|
||||
PublicNode/Allnodes a déjà validé le standard KSP sur :
|
||||
|
||||
```text
|
||||
Mainnet
|
||||
Testnet
|
||||
```
|
||||
|
||||
OrbitFlare complète cette couverture avec :
|
||||
|
||||
```text
|
||||
Devnet
|
||||
```
|
||||
|
||||
Le pricing OrbitFlare observé le 2026-08-25 confirme :
|
||||
|
||||
```text
|
||||
plan Free 0 USD/mois
|
||||
RPC 10 RPS
|
||||
transactions 1 TPS
|
||||
gRPC Devnet only
|
||||
credit limits unlimited
|
||||
carte bancaire non requise selon la page d'accueil
|
||||
```
|
||||
|
||||
Sources principales :
|
||||
|
||||
```text
|
||||
https://orbitflare.com/pricing
|
||||
https://orbitflare.com/products/rpc-nodes
|
||||
https://docs.orbitflare.com/cli
|
||||
```
|
||||
|
||||
Mainnet OrbitFlare payant n'est pas un gate de cette release. Le résultat minimal utile est un smoke Yellowstone standard KSP sur l'endpoint Devnet gratuit.
|
||||
|
||||
## 3. Invariant architectural renforcé
|
||||
|
||||
### 3.1 N1 gRPC immuable
|
||||
|
||||
Les modules qui constituent le moteur Yellowstone livré par `0.2.9` ne doivent pas être modifiés pour OrbitFlare :
|
||||
|
||||
```text
|
||||
src/grpc_settings.rs
|
||||
src/grpc_channel.rs
|
||||
src/grpc_unary.rs
|
||||
src/grpc_subscribe.rs
|
||||
src/grpc_stream.rs
|
||||
```
|
||||
|
||||
Le même principe vaut durablement pour les moteurs physiques :
|
||||
|
||||
```text
|
||||
moteur HTTP provider-neutral
|
||||
moteur WebSocket provider-neutral
|
||||
moteur gRPC provider-neutral
|
||||
```
|
||||
|
||||
Une release provider peut modifier :
|
||||
|
||||
```text
|
||||
provider facade
|
||||
provider policy
|
||||
provider capability projection
|
||||
Config provider profile
|
||||
provider smoke/compliance
|
||||
provider-specific tests
|
||||
```
|
||||
|
||||
Elle ne modifie pas le moteur pour faire rentrer une différence fournisseur.
|
||||
|
||||
### 3.2 N2 standard Yellowstone immuable
|
||||
|
||||
La surface Solana Yellowstone standard publiée par `0.2.9` reste la seule surface wire de référence :
|
||||
|
||||
```text
|
||||
Subscribe
|
||||
SubscribeReplayInfo
|
||||
Ping
|
||||
GetLatestBlockhash
|
||||
GetBlockHeight
|
||||
GetSlot
|
||||
IsBlockhashValid
|
||||
GetVersion
|
||||
|
||||
accounts
|
||||
slots
|
||||
transactions
|
||||
transactions_status
|
||||
blocks
|
||||
blocks_meta
|
||||
entry
|
||||
commitment
|
||||
accounts_data_slice
|
||||
ping
|
||||
from_slot
|
||||
|
||||
9 variantes SubscribeUpdate
|
||||
```
|
||||
|
||||
OrbitFlare ne redéfinit aucun DTO, filtre, unary, request ou update standard.
|
||||
|
||||
### 3.3 N3 provider uniquement si divergence démontrée
|
||||
|
||||
Architecture cible :
|
||||
|
||||
```text
|
||||
OrbitFlare N3
|
||||
-> standard Yellowstone N2 inchangé
|
||||
-> moteur gRPC N1 inchangé
|
||||
```
|
||||
|
||||
Une façade OrbitFlare publique n'est créée que si un consumer KSP doit réellement voir une restriction ou une extension provider. Un simple endpoint, un `provider = orbitflare`, une policy Config ou un smoke ne suffisent pas à justifier une nouvelle API publique.
|
||||
|
||||
## 4. Baseline opérateur `v0.2.9`
|
||||
|
||||
Preuves fournies le 2026-08-25 :
|
||||
|
||||
| Gate | Résultat |
|
||||
|-------------------------------------------|------------------------------------------|
|
||||
| `cargo fmt --all` | PASS |
|
||||
| audit Rust workspace | PASS, 0 candidate export |
|
||||
| audit Markdown tables | PASS, 97 tables et 233 files sur `pre.1` |
|
||||
| `cargo check --workspace` | PASS |
|
||||
| `cargo clippy --workspace --all-targets` | PASS |
|
||||
| `cargo test --workspace` | PASS |
|
||||
| Transport unit | 383 sur 383 PASS |
|
||||
| Transport public API | 49 sur 49 PASS |
|
||||
| Transport release completeness | 43 sur 43 PASS |
|
||||
| Config unit | 113 sur 113 PASS |
|
||||
| PublicNode Yellowstone live | ignored opt-in comme attendu |
|
||||
| `cargo tree --duplicates` Transport | fourni et inspecté |
|
||||
| `cargo tree -p ksp-onchain-transport-lib` | fourni sur `0.2.10-pre.1` |
|
||||
|
||||
Versions visibles dans le graphe fourni :
|
||||
|
||||
```text
|
||||
yellowstone-grpc-proto 12.6.0
|
||||
tonic 0.14.6
|
||||
tonic-prost 0.14.6
|
||||
prost 0.14.4
|
||||
tokio 1.53.1
|
||||
http 1.5.0
|
||||
reqwest 0.13.4
|
||||
```
|
||||
|
||||
Aucune seconde génération Tonic/Prost/Yellowstone incompatible n'est observée. Les doublons restants ne justifient aucun changement pour `0.2.10`.
|
||||
|
||||
## 5. Réaudit Yellowstone upstream du 2026-08-25
|
||||
|
||||
État courant observé :
|
||||
|
||||
```text
|
||||
release GitHub courante v15.1.2+solana.4.2.0
|
||||
publication 2026-08-18
|
||||
yellowstone-grpc-proto publié 12.6.0
|
||||
stack proto prost 0.14 / tonic 0.14
|
||||
```
|
||||
|
||||
Sources :
|
||||
|
||||
```text
|
||||
https://github.com/rpcpool/yellowstone-grpc/releases
|
||||
https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md
|
||||
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto
|
||||
https://crates.io/crates/yellowstone-grpc-proto
|
||||
```
|
||||
|
||||
Aucune évolution matérielle n'impose de changer le contrat KSP de `0.2.9`. Il n'y a donc aucun bump de dependency Yellowstone/Tonic/Prost prévu dans `0.2.10`.
|
||||
|
||||
## 6. Endpoint et network OrbitFlare
|
||||
|
||||
Les sources actuelles distinguent plusieurs formes de service.
|
||||
|
||||
| Élément | Observation actuelle | Décision KSP |
|
||||
|------------------------|-----------------------------------------------|---------------------------------------------|
|
||||
| Devnet gRPC | `http://devnet.rpc.orbitflare.com:10000` | premier endpoint live à tester |
|
||||
| endpoint régional | `http://{region}.rpc.orbitflare.com:10000` | descriptif, pas de fallback inventé |
|
||||
| endpoint licence/dédié | `https://your-endpoint.grpc.orbitflare.com` | utiliser uniquement si Dashboard le fournit |
|
||||
| `http` | HTTP/2 plaintext | ne jamais présenter comme TLS |
|
||||
| `https` | HTTP/2 TLS | conserver tel quel |
|
||||
| Mainnet | disponible selon plan/service | non requis pour le gate gratuit |
|
||||
| Devnet | explicitement documenté | **IN** |
|
||||
| Testnet | CLI accepte le label mais endpoint non prouvé | non requis, ne rien inventer |
|
||||
|
||||
Sources :
|
||||
|
||||
```text
|
||||
https://docs.orbitflare.com/cli
|
||||
https://docs.orbitflare.com/sdk/go-grpc
|
||||
https://docs.orbitflare.com/data-streaming/yellowstone-quickstart
|
||||
```
|
||||
|
||||
L'endpoint Dashboard opérateur reste autoritaire lorsqu'il existe. KSP ne transforme jamais automatiquement une URL `http` en `https`, ne change pas de région et ne construit pas un hostname provider non documenté.
|
||||
|
||||
## 7. Auth OrbitFlare : classification live corrigée
|
||||
|
||||
Le live `pre.002` et les sources OrbitFlare permettent désormais de séparer les credentials sans supposition.
|
||||
|
||||
| Credential ou mécanisme | Usage observé | Classification après `pre.002` |
|
||||
|-------------------------|---------------------------|------------------------------------------------------|
|
||||
| `X-ORBIT-KEY` | Customer API | control-plane, interdit sur Yellowstone |
|
||||
| Bearer Device Flow | Customer API v2 | control-plane |
|
||||
| `api_key` RPC | Solana HTTP RPC | data-plane HTTP |
|
||||
| License Key `ORBIT-*` | produit Solana Free | data-plane provider |
|
||||
| metadata `x-token` | Yellowstone gRPC | transport prouvé de la License Key |
|
||||
| aucune metadata | premier smoke KSP | rejetée `Unauthenticated` au `SubscribeOpen` |
|
||||
| IP whitelist | autre mode d’auth produit | non retenu pour le service opérateur en API Key Mode |
|
||||
|
||||
La référence Yellowstone OrbitFlare donne explicitement le modèle suivant :
|
||||
|
||||
```text
|
||||
ORBITFLARE_LICENSE_KEY
|
||||
-> metadata gRPC x-token
|
||||
-> Yellowstone
|
||||
```
|
||||
|
||||
Le Dashboard opérateur confirme parallèlement que le produit `Solana Free` est en `API Key Mode Active`, donc utilisable depuis toute IP avec la License Key. Le `X-ORBIT-KEY` et le Bearer issus du Device Flow restent réservés au Customer API.
|
||||
|
||||
Le CLI OrbitFlare courant ne constitue pas un canari gRPC d’auth fiable : son `ping` ouvre le canal sans injecter la License Key et reçoit `invalid x-token: api key not found`. Ce défaut du CLI ne modifie pas le contrat provider documenté.
|
||||
|
||||
Décision :
|
||||
|
||||
```text
|
||||
ne jamais versionner la License Key réelle
|
||||
ne jamais injecter X-ORBIT-KEY dans Yellowstone
|
||||
représenter la License Key par secret_metadata x-token dans Config V3
|
||||
faire lire le secret du smoke par stdin, jamais par argument CLI
|
||||
ne modifier ni N1 ni N2 pour cette auth provider
|
||||
```
|
||||
|
||||
## 8. Heartbeat : réconciliation KSP / Yellowstone / OrbitFlare
|
||||
|
||||
### 8.1 Faits upstream
|
||||
|
||||
Yellowstone upstream documente que :
|
||||
|
||||
```text
|
||||
les load balancers peuvent fermer un stream si le client reste silencieux
|
||||
le serveur Yellowstone envoie un SubscribeUpdate::Ping périodique
|
||||
le client peut répondre par un SubscribeRequest::Ping
|
||||
le serveur répond alors par SubscribeUpdate::Pong
|
||||
```
|
||||
|
||||
Source :
|
||||
|
||||
```text
|
||||
https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md
|
||||
```
|
||||
|
||||
### 8.2 Faits OrbitFlare
|
||||
|
||||
OrbitFlare documente :
|
||||
|
||||
```text
|
||||
idle timeout partagé environ 10 minutes
|
||||
client ping recommandé toutes les 30 secondes
|
||||
produit gRPC recommandation 15 à 30 secondes
|
||||
SDK Go PingInterval default 10 secondes
|
||||
SDK Go MaxMissedPongs 3
|
||||
```
|
||||
|
||||
Sources :
|
||||
|
||||
```text
|
||||
https://docs.orbitflare.com/data-streaming/yellowstone
|
||||
https://docs.orbitflare.com/authentication
|
||||
https://docs.orbitflare.com/sdk/go-grpc
|
||||
https://orbitflare.com/products/solana-grpc
|
||||
```
|
||||
|
||||
### 8.3 Couverture KSP déjà présente
|
||||
|
||||
Le moteur `0.2.9` possède déjà exactement la réponse standard :
|
||||
|
||||
```text
|
||||
SubscribeUpdate::Ping reçu
|
||||
-> send_automatic_ping(...)
|
||||
-> SubscribeRequest::Ping envoyé sur le stream existant
|
||||
-> request de subscription mémorisée inchangée
|
||||
```
|
||||
|
||||
Cette propriété est testée déterministiquement par le moteur N1.
|
||||
|
||||
### 8.4 Décision `pre.001`
|
||||
|
||||
La décision initiale de ne pas modifier le moteur est confirmée par le live final.
|
||||
|
||||
Le smoke authentifié `pre.002-fix.001`, puis son rerun dans `pre.003`, ont observé le `SubscribeUpdate::Ping` standard sur OrbitFlare Devnet :
|
||||
|
||||
```text
|
||||
OrbitFlare server Ping live
|
||||
+
|
||||
KSP N1 automatic Ping reply deterministic test
|
||||
=
|
||||
activité bidirectionnelle standard suffisante sans code provider supplémentaire
|
||||
```
|
||||
|
||||
Résultat final :
|
||||
|
||||
```text
|
||||
heartbeat OrbitFlare N3 non nécessaire
|
||||
façade provider non nécessaire
|
||||
YellowstoneGrpcSessionSettings inchangé
|
||||
grpc_stream.rs inchangé
|
||||
latest_request reconnect sémantique N1 préservée
|
||||
```
|
||||
|
||||
Il reste interdit d'ajouter un timer dans `grpc_stream.rs` ou `YellowstoneGrpcSessionSettings` pour OrbitFlare. La recommandation provider d’un ping client périodique ne devient pas une policy globale tant que le chemin standard serveur Ping -> réponse automatique N1 satisfait le service live.
|
||||
|
||||
Le point de sécurité initial reste durable : envoyer naïvement une requête `ping` seule via la mutation publique remplacerait la dernière subscription mémorisée pour reconnect. Si un futur provider exige un heartbeat proactif indépendant, cette policy doit être composée au-dessus de N1 sans corrompre cette sémantique.
|
||||
|
||||
## 9. Capabilities OrbitFlare
|
||||
|
||||
### 9.1 Streaming documenté
|
||||
|
||||
OrbitFlare documente actuellement :
|
||||
|
||||
```text
|
||||
accounts
|
||||
transactions
|
||||
slots
|
||||
blocks
|
||||
blocks_meta
|
||||
entry
|
||||
commitment
|
||||
accounts_data_slice
|
||||
ping
|
||||
```
|
||||
|
||||
Le CLI/SDK montre également les filtres account/transaction/slot/block standard nécessaires aux canaris principaux.
|
||||
|
||||
### 9.2 Surface non encore prouvée provider
|
||||
|
||||
Ces capacités existent dans N2 KSP mais n'ont pas été exhaustivement sondées sur le plan OrbitFlare Free pendant `0.2.10` :
|
||||
|
||||
```text
|
||||
SubscribeReplayInfo
|
||||
Ping unary
|
||||
GetLatestBlockhash
|
||||
GetBlockHeight
|
||||
GetSlot
|
||||
IsBlockhashValid
|
||||
GetVersion
|
||||
from_slot et retention réelle
|
||||
transactions_status complet
|
||||
champs récents compressed/cuckoo/token expansion selon endpoint déployé
|
||||
```
|
||||
|
||||
La clôture de `0.2.10` ne transforme pas l'absence de probe en absence de support. Le live final requis portait sur le chemin opérationnel utile à la release : authentification, standard Subscribe, Slot, Ping serveur et close borné.
|
||||
|
||||
Classification durable :
|
||||
|
||||
```text
|
||||
standard KSP disponible oui dans N2
|
||||
provider support live prouvé Subscribe slots + commitment + Ping
|
||||
provider entitlement éventuel provider-owned
|
||||
reste unknown / non exhaustivement testé
|
||||
```
|
||||
|
||||
Aucune capacité N2 n'est supprimée du standard global à cause d'une restriction ou d'un inconnu OrbitFlare.
|
||||
|
||||
## 10. Limits et quotas utiles
|
||||
|
||||
La documentation actuelle des services partagés indique :
|
||||
|
||||
| Limite | Valeur observée | Traitement KSP |
|
||||
|-----------------------------|------------------------------------------|---------------------------------------------|
|
||||
| connexions gRPC simultanées | 50 par IP | information provider, pas borne N1 |
|
||||
| portée du cap | globale par IP et régions gRPC partagées | éviter les reconnect storms |
|
||||
| subscriptions par connexion | unlimited | ne pas convertir en garantie universelle |
|
||||
| idle timeout | environ 10 minutes | chemin Ping standard validé live |
|
||||
| dépassement | gRPC `RESOURCE_EXHAUSTED` | status distant safe existant |
|
||||
| reconnect conseillé | exponential backoff | KSP N1 possède déjà un budget/backoff borné |
|
||||
|
||||
Ces valeurs commerciales/opérationnelles ne deviennent pas des constantes du standard KSP.
|
||||
|
||||
## 11. Config V3
|
||||
|
||||
Le schéma V3 existant représente directement le contrat live corrigé :
|
||||
|
||||
```text
|
||||
provider = orbitflare
|
||||
cluster = devnet
|
||||
protocol = solana_yellowstone
|
||||
url = http://devnet.rpc.orbitflare.com:10000
|
||||
metadata = []
|
||||
secret_metadata = x-token <- ${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}
|
||||
```
|
||||
|
||||
La variable porte la License Key `ORBIT-*` du produit Solana Free. Elle ne porte jamais `X-ORBIT-KEY`.
|
||||
|
||||
Décision :
|
||||
|
||||
```text
|
||||
pas de format_version 4
|
||||
pas de champ region si l’URL suffit
|
||||
pas de heartbeat dans grpc_defaults
|
||||
secret provider géré par Config V3 existante
|
||||
redaction KSP obligatoire dans safe/debug projections
|
||||
```
|
||||
|
||||
Le profil `orbitflare_devnet` conserve les companions HTTP/WS Solana Devnet standards et ajoute exactement un endpoint gRPC authentifié par secret metadata `x-token`.
|
||||
|
||||
## 12. Stratégie live Devnet
|
||||
|
||||
### 12.1 Résultat du premier canari `pre.002`
|
||||
|
||||
Le smoke initial utilisait le standard N2 directement, sans Config et sans metadata. Le canal physique a atteint le service OrbitFlare, puis l’ouverture du Subscribe a retourné :
|
||||
|
||||
```text
|
||||
grpc_operation = SubscribeOpen
|
||||
grpc_status = Unauthenticated
|
||||
grpc_code = The request does not have valid authentication credentials
|
||||
```
|
||||
|
||||
Cette preuve ferme l’hypothèse « Devnet Free sans metadata ».
|
||||
|
||||
### 12.2 Canari corrigé `pre.002-fix.001`
|
||||
|
||||
Le même test reste provider-neutral et ne dépend toujours pas de Config. Il lit une seule License Key sur stdin puis construit la metadata secrète standard :
|
||||
|
||||
```text
|
||||
endpoint = http://devnet.rpc.orbitflare.com:10000
|
||||
provider = orbitflare
|
||||
cluster = devnet
|
||||
protocol = solana_yellowstone
|
||||
auth = x-token <- License Key lue sur stdin
|
||||
request = Subscribe slots à commitment confirmed
|
||||
preuve = au moins un Slot non nul + SubscribeUpdate::Ping
|
||||
close = borné
|
||||
```
|
||||
|
||||
La valeur secrète n'apparaît ni dans Debug ni dans la ligne de commande.
|
||||
|
||||
Résultats opérateur :
|
||||
|
||||
```text
|
||||
pre.002-fix.001 Subscribe -> Slot + Ping PASS en 5.10 s
|
||||
pre.003 final Subscribe -> Slot + Ping PASS en 5.19 s
|
||||
```
|
||||
|
||||
Le second passage ferme le gate live sur la version `0.2.10-pre.3` utilisée pour la clôture technique.
|
||||
|
||||
### 12.3 Preuve heartbeat
|
||||
|
||||
Le `SubscribeUpdate::Ping` standard est observé live sur OrbitFlare Devnet avec l'auth correcte.
|
||||
|
||||
La combinaison suivante est donc prouvée :
|
||||
|
||||
```text
|
||||
Subscribe authentifié
|
||||
-> Slot non nul
|
||||
-> SubscribeUpdate::Ping serveur
|
||||
-> chemin de réponse automatique N1 déjà couvert déterministiquement
|
||||
-> close borné
|
||||
```
|
||||
|
||||
Verdict final : aucune divergence heartbeat OrbitFlare ne justifie une façade ou une policy provider. Le moteur gRPC et le standard Yellowstone restent inchangés.
|
||||
|
||||
### 12.4 Unary et replay
|
||||
|
||||
Les sept unary N2, `SubscribeReplayInfo` et la retention `from_slot` n'ont pas été rendus obligatoires pour le gate OrbitFlare Free.
|
||||
|
||||
Cette décision évite de confondre :
|
||||
|
||||
```text
|
||||
complétude du standard KSP N2
|
||||
support/entitlement d'un provider particulier
|
||||
preuve minimale nécessaire à la mission Devnet de 0.2.10
|
||||
```
|
||||
|
||||
Le moteur N1 reste capable d'utiliser `SubscribeReplayInfo` quand il est disponible et tolère son indisponibilité pendant reconnect en poursuivant avec le `from_slot` conservateur demandé. Aucun comportement provider n'est introduit pour combler un inconnu de service.
|
||||
|
||||
## 13. Threat model recalibré
|
||||
|
||||
Le gate retient :
|
||||
|
||||
```text
|
||||
Customer API key injectée sur le mauvais plan
|
||||
gRPC token confondu avec X-ORBIT-KEY
|
||||
secret metadata leak
|
||||
endpoint Dashboard sensible
|
||||
plaintext http présenté comme TLS
|
||||
IP/network entitlement mal classifié
|
||||
heartbeat ajouté au moteur global par erreur
|
||||
ping provider écrasant le last accepted full request
|
||||
ping flood
|
||||
missed pong surinterprété
|
||||
RESOURCE_EXHAUSTED et reconnect storm
|
||||
50 connections par IP consommées par des smokes mal fermés
|
||||
region failover changeant de node/fork
|
||||
from_slot hors retention
|
||||
provider unary unavailable
|
||||
remote Status arbitraire
|
||||
SDK OrbitFlare introduit sans nécessité
|
||||
```
|
||||
|
||||
## 14. Surface de code autorisée et interdite
|
||||
|
||||
### 14.1 Interdit par défaut
|
||||
|
||||
```text
|
||||
modification grpc_stream.rs
|
||||
modification grpc_channel.rs pour une policy OrbitFlare
|
||||
modification grpc_settings.rs pour un heartbeat OrbitFlare
|
||||
modification des DTOs grpc_subscribe.rs
|
||||
second raw Tonic client
|
||||
orbitflare-sdk-rs dependency
|
||||
provider-specific proto
|
||||
Transport -> Config/env
|
||||
```
|
||||
|
||||
### 14.2 Autorisé si nécessaire
|
||||
|
||||
```text
|
||||
profil Config V3 orbitflare_devnet
|
||||
smoke provider utilisant les APIs N2 existantes
|
||||
provider capability descriptor sans wire nouveau
|
||||
petite façade OrbitFlare au-dessus de N2 seulement si divergence live prouvée
|
||||
provider-specific tests/compliance
|
||||
README/USAGE dans la tranche documentaire finale
|
||||
```
|
||||
|
||||
## 15. Forecast recalibré
|
||||
|
||||
Le chemin effectif de `0.2.10` est désormais figé :
|
||||
|
||||
```text
|
||||
pre.001 audit actuel + architecture immuable N1/N2 + auth/endpoints + Free Devnet + heartbeat + sizing
|
||||
pre.002 profil Config V3 OrbitFlare Devnet + smoke de caractérisation sans metadata
|
||||
pre.002-fix.001 auth corrigée : License Key -> secret x-token ; live Subscribe -> Slot + Ping PASS
|
||||
pre.003 gate technique/live final ; workspace + graphes + rerun OrbitFlare PASS
|
||||
pre.004 réconciliation documentaire finale
|
||||
pre.005 publication minimale après décision explicite sur la release suivante
|
||||
rel.001 publication stable stricte
|
||||
```
|
||||
|
||||
La branche de divergence heartbeat n'a pas été déclenchée. Aucun couloir supplémentaire n'est requis.
|
||||
|
||||
Le numéro n'est jamais le critère de clôture ; l'ordre technique, documentaire puis publication reste obligatoire.
|
||||
|
||||
## 16. Critères de split
|
||||
|
||||
Les critères de split définis pendant l'audit étaient :
|
||||
|
||||
```text
|
||||
OrbitFlare n'émet pas le Ping standard et exige un Ping client proactif
|
||||
l’auth x-token fonctionne mais révèle une exigence provider supplémentaire non composable au-dessus de N1/N2
|
||||
le service requiert un mécanisme TLS/channel absent du moteur stable
|
||||
un method/filter standard est remplacé par une extension wire OrbitFlare
|
||||
le replay/from_slot exige une policy provider visible au consumer
|
||||
un failover provider impose une sémantique que N1 ne peut pas composer sans modification
|
||||
```
|
||||
|
||||
**Aucun de ces critères n'a été déclenché par le gate live final.**
|
||||
|
||||
Le principe reste durable pour les providers futurs : si une divergence ne peut pas être composée proprement au-dessus de N1/N2, ne pas modifier le moteur pour le provider ; qualifier le blocker et replanifier l'architecture.
|
||||
|
||||
## 17. Critères de clôture
|
||||
|
||||
État à l'entrée de `pre.004` :
|
||||
|
||||
```text
|
||||
Devnet OrbitFlare Free atteint par KSP PASS
|
||||
N1 gRPC inchangé PASS
|
||||
N2 Yellowstone inchangé PASS
|
||||
aucun SDK OrbitFlare runtime PASS
|
||||
provider/auth correctement classifiés PASS
|
||||
aucune Customer API key sur le data-plane Yellowstone PASS
|
||||
heartbeat live classifié PASS
|
||||
Config V3 cohérente sans nouveau format PASS
|
||||
smoke provider architecture-safe PASS
|
||||
PublicNode non régressé déterministiquement PASS
|
||||
Yellowstone standard non régressé PASS
|
||||
HTTP 52+14 non régressé PASS
|
||||
WebSocket standard 18/18 non régressé PASS
|
||||
Helius WebSocket non régressé PASS
|
||||
workspace complet vert PASS
|
||||
graphes Cargo inspectés PASS
|
||||
réconciliation documentaire séparée EN COURS pre.004
|
||||
publication minimale séparée À FAIRE pre.005
|
||||
```
|
||||
|
||||
Le gate technique est donc fermé. La stabilité `0.2.10` reste conditionnée à la validation documentaire `pre.004`, au couloir publication-minimal `pre.005`, puis à `rel.001`.
|
||||
|
||||
## 18. Release suivante
|
||||
|
||||
La décision préalable à `pre.005` est désormais prise. L'ancien forecast :
|
||||
|
||||
```text
|
||||
0.2.11 — Helius LaserStream gRPC
|
||||
```
|
||||
|
||||
est reporté dans les TODO Yellowstone sans numéro de release. L'audit du 2026-08-25 conclut que la surface Helius reste largement wire-compatible Yellowstone et pourrait probablement se composer au-dessus de N1/N2, mais l'accès gRPC Helius exige actuellement un plan payant que l'opérateur ne retient pas uniquement pour ce test. La release provider est donc différée jusqu'à disponibilité d'un accès live permettant de valider réellement auth, endpoints, Subscribe, Ping, replay/from_slot et erreurs provider. Les extensions Helius non standard restent un sujet séparé.
|
||||
|
||||
La séquence active avance d'un cran :
|
||||
|
||||
```text
|
||||
0.2.11 — off-chain price transport
|
||||
0.2.12 — Price Desk + intégration prix Wallet Desk
|
||||
0.2.13 — interface/wire foundation
|
||||
0.2.14 — program-api foundation
|
||||
```
|
||||
|
||||
Décision de publication :
|
||||
|
||||
```text
|
||||
aucun prompt suivant dans pre.004
|
||||
aucun changement CHANGELOG/ROADMAP dans pre.004
|
||||
Helius gRPC -> TODO futur sans numéro
|
||||
0.2.11 -> off-chain price transport
|
||||
pre.005 préparera seulement le prompt 0.2.11 + CHANGELOG + ROADMAP
|
||||
```
|
||||
|
||||
Quel que soit le provider futur, l'invariant reste le même : N1 gRPC et N2 Yellowstone sont des fondations stables ; les différences provider se composent au-dessus et ne modifient jamais le moteur pour satisfaire un fournisseur.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/FILE_CONTRACTS.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Contrats des fichiers
|
||||
|
||||
@@ -20,25 +20,25 @@ 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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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 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/`
|
||||
|
||||
| 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/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/rules/*.md` | Définir les règles normatives par portée. | Modifier uniquement pour une décision normative ; incrémenter la version du fichier à chaque enregistrement modifiant son contenu. |
|
||||
| `docs/rules/PROMPT_STRUCTURE.md` | Définir la structure, le cycle de vie et le dimensionnement des prompts/sessions KSP. | Modifier lorsque le contrat des prompts ou les règles de découpage de sessions/prereleases changent. |
|
||||
| `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/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/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. |
|
||||
| futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. |
|
||||
| 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/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/rules/*.md` | Définir les règles normatives par portée. | Modifier uniquement pour une décision normative ; incrémenter la version du fichier à chaque enregistrement modifiant son contenu. |
|
||||
| `docs/rules/PROMPT_STRUCTURE.md` | Définir la structure, le cycle de vie et le dimensionnement des prompts/sessions KSP. | Modifier lorsque le contrat des prompts ou les règles de découpage de sessions/prereleases changent. |
|
||||
| `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/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 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. |
|
||||
| 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. 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/`
|
||||
|
||||
@@ -61,10 +61,10 @@ Pour un document standard profilé, `default_profile` et `profiles` sont des cl
|
||||
|
||||
## Répertoire `prompts/`
|
||||
|
||||
| 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/*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. |
|
||||
| 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/*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/`
|
||||
|
||||
@@ -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`. |
|
||||
|
||||
## 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
|
||||
|
||||
| Fichier/famille | Responsabilité | Règle de modification |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user