From 21ed5f88da137e0bc8a93b67dad05223090bc701 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 24 Aug 2026 19:53:45 +0200 Subject: [PATCH] v0.2.9-pre.011 --- Cargo.toml | 2 +- config/schemas/std.transport.schema.json | 276 ++- config/std.transport.json | 67 +- crates/ksp-config-lib/README.md | 6 +- crates/ksp-config-lib/USAGE.md | 30 +- crates/ksp-config-lib/src/lib.rs | 17 +- crates/ksp-config-lib/src/registry.rs | 10 +- crates/ksp-config-lib/src/transport.rs | 411 ++++- crates/ksp-config-lib/tests/public_api.rs | 4 +- .../unit_tests/fixtures_v3/std.transport.json | 242 +++ crates/ksp-config-lib/unit_tests/transport.rs | 147 +- deltas/0.2.9/pre.011.md | 284 +++ .../plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md | 1631 +++++------------ .../validation/012-V0_2_9_YELLOWSTONE_GRPC.md | 1279 ++++--------- 14 files changed, 2285 insertions(+), 2121 deletions(-) create mode 100644 crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json create mode 100644 deltas/0.2.9/pre.011.md diff --git a/Cargo.toml b/Cargo.toml index 13c58e1..99edfd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ 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.9-pre.10.fix.1" +version = "0.2.9-pre.11" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/config/schemas/std.transport.schema.json b/config/schemas/std.transport.schema.json index a7251c9..7a35cf6 100644 --- a/config/schemas/std.transport.schema.json +++ b/config/schemas/std.transport.schema.json @@ -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" + } + } + } } } } diff --git a/config/std.transport.json b/config/std.transport.json index eff4964..27dec6d 100644 --- a/config/std.transport.json +++ b/config/std.transport.json @@ -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": [ { @@ -105,6 +119,57 @@ "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" + } + ] } ] } diff --git a/crates/ksp-config-lib/README.md b/crates/ksp-config-lib/README.md index ec4ed48..13cf9c1 100644 --- a/crates/ksp-config-lib/README.md +++ b/crates/ksp-config-lib/README.md @@ -1,5 +1,5 @@ - + # 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. diff --git a/crates/ksp-config-lib/USAGE.md b/crates/ksp-config-lib/USAGE.md index e7696c2..8e5fb13 100644 --- a/crates/ksp-config-lib/USAGE.md +++ b/crates/ksp-config-lib/USAGE.md @@ -1,5 +1,5 @@ - + # 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)`. 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 diff --git a/crates/ksp-config-lib/src/lib.rs b/crates/ksp-config-lib/src/lib.rs index f3aa9c8..93465aa 100644 --- a/crates/ksp-config-lib/src/lib.rs +++ b/crates/ksp-config-lib/src/lib.rs @@ -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; diff --git a/crates/ksp-config-lib/src/registry.rs b/crates/ksp-config-lib/src/registry.rs index e78889e..c3ba72e 100644 --- a/crates/ksp-config-lib/src/registry.rs +++ b/crates/ksp-config-lib/src/registry.rs @@ -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"; diff --git a/crates/ksp-config-lib/src/transport.rs b/crates/ksp-config-lib/src/transport.rs index c6085af..2673619 100644 --- a/crates/ksp-config-lib/src/transport.rs +++ b/crates/ksp-config-lib/src/transport.rs @@ -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, + grpc_settings: std::option::Option, } 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) { 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, + std::option::Option, + ) { + 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, + #[serde(default)] + grpc_defaults: std::option::Option, endpoints: std::vec::Vec, #[serde(default)] ws_endpoints: std::option::Option>, + #[serde(default)] + grpc_endpoints: std::option::Option>, } #[derive(serde::Deserialize)] @@ -255,6 +286,72 @@ struct EffectiveWsReconnectOverrideSource { max_backoff_ms: std::option::Option, } +#[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, + initial_backoff_ms: std::option::Option, + max_backoff_ms: std::option::Option, +} + +#[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, + unary_timeout_ms: std::option::Option, + close_timeout_ms: std::option::Option, + reconnect: std::option::Option, + request_channel_capacity: std::option::Option, + update_channel_capacity: std::option::Option, + max_inbound_message_size_bytes: std::option::Option, + max_outbound_message_size_bytes: std::option::Option, +} + +#[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, + #[serde(default)] + secret_metadata: std::vec::Vec, + #[serde(default)] + session: std::option::Option, +} + fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { 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, + sources: std::option::Option>, + effective: &crate::ResolvedConfigJson, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + 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, + defaults: &EffectiveGrpcSessionSource, + effective: &crate::ResolvedConfigJson, + profile: &crate::ResolvedConfigProfile, +) -> ksp_core_lib::Result> { + let mut endpoints = std::vec::Vec::::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 { + 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, + secret_sources: std::vec::Vec, + effective: &crate::ResolvedConfigJson, + profile: &crate::ResolvedConfigProfile, + endpoint_name: &str, + endpoint_index: usize, +) -> ksp_core_lib::Result> { + let mut metadata = std::vec::Vec::::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, profile: &crate::ResolvedConfigProfile, diff --git a/crates/ksp-config-lib/tests/public_api.rs b/crates/ksp-config-lib/tests/public_api.rs index 1c2452a..32ac03c 100644 --- a/crates/ksp-config-lib/tests/public_api.rs +++ b/crates/ksp-config-lib/tests/public_api.rs @@ -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::() > 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"); diff --git a/crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json b/crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json new file mode 100644 index 0000000..24ceada --- /dev/null +++ b/crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json @@ -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 + } + } + ] + } + ] +} diff --git a/crates/ksp-config-lib/unit_tests/transport.rs b/crates/ksp-config-lib/unit_tests/transport.rs index 98d9086..4364958 100644 --- a/crates/ksp-config-lib/unit_tests/transport.rs +++ b/crates/ksp-config-lib/unit_tests/transport.rs @@ -1,5 +1,5 @@ // file: crates/ksp-config-lib/unit_tests/transport.rs -// version: 6 +// version: 7 #[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,121 @@ 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_publicnode_mainnet_maps_provider_neutral_yellowstone_grpc() { + let engine = committed_engine(); + let engine = match engine { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + 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("publicnode_mainnet"), &environment); + assert!(resolved.is_ok(), "committed PublicNode Mainnet V3 profile should map: {resolved:?}"); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(resolved.profile_id(), "publicnode_mainnet"); + assert!(resolved.ws_settings().is_some()); + let grpc = resolved.grpc_settings(); + assert!(grpc.is_some(), "PublicNode Mainnet profile 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(), "publicnode_solana_mainnet_yellowstone"); + assert_eq!(endpoint.provider().as_str(), "publicnode"); + assert_eq!(endpoint.cluster().as_str(), "mainnet-beta"); + assert_eq!(endpoint.url().as_str(), "https://solana-yellowstone-grpc.publicnode.com:443"); + assert!(endpoint.url().uses_tls()); + assert!(endpoint.metadata().is_empty()); + 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("solana-yellowstone-grpc.publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs"); + } +} + +#[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::::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::::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 +256,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 +288,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 +540,22 @@ fn v1_fixture_engine() -> ksp_core_lib::Result { return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); } +fn v3_fixture_engine() -> ksp_core_lib::Result { + 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 { let workspace = workspace_root(); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); diff --git a/deltas/0.2.9/pre.011.md b/deltas/0.2.9/pre.011.md new file mode 100644 index 0000000..d3e7989 --- /dev/null +++ b/deltas/0.2.9/pre.011.md @@ -0,0 +1,284 @@ + + + +# 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) +``` + +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. diff --git a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md index 6ad96a9..ee46d0e 100644 --- a/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md +++ b/docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md @@ -1,277 +1,122 @@ - + # Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode -> **Statut : `0.2.9-pre.010` a passé fmt/audit/check, Transport 383 unit + 49 public API + 43 completeness + 4 doctests et le workspace complet, mais le gate Clippy a remonté deux `collapsible_if`. `0.2.9-pre.010-fix.001` est candidate de fermeture : hygiène Clippy + correction de la description crate-root obsolète, sans changement de sémantique reconnect/replay. `pre.011` reste hors application tant que ce fix n'a pas repassé le gate opérateur sans warning.** +> **Statut courant : `0.2.9-pre.010-fix.001` est fermée par gate opérateur sans warning. `0.2.9-pre.011` est la tranche active : Config Transport V3, séparation explicite protocol/provider, mapping Config -> Yellowstone gRPC et premier profil PublicNode Mainnet. Le présent document a été réorganisé pendant `pre.011` : les décisions actives restent ici ; les journaux historiques détaillés restent dans `deltas/0.2.9/`.** -## 1. Objet, base et état d'ouverture +## 1. Objet et autorité de la release -`0.2.9` introduit dans `ksp-onchain-transport-lib` une première fondation Yellowstone gRPC **standard et provider-neutral**, distincte de HTTP et de WebSocket. - -Base autoritaire auditée à l'ouverture : +Base stable d'ouverture : ```text -archive opérateur = khadhroony-solana-project-v0.2.8-full-from-gitea.zip -workspace.package.version initial = 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 opérateur +v0.2.8 ``` -Le signal d'ouverture est : +Release : ```text -workspace.package.version = 0.2.9-pre.1 -commit attendu = v0.2.9-pre.001 -aucun tag prerelease +0.2.9 — Yellowstone gRPC standard/provider-neutral ``` -État hérité à ne pas régresser : +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 -HTTP Solana 52/52 current typed + 14 historiques Deprecated/Removed -KSP-TRANSPORT-007 appliqué -WebSocket standard 9 familles / 18 subscribe-unsubscribe -Helius LaserStream WS 7 familles standard + transactionSubscribe/unsubscribe -Helius heartbeat Ping control frame 60 s, actor-owned -Config Transport V1 HTTP + V2 HTTP/WS backward-readable -Config -> Transport autorisé -Transport -> Config interdit +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 ``` -## 2. Résultat du gate `pre.001` +Les deltas historiques ne sont jamais réécrits pour refléter une décision ultérieure. -Le gate est **positif avec scope borné**. +## 2. Résultat final attendu -`0.2.9` peut raisonnablement porter **trois niveaux distincts dans la même crate Transport** : +`0.2.9` doit fermer une première fondation Yellowstone gRPC exploitable sans devenir un SDK fournisseur : ```text -N1 moteur client Yellowstone gRPC partagé - channel/TLS/metadata, stream bidi, bounds, backpressure, shutdown, reconnect/replay observables - -N2 façade protocolaire Solana Yellowstone standard - 7 unary retenus + Subscribe standard + filters/updates typed provider-neutral - -N3 première intégration provider : PublicNode / Allnodes-backed - Mainnet + Testnet, capabilities/profile/config + smokes live opt-in - sans duplication du moteur ; le wire standard est réutilisé uniquement pour les capacités réellement compatibles +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 sans credential versionné +non-régressions HTTP, WebSocket standard et Helius WebSocket ``` -Le moteur est **Yellowstone-specific**, pas une abstraction gRPC universelle. Le serveur/plugin Geyser reste hors de KSP ; KSP implémente le client du protocole Yellowstone exposé par les providers. +## 3. Scope fermé par `pre.001` -La première intégration PublicNode peut rester volontairement mince si le provider n'ajoute aucun wire ou lifecycle propriétaire : l'objectif est de matérialiser la frontière provider, les capabilities et la validation live, pas de créer artificiellement un second actor/session. Un type/facade provider-specific public n'est justifié que si PublicNode impose une différence réelle de contrat. - -La compatibilité provider n'est toutefois **jamais présumée totale**. Pour chaque provider, N3 peut : +### 3.1 Inclus ```text -réutiliser N2 pour une capacité Yellowstone réellement compatible -restreindre une capacité standard absente/non supportée -ajouter une extension provider-specific typed -adapter auth/metadata, compression, keepalive, replay/from_slot, limites ou lifecycle +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 ``` -Le moteur N1 reste unique. Le wire N2 n'est jamais recopié quand il est identique, mais une divergence réelle de wire ou de sémantique doit être isolée explicitement dans N3 plutôt que masquée derrière le standard. - -Sont explicitement exclus de `0.2.9` : +### 3.2 Hors scope `0.2.9` ```text -SubscribeDeshred et pré-exécution/deshred -OrbitFlare provider integration -Helius LaserStream gRPC provider integration -eRPC/Triton/Alchemy/QuickNode/Chainstack/Tatum/Shyft/Solinfra/NodeFlare/autres providers -client autoreconnect upstream comme contrat public KSP +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 -exactly-once / lossless / ordre global garanti serveur Geyser/plugin validator -Store/workers/backfill historique +Store/persistence/backfill historique +workers/jobs d'acquisition +replay lossless garanti +refonte HTTP ou WebSocket ``` -Après `0.2.9`, seules deux releases provider sont actuellement réservées : **OrbitFlare**, puis **Helius LaserStream gRPC**. Elles réutiliseront N1/N2 lorsque compatibles et isoleront leurs restrictions/extensions dans N3. Tous les autres providers restent en TODO/IDEAS sans numéro réservé jusqu'à décision explicite ultérieure. +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. -## 3. Sources internes relues +## 4. Audit upstream et dépendances retenues -Le gate a relu les règles et documents imposés par le prompt depuis la base stable : +### 4.1 Snapshot normatif du gate + +Le gate d'ouverture a réaudité Yellowstone courant et a constaté des versions indépendantes entre plugin, client et proto : ```text -RULES.md -docs/000-README.md - -docs/rules/RULES_GENERAL.md -docs/rules/RULES_KSP.md -docs/rules/RULES_RUST.md -docs/rules/RULES_DEPENDENCIES.md -docs/rules/RULES_DOCUMENTATION.md -docs/rules/FILE_CONTRACTS.md -docs/rules/VERSION_WORKFLOW.md -docs/rules/PROMPT_STRUCTURE.md - -docs/architecture/000-README.md -docs/architecture/002-LAYERS_AND_DEPENDENCIES.md -docs/architecture/003-COMPONENT_CONTRACTS.md -docs/architecture/004-COMPONENT_INVENTORY.md -docs/architecture/005-DEPENDENCY_GRAPH.md -docs/architecture/009-ACQUISITION_WORKERS_AND_JOBS.md -docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md - -docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -docs/plans/007-V0_2_0_SERIES_PLANNING.md -docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md - -docs/validation/003-V0_2_1_ONCHAIN_HTTP.md -docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md -docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md -docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md - -deltas/0.2.8/rel.001.md +release GitHub observée v15.1.2+solana.4.2.0 — 2026-08-18 +yellowstone-grpc-client 13.3.0 +yellowstone-grpc-proto 12.6.0 +prost/prost-types 0.14.x +tonic 0.14.x ``` -Le code réel audité confirme notamment : - -- Transport possède déjà HTTP et WebSocket dans une seule crate ; -- Config dépend de Transport, l'inverse est interdit ; -- le schéma Transport V2 est fermé par `additionalProperties: false` et distingue `endpoints` / `ws_endpoints` ; -- aucun contrat gRPC n'existe encore ; -- les abstractions WebSocket ne doivent pas être réutilisées pour gRPC. - -## 4. Baseline stable enregistrée - -La preuve opérateur fournie juste avant l'ouverture enregistre 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 -``` - -Sous-ensembles Transport observés pendant `cargo test --workspace` : - -```text -unit tests 335 passed -public_api 41 passed -release_completeness 34 passed -doc-tests 4 passed -live smokes opt-in / ignored par défaut -``` - -Dépendances directes Transport observées avant gRPC : - -```text -futures-util 0.3.34 -reqwest 0.13.4 -serde 1.0.229 -serde_json 1.0.151 -tokio 1.53.1 -tokio-tungstenite 0.30.0 -ksp-core-lib 0.2.8 -ksp-logging-lib 0.2.8 -``` - -Le graphe existant contient déjà les familles modernes suivantes via HTTP/WS : - -```text -bytes 1.x -http 1.x -hyper 1.x -hyper-util 0.1.x -tower 0.5.x -rustls 0.23.x -tokio-rustls 0.26.x -``` - -Le coût réel de `tonic/prost/yellowstone-grpc-proto` devra néanmoins être mesuré après matérialisation en `pre.002`; aucun doublon n'est préjugé acceptable avant `cargo tree`. - -## 5. Audit upstream Yellowstone au 2026-08-23 - -### 5.1 Divergence du snapshot du prompt - -Le snapshot préparatoire du prompt mentionnait `v14.2.2+solana.4.1.0` comme release GitHub observée. Le réaudit courant trouve désormais : - -```text -release GitHub latest = v15.1.2+solana.4.2.0 -publication release = 2026-08-18 -Rust annoncé = 1.96.1 -``` - -Le changelog master indique en outre : - -```text -2026-08-17 yellowstone-grpc-geyser 15.1.2 -2026-08-10 yellowstone-grpc-proto 12.6.0 -2026-07-31 yellowstone-grpc-client 13.3.0 -``` - -Cela confirme que **version du plugin GitHub, version du client crate et version du proto crate évoluent indépendamment**. - -Sources primaires : +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 ``` -### 5.2 Crates publiées retenues pour le gate +Une réaudite finale est requise en `pre.012` si l'upstream a changé matériellement pendant la release. -État publié observé : +### 4.2 Licence -```text -yellowstone-grpc-client = 13.3.0, publié 2026-07-31 -yellowstone-grpc-proto = 12.6.0, publié 2026-08-13 sur docs.rs -prost/prost-types = 0.14.x -tonic = 0.14.x -``` - -`yellowstone-grpc-client 13.3.0` dépend notamment de : - -```text -bytes ^1.10.1 -futures ^0.3.24 -hyper ^1.4.1 -hyper-util ^0.1.7 -tokio ^1.47.1 -tonic ^0.14.0 -tonic-health ^0.14.0 -tower ^0.5.0 -yellowstone-grpc-proto ^12.5.0 -``` - -Il expose désormais un `AutoReconnect`, une politique de reconnexion et de la déduplication/replay. Ces capacités sont utiles comme référence, mais ne doivent pas posséder la sémantique publique KSP. - -`yellowstone-grpc-proto 12.6.0` dépend notamment de : - -```text -prost ^0.14.0 -prost-types ^0.14.0 -solana-pubkey ^4.0.0 -thiserror ^2.0.16 -siphasher ^1 -tonic ^0.14.0 optionnel -tonic-prost ^0.14.0 optionnel -bytes ^1.10.1 optionnel -``` - -Le `solana-pubkey ^4.0` du proto est compatible en gamme avec le `^4.3` déjà utilisé par KSP ; l'unification exacte sera vérifiée par Cargo en `pre.002`. - -### 5.3 Licences - -Le dépôt upstream déclare : - -```text -licence par défaut du repository = AGPL-3.0-only -``` - -mais `LICENSING.md` affecte explicitement **Apache-2.0** aux sous-arbres : +Le repository upstream est globalement `AGPL-3.0-only`, mais `LICENSING.md` affecte explicitement Apache-2.0 à : ```text examples/ @@ -280,80 +125,73 @@ yellowstone-grpc-client-nodejs/ yellowstone-grpc-proto/ ``` -Conséquence du gate : - -- dépendre de la crate publiée `yellowstone-grpc-proto` est compatible avec la distribution MIT de KSP sous réserve des obligations Apache usuelles ; -- aucun fichier provenant des zones AGPL du repository ne sera copié dans KSP ; -- KSP ne vendore pas les `.proto` dans la stratégie retenue ; -- si un fichier upstream devait être copié ultérieurement, sa provenance et sa licence seraient réauditées fichier par fichier avant incorporation. - -Sources : +Décision : ```text -https://github.com/rpcpool/yellowstone-grpc/blob/master/LICENSING.md -https://docs.rs/crate/yellowstone-grpc-proto/latest +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 ``` -### 5.4 Compatibilité toolchain / build +### 4.3 Stratégie client -KSP suit la **Rust stable courante de l'opérateur** et ne documente pas de numéro Rust upstream comme objectif de projet. Le seul gate utile est opérationnel : les dépendances finalement retenues doivent compiler avec la stable courante utilisée par le workspace. +| 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 | -Résultat après matérialisation `pre.003` : +Matérialisation courante : ```text -yellowstone-grpc-proto ^12.6 runtime : default-features = false, aucune feature `tonic` - dev/test : feature `tonic` uniquement pour GeyserServer de fixture -tonic ^0.14 runtime : channel + tls-aws-lc + tls-webpki-roots - dev/test : codegen + server ajoutés explicitement -http ^1.5 direct KSP pour PathAndQuery unary sans constructeur panic -tonic-prost ^0.14 direct KSP pour ProstCodec ; sa dépendance Tonic désactive les defaults +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 crate publiée/générée ; aucun .proto copié dans KSP -protoc aucun outil système KSP ajouté ; la crate proto publiée utilise son build vendored +proto vendored absent ``` -La séparation reste intentionnelle : les messages Protobuf officiels sont consommés sans activer le client généré Yellowstone dans le runtime. La feature `tonic` du proto est réservée au graphe dev/test pour matérialiser le serveur Geyser de fixture. KSP utilise le dispatcher `tonic::client::Grpc` + `tonic-prost::ProstCodec` derrière sa façade N2 et conserve ainsi ses propres timeouts, redactions et erreurs. +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`. -Un minimum Rust déclaré par une dépendance n'est enregistré que s'il devient un **blocage réel** lors de la compilation ; il n'est pas suivi comme métrique de release. +## 5. Matrice protocolaire fermée -## 6. Matrice du service `Geyser` courant +### 5.1 Service `Geyser` -Le proto publié `yellowstone-grpc-proto 12.6.0` et le proto master exposent le même inventaire de service observé pendant le gate : +| 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** | -| RPC | Forme | Statut | Cible `0.2.9` | Décision | -|-----------------------|-------------|----------------------------------------------------------------|---------------|----------------------------------------------| -| `Subscribe` | bidi stream | standard Yellowstone | **oui** | fondation principale | -| `SubscribeDeshred` | bidi stream | présent dans proto, trajectoire Triton extension/pré-exécution | **non** | report explicite | -| `SubscribeReplayInfo` | unary | standard | **oui** | information de replay, pas garantie lossless | -| `Ping` | unary | standard | **oui** | canari/liveness unary | -| `GetLatestBlockhash` | unary | standard | **oui** | canari typed | -| `GetBlockHeight` | unary | standard | **oui** | canari typed | -| `GetSlot` | unary | standard | **oui** | canari typed | -| `IsBlockhashValid` | unary | standard | **oui** | canari typed | -| `GetVersion` | unary | standard | **oui** | canari typed | +`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. -`SubscribeDeshred` est techniquement publié dans le proto, mais le changelog upstream le rattache explicitement aux **Triton Extension Patches** et décrit la réception de transactions avant exécution. Il reste donc hors fondation provider-neutral `0.2.9`. +### 5.2 `SubscribeRequest` -## 7. Matrice `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 | -Surface standard retenue intégralement : - -| Champ | Type / sémantique | `0.2.9` | -|-----------------------|-------------------------------------------------|--------------------------| -| `accounts` | map nom -> `SubscribeRequestFilterAccounts` | oui | -| `slots` | map nom -> `SubscribeRequestFilterSlots` | oui | -| `transactions` | map nom -> `SubscribeRequestFilterTransactions` | oui | -| `transactions_status` | même famille de filtre transaction | oui | -| `blocks` | map nom -> `SubscribeRequestFilterBlocks` | oui | -| `blocks_meta` | map nom -> filtre vide | oui | -| `entry` | map nom -> filtre vide | oui | -| `commitment` | optional Processed/Confirmed/Finalized | oui | -| `accounts_data_slice` | repeated offset/length | oui | -| `ping` | optional request ping/id | oui | -| `from_slot` | optional u64 | oui, sémantique prudente | - -### 7.1 Accounts +Accounts : ```text account[] @@ -361,39 +199,22 @@ owner[] filters[] nonempty_txn_signature? cuckoo_accounts_filter? -``` -Filtres account : - -```text memcmp { offset, oneof bytes | base58 | base64 } datasize token_account_state lamports { oneof eq | ne | lt | gt } ``` -Les formes Cuckoo actuelles sont conservées parce qu'elles appartiennent au proto publié standard observé, pas parce qu'un provider particulier les demande. - -### 7.2 Slots +Slots : ```text filter_by_commitment? interslot_updates? +statuses = processed | confirmed | finalized | first_shred_received | completed | created_bank | dead ``` -`SlotStatus` courant : - -```text -processed -confirmed -finalized -first_shred_received -completed -created_bank -dead -``` - -### 7.3 Transactions / transaction_status +Transactions et `transaction_status` : ```text vote? @@ -406,9 +227,7 @@ cuckoo_account_include? token_accounts? = ALL | BALANCE_CHANGED ``` -L'optional `token_accounts` contrôle l'expansion vers les owners de token accounts dans les balances pre/post ; son absence est distincte de ses deux valeurs connues. - -### 7.4 Blocks +Blocks : ```text account_include[] @@ -418,597 +237,459 @@ include_entries? cuckoo_account_include? ``` -### 7.5 BlocksMeta / Entry +`blocks_meta` et `entry` conservent la distinction absence / map vide / filtre nommé vide. -Les deux filtres sont actuellement des messages vides : leur **présence nommée** active la famille ; KSP doit donc conserver la distinction absence / map vide / entrée nommée vide au niveau de son contrat logique. - -### 7.6 Common +Bornes KSP communes matérialisées : ```text -commitment? Processed | Confirmed | Finalized -accounts_data_slice offset + length -ping? id -from_slot? u64 +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 ``` -KSP applique des bornes déterministes avant I/O sur les noms, cardinalités, listes de comptes/owners, memcmp, data slices et tailles de payload. Les valeurs exactes sont un contrat KSP et non une copie aveugle des quotas d'un provider. `pre.004` matérialise les bornes communes suivantes : +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 -filter groups nommés, total <= 1024 sur les sept maps -filter name non vide, trim exact, sans caractère de contrôle, <= 128 octets -filter names uniques globalement entre les sept maps -accounts_data_slice count <= 128 -accounts_data_slice length <= 64 MiB -offset + length aucun overflow u64 -``` - -Les bornes Accounts sont matérialisées en `pre.005`; les bornes include/exclude/required et Cuckoo propres aux Transactions/Blocks restent dans `pre.007–008`. - -## 8. Matrice `SubscribeUpdate` - -Le `oneof update_oneof` standard contient exactement neuf variantes observées : - -| Variante | Champs structurants à préserver | -|----------------------|-----------------------------------------------------------------------------| -| `account` | account info + slot + `is_startup` | -| `slot` | slot + optional parent + status + optional dead_error | -| `transaction` | signature/is_vote/transaction/meta/index + slot | -| `transaction_status` | slot/signature/is_vote/index/error | -| `block` | slot/hash/rewards/time/height/parent/counts + transactions/accounts/entries | -| `ping` | marker server ping | -| `pong` | id | -| `block_meta` | block metadata/counts sans tableaux complets | -| `entry` | slot/index/num_hashes/hash/transaction counts/index | - -Le top-level contient aussi : - -```text -filters[] noms de filtres correspondants -created_at google.protobuf.Timestamp -``` - -Les types imbriqués `solana-storage.proto` nécessaires aux transactions/blocs seront projetés dans des types KSP sans perte arbitraire de champs utiles. Les types Prost/Yellowstone générés restent internes au backend et ne sont pas réexportés dans l'API publique. - -## 9. RPCs unary retenus - -| RPC | Request | Response | Notes | -|-----------------------|---------------------------|------------------------------------------|----------------------------------------| -| `SubscribeReplayInfo` | vide | `first_available?` | information de disponibilité seulement | -| `Ping` | `count` | `count` | exact echo attendu | -| `GetLatestBlockhash` | `commitment?` | slot, blockhash, last_valid_block_height | typed | -| `GetBlockHeight` | `commitment?` | block_height | typed | -| `GetSlot` | `commitment?` | slot | typed | -| `IsBlockhashValid` | blockhash + `commitment?` | slot + valid | typed | -| `GetVersion` | vide | version | opaque string bornée | - -Les unary ne remplacent pas les méthodes HTTP équivalentes : ce sont des capacités du backend Yellowstone et restent séparées des wrappers JSON-RPC HTTP existants. - -## 10. Replay, reconnexion et continuité - -Le changelog upstream contient deux signaux qui interdisent une promesse simpliste : - -```text -2026-07-22 : correction d'un replay blocks from_slot accepté mais reprenant live avec state gap -2026-06-15 : auto-reconnect upstream modifié pour mettre le replay en quarantaine et comparer les blockhashes afin de traiter l'equivocation entre nodes -``` - -Décision KSP : - -```text -reconnect automatique oui, borné et KSP-owned -resubscribe déterministe oui -from_slot oui, sans promesse lossless -SubscribeReplayInfo oui, informatif -exactly-once non garanti -lossless non garanti -ordre global sans gap non garanti -duplicate possible oui, observable/traité selon scope -gap possible oui, observable -equivocation node/fork observable quand preuve disponible -``` - -Observabilité cible : - -```text -reconnect_count -continuity_gap_count -duplicate_update_count -replay_attempt_count -last_requested_from_slot -last_observed_slot -terminal error code safe -``` - -Une détection d'equivocation peut nécessiter une preuve de blockhash ; si elle ne peut pas être généralisée proprement à toutes les familles, le plan exige de documenter sa couverture exacte au lieu de l'annoncer globalement. - -## 11. Stratégie dépendances — A/B/C - -### A. `yellowstone-grpc-client + yellowstone-grpc-proto` - -Avantages : client prêt, TLS/connect/reconnect déjà implémentés. - -Inconvénients : - -- sémantique autoreconnect/replay/dedup upstream importée implicitement ; -- davantage de dépendances et types upstream ; -- risque de fuite de types/client brut dans l'API KSP ; -- contrôle moindre sur redaction, backpressure et lifecycle. - -**Décision : non retenue comme stratégie principale.** Le client reste une référence et peut servir ponctuellement à vérifier le wire dans les tests/outils si nécessaire, sans devenir contrat public. - -### B. `yellowstone-grpc-proto + client KSP autour de tonic` - -Avantages : - -- proto publié Apache-2.0, pas de copie vendored ; -- wire officiel généré disponible ; -- Tonic 0.14 aligné avec l'écosystème HTTP/2 moderne déjà présent ; -- KSP garde reconnect/backpressure/errors/redaction ; -- types upstream cachés derrière les DTOs KSP ; -- `solana-pubkey` reste dans la même major actuelle. - -**Décision : stratégie cible retenue.** - -Matérialisation après `pre.003` : - -```text -yellowstone-grpc-proto ^12.6 runtime sans feature ; dev/test feature `tonic` pour le serveur de fixture uniquement -tonic ^0.14 runtime features = ["channel", "tls-aws-lc", "tls-webpki-roots"] - dev/test ajoute ["codegen", "server"] -tonic-prost ^0.14 runtime, ProstCodec bas niveau -http ^1.5 runtime, PathAndQuery borné/interne -yellowstone-grpc-client absent -prost/prost-types pas de dépendance KSP directe -tokio-stream pas ajouté directement -futures-util dépendance existante réutilisée par la fixture Stream -``` - -`pre.003` n'active toujours ni client Yellowstone upstream ni compression. Les sept unary passent par une façade KSP au-dessus du channel N1 ; la surface `GeyserServer` générée n'existe que dans les tests pour prouver le wire exact localement. Le graphe Cargo doit être réinspecté après application parce que les features TLS et les deux dépendances directes `http`/`tonic-prost` changent le graphe runtime. - -Le premier gate opérateur de `pre.002` confirme l'alignement de versions utile : Tonic 0.14.6 réutilise `http` 1.5, `hyper` 1.11, `hyper-util` 0.1, `tower` 0.5 et `bytes` 1.12 déjà présents ; `yellowstone-grpc-proto` unifie `solana-pubkey` en 4.3.0. Les occurrences Prost 0.14.4 visibles dans `cargo tree --duplicates` correspondent aux contextes runtime/build de la même version, notamment `prost-build`/`tonic-prost-build`, et ne constituent pas une seconde génération de version à corriger. - -### C. proto/génération KSP minimale bornée - -Avantage : contrôle maximum du code généré. - -Inconvénients : copie/licence/synchronisation du proto, `build.rs`, protoc et dette de suivi plus forte. - -**Décision : reportée/fallback uniquement si B bloque une exigence KSP démontrée.** - -## 12. Architecture publique cible - -### 12.1 Séparation des backends et des niveaux Yellowstone - -```text -HTTP TransportSettings / EndpointClient / pool HTTP existants +HTTP HttpTransportSettings / pool HTTP WebSocket engine WsSession actor partagé Solana standard WS SolanaStandardWsSession Helius LaserStream WS HeliusLaserStreamWsSession -Yellowstone gRPC engine nouveau runtime/session physique partagé -Solana Yellowstone standard façade typed standard sur ce moteur -PublicNode Yellowstone première intégration provider sur standard -future providers adapters/capabilities pouvant réutiliser, restreindre ou étendre le standard +Yellowstone gRPC engine YellowstoneGrpcChannel + moteur bidi KSP +Solana Yellowstone standard contrats typed KSP +provider descripteur d'exécution/capability, pas nouveau protocole ``` -Cette structure reprend le principe validé par `0.2.7`/`0.2.8` : **le moteur physique n'est jamais recopié par provider**. En revanche, l'intégration provider doit pouvoir exprimer un sous-ensemble du standard, des overrides de comportement ou des extensions wire réelles ; l'équivalence complète avec N2 n'est jamais supposée. - Interdictions : ```text pas de WsProtocolKind pour gRPC -pas de WsEndpointSettings réutilisé -pas de WsSession déguisée +pas de WsEndpointSettings pour gRPC pas de client Tonic brut réexporté -pas de second actor Yellowstone par provider -pas de façade provider vide uniquement pour renommer le même protocole +pas de second moteur physique par provider +pas de façade provider vide qui ne ferait que renommer le standard ``` -### 12.2 Noms et ownership - -Noms cibles, affinables sans casser le principe : +### 6.2 Contrats publics principaux matérialisés ```text -# N1 — moteur Yellowstone matérialisé en pre.002 YellowstoneGrpcEndpointUrl YellowstoneGrpcProviderName YellowstoneGrpcClusterName +YellowstoneGrpcMetadataEntry YellowstoneGrpcReconnectSettings -YellowstoneGrpcEndpointSettings YellowstoneGrpcSessionSettings +YellowstoneGrpcEndpointSettings YellowstoneGrpcTransportSettings -YellowstoneGrpcChannel # channel Tonic lazy privé, sans I/O réseau en pre.002 +YellowstoneGrpcChannel -# N1 — étapes ultérieures -YellowstoneGrpcSession -YellowstoneSubscriptionHandle - -# N2 — standard Solana Yellowstone -SolanaYellowstoneGrpcSession # façade candidate, si le gate API confirme ce nom -YellowstoneSubscribeRequest / filters KSP -YellowstoneUpdate / typed update projections - -# N3 — provider -provider descriptor/capabilities ouverts -standard capabilities supportées / restreintes explicitement -provider extensions typed si nécessaires -provider auth/metadata/compression/replay/lifecycle policy -PublicNode integration/profile/canaries -PublicNode-specific facade seulement si une différence réelle le justifie +YellowstoneSubscribeRequest + filtres typed +YellowstoneSubscribeUpdate + variantes typed +SolanaYellowstoneGrpcSubscribeSession +YellowstoneGrpcSubscribeSnapshot +7 unary typed ``` -Le backend wire Tonic/Prost reste privé. Tous les types publics nécessaires sont réexportés au crate root conformément aux règles KSP. Le provider et le protocole restent deux axes distincts : `PublicNode` décrit **où/comment et avec quelles capabilities** on exécute Yellowstone. Si une capacité provider est strictement standard, elle réutilise N2 ; si elle diverge, N3 porte explicitement cette divergence. +Le wire Tonic/Prost reste privé et n'est pas une escape hatch publique. -### 12.3 Metadata/auth provider-neutral +### 6.3 Credentials et diagnostics -Transport reçoit : +Transport reçoit des valeurs déjà résolues par son consumer. Il ne connaît : ```text -metadata publique bornée -metadata sensible via wrapper opaque/redacted -``` - -Il ne connaît : - -```text -aucun nom KSP_SECRET_* +aucun KSP_SECRET_* +aucun KSP_PUBLIC_* aucun std::env -aucun header PublicNode/OrbitFlare/Helius hardcodé comme contrat standard +aucun header commercial hardcodé dans le standard ``` -Les clés metadata sont validées avant I/O ; les valeurs sensibles ne sont jamais dans `Debug`, `Display`, `KspError`, logs ou snapshots. +Les URLs, metadata sensibles, messages/details de `tonic::Status` et payloads arbitraires ne sont pas recopiés dans `Debug`, `Display`, snapshots ou contexts KSP. -## 13. Config V3 cible +## 7. Lifecycle, backpressure et continuité -Le schéma V2 actuel est fermé et possède : +### 7.1 Stream bidi + +Acquis depuis `pre.009` : ```text -profiles[].endpoints -profiles[].ws_endpoints +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 ``` -Ajouter gRPC dans V2 ferait évoluer silencieusement une shape fermée. Le gate retient donc **une V3 explicite**, tout en conservant V1/V2 backward-readable. +Aucune queue non bornée et aucun drop silencieux n'est présenté comme lossless. -Shape conceptuelle cible : +### 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 -globals.grpc_defaults -profiles[].grpc_endpoints[] +retry +ws_defaults +grpc_defaults +default_profile +profiles[] { + profile_id + endpoints[] + ws_endpoints[] + grpc_endpoints[]? # optionnel par profil +} ``` -Chaque endpoint gRPC doit pouvoir porter au minimum : +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 descriptif -cluster descriptif +provider +cluster +protocol = solana_yellowstone url -metadata publique optionnelle -secret_metadata optionnelle -session/runtime overrides bornés optionnels +metadata[]? # classe publique +secret_metadata[]? # classe secrète +session? # overrides bornés ``` -Règle de sensibilité : - -- `metadata` ne contient que des valeurs non secrètes ; -- `secret_metadata` est une classe séparée ; -- Config résout les placeholders et exige une provenance/sensibilité secret appropriée avant mapping ; -- Transport reçoit une valeur opaque/redacted et ne sait pas quel env l'a produite. - -`grpc_defaults` porte les defaults génériques utiles : +Axes distincts : ```text -connect timeout -unary timeout -close timeout -max inbound/outbound message size -request/update channel capacities -max logical filter groups/names -reconnect attempts/backoff +protocol = contrat wire standard, actuellement solana_yellowstone +provider = environnement d'exécution descriptif, par exemple publicnode ``` -La forme JSON exacte et les bornes sont matérialisées en `pre.011`, mais **la décision V3 + `grpc_endpoints` séparés + metadata publique/secrète séparée est fermée par `pre.001`**. +`provider = publicnode` ne crée donc pas un `PublicNodeGrpcProtocol` ni une façade provider sans divergence réelle. -## 14. Audit fournisseurs gRPC gratuits et durables +### 8.4 Provenance des metadata -L'objectif n'est pas de sélectionner un SDK provider mais de disposer de smokes accessibles sans abonnement payant éphémère. - -### 14.1 PublicNode / Allnodes — priorité 1 - -PublicNode annonce explicitement des endpoints « free-est » et liste pour Solana : +Config est propriétaire de la résolution : ```text -Mainnet: Yellowstone GRPC -Testnet: GRPC +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 ``` -Endpoint Mainnet affiché officiellement au gate : +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) +``` + +`pre.011` ajoute : + +```text +grpc_settings() +into_all_transport_settings() -> (HTTP, Option, Option) +``` + +Le tuple historique n'est pas modifié silencieusement. + +## 9. PublicNode dans `0.2.9` + +### 9.1 Mainnet + +La surface publique réauditée le 2026-08-24 confirme Yellowstone gRPC Solana Mainnet et affiche le host/port : ```text solana-yellowstone-grpc.publicnode.com:443 ``` -PublicNode est un service soutenu/opéré par Allnodes dans l'écosystème actuel ; il ne faut pas modéliser « PublicNode » et « Allnodes » comme deux protocoles Yellowstone distincts. +Le wrapper KSP attend une URL `http/https`; Config matérialise donc le même endpoint TLS sous la forme `https://solana-yellowstone-grpc.publicnode.com:443`. -Décision `0.2.9` : +`pre.011` versionne donc un profil : ```text -PublicNode = première intégration provider concrète du moteur Yellowstone -PublicNode Mainnet = premier environnement live opt-in sans secret -PublicNode Testnet = second cluster du même provider si endpoint exact confirmé live -provider/capabilities/profile explicitement représentés -aucun second moteur/actor/session physique -façade PublicNode spécialisée seulement si une différence réelle est démontrée -Allnodes n'est pas modélisé comme un protocole distinct +profile_id = publicnode_mainnet +provider = publicnode +cluster = mainnet-beta +protocol = solana_yellowstone +credential = aucun ``` -La page officielle confirme l'existence de Testnet GRPC, mais le hostname exact n'est pas figé dans ce gate tant qu'il n'a pas été confirmé depuis la surface officielle/live. Il sera vérifié avant ajout d'un profil committé. +La partie HTTP/WS de ce profil reste standard Solana ; seul l'endpoint gRPC est PublicNode. -Sources : +### 9.2 Testnet + +PublicNode affiche toujours une offre Solana Testnet gRPC, mais le hostname exact n'est pas exposé de façon suffisamment autoritative dans la surface publique inspectable pendant `pre.011`. + +Décision : ```text -https://publicnode.com/ -https://solana-yellowstone-grpc.publicnode.com/ +existence Testnet gRPC confirmée +hostname Testnet exact non inventé +profil Testnet committé non en pre.011 +réaudit endpoint/live pre.012 ``` -### 14.2 OrbitFlare — priorité 2 +Un échec à confirmer le hostname Testnet ne bloque pas la fondation Mainnet ; il doit être documenté explicitement au gate final. -Le pricing officiel courant annonce pour le plan Free : +## 10. Threat model et bornes + +Menaces couvertes : ```text -$0/mo -10 RPS -1 TPS -gRPC Access = Devnet only -Credit Limits = Unlimited +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 ``` -Cela répond au besoin « gratuit durable » mieux qu'un trial de quelques jours, mais nécessite un compte/credential. - -Décision de séquence : +Réponses : ```text -OrbitFlare = release provider dédiée 0.2.10 -Devnet Free = cible live prioritaire de cette release -auth/capabilities/limites = auditées comme delta provider N3 -réutilisation de N2 seulement pour les capacités réellement compatibles -aucun OrbitFlareGrpc* public vide si aucune divergence de contrat ne le justifie +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 ``` -Source : +## 11. Smoke ownership + +Ordre de preuve : ```text -https://orbitflare.com/pricing +Transport programmatic -> PublicNode Mainnet Yellowstone +Transport programmatic -> PublicNode Testnet seulement si endpoint exact confirmé +Config V3 -> Transport -> PublicNode via composition légitime, jamais un smoke réseau placé dans Config par facilité ``` -### 14.3 Helius LaserStream gRPC — provider planifié après OrbitFlare +Un smoke Transport pur peut vivre dans `ksp-onchain-transport-lib/tests` puisqu'il construit ses settings programmatiquement. -Helius reste volontairement hors `0.2.9` et `0.2.10`. Sa release dédiée `0.2.11` devra auditer son delta réel avec Yellowstone upstream : auth, endpoints, replay, reconnect/continuity, capacités supplémentaires ou restrictions, et toute extension wire éventuelle. La compatibilité Yellowstone annoncée ne vaut pas preuve d'équivalence complète. - -### 14.4 TODO/IDEAS — autres providers non planifiés - -Aucune version n'est réservée actuellement pour les providers suivants : - -```text -TODO eRPC — réauditer accès, auth/IP policy, capabilities et éventuels produits Burst/Shred séparés -TODO Triton — réauditer upstream vs extensions Triton, notamment Deshred et évolutions futures -TODO Alchemy — réauditer capabilities, auth, replay et limites du produit Yellowstone -TODO QuickNode — réauditer auth, compression, from_slot, filtres et limites par plan -TODO Chainstack — réauditer add-on, networks, auth et capabilities -IDEAS Tatum — accès borné par lifetime credits ; intérêt secondaire -IDEAS Shyft — réauditer seulement si gRPC durable devient accessible -IDEAS Solinfra — free tier + Yellowstone annoncés mais entitlement gRPC gratuit non confirmé -IDEAS NodeFlare — réauditer seulement si offre Yellowstone gratuite durable apparaît -``` - -Ces entrées n'ont **aucun numéro de release**, aucun forecast et aucun engagement d'implémentation. Elles ne sont reprises dans la séquence active que sur décision explicite ultérieure. Bitquery/CoreCast reste hors de cette file Yellowstone tant que son protocole n'est pas Yellowstone standard. - -## 15. Smoke ownership - -Le smoke live reste opt-in et n'autorise aucune violation architecturale. - -Hiérarchie cible : - -```text -1. Transport pur programmatic -> PublicNode Mainnet, sans secret -2. Transport pur programmatic -> PublicNode Testnet, si endpoint exact confirmé -3. Config V3 -> Transport -> PublicNode profile, si Config est matérialisée dans 0.2.9 - -OrbitFlare et Helius sont validés dans leurs releases dédiées. Les autres providers ne font l'objet d'aucun smoke planifié tant qu'ils restent en TODO/IDEAS. -4. Tatum Mainnet authentifié, opérateur-only si utile -``` - -Le smoke 1/2 peut vivre dans `ksp-onchain-transport-lib/tests` car il construit ses settings programmatiquement et ne teste que Transport. - -Le smoke 3 ne doit pas être ajouté à `ksp-config-lib` par facilité. Si aucune surface d'intégration dédiée n'existe encore, il peut rester une procédure opérateur/documentée ou être placé sur une surface de composition déjà légitime ; le plan doit revalider l'owner au moment de `pre.011/pre.012`. +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 procédure reste opérateur/documentée en `pre.012`. Aucun secret provider n'est versionné. -## 16. Threat model et bornes +## 12. État des tranches et historique compact -### 16.1 Secrets / diagnostics +Les preuves détaillées restent dans les fichiers `deltas/0.2.9/*.md`. Le plan ne duplique plus leurs journaux complets. -Menaces : +| 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 | **ACTIVE CANDIDATE** | +| `pre.012` | live/compliance/docs/graph/prompt suivant | **PLANNED** | + +Gate opérateur de fermeture `pre.010-fix.001` : ```text -URI avec credential -metadata gRPC sensible -Status/message/details provider arbitraires -Debug dérivé de requests/filtres -TLS/connect errors réémettant URI/metadata +fmt/audit/check/clippy PASS sans warning +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 ``` -Réponse : projections sûres, error codes KSP, contexts allowlistés et redaction testée. +## 13. Forecast restant -### 16.2 Ressources +### `pre.011` — Config V3 + PublicNode Mainnet -À borner avant I/O : +Cible : ```text -URL/metadata key/value lengths -connect/unary/close timeouts -max inbound/outbound message sizes -request/update channel capacities -nombre de filter groups -longueur et unicité des filter names -account/owner/include/exclude/required counts -memcmp filters + payload size -data slices -Cuckoo filter dimensions/data size -block/account/transaction update payload -reconnect attempts/backoff +workspace.package.version = 0.2.9-pre.11 +schema std.transport V3 strict + branches V1/V2 +Config adapter gRPC +metadata publique/secrète + provenance +protocol/provider distincts +profil publicnode_mainnet +compatibilité API V2 conservée +plan 016 + validation 012 réorganisés ``` -### 16.3 Backpressure - -Politique : +Preuves : ```text -aucune queue non bornée -aucun drop silencieux présenté lossless -overflow observable -slow logical subscription isolée si possible -shutdown déterministe +schema Draft 2020-12 valide +fixtures V1/V2/V3 +Config unit/public API/ownership +Transport non-régressé +workspace complet ``` -### 16.4 Lifecycle adversarial +### `pre.012` — fermeture technique et live -Tester au minimum : +Cible : ```text -server half-close -client close -remote Status -malformed/unknown enum -oneof absent/inattendu -oversized inbound/outbound -stream flood -mutation de filtres pendant updates -late update après mutation/unsubscribe -reconnect loop -shutdown during reconnect -reconnect sur node divergent -duplicate/gap après from_slot/replay -TLS/certificate failure -unary timeout +réaudit upstream final +réaudit PublicNode Testnet hostname +smoke PublicNode Mainnet opt-in +Testnet opt-in seulement si endpoint exact confirmé +compliance HTTP 52 current + 14 historical +compliance Standard WS 18/18 +Helius WebSocket non régressé +cargo tree direct + duplicates final +README/USAGE Transport synchronisés +matrice validation fermée +prompt 0.2.10 préparé selon la séquence active +workspace final vert ``` -## 17. Forecast souple recalibré — **15–20 min max par prerelease ; 1 release <= 1 session** +Si `pre.012` devient trop large, une `pre.013+` est créée ; le numéro n'est pas une deadline. -Règles de dimensionnement **obligatoires** : +## 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 final montre une dette dépendance/licence ou une non-régression qui ne peut pas être corrigée proprement dans la tranche. + +Le noyau à préserver reste : ```text -chaque prerelease = tranche nominale de 15–20 minutes de travail effectif maximum -si une tranche paraît dépasser 20 minutes -> la scinder avant implémentation -0.2.9 complète = doit rester ouvrable et clôturable dans une seule session de chat -si la clôture dans la session devient incertaine -> scinder la release avant dette lourde -le numéro final des prereleases n'est jamais une deadline +moteur Yellowstone + façade Solana standard + Config provider-neutral + première intégration PublicNode minimale ``` -Prévision courante : +## 15. Gates opérateur -```text -pre.001 DONE — audit upstream/service/proto + providers gratuits + licences/deps + architecture + threat model + sizing - budget : 15–20 min nominal ; preuve : plan + matrice + stratégie B + forecast recalibré - -pre.002 DONE — moteur Yellowstone : proto/dependencies + settings/errors + channel minimal - budget : 15–20 min ; gate final fix.002 : fmt/audit/check/Clippy + Transport 346/42/35/4 + dependency canary + workspace PASS - -pre.003 DONE — moteur TLS/metadata + façade N2 unary + fixture locale + 7 unary RPCs - budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS + graphes Cargo inspectés - -pre.004 DONE — standard Solana : Subscribe foundation + maps/commitment/ping/from_slot/data slices/bounds - budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 359/44/37/4 - -pre.005 DONE — standard Solana : Accounts + Slots filters/updates - budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 364/45/38/4 - -pre.006 DONE — structure Transport : namespace privé HTTP explicite - budget : 15–20 min ; gate opérateur final PASS 364/45/39/4 + workspace - -pre.007 DONE — standard Solana : Transactions + transaction_status - budget : 15–20 min ; preuve : include/exclude/required/Cuckoo/token expansion + tx/meta + TransactionConfig V1 - -pre.008 DONE — standard Solana : Blocks + block_meta + entry - budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 370/47/41/4 - -pre.009 DONE — moteur partagé : bidi mutation + Ping/Pong + half-close + backpressure + shutdown - budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 379/48/42/4 - -pre.010 CANDIDATE — moteur partagé : reconnect/resubscribe + from_slot/ReplayInfo + gaps/duplicates - budget : 15–20 min ; preuve source : reconnect local déterministe + ReplayInfo clamp + duplicate observable + aucune promesse lossless - -pre.011 Config V3 + séparation protocol/provider + profils PublicNode Mainnet/Testnet - budget : 15–20 min ; preuve : V1/V2 backward + schema/mapping/redaction + Config -> Transport - -pre.012 intégration PublicNode + smokes live + compliance + docs finales + prompt 0.2.10 OrbitFlare - budget : 15–20 min ; preuve : Mainnet/Testnet opt-in + HTTP 52/14 + WS 18/18 + Helius + cargo graphs + workspace final - -rel.001 publication stable stricte -``` - -Prévision : **12 prereleases**, soit environ **180–240 minutes de travail effectif nominal hors temps d'attente des commandes**, compatible avec une session complète. Si une tranche réelle excède son budget ou si `pre.012` ne peut pas raisonnablement fermer la release dans la session, on scinde avant de poursuivre au lieu de prolonger artificiellement `0.2.9`. - -### Critères de split - -Scinder avant dette silencieuse si l'un de ces cas apparaît : - -1. `yellowstone-grpc-proto + tonic` impose une incompatibilité avec la Rust stable courante ou un doublon majeur de stack réseau impossible à justifier ; -2. les DTOs transactions/blocs exigent une réexposition massive des types upstream ou une réimplémentation disproportionnée ; -3. l'upstream modifie encore matériellement le proto pendant la release ; -4. le replay/reconnect devient un sous-système plus grand que la foundation ; -5. PublicNode exige finalement un comportement provider-specific assez large pour dépasser la session ; -6. toute prerelease dépasse le budget nominal de 20 minutes sans frontière claire de split. - -En cas de split, le noyau prioritaire à conserver dans `0.2.9` est : - -```text -moteur Yellowstone + façade Solana standard + première intégration PublicNode minimale -``` - -et le reste est replanifié explicitement ; aucune capacité n'est abandonnée silencieusement. - -## 18. Fichiers attendus par tranche - -Cibles réelles ouvertes par `pre.002`, puis cibles probables suivantes : - -```text -# pre.002 -crates/ksp-onchain-transport-lib/src/grpc_settings.rs -crates/ksp-onchain-transport-lib/src/grpc_channel.rs -crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs -crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs - -# pre.003 -crates/ksp-onchain-transport-lib/src/grpc_unary.rs -crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs -crates/ksp-onchain-transport-lib/src/grpc_channel.rs -crates/ksp-onchain-transport-lib/src/grpc_settings.rs - -# pre.004 -crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs -crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs -tests/public_api.rs -tests/release_completeness.rs - -# pre.005+ -crates/ksp-onchain-transport-lib/src/grpc_session.rs -crates/ksp-onchain-transport-lib/src/grpc_updates.rs -unit_tests/ correspondants -tests/yellowstone_grpc_*_smoke.rs -crates/ksp-config-lib/src/transport.rs -config/std.transport.json -config/schemas/std.transport.schema.json -.env.example si des variables provider committées sont introduites -``` - -Les noms exacts restent soumis aux règles de structure du code réel ; ce plan ne force pas un fichier par concept si une composition plus claire apparaît. - -## 19. Gates de validation - -Après chaque changement Rust : +Après changement Rust : ```bash cargo fmt --all @@ -1017,148 +698,54 @@ cargo check --workspace cargo clippy --workspace --all-targets ``` -Tests ciblés : +Pour `pre.011` : ```bash +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-config-lib # seulement si Config modifiée -``` - -À la fermeture technique d'une prerelease : - -```bash +cargo test -p ksp-core-lib --test workspace_dependencies cargo test --workspace ``` -Après ajout/modification de la stack gRPC : +Le graphe de dépendances gRPC n'est pas modifié par `pre.011`; son gate final complet reste en `pre.012` : ```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 ``` -Inspecter en particulier : +## 16. Conditions de clôture `0.2.9` ```text -yellowstone-grpc-proto -tonic / tonic-prost -prost / prost-types -bytes / http / hyper / hyper-util -tower -rustls / tokio-rustls -features Tonic : channel/TLS runtime ; codegen/server dev ; pas de router/gzip/zstd KSP -solana-* transitifs -``` - - -## 19.1 Gate opérateur final `pre.002-fix.002` - -Preuve opérateur du `2026-08-24` : - -```text -cargo fmt --all PASS -python3 scripts/audit_rust_workspace_rules.py PASS / clean -cargo check --workspace PASS -cargo clippy --workspace --all-targets PASS -Transport unit PASS 346/346 -Transport public_api PASS 42/42 -Transport release_completeness PASS 35/35 -Transport doctests PASS 4/4 -Core workspace_dependencies PASS 3/3 -cargo test --workspace PASS ; seuls smokes/bench diagnostics explicitement ignored -``` - -`pre.002` est donc **fermée**. Les graphes Cargo fournis au premier gate restent valides pour son dependency set ; `pre.003` doit les relancer parce qu'elle active TLS et ajoute `http`/`tonic-prost`. - -## 19.2 Gate source `pre.003` - -Le premier gate opérateur de `pre.003` confirme que la surface fonctionnelle compile et que les tests sont verts, mais Clippy isole 11 diagnostics `implicit_return` exclusivement dans `unit_tests/grpc_unary.rs` : neuf sur les méthodes transformées par `#[tonic::async_trait]` malgré des retours explicites dans leurs corps, et deux sur des closures `and_then`. `pre.003-fix.001` applique une exception de lint localisée à l'implémentation fixture générée par la macro et rend explicites les deux retours de closures. Aucun code runtime N1/N2 n'est modifié. - - - -La candidate matérialise : - -```text -N1 : connect() réel + TLS WebPKI/rustls + metadata ASCII publique/secrète redacted -N2 : exactement 7 unary Yellowstone standard via dispatcher Tonic privé -fixture : GeyserServer local dev-only, metadata + commitment + timeout + Status hostile -OUT : Subscribe, SubscribeDeshred, PublicNode, Config V3, reconnect/stream lifecycle -``` - -Le runtime n'active pas la feature `tonic` de `yellowstone-grpc-proto`; cette feature et `tonic codegen/server` sont réservées au graphe dev/test de la fixture. Le gate Cargo opérateur reste requis avant commit. - -## 19.3 Gate opérateur final `pre.004-fix.001` - -Preuve opérateur du `2026-08-24` : - -```text -cargo fmt --all PASS -python3 scripts/audit_rust_workspace_rules.py PASS / clean -cargo check --workspace PASS sans warning gRPC pre.004 -cargo clippy --workspace --all-targets PASS sans warning gRPC pre.004 -Transport unit PASS 359/359 -Transport public_api PASS 44/44 -Transport release_completeness PASS 37/37 -Transport doctests PASS 4/4 -cargo test --workspace PASS ; seuls smokes/bench diagnostics explicitement ignored -``` - -`pre.004` est donc **fermée**. Aucun changement de dépendance n'ayant eu lieu depuis `pre.003`, les graphes Cargo déjà inspectés restent l'autorité du dependency set. - -## 19.4 Gate source `pre.005` — Accounts + Slots - -La candidate matérialise exactement : - -```text -Accounts request : account[]/owner[]/filters[]/nonempty_txn_signature?/cuckoo_accounts_filter? -Account predicates : memcmp bytes/base58/base64, datasize, token_account_state, lamports eq/ne/lt/gt -Slots request : filter_by_commitment? + interslot_updates? -Account update : filters/created_at + account info + slot + is_startup -Slot update : filters/created_at + slot + parent? + 7 SlotStatus + dead_error? -``` - -Les bornes provider-neutral KSP de cette tranche couvrent notamment les sélecteurs Accounts, predicates, memcmp, Cuckoo, taille account-data, noms de filtres d'update, timestamp nanos, signature fixe 64 octets et `dead_error`. Les Debug KSP n'exposent ni pubkeys sélectionnées, ni payload memcmp/Cuckoo/account-data, ni texte `dead_error`. - -Les conversions protobuf et décodeurs update restent sous `#[cfg(test)]` jusqu'à `pre.009`, car aucun stream runtime ne les consomme encore. Cette décision évite du faux `dead_code` sans créer une seconde implémentation : les mêmes helpers seront remis en runtime au moment de l'ouverture bidi. - -OUT de `pre.005` : Transactions/transaction_status, Blocks/block_meta/entry, stream bidi, provider PublicNode, Config V3 et `SubscribeDeshred`. - -**Verdict `pre.005` : candidate source prête ; fermeture après gate Cargo opérateur.** - -## 20. Conditions de clôture - -`0.2.9` ne devient stable que si : - -```text -inventaire service/proto courant réconcilié -SubscribeDeshred explicitement exclu/classifié -7 unary RPCs retenus validés -Subscribe standard retenu sans perte arbitraire -9 update variants traitées -provider-neutral API sans raw client escape hatch -metadata/secrets redacted -resource bounds et backpressure testés -reconnect/from_slot/replay documentés sans promesse lossless -Config V3 backward V1/V2 si Config intégrée -PublicNode intégration provider matérialisée sans duplication du moteur -PublicNode interop Mainnet validée opt-in ou impossibilité externe documentée -PublicNode Testnet validé si endpoint live confirmé, sinon raison documentée -OrbitFlare/Helius absents du runtime 0.2.9 hors documentation de séquence ; autres providers uniquement TODO/IDEAS +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 validé +Testnet validé si endpoint exact confirmé, sinon report factuel documenté HTTP 52+14 non régressé -standard WS 18/18 non régressé +Standard WS 18/18 non régressé Helius WS non régressé -cargo graphs inspectés -README/USAGE synchronisés -matrice `012` fermée -prompt 0.2.10 OrbitFlare prêt +cargo graphs finaux inspectés +README/USAGE finaux synchronisés +validation 012 fermée +prompt release suivante prêt workspace final vert ``` -## 21. Releases suivantes recalibrées +## 17. Séquence après `0.2.9` -Le gate `pre.001-fix.002` reprend la logique WebSocket : moteur/standard d'abord, puis uniquement les providers effectivement retenus pour implémentation. +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 @@ -1170,268 +757,18 @@ Le gate `pre.001-fix.002` reprend la logique WebSocket : moteur/standard d'abord 0.2.15 program-api foundation ``` -Les autres providers Yellowstone — eRPC, Triton, Alchemy, QuickNode, Chainstack, Tatum, Shyft, Solinfra, NodeFlare et autres — restent en **TODO/IDEAS non numérotés**. Ils ne doivent pas déplacer la séquence active tant qu'une décision explicite d'implémentation n'est pas prise. - -Pour toute intégration provider future, la règle reste : N1 n'est jamais dupliqué ; N2 est réutilisé seulement là où le provider est réellement compatible ; N3 exprime explicitement les restrictions, overrides et extensions. - - -### 19.5 `pre.005-fix.001` — hygiène warning/Clippy et format documentaire - -Le premier gate opérateur de `pre.005` confirme la surface fonctionnelle : 364/364 unit, 45/45 public API, 38/38 release-completeness et workspace complet PASS. Les seuls écarts sont quatre constantes utilisées uniquement par les décodeurs `#[cfg(test)]`, une convention `to_wire(&self)` sur un type `Copy`, et une closure `is_some_and` soumise à `-D clippy::implicit-return`. - -| Correction | Traitement | Impact runtime | -|-------------------------------------------|----------------------------------------------------------------------|-------------------------| -| quatre constantes de bounds update | `#[cfg(test)]` | aucun avant `pre.009` | -| `YellowstoneSubscribeSlotFilter::to_wire` | receiver `self` | aucun, helper test-only | -| closure `dead_error.is_some_and` | `return` explicite | aucun | -| tableaux `016` et `012` | reformatage JetBrains RustRover (largeur max + un espace de padding) | documentaire uniquement | - -Le reformatage documentaire reproduit le comportement RustRover : largeur calculée sur le contenu le plus large de chaque colonne et exactement un espace de padding de chaque côté du contenu avant les pipes. - -**Verdict `pre.005-fix.001` : correctif minimal prêt ; `pre.005` reste ouverte jusqu'à réexécution sans warning de check/Clippy/workspace.** - -### 19.6 Gate final `pre.005-fix.001` - -Le second gate opérateur confirme la fermeture complète de `pre.005` : fmt, audit Rust, check et Clippy passent sans warning ; Transport passe 364 unit + 45 public API + 38 release-completeness + 4 doctests ; le dependency canary Core passe 3/3 et `cargo test --workspace` est vert. - -**Verdict : `pre.005` fermée.** - -## 20. `pre.006` — namespace privé HTTP explicite - -Le développement simultané de HTTP, WebSocket et Yellowstone gRPC rend les anciens noms privés `client`, `executor`, `pool`, `resilience` et `settings` trop ambigus. Ces cinq modules sont exclusivement propriétaires de la pile HTTP et deviennent donc : +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 -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 +TODO eRPC +TODO Triton +TODO Alchemy +TODO QuickNode +TODO Chainstack +IDEAS Tatum +IDEAS Shyft +IDEAS Solinfra +IDEAS NodeFlare ``` -Les unit tests miroirs reçoivent les mêmes noms. Le changement reste privé à la crate : les types publics sont déjà explicitement nommés `Http*` et leurs chemins au crate root ne changent pas. - -Le mini-audit interdit un renommage aveugle des autres modules : `rpc_accounts`, `rpc_blocks`, `rpc_transactions` et `rpc_common` portent des DTOs/types déjà réutilisés par WebSocket et/ou gRPC ; `json_rpc` décrit le protocole d'enveloppe ; `constants` et `error` sont transverses. Ils conservent donc leur nom actuel. - -Cette tranche est volontairement séparée de Transactions afin de respecter le budget 15–20 minutes et d'éviter de combiner un refactor de fichiers avec une nouvelle surface protobuf. L'ancien forecast fonctionnel `pre.006–011` est décalé vers `pre.007–012` sans changement de contenu. - -**Gate candidat :** audit Rust clean, public API inchangée, cinq anciens modules absents après suppression opérateur, cinq nouveaux modules `http_*` compilés, release-completeness canary dédié, workspace complet vert. - - -## 21. Gate final `pre.006` — namespace privé HTTP - -Preuve opérateur du `2026-08-24` : - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 364/364 PASS | -| Transport `public_api` | 45/45 PASS | -| Transport `release_completeness` | 39/39 PASS | -| Transport doctests | 4/4 PASS | -| Core dependency canary | 3/3 PASS | -| `cargo test --workspace` | PASS | - -Les cinq modules privés HTTP et leurs unit tests miroirs sont désormais effectivement nommés `http_*`. Le canari `release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules` passe et les modules partagés `rpc_*`/`json_rpc` restent volontairement non préfixés. - -**Verdict : `pre.006` fermée.** - -## 22. `pre.007` — Transactions + `transaction_status` candidate - -Le proto `yellowstone-grpc-proto 12.6.0` est réaudité avant implémentation. `SubscribeRequestFilterTransactions` contient exactement `vote?`, `failed?`, `signature?`, `account_include[]`, `account_exclude[]`, `account_required[]`, `cuckoo_account_include?` et `token_accounts?`. La même structure filtre `transactions` et `transactions_status`. - -La candidate matérialise : - -```text -request filters - vote? / failed? - signature? : Base58 validé et décodé exactement sur 64 octets - account_include[] / account_exclude[] / account_required[] - cuckoo_account_include? - token_accounts? = ALL | BALANCE_CHANGED - -updates - transaction : filters + created_at + slot + signature/is_vote/index + transaction + meta - transaction_status : filters + created_at + slot + signature/is_vote/index + error? - -solana-storage typed - Transaction / Message / MessageHeader - CompiledInstruction / MessageAddressTableLookup - TransactionConfig V1 (priority_fee/compute_unit_limit/loaded_accounts_data_size_limit/heap_size) - TransactionStatusMeta + TransactionError - InnerInstructions / InnerInstruction - TokenBalance / UiTokenAmount - ReturnData / Reward - loaded writable/readonly addresses - compute_units_consumed? / cost_units? -``` - -Les marqueurs `inner_instructions_none`, `log_messages_none` et `return_data_none` sont conservés séparément de leurs payloads. Les erreurs transaction sont des bytes opaques bornés : aucun décodage Program/runtime arbitraire n'est introduit. Les types protobuf upstream restent internes. Les conversions request et les décodeurs update restent `#[cfg(test)]` jusqu'à l'ouverture du stream runtime en `pre.009`. - -OUT de `pre.007` : Blocks/block_meta/entry, stream bidi/Ping-Pong, reconnect/replay, Config V3, PublicNode et `SubscribeDeshred`. - -**Gate candidat :** audit statique clean ; compilation/Clippy/tests opérateur requis avant fermeture. - - -## 23. Gate final `pre.007` — Transactions + `transaction_status` - -Preuve opérateur du `2026-08-24` : - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 367/367 PASS | -| Transport `public_api` | 46/46 PASS | -| Transport `release_completeness` | 40/40 PASS | -| Transport doctests | 4/4 PASS | -| Core dependency canary | 3/3 PASS | -| `cargo test --workspace` | PASS | - -Le gate confirme la projection Transactions complète, incluant `TransactionConfig` V1 et les marqueurs legacy de `TransactionStatusMeta`, sans régression HTTP/WS et sans avancer Blocks ou bidi. - -**Verdict : `pre.007` fermée.** - -## 24. `pre.008` — Blocks + `block_meta` + `entry` candidate - -Le proto `yellowstone-grpc-proto 12.6.0` est réaudité avant implémentation. `SubscribeRequestFilterBlocks` contient exactement `account_include[]`, `include_transactions?`, `include_accounts?`, `include_entries?` et `cuckoo_account_include?`. `blocks_meta` et `entry` conservent leurs filtres marqueurs vides. - -La candidate matérialise : - -```text -request filter Blocks - account_include[] ordonné et borné - include_transactions? - include_accounts? - include_entries? - cuckoo_account_include? - -SubscribeUpdateBlock - slot / blockhash / parent_slot / parent_blockhash - rewards? + num_partitions? - block_time? / block_height? - executed_transaction_count - transactions[] -> YellowstoneTransactionInfo réutilisé - updated_account_count - accounts[] -> YellowstoneAccountInfo réutilisé - entries_count - entries[] -> YellowstoneEntryInfo - -SubscribeUpdateBlockMeta - même metadata sans payloads transaction/account/entry - -SubscribeUpdateEntry - slot / index / num_hashes / hash[32] - executed_transaction_count - starting_transaction_index -``` - -Les compteurs serveur ne sont volontairement pas comparés aux longueurs des vecteurs : les options `include_transactions`, `include_accounts` et `include_entries` permettent au serveur de rapporter les totaux tout en omettant les payloads correspondants. Les `blockhash`/`parent_blockhash` sont validés comme Base58 représentant 32 octets, et les hash d’entrée restent des 32 octets exacts. `Debug` n’expose aucun hash, account selector, reward pubkey ou payload imbriqué. - -Les conversions request protobuf et décodeurs Block/BlockMeta/Entry restent `#[cfg(test)]` jusqu’à l’ouverture du stream runtime en `pre.009`. Aucun `SubscribeDeshred`, provider PublicNode, Config V3, Ping/Pong lifecycle ou reconnect n’entre dans cette tranche. - -**Gate candidat :** audit statique clean ; compilation/Clippy/tests opérateur requis avant fermeture. - -## 25. `pre.008-fix.001` — hygiène Clippy de la fixture Blocks - -Le premier gate opérateur de `pre.008` confirme l'intégralité du contrat Blocks : fmt/audit/check/tests/workspace passent, avec Transport 370/370 unit, 47/47 public API, 41/41 completeness et 4/4 doctests. Clippy termine également avec succès mais signale un unique `field_reassign_with_default` dans `minimal_transaction_info()` de la fixture `grpc_subscribe`. - -Le fix remplace la construction `TransactionStatusMeta::default()` suivie de `meta.fee = 5_000` par un initialiseur struct avec `fee: 5_000` et `..Default::default()`. Aucun `allow`, aucune API, aucun DTO, aucun wire et aucune logique runtime ne changent. - -**Gate final :** fmt/audit/check/Clippy/workspace PASS sans warning. `pre.008-fix.001` est fermée ; le bidi reste strictement `pre.009`. - -## 26. `pre.009` — stream bidi standard + lifecycle borné candidate - -`pre.009` ouvre pour la première fois le RPC `/geyser.Geyser/Subscribe` en runtime KSP, sans `yellowstone-grpc-client` et sans second moteur physique. `YellowstoneGrpcChannel::open_standard_subscribe()` construit une `SolanaYellowstoneGrpcSubscribeSession` sur le channel Tonic déjà possédé par N1. - -Contrat de la tranche : - -```text -request initial validé + taille protobuf bornée -file request mpsc bornée et directement consommée par Tonic -try_update() non bloquant : validation + encoded_len + Full/Closed explicites -file update mpsc bornée ; slow receiver => terminal backpressure overflow -SubscribeUpdate complet : Account/Slot/Transaction/TransactionStatus/Block/Ping/Pong/BlockMeta/Entry -server Ping => réponse automatique ping-only id=1 -Pong => update KSP observable -server half-close => terminal normal Ok(None) -client close => drop des senders request + half-close + attente close_timeout -Drop session => signal best-effort de half-close borné -Status/malformed/overflow => terminal Failed avec KspError sûr -inbound/outbound max message sizes => Tonic + validation locale outbound -``` - -Les conversions request et décodeurs update introduits sous `#[cfg(test)]` en `pre.004–008` deviennent ici des helpers runtime privés réellement consommés. Aucun type Tonic/protobuf n'est exposé dans l'API publique. - -Le reconnect est volontairement absent de `pre.009` : un server half-close est normal et terminal pour cette tranche ; un `Status` ou une erreur de protocole termine la session. `from_slot`, ReplayInfo, resubscribe, gaps/duplicates et changement de node restent `pre.010`. `SubscribeDeshred`, PublicNode et Config V3 restent hors scope. - -**Gate candidat :** audit statique clean ; fixture locale couvre round-trip, mutation, Ping/Pong, server half-close, client half-close, shutdown hostile borné, Drop best-effort, update overflow, Status distant sûr, update malformed et rejet outbound oversized. Compilation/Clippy/tests opérateur requis avant fermeture. - -## 27. `pre.009-fix.001` — compaction de l’update transaction et hygiène du canari `Send` - -Le gate opérateur de `pre.009` valide intégralement le comportement bidi : fmt/audit/check passent ; Transport passe 379/379 unit, 48/48 public API, 42/42 release-completeness et 4/4 doctests ; le dependency canary Core passe 3/3 et `cargo test --workspace` est vert. Clippy termine avec succès mais signale deux warnings : - -```text -large_enum_variant YellowstoneSubscribeUpdate::Transaction -extra_unused_type_parameters assert_send() dans tests/public_api.rs -``` - -Le fix applique deux corrections sans `allow` : - -- `YellowstoneSubscribeUpdate::Transaction` transporte désormais `Box`. La queue ne réserve donc plus la taille de la variante transaction (~664 octets) pour chaque élément ; le wire et le DTO transaction eux-mêmes restent inchangés. La modification intervient avant fermeture de la candidate `pre.009`. -- le canari `assert_send()` matérialise `PhantomData` afin que le paramètre générique soit effectivement utilisé tout en conservant exactement la même preuve compile-time. - -Aucun changement n’est apporté aux neuf variantes wire, à la mutation request, à Ping/Pong, au half-close, au shutdown, à la backpressure, aux dépendances ou aux frontières de `pre.010`. - -**Gate fix final :** fmt/audit/check/Clippy sans warning + Transport 379/48/42/4 + dependency canary + workspace **PASS le 2026-08-24**. - - -## 28. `pre.010` — reconnect KSP-owned, reprise `from_slot` et continuité prudente - -Le proto courant conserve `SubscribeRequest.from_slot` et le unary `SubscribeReplayInfo.first_available`. Le changelog upstream relu le `2026-08-24` confirme qu’un replay `from_slot` peut avoir des défauts spécifiques de famille : le correctif du `2026-07-22` concernait précisément des subscriptions Blocks acceptées mais reprenant live avec un state gap. KSP ne transforme donc pas la présence de `from_slot` en garantie lossless. - -Politique retenue : - -```text -stream perdu -> état Reconnecting -budget -> max_retries borné déjà dans YellowstoneGrpcReconnectSettings -backoff -> exponentiel, initial/max bornés -request de resubscribe -> dernier request complet accepté -resume slot -> max(from_slot explicite, highest observed slot) -ReplayInfo first_available > resume -> couverture replay indisponible prouvée + compteur + clamp à first_available -ReplayInfo indisponible -> reconnect poursuit sans inférer de gap -mutation pendant reconnect -> rejet explicite, pas de coalescing ambigu -duplicate replay -> identité bornée comptée mais update toujours livrée -shutdown pendant backoff -> interruption, aucune nouvelle ouverture -``` - -`YellowstoneGrpcSubscribeSnapshot` expose `reconnect_count`, `continuity_gap_count`, `duplicate_update_count`, `replay_attempt_count`, `last_requested_from_slot`, `last_observed_slot`, l’état lifecycle et le terminal error code sûr. Le cache d’identité est borné à 512 entrées et ne contient pas les payloads arbitraires complets. - -La détection de gap est volontairement **plus stricte** qu’un simple saut de numéro de slot : avec des filtres Accounts/Transactions/Blocks, l’absence d’update sur un slot intermédiaire peut être normale. Seule une borne de rétention `first_available` supérieure au replay demandé constitue ici une preuve objective que cette plage n'est plus rejouable ; elle ne prouve pas qu'un update correspondant aux filtres existait ou a été perdu. - -De même, KSP ne supprime pas les duplicates : il les observe autour du replay, les compte et continue de les livrer. Cela maintient explicitement les non-promesses `exactly-once`, `lossless` et `ordre global sans gap`. - -Couverture de divergence : les identités Block/BlockMeta incluent le blockhash ; deux histoires de même slot avec blockhash différent ne sont donc pas prises pour un duplicate. Aucune généralisation d’equivocation n’est annoncée pour les familles sans preuve de blockhash. - -**Gate candidat :** fixture locale reconnect/replay, duplicate, gap de couverture replay, budget épuisé, mutation rejetée pendant reconnect et shutdown pendant backoff ; audit statique clean. Aucun changement de dépendance ou feature. - -## 29. `pre.010-fix.001` — hygiène Clippy + synchronisation crate-root - -Le premier gate opérateur de `pre.010` confirme le contrat reconnect/replay : fmt, audit Rust, check, Transport 383/383 unit, 49/49 public API, 43/43 release-completeness, 4/4 doctests et `cargo test --workspace` passent. Clippy termine avec succès mais remonte deux `collapsible_if` dans `grpc_stream.rs`. - -Le correctif fusionne uniquement les deux conditions imbriquées signalées par Clippy : éviction du cache borné d'identités et réponse automatique au Ping Yellowstone. Aucun branchement, code d'erreur, borne, compteur, ordre de replay ou état lifecycle ne change. - -La revue de code relève aussi une description crate-root restée au niveau `pre.009`, affirmant à tort que reconnect/replay sont encore hors tranche. `src/lib.rs` est synchronisé avec `pre.010` pour décrire le reconnect borné, la reprise `from_slot`, les gaps ReplayInfo et les duplicates observables sans promesse exactly-once/lossless. - -Signal technique : - -```text -workspace.package.version = 0.2.9-pre.10.fix.1 -``` - -**Gate fix attendu :** fmt + audit + check + Clippy sans warning + Transport 383/49/43/4 + dependency canary + workspace. Aucune dépendance/feature n'ayant changé, aucun nouveau `cargo tree` n'est requis. `pre.011` ne commence qu'après ce gate. - +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. diff --git a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md index 106a04a..8ae15e7 100644 --- a/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md +++ b/docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md @@ -1,1012 +1,437 @@ - + -# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode +# Validation `0.2.9` — Yellowstone gRPC standard + PublicNode -> **Statut : `pre.010` est fonctionnellement vert sur le gate opérateur (383 unit + 49 public API + 43 completeness + 4 doctests, workspace PASS), avec deux warnings Clippy `collapsible_if`. `pre.010-fix.001` corrige ces warnings et la description crate-root restée au niveau `pre.009`; aucun changement reconnect/replay n'est introduit. Fermeture requise sans warning avant `pre.011`.** +> **Statut courant : `pre.010-fix.001` est fermée sans warning. `pre.011` est candidate et doit prouver Config Transport V3, le mapping Config -> Yellowstone gRPC, la séparation protocol/provider et le profil PublicNode Mainnet. Ce document a été restructuré : les logs détaillés historiques vivent dans `deltas/0.2.9/`; cette matrice porte les exigences et le verdict courant.** -## 1. Autorités du gate +## 1. Autorités et baseline courante -Base interne : +Autorités : ```text -v0.2.8 / archive Gitea stable fournie -workspace.package.version initial = 0.2.8 -deltas/0.2.8/rel.001.md -prompts/014-V0_2_9_START_PROMPT.md +RULES.md + docs/rules/* +plan 016 courant +code réel Transport/Config +prompts/014-V0_2_9_START_PROMPT.md pour les contraintes non supersédées +deltas/0.2.9/* pour l'historique immuable et les décisions postérieures au prompt +upstream Yellowstone primaire ``` -Sources upstream auditées le `2026-08-23` : +Baseline opérateur immédiatement avant `pre.011` (`0.2.9-pre.010-fix.001`) : -```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/LICENSING.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://docs.rs/crate/yellowstone-grpc-client/latest -https://docs.rs/crate/yellowstone-grpc-proto/latest -``` +| Gate | Résultat | +|------------------------------------------|--------------------------| +| `cargo fmt --all` | PASS | +| audit Rust workspace | PASS, 0 candidate export | +| `cargo check --workspace` | PASS | +| `cargo clippy --workspace --all-targets` | PASS sans warning | +| 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 | -Divergence enregistrée : +Cette baseline est le seuil de non-régression de `pre.011` pour Transport. -```text -snapshot du prompt : GitHub release v14.2.2+solana.4.1.0 -réaudit courant : GitHub release v15.1.2+solana.4.2.0 (2026-08-18) -crate client : 13.3.0 -crate proto : 12.6.0 -``` +## 2. Gate dépendances et licence -## 2. Baseline stable +| Exigence | Décision / preuve | État | +|--------------------------------|------------------------------------------|-------------------| +| proto officiel sans vendoring | `yellowstone-grpc-proto ^12.6` | **PASS** | +| pas de client upstream runtime | `yellowstone-grpc-client` absent | **PASS** | +| runtime Tonic KSP-owned | `tonic ^0.14` + `tonic-prost ^0.14` | **PASS** | +| proto subtree compatible | Apache-2.0 selon `LICENSING.md` upstream | **PASS** | +| source AGPL copiée | aucune | **PASS** | +| raw client Tonic public | aucun | **PASS** | +| graph final | réinspection `pre.012` | **PENDING FINAL** | -| Gate | Preuve | État | -|-------------------------------------------|------------------------|------| -| `cargo fmt --all` | log opérateur `v0.2.8` | DONE | -| audit Rust workspace | clean | DONE | -| `cargo check --workspace` | log opérateur | DONE | -| `cargo clippy --workspace --all-targets` | log opérateur | DONE | -| `cargo test --workspace` | log opérateur | DONE | -| Transport unit | 335 passed | DONE | -| Transport public API | 41 passed | DONE | -| Transport release completeness | 34 passed | DONE | -| Transport doctests | 4 passed | DONE | -| `cargo tree -p ksp-onchain-transport-lib` | log opérateur | DONE | -| `cargo tree --duplicates` | log opérateur | DONE | +Le graphe n'est pas modifié par `pre.011`; aucune nouvelle dépendance Cargo n'est introduite dans cette tranche. ## 3. Matrice service `Geyser` -| RPC | Forme | Classification | Scope | Preuve cible | État | -|-----------------------|-------|------------------------------------------------------|-------|-----------------------|--------------------------------| -| `Subscribe` | bidi | standard | IN | fixture locale + live | CANDIDATE pre.009 runtime bidi | -| `SubscribeDeshred` | bidi | Triton extension/pré-exécution malgré présence proto | OUT | canari d'absence/API | OUT | -| `SubscribeReplayInfo` | unary | standard | IN | fixture unary | DONE pre.003 | -| `Ping` | unary | standard | IN | fixture unary | DONE pre.003 | -| `GetLatestBlockhash` | unary | standard | IN | fixture unary | DONE pre.003 | -| `GetBlockHeight` | unary | standard | IN | fixture unary | DONE pre.003 | -| `GetSlot` | unary | standard | IN | fixture unary | DONE pre.003 | -| `IsBlockhashValid` | unary | standard | IN | fixture unary | DONE pre.003 | -| `GetVersion` | unary | standard | IN | fixture unary | DONE pre.003 | +| RPC | Classification | Cible | Preuve | Verdict | +|-----------------------|--------------------------------|-------|---------------------------------|----------| +| `Subscribe` | standard | oui | stream bidi local + lifecycle | **PASS** | +| `SubscribeDeshred` | extension/pré-exécution Triton | non | exclusion documentée | **OUT** | +| `SubscribeReplayInfo` | standard unary | oui | fixture unary + reconnect tests | **PASS** | +| `Ping` | standard unary | oui | fixture exact echo | **PASS** | +| `GetLatestBlockhash` | standard unary | oui | fixture typed | **PASS** | +| `GetBlockHeight` | standard unary | oui | fixture typed | **PASS** | +| `GetSlot` | standard unary | oui | fixture typed | **PASS** | +| `IsBlockhashValid` | standard unary | oui | fixture typed + invalid input | **PASS** | +| `GetVersion` | standard unary | oui | fixture typed | **PASS** | ## 4. `SubscribeRequest` — coverage normative ### 4.1 Top-level -| Champ | Sémantique | Scope | Preuve | -|-----------------------|----------------------------------------|-------|--------------------------| -| `accounts` | map filter-name -> account filter | IN | exact wire + bounds | -| `slots` | map -> slot filter | IN | exact wire | -| `transactions` | map -> transaction filter | IN | exact wire + bounds | -| `transactions_status` | map -> transaction filter | IN | exact wire + bounds | -| `blocks` | map -> block filter | IN | exact wire + bounds | -| `blocks_meta` | map -> empty filter | IN | presence/empty semantics | -| `entry` | map -> empty filter | IN | presence/empty semantics | -| `commitment` | optional Processed/Confirmed/Finalized | IN | omitted/value exact | -| `accounts_data_slice` | repeated offset/length | IN | order + bounds | -| `ping` | optional id | IN | Ping/Pong lifecycle | -| `from_slot` | optional u64 | IN | reconnect/replay tests | +| Champ | Verdict | Preuve minimale | +|-----------------------|----------|----------------------------------| +| `accounts` | **PASS** | maps typed + wire exact | +| `slots` | **PASS** | maps typed + wire exact | +| `transactions` | **PASS** | maps typed + wire exact | +| `transactions_status` | **PASS** | same filter family, separate map | +| `blocks` | **PASS** | maps typed + wire exact | +| `blocks_meta` | **PASS** | named empty marker preserved | +| `entry` | **PASS** | named empty marker preserved | +| `commitment` | **PASS** | optional wire semantics | +| `accounts_data_slice` | **PASS** | order + bounds | +| `ping` | **PASS** | optional id | +| `from_slot` | **PASS** | optional + replay mutation | ### 4.2 Accounts -| Capacité | Scope | Preuve | -|---------------------------|-------|--------------------------------------| -| `account[]` | IN | order/cardinality/address validation | -| `owner[]` | IN | order/cardinality/address validation | -| `filters[]` | IN | AND semantics preserved structurally | -| `nonempty_txn_signature?` | IN | omitted/false/true | -| `cuckoo_accounts_filter?` | IN | exact wire + dimension/data bounds | -| memcmp bytes | IN | exact bytes + bounds | -| memcmp base58 | IN | exact string + validation | -| memcmp base64 | IN | exact string + validation | -| `datasize` | IN | exact u64 | -| `token_account_state` | IN | bool | -| lamports `eq/ne/lt/gt` | IN | oneof exact | +```text +account[] +owner[] +filters[] +nonempty_txn_signature? +cuckoo_accounts_filter? +memcmp bytes/base58/base64 +datasize +token_account_state +lamports eq/ne/lt/gt +``` + +Verdict : **PASS** — wire complet retenu, bounds déterministes, payload Debug redacted, malformed fixed-width rejeté. ### 4.3 Slots -| Capacité | Scope | Preuve | -|--------------------------------------------------|-------|--------------------------| -| `filter_by_commitment?` | IN | optional semantics | -| `interslot_updates?` | IN | optional semantics | -| processed/confirmed/finalized | IN | enum decode | -| first_shred_received/completed/created_bank/dead | IN | enum decode + dead_error | +```text +filter_by_commitment? +interslot_updates? +processed +confirmed +finalized +first_shred_received +completed +created_bank +dead + dead_error? +``` -### 4.4 Transactions / transaction_status +Verdict : **PASS**. -| Capacité | Scope | Preuve | -|-------------------------------------|-------|---------------------| -| `vote?` | IN | optional semantics | -| `failed?` | IN | optional semantics | -| `signature?` | IN | exact/validated | -| `account_include[]` | IN | order/bounds | -| `account_exclude[]` | IN | order/bounds | -| `account_required[]` | IN | order/bounds | -| `cuckoo_account_include?` | IN | exact wire + bounds | -| `token_accounts? = ALL` | IN | optional enum | -| `token_accounts? = BALANCE_CHANGED` | IN | optional enum | +### 4.4 Transactions et transaction_status -### 4.5 Blocks +```text +vote? +failed? +signature? +account_include[] +account_exclude[] +account_required[] +cuckoo_account_include? +token_accounts? = ALL | BALANCE_CHANGED +``` -| Capacité | Scope | Preuve | -|---------------------------|-------|---------------------| -| `account_include[]` | IN | order/bounds | -| `include_transactions?` | IN | optional semantics | -| `include_accounts?` | IN | optional semantics | -| `include_entries?` | IN | optional semantics | -| `cuckoo_account_include?` | IN | exact wire + bounds | +Verdict : **PASS** — request wire, transaction storage/meta, status error et malformed signature couverts. + +### 4.5 Blocks, block_meta, entry + +```text +account_include[] +include_transactions? +include_accounts? +include_entries? +cuckoo_account_include? +blocks_meta marker +entry marker +``` + +Verdict : **PASS** — block complet, metadata, entries et champs optional/legacy couverts. ## 5. `SubscribeUpdate` — coverage normative -| Variante | Champs structurants | Scope | Preuve | -|----------------------|---------------------------------------------------------------|-------|-----------------------| -| `account` | account info, slot, is_startup | IN | exact decode | -| `slot` | slot, parent?, status, dead_error? | IN | exact decode | -| `transaction` | signature, vote, transaction, meta, index, slot | IN | exact decode | -| `transaction_status` | slot, signature, vote, index, err | IN | exact decode | -| `block` | hash/rewards/time/height/parents/counts + tx/accounts/entries | IN | exact decode + bounds | -| `ping` | marker | IN | lifecycle | -| `pong` | id | IN | lifecycle | -| `block_meta` | metadata/counts | IN | exact decode | -| `entry` | slot/index/hash/counts | IN | exact decode | +| Variante | Verdict | +|--------------------------------------|----------| +| account | **PASS** | +| slot | **PASS** | +| transaction | **PASS** | +| transaction_status | **PASS** | +| block | **PASS** | +| ping | **PASS** | +| pong | **PASS** | +| block_meta | **PASS** | +| entry | **PASS** | +| top-level `filters[]` / `created_at` | **PASS** | -Top-level à préserver : +Les raw protobufs ne sortent pas de la façade publique. Les décodeurs rejettent les formes structurellement invalides sans copier des payloads arbitraires dans les erreurs. + +## 6. Settings, bounds et redaction + +| Contrôle | État | +|-----------------------------------------------|--------------------| +| URL `http/https` seulement et longueur bornée | **PASS** | +| URL Debug redacted | **PASS** | +| metadata key/value/count bornés | **PASS** | +| metadata secret/public distincte | **PASS** Transport | +| timeouts non nuls et bornés | **PASS** | +| reconnect bounds cohérents | **PASS** | +| inbound/outbound message sizes | **PASS** | +| request/update queue capacities | **PASS** | +| filter group/name bounds | **PASS** | +| account/owner/memcmp/data slice bounds | **PASS** | +| transaction/block selectors bounds | **PASS** | +| remote `Status` message/details non recopiés | **PASS** | +| Transport -> env/config | absent ou **PASS** | + +## 7. Lifecycle, backpressure et replay + +| Exigence | Verdict | Preuve | +|------------------------------|-----------------|------------------------------------------------| +| stream bidi unique | **PASS** | round-trip fixture | +| mutation request | **PASS** | same request channel | +| server Ping / client reply | **PASS** | actor test | +| Pong decode | **PASS** | update decode | +| server half-close | **PASS** | state terminal observable | +| explicit client close | **PASS** | half-close avant deadline | +| hostile server shutdown | **PASS** | close timeout borné | +| slow receiver | **PASS** | overflow terminal observable | +| oversized inbound | **PASS** | Tonic decoder bound | +| oversized outbound | **PASS** | reject avant queue dispatch | +| reconnect backoff | **PASS** | budget borné | +| shutdown pendant backoff | **PASS** | interruption sans nouvelle connexion | +| resubscribe déterministe | **PASS** | last accepted full request | +| reprise `from_slot` | **PASS** | dernier slot observé + demande explicite | +| ReplayInfo `first_available` | **PASS** | clamp/gap seulement si prouvé | +| duplicate observability | **PASS** | compteur borné, pas de suppression silencieuse | +| exactly-once/lossless | **NON GARANTI** | contrat explicite | + +Snapshot public requis et présent : ```text -filters[] -created_at -oneof absent/inconnu -> comportement explicitement testé +reconnect_count +replay_attempt_count +continuity_gap_count +duplicate_update_count +last_requested_from_slot +last_observed_slot +state/error code safe ``` -## 6. `solana-storage.proto` — nested wire utile +## 8. Gate Config V3 — `pre.011` -Le backend doit préserver les champs nécessaires des structures imbriquées actuelles, sans exposer les types upstream comme contrat public : +### 8.1 Schema et backward compatibility + +Exigences candidate : + +| Exigence | Preuve source actuelle | Verdict avant gate opérateur | +|------------------------------------------|------------------------------------------|------------------------------| +| schema `$id` V3 | `urn:ksp:schema:std.transport:v3` | **SOURCE OK** | +| branches V1/V2 conservées | `documentV1`, `documentV2`, `documentV3` | **SOURCE OK** | +| V1 HTTP-only | fixture historique | **PENDING TEST** | +| V2 HTTP+WS | fixture historique | **PENDING TEST** | +| V3 HTTP+WS+gRPC | fixture V3 + config committée | **PENDING TEST** | +| `grpc_endpoints` optionnel par profil V3 | profiles génériques sans gRPC | **PENDING TEST** | +| no `additionalProperties` relaxation | branches strictes | **SOURCE OK** | + +### 8.2 Mapping runtime + +V3 doit mapper : ```text -ConfirmedBlock -ConfirmedTransaction -Transaction -Message -MessageHeader -MessageAddressTableLookup -TransactionConfig -TransactionStatusMeta -TransactionError -InnerInstructions / InnerInstruction -CompiledInstruction -TokenBalance / UiTokenAmount -ReturnData -Reward / Rewards -UnixTimestamp -BlockHeight -NumPartitions +grpc_defaults -> YellowstoneGrpcSessionSettings +grpc_endpoints[].url -> YellowstoneGrpcEndpointUrl +provider -> YellowstoneGrpcProviderName +cluster -> YellowstoneGrpcClusterName +protocol = solana_yellowstone -> gate Config explicite +metadata -> YellowstoneGrpcMetadataEntry::public +secret_metadata -> YellowstoneGrpcMetadataEntry::secret +session overrides -> merge avec grpc_defaults ``` -États optionnels/legacy particulièrement sensibles : +API : + +| Surface | Exigence | Statut source | +|---------------------------------|-------------------------------------|---------------| +| `http_settings()` | inchangée | **OK** | +| `ws_settings()` | V1 None, V2/V3 selon profil | **OK** | +| `grpc_settings()` | V1/V2 None, V3 optionnel | **ADDED** | +| `into_transport_settings()` | tuple historique HTTP + WS inchangé | **PRESERVED** | +| `into_all_transport_settings()` | nouveau tuple HTTP + WS + gRPC | **ADDED** | + +### 8.3 Provenance et secrets + +Règles candidate : ```text -compute_units_consumed? -cost_units? -inner_instructions_none -log_messages_none -return_data_none -loaded writable/readonly addresses -commission / commission_bps +metadata publique + provenance KSP_SECRET_* -> reject +secret_metadata sans provenance secret -> reject +secret_metadata + variable KSP_PUBLIC_/KSP_* -> reject +secret_metadata + KSP_SECRET_/KSPB_SECRET_ -> accept +segments littéraux autour du secret -> accept +safe_value -> secret segment ******** +Transport Debug -> URL/metadata secret absents ``` -Preuve cible : fixtures issues du wire Protobuf + cas old/current + malformed/oversized, sans conversion JSON arbitraire. +Le test V3 doit démontrer le cas valide et les deux croisements invalides sans exposer les canaris. -## 7. Unary RPC matrix +### 8.4 PublicNode Mainnet -| RPC | Request exact | Response exact | État | -|------------------|---------------------------|-----------------------------|-----------| -| ReplayInfo | empty | `first_available?` | CANDIDATE | -| Ping | `count` | `count` | CANDIDATE | -| LatestBlockhash | `commitment?` | slot/hash/last_valid_height | CANDIDATE | -| BlockHeight | `commitment?` | block_height | CANDIDATE | -| Slot | `commitment?` | slot | CANDIDATE | -| IsBlockhashValid | blockhash + `commitment?` | slot + bool | CANDIDATE | -| Version | empty | version | CANDIDATE | - -## 8. Dépendances / licence - -| Gate | Décision / matérialisation `pre.003` | Preuve / état | -|--------------------------------|---------------------------------------------------------------------|---------------------------------| -| repository default | AGPL-3.0-only | `LICENSING.md` | -| client subtree | Apache-2.0 | `LICENSING.md` | -| proto subtree | Apache-2.0 | `LICENSING.md` | -| strategy A client+proto | rejetée comme default | documenté | -| strategy B proto+Tonic KSP | **retenue ; TLS/unary matérialisés** | source PASS / Cargo pending | -| `yellowstone-grpc-proto` | `^12.6`, `default-features = false`, aucune feature KSP | Cargo tree pending | -| `tonic` | runtime `channel+tls-aws-lc+tls-webpki-roots`; dev `codegen+server` | Cargo tree pending | -| `yellowstone-grpc-client` | absent | manifest canary ajouté | -| `prost/prost-types` direct KSP | absent | manifest / Cargo tree pending | -| `http` / `tonic-prost` direct | `^1.5` / `^0.14`, runtime minimal pour PathAndQuery/ProstCodec | Cargo tree pending | -| strategy C vendored proto | fallback seulement | aucun vendoring | -| system protoc | aucun outil système KSP ajouté | proto publié/build vendored | -| raw upstream types public | interdit ; aucun reexport Tonic/Yellowstone | public API canary ajouté | -| executable Yellowstone deps | interdit | dependency firewall à revalider | - -Versions observées : +Profil committé attendu : ```text -yellowstone-grpc-client 13.3.0 -yellowstone-grpc-proto 12.6.0 -tonic 0.14.x (0.14.6 latest observé) -prost/prost-types 0.14.x (0.14.4 latest observé) +profile_id = publicnode_mainnet +provider = publicnode +cluster = mainnet-beta +protocol = solana_yellowstone +url = https://solana-yellowstone-grpc.publicnode.com:443 # provider affiche host:port ; KSP ajoute le scheme TLS requis +metadata = aucune ``` -## 9. Architecture / ownership +Le mapping doit produire un endpoint TLS Yellowstone standard et `Debug` ne doit pas exposer l'URL. -| Invariant | Preuve cible | État `pre.003` | -|--------------------------------------------------------------------------------|--------------------------------|---------------------------------------| -| backend gRPC distinct HTTP/WS | source/API canary | IMPLEMENTED / Cargo pending | -| aucun `WsProtocolKind` gRPC | source scan | SOURCE PASS | -| Transport owns gRPC | dependency graph | IMPLEMENTED / tree pending | -| Config -> Transport seulement | ownership test | hérité / final TODO | -| Transport -X-> Config | ownership test | SOURCE PASS / Cargo pending | -| Transport -X-> std::env KSP_* | ownership test | SOURCE PASS | -| Logging façade KSP only | logging ownership | SOURCE PASS | -| no raw Tonic client escape hatch | public API canary | IMPLEMENTED / Cargo pending | -| moteur Yellowstone partagé sans duplication provider | source/API canary | N1 FOUNDATION IMPLEMENTED | -| façade Solana Yellowstone standard distincte du moteur | public API canary | PARTIAL : 7 unary N2 / Subscribe TODO | -| PublicNode représenté comme provider/capabilities, pas comme nouveau protocole | public API/config canary | TODO `pre.011/012` | -| provider peut réutiliser, restreindre ou étendre N2 sans dupliquer N1 | capability/completeness review | architecture conservée | -| aucune équivalence provider/standard présumée sans preuve | provider matrix/tests | architecture conservée | -| façade provider spécialisée seulement si delta réel | completeness review | TODO provider integration | +### 8.5 PublicNode Testnet -## 10. Settings, bounds et redaction +Verdict `pre.011` : **DEFERRED, NOT GUESSED**. -État après `pre.003` : +La page publique réauditée le 2026-08-24 expose une capacité Solana Testnet gRPC, mais l'hostname exact n'a pas été obtenu depuis une source suffisamment autoritative/indexable. Aucun profil Testnet n'est committé avant confirmation. + +`pre.012` doit soit : ```text -DONE/source+tests ajoutés endpoint schemes http/https validés ; TLS réel matérialisé en pre.003 -DONE/source+tests ajoutés endpoint URL, descripteurs et nombre d’endpoints plafonnés -DONE/source+tests ajoutés connect timeout > 0 et plafonné -DONE/source+tests ajoutés unary timeout > 0 et plafonné -DONE/source+tests ajoutés close timeout > 0 et plafonné -DONE/source+tests ajoutés max inbound/outbound > 0 et plafonnés -DONE/source+tests ajoutés request/update queue capacities > 0 et plafonnées -DONE/source+tests ajoutés reconnect attempt/backoff bornés -CANDIDATE pre.004 filter-group count <= 1024 total sur les sept maps -CANDIDATE pre.004 filter-name non vide/trim/control-free <= 128 octets + unicité globale -DONE/source+tests ajoutés metadata ASCII key/value count/size + reserved/bin bounds -TODO pre.005+ account/owner/include/exclude/required counts -CANDIDATE pre.004 data slice count <= 128, length <= 64 MiB, offset+length sans overflow ; memcmp TODO pre.005 -TODO pre.005+ Cuckoo dimensions/data bounds +confirmer hostname + réussir le smoke -> PASS +ou documenter l'impossibilité externe -> EXTERNAL BLOCK, sans endpoint inventé ``` -Security canaries : +## 9. Provider-neutrality + +| Point | Verdict | +|-----------------------------------------------|----------------------| +| type public `PublicNodeGrpc*` sans divergence | absent, **PASS** | +| protocol standard encodé comme provider | non, **PASS** | +| provider descriptif séparé | oui, **PASS source** | +| auth PublicNode hardcodée dans Transport | non, **PASS** | +| Helius/OrbitFlare runtime gRPC dans `0.2.9` | non, **PASS** | +| provider extensions dans N2 | aucune, **PASS** | + +## 10. Non-régressions obligatoires + +Le gate final doit préserver : ```text -DONE/source+tests ajoutés endpoint URL Debug redacted -DONE/source+tests ajoutés metadata publique/secrète redacted ; secret marqué sensitive -DONE/source+fixture Status message/details/metadata distants exclus des KspError -DONE pre.003/fix.001 connect réel + TLS WebPKI configuré ; erreur connect/TLS safe, gate Cargo PASS -CANDIDATE pre.004 SubscribeRequest Debug sans filter names/payload arbitraire ; update Debug TODO pre.005+ -DONE pre.003 source/tests channel/client Debug sans URL, metadata value ni raw Tonic ; lifecycle stream ultérieur +HTTP current typed 52/52 +HTTP historical 14/14 Deprecated/Removed +KSP-TRANSPORT-007 vert +Standard WebSocket 9 familles / 18 opérations +Helius LaserStream WebSocket 7 familles standard + transaction + slotsUpdates, heartbeat provider-owned +Transport -> Config interdit +Transport -> std::env KSP_* interdit +tracing direct Transport interdit ``` -## 11. Lifecycle / backpressure / replay +`pre.011` ne modifie pas Transport runtime ; son seuil est donc au minimum la baseline `pre.010-fix.001`. -| Cas | Attendu | État | -|--------------------------------------------|-----------------------------------------------------------------|----------------------------------------| -| stream open | session bornée | DONE pre.009 / gate fix.001 PASS | -| request mutation | ordre déterministe | DONE pre.009 | -| server Ping -> client request ping -> Pong | explicite | DONE pre.009 | -| server half-close | reconnect si budget > 0 ; terminal normal sinon | CANDIDATE pre.010 | -| client close | half-close + cleanup borné | DONE pre.009 | -| receiver drop | Drop session => cleanup best-effort | DONE pre.009 | -| slow subscription | pas de queue infinie | DONE pre.009 | -| inbound oversized | limite Tonic avant payload KSP | DONE pre.009 | -| outbound oversized | encoded_len avant queue/write | DONE pre.009 | -| reconnect budget | borné | CANDIDATE pre.010 | -| resubscribe order | dernier request complet accepté | CANDIDATE pre.010 | -| `from_slot` | max(explicite, highest observed), sans promesse lossless | CANDIDATE pre.010 | -| ReplayInfo | informatif ; clamp seulement si gap de couverture replay prouvé | DONE unary + CANDIDATE usage reconnect | -| duplicates | identités bornées observables, jamais supprimées | CANDIDATE pre.010 | -| gaps | seulement si `first_available > requested_from_slot` | CANDIDATE pre.010 | -| divergent node history | Block/BlockMeta hash-aware ; pas de claim global | CANDIDATE couverture documentée | -| shutdown during reconnect | aucune nouvelle connexion après shutdown | CANDIDATE pre.010 | +## 11. Historique des gates fermé -Claims interdits sans nouvelle preuve : +Les détails de commandes, warnings corrigés et fichiers exacts restent dans leurs deltas immuables. + +| Tranche | Gate consolidé | +|-----------------------------------|--------------------------------------| +| `pre.001` + `fix.001` + `fix.002` | audit/sizing/providers/licence fermé | +| `pre.002` + `fix.001` + `fix.002` | moteur/settings/channel fermé | +| `pre.003` + `fix.001` | TLS/metadata/unary fermé | +| `pre.004` + `fix.001` | Subscribe common fermé | +| `pre.005` + `fix.001` | Accounts/Slots fermé | +| `pre.006` | namespace HTTP fermé | +| `pre.007` | Transactions fermé | +| `pre.008` + `fix.001` | Blocks fermé | +| `pre.009` + `fix.001` | bidi/backpressure fermé | +| `pre.010` + `fix.001` | reconnect/replay fermé sans warning | + +Cette table remplace les anciens appendices numérotés successivement `19.x`, `20`, `21`, etc. qui rendaient le document ambigu. + +## 12. Gate opérateur `pre.011` + +Commandes requises : + +```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 +``` + +Résultats attendus si aucun fix n'est requis : ```text -exactly-once -lossless -historical replay complet -ordre global sans gap +audit Rust clean / 0 export candidate +check/clippy PASS sans warning +Config unit baseline 110 + 3 nouveaux tests V3 = 113 attendus +Config public API 15 attendus, canari enrichi +Config ownership 5 attendus +Transport unit >= 383 sans régression +Transport public API >= 49 sans régression +Transport completeness >= 43 sans régression +Transport doctests 4 sans régression +workspace dependencies 3 sans régression +cargo test --workspace PASS ``` -## 12. Config V3 +Ces nombres sont des attentes de candidate, pas un résultat déclaré avant exécution opérateur. -Décision de gate : +## 13. Gate `pre.012` et clôture stable + +### 13.1 Live PublicNode + +Mainnet minimal : ```text -V1 HTTP reste lisible -V2 HTTP+WS reste lisible -V3 ajoute gRPC explicitement +opt-in seulement +settings Transport programmatic +aucun secret +connexion TLS +unary simple et/ou Subscribe borné selon smoke retenu +close borné +aucune dépendance Config ajoutée au test Transport pur ``` -Shape conceptuelle à matérialiser : +Testnet seulement après endpoint exact confirmé. + +### 13.2 Compliance finale ```text -globals.grpc_defaults -profiles[].grpc_endpoints[] -endpoint.metadata[] # public/non-secret -endpoint.secret_metadata[] # Config-owned secret provenance +réaudit upstream release/proto +cargo tree Transport +cargo tree Transport --duplicates +cargo tree --duplicates +HTTP 52 + 14 +Standard WS 18/18 +Helius WS +public API crate-root +release completeness +Config V1/V2/V3 +security/redaction +README/USAGE +prompt release suivante +workspace complet ``` -Canaries : +### 13.3 Verdict stable -| Gate | État | -|------------------------------------------|------| -| V1 backward readable | TODO | -| V2 backward readable | TODO | -| V3 schema strict | TODO | -| gRPC endpoint mapping | TODO | -| secret provenance enforced | TODO | -| safe projection redacted | TODO | -| no Transport -> Config | TODO | -| `.env.example` inventory if new env vars | TODO | - -## 13. Providers — validation et séquence - -| Provider | Accès gratuit/durable observé | Rôle architectural | Release cible | Verdict | -|-----------------------------------------------|------------------------------------|-----------------------------------------------------------|---------------|-----------------| -| **PublicNode / Allnodes-backed** | Mainnet + Testnet Yellowstone gRPC | première intégration concrète | **0.2.9** | **IN provider** | -| **OrbitFlare** | Devnet Free | provider dédié ; compat/restrictions/extensions à auditer | **0.2.10** | **PLANNED** | -| **Helius LaserStream gRPC** | accès dépendant du plan | provider dédié ; compat/replay/auth/extensions à auditer | **0.2.11** | **PLANNED** | -| eRPC | à réauditer | waitlist provider | non réservé | TODO | -| Triton | à réauditer | upstream + extensions provider possibles | non réservé | TODO | -| Alchemy | à réauditer | waitlist provider | non réservé | TODO | -| QuickNode | à réauditer | waitlist provider | non réservé | TODO | -| Chainstack | à réauditer | waitlist provider | non réservé | TODO | -| Tatum / Shyft / Solinfra / NodeFlare / autres | selon accès/capabilities | attente exploratoire | non réservé | IDEAS | - -### PublicNode `0.2.9` - -Cible principale officiellement affichée : +`0.2.9` peut devenir stable seulement si tous les éléments suivants sont vrais : ```text -solana-yellowstone-grpc.publicnode.com:443 +standard vs extension explicitement classifié +SubscribeDeshred OUT documenté +7 unary verts +Subscribe + 9 updates verts +resource/backpressure/lifecycle verts +reconnect/replay sans promesse lossless +Config V3 backward V1/V2 +provider/protocol distincts +PublicNode Mainnet validé ou incident externe précisément documenté +Testnet jamais inventé +HTTP/WS/Helius non régressés +dependency firewall vert +cargo graphs inspectés +workspace final vert ``` - -Gates : - -```text -utilise le moteur Yellowstone partagé -utilise la façade standard Solana -provider/capabilities/profile explicitement identifiables -aucune duplication actor/channel/stream -wire N2 réutilisé seulement pour les capacités réellement compatibles ; divergence N3 explicite -Mainnet smoke opt-in -Testnet smoke opt-in si endpoint exact confirmé -``` - -### Providers suivants - -OrbitFlare (`0.2.10`) et Helius LaserStream gRPC (`0.2.11`) sont les seules intégrations provider actuellement planifiées après PublicNode. Pour chacune, le gate doit comparer le provider au Yellowstone upstream courant : capacités supportées, restrictions, extensions wire, auth/metadata, compression, keepalive, replay/from_slot, limites et lifecycle. - -Les autres providers restent en TODO/IDEAS sans release dédiée. Aucune façade, dépendance, Config profile ou smoke n'est préparé pour eux tant qu'une décision explicite ne les fait pas entrer dans la séquence active. - -## 14. Non-régressions obligatoires - -| Surface héritée | Attendu final | État | -|--------------------|----------------------------------------------------|------------| -| HTTP current | 52 typed | TODO final | -| HTTP historical | 14 Deprecated/Removed | TODO final | -| Standard WS | 18 opérations / 9 familles | TODO final | -| Helius WS | 7 standard + transaction | TODO final | -| Helius heartbeat | Ping 60 s | TODO final | -| Config V1/V2 | backward read | TODO final | -| Wallet/Wallet Desk | aucune dépendance protocolaire Yellowstone directe | TODO final | - -## 15. Forecast / preuve par tranche — **15–20 min max ; release <= une session** - -```text -pre.001 DONE audit/sizing/architecture 15–20 min nominal -pre.002 DONE moteur: deps/settings/errors/channel 15–20 min ; gate final fix.002 PASS -pre.003 DONE TLS/metadata + fixture + 7 unary standard 15–20 min ; gate final fix.001 PASS -pre.004 DONE standard: Subscribe common/from_slot/bounds 15–20 min ; gate final fix.001 PASS -pre.005 DONE standard: accounts + slots 15–20 min ; gate final fix.001 PASS -pre.006 DONE structure: namespace privé HTTP `http_*` 15–20 min ; gate PASS -pre.007 DONE standard: transactions + transaction_status 15–20 min ; gate PASS -pre.008 DONE standard: blocks + block_meta + entry 15–20 min ; gate final fix.001 PASS sans warning -pre.009 DONE moteur: bidi/backpressure/half-close/shutdown 15–20 min ; gate final fix.001 PASS sans warning, 379/48/42/4 -pre.010 CANDIDATE moteur: reconnect/replay/gap/duplicate 15–20 min ; source/fixture candidate, gate opérateur à exécuter -pre.011 TODO Config V3 + protocol/provider + profils PublicNode 15–20 min -pre.012 TODO PublicNode live + compliance + docs/prompt 0.2.10 15–20 min -rel.001 TODO stable -``` - -Gate de sizing : une prerelease estimée à plus de 20 minutes est scindée ; si `0.2.9` ne paraît plus clôturable dans la session courante, la release est scindée avant implémentation supplémentaire. - -## 16. Gate `pre.001` - -| Critère du prompt | Verdict | -|------------------------------------------|--------------------------------| -| base stable `v0.2.8` confirmée | PASS | -| baseline opérateur enregistrée | PASS | -| upstream courant relu | PASS | -| divergence snapshot/latest enregistrée | PASS | -| service/proto exhaustif inventorié | PASS | -| standard vs provider extension classifié | PASS | -| `SubscribeDeshred` classifié | PASS / OUT | -| unary RPCs classifiés | PASS / 7 IN | -| replay/from_slot audités | PASS | -| strategy dependency choisie | PASS / B | -| licence auditée avant ajout | PASS | -| features/build/transitifs audit initial | PASS / matérialisation pre.002 | -| architecture distincte WS | PASS | -| metadata/auth neutral | PASS | -| Config shape décidée | PASS / V3 | -| resource/backpressure policy décidée | PASS | -| smoke ownership décidé | PASS | -| providers gratuits audités | PASS | -| release dimensionnée | PASS | -| forecast recalibré | PASS | -| split criteria écrits | PASS | -| aucune implémentation lourde prématurée | PASS | - -**Verdict `pre.001` : gate positif ; `pre.002` peut matérialiser la stratégie B sous validation Cargo.** - -## 17. Gate `pre.002` / `pre.002-fix.001` - -Premier passage opérateur sur `pre.002` : - -| Gate / observation | Résultat avant fix | État après `fix.001` | -|---------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------| -| workspace version | `0.2.9-pre.2` | `0.2.9-pre.2.fix.1` | -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | PASS local / clean | -| `cargo check --workspace` | PASS, avec warning `channel` jamais lu | warning supprimé par état privé `_channel` | -| `cargo clippy --workspace --all-targets` | FAIL : `implicit_return` dans `unit_tests/grpc_settings.rs` | corrigé ; à réexécuter | -| `cargo test -p ksp-onchain-transport-lib` | FAIL : `connect_lazy()` appelé hors reactor Tokio, 344/345 avant fix | test/runtime corrigés ; à réexécuter | -| dependency canary Core | PASS / 3 passed | inchangé | -| `cargo tree -p ksp-onchain-transport-lib` | obtenu et inspecté | PASS graphe | -| `cargo tree -p ... --duplicates` / workspace duplicates | obtenu et inspecté | PASS avec doublons build/runtime documentés | - -Correction `fix.001` : - -```text -le champ Tonic reste présent en production ; il n'est pas placé sous cfg(test) -le champ privé devient `_channel` tant qu'aucune opération pre.003 ne le consomme -prepare() vérifie tokio::runtime::Handle::try_current() avant connect_lazy() -absence de runtime -> KspError grpc_channel_failed sûr, jamais panic Tonic -le test positif s'exécute sous #[tokio::test] -un test sync couvre explicitement l'absence de runtime et la redaction -la closure de cardinalité settings utilise un return explicite conforme à Clippy -``` - -Le choix de ne pas mettre le channel sous `#[cfg(test)]` est intentionnel : `pre.002` matérialise une ressource N1 réelle de production qui sera consommée par `pre.003`; une compilation normale sans ce champ rendrait le contrat de tranche artificiellement différent entre production et tests. - -Inspection du graphe opérateur : - -```text -tonic 0.14.6 -yellowstone-grpc-proto 12.6.0 -prost/prost-types 0.14.4 -solana-pubkey 4.3.0 unifié -http/hyper/hyper-util 1.5.0 / 1.11.0 / 0.1.20 -tower 0.5.3 -bytes 1.12.1 -``` - -Aucune version concurrente Tonic/Prost/Solana n'a été introduite dans le graphe Transport. Les répétitions Prost de même version proviennent des unités build/runtime de la crate proto publiée. - -**Verdict `pre.002-fix.001` : correctif source prêt ; `pre.002` reste ouverte jusqu'à réexécution verte de fmt/audit/check/clippy/tests/workspace et confirmation finale du graphe.** - -### Second passage opérateur après `fix.001` - -| Gate / observation | Résultat `fix.001` | État `fix.002` | -|------------------------------------------|--------------------------------------------------------------------|------------------------------------| -| workspace version | `0.2.9-pre.2.fix.1` | `0.2.9-pre.2.fix.2` | -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | PASS local / clean | -| `cargo check --workspace` | PASS | inchangé | -| `cargo clippy --workspace --all-targets` | PASS | inchangé | -| Transport unit | PASS / 346 passed | inchangé | -| Transport `public_api` | FAIL / 41 passed, 1 failed : `prepare()` appelé hors runtime Tokio | canari placé sous `#[tokio::test]` | -| dependency canary Core | PASS / 3 passed | inchangé | -| graphes Cargo | déjà fournis et inspectés | inchangés | - -Le second échec ne révèle aucune nouvelle faiblesse du moteur N1 : il confirme au contraire le contrat introduit par `fix.001`. Le canari public API doit être exécuté dans le même contexte Tokio que le test unitaire positif ; aucune modification supplémentaire de `YellowstoneGrpcChannel` n'est nécessaire. - -**Verdict `pre.002-fix.002` : correctif de test d'intégration prêt ; `pre.002` reste ouverte jusqu'à réexécution verte de `cargo test -p ksp-onchain-transport-lib`, `cargo test --workspace` et des gates habituels.** - -### Gate final opérateur `pre.002-fix.002` - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 346/346 PASS | -| Transport `public_api` | 42/42 PASS | -| Transport `release_completeness` | 35/35 PASS | -| Transport doctests | 4/4 PASS | -| Core dependency canary | 3/3 PASS | -| `cargo test --workspace` | PASS | - -**Verdict : `pre.002` fermée.** - -## 18. Gate `pre.003` — candidate - -| Gate / surface | État candidate | -|---------------------------------------------|----------------| -| workspace version | `0.2.9-pre.3` | -| runtime proto sans feature `tonic` | SOURCE PASS | -| Tonic runtime channel + AWS-LC + WebPKI TLS | SOURCE PASS | -| Tonic server/codegen uniquement dev/test | SOURCE PASS | -| metadata ASCII publique/secrète redacted | SOURCE PASS | -| connexion réelle bornée | SOURCE PASS | -| 7 unary standard exacts | SOURCE PASS | -| fixture Geyser locale | SOURCE PASS | -| commitment mapping | SOURCE PASS | -| Status hostile sans payload distant | SOURCE PASS | -| timeout unary borné | SOURCE PASS | -| Subscribe / SubscribeDeshred | OUT pre.003 | -| PublicNode / Config V3 | OUT pre.003 | -| audit Rust workspace local | PASS / clean | -| fmt/check/Clippy/tests/Cargo trees | opérateur TODO | - -La fixture active `yellowstone-grpc-proto/tonic` et Tonic `codegen+server` uniquement dans le graphe dev/test. Le runtime utilise les messages Protobuf publiés, `tonic::client::Grpc` et `tonic-prost::ProstCodec` derrière une façade KSP sans raw escape hatch. - -**Verdict `pre.003` : candidate source prête ; fermeture seulement après gate Cargo opérateur complet et réinspection des graphes.** -### 18.1 Premier gate opérateur `pre.003` et `pre.003-fix.001` - -| Gate / observation | Résultat `pre.003` | État `fix.001` | -|------------------------------------------|-------------------------------------------------------------|----------------------------| -| workspace version | `0.2.9-pre.3` | `0.2.9-pre.3.fix.1` | -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | audit local PASS / clean | -| `cargo check --workspace` | PASS | inchangé | -| `cargo clippy --workspace --all-targets` | FAIL : 11 `implicit_return` dans `unit_tests/grpc_unary.rs` | corrigé ; à réexécuter | -| Transport unit | 354/354 PASS | inchangé | -| Transport `public_api` | 43/43 PASS | inchangé | -| Transport `release_completeness` | 36/36 PASS | inchangé | -| Transport doctests | 4/4 PASS | inchangé | -| Core dependency canary | 3/3 PASS | inchangé | -| `cargo test --workspace` | PASS | inchangé fonctionnellement | - -Correction : - -```text -9 diagnostics proviennent de l'expansion #[tonic::async_trait] sur la fixture serveur, -alors que les corps source ont déjà des return explicites. -=> allow(clippy::implicit_return) strictement local à cette implémentation de test, avec justification. - -2 diagnostics concernent les closures metadata and_then. -=> return explicite dans les closures. - -aucun changement N1/N2 runtime -aucune dépendance/feature modifiée -aucun Subscribe/PublicNode/Config V3 anticipé -``` - -**Verdict `pre.003-fix.001` : correctif Clippy minimal prêt ; fermeture de `pre.003` après réexécution verte des gates habituels et inspection des graphes Cargo demandée par la tranche.** - -### 18.2 Gate final opérateur `pre.003-fix.001` - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 354/354 PASS | -| Transport `public_api` | 43/43 PASS | -| Transport `release_completeness` | 36/36 PASS | -| Transport doctests | 4/4 PASS | -| `cargo test --workspace` | PASS | -| `cargo tree -p ...` | fourni/relu | -| `cargo tree -p ... -e features` | fourni/relu | -| `cargo tree -p ... --duplicates` | fourni/relu | -| `cargo tree --duplicates` | fourni/relu | - -Graphe pertinent confirmé : `tonic 0.14.6`, `tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, `yellowstone-grpc-proto 12.6.0`, `solana-pubkey 4.3.0`. Les features serveur/codegen et `yellowstone-grpc-proto/tonic` restent liées à la fixture dev/test ; aucune seconde version Tonic/Prost/Solana n'est introduite par la tranche. - -**Verdict : `pre.003` fermée.** - -## 19. Gate `pre.004` — candidate - -| Surface | État candidate | -|--------------------------------------------------|----------------| -| workspace version | `0.2.9-pre.4` | -| sept maps `SubscribeRequest` | SOURCE PASS | -| map vide / entrée nommée vide | SOURCE+TEST | -| commitment optional exact | SOURCE+TEST | -| account-data slices ordonnées | SOURCE+TEST | -| ping optional exact | SOURCE+TEST | -| `from_slot` optional exact | SOURCE+TEST | -| filter names <=128 octets / unicité globale | SOURCE+TEST | -| filter groups <=1024 total | SOURCE+TEST | -| slices <=128 / length <=64 MiB / overflow rejeté | SOURCE+TEST | -| request Debug sans noms/payloads de filtres | SOURCE+TEST | -| filtres Accounts/Slots détaillés | OUT pre.004 | -| filtres Transactions/Blocks détaillés | OUT pre.004 | -| stream bidi / lifecycle / updates | OUT pre.004 | -| PublicNode / Config V3 | OUT pre.004 | -| audit Rust workspace local | PASS / clean | -| fmt/check/Clippy/tests | opérateur TODO | - -Le proto publié `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant implémentation : les onze champs top-level retenus restent `accounts`, `slots`, `transactions`, `transactions_status`, `blocks`, `blocks_meta`, `entry`, `commitment`, `accounts_data_slice`, `ping`, `from_slot`. Aucun `SubscribeDeshred` n'entre dans la surface KSP. - -**Verdict `pre.004` : candidate source prête ; fermeture après gate Cargo opérateur.** - - -### 19.1 Premier gate opérateur `pre.004` et `pre.004-fix.001` - -| Gate / observation | Résultat `pre.004` | État `fix.001` | -|------------------------------------------|----------------------------------------|--------------------------------------------| -| workspace version | `0.2.9-pre.4` | `0.2.9-pre.4.fix.1` | -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | audit local PASS / clean | -| `cargo check --workspace` | PASS + 10 warnings `dead_code` | helpers wire test-only sous `#[cfg(test)]` | -| `cargo clippy --workspace --all-targets` | PASS + mêmes 10 warnings | idem | -| Transport unit | 359/359 PASS | inchangé | -| Transport `public_api` | 44/44 PASS | inchangé | -| Transport `release_completeness` | 37/37 PASS | inchangé | -| Transport doctests | 4/4 PASS | inchangé | -| Core dependency canary | 3/3 PASS | inchangé | -| `cargo test --workspace` | PASS + mêmes warnings à la compilation | à réexécuter sans warning | - -Les dix symboles concernés (`to_wire` et `commitment_to_wire`) servent uniquement aux tests de projection protobuf de `pre.004`. Le stream bidi n'étant pas ouvert avant `pre.009`, ils ne font pas encore partie du runtime. `#[cfg(test)]` évite donc un faux code mort de production sans créer une seconde implémentation ni modifier le contrat public. Lors de l'intégration runtime du stream, ces helpers seront naturellement retirés du `cfg(test)` au moment où ils auront un consommateur de production. - -**Verdict `pre.004-fix.001` : fermé.** - -### 19.2 Gate opérateur final `pre.004-fix.001` - -| Gate / observation | Résultat final | -|------------------------------------------|----------------------------| -| workspace version | `0.2.9-pre.4.fix.1` | -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS sans warnings pre.004 | -| `cargo clippy --workspace --all-targets` | PASS sans warnings pre.004 | -| Transport unit | 359/359 PASS | -| Transport `public_api` | 44/44 PASS | -| Transport `release_completeness` | 37/37 PASS | -| Transport doctests | 4/4 PASS | -| `cargo test --workspace` | PASS | - -`pre.004` est donc fermée. - -## 20. Gate `pre.005` — Accounts + Slots candidate - -| Surface | État candidate | -|------------------------------------------------------|----------------| -| workspace version | `0.2.9-pre.5` | -| account selectors `account[]` / `owner[]` | SOURCE+TEST | -| account predicates memcmp bytes/base58/base64 | SOURCE+TEST | -| account predicates datasize/token-state/lamports | SOURCE+TEST | -| `nonempty_txn_signature?` | SOURCE+TEST | -| `cuckoo_accounts_filter?` + SipHash | SOURCE+TEST | -| Slots `filter_by_commitment?` / `interslot_updates?` | SOURCE+TEST | -| Account update DTO/decode | SOURCE+TEST | -| Slot update DTO/decode + 7 statuts + `dead_error?` | SOURCE+TEST | -| fixed-width pubkey/signature malformed | SOURCE+TEST | -| payload/timestamp/update-filter/dead-error bounds | SOURCE+TEST | -| Debug sans sélecteurs/payloads arbitraires | SOURCE+TEST | -| Transactions / transaction_status | OUT pre.005 | -| Blocks / block_meta / entry | OUT pre.005 | -| stream bidi / lifecycle | OUT pre.005 | -| PublicNode / Config V3 | OUT pre.005 | -| audit Rust workspace local | PASS / clean | -| fmt/check/Clippy/tests | opérateur TODO | - -Les conversions request protobuf et les décodeurs Account/Slot sont test-only jusqu'à `pre.009`, faute de consommateur runtime avant l'ouverture du stream. Le contrat public reste entièrement KSP-owned. - -**Verdict `pre.005` : candidate source prête ; fermeture après gate Cargo opérateur.** - - -### 20.1 Premier gate opérateur `pre.005` et `pre.005-fix.001` - -| Gate / observation | Résultat `pre.005` | État `fix.001` | -|------------------------------------------|---------------------------------------------------------------------------|------------------------------------------| -| workspace version | `0.2.9-pre.5` | `0.2.9-pre.5.fix.1` | -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | audit local à revalider | -| `cargo check --workspace` | PASS + 4 warnings `dead_code` | constantes test-only sous `#[cfg(test)]` | -| `cargo clippy --workspace --all-targets` | FAIL : 1 `implicit_return` + 1 `wrong_self_convention` ; mêmes 4 warnings | corrigé | -| Transport unit | 364/364 PASS | inchangé | -| Transport `public_api` | 45/45 PASS | inchangé | -| Transport `release_completeness` | 38/38 PASS | inchangé | -| Transport doctests | 4/4 PASS | inchangé | -| Core dependency canary | 3/3 PASS | inchangé | -| `cargo test --workspace` | PASS + 4 warnings à la compilation | à réexécuter sans warning | - -Les quatre constantes concernées sont exclusivement consommées par les décodeurs Account/Slot eux-mêmes sous `#[cfg(test)]` jusqu'à l'ouverture du stream en `pre.009`. Elles passent donc sous le même `cfg(test)` plutôt que d'introduire un `allow(dead_code)`. Le receiver du helper Slot devient `self` car le type est `Copy`; la closure `is_some_and` reçoit un `return` explicite conformément à la politique Clippy KSP. - -Le même fix réaligne tous les tableaux Markdown de `016` et `012` sans modifier leur contenu sémantique. - -**Verdict `pre.005-fix.001` : correctif source/documentaire prêt ; fermeture de `pre.005` après gate opérateur sans warning.** - -### 20.2 Gate final opérateur `pre.005-fix.001` - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| 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 | - -**Verdict : `pre.005` fermée.** - -## 21. Gate `pre.006` — namespace privé HTTP - -| Surface / invariant | État candidate | -|----------------------------------------------------------------|----------------| -| `client.rs -> http_client.rs` | SOURCE PASS | -| `executor.rs -> http_executor.rs` | SOURCE PASS | -| `pool.rs -> http_pool.rs` | SOURCE PASS | -| `resilience.rs -> http_resilience.rs` | SOURCE PASS | -| `settings.rs -> http_settings.rs` | SOURCE PASS | -| unit tests miroirs `http_*` | SOURCE PASS | -| types/fonctions publics HTTP | INCHANGÉS | -| `rpc_common/accounts/blocks/transactions` restent partagés | SOURCE PASS | -| `json_rpc`, `constants`, `error` non artificiellement préfixés | SOURCE PASS | -| anciens fichiers supprimés après application overlay | opérateur TODO | -| release-completeness canary namespace | SOURCE PASS | -| audit Rust workspace local | PASS / clean | -| fmt/check/Clippy/tests/workspace | opérateur TODO | - -Le renommage est limité aux cinq modules dont l'ownership HTTP est sans ambiguïté. Il ne modifie aucune signature publique et n'introduit aucune dépendance. L'overlay ZIP ajoute les nouveaux chemins ; les cinq anciens fichiers source et leurs cinq unit tests miroirs doivent être supprimés explicitement par l'opérateur après extraction. - -**Verdict `pre.006` : candidate structurelle prête ; fermeture après suppressions opérateur et gate Cargo complet.** - - -## 22. Gate final `pre.006` - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 364/364 PASS | -| Transport `public_api` | 45/45 PASS | -| Transport `release_completeness` | 39/39 PASS | -| Transport doctests | 4/4 PASS | -| Core dependency canary | 3/3 PASS | -| `cargo test --workspace` | PASS | - -**Verdict : `pre.006` fermée.** - -## 23. Gate `pre.007` — Transactions + `transaction_status` candidate - -| Surface | État candidate | -|--------------------------------------------------------------------|----------------| -| workspace version | `0.2.9-pre.7` | -| `vote?` / `failed?` | SOURCE+TEST | -| signature Base58 décodée exactement 64 octets | SOURCE+TEST | -| include/exclude/required ordonnés et bornés | SOURCE+TEST | -| Cuckoo transaction include | SOURCE+TEST | -| token expansion ALL/BALANCE_CHANGED | SOURCE+TEST | -| `SubscribeUpdateTransaction` | SOURCE+TEST | -| `SubscribeUpdateTransactionStatus` | SOURCE+TEST | -| Transaction/Message/Header | SOURCE+TEST | -| compiled instructions / ALT lookups | SOURCE+TEST | -| Transaction V1 `TransactionConfig` | SOURCE+TEST | -| TransactionStatusMeta complet | SOURCE+TEST | -| inner instructions + legacy none marker | SOURCE+TEST | -| logs + legacy none marker | SOURCE+TEST | -| token balances / UiTokenAmount | SOURCE+TEST | -| loaded addresses | SOURCE+TEST | -| ReturnData + legacy none marker | SOURCE+TEST | -| rewards | SOURCE+TEST | -| compute_units_consumed / cost_units | SOURCE+TEST | -| opaque TransactionError borné | SOURCE+TEST | -| Debug sans signatures/token-balance text/payloads/logs/error bytes | SOURCE+TEST | -| Blocks / block_meta / entry | OUT pre.007 | -| stream bidi / lifecycle | OUT pre.007 | -| PublicNode / Config V3 | OUT pre.007 | -| audit Rust workspace local | PASS / clean | -| fmt/check/Clippy/tests | opérateur TODO | - -Le proto 12.6.0 ajoute à la représentation de transaction le `Message.config` optionnel pour Transaction V1 / SIMD-0385 ; la candidate le conserve explicitement. Les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none` ne sont pas fusionnés avec leurs collections/messages. - -**Verdict `pre.007` : candidate source prête ; fermeture après gate Cargo opérateur.** - - -## 24. Gate final `pre.007` - -| Gate | Résultat final | -|------------------------------------------|----------------| -| `cargo fmt --all` | PASS | -| audit Rust workspace | PASS / clean | -| `cargo check --workspace` | PASS | -| `cargo clippy --workspace --all-targets` | PASS | -| Transport unit | 367/367 PASS | -| Transport `public_api` | 46/46 PASS | -| Transport `release_completeness` | 40/40 PASS | -| Transport doctests | 4/4 PASS | -| Core dependency canary | 3/3 PASS | -| `cargo test --workspace` | PASS | - -**Verdict : `pre.007` fermée.** - -## 25. Gate `pre.008` — Blocks + `block_meta` + `entry` candidate - -| Surface / invariant | État candidate | -|-------------------------------------------------------|--------------------------| -| workspace version | `0.2.9-pre.8` | -| Blocks `account_include[]` | SOURCE+TEST | -| `include_transactions?` / `include_accounts?` | SOURCE+TEST | -| `include_entries?` | SOURCE+TEST | -| Cuckoo block account include | SOURCE+TEST | -| `SubscribeUpdateBlock` | SOURCE+TEST | -| rewards + `num_partitions?` | SOURCE+TEST | -| block time / block height optionnels | SOURCE+TEST | -| transactions réutilisent `YellowstoneTransactionInfo` | SOURCE+TEST | -| accounts réutilisent `YellowstoneAccountInfo` | SOURCE+TEST | -| compteurs serveur indépendants des payload vectors | SOURCE+TEST | -| `SubscribeUpdateBlockMeta` | SOURCE+TEST | -| `SubscribeUpdateEntry` + `starting_transaction_index` | SOURCE+TEST | -| blockhash/parent blockhash Base58 -> 32 octets | SOURCE+TEST | -| entry hash exactement 32 octets | SOURCE+TEST | -| Debug sans hashes/sélecteurs/payloads imbriqués | SOURCE+TEST | -| stream bidi / Ping-Pong / lifecycle | OUT pre.008 | -| reconnect / replay | OUT pre.008 | -| PublicNode / Config V3 | OUT pre.008 | -| audit Rust workspace local | PASS / clean | -| fmt/audit/check/tests/workspace | PASS | -| Clippy | PASS + 1 warning fixture | - -Les `executed_transaction_count`, `updated_account_count` et `entries_count` sont conservés tels que fournis par le serveur ; ils ne sont pas forcés à égaler les tailles de `transactions[]`, `accounts[]` ou `entries[]`, car les trois flags `include_*` peuvent omettre ces payloads. Les messages imbriqués Transaction et Account réutilisent strictement les DTOs déjà introduits ; aucune seconde projection n’est créée. - -Les helpers wire/decode restent test-only jusqu’au premier consommateur runtime en `pre.009`. - -**Verdict `pre.008` : contrat fonctionnel validé ; fix warning-only requis avant fermeture.** - -## 26. Gate `pre.008-fix.001` — warning Clippy fixture - -| Gate / observation | Résultat `pre.008` | Correctif `fix.001` | -|------------------------------------------|--------------------------------------|---------------------------------------| -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | PASS local / clean | -| `cargo check --workspace` | PASS | inchangé | -| `cargo clippy --workspace --all-targets` | PASS + `field_reassign_with_default` | initialiseur struct, warning supprimé | -| Transport unit | 370/370 PASS | contrat inchangé | -| Transport `public_api` | 47/47 PASS | contrat inchangé | -| Transport `release_completeness` | 41/41 PASS | contrat inchangé | -| Transport doctests | 4/4 PASS | contrat inchangé | -| Core dependency canary | 3/3 PASS | dépendances inchangées | -| `cargo test --workspace` | PASS | contrat inchangé | - -Le warning provient uniquement de `minimal_transaction_info()` dans `unit_tests/grpc_subscribe.rs`. `fix.001` initialise `TransactionStatusMeta.fee` directement dans le literal struct et conserve `..Default::default()`. Aucun `allow`, aucun changement runtime, aucune dépendance et aucun élargissement vers le bidi. - -**Verdict `fix.001` : PASS sans warning ; `pre.008` fermée.** - -## 27. Gate `pre.009` — bidi standard + backpressure + half-close + shutdown candidate - -| Surface / invariant | État candidate | -|-------------------------------------------------|-------------------| -| workspace version | `0.2.9-pre.9` | -| `/geyser.Geyser/Subscribe` bidi | SOURCE+TEST | -| request initial dans queue bornée | SOURCE+TEST | -| mutation `try_update()` ordonnée/non bloquante | SOURCE+TEST | -| aucune `unbounded_channel` | CANARY | -| decode des 9 variantes standard | SOURCE+TEST | -| server Ping -> ping-only id=1 | SOURCE+TEST | -| Pong observable | SOURCE+TEST | -| server half-close => `Ok(None)` | SOURCE+TEST | -| client close => request half-close | SOURCE+TEST | -| shutdown hostile borné par `close_timeout` | SOURCE+TEST | -| Drop session => half-close best-effort | SOURCE+TEST | -| slow receiver => terminal backpressure overflow | SOURCE+TEST | -| Status distant sans message/details secrets | SOURCE+TEST | -| update malformed => invalid response | SOURCE+TEST | -| max inbound Tonic | SOURCE+TEST | -| outbound encoded_len avant dispatch | SOURCE+TEST | -| reconnect/resubscribe/replay | OUT pre.009 | -| `SubscribeDeshred` | OUT 0.2.9 | -| PublicNode / Config V3 | OUT pre.009 | -| audit Rust workspace local | PASS / clean | -| fmt/check/tests/workspace | PASS | -| Clippy | PASS + 2 warnings | - -Les helpers protobuf qui étaient test-only jusqu'à `pre.008` sont maintenant compilés en runtime parce que `grpc_stream` les consomme effectivement. Aucun helper raw n'est public : les frontières restent `YellowstoneSubscribeRequest` et `YellowstoneSubscribeUpdate`. - -Le flux sortant utilise la même `mpsc` bornée que celle fournie à `tonic::client::Grpc::streaming`; aucune queue cachée non bornée n'est interposée. Le Ping de keepalive serveur provoque une mutation ping-only actor-owned avec `id=1`, puis le Pong reste visible dans la file d'updates KSP. - -`pre.009` ne tente aucun reconnect. Un half-close serveur est terminal normal ; `Status`, decode invalide et overflow sont terminaux en erreur. Le budget de reconnect, l'ordre de resubscribe, `from_slot`/ReplayInfo, gaps, duplicates et node divergence restent exclusivement `pre.010`. - -**Verdict `pre.009` :** fermé après `pre.009-fix.001`, gate opérateur final intégralement vert et sans warning. - -## 28. Gate `pre.009-fix.001` — taille de l’enum update + canari `Send` - -| Gate / observation | Résultat `pre.009` | Correctif `fix.001` | -|------------------------------------------|-------------------------------------------|---------------------------------------------------| -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | PASS local / clean | -| `cargo check --workspace` | PASS | contrat inchangé | -| `cargo clippy --workspace --all-targets` | PASS + 2 warnings | boxing Transaction + usage réel de `T` | -| Transport unit | 379/379 PASS | contrat attendu inchangé | -| Transport `public_api` | 48/48 PASS | contrat attendu inchangé | -| Transport `release_completeness` | 42/42 PASS | contrat attendu inchangé | -| Transport doctests | 4/4 PASS | contrat attendu inchangé | -| Core dependency canary | 3/3 PASS | dépendances inchangées | -| `cargo test --workspace` | PASS | contrat attendu inchangé | -| `large_enum_variant` | Transaction ~664 B, Block ~280 B | `Transaction(Box)` | -| `extra_unused_type_parameters` | helper `assert_send()` n’utilise pas T | `PhantomData` matérialise l’usage compile-time | - -Le boxing ne modifie ni le protobuf ni `YellowstoneTransactionUpdate` : il réduit uniquement la taille du discriminant public transporté dans la queue bidi. Le canari `Send` conserve la même contrainte et ne construit aucune session réseau. Aucun `allow` Clippy n’est ajouté. Le gate opérateur final du `2026-08-24` repasse intégralement vert, Clippy sans warning, avec les compteurs 379/48/42/4 inchangés. - -**Verdict fix candidate :** correction minimale prête ; fermeture de `pre.009` après gate opérateur sans warning. - - -## 29. Candidate `pre.010` — reconnect/replay et continuité observable - -Réaduit upstream au `2026-08-24` : - -```text -SubscribeRequest.from_slot toujours présent -SubscribeReplayInfoResponse.first_available? toujours présent -changelog 2026-07-22 fix replay Blocks/state gap -changelog 2026-06-15 upstream autoreconnect traite l'equivocation par quarantaine/blockhash -``` - -KSP conserve sa stratégie B : aucune importation de la sémantique `yellowstone-grpc-client` autoreconnect. Le stream KSP rouvre lui-même `/geyser.Geyser/Subscribe` sur le channel N1 existant. - -| Preuve source `pre.010` | État | -|-------------------------------------------------------------|--------------------| -| état public `Reconnecting` | SOURCE | -| snapshot reconnect/replay/gap/duplicate | SOURCE | -| budget `max_retries` | SOURCE | -| backoff exponentiel initial/max | SOURCE | -| dernier request complet resoumis | SOURCE | -| resume `max(explicit, last_observed)` | SOURCE | -| ReplayInfo consulté avant replay | SOURCE | -| `first_available > requested` => replay non couvert + clamp | SOURCE+TEST | -| duplicate identité bornée, update livrée | SOURCE+TEST | -| mutation refusée pendant `Reconnecting` | SOURCE+TEST | -| shutdown interrompt le backoff | SOURCE+TEST | -| épuisement budget => terminal sûr | SOURCE+TEST | -| cache identité borné à 512 | SOURCE | -| exactement-once/lossless | EXPLICIT NON-CLAIM | -| Config V3 / PublicNode | OUT pre.010 | -| `SubscribeDeshred` | OUT 0.2.9 | - -La détection de gap n'utilise pas les sauts entre slots d'updates filtrés : un filtre peut légitimement ne produire aucun message pendant plusieurs slots. Le seul gap compté par cette tranche est une plage de replay objectivement devenue indisponible d'après `SubscribeReplayInfo.first_available` ; cela ne prouve pas qu'un update correspondant aux filtres existait ou a été perdu. - -Le cache de duplicates retient des identités minimales Account/Slot/Transaction/TransactionStatus/Block/BlockMeta/Entry. Il ne retient pas les payloads complets et ne supprime jamais un update. Pour Block/BlockMeta, le blockhash fait partie de l'identité afin de ne pas classer deux histoires divergentes de même slot comme un simple duplicate ; aucune détection globale d'equivocation n'est prétendue. - -**Gate candidate attendu :** audit statique clean ; après exécution opérateur, Transport attendu autour de 383 unit / 49 public API / 43 release-completeness / 4 doctests. Aucune dépendance/feature changée, donc pas de `cargo tree` supplémentaire. - -## 30. Gate `pre.010-fix.001` — Clippy + cohérence crate-root - -Premier gate opérateur de `pre.010` : - -| Gate | Résultat avant fix | Correctif `fix.001` | -|------------------------------------------|--------------------|----------------------------------------------| -| `cargo fmt --all` | PASS | à réexécuter | -| audit Rust workspace | PASS / clean | à réexécuter | -| `cargo check --workspace` | PASS | contrat inchangé | -| `cargo clippy --workspace --all-targets` | PASS + 2 warnings | deux `collapsible_if` fusionnés | -| Transport unit | 383/383 PASS | contrat attendu inchangé | -| Transport `public_api` | 49/49 PASS | contrat attendu inchangé | -| Transport `release_completeness` | 43/43 PASS | contrat attendu inchangé | -| Transport doctests | 4/4 PASS | contrat attendu inchangé | -| `cargo test --workspace` | PASS | contrat attendu inchangé | -| description crate-root | stale `pre.009` | synchronisée avec reconnect/replay `pre.010` | - -Warnings observés : - -```text -crates/ksp-onchain-transport-lib/src/grpc_stream.rs:491 clippy::collapsible_if -crates/ksp-onchain-transport-lib/src/grpc_stream.rs:537 clippy::collapsible_if -``` - -Le premier concerne uniquement l'éviction lorsque le cache d'identités dépasse 512 entrées ; le second uniquement le chemin `Ping -> send_automatic_ping`. Les let-chains produisent les mêmes conditions et les mêmes sorties qu'avant fix. Aucun `allow` Clippy n'est ajouté. - -La revue de la sémantique `pre.010` ne révèle pas d'écart supplémentaire avec le contrat retenu : `from_slot = max(explicite, last_observed)`, ReplayInfo reste informatif, le clamp n'a lieu que si `first_available > requested`, les duplicates sont comptés mais livrés, et les non-promesses exactly-once/lossless restent explicites. - -**Verdict candidate :** `pre.010-fix.001` est prête pour gate opérateur. `pre.011` reste TODO jusqu'à PASS sans warning. -