48 KiB
Validation 0.2.7 — WebSocket Solana standard
Statut : matrice active,
0.2.7-pre.013. La compliance 18/18 est acquise.pre.013ajoute le smoke WebSocket Devnet opt-in, synchronise README/USAGE et renforce le gate de dépendances avant la validation workspace finale.
1. Baseline normative
Audit officiel effectué le 22 août 2026 contre :
https://solana.com/docs/rpc/websocket
Compte exact de l'index courant :
9 subscribe
9 unsubscribe
18 méthodes WebSocket totales
Répartition statut KSP :
12 méthodes appartenant à 6 paires documentées non marquées unstable/deprecated
6 méthodes appartenant à 3 paires unstable : block, slotsUpdates, vote
0 méthode de l'index courant marquée Deprecated
Pour une paire unstable, l'unsubscribe associé est classé Unstable pair dans KSP même si sa propre page n'affiche pas nécessairement le bandeau, car il n'existe que pour annuler la subscription unstable correspondante.
2. Matrice exhaustive des 18 opérations
| # | Méthode | Type | Statut pre.001 |
Paramètres / résultat essentiels | Notification / paire | Stratégie de test | Source officielle | Compliance |
|---|---|---|---|---|---|---|---|---|
| 1 | accountSubscribe |
subscribe | Stable/documented | pubkey ; config commitment, encoding, dataSlice ; result numeric id ; minContextSlot upstream actuellement ignoré, donc non promis |
accountNotification |
fixture encodings/config + subscribe/notify | https://solana.com/docs/rpc/websocket/accountsubscribe |
Done pre.009 |
| 2 | accountUnsubscribe |
unsubscribe | Stable/documented | remote id ; true or RPC error unknown id |
account pair | handle local -> remote id fixture | https://solana.com/docs/rpc/websocket/accountunsubscribe |
Done pre.009 |
| 3 | blockSubscribe |
subscribe | Unstable | all/mentions filter ; confirmed/finalized ; encoding ; tx details ; max tx version ; showRewards |
blockNotification |
all options + null block/error + validator capability fixture | https://solana.com/docs/rpc/websocket/blocksubscribe |
Done pre.011 |
| 4 | blockUnsubscribe |
unsubscribe | Unstable pair | remote id ; boolean/error | block pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/blockunsubscribe |
Done pre.011 |
| 5 | logsSubscribe |
subscribe | Stable/documented | all, allWithVotes, exactly one mentions; commitment |
logsNotification |
3 filters + invalid multi-mention + notification | https://solana.com/docs/rpc/websocket/logssubscribe |
Done pre.009 |
| 6 | logsUnsubscribe |
unsubscribe | Stable/documented | remote id ; boolean/error | logs pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/logsunsubscribe |
Done pre.009 |
| 7 | programSubscribe |
subscribe | Stable/documented | program pubkey ; commitment ; filters ; encoding ; dataSlice ; withContext |
programNotification |
contexted/non-contexted fixtures + filters | https://solana.com/docs/rpc/websocket/programsubscribe |
Done pre.009 |
| 8 | programUnsubscribe |
unsubscribe | Stable/documented | remote id ; boolean/error | program pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/programunsubscribe |
Done pre.009 |
| 9 | rootSubscribe |
subscribe | Stable/documented | no params ; numeric id | rootNotification => u64 |
exact root fixture | https://solana.com/docs/rpc/websocket/rootsubscribe |
Done pre.010 |
| 10 | rootUnsubscribe |
unsubscribe | Stable/documented | remote id ; boolean/error | root pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/rootunsubscribe |
Done pre.010 |
| 11 | signatureSubscribe |
subscribe | Stable/documented | first transaction signature ; commitment ; enableReceivedNotification |
signatureNotification early string or terminal error object |
early + terminal + auto-close/no-resubscribe | https://solana.com/docs/rpc/websocket/signaturesubscribe |
Done pre.010 |
| 12 | signatureUnsubscribe |
unsubscribe | Stable/documented | remote id before terminal fire ; boolean/error | signature pair | cancel before terminal + stale after terminal | https://solana.com/docs/rpc/websocket/signatureunsubscribe |
Done pre.010 |
| 13 | slotSubscribe |
subscribe | Stable/documented | no params ; numeric id | slotNotification {slot,parent,root} |
exact fixture + live smoke candidate | https://solana.com/docs/rpc/websocket/slotsubscribe |
Done pre.010 |
| 14 | slotUnsubscribe |
unsubscribe | Stable/documented | remote id ; boolean/error | slot pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/slotunsubscribe |
Done pre.010 |
| 15 | slotsUpdatesSubscribe |
subscribe | Unstable | no params ; numeric id | tagged slotsUpdatesNotification |
each known variant + unknown fallback | https://solana.com/docs/rpc/websocket/slotsupdatessubscribe |
Done pre.011 |
| 16 | slotsUpdatesUnsubscribe |
unsubscribe | Unstable pair | remote id ; boolean/error | slotsUpdates pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/slotsupdatesunsubscribe |
Done pre.011 |
| 17 | voteSubscribe |
subscribe | Unstable | no params ; validator flag required | voteNotification |
fields + timestamp omitted/null/value + warning | https://solana.com/docs/rpc/websocket/votesubscribe |
Done pre.011 |
| 18 | voteUnsubscribe |
unsubscribe | Unstable pair | remote id ; boolean/error | vote pair | generic registry unsubscribe | https://solana.com/docs/rpc/websocket/voteunsubscribe |
Done pre.011 |
3. Notification matrix
| Subscribe | Notification | Shape à préserver | Point lossless / lifecycle |
|---|---|---|---|
accountSubscribe |
accountNotification |
contextual account payload | reuse account DTOs/encodings/dataSlice |
programSubscribe |
programNotification |
keyed account, documenté avec contexte | accepter contexted/non-contexted à cause de l'écart docs/source audité ; préserver withContext |
logsSubscribe |
logsNotification |
context + {signature, err, logs} |
err nullable ; filtre mentions exactement une adresse |
signatureSubscribe |
signatureNotification |
context + "receivedSignature" ou {err} |
terminal object clôt la subscription ; early string ne la clôt pas |
slotSubscribe |
slotNotification |
{slot,parent,root} |
non-contextual |
rootSubscribe |
rootNotification |
u64 |
non-contextual |
blockSubscribe |
blockNotification |
context + {slot, block, err} |
unstable ; block/err nullable ; variants block selon config |
slotsUpdatesSubscribe |
slotsUpdatesNotification |
tagged union slot lifecycle | unstable ; fallback unknown/raw borné |
voteSubscribe |
voteNotification |
{votePubkey,slots,hash,timestamp,signature} |
unstable/pre-consensus ; timestamp tolerant wire |
Les contexts WebSocket documentés omettent apiVersion. SolanaRpcContext.api_version: Option<String> est compatible avec cette omission.
4. Baselines Git Agave et SIMD
4.1 Hiérarchie de contrôle
La compliance applique le même modèle que la compliance HTTP finale :
documentation Solana actuelle
-> surface publique annoncée
Git Agave tag courant audité v4.2.1
-> implémentation réelle et types wire
SIMD main
-> évolutions Activated, Review, Draft ou Idea à surveiller
Les pages Solana peuvent lier une révision Agave plus ancienne. KSP ne confond donc pas le lien source embarqué dans une page documentaire avec la baseline Git courante. Au 2026-08-22, Agave v4.2.1 est disponible sur Git et devient la baseline d'implémentation de 0.2.7.
Sources Agave ciblées :
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc/src/rpc_pubsub.rs
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc/src/rpc_subscriptions.rs
https://github.com/anza-xyz/agave/blob/v4.2.1/rpc-client-types/src/response.rs
4.2 Constats Agave v4.2.1
Le cross-check du tag courant confirme les 9 subscribe + 9 unsubscribe déjà inventoriés par la documentation publique et ne révèle aucune opération PubSub standard supplémentaire.
Constats ciblés :
accountSubscribe: le type partagéRpcAccountInfoConfigexposemin_context_slot, mais le handler PubSubv4.2.1le destructure en_ // ignored. KSP ne le compte donc pas comme option WebSocket effective ;programSubscribe:RpcProgramAccountsConfig.with_contextest lu et doit rester dans le contrat KSP ; la tolérance contexted/non-contexted reste requise pour ne pas convertir les divergences docs/source/provider en perte wire ;logsSubscribe: le handler accepteall,allWithVotesoumentionset rejette un filtrementionscontenant autre chose qu'exactement une adresse ;signatureSubscribe: le handler utilise la signature,commitmentetenable_received_notification, sans option WebSocket supplémentaire ;blockSubscribe: le handler consomme le jeu d'options documenté et impose un commitment au moinsconfirmed;*Unsubscribe: un ID serveur inconnu produitInvalidParamsdans le handler audité ;voteSubscribe:RpcVote.timestampestOption<UnixTimestamp>, donc optionnel au niveau wire ;slotsUpdatesSubscribe:SlotUpdatepossède toujours les sept variantes courantesFirstShredReceived,Completed,CreatedBank,Frozen,Dead,OptimisticConfirmationetRoot.
Décisions compliance : transmettre programSubscribe.withContext, accepter les formes contextée/non-contextée, ne pas promettre accountSubscribe.minContextSlot tant qu'il est ignoré upstream, conserver vote.timestamp optionnel et garder un fallback borné pour toute future variante SlotUpdate inconnue.
4.3 Audit SIMD ciblé
Aucun SIMD audité n'ajoute actuellement une dixième famille de subscription standard à la surface Agave v4.2.1. Ils modifient en revanche des hypothèses que KSP doit éviter de figer.
| SIMD | Statut au 2026-08-22 | Conséquence WebSocket KSP |
|---|---|---|
0118 Partitioned Epoch Rewards Distribution |
Activated | conserver les DTOs bloc/rewards HTTP déjà lossless, dont numRewardPartitions omitted, null ou valeur |
0291 Commission Rate in Basis Points |
Review | conserver les représentations de commission upstream indépendantes lorsqu'elles sont présentes |
0296 Larger Transaction Size |
Review | proposition jusqu'à 4096 octets : ne pas déduire les limites WS de l'ancienne taille transaction 1232 ; surveiller blockSubscribe |
0298 Bank Hash in Block Footer |
Idea | aucun bankHash spéculatif dans le wire courant ; surveiller le DTO bloc partagé |
0301 parent bank hash |
PR fermé, non mergé | aucun parentBankHash spéculatif ; même conclusion que la compliance HTTP |
0307 Add Block Footer |
Review | aucun footer spéculatif ; réauditer blockSubscribe lorsque l'upstream l'expose réellement |
0326 Alpenglow |
Review | ne pas figer les sémantiques TowerBFT de voteSubscribe ou optimisticConfirmation dans un contrat durable |
0337 Alpenglow Fast Leader Handover Markers |
Review | nouveaux block markers futurs : surveiller shape et taille, sans ajout WS anticipé |
0384 Alpenglow migration |
Review | des notifications RPC commitment/optimistic confirmation peuvent être suspendues pendant migration ; aucune hypothèse de séquence exhaustive |
0385 Transaction V1 |
Review | maxSupportedTransactionVersion et les versions transaction restent génériques ; ne pas caper KSP à v0 |
Sources SIMD :
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
Les limites WebSocket sont donc des bornes KSP configurables et finies, jamais une transposition en dur de l'ancienne limite transaction de 1232 octets.
5. Méthodes unstable
blockSubscribe
Conditions officielles :
--rpc-pubsub-enable-block-subscription
--enable-rpc-transaction-history
KSP : warning centralisé à la création, test fixture toujours disponible, smoke live non requis.
slotsUpdatesSubscribe
Le format est explicitement annoncé comme susceptible de changer. Variants actuels :
firstShredReceived slot,timestamp
completed slot,timestamp
createdBank slot,parent,timestamp
frozen slot,timestamp,stats
dead slot,timestamp,err
optimisticConfirmation slot,timestamp
root slot,timestamp
stats actuel : numTransactionEntries, numSuccessfulTransactions, numFailedTransactions, maxTransactionsPerEntry.
voteSubscribe
Condition officielle :
--rpc-pubsub-enable-vote-subscription
Les votes observés sont gossip/pre-consensus ; aucune garantie d'entrée dans le ledger. Transport les livre comme wire, sans interprétation métier.
6. Lifecycle compliance initiale
| Contrat | Décision pre.001 |
Gate cible |
|---|---|---|
| plusieurs sessions / même URL | création physique explicite ; aucun singleton/pool automatique | Done pre.004, canary final pre.012 |
| plusieurs subs / session | registry actor par session | Done pre.006 |
| ID public subscription | local KSP stable | Done pre.006 |
| ID serveur | éphémère interne et remappé | Done pre.006 puis pre.007 |
| session states | Disconnected/Connecting/Active/Reconnecting/Closing/Closed/Failed | Done pre.002 |
| subscription states | Requested/Active/Resubscribing/Cancelling/Closed/Failed | Done pre.002 |
| reconnect | physique uniquement, budget/backoff finis | Done pre.007 |
| resubscribe | policy Never ou ActiveSubscriptions, ordre local déterministe |
Done pre.007 |
| continuity | gap observable, aucune promesse lossless | Done pre.007 |
| backpressure | queue par sub bounded ; overflow => fail local explicite | Done pre.008 |
| shutdown | explicite, bounded, annule reconnect et subscriptions | Done pre.005 |
| keepalive | pas de ping applicatif périodique sans besoin démontré | Done pre.005 |
7. Threat/security compliance initiale
| Invariant | Preuve attendue | Statut |
|---|---|---|
URL/credentials absents de Debug |
unit tests URL wrapper | Done pre.002 |
| URL/credentials absents des erreurs | validation URL + connection errors safe | Done through pre.004 |
| URL/credentials absents des logs | actor logs only safe endpoint metadata | Done through pre.011, source audit pre.013 |
| snapshots sans URL/raw payload | unit shape + public contract | Done pre.002 |
| frame/message finis | settings bornés + WebSocketConfig raccordé |
Done pre.005 |
| JSON borné indirectement par message | oversized + malformed fixture | Done pre.005 |
| queues notifications bornées | queue typed bornée + overflow isolé | Done pre.008 |
| pending RPC borné + timeout | map actor bornée + timeout request | Done pre.004 |
| reconnect loop bornée | repeated disconnect fixture | Done pre.007 |
| unsubscribe pendant reconnect ne resubscribe pas | race fixture | Done pre.007 |
| signature terminale ne resubscribe pas | terminal fixture | Done pre.010 |
| shutdown ne bloque pas | peer hostile/no close ack fixture | Done pre.005 |
| no Store/Program/Wallet/Config dep dans Transport | cargo tree + source canary | Source gate pre.013, cargo tree opérateur requis |
no direct tracing dans Transport |
workspace audit + source audit | Done pre.002 |
8. Dependency compliance initiale
Candidates auditées à l'ouverture de release :
tokio-tungstenite 0.30.x
futures-util 0.3.x
Le workspace conserve des contraintes caret ^0.30 et ^0.3 sans lockfile versionné. La version transitive effectivement résolue n'est donc pas figée dans cette matrice ; pre.013 exige l'inspection du cargo tree opérateur correspondant au checkout validé.
Features retenues :
tokio-tungstenite: default-features=false + connect + rustls-tls-webpki-roots
futures-util: default-features=false + std + sink
Sources :
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
Alternatives auditées mais non retenues : tokio-websockets 0.13.3, fastwebsockets 0.10.0.
pre.004 matérialise tokio-tungstenite et futures-util dans [workspace.dependencies] sans features consumer au root. Transport active seulement connect, rustls-tls-webpki-roots, std et sink; tokio/net est ajouté côté dev fixture local.
9. Config compliance initiale
V1 hérité : HTTP-only strict. Contrat de compatibilité :
V1 -> support de lecture conservé, WS vide
V2 -> HTTP existant + ws_defaults + profiles[].ws_endpoints
profiles[].ws_endpoints[].kind -> discriminateur de famille/protocole WS
0.2.7 -> seule valeur supportée : solana_standard
Config -> WsTransportSettings
Transport -X-> Config
Le type Transport correspondant au discriminateur est prévu #[non_exhaustive]. L'objectif est de pouvoir ajouter ultérieurement une famille telle que Helius Enhanced WebSocket sans créer un nouveau conteneur Config ni injecter des options provider-specific dans les settings Solana standard. Toute valeur inconnue reste explicitement rejetée en 0.2.7; aucun paramètre Helius n'est implémenté par anticipation.
pre.003 remplace le schema enregistré par urn:ksp:schema:std.transport:v2, avec deux branches strictes : V1 HTTP-only et V2 HTTP + WebSocket. La compatibilité V1 ne relâche donc ni additionalProperties, ni la shape historique.
Preuves pre.003 :
config/std.transport.json format_version = 2
config/examples/std.transport.example.json format_version = 2
V1 fixture dédiée load + HTTP mapping OK attendu
V1 ws_settings = None
V2 ws_settings = Some(validated)
ws_defaults globals
profiles[].ws_endpoints profile-local
ws_endpoints[].kind enum schema solana_standard
ws_endpoints[].session overrides génériques optionnels
Config -> Transport seule direction de dépendance
ResolvedTransportConfig::settings() reste l'accesseur HTTP historique. http_settings() l'explicite et ws_settings() expose Option<&WsTransportSettings> afin que le V1 backward ne force jamais un faux WsTransportSettings vide. into_transport_settings() permet de consommer les deux contrats ensemble.
9.1 Checkpoint settings/lifecycle pre.002
Surface publique matérialisée :
WsEndpointUrl
WsProviderName
WsClusterName
WsProtocolKind::SolanaStandard
WsReconnectSettings
WsResubscribePolicy::{Never, ActiveSubscriptions}
WsSessionSettings
WsEndpointSettings
WsTransportSettings
WsSessionId
WsSubscriptionId
WsSessionState
WsSubscriptionState
WsSubscriptionKind
WsSessionSnapshot
WsSubscriptionSnapshot
Gates déterministes ajoutés :
ws:// et wss:// acceptés
HTTP rejeté par WsEndpointUrl
Debug URL redacted
erreur de scheme sans URL/credential
settings session default bornés
zero runtime bound rejeté
reconnect backoff inversé rejeté
endpoint names uniques
au moins un endpoint enabled
Debug WsTransportSettings sans URL/credential
9 familles standard WsSubscriptionKind
snapshots sans URL ni remote subscription id
public API canary crate-root
Logging : TRACING_TARGET reste défini dans constants.rs; les nouveaux événements utilisent exclusivement ksp_logging_lib::trace!, debug! et warn! avec des fields sûrs. Aucun tracing direct n'est ajouté.
Les defaults de taille/queue sont des policies KSP locales, pas des limites Solana. Leur enforcement réel et leurs tests oversized/slow-consumer restent attendus dans les tranches socket/backpressure.
9.2 Checkpoint runtime physique pre.004
Surface matérialisée :
WsSession::connect(endpoint) public, une connexion physique par appel
actor socket propriétaire exclusif du WebSocket
command queue tokio mpsc bounded
pending JSON-RPC BTreeMap bounded par max_pending_requests
request timeout command_timeout
response dispatch par id numérique KSP
state snapshot watch + WsSessionSnapshot
raw JSON-RPC public non, primitive pub(crate)
reconnect/resubscribe non, gates futurs
Fixtures déterministes pre.004 :
handshake local + JSON-RPC round-trip
deux sessions physiques distinctes sur la même URL
deux requests concurrentes + réponses inversées
erreur RPC applicative sans teardown de session
erreur de connexion sans URL/credential dans Error Debug
Les limites max_message_size, max_frame_size et max_write_buffer_size sont raccordées à tungstenite::WebSocketConfig. Les fixtures oversized et le shutdown/control-frame hostile restent explicitement pre.005.
9.3 Checkpoint limits/control/shutdown pre.005
Gates déterministes ajoutés :
max_pending_requests saturé -> seule la request excédentaire est rejetée
outbound JSON-RPC > borne message/frame -> rejet avant socket write, session Active
inbound frame/message oversized -> rejet Tungstenite avant parse JSON ; depuis pre.007, reconnect borné
pending request silencieuse -> timeout + purge de capacité, session Active
Ping distant -> Pong automatique flushé, session Active
Close distant propre -> checkpoint `pre.005` Closed ; à partir de `pre.007`, perte physique reprise par reconnect borné
WsSession::close() -> Closing -> Closed
close() annule les pending requests avec ws_session_closed
peer hostile qui ne répond pas au Close -> shutdown borné
cycles connect/close répétés -> terminaison bornée
Le signal de shutdown est indépendant de la command queue et les opérations socket longues de l'actor surveillent ce signal. Aucun reconnect/resubscribe n'est activé par cette tranche.
9.4 Checkpoint registry subscriptions pre.006
Surface matérialisée :
WsSubscription<T> handle public typed
WsSubscriptionId local stable, actor-assigned
remote subscription id interne/transient uniquement
registry local BTreeMap ordonnée par local ID
remote -> local mapping actor-owned
subscribe generic crate-private, wrappers publics futurs
unsubscribe handle public, bool Solana préservé
notification queue typed + bounded
reconnect/resubscribe non à ce checkpoint ; matérialisé en pre.007
backpressure adversarial complet oui, pre.008
Gates déterministes ajoutés :
subscribe ACK -> binding remote/local atomique
notification -> bonne subscription typed
2 familles -> IDs locaux 1 puis 2, remote IDs indépendants
unknown/stale remote ID -> safe drop, session Active
notification method mismatch -> subscription Failed seulement
typed decoder failure -> erreur livrée + subscription Failed seulement
unsubscribe -> exact *Unsubscribe avec remote ID interne
unsubscribe bool -> préservé au caller
snapshot -> local IDs + remote_bound uniquement
Le registry est détenu par le même actor que le socket et la pending map JSON-RPC. Aucun caller ne manipule le socket ni le remote ID. Les wrappers publics accountSubscribe, programSubscribe, etc. restent volontairement différés aux lots pre.009+ afin de ne pas exposer une API raw provider-extension intermédiaire.
9.5 Checkpoint reconnect/resubscribe pre.007
Surface matérialisée :
reconnect triggers EOF, Close distant inattendu, I/O/TLS/WebSocket/protocole structurel
reconnect budget fini, configurable
backoff exponentiel borné, sans jitter
shutdown pendant reconnect prioritaire, annule backoff/handshake
continuity_gap_count incrémenté une fois par perte de continuité logique
remote IDs après perte invalidés immédiatement
ActiveSubscriptions resubscribe local-ID croissant
Never aucune restauration automatique
budget reset seulement après retour complet à Active
unsubscribe pendant reconnect cancellation locale gagnante
ACK resubscribe tardif cleanup distant best-effort, aucune réactivation locale
backfill HTTP aucun
lossless guarantee aucune
Gates déterministes ajoutés :
remote Close + budget insuffisant -> Reconnecting puis Failed, jamais boucle infinie
2 subscriptions -> resubscribe dans l'ordre WsSubscriptionId 1 puis 2
remote IDs 101/202 -> remappés 301/302 sans changer les IDs locaux
params de subscribe -> conservés et rejoués à l'identique
unsubscribe pendant backoff -> sub Closed et absente de la sélection resubscribe
unsubscribe après émission resubscribe -> ACK tardif nettoyé par *Unsubscribe best-effort
policy Never -> session reconnectée Active, subscription terminale Failed
2 pertes séparées avec max_retries=1 -> deux recoveries possibles, budget réinitialisé après Active
continuity_gap_count -> 1 puis 2 sur deux pertes distinctes
Les requests applicatives en vol au moment d'une perte physique échouent ; Transport ne les rejoue pas implicitement. La restauration ne promet aucune continuité lossless et n'effectue aucun backfill HTTP. Les remote subscription IDs restent strictement internes et sont remplacés à chaque ACK de resubscribe.
9.6 Checkpoint backpressure/lifecycle pre.008
Surface matérialisée :
notification_queue_capacity borne effective par WsSubscription<T>
overflow_count compteur session saturating, réellement incrémenté
terminal_error_code ErrorCode KSP sûr sur le handle Failed
queue pleine fail local uniquement, aucun drop silencieux
remote cleanup *Unsubscribe best-effort après overflow/receiver drop/erreur locale
max_active_subscriptions admission bornée, capacité réutilisable après terminaison
receiver abandonné détecté sur notification suivante, registry libéré
reconnect après overflow subscription Failed absente de toute restauration future
Gates déterministes ajoutés :
queue capacité 1 + 2 notifications -> overflow_count 1 + ws_backpressure_overflow
subscription lente en overflow -> Failed, première notification déjà queueée reste lisible puis channel fermé
subscription saine parallèle -> notification livrée et état Active
session après overflow isolé -> Active
binding distant overflow -> *Unsubscribe best-effort exact
max_active_subscriptions atteint -> création excédentaire rejetée sans incrémenter overflow_count
unsubscribe terminal -> capacité locale libérée puis nouvelle subscription acceptée
receiver droppé -> notification suivante déclenche cleanup distant et libère la capacité
method mismatch -> ws_protocol_error terminal sur le handle, session Active
decode typed invalide -> code invalid_response terminal sur le handle, session Active
policy Never après perte physique -> ws_connection_failed terminal sur le handle
resubscribe RPC application error -> rpc_application_error terminal sur le handle
overflow_count et continuity_gap_count sont deux signaux distincts : le premier mesure les saturations locales de consumers, le second les ruptures de continuité physique. Aucun des deux ne déclenche de replay ou de backfill automatique. Les causes terminales n'exposent qu'un ErrorCode KSP, jamais le payload distant, le remote subscription ID ou l'URL.
9.7 Checkpoint wrappers stables lot A pre.009
Surface publique ajoutée :
WsSession::account_subscribe
SolanaAccountSubscribeConfig
WsSubscription<SolanaRpcResponse<SolanaAccount>>
WsSession::program_subscribe
SolanaProgramSubscribeConfig
SolanaProgramNotification = bare keyed account | contextual keyed account
WsSession::logs_subscribe
SolanaLogsSubscribeFilter = All | AllWithVotes | Mentions(Pubkey)
SolanaLogsNotification = signature + err nullable + logs ordonnés
Gates déterministes ajoutés :
accountSubscribe -> params exacts pubkey + encoding/dataSlice/commitment
accountSubscribe -> aucun minContextSlot promis ou sérialisable par le config WS
accountNotification -> SolanaRpcContext + SolanaAccount partagé
account handle unsubscribe -> accountUnsubscribe avec remote ID interne
programSubscribe -> filters + withContext préservés, sortResults absent
program filters -> >4 et raw memcmp >128 rejetés avant émission subscribe
programNotification -> forme bare acceptée
programNotification -> forme contextualisée acceptée
program handle unsubscribe -> programUnsubscribe exact
logs filter -> all/allWithVotes/mentions exactement une pubkey
logsNotification -> context + signature + err null/object + ordre logs préservés
logsNotification sans champ err requis -> invalid_response typed, session non concernée
logs handle unsubscribe -> logsUnsubscribe exact
API publique -> aucun subscribe(method, raw params) exposé
Les trois wrappers passent par le même moteur actor/registry acquis en pre.006–pre.008; les remote IDs ne deviennent donc pas publics et les paramètres typés initiaux restent les specs rejouées lors d'un resubscribe ActiveSubscriptions.
9.8 Checkpoint wrappers stables lot B pre.010
Surface publique ajoutée :
WsSession::signature_subscribe
SolanaSignatureSubscribeConfig = commitment + enableReceivedNotification
SolanaSignatureNotification = ReceivedSignature | Processed { err }
WsSession::slot_subscribe
SolanaSlotNotification = slot + parent + root
WsSession::root_subscribe
notification = u64
Gates déterministes ajoutés :
signatureSubscribe -> signature + config commitment/enableReceivedNotification exacts
config signature vide explicite -> second paramètre omis
receivedSignature -> notification typed non terminale, handle reste Active
Processed { err:null } -> notification terminale success, handle Closed sans terminal_error_code
Processed { err:object } -> transaction error wire préservée comme valeur terminale, pas erreur Transport
string signature inconnue ou objet sans err -> invalid_response typed
cancellation avant terminal -> signatureUnsubscribe exact avec remote ID interne
terminal déjà observé -> unsubscribe local-only false, aucun remote cleanup inutile
perte physique après terminal -> reconnect session possible mais signature jamais resubscribe
slotSubscribe -> params vides + {slot,parent,root} exact
slotUnsubscribe -> remote ID interne via handle
rootSubscribe -> params vides + root u64 exact
rootUnsubscribe -> remote ID interne via handle
API publique -> aucun remote subscription ID ni méthode raw arbitraire exposés
La terminaison signature est classée après livraison dans la queue typed : le consumer reçoit donc toujours la valeur terminale avant fermeture du canal. Le registry retire ensuite la subscription one-shot avant toute future sélection de resubscribe. slot et root restent des subscriptions continues et conservent les règles génériques de reconnect, backpressure et cancellation.
9.9 Checkpoint familles unstable pre.011
Surface publique ajoutée :
WsSession::block_subscribe
SolanaBlockSubscribeFilter = All | MentionsAccountOrProgram(Pubkey)
SolanaBlockSubscribeConfig = commitment + encoding + transactionDetails + maxSupportedTransactionVersion + showRewards
SolanaBlockNotification = slot + block nullable + err nullable
WsSession::slots_updates_subscribe
SolanaSlotUpdate = 7 variantes connues + Unknown raw borné
SolanaSlotUpdateStats
WsSession::vote_subscribe
SolanaVoteNotification = votePubkey + slots + hash + timestamp optionnel + signature
Gates déterministes ajoutés :
partition unstable exacte = block + slotsUpdates + vote
warning centralisé dans le moteur typed avant création d'une famille unstable
block filter all + mentionsAccountOrProgram
block config -> confirmed/finalized seulement ; processed rejeté avant I/O
block config -> binary/base58/base64/json/jsonParsed + full/accounts/signatures/none
block maxSupportedTransactionVersion -> valeur numérique générique, pas de hardcode 0
block notification -> context + slot + block nullable + err nullable
block payload > 1232 octets -> accepté tant qu'il reste sous les limites WS KSP
block capability RPC error -> erreur applicative caller, session physique reste Active
blockUnsubscribe -> remote ID interne via handle
slotsUpdates -> 7 variantes actuelles exactes
slotsUpdates future type -> Unknown avec raw conservé sous la borne message
createdBank/frozen/dead -> champs spécifiques requis
slotsUpdatesSubscribe/Unsubscribe -> params vides + remote ID interne
vote timestamp omitted/null/value -> None/None/Some(i64)
votePubkey -> Pubkey typed ; slots ordonnés ; hash/signature opaques
voteSubscribe/Unsubscribe -> params vides + remote ID interne
La protection Unknown de slotsUpdates est volontairement limitée aux nouvelles variantes de type. Une variante connue avec des champs obligatoires invalides reste invalid_response pour la subscription concernée : KSP ne masque pas une rupture du contrat déjà audité.
Le warning unstable ne contient ni filtre, ni pubkey, ni payload, ni remote subscription ID, ni URL. Les trois familles restent continues et réutilisent le lifecycle générique reconnect/resubscribe/backpressure/cancellation.
Comptages attendus après compilation :
Transport unit tests = 309
Transport public API tests = 35
release completeness = 22
9.10 Checkpoint compliance consolidée pre.012
pre.012 n'ajoute aucune nouvelle famille WebSocket. Il transforme les preuves acquises de pre.002 à pre.011 en gates de release explicites et vérifie simultanément que la surface HTTP héritée reste intacte.
Le réaudit de l'index public Solana effectué le 23 août 2026 confirme que la surface courante reste inchangée à 9 subscribe + 9 unsubscribe.
Gates ajoutés ou consolidés :
9 WsSubscriptionKind publics exactement couverts
9 wrappers subscribe publics disponibles depuis la crate root
1 handle WsSubscription<T>::unsubscribe commun aux 9 familles
9 paires subscribe/unsubscribe = 18 opérations standard comptabilisées
triplets exacts subscribe/unsubscribe/notification toujours verrouillés par le canari ws_lifecycle
partition unstable exacte toujours verrouillée : block + slotsUpdates + vote
Config V2 committed -> ResolvedTransportConfig -> WsTransportSettings validé
WsEndpointSettings produit par Config -> type accepté directement par WsSession::connect
Config V1 -> HTTP-only reste couvert par le checkpoint pre.003
HTTP courant -> 52 méthodes Supported
HTTP historique -> 14 méthodes Removed
surface API publique WebSocket complète -> canari crate-root dédié
La composition Config est testée sans réseau : l'async future WsSession::connect est construite à partir de l'endpoint produit par Config et laissée non pollée jusqu'à sa destruction en fin de portée. Le gate vérifie donc la compatibilité de types et de contrats entre Config et Transport sans transformer pre.012 en smoke live ; le vrai smoke reste réservé à pre.013.
Comptages attendus après compilation :
Transport unit tests = 309
Transport public API tests = 36
Transport release completeness = 24
Config unit tests = 109
9.11 Checkpoint smoke live et audit de dépendances pre.013
Le smoke WebSocket live retenu est un test Transport pur #[ignore] qui ne lit ni Config ni environnement :
endpoint programmatique wss://api.devnet.solana.com
WsSession::connect
slotSubscribe stable
attente bornée d'une slotNotification
slot > 0
slotUnsubscribe via handle
close explicite de la session
Les familles block, slotsUpdates et vote ne sont pas utilisées dans ce smoke car leur disponibilité dépend de capabilities validator unstable. Les fixtures locales restent les preuves reproductibles de ces familles et du lifecycle.
Le canari workspace de dépendances verrouille désormais les noms directs de Transport :
runtime : futures-util, ksp-core-lib, ksp-logging-lib, reqwest, serde, serde_json, tokio, tokio-tungstenite
dev : tokio
Il complète le firewall existant contre Config/Store/Program/tracing direct. L'inspection du graphe résolu reste une preuve opérateur avec :
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
Une duplication transitive n'est pas considérée automatiquement comme un défaut : elle doit être comprise et n'est supprimée que si KSP peut la résoudre sans downgrade, pin artificiel ou violation des responsabilités upstream.
Comptages déterministes attendus inchangés côté Transport, plus un smoke live ignoré et un canari workspace supplémentaire dans Core :
Transport unit tests = 309
Transport public API tests = 36
Transport release completeness = 24
Transport WebSocket live smoke = 1 ignored par défaut
Config unit tests = 109
Core workspace dependency tests = 3
10. Validation du gate pre.001
Exécuté dans le sandbox :
archive stable v0.2.6 vérifiée OK
lecture règles/architecture/plans/validation OK
inventaire Transport/Config réel OK
archive bot3 WebSocket auditée OK
audit docs officielles WebSocket OK
cross-check Git Agave v4.2.1 OK
audit SIMD WebSocket ciblé OK
inventaire exact 18 = 9+9 OK
audit dependencies candidates OK
state machines / reconnect / resubscribe DECIDED
backpressure / cancellation / shutdown / secrets DECIDED
shape Config V2 + discriminateur famille WS DECIDED
plan/sizing OK
python3 scripts/audit_rust_workspace_rules.py baseline OK
Tenté mais non exécutable dans le sandbox :
cargo fmt --all cargo absent
cargo check --workspace cargo absent
cargo clippy --workspace --all-targets cargo absent
La matrice ne considère donc pas pre.001 techniquement validé par Cargo tant que l'opérateur n'a pas exécuté ces gates sur son checkout.
11. Critères finaux à transformer en preuves
Avant rel.001, cette matrice doit obtenir :
18/18 méthodes official-index accounted (**Done `pre.012`**)
9/9 subscribe wrappers public typed (**Done `pre.012`**)
9/9 unsubscribe couverts par handles/registry (**Done `pre.012`**)
0 option officielle perdue
0 fuite URL/credential
N sessions same URL prouvé
N subscriptions same session prouvé
reconnect/resubscribe/backpressure/shutdown gates verts
unstable warnings centralisés
HTTP 52+14 non régressé (**Done `pre.012`**)
Config V1 backward + V2 WS validés (**Done `pre.003`**, compilation opérateur requise)
smoke live opt-in documenté (**Done `pre.013`**)
cargo tree inspecté (**preuve opérateur `pre.013` requise**)
cargo test --workspace vert
README/USAGE synchronisés (**Done `pre.013`**)
prompt 0.2.8 préparé