# Plan `0.2.7` — WebSocket Solana standard > **Statut : actif, `0.2.7-pre.004`.** `pre.003` est validé opérateur. `pre.004` matérialise les dépendances WebSocket, la première session physique actor-owned, le handshake/read/write JSON-RPC, la map bornée de requests en attente et les fixtures serveur local. Les subscriptions typed restent différées. ## 1. Objet et base vérifiée `0.2.7` étend `ksp-onchain-transport-lib` avec le transport WebSocket Solana standard sans créer un second composant on-chain et sans déplacer de logique Config, Store, Program, worker ou métier dans Transport. Base opérateur auditée : ```text archive Gitea fournie : khadhroony-solana-project-v0.2.6-from-gitea.zip workspace.package.version avant ouverture = 0.2.6 delta stable présent = deltas/0.2.6/rel.001.md prochaine release annoncée = 0.2.7 — WebSocket Solana standard ``` La clôture `0.2.6` confirme l'héritage : Wallet Desk, `.kspwallet` V1 historique + V2 binaire par défaut, APIs Wallet multi-version, migration V1 -> V2 explicite OWNER-authentifiée, runtime packagé commun aux Desks et transport HTTP Solana stable. `0.2.7-pre.001` ouvre techniquement : ```text workspace.package.version = 0.2.7-pre.1 livraison = 0.2.7-pre.001 commit = v0.2.7-pre.001 ``` Aucune dependency WebSocket n'est ajoutée dans ce gate. ## 2. Sources internes relues et hiérarchie appliquée L'audit a relu, dans l'ordre demandé par le prompt d'ouverture : ```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/PROMPT_STRUCTURE.md docs/rules/VERSION_WORKFLOW.md docs/rules/FILE_CONTRACTS.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/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 crates/ksp-onchain-transport-lib/** crates/ksp-config-lib/src/transport.rs config/std.transport.json config/schemas/std.transport.schema.json contrats consommés de ksp-core-lib et ksp-logging-lib deltas/0.2.6/rel.001.md docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md ``` `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` reste l'autorité de numérotation courante. `007-V0_2_0_SERIES_PLANNING.md` est historique. ## 3. Baseline de démarrage Le sandbox d'analyse ne fournit pas le binaire `cargo`. Les commandes imposées ont été tentées avant modification : ```text cargo fmt --all IMPOSSIBLE : cargo absent, code 127 python3 scripts/audit_rust_workspace_rules.py OK cargo check --workspace IMPOSSIBLE : cargo absent, code 127 cargo clippy --workspace --all-targets IMPOSSIBLE : cargo absent, code 127 ``` L'audit Python retourne : ```text General Rust rule audit: clean Rust export completeness audit: 0 candidate(s) KSP workspace Rust rule audit: clean ``` Aucun gate Cargo n'est déclaré réussi. Une validation opérateur est obligatoire avant commit de `pre.001`. ## 4. État réel de Transport et Config hérité ### 4.1 Surface HTTP stable à préserver `ksp-onchain-transport-lib` contient actuellement : - settings HTTP publics (`HttpEndpointUrl`, provider/cluster/role, retry, limits, endpoint, transport) ; - `HttpEndpointClient` et `HttpTransportPool` ; - 52 wrappers HTTP courants typés et 14 méthodes historiques Deprecated tracées ; - JSON-RPC, retry/no-resend, snapshots et observabilité ; - DTOs wire communs réutilisables, notamment `SolanaCommitment`, `SolanaRpcContext`, `SolanaRpcResponse`, encodings/account DTOs, filters et `SolanaWireField` ; - émissions runtime via `ksp-logging-lib`, sans `tracing` direct. La surface WebSocket doit être additive : aucune renormalisation invasive des types `Http*` n'est requise dans `0.2.7`. ### 4.2 Config hérité et évolution V2 À l’ouverture de `0.2.7`, `config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivaient un document V1 HTTP : ```text format_version = 1 schema title = standard HTTP Transport additionalProperties = false serde(deny_unknown_fields) format_version != 1 rejeté ``` Décision : **ne pas ajouter silencieusement des clés WebSocket au V1**. `pre.003` fait évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport. Shape cible minimale : ```text format_version = 2 retry # HTTP existant, conservé ws_defaults # defaults de session WS KSP, communs aux familles WS default_profile profiles[] profile_id endpoints[] # HTTP existant, conservé ws_endpoints[] # endpoints WebSocket explicites kind # discriminateur de famille/protocole WS ``` `ws_endpoints[]` porte seulement les données nécessaires à Transport : nom logique, enabled, provider, cluster, URL, `kind` et overrides de session éventuellement nécessaires. Les credentials restent résolus par Config comme aujourd'hui ; Transport reçoit un endpoint déjà résolu et doit redacter l'URL dans `Debug`/logs/errors. Décision d'extensibilité : **le conteneur V2 ne doit pas supposer qu'il n'existera qu'un seul type de WebSocket**. `0.2.7` matérialise uniquement `kind = solana_standard`; toute valeur inconnue ou future est rejetée explicitement comme non supportée. Le discriminateur est mappé vers un type Transport public non exhaustif, afin qu'une release ultérieure puisse ajouter une famille telle que Helius Enhanced WebSocket sans réécrire le conteneur `profiles[].ws_endpoints[]` ni contaminer les settings Solana standard avec des champs provider-specific. Cette préparation ne promet aucune option Helius dans `0.2.7`. L'audit provider-specific de la release suivante décidera la relation exacte entre Helius Enhanced WebSocket, LaserStream et le moteur standard avant d'ajouter des variantes/configurations concrètes. Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et aucune instance `WsTransportSettings`. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket. Checkpoint `pre.003` matérialisé : ```text std.transport schema id = urn:ksp:schema:std.transport:v2 V1 strict HTTP-only -> toujours accepté V2 strict HTTP + WS -> format livré ws_defaults -> session defaults génériques ws_endpoints[].session -> overrides génériques optionnels ws_endpoints[].kind -> solana_standard uniquement en 0.2.7 ResolvedTransportConfig -> HTTP toujours présent + WS Option pour backward V1 ``` Le schema V2 utilise deux branches strictes discriminées par `format_version`; la compatibilité V1 n'est donc pas obtenue en relâchant `additionalProperties`. Les overrides endpoint ne contiennent que des paramètres génériques de `WsSessionSettings`, jamais des options Helius/provider-specific. ## 5. Audit historique bot3 Archive auditée uniquement comme référence : ```text khadhroony-bot3_v0.5.3-pre.005-fix010.zip ks-onchain-transport/src/standard_ws.rs ks-onchain-transport/src/ws_client.rs ks-onchain-transport/src/ws_pool.rs ks-onchain-transport/src/ws_session.rs consumers/demo et rapports de validation WebSocket ``` Invariants utiles repris : - plusieurs subscriptions sur une session physique ; - plusieurs sessions possibles ; - identifiant local stable distinct de l'identifiant serveur ; - remapping des IDs serveur après reconnect ; - reconnect borné et resubscribe ; - canaris locaux de lifecycle. Choix bot3 explicitement **non repris** : - `Transport -> Config` ; - `tracing` direct ; - rôles HTTP et `max_subscriptions` mélangés au modèle endpoint générique ; - pool/scheduler de sessions sans besoin démontré ; - API d'unsubscribe publique fondée sur l'ID serveur éphémère ; - broadcast unique comme canal principal de notifications data ; - états lifecycle trop grossiers pour les races reconnect/unsubscribe. Le retour historique bot3 sur les flux à fort débit renforce le besoin d'une policy de backpressure explicite et observable ; aucune hypothèse de livraison lossless ne doit reposer sur un canal broadcast qui peut lagger silencieusement. ## 6. Inventaire WebSocket Solana officiel au 2026-08-22 Source d'index : ```text https://solana.com/docs/rpc/websocket ``` L'index courant contient exactement **18 méthodes WebSocket : 9 subscribe + 9 unsubscribe**. ```text accountSubscribe / accountUnsubscribe blockSubscribe / blockUnsubscribe logsSubscribe / logsUnsubscribe programSubscribe / programUnsubscribe rootSubscribe / rootUnsubscribe signatureSubscribe / signatureUnsubscribe slotSubscribe / slotUnsubscribe slotsUpdatesSubscribe / slotsUpdatesUnsubscribe voteSubscribe / voteUnsubscribe ``` Statut observé : ```text 6 paires sans marque unstable/deprecated dans la documentation actuelle 3 paires dont le subscribe est explicitement Unstable : blockSubscribe slotsUpdatesSubscribe voteSubscribe 0 méthode WebSocket de cet index marquée Deprecated ``` Pour KSP, le label interne `Stable` signifie ici « documentée et non marquée unstable/deprecated par la source officielle auditée », pas une garantie de standardisation extérieure supplémentaire. Règles générales documentées : JSON-RPC 2.0 sur connexion persistante ; résultat subscribe numérique ; notifications avec `params.subscription` numérique ; lorsqu'un contexte est présent `context.slot` est fourni et `context.apiVersion` est omis ; les subscriptions qui acceptent `commitment` utilisent `finalized` par défaut. La matrice détaillée initiale est conservée dans `docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md`. ### 6.1 Baseline à trois niveaux : docs Solana, Git Agave, SIMD La compliance WebSocket reprend la méthode utilisée pour la clôture HTTP et ne traite pas le site documentaire comme unique vérité technique : ```text 1. documentation Solana actuelle -> surface publique annoncée, paramètres, statuts et formes documentées 2. Git Agave, tag courant audité v4.2.1 -> parité d'implémentation réelle, types wire, options ignorées, variantes et handlers 3. solana-improvement-documents main -> SIMDs Activated, Review, Draft ou Idea susceptibles de modifier le wire, la sémantique des notifications ou les hypothèses de taille/lifecycle ``` Au 2026-08-22, le tag Git **Agave `v4.2.1`** est disponible et constitue la baseline d'implémentation de cette release, même lorsque des liens de la documentation publique pointent encore vers une révision Agave plus ancienne. Le cross-check `v4.2.1` confirme les 9 familles subscribe/unsubscribe actuelles et ne révèle aucune méthode PubSub standard supplémentaire à ajouter aux 18 méthodes de l'index Solana. Les constats ciblés déjà retenus restent valides sous `v4.2.1` : `accountSubscribe.minContextSlot` est toujours ignoré par le handler PubSub, `programSubscribe.withContext` reste un paramètre réel à préserver, `RpcVote.timestamp` reste optionnel et `SlotUpdate` conserve les sept variantes connues de la matrice actuelle. ### 6.2 Audit SIMD WebSocket ciblé L'audit SIMD ne consiste pas à implémenter des propositions par anticipation. Il sert à éviter de figer dans KSP des hypothèses déjà fragilisées par l'évolution upstream. | SIMD | Statut au 2026-08-22 | Conséquence WebSocket KSP | |-----------------------------------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| | `0118` Partitioned Epoch Rewards Distribution | Activated | réutiliser les DTOs bloc/rewards déjà lossless acquis par HTTP, notamment `numRewardPartitions` omitted, `null` ou valeur | | `0291` Commission Rate in Basis Points | Review | préserver les champs de commission déjà exposés par Agave et les DTOs partagés, sans dérivation locale entre représentations | | `0296` Larger Transaction Size | Review | proposition jusqu'à 4096 octets : ne jamais dimensionner frames/messages sur l'ancienne limite transaction de 1232 ; surveiller `blockSubscribe` | | `0298` Bank Hash in Block Footer | Idea | ne pas inventer `bankHash` dans le wire actuel ; surveiller l'évolution du DTO bloc partagé | | `0301` parent bank hash | PR fermé, non mergé | aucun `parentBankHash` spéculatif ; conserver le contrôle déjà appliqué à la compliance HTTP | | `0307` Add Block Footer | Review | ne pas ajouter `footer` avant exposition réelle par Agave ; réauditer `blockSubscribe` si le DTO bloc partagé évolue | | `0326` Alpenglow | Review | `voteSubscribe` et `slotsUpdatesSubscribe.optimisticConfirmation` sont unstable ; ne pas coder de sémantique durable TowerBFT autour de ces flux | | `0337` Alpenglow Fast Leader Handover Markers | Review | nouveaux block markers potentiels : risque futur de shape/taille pour les blocs, sans nouveau champ WS courant | | `0384` Alpenglow migration | Review | la migration peut suspendre des notifications RPC de commitment/optimistic confirmation ; aucun flux KSP ne doit supposer une séquence complète | | `0385` Transaction V1 | Review | conserver `maxSupportedTransactionVersion` et les versions transaction comme contrats génériques ; ne pas caper localement à v0 | Conséquence de sizing : les bornes WebSocket KSP sont des protections runtime configurables et finies, **pas** des constantes dérivées de l'ancienne taille de transaction Solana. Les tests `blockSubscribe` devront inclure des payloads sensiblement supérieurs aux transactions legacy tout en restant sous les limites KSP configurées. Sources SIMD auditées : ```text https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0118-partitioned-epoch-reward-distribution.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0291-commission-rate-in-basis-points.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0296-larger-transactions.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0298-bank-hash-in-block-footer.md https://github.com/solana-foundation/solana-improvement-documents/pull/301 https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0307-add-block-footer.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0326-alpenglow.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0337-parent-ready-update-marker.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0384-alpenglow-migration.md https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md ``` ## 7. Points wire sensibles sous `KSP-TRANSPORT-007` ### `accountSubscribe` Conserver : pubkey ; `commitment` ; encodings `base58|base64|base64+zstd|binary|jsonParsed` ; `dataSlice` ; notification `accountNotification` contextualisée. Le type partagé Agave `RpcAccountInfoConfig` contient aussi `min_context_slot`, mais le handler PubSub `v4.2.1` l'ignore explicitement. KSP ne doit donc pas promettre `minContextSlot` comme option WebSocket effective tant que l'upstream ne lui donne pas une sémantique réelle ; cette non-promise doit rester visible dans la compliance pour éviter qu'une option ignorée soit comptée comme supportée. ### `programSubscribe` Conserver : program pubkey ; `commitment` ; `filters` ; mêmes encodings account ; `dataSlice` ; **`withContext` default false**. La documentation actuelle expose `withContext`, mais le code Agave `v4.2.1` lié par la page stocke bien `with_context` alors que le chemin générique de notification audité sérialise un `RpcResponse` contexté. KSP ne doit ni ignorer le paramètre, ni convertir cette incohérence amont en hypothèse stricte. Décision : exposer le flag et rendre le décodeur capable d'accepter la forme documentée non-contextée comme la forme contextée observée. ### `logsSubscribe` Conserver les trois filtres : `all`, `allWithVotes`, `{mentions:[pubkey]}`. La forme `mentions` autorise exactement une adresse dans la documentation actuelle. Conserver `commitment`; notification `signature + err + logs` contextualisée. ### `signatureSubscribe` Conserver `commitment` et `enableReceivedNotification`. Le résultat notification est une union wire : ```text "receivedSignature" OU { "err": null | transaction error } ``` La subscription se termine après la notification terminale ; elle ne doit donc pas être resubscribed après que le runtime KSP a observé cette terminaison. ### `slotSubscribe` / `rootSubscribe` Aucun paramètre. `slotNotification` conserve `slot`, `parent`, `root`; `rootNotification` conserve le root `u64`. ### `blockSubscribe` — Unstable Conserver : filtre `all` ou `{mentionsAccountOrProgram:}` ; commitment limité à `confirmed|finalized` ; encoding `binary|base58|base64|json|jsonParsed` ; `transactionDetails` `full|accounts|signatures|none` ; `maxSupportedTransactionVersion` ; `showRewards`. La méthode nécessite côté validator les flags documentés ; KSP ne les simule pas. La notification conserve `context`, `slot`, `block` nullable et `err` nullable, ainsi que les variantes de block liées à la config. ### `slotsUpdatesSubscribe` — Unstable Conserver les variantes taggées actuelles et leurs champs : ```text firstShredReceived completed createdBank frozen dead optimisticConfirmation root ``` Le runtime doit prévoir une forme `Unknown`/raw bornée pour qu'une variante ajoutée par une version upstream ne ferme pas la session entière avant réaudit KSP. ### `voteSubscribe` — Unstable Aucun paramètre. La méthode nécessite le flag validator documenté et observe des votes gossip pre-consensus. Conserver `votePubkey`, `slots`, `hash`, `timestamp`, `signature`. Agave `v4.2.1` définit `timestamp` comme `Option` : le décodeur KSP doit donc préserver cette optionalité et accepter de manière tolérante les formes wire `omitted/null/value`, sans inventer une valeur. ### `*Unsubscribe` Chaque unsubscribe reçoit l'ID serveur et retourne `true` quand la subscription est supprimée ; le handler Agave audité retourne `InvalidParams` pour un ID inconnu. Cette réalité wire ne devient pas l'API publique KSP : le caller utilise l'ID local/handle stable et la session traduit vers l'ID serveur courant. Le cross-check Agave `v4.2.1` confirme également que `logsSubscribe` impose exactement une adresse pour `mentions`, que `signatureSubscribe` ne consomme que `commitment` et `enableReceivedNotification`, et que `blockSubscribe` applique bien le jeu d'options documenté avec un commitment au moins `confirmed`. Ces constats renforcent la matrice normative sans ajouter d'options non documentées à KSP. ## 8. Dépendances WebSocket candidates Audit au 2026-08-22 : | Candidate | Version auditée | Verdict | Motif | |---------------------|----------------:|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `tokio-tungstenite` | `0.30.0` | **retenue** | mature, Tokio-native, TLS rustls, continuité avec bot3 mais réauditée, contrôle de `WebSocketConfig`, client + serveur local de test | | `futures-util` | `0.3.34` | **retenue comme utilitaire** | `StreamExt`/`SinkExt`; features minimales `std,sink` | | `tokio-websockets` | `0.13.3` | alternative viable, non retenue | strict/minimal et performant, mais exige davantage d'assemblage/features et n'apporte pas de besoin fonctionnel supérieur démontré pour cette foundation | | `fastwebsockets` | `0.10.0` | non retenue | plus bas niveau ; peut déléguer davantage de compliance au caller, inutile pour la première foundation KSP | Landing matérialisé par **`pre.004`** : ```toml # root [workspace.dependencies] tokio-tungstenite = { version = "^0.30", default-features = false } futures-util = { version = "^0.3", default-features = false } # ksp-onchain-transport-lib tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] } futures-util = { workspace = true, features = ["std", "sink"] } tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } ``` Le serveur de test local active `tokio/net` en dev et utilise directement `TcpListener`. `url` n'est pas retenu a priori : l'endpoint KSP peut être validé et passé comme chaîne/request sans ajouter la feature uniquement par habitude bot3. `handshake`/`stream` sont déjà requis transitivement par `connect`/TLS et ne doivent pas être listés sans nécessité directe. Les defaults de `tungstenite::WebSocketConfig` ne sont **pas** des limites Solana normatives. KSP configurera explicitement des plafonds finis pour messages, frames et write buffer. Les valeurs numériques KSP seront figées avec les tests adversariaux de la tranche settings/session, et documentées comme policy locale, jamais comme limite protocolaire Solana. Sources : ```text https://docs.rs/crate/tokio-tungstenite/0.30.0 https://docs.rs/crate/tokio-tungstenite/0.30.0/features https://docs.rs/crate/futures-util/0.3.34 https://docs.rs/crate/tokio-websockets/0.13.3 https://docs.rs/crate/fastwebsockets/0.10.0 ``` ## 9. Modèle public Transport retenu ### 9.1 Settings Noms cibles : ```text WsEndpointUrl WsProviderName WsClusterName WsProtocolKind WsReconnectSettings WsResubscribePolicy WsSessionSettings WsEndpointSettings WsTransportSettings ``` Les wrappers `WsProviderName`/`WsClusterName` sont parallèles aux `Http*` pour éviter une migration publique HTTP dans cette release. Une généralisation future n'est justifiée que si un troisième backend démontre un type commun réellement utile. `WsProtocolKind` est un discriminateur public `#[non_exhaustive]`. `0.2.7` n'expose concrètement que `SolanaStandard`. Ajouter plus tard une famille provider-specific ne doit donc ni casser les matches externes ni forcer une nouvelle forme de `WsEndpointSettings`. Les options spécifiques restent portées par leurs contrats dédiés et ne sont jamais ajoutées arbitrairement à `WsSessionSettings`. `WsEndpointUrl` : - accepte seulement `ws://`/`wss://` après validation ; - fournit un accès explicite à la valeur au code de connexion ; - `Debug` et tout diagnostic par défaut sont redacted ; - aucun snapshot public ne contient l'URL complète. `WsSessionSettings` porte les bornes runtime nécessaires : command timeout, close timeout, reconnect, resubscribe, capacités de queues, maximum de subscriptions actives, maximum de requests JSON-RPC en vol, maximum de message/frame/write buffer. Pas de rôle HTTP ni de scheduler. Checkpoint `pre.002` matérialisé : ```text WsEndpointUrl WsProviderName WsClusterName WsProtocolKind WsReconnectSettings WsResubscribePolicy WsSessionSettings WsEndpointSettings WsTransportSettings ``` Defaults Transport initiaux, explicitement **policy KSP locale** et non limites Solana : ```text command timeout 10 s close timeout 5 s reconnect retries 5 reconnect initial backoff 250 ms reconnect maximum backoff 5 s command queue 128 notification queue per sub 256 active subscriptions 1024 pending JSON-RPC requests 128 maximum message 64 MiB maximum frame 16 MiB maximum write buffer 1 MiB resubscribe default ActiveSubscriptions ``` Les bornes message/frame/write buffer sont seulement **contractuelles** dans `pre.002`; leur raccordement à la crate WebSocket et les tests oversized restent les gates `pre.004`/`pre.005`. Elles peuvent être recalibrées avant `rel.001` si les fixtures adversariales ou les formes `blockSubscribe` démontrent qu'une autre valeur KSP est préférable. ### 9.2 Cardinalité Contrat : ```text 1 WsEndpointSettings -> 0..N WsSession physiques créées explicitement par caller -> 0..N WsSubscription actives ``` Deux appels de création sur le même endpoint ouvrent deux connexions physiques. Transport ne distribue pas automatiquement les subscriptions entre elles. ### 9.3 Session actor Une tâche actor possède exclusivement : - le stream WebSocket physique ; - le compteur d'IDs JSON-RPC requests ; - la map des requests en vol ; - le registre local des subscriptions ; - le mapping `remote_subscription_id -> local_subscription_id` ; - le lifecycle reconnect/resubscribe ; - les compteurs/snapshot sûrs. Le handle public `WsSession` communique avec cet actor par canal bounded. Aucun caller ne split/manipule directement le socket. `pre.004` matérialise cette foundation : `WsSession::connect` ouvre une connexion physique explicite, le socket reste exclusivement dans l'actor, les commandes passent par `mpsc` borné, les réponses JSON-RPC sont dispatchées par ID KSP dans une map bornée et les snapshots sûrs sont publiés par `watch`. La primitive JSON-RPC reste `pub(crate)` afin de ne pas créer une API publique raw provider-extension avant les wrappers typed. Le registry des subscriptions et le mapping remote/local restent `pre.006`. ### 9.4 Identités ```text WsSessionId # KSP local, stable pour la vie du handle WsSubscriptionId # KSP local, stable malgré reconnect remote id u64 # strictement runtime/interne, remappable ``` `pre.002` matérialise les deux IDs locaux comme wrappers de `NonZeroU64`. La génération est volontairement laissée au futur actor ; aucun compteur global ni singleton n'est introduit dans cette tranche. Une `WsSubscription` typée contient l'identité locale, la projection de lifecycle et un receiver bounded de notifications typées. L'ID remote ne devient pas un identifiant métier/public de contrôle. ## 10. State machines retenues ### 10.1 Session ```text Disconnected -> Connecting -> Active -> Failed Active -> Reconnecting -> Active -> Failed -> Closing -> Closed Reconnecting -> Closing -> Closed ``` État public cible : ```text Disconnected | Connecting | Active | Reconnecting { attempt } | Closing | Closed | Failed ``` ### 10.2 Subscription ```text Requested -> Active -> Failed Active -> Resubscribing -> Active -> Failed -> Cancelling -> Closed -> Closed # terminaison serveur connue, ex. signature terminale Requested/Resubscribing -> Cancelling -> Closed ``` État public cible : ```text Requested | Active | Resubscribing | Cancelling | Closed | Failed ``` Le motif terminal est séparé (`Unsubscribed`, `CompletedByServer`, `SessionClosed`, `BackpressureOverflow`, `ProtocolFailure`, etc.) afin de ne pas gonfler l'enum d'états. ## 11. Reconnect et resubscribe ### 11.1 Reconnect Reconnect uniquement pour perte du canal physique : EOF, close inattendue, erreur I/O/TLS/WebSocket read/write. Une erreur RPC applicative de subscribe/unsubscribe n'entraîne pas une reconnexion automatique de la session. Policy : - nombre d'essais fini et configurable ; - backoff exponentiel borné par `initial_backoff`/`max_backoff` ; - **pas de jitter dans `0.2.7`** afin d'éviter une dépendance/randomisation supplémentaire et garder les tests déterministes ; - le budget se réinitialise après retour complet à `Active` ; - exhaustion => session `Failed`, subscriptions terminales avec cause explicite ; - shutdown/close annule immédiatement backoff et interdit toute nouvelle tentative. Les valeurs numériques par défaut sont une policy KSP et seront figées avec `WsSessionSettings` après tests ; elles ne seront pas attribuées à Solana. ### 11.2 Resubscribe Enum public : ```text WsResubscribePolicy::Never WsResubscribePolicy::ActiveSubscriptions ``` Default KSP proposé : `ActiveSubscriptions`. Après perte de connexion : 1. tous les remote IDs deviennent invalides et sont retirés du mapping ; 2. la session émet/incrémente un signal de **continuity gap** ; 3. après reconnect, seules les subscriptions encore désirées actives sont restaurées ; 4. ordre déterministe : `WsSubscriptionId` croissant / ordre de création ; 5. chaque ack remappe un nouvel ID remote ; 6. une erreur de resubscribe marque cette subscription `Failed` mais ne ferme pas les autres si le socket reste sain. KSP **ne garantit pas la continuité lossless** entre la déconnexion et la restauration. Aucun backfill HTTP automatique n'est ajouté à Transport. Les consumers workers/jobs pourront réconcilier plus tard avec leur propre checkpoint/persistence. ### 11.3 Race unsubscribe pendant reconnect La cancellation locale gagne toujours : - marquer la subscription non restaurable avant de traiter les acks tardifs ; - retirer sa spec de la sélection resubscribe ; - si un subscribe/resubscribe distant tardif réussit malgré tout, envoyer un best-effort unsubscribe de son nouvel ID ; - ne jamais repasser la subscription locale à `Active` après cancellation. ## 12. Backpressure et bornes de ressources ### 12.1 Queues - canal commands session : bounded ; - notifications : **queue bounded par subscription** ; - lifecycle/snapshot : `watch`/état partagé compact, pas une file data infinie ; - requests JSON-RPC en vol : map bornée + timeout ; - subscriptions actives : plafond configurable par session. ### 12.2 Overflow notifications Aucun drop silencieux. Si la queue d'une subscription est pleine : 1. compteur overflow incrémenté ; 2. subscription passe `Failed(BackpressureOverflow)` ; 3. best-effort unsubscribe distant si possible ; 4. son receiver se termine avec la cause observable via état final ; 5. les autres subscriptions de la session restent actives. Cette policy isole un consumer lent sans sacrifier toute la connexion et sans prétendre être lossless. ### 12.3 Frames/messages/JSON Configurer des plafonds finis pour : ```text max_message_size max_frame_size max_write_buffer_size max_pending_requests max_active_subscriptions command_queue_capacity notification_queue_capacity ``` La parse JSON ne reçoit donc jamais un message WebSocket arbitrairement grand. Les payloads raw conservés pour losslessness restent eux aussi sous cette borne. ## 13. Cancellation, control frames et shutdown ### 13.1 Control frames Le runtime gère proprement Ping/Pong/Close selon la crate WebSocket retenue. Aucun heartbeat applicatif périodique n'est activé par défaut dans `0.2.7` : la documentation Solana actuelle n'impose pas un ping KSP. Un keepalive configurable sera ajouté seulement si un besoin interop réel est démontré. ### 13.2 Shutdown explicite `WsSession::close().await` doit : 1. passer `Closing` et refuser de nouvelles subscriptions ; 2. annuler reconnect/backoff/resubscribe et requests en vol ; 3. si connecté, best-effort unsubscribe des actives dans un ordre déterministe sous un budget global ; 4. envoyer Close WebSocket ; 5. fermer toutes les subscriptions localement avec raison terminale ; 6. attendre la fin de l'actor sous `close_timeout` ; 7. finir `Closed` même si le peer ne répond pas à temps. Si la session est déjà reconnecting, elle ne se reconnecte jamais seulement pour envoyer des unsubscribe. `Drop` peut déclencher un signal best-effort, mais ne remplace pas l'API async explicite pour les garanties de lifecycle. ## 14. Erreurs et anomalies de protocole Réutiliser lorsque possible les codes HTTP/JSON-RPC génériques existants : ```text invalid_settings invalid_rpc_parameters json_encode_failed json_decode_failed json_rpc_protocol_invalid rpc_application_error timeout ``` Codes WS cibles minimaux : ```text ws_connection_failed ws_protocol_error ws_session_closed ws_subscription_failed ws_backpressure_overflow ``` Policy d'anomalies : - JSON/message WebSocket structurellement invalide : erreur protocole session, fermeture/reconnect selon budget ; - notification JSON valide mais payload typed invalide pour une subscription connue : fail de cette subscription, pas de teardown automatique des autres ; - notification valide pour ID remote inconnu/stale : compteur + log safe, drop ; - method notification incompatible avec la subscription connue : fail de cette subscription ; - réponse JSON-RPC d'erreur à subscribe/unsubscribe : `rpc_application_error`, pas reconnect implicite. ## 15. Observabilité et secrets Tous les événements KSP passent par `ksp-logging-lib`. Autorisés dans logs/snapshots : ```text endpoint logical name provider label cluster label WsSessionId WsSubscriptionId subscription kind state transitions active/pending counts reconnect attempt / exhaustion continuity_gap_count overflow_count error code qualifié ``` Interdits : ```text URL WebSocket complète query token/credential Authorization/header sensible raw notification arbitraire payload massif remote request body complet ``` Snapshot public cible : état session, endpoint name/provider/cluster, counts, reconnect state, continuity gaps, subscription IDs locaux + kinds + states. L'ID serveur peut rester interne ; un booléen `remote_bound` suffit pour diagnostiquer le binding sans faire croire à sa stabilité. `pre.002` matérialise `WsSessionSnapshot` et `WsSubscriptionSnapshot`. Leurs constructeurs restent crate-internal : les consumers ne peuvent pas fabriquer une fausse projection runtime. Aucun des deux types ne contient URL, credential, request body, raw notification ou remote subscription id. Observabilité `pre.002` : - `constants.rs` conserve le `TRACING_TARGET = "ksp-onchain-transport-lib"` crate-wide ; - toute émission ajoutée passe par les macros `ksp-logging-lib`, jamais par `tracing` directement ; - `trace` couvre les validations de settings et metadata sûres ; - `debug` confirme les settings validés avec uniquement des compteurs/bornes et descriptors sûrs ; - `warn` qualifie les rejets de settings/URL sans inclure la valeur URL ; - aucun `error` n'est ajouté artificiellement pour une simple erreur de validation caller. Les erreurs runtime terminales seront instrumentées quand l'actor/socket existera. Tests dédiés : URL `wss://user:pass@host/path?api-key=secret` ne doit apparaître ni dans `Debug`, ni erreurs, ni logs capturés, ni snapshots. `pre.002` couvre déjà `Debug`, erreur de validation et forme des snapshots ; la capture de logs runtime reste à compléter avec l'actor. ## 16. DTOs et losslessness Réutiliser les DTOs HTTP lorsque leur sémantique wire est réellement identique : ```text SolanaCommitment SolanaRpcContext SolanaRpcResponse SolanaAccountEncoding SolanaDataSliceConfig SolanaProgramAccountFilter SolanaAccount SolanaKeyedAccount block/transaction DTOs compatibles SolanaWireField ``` Définir des DTOs WS dédiés pour : enveloppes notification, lifecycle, subscription kind/config, logs, signature union, slot/root/slotsUpdates/vote, block update et toute forme où le wire diffère. `SolanaRpcContext.api_version` est déjà optionnel ; il peut donc représenter les contexts PubSub où la documentation indique que `apiVersion` est omis. Pour unstable/évolutif, préférer des enums avec fallback `Unknown { type_name, raw }` borné plutôt qu'un rejet session-wide d'un variant upstream nouveau. ## 17. API générique provider-specific Le moteur interne doit encoder une spec générique `subscribe_method + unsubscribe_method + params + decoder`, afin qu'une release provider-specific puisse réutiliser la session. La préparation Config/Transport de `0.2.7` se limite au discriminateur `WsProtocolKind` et au conteneur commun `ws_endpoints[]`. Elle doit permettre d'ajouter ultérieurement au moins une famille comme **Helius Enhanced WebSocket**, tout en laissant l'audit de la release suivante décider si Helius Enhanced WebSocket, LaserStream WebSocket et d'autres surfaces Helius partagent exactement le même moteur, les mêmes credentials et les mêmes paramètres. **Aucune API publique raw provider-extension et aucun paramètre Helius ne sont promis dans `0.2.7`.** Ils seront décidés après audit Helius. Cela évite de figer trop tôt une escape hatch qui deviendrait le contrat principal et contournerait les wrappers typés standards. ## 18. Tests déterministes attendus Le test runtime utilise un serveur WebSocket local déterministe, sans provider externe : ```text handshake + close subscribe -> notification -> unsubscribe plusieurs subscriptions sur une session deux sessions physiques sur la même URL IDs locaux stables / IDs distants remappés erreur RPC subscribe/unsubscribe JSON/message malformé notification method/payload incompatible unknown/stale remote subscription id message/frame oversized connexion interrompue reconnect borné + exhaustion + reset budget resubscribe déterministe unsubscribe pendant reconnect + ack stale signature terminale => closed, jamais resubscribe shutdown active/pending/reconnecting backpressure : overflow d'une sub n'impacte pas les autres limits active subscriptions/pending requests program notification contexted + non-contexted slotsUpdates unknown variant vote timestamp omitted/null/value URL/credentials absents de Debug/error/log/snapshot warning centralisé pour unstable à la création, pas à chaque notification ``` Le serveur local est un fixture de Transport ; aucune application Tauri n'est modifiée pour tester WebSocket. ## 19. Smoke live opt-in Après stabilisation : un test `#[ignore]` Transport pur sur Devnet, préférentiellement une subscription stable simple (`slotSubscribe`) : ```text connect slotSubscribe attendre une notification sous timeout unsubscribe close ``` Un endpoint override pourra être fourni par l'environnement Config seulement dans le smoke de composition séparé si celui-ci est réellement utile. Les unstable ne deviennent pas gates live car les validators publics peuvent les désactiver. Un rate-limit, refus provider ou indisponibilité externe ne constitue pas automatiquement une régression locale. ## 20. Threat model synthétique | Risque | Défense retenue | Preuves attendues | |--------------------------------------------------|----------------------------------------------------------------------|------------------------------| | URL/token fuit dans diagnostics | type URL redacted + snapshots sans URL + messages d'erreur qualifiés | tests Debug/error/log | | message/frame géant | limites WebSocket explicites avant JSON | tests oversized | | JSON/allocation non bornée | taille message bornée + DTOs ciblés/raw borné | adversarial fixtures | | consumer lent | queue par sub bounded, fail local explicite | overflow isolé | | reconnect infini | budget fini + backoff borné | exhaustion test | | notifications manquées pendant reconnect | continuity gap observable, aucune promesse lossless | lifecycle test/docs | | resubscribe stale après unsubscribe | ID local desired-state, cancellation gagne | race test | | remote ID réutilisé/remappé | mapping éphémère interne | reconnect/remap tests | | subscription orpheline | registry actor + close explicite + cleanup terminal | shutdown tests | | pending request leak | map bornée + timeout + purge reconnect/close | timeout tests | | task leak | actor unique joinable + close timeout | repeated connect/close tests | | shutdown bloqué | budget global + close timeout | hostile peer fixture | | unstable upstream variant casse session | typed enum + Unknown/raw borné | unknown variant fixture | | logique worker/persistence glisse dans Transport | aucun backfill/persistence/checkpoint | dependency/public API audit | ## 21. Hors périmètre confirmé ```text Helius Enhanced WS / provider params -> audit provider-specific futur Helius LaserStream WebSocket -> 0.2.8 selon séquence courante Yellowstone gRPC -> 0.2.9 provider Yellowstone commercial -> plus tard shred/deshred/pre-execution -> plus tard off-chain prices -> 0.2.10 Wallet / 2FA / .kspwallet -> hors 0.2.7 Store/persistence -> 0.3.x Program decode/materialization -> plus tard frontend réseau direct -> interdit pool/scheduler automatique de sessions -> différé backfill HTTP automatique sur gap WS -> workers/jobs futurs public raw provider-extension API -> gate 0.2.8 application heartbeat/ping périodique -> différé sans besoin démontré ``` ## 22. Forecast recalibré L'inventaire officiel n'impose que 9 familles de subscriptions, mais le lifecycle concurrent est plus coûteux que le forecast initial. Le gate reste **positif sans split de release**, à condition de granulariser les tranches au lieu de compresser le moteur et les wrappers. ```text pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing pre.002 DONE — settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings pre.003 DONE — std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings pre.004 DONE — deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests pre.006 registry subscriptions + IDs locaux + generic subscribe/unsubscribe engine + channels typed bounded pre.007 reconnect borné + resubscribe déterministe + continuity gap + races unsubscribe/reconnect pre.008 backpressure per-sub + overflow/limits + leak/lifecycle adversarial tests pre.009 wrappers stable lot A : account + program + logs, DTOs/options/KSP-TRANSPORT-007 pre.010 wrappers stable lot B : signature + slot + root, terminaison signature/KSP-TRANSPORT-007 pre.011 unstable : block + slotsUpdates + vote, warnings + fallbacks wire/KSP-TRANSPORT-007 pre.012 compliance 18/18 + canaries public API + composition Config + régressions HTTP pre.013 smoke live opt-in + README/USAGE + cargo tree/duplicates + dependency audit final pre.014 validation workspace finale + docs/compliance + prompt 0.2.8 rel.001 publication stable stricte ``` Chaque tranche reste scindable si son implémentation réelle dépasse le budget nominal de 15–20 minutes. Le nombre `pre.014` n'est pas une deadline normative. ## 23. Gates de clôture `0.2.7` La release ne peut passer stable que si : - l'inventaire officiel est réaudité et reste rapproché par noms exacts ; - 18/18 opérations standard de l'index ciblé ont un statut explicite ; - 9/9 subscribe ont leur wrapper KSP promis et 9/9 unsubscribe sont couverts via handles/registry ; - options et variantes wire sont conservées sous `KSP-TRANSPORT-007` ; - la cardinalité endpoint -> N sessions -> N subscriptions est démontrée ; - reconnect/resubscribe/backpressure/cancellation/shutdown sont bornés et testés ; - continuity gaps sont observables sans promesse lossless ; - secrets/URLs ne fuitent pas ; - Config -> Transport reste l'unique direction d'adaptation ; - aucune dépendance Store/Program/Wallet/Config/tracing direct n'apparaît dans Transport ; - HTTP 52+14 ne régresse pas ; - tests déterministes et workspace sont verts ; - smoke live retenu reste opt-in ; - README/USAGE, matrice finale, graphes Cargo et prompt `0.2.8` sont synchronisés. ## 24. Validation opérateur requise pour `pre.001` Après application de ce gate documentaire/versionné : ```bash cargo fmt --all python3 scripts/audit_rust_workspace_rules.py cargo check --workspace cargo clippy --workspace --all-targets ``` `cargo test --workspace` n'est pas imposé par cette tranche documentaire d'ouverture tant qu'aucun Rust runtime n'est ajouté, mais reste autorisé comme checkpoint opérateur.